/* Application shell styling that is not part of the Sneat template.
   Plain CSS rather than SCSS on purpose: site.scss is wired into the gulp/webpack build and its
   compiled output (site.dist.css) is currently empty, so adding here avoids making a build step
   a prerequisite for the shell rendering correctly. Uses the template's CSS variables so it
   follows the light/dark theme and the customizer's colour choice. */

/* ---------------------------------------------------------------- top bar search */

/* The navbar's search box.

   The markup for it (_NavbarPartial.cshtml, "Search Small Screens") came with the template and
   main.js already toggles it, but this Sneat build ships no rule for `.search-input-wrapper`
   anywhere in core.dist.css - the element collapsed to zero width, which is why the box appeared
   dead and the search icon looked like it did nothing. The positioning below is what was
   missing: cover the navbar row while open.

   It has to be the whole row rather than the toggler's own slot, or the toggler's
   "Search (Ctrl+/)" label shows through to the left of the input. */
#layout-navbar {
  position: relative;
}

/* Detached navbar width below lg. Sneat sizes it at calc(100% - 1.625rem * 2) at every width and only
   narrows the side gap to 1rem under `.layout-navbar-fixed`, which this app's <html> doesn't carry.
   The content's side gutter (.container-p-x) drops to 1rem below 992px, so on phones and tablets the
   navbar sat 10px further in than the cards on each side. Match the content gutter. */
@media (max-width: 991.98px) {
  .layout-navbar.navbar-detached {
    inline-size: calc(100% - 1rem * 2) !important;
  }
}

#layout-navbar > .navbar-search-wrapper.search-input-wrapper {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0 1.25rem;
  background: var(--bs-body-bg);
  border-radius: inherit;
  z-index: 1080;
}

#layout-navbar > .navbar-search-wrapper.search-input-wrapper.d-none {
  display: none !important;
}

#layout-navbar > .navbar-search-wrapper.search-input-wrapper .search-input {
  flex: 1 1 auto;
  height: 3rem;
  background: transparent;
  box-shadow: none;
  font-size: 1rem;
}

#layout-navbar > .navbar-search-wrapper.search-input-wrapper .search-input:focus {
  box-shadow: none;
}

/* main.js unconditionally attaches its typeahead to `.search-input`, and with the demo index
   emptied that typeahead renders a "Pages / No Results Found" card over the real results. The
   class has to stay (main.js dereferences `.search-input` on resize), so its menu is hidden here
   instead. */
#layout-navbar .navbar-search-suggestion,
#layout-navbar .tt-menu {
  display: none !important;
}

.navbar-search-wrapper.search-input-wrapper .app-search-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  max-height: 26rem;
  overflow-y: auto;
  background: var(--bs-paper-bg, var(--bs-body-bg));
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.15);
}

.app-search-item {
  color: var(--bs-body-color);
  text-decoration: none;
}

.app-search-item:hover,
.app-search-item.active {
  background: var(--bs-secondary-bg, rgba(0, 0, 0, 0.04));
  color: var(--bs-body-color);
}

.app-search-item mark {
  background: transparent;
  color: var(--bs-primary);
  font-weight: 600;
}

.app-search-group {
  letter-spacing: 0.04em;
}

/* ---------------------------------------------------------------- unsaved changes prompt */

/* Sneat puts #layout-menu at z-index 1100 and .layout-overlay at 1099, both above Bootstrap's
   modal layer (1090). With the off-canvas menu open - which is exactly when someone clicks a menu
   item and triggers this prompt - the dialog rendered *behind* the overlay: navigation was
   correctly blocked but nothing appeared to say why. This prompt has to outrank the menu. */
#dl-unsaved-modal {
  z-index: 1120;
}

body.dl-unsaved-prompting .modal-backdrop {
  z-index: 1119;
}

/* ---------------------------------------------------------------- inline week calendar */

/* The scheduling card's mini calendar. Its `height` option did not constrain this FullCalendar
   build, so the 06:00-20:00 grid rendered at full natural height and ran straight over the Notes
   card beneath it. Constrained here instead, with the grid scrolling inside its own box. */
/* The whole calendar scrolls inside a fixed box.
   This build ignores FullCalendar's `height` option and sizes its view harness from content
   (650px for a 06:00-20:00 grid), which ran straight over the Notes card below. Constraining the
   harness itself did not hold either. Scrolling the host is what actually contains it - and
   unlike `overflow: hidden`, it does not cut the hour labels off the left gutter, since anything
   clipped can still be scrolled to. */
#mini-calendar {
  height: 26rem;
  overflow-y: auto;

  /* Vertical only. A horizontal scrollbar here let the grid be scrolled sideways, which pushed
     the hour-label gutter out of view; the week already fits the card's width. */
  overflow-x: hidden !important;

  /* Room for the vertical scrollbar. Without it the grid is laid out at full width, the scrollbar
     overlaps its right edge, and the whole thing shifts enough to clip the "6am" gutter on the
     left. */
  padding-right: 0.75rem;

  /* Not sub-pixel rounding - measured via devtools: the axis column
     (.fc-timegrid-axis/.fc-scrollgrid-shrink) uses a negative margin to align the sticky "6am"
     gutter with the scrollable grid body, which lands its left edge ~15px to the LEFT of this
     element's own content box. `overflow-x: hidden` clips at the padding-box edge, so any pad
     smaller than that escape distance still gets partially cut - 0.5rem (8px) was not enough,
     hence 1.25rem (20px), comfortably past the 15px measured. */
  padding-left: 1.25rem;
}

/* Free time reads as a filled block, not as a faint tint - it is the thing being chosen. */
#mini-calendar .fc-bg-event {
  opacity: 0.35;
  cursor: pointer;
}

#mini-calendar .fc-bg-event .fc-event-title {
  font-size: 0.7rem;
  font-weight: 600;
  padding: 0.1rem 0.2rem;
}

/* ---------------------------------------------------------------- documentation */

.doc-article {
  line-height: 1.7;
}

.doc-article h1,
.doc-article h2,
.doc-article h3 {
  margin-top: 1.75rem;
  margin-bottom: 0.75rem;
  font-weight: 600;
}

.doc-article h1 { font-size: 1.35rem; }
.doc-article h2 { font-size: 1.15rem; }
.doc-article h3 { font-size: 1rem; }

.doc-article > *:first-child {
  margin-top: 0;
}

.doc-article ul,
.doc-article ol {
  padding-left: 1.35rem;
  margin-bottom: 1rem;
}

.doc-article li {
  margin-bottom: 0.35rem;
}

.doc-article code {
  background: var(--bs-secondary-bg, rgba(0, 0, 0, 0.05));
  padding: 0.1rem 0.3rem;
  border-radius: 0.25rem;
}

.doc-article pre {
  background: var(--bs-secondary-bg, rgba(0, 0, 0, 0.05));
  padding: 0.85rem 1rem;
  border-radius: var(--bs-border-radius);
  overflow-x: auto;
}

.doc-article pre code {
  background: transparent;
  padding: 0;
}

.doc-article blockquote {
  border-left: 3px solid var(--bs-primary);
  padding-left: 1rem;
  color: var(--bs-secondary-color, #6c757d);
}

/* The section list above an article body. Quieter than a card - it is a way in, not content. */
.doc-toc {
  border-left: 2px solid var(--bs-primary);
  padding: 0.25rem 0 0.25rem 1rem;
  font-size: 0.9375rem;
}

.doc-toc ul {
  list-style: none;
  padding-left: 0;
  margin-bottom: 0;
  display: grid;
  gap: 0.3rem;
}

.doc-toc a {
  text-decoration: none;
}

.doc-toc a:hover {
  text-decoration: underline;
}

/* The permission a task needs, beside the task rather than in a block at the top - which is where
   it is actually useful, since permissions vary between tasks in one article. */
.doc-perm {
  font-size: 0.6875rem;
  font-weight: 400;
  vertical-align: middle;
}

/* A heading is a link target, so it must not sit under the sticky navbar when jumped to. */
.doc-article h2,
.doc-article h3 {
  scroll-margin-top: 5rem;
}

/* The "what you need" block above an article body. Tinted rather than bordered so it reads as
   context for the article, not as a warning about it - most readers do have the permission. */
.doc-requirements {
  background: var(--bs-secondary-bg, rgba(0, 0, 0, 0.04));
  border-radius: var(--bs-border-radius);
  padding: 0.75rem 1rem;
  font-size: 0.9375rem;
}

.doc-requirements ul {
  padding-left: 1.15rem;
  margin-bottom: 0;
}

.doc-requirements li {
  margin-bottom: 0.2rem;
}

/* Markdig emits a bare <table> with no classes, so Bootstrap's .table styling never reaches it.
   These rules give an article table the same look without the author having to write HTML. The
   wrapper handles narrow screens: a table with more than two or three columns will not fit a
   phone, and the article column must never be what scrolls sideways. */
.doc-article table {
  width: 100%;
  margin-bottom: 1rem;
  border-collapse: collapse;
  font-size: 0.9375rem;
}

.doc-article thead th {
  text-align: left;
  font-weight: 600;
  white-space: nowrap;
  border-bottom: 2px solid var(--bs-border-color, #d9dee3);
  padding: 0.5rem 0.75rem;
}

.doc-article tbody td {
  border-bottom: 1px solid var(--bs-border-color, #d9dee3);
  padding: 0.5rem 0.75rem;
  vertical-align: top;
}

.doc-article tbody tr:last-child td {
  border-bottom: 0;
}

/* A leading column holding only a short label - a status name, an enum value - reads better
   without wrapping mid-word. */
.doc-article tbody td:first-child {
  white-space: nowrap;
}

/* Markdig gives the table no wrapper to scroll inside, so on a phone the table becomes its own
   scroll container. Anything wider than the column scrolls sideways within itself rather than
   dragging the whole article with it. The first-column nowrap is released here - on a narrow
   screen a wrapped label costs less than a second axis of scrolling. */
@media (max-width: 767.98px) {
  .doc-article table {
    display: block;
    overflow-x: auto;
  }

  .doc-article tbody td:first-child {
    white-space: normal;
  }
}

/* The type-ahead panel floats over the category cards below it. Two things it needs and did not
   have: an opaque background of its own - .list-group is only a layout class, and the Sneat theme
   leaves --bs-list-group-bg transparent, so the cards were reading straight through the
   suggestions - and a stacking context high enough that the inline z-index on the panel is
   actually compared against those cards rather than against its own clipped parent. */
#doc-search-suggestions {
  top: 100%;
  max-height: 20rem;
  overflow-y: auto;
  background: var(--bs-body-bg, #fff);
  border: 1px solid var(--bs-border-color, #d9dee3);
  border-radius: var(--bs-border-radius);
}

#doc-search-suggestions .list-group-item {
  background: var(--bs-body-bg, #fff);
  border-left: 0;
  border-right: 0;
}

#doc-search-suggestions .list-group-item:first-child { border-top: 0; }
#doc-search-suggestions .list-group-item:last-child  { border-bottom: 0; }

#doc-search-suggestions .list-group-item:hover,
#doc-search-suggestions .list-group-item:focus {
  background: var(--bs-secondary-bg, rgba(0, 0, 0, .04));
}

/* Raising the search card itself, not just the panel: the panel's own z-index only ranks it
   against siblings inside this card. */
.doc-search-card {
  position: relative;
  z-index: 1060;
}

/* ---------------------------------------------------------------- loading indicator (crewduo) */

/* The brand "bob" loader (crewduo-loader-bob.svg in the logo export): the mark's route held faint
   while its two heads bob one after the other. Used in blockUI's overlay
   (wwwroot/js/doablelink/blockui.js). Inline SVG rather than an <img>, because an SVG loaded as an
   image cannot see this page's keyframes. Colour comes from `color`, so one markup covers green on
   light and white on dark. */
.cd-load {
  display: block;
  overflow: visible;
  color: var(--bs-primary, #1D9E75);
}

.cd-load-a {
  animation: cd-bob-a 1.1s ease-in-out infinite;
}

.cd-load-b {
  animation: cd-bob-b 1.1s ease-in-out infinite;
}

@keyframes cd-bob-a {
  0%, 60%, 100% { transform: translateY(0); }
  30% { transform: translateY(-5px); }
}

@keyframes cd-bob-b {
  0%, 85%, 100% { transform: translateY(0); }
  55% { transform: translateY(-5px); }
}

@media (prefers-reduced-motion: reduce) {
  .cd-load * {
    animation: none !important;
  }
}

/* Full-page overlay for actions that wait on Stripe (wwwroot/js/doablelink/busy-overlay.js): the bob loader and what is
   happening, over a dimmed page, until the redirect or reload. */
.cd-busy-overlay {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(var(--bs-body-bg-rgb, 255, 255, 255), 0.72);
  backdrop-filter: blur(2px);
}

.cd-busy-overlay.show {
  display: flex;
}

.cd-busy-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.875rem;
  max-width: 20rem;
  padding: 1.5rem 2rem;
  border-radius: var(--bs-border-radius-lg, 0.5rem);
  background: var(--bs-paper-bg, var(--bs-body-bg, #fff));
  box-shadow: var(--bs-box-shadow-lg, 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15));
  text-align: center;
  color: var(--bs-heading-color, var(--bs-body-color));
  font-weight: 500;
}

/* Logo beside the hamburger below xl (_NavbarPartial). */
.navbar-brand-logo {
  display: inline-flex;
  align-items: center;
  /* Sizes to its content, unlike .app-brand-link which is sized for the sidebar rail. */
  flex: 0 0 auto;
  width: auto;
  margin-right: 0.75rem;
  /* Never let the wordmark squeeze the action icons off the right of a small screen. */
  max-width: 38vw;
  overflow: hidden;
}

/* Brand sizing (_Partials/_Logo.cshtml). The wordmark carries its 128 x 24 inline, sized against the
   nav type rather than the space available. The "mark" variant - the top bar below xl, mark-only at
   every width - is 28px; the collapsed desktop rail uses _Macros directly at its own 30 x 29. */
.cd-logo {
  display: inline-flex;
  align-items: center;
}

.cd-logo .cd-logo-mark svg {
  width: 29px !important;
  height: 28px !important;
}

/* The collapsed rail's mark (_Macros, 30 x 29): centred on the same axis as the menu icons below it. The brand row keeps its
   expanded side padding, which pushed the mark right of the icons while it was also drawn at 41 x 40, twice their size
   (2026-10-13). */
@media (min-width: 1200px) {
  .layout-menu-collapsed:not(.layout-menu-hover) .layout-menu .app-brand {
    padding-inline: 0;
    justify-content: center;
  }
  .layout-menu-collapsed:not(.layout-menu-hover) .layout-menu .app-brand .app-brand-link {
    margin-inline: auto;
    justify-content: center;
  }
}

/* Page title strip ("Customers / Inbox"). Every page writes its own (an h4, often inside a flex row with its buttons),
   always with the muted section part as .fw-light - so one rule here tightens them all: a smaller title and about half
   the space above and below it (2026-10-13). */
.content-wrapper .container-p-y {
  padding-top: 1rem !important;
}
.content-wrapper h4:has(.fw-light) {
  font-size: 1.125rem;
  line-height: 1.4;
}
.content-wrapper h4.py-3:has(.fw-light),
.content-wrapper .py-3:has(> h4 .fw-light) {
  padding-top: 0.25rem !important;
  padding-bottom: 0.25rem !important;
}
.content-wrapper h4.mb-4:has(.fw-light),
.content-wrapper .mb-4:has(> h4 .fw-light) {
  margin-bottom: 0.875rem !important;
}

/* Plan cards (Subscription/Plans): on a narrow card a button's words broke mid-word ("Continu e on Free") because Sneat's
   .btn is an inline flex row and each <span> in it shrank on its own; the words now stay whole and the label wraps between
   them. The users box and its label wrap instead of running out of the card (2026-10-13). */
.plan-card .btn {
  flex-wrap: wrap;
  justify-content: center;
  column-gap: 0.25rem;
  white-space: normal;
}
.plan-card .btn > span {
  white-space: nowrap;
}
.plan-card .plan-price .d-flex {
  flex-wrap: wrap;
}

/* DataTables loading overlay (wwwroot/js/doablelink/datatable-loader.js). */
.cd-dt-busy {
  position: relative;
}

.cd-dt-loading {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 7rem;
  background: rgba(var(--bs-body-bg-rgb, 255, 255, 255), 0.72);
  font-size: 0.8125rem;
  color: var(--bs-body-color);
}

/* Bootstrap's spinner-border, used across the app for "working..." states (buttons, panels, the
   setup assistant, recommendations), drawn as the brand bob loader instead of a spinning ring.
   A mask rather than a background image so it keeps currentColor: green on a page, white inside a
   primary button. Keeps Bootstrap's own sizing (spinner-border / spinner-border-sm). */
.spinner-border {
  border: 0 !important;
  border-radius: 0 !important;
  animation: none !important;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-32 -62 64 66'%3E%3Cstyle%3E@keyframes a%7B0%25,60%25,100%25%7Btransform:translateY(0)%7D30%25%7Btransform:translateY(-5px)%7D%7D@keyframes b%7B0%25,85%25,100%25%7Btransform:translateY(0)%7D55%25%7Btransform:translateY(-5px)%7D%7D.a%7Banimation:a 1.1s ease-in-out infinite%7D.b%7Banimation:b 1.1s ease-in-out infinite%7D%3C/style%3E%3Cpath d='M-27.19 -38.28L-13.6 0L0 -38.28L13.6 0L27.19 -38.28' fill='none' stroke='%23000' stroke-opacity='.28' stroke-width='8' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle class='a' cx='-13.6' cy='-47.44' r='9.2'/%3E%3Ccircle class='b' cx='13.6' cy='-47.44' r='9.2'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-32 -62 64 66'%3E%3Cstyle%3E@keyframes a%7B0%25,60%25,100%25%7Btransform:translateY(0)%7D30%25%7Btransform:translateY(-5px)%7D%7D@keyframes b%7B0%25,85%25,100%25%7Btransform:translateY(0)%7D55%25%7Btransform:translateY(-5px)%7D%7D.a%7Banimation:a 1.1s ease-in-out infinite%7D.b%7Banimation:b 1.1s ease-in-out infinite%7D%3C/style%3E%3Cpath d='M-27.19 -38.28L-13.6 0L0 -38.28L13.6 0L27.19 -38.28' fill='none' stroke='%23000' stroke-opacity='.28' stroke-width='8' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle class='a' cx='-13.6' cy='-47.44' r='9.2'/%3E%3Ccircle class='b' cx='13.6' cy='-47.44' r='9.2'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* The audit line ("Created by ... Last updated by ...") normally sits on its own row and carries
   a bottom margin for the content beneath it. Inside the sticky save bar it is a flex item beside
   the buttons, where that margin only pushes it off the shared baseline. */
.sticky-save-bar .audit-info {
  margin-bottom: 0;
}

/* min-width:0 lets a long audit line shrink and ellipsize instead of shoving the Save button off
   the right edge on a narrow viewport - a flex item's default min-width is its content. */
.min-width-0 {
  min-width: 0;
}

.sticky-save-bar .audit-info {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Collapsing panel cards (job Work summary, Forms & Checklists). The whole header is the toggle,
   so it has to stop looking like a link and start looking like a card header. */
.job-panel-card .card-header > .btn-link {
  border: 0;
  border-radius: inherit;
  text-align: inherit;
}

.job-panel-card .job-panel-caret {
  transition: transform .15s ease-in-out;
}

/* Bootstrap puts .collapsed on the toggle while the target is closed, so the caret points down
   when shut and up when open - the arrow says what pressing it will do. */
.job-panel-card .card-header > .btn-link:not(.collapsed) .job-panel-caret {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .job-panel-card .job-panel-caret {
    transition: none;
  }
}

/* Card order on small screens.
   Below lg the two layout columns stack, which put the whole tab card - Service, Visits,
   Materials, Resources, Files - between Job Details and Client Information. On a phone that is a
   long scroll past the working surface to reach who the client is and where they are, which is
   what you actually want first when the job is in front of you.
   `display: contents` drops the two column wrappers out of the box tree below lg so their cards
   become flex items of the row itself and can be ordered against each other. Above lg neither
   rule applies and the two-column layout is exactly as it was. */
@media (max-width: 991.98px) {
  .job-layout > .job-layout-col {
    display: contents;
  }

  /* `display: contents` also throws away the columns' gutter padding, and that padding was the only
     thing offsetting the row's own negative margins. The cards ended up hanging half a gutter past
     the page on both sides - no space at all to the left of a card on a phone. There is nothing
     left to offset, so the negative margins go and the cards line up with every other page's. */
  .job-layout {
    margin-left: 0;
    margin-right: 0;
  }

  /* The wrappers no longer size their children, and `.row > *` does not match a grandchild, so
     each card has to claim the full line itself. */
  .job-layout > .job-layout-col > .card {
    flex: 0 0 100%;
    width: 100%;
    max-width: 100%;
  }

  /* Everything without an explicit order defaults to 0 and would sort FIRST, so the rest of the
     cards are pinned behind the three below rather than left to chance. The id and class
     selectors that follow both out-rank this one, so source order here does not matter.

     What this puts first is what you need with the job in front of you: what the work is, who it
     is for, when it happens, and what it is worth. Billing comes before the Invoices card
     deliberately - it is the summary, and the invoice list is the detail behind it. The tab card
     and the remaining panel cards follow in source order. */
  .job-layout > .job-layout-col > .card { order: 5; }
  .job-layout > .job-layout-col > .job-card-billing { order: 4; }
  .job-layout > .job-layout-col > .job-card-schedule { order: 3; }
  .job-layout > .job-layout-col > .job-card-client { order: 2; }
  .job-layout > .job-layout-col > #job-details-scope { order: 1; }
}

/* Job page tab strip: every tab stays visible. It used to be one row that scrolled sideways with the
   scrollbar hidden, and with seven tabs the last ones (Photos, Findings) sat past the edge where
   nobody could see them. Slightly tighter pills keep all seven on one line at ordinary widths; on a
   narrower screen they wrap to a second row, as the visit, quote and request pages already do. */
.job-tab-strip {
  row-gap: 0.25rem;
}

.job-tab-strip > .nav-item {
  flex: 0 0 auto;
}

.job-tab-strip .nav-link {
  white-space: nowrap;
  padding-left: 0.75rem;
  padding-right: 0.75rem;
}

/* Card headers with an action button (Recommend work, Change client, Preview as client...). In a narrow
   sidebar card - a 1366px laptop gives it about 330px - the button used to squeeze onto two lines
   (48px tall instead of 30) or slide over the title. Now the title and the button share the row while
   they fit and the button drops under the title when they do not, always one line at the usual size.
   Collapsible headers (a full-width btn-link) are left alone. */
.card-header.d-flex {
  flex-wrap: wrap;
  gap: 0.5rem;
}

.card-header.d-flex .btn:not(.btn-link) {
  white-space: nowrap;
  flex-shrink: 0;
}

/* ---------------------------------------------------------------------------------------------
   Line-item rows (line-items.js buildRow) - quote details, job services, visit lines, request
   services. One rule set, because one function builds all four.

   THE BUG THIS FIXES. The row is a Bootstrap grid inside a DataTables cell, so a col-md-2 picker
   was resolving to about 74px of inner width while "ShopSupply" needs 104px. Every band combo
   read "C... v" - the control was narrower than the shortest thing it could possibly say, which
   makes it not a combo but a puzzle. Bootstrap columns have min-width:0 by design, so they will
   crush their own contents indefinitely rather than let the row wrap.

   WHY THE GRID IS ABANDONED HERE rather than just widened. Bootstrap col-md-* is a percentage of
   the CONTAINER, chosen by the width of the VIEWPORT. The lines card sits beside a sidebar and is
   about 520px wide, so on a perfectly ordinary 1500px screen every col-md-2 inside it resolved to
   roughly 87px - wide enough for "AC rep" and not for "AC repair". Widening the columns cannot
   fix that, because the mismatch is between what decides the breakpoint and what decides the
   width; any number chosen would be right at one card width and wrong at every other.

   Flex with a per-field basis is measured in what the field actually holds. Each control asks for
   the width its content needs, shares out what is left, and drops to the next line when there is
   not enough - so the row reflows against ITS OWN box and nothing is truncated to fit a share of
   a percentage. The col-* classes stay on the markup and are simply overridden: they still
   describe the intent, and any page not opted in keeps the grid.
   --------------------------------------------------------------------------------------------- */
.dl-line-row {
  display: flex;
  flex-wrap: wrap;

  /* No horizontal gap: the controls on a line sit flush and read as ONE strip describing one
     charge, instead of six separate boxes that happen to be near each other. The vertical gap
     stays, because that separation is real - it is the gap between line 1 and line 2. */
  gap: 0.5rem 0;
  margin-left: 0;
  margin-right: 0;
}

.dl-line-row > [class*="col-"] {
  flex: 1 1 8rem;
  width: auto;
  max-width: none;
  padding-left: 0;
  padding-right: 0;
}

/* EVERY CONTROL HAS A FLOOR AND A CEILING, AND THE LAST ONE CLOSES THE LINE.

   The floor is what the field has to be able to show - a band combo that cannot fit "ShopSupply"
   is not a combo, it is a puzzle. The ceiling is what it is worth showing: a quantity box stretched
   to 260px has stopped looking like a number. Between the two the control flexes; below the floor
   it stops shrinking and the row wraps, which is why "Time (min)" never turns into "Time...".

   Slack is the awkward part. Spread across every control it opens a ragged gap between each pair
   of fields; left alone it makes the row look like it stopped early. It goes to the LAST control
   on each line, which closes the line at the right edge - so the three lines end level with one
   another and the row reads as one block instead of three strips. Which control is last is decided
   in buildRow (dl-line-col-last), because :last-child would be the last in the whole row rather
   than the last on a visual line. */
.dl-line-row > [class*="col-"] {
  flex: 1 1 auto;
  min-width: 8rem;
  max-width: 20rem;
}

/* The catalogue picker holds the longest text on the line - real service and product names. */
.dl-line-row > .dl-line-col-select {
  min-width: 12rem;
  max-width: 26rem;
}

/* The band: a short, closed list of words, so it needs little and deserves no more.

   AFTER the picker rule, and that is not cosmetic. The band carries both classes - it IS a picker
   and it is also the band - and the two selectors have equal specificity, so whichever is written
   second wins. With them the other way round the band silently inherited the picker's 26rem and
   took half the line. */
.dl-line-row > .dl-line-col-band {
  min-width: 9rem;
  max-width: 12rem;
}

/* What the line is called. Free text, and the field read down the column. */
.dl-line-row > .dl-line-col-name {
  min-width: 10rem;
  max-width: 22rem;
}

/* Figures: sized to their digits. */
.dl-line-row > .dl-line-col-num {
  min-width: 5.5rem;
  max-width: 8rem;
}

/* Unit price holds the longest value on its line and reads as currency. */
.dl-line-row > .dl-line-col-num.dl-line-col-price {
  min-width: 7.5rem;
  max-width: 11rem;
}

/* Free text takes its own line whole - it has no natural width, so it is given the lot. */
.dl-line-row > .dl-line-col-notes,
.dl-line-row > .dl-line-custom-wrap {
  flex: 1 1 100%;
  min-width: 100%;
  max-width: 100%;
}

/* The one control allowed past its ceiling: the last on its line, which absorbs the slack so the
   line finishes flush at the right edge.

   !important because this has to beat the per-kind ceilings above, and some of those are written
   with two classes (.dl-line-col-num.dl-line-col-price) and so outrank a single-class selector no
   matter where it sits in the file. The alternative was to repeat this rule once per kind, which
   is the same statement four times and three chances to forget one. It is deliberately the only
   !important in this block: the rule exists precisely to have the final say. */
.dl-line-row > .dl-line-col-last {
  flex-grow: 1;
  max-width: none !important;
}

/* The other way to close a line: push the trailing run of figures to the right edge and let the
   slack open up in front of it. Used when the line ENDS in figures, because stretching a number
   box is the one thing that cannot be allowed to close a line. See markLast in line-items.js. */
.dl-line-row > .dl-line-col-group-end {
  margin-left: auto;
}

/* The forced wrap. A flex container has no "start a new line here", so the break is an actual
   element: full width, no height, no content. It is what makes the three lines a decision rather
   than a consequence of how wide the card happens to be - see buildRow in line-items.js.

   Nothing above it can share its line because it claims 100%; nothing below it can move up. */
.dl-line-row > .dl-line-break {
  flex: 0 0 100%;
  width: 100%;
  height: 0;
  padding: 0;
  margin: 0;
}

/* select2 sizes its own control to the container, and the container to the column - so the floor
   above has to be allowed to reach it. Without this the widget keeps whatever width it measured
   on first paint, which on a DataTables redraw is frequently zero. */
.dl-line-row .select2-container {
  width: 100% !important;
  min-width: 100%;
}

/* And the panel it drops down. select2 appends that to <body>, so it is NOT a descendant of the
   row and cannot be reached by nesting - select2-refresh.js tags it with dl-line-dropdown for
   exactly this rule. It inherits the control's width, so a long catalogue name would be clipped
   inside the list as well as on the closed control; sized to its content instead, capped so one
   very long entry cannot run off the side of the screen. */
.select2-container.dl-line-dropdown .select2-dropdown {
  width: auto !important;
  min-width: 100%;
  max-width: 28rem;
}

/* Line-item rows, part 2: reading order.

   A line row is not a form, it is a RECORD OF ONE CHARGE, and it was being read as a flat run of
   eight identical boxes - same label weight, same widths chosen per page, nothing saying which
   fields belong together. These rules give it the three groups it actually has: WHAT this is
   (band, catalogue item, name), HOW MUCH (count, time, price), and ANYTHING ELSE (notes). */

/* Labels are signposts, not content. Small, quiet and uppercase, so the eye lands on the values -
   a 1rem label above a 1rem value makes every row twice as loud as it needs to be. */
.dl-line-row .form-label {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.75;
  margin-bottom: 0.25rem;

  /* A wrapped label is worse than a wide column: "UNIT PRICE" breaking to two lines made that one
     field taller than its neighbours, so the controls on the line stopped sharing a baseline and
     the row grew by 13px for no content. Kept on one line, the label becomes the field's
     min-content width and the flex item widens to fit it instead - the column sizes itself to
     what it has to say. */
  white-space: nowrap;
}

/* Figures line up on the right and share one width per column across every module, so a reader
   scanning down a table compares like with like instead of re-finding the number on each row.
   tabular-nums keeps the digits on a fixed pitch - proportional digits make 1.00 and 8.88 visibly
   different widths and the column stops being a column. */
.dl-line-row .dl-line-col-num .form-control {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Notes runs the full width and sits last, below the figures - it is the one field with no fixed
   shape, and putting it inline forces everything beside it to the size of its worst case. */
.dl-line-row .dl-line-col-notes {
  margin-top: 0.25rem;
}

/* The multi-line note itself. Starts at input height and grows with its content (see dl-autogrow
   in line-items.js); the manual resize handle is left on the vertical axis only, because dragging
   one horizontally inside a grid column just breaks the row. */
.dl-line-row textarea.dl-autogrow {
  min-height: calc(1.5em + 0.876rem + 2px);
  resize: vertical;
  overflow-y: hidden;
  line-height: 1.5;
}

/* One line between the row's controls and the next line's, so consecutive lines read as separate
   records rather than one long column of boxes. */
.datatables-quote-details > tbody > tr + tr > td,
.datatables-job-services > tbody > tr + tr > td,
.datatables-visit-details > tbody > tr + tr > td,
.datatables-request-services > tbody > tr + tr > td {
  border-top: 1px solid rgba(128, 128, 128, 0.18);
}

/* ---------------------------------------------------------------------------------------------
   Flush controls: one strip per line, not a row of separate boxes.

   With the horizontal gap gone, two neighbouring controls put their 1px borders side by side and
   the seam reads as 2px - heavier than either control's own edge, which is exactly the thing that
   makes butted-up inputs look like a mistake rather than a decision. Each control after the first
   on its line is pulled 1px left so the two borders land on top of each other, and the corners
   where they meet are squared: the line ends up with one continuous outline and rounded ends,
   the way a joined control group should look.

   Written against the generic column selector, so it applies to every module at once - the rows
   are all built by the same function.
   --------------------------------------------------------------------------------------------- */

/* A column that FOLLOWS another on the same line. The line breaks are not columns, so the first
   control of each line is never matched and keeps its own left edge. The figure group is excluded
   because its auto margin is what closes the line - a -1px margin would silently cancel it. */
.dl-line-row > [class*="col-"] + [class*="col-"]:not(.dl-line-col-group-end) {
  margin-left: -1px;
}

.dl-line-row > [class*="col-"] + [class*="col-"] .form-control,
.dl-line-row > [class*="col-"] + [class*="col-"] .select2-selection {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

/* And the control BEFORE one on the same line loses its right corners, so the join is square on
   both sides rather than a rounded edge overlapping a straight one. */
.dl-line-row > [class*="col-"]:has(+ [class*="col-"]) .form-control,
.dl-line-row > [class*="col-"]:has(+ [class*="col-"]) .select2-selection {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

/* A focused control has to sit ON TOP of the one overlapping it, or its focus ring is drawn
   underneath its neighbour and half of it disappears - which on a keyboard pass makes the focus
   look like it skipped a field. */
.dl-line-row .form-control:focus,
.dl-line-row .select2-container--focus .select2-selection,
.dl-line-row .select2-container--open .select2-selection {
  position: relative;
  z-index: 2;
}



/* ==========================================================================================
   Boxicons compatibility on Sneat v3's SVG icons.
   v3 draws `bx bx-*` as masked SVGs sized 1em (iconify-icons.dist.css), so every existing icon
   keeps rendering - but the icon FONT's modifier classes (sizes, spin) went with the font, and
   the font's middle alignment with them. These restore both, sized the way the font sized them.
   ========================================================================================== */
.bx {
  vertical-align: middle;
  flex-shrink: 0;
}

.bx.bx-xs { font-size: 1rem !important; }
.bx.bx-sm { font-size: 1.25rem !important; }
.bx.bx-md { font-size: 1.5rem !important; }
.bx.bx-lg { font-size: 2rem !important; }

.bx.bx-spin { animation: dl-bx-spin 2s linear infinite; }

@keyframes dl-bx-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(359deg); }
}

@media (prefers-reduced-motion: reduce) {
  .bx.bx-spin { animation: none; }
}

/* ------------------------------------------------------------------
   Sneat 3 compatibility: classes and behaviour 1.5.1 had
   ------------------------------------------------------------------ */

/* Bootstrap 5.3 deprecated .text-muted and Sneat 3 no longer ships it; about 190 files still use
   it for secondary text. Same value Bootstrap 5.3 gave it, so it follows light and dark. */
.text-muted {
  --bs-text-opacity: 1;
  color: var(--bs-secondary-color) !important;
}
a.text-muted:hover,
a.text-muted:focus {
  color: var(--bs-heading-color) !important;
}

/* Sort arrows: Sneat 3 hides them until the header is hovered, so nothing says a column sorts
   or which way it is sorted. Always show both faintly and the active direction strongly, as 1.5.1 did. */
table.dataTable thead .dt-orderable-asc .dt-column-order::before,
table.dataTable thead .dt-orderable-asc .dt-column-order::after,
table.dataTable thead .dt-orderable-desc .dt-column-order::before,
table.dataTable thead .dt-orderable-desc .dt-column-order::after,
table.dataTable thead .dt-ordering-asc .dt-column-order::before,
table.dataTable thead .dt-ordering-asc .dt-column-order::after,
table.dataTable thead .dt-ordering-desc .dt-column-order::before,
table.dataTable thead .dt-ordering-desc .dt-column-order::after {
  visibility: visible !important;
  opacity: 0.3 !important;
}
table.dataTable thead .dt-ordering-asc .dt-column-order::before,
table.dataTable thead .dt-ordering-desc .dt-column-order::after {
  opacity: 1 !important;
}
table.dataTable thead .dt-ordering-asc .dt-column-order::after,
table.dataTable thead .dt-ordering-desc .dt-column-order::before {
  opacity: 0.15 !important;
}

/* Tabbed cards (Job, Quote, Request, Visit, Invoice): Sneat 3 removes a card body's top padding
   when it follows the card header, so every tab's content started flush against the tab strip.
   A tab body is its own section and gets the full card spacing back. */
.card > .card-header + .card-body.tab-content {
  padding-block-start: 1.5rem;
}

/* Side menu: below xl the menu scrolls natively, and the vertical scrollbar takes its width out of
   the 260px rail - so the full-width menu items overflowed by the scrollbar's width and a
   horizontal scrollbar appeared under them. The menu never needs to scroll sideways. !important
   because the menu markup carries Bootstrap's .overflow-auto, which is itself !important. */
.layout-menu .menu-inner,
.layout-menu .menu-inner.overflow-auto {
  overflow-x: hidden !important;
}

/* Calendar: the default visit colour. Sneat 1.5.1 only styled .fc-event-info under .light-style,
   which Sneat 3 no longer puts on <html>, so uncoloured visits lost their colour entirely. Same
   tinted look, from the theme's info colour so it works on light and dark. */
.fc .fc-event.fc-event-info:not(.fc-list-event) {
  background-color: color-mix(in sRGB, var(--bs-info) 16%, var(--bs-paper-bg)) !important;
  border-color: color-mix(in sRGB, var(--bs-info) 30%, var(--bs-paper-bg)) !important;
  color: var(--bs-info) !important;
}
.fc .fc-event.fc-event-info:not(.fc-list-event) .fc-event-main,
.fc .fc-event.fc-event-info:not(.fc-list-event) .fc-event-time,
.fc .fc-event.fc-event-info:not(.fc-list-event) .fc-event-title {
  color: inherit !important;
}
.fc .fc-event.fc-event-info.fc-list-event .fc-list-event-dot {
  border-color: var(--bs-info) !important;
}

/* Fixed footer: the copilot button and panel sit 22px / 84px above the window's bottom edge (their own
   styles in _CopilotWidget), which put the button on top of the footer's links. Lift both clear of it.
   The footer wraps to two rows on narrow screens, so it is taller there. */
.layout-footer-fixed .cp-fab {
  bottom: 76px;
}
.layout-footer-fixed .cp-panel {
  bottom: 138px;
}
/* Footer links: Sneat 3 gives .footer-link no spacing, so the links ran into each other ("Terms Privacy Acceptable
   use..."). A gap between them; on narrow screens they wrap, centred and a little smaller. */
.app-footer-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 1rem;
  row-gap: .25rem;
  /* In the phone footer's centred column a flex item sizes to its content and never wraps: held to the column's width it does. */
  max-width: 100%;
}
@media (max-width: 767.98px) {
  .app-footer-links {
    justify-content: center;
    column-gap: .85rem;
    font-size: .75rem;
  }
  .footer-container > .small:first-child {
    font-size: .75rem;
    text-align: center;
  }
}

/* A phone: no fixed footer. Eight links do not fit a 54px bar - the row was cut off and the last links never showed - so the
   footer is an ordinary block at the end of the page, wrapping as tall as it needs to. The copilot button goes back to its own
   default place, since nothing is fixed under it any more. */
@media (max-width: 767.98px) {
  .layout-footer-fixed .content-footer,
  .layout-footer-fixed .content-footer .footer-container {
    position: static;
    height: auto !important;
    min-height: 0 !important;
  }
  .layout-footer-fixed .content-wrapper {
    padding-bottom: 0;
  }
  /* The template gives the container a fixed 54px height; a wrapping column of that height put the links in a second column
     off the right edge (the page scrolled sideways). As tall as it needs, one column. */
  .footer-container {
    height: auto !important;
    flex-wrap: nowrap !important;
    align-items: center;
    text-align: center;
    padding-top: 1rem !important;
    padding-bottom: 1.25rem !important;
    gap: .75rem !important;
  }
  .app-footer-links {
    justify-content: center;
    column-gap: 1rem;
    row-gap: .5rem;
    font-size: .8rem;
  }
  .layout-footer-fixed .cp-fab {
    bottom: 22px;
  }
  .layout-footer-fixed .cp-panel {
    bottom: 84px;
  }
}

/* ---------------------------------------------------------------- DataTables lists */

/* Every list table: tighter cells, so a row reads as one line and the columns sit closer together. Cells had 24px of side
   padding, which spread a five-column list across the whole card. Sortable headers keep DataTables' own right padding
   for the sort arrows; the filter row keeps its own (datatable-column-filters.js). */
table.dataTable > thead > tr > th,
table.dataTable > tbody > tr > td {
  padding-top: .625rem;
  padding-bottom: .625rem;
  padding-left: .625rem;
  vertical-align: middle;
}

table.dataTable > tbody > tr > td,
table.dataTable > thead > tr > th.dt-orderable-none {
  padding-right: .625rem;
}

table.dataTable > thead > tr:first-child > th {
  white-space: nowrap;
}

/* List rows in the smaller type the phone cards use, at every width (the user's choice, 2026-09-24). */
table.dataTable > tbody > tr > td {
  font-size: .8125rem;
}

/* DataTables' own stylesheet gives the first and last cell of every row 1.5rem of inline padding with a :first-child /
   :last-child selector that outranks the rules above; match it so the outer columns are not the widest-padded ones. */
table.dataTable > thead > tr > th:first-child,
table.dataTable > tbody > tr > td:first-child {
  padding-inline-start: .625rem;
  padding-inline-end: .625rem;
}

table.dataTable > tbody > tr > td:last-child,
table.dataTable > thead > tr > th.dt-orderable-none:last-child {
  padding-inline-end: .625rem;
}

/* datatable-layout.js: short values stay on one line; the actions column shrinks to its buttons. */
table.dataTable .dt-cell-nowrap {
  white-space: nowrap;
}

table.dataTable .dt-col-actions {
  width: 1%;
  white-space: nowrap;
}

/* Empty lists (datatable-empty-state.js). A list with no records at all shows one message in place of its search, table,
   info and pager; its buttons row stays, minus the New button, which moves into the message. */
.dt-is-empty > .dt-empty-hidden,
.dt-is-empty .dt-empty-moved {
  display: none !important;
}

.dt-empty-state {
  text-align: center;
  padding: 2.5rem 1rem 3rem;
}

.dt-empty-state-icon {
  display: inline-grid;
  place-items: center;
  width: 4rem;
  height: 4rem;
  border-radius: 50%;
  background: rgba(var(--bs-primary-rgb), .1);
  color: var(--bs-primary);
  font-size: 1.75rem;
}

.dt-empty-state-title {
  margin: 1rem 0 0;
}

.dt-empty-state-hint {
  max-width: 40ch;
  margin: .375rem auto 0;
  color: var(--bs-secondary-color);
}

.dt-empty-state-actions {
  margin-top: 1.25rem;
}

/* The link that opens a row's record (its number or name, class dl-row-link in every list's render) is drawn in the link
   colour, at every width: in the heading colour nobody could tell it opened anything. */
a.dl-row-link,
a.dl-row-link.text-heading {
  color: var(--bs-link-color) !important;
}

a.dl-row-link:hover,
a.dl-row-link:focus-visible {
  color: var(--bs-link-hover-color) !important;
  text-decoration: underline;
}

/* Lists on a phone (datatable-mobile.js). Below md: one line of search + Filters + the table's buttons, rows as cards,
   and "< 1 / 7 >" instead of the length picker, info line and pager. */
.dt-mobile-filters-btn {
  display: none;
}

/* The "< 1 / 7 >" pager (datatable-mobile.js) is every list's pager at every width; DataTables' numbered buttons are hidden
   wherever it was added. Small, like the rest of the footer, and at the footer's right edge. */
.dt-mobile-ready .dt-paging {
  display: none !important;
}

.dt-mobile-pager {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: .625rem;
  margin-inline-start: auto;
  font-size: .8125rem;
  font-variant-numeric: tabular-nums;
}

.dt-mobile-pager .btn {
  width: 2rem;
  height: 2rem;
  padding: 0;
  font-size: 1rem;
}

.dt-mobile-pager:empty {
  display: none;
}

@media (max-width: 767.98px) {
  .dt-mobile-ready .dt-length,
  .dt-mobile-ready .dt-info,
  .dt-mobile-ready .dt-paging {
    display: none !important;
  }

  .dt-mobile-ready .dt-search {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;
  }

  .dt-mobile-ready .dt-search label {
    display: none;
  }

  .dt-mobile-ready .dt-search input {
    flex: 1;
    width: auto;
    margin: 0 !important;
  }

  .dt-mobile-ready .dt-search > div,
  .dt-mobile-ready .dt-layout-cell:has(.dt-search),
  .dt-mobile-ready .row > div:has(.dt-search) {
    width: 100%;
  }

  .dt-mobile-ready .dt-mobile-filters-btn {
    display: inline-flex;
    flex: none;
  }

  .dt-mobile-ready .dt-mobile-pager {
    flex: 1 1 100%;
    padding: 1rem 0 .25rem;
  }

  .dt-mobile-ready table.dataTable > thead {
    display: none;
  }

  .dt-mobile-ready table.dataTable,
  .dt-mobile-ready table.dataTable > tbody {
    display: block;
    width: 100% !important;
  }

  /* A card always reads the same way: the title (and the row's actions, at the end) on the first line, every other column
     on the lines under it. Without the forced break a short client name joined the title's line and a long one did not,
     so the same column sat in a different place card to card. datatable-mobile.js marks the title and actions cells. */
  .dt-mobile-ready table.dataTable > tbody > tr:not(.child) {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .125rem .75rem;
    padding: .75rem .25rem;
    border-bottom: 1px solid var(--bs-border-color);
  }

  .dt-mobile-ready table.dataTable > tbody > tr:not(.child):has(> td.dt-card-title)::after {
    content: "";
    order: -1;
    flex: 0 0 100%;
    height: 0;
  }

  .dt-mobile-ready table.dataTable > tbody > tr > td {
    display: block;
    padding: 0 !important;
    border: 0 !important;
    white-space: normal !important;
    font-size: .8125rem;
    color: var(--bs-secondary-color);
  }

  .dt-mobile-ready table.dataTable > tbody > tr > td.dtr-hidden,
  .dt-mobile-ready table.dataTable > tbody > tr > td:has(> input[type=checkbox]) {
    display: none !important;
  }

  .dt-mobile-ready table.dataTable > tbody > tr > td.dt-card-title {
    order: -2;
    flex: 1 1 0;
    min-width: 0;
    font-size: .9375rem;
    font-weight: 500;
    color: var(--bs-heading-color);
  }

  /* The actions cell is 1% wide on a desktop row; in a card that stacked its buttons into a tall column. It sits at the end
     of the title's line in every card. */
  .dt-mobile-ready table.dataTable > tbody > tr > td.dt-col-actions,
  .dt-mobile-ready table.dataTable > tbody > tr > td.dt-card-actions {
    width: auto !important;
    margin-inline-start: auto;
    white-space: nowrap !important;
  }

  .dt-mobile-ready table.dataTable > tbody > tr > td.dt-card-actions {
    order: -1;
  }

  /* An expand arrow (DataTables' dt-control, the materials list) sits just before the title, the same place in every card. */
  .dt-mobile-ready table.dataTable > tbody > tr > td.dt-control {
    order: -3;
    flex: none;
    width: 1.25rem !important;
  }

  .dt-mobile-ready table.dataTable > tbody > tr > td.dt-empty {
    flex: 1 1 100%;
    text-align: center;
    padding: 1.5rem 0 !important;
  }

  .dt-mobile-ready .table-responsive,
  .card-datatable.table-responsive:has(.dt-mobile-ready) {
    overflow-x: visible;
  }

  /* The toolbar keeps its place on a phone: the buttons just lose their labels. */
  .dt-mobile-ready .dt-toolbar {
    flex-wrap: nowrap;
  }

  /* Descendant, not child: DataTables wraps a button's text in a span of its own. */
  .dt-mobile-ready .dt-toolbar .dt-buttons .btn span.d-sm-inline-block {
    display: none !important;
  }

  .dt-mobile-ready .dt-toolbar .dt-buttons .btn i {
    margin: 0 !important;
  }

  /* One size for every button on that line: the Filters button, New, Search, Import... */
  .dt-mobile-ready .dt-toolbar .btn {
    flex: none;
    width: 38px;
    height: 38px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* The Responsive "+" (the row's hidden columns) was a 0-wide cell in a card, its circle hanging half outside the card. */
  .dt-mobile-ready table.dataTable > tbody > tr > td.control,
  .dt-mobile-ready table.dataTable > tbody > tr > td.dtr-control {
    position: relative;
    flex: none;
    width: 1.25rem !important;
    height: 1.25rem;
  }

  /* A card shows every column, so a row with nothing hidden has nothing for the "+" to open: it did nothing when tapped. */
  .dt-mobile-ready table.dataTable > tbody > tr:not(:has(> td.dtr-hidden:not(.control):not(.dtr-control))) > td.control,
  .dt-mobile-ready table.dataTable > tbody > tr:not(:has(> td.dtr-hidden:not(.control):not(.dtr-control))) > td.dtr-control {
    display: none !important;
  }

  .dt-mobile-ready table.dataTable > tbody > tr > td.control::before,
  .dt-mobile-ready table.dataTable > tbody > tr > td.dtr-control::before {
    top: 50% !important;
    left: 50% !important;
    margin: 0 !important;
    transform: translate(-50%, -50%);
  }

  /* The stat tiles above a list: two a row, without the big icons, instead of four tall blocks. A full-width row in the
     same card (the dashboard's "waiting to be invoiced" and setup alerts) stays full width. */
  .card-widget-separator .row > [class*="col-"]:not(.col-12) {
    flex: 0 0 50%;
    max-width: 50%;
  }

  .card-widget-separator .card-widget-1 {
    border: 0 !important;
    padding-bottom: .75rem !important;
  }

  .card-widget-separator .card-widget-1 .avatar {
    display: none;
  }

  .card-widget-separator hr {
    display: none;
  }
}

.dt-mobile-sheet {
  height: auto;
  max-height: 85vh;
  border-radius: 1rem 1rem 0 0;
}

/* The toolbar row above a list (its New button) had a whole card header's padding to itself. */
.card-datatable .dt-container > .card-header {
  padding: 0 0 1rem;
}

/* Two-column record pages (job, visit, request, quote).
   The page used to be a Bootstrap row of three columns - details (8), the tab card (8, order-last) and the sidebar (4).
   A flex row wraps, so details and the sidebar shared the first line and the tab card the second; whenever the sidebar
   was taller than the details card (a job with recommendations, profitability, schedule and billing) the space under
   details was dead - the tab card started at the bottom of the sidebar, screens below where it belonged.
   A grid lets the sidebar span both rows instead: details on top, the tab card straight under it, the sidebar beside
   both, and any spare height goes to the row under the tab card where nobody sees it. Below lg nothing here applies and
   the stacking (and the job page's own phone order, above) is exactly as before. */
@media (min-width: 992px) {
  .row.core-grid {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    align-items: start;
  }

  /* Grid items size themselves: the column widths above are the gutters' and Bootstrap's own 8/4 split. */
  .row.core-grid > * {
    width: auto;
    max-width: none;
    flex: none;
  }

  .core-grid > .core-main-top { grid-column: 1; grid-row: 1; }
  .core-grid > .core-main { grid-column: 1; grid-row: 2; }
  .core-grid > .core-side { grid-column: 2; grid-row: 1 / span 2; }

  /* Quote: three cards across the top (details, client, workflow approval), the lines under the first two and the
     sidebar under the third. With no workflow approval the sidebar takes the third column from the top instead of
     leaving it empty. */
  .row.core-grid.core-grid-quote {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .core-grid-quote > .qg-details { grid-column: 1; grid-row: 1; align-self: stretch; }
  .core-grid-quote > .qg-client { grid-column: 2; grid-row: 1; align-self: stretch; }
  .core-grid-quote > .qg-approval { grid-column: 3; grid-row: 1; align-self: stretch; }
  .core-grid-quote > .qg-main { grid-column: 1 / span 2; grid-row: 2; }
  .core-grid-quote > .qg-side { grid-column: 3; grid-row: 2; }
  .core-grid-quote:not(.has-approval) > .qg-side { grid-row: 1 / span 2; }
}

/* No workflow approval card: its empty cell must not sit under the sidebar that now spans the top row. */
@media (min-width: 992px) {
  .core-grid-quote:not(.has-approval) > .qg-approval { display: none; }
}

/* DataTables lists: a table's own toolbar (its New button) shares one line with Search above the table, and
   entries-per-page + "Showing x of y" share one line with pagination below it, at every width - each pair reflows by
   its own content instead of stacking into two rows at a fixed Bootstrap breakpoint regardless of how much room the
   card actually has. datatable-layout.js does the moving; the pages' own dom strings are unchanged. */
/* !important: the vendor DataTables Bootstrap5 stylesheet sets its own display on .card-header / row columns at the
   same specificity and loads after this file (page VendorStyles), so a plain override loses that tie on cascade order. */
.dt-container .dt-top-vacated,
.dt-container .dt-foot-vacated {
  display: none !important;
}

/* Bootstrap's .row is already a flex row; only its columns' fixed col-sm-12/col-md-6 widths force an early stack.
   Let each column size to its own content and wrap only when it truly no longer fits. */
.dt-container > .row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  row-gap: .5rem;
}

.dt-container > .row > div {
  width: auto;
  max-width: 100%;
  flex: 0 1 auto;
}

/* The line above a list (datatable-layout.js placeToolbar): Search on the left, the table's buttons on the right, the
   same at every width. The "Search:" label goes; the box carries a placeholder instead. */
.dt-container > .dt-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-bottom: 1rem;
}

.dt-toolbar .dt-search {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex: 1 1 12rem;
  min-width: 0;
  max-width: 24rem;
  /* !important: the vendor DataTables sheet loads later and gives .dt-search a 1.5rem block margin. */
  margin: 0 !important;
}

.dt-toolbar .dt-search label {
  display: none;
}

.dt-toolbar .dt-search input {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  margin: 0 !important;
}

.dt-toolbar .dt-action-buttons,
.dt-toolbar > .dt-buttons {
  display: flex;
  margin-inline-start: auto;
  padding: 0 !important;
  text-align: end;
}

.dt-toolbar .dt-buttons {
  flex-wrap: nowrap !important;
  gap: .25rem;
  /* !important: DataTables' sheet gives a phone's .dt-buttons a bottom margin for its old stacked layout. */
  margin: 0 !important;
}

/* Separate buttons with a gap, not a joined group: Bootstrap squared the New button's inner corners (with !important and a
   long selector, hence the doubled class here). */
.dt-container .dt-toolbar .dt-buttons.btn-group > .btn.btn {
  border-radius: var(--bs-btn-border-radius) !important;
}

@media (max-width: 767.98px) {
  .dt-toolbar .dt-search {
    flex: 1 1 0;
    max-width: none;
  }
}

.dt-is-empty .dt-toolbar .dt-search {
  display: none !important;
}

/* Spans the full row (its vacated sibling is hidden) so pagination can sit flush right of length + info instead of
   being capped at the half-width Bootstrap column it used to share only with "Showing x of y". */
/* ".dt-container > .row > .dt-foot-start": it must outrank ".dt-container > .row > div" above, or the footer shrinks to its
   content and the pager cannot reach the right edge. */
.dt-container .dt-foot-start,
.dt-container > .row > .dt-foot-start {
  display: flex;
  flex: 0 0 100%;
  max-width: 100%;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: .5rem .75rem;
  text-align: left;
}

/* A compact footer: small length picker, "1 to 7 of 40" right beside it, a small pager pushed to the right edge. */
.dt-container .dt-foot-start .dt-length .form-select {
  padding: .25rem 1.75rem .25rem .625rem;
  font-size: .8125rem;
  min-height: 0;
  height: 2rem;
  background-position: right .5rem center;
}

.dt-container .dt-foot-start .dt-info {
  padding: 0 !important;
  font-size: .8125rem;
}

.dt-container .dt-foot-start .dt-paging .pagination {
  margin: 0 !important;
  gap: .25rem;
}

.dt-container .dt-foot-start .dt-paging .page-link {
  min-width: 2rem;
  height: 2rem;
  padding: 0 .5rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: .8125rem;
}

.dt-container .dt-foot-start .dt-length {
  display: flex;
  align-items: center;
  gap: .5rem;
  text-align: left;
}

.dt-container .dt-foot-start .dt-length .form-select {
  width: auto;
}

.dt-container .dt-foot-start .dt-paging {
  /* !important: DataTables' sheet centres the pager with its own margins. */
  margin-inline-start: auto !important;
  margin-inline-end: 0 !important;
}

.dt-container .dt-foot-start .dt-paging nav,
.dt-container .dt-foot-start .dt-paging .pagination {
  justify-content: flex-end !important;
}

/* The unread count sits on the bell. Sneat positions the badge against the nearest positioned ancestor, which on a phone is
   the whole navbar (the dropdown item there is static), so the number floated at the navbar's top right. Anchoring it to the
   bell's own link keeps it on the bell at every width. */
.dropdown-notifications > .nav-link {
  position: relative;
}

/* One look for every count: the bell's unread number and the menu's (Inbox, Recommendations) were two different pills - a
   small one on the bell, a large one in the menu. Both caps are 99+. */
.dropdown-notifications .badge-notifications,
.menu-inner .menu-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.125rem;
  height: 1.125rem;
  padding: 0 .3125rem;
  border-radius: 50rem;
  /* !important: Sneat's navbar sheet shrinks the bell's badge text with a longer selector. */
  font-size: .6875rem !important;
  font-weight: 600;
  line-height: 1;
}

.dropdown-notifications .badge-notifications {
  position: absolute;
  top: 0;
  right: 0;
  left: auto;
  transform: translate(35%, -20%);
}

/* Profile header: the header block is pulled up 2rem so the avatar straddles the banner's lower edge, and the name and
   details beside it were pulled up with it, landing across the edge between the banner and the white card. Beside the
   avatar they start below the banner instead. (Stacked on a phone, they already sit under the avatar.) */
@media (min-width: 576px) {
  .user-profile-header .user-profile-info {
    margin-top: calc(2rem + .75rem);
  }
}

/* Tab strips (Sneat's .nav-align-top wrapper): the tabs never wrap, and the strip had no bounds, so on a phone six tabs ran
   far past the card and dragged the page sideways with them, out from under the template. The strip now scrolls inside its own
   box, without a scrollbar, and fades at whichever edge has more tabs beyond it (tab-scroll.js adds .has-more-start /
   .has-more-end). Every strip of this kind gets this, and so does the module tab strip (Shared/_SettingsTabs, .dl-settings-tabs); the ones
   that wrap or stack are unaffected. */
.nav-align-top > .nav,
.nav-align-bottom > .nav ,
.nav.dl-settings-tabs {
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* room for the active tab's shadow, which the scroll box would otherwise cut off */
  padding-bottom: .6rem;
  margin-bottom: -.3rem;
}

.nav-align-top > .nav::-webkit-scrollbar,
.nav-align-bottom > .nav::-webkit-scrollbar,
.nav.dl-settings-tabs::-webkit-scrollbar {
  display: none;
}

.nav-align-top > .nav > .nav-item,
.nav-align-bottom > .nav > .nav-item,
.nav.dl-settings-tabs > .nav-item {
  flex: 0 0 auto;
}

.nav-align-top > .nav .nav-link,
.nav-align-bottom > .nav .nav-link,
.nav.dl-settings-tabs .nav-link {
  white-space: nowrap;
}

.nav-align-top > .nav.has-more-end,
.nav-align-bottom > .nav.has-more-end,
.nav.dl-settings-tabs.has-more-end {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
}

.nav-align-top > .nav.has-more-start,
.nav-align-bottom > .nav.has-more-start,
.nav.dl-settings-tabs.has-more-start {
  -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 2.5rem), transparent);
  mask-image: linear-gradient(to left, #000 calc(100% - 2.5rem), transparent);
}

.nav-align-top > .nav.has-more-start.has-more-end,
.nav-align-bottom > .nav.has-more-start.has-more-end,
.nav.dl-settings-tabs.has-more-start.has-more-end {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 2.5rem), transparent);
  mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 2.5rem), transparent);
}

/* ---- Lifecycle strip (Pages/Shared/_LifecycleStrip.cshtml) ----
   Request -> Quote -> Approved -> Scheduled -> Invoice. Sneat's cards, avatars, badges and timeline
   do the drawing; these rules only lay the stages out and mark their state, in the stepper's own
   scheme: done is light primary, current is solid primary with a ring, upcoming is muted. The
   chevrons between stages are drawn with borders (Sneat 3 icons are SVG masks, not a font). */
/* R3: a row, not a five-column grid - a stage the work skipped is a dot (.lc-dot-stage) and gives its width to the
   stages that happened. */
.lc-stages {
  display: flex;
  align-items: stretch;
  gap: 1.75rem;
}
.lc-stages > .lc-stage { flex: 1 1 0; }
.lc-dot {
  display: inline-block;
  flex: none;
  width: .55rem;
  height: .55rem;
  border-radius: 50%;
  border: 2px dashed var(--bs-secondary-color);
  opacity: .7;
}
.lc-dot-stage { align-self: center; margin: 0 -.85rem; }
.lc-slim-step.lc-dot-step { flex: 0 0 auto; min-width: 0; padding: 0 .25rem; }
.lc-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 1rem;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius-lg);
  background: var(--bs-paper-bg, var(--bs-body-bg));
}
.lc-stage:not(:last-child)::after {
  content: "";
  position: absolute;
  top: 50%;
  right: -1.15rem;
  width: .55rem;
  height: .55rem;
  border-top: 2px solid var(--bs-secondary-color);
  border-right: 2px solid var(--bs-secondary-color);
  transform: translateY(-50%) rotate(45deg);
}
.lc-stage.lc-done:not(:last-child)::after { border-color: var(--bs-primary); }
/* Only the first unfinished stage is outlined - where the work is waiting. */
.lc-stage.lc-focus {
  border-color: var(--bs-primary);
  box-shadow: 0 0 0 1px var(--bs-primary), 0 .5rem 1.25rem rgba(var(--bs-primary-rgb), .16);
}
.lc-stage.lc-problem { border-color: rgba(var(--bs-danger-rgb), .6); }
.lc-stage.lc-upcoming { background: var(--bs-body-bg); }
.lc-stage.lc-upcoming .lc-stage-title,
.lc-stage.lc-skipped .lc-stage-title { color: var(--bs-secondary-color); }
.lc-stage.lc-skipped { border-style: dashed; background: transparent; }
.lc-stage-foot { margin-top: auto; padding-top: .25rem; }
.lc-lines { margin: 0; }
.lc-line {
  display: flex;
  justify-content: space-between;
  gap: .75rem;
  padding: .3rem 0;
  border-bottom: 1px dashed var(--bs-border-color);
}
.lc-line:last-child { border-bottom: 0; }
.lc-line dt, .lc-line dd { margin: 0; min-width: 0; }
.lc-line dt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lc-note {
  margin: 0;
  padding: .5rem .65rem;
  border-radius: var(--bs-border-radius);
  background: rgba(var(--bs-secondary-rgb), .08);
  color: var(--bs-body-color);
}
.lc-signature {
  border: 1px dashed var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  padding: .25rem;
  text-align: center;
}
.lc-signature img { max-width: 100%; max-height: 3.25rem; }
.lc-journey .timeline-header { display: flex; justify-content: space-between; gap: 1rem; }

/* Compact by default: about half of the first row of stages shows, fading into "Show more".
   Expanded shows everything (stages and activity); hidden keeps only the header row. Both are
   remembered per browser by lifecycle-strip.js. */
.lc-card .lc-clip {
  max-height: 11.5rem;
  overflow: hidden;
  position: relative;
}
.lc-card .lc-clip::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 5.5rem;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--bs-paper-bg, #fff) 85%);
}
/* Above the fade: the clipped body is positioned, so it would otherwise paint over the pill. */
.lc-expander { position: relative; z-index: 1; margin-top: -1rem; }
.lc-card .lc-expand .lc-expand-less,
.lc-card.lc-expanded .lc-expand .lc-expand-more { display: none; }
.lc-card.lc-expanded .lc-expand .lc-expand-less { display: inline; }
.lc-card.lc-hidden .lc-body,
.lc-card.lc-hidden .lc-expander { display: none; }
.lc-card.lc-hidden .card-header { padding-bottom: 1.25rem !important; }
.lc-card .lc-hide .bx { transition: transform .2s ease; }
.lc-card.lc-hidden .lc-hide .bx { transform: rotate(180deg); }
.lc-open {
  flex: none;
  color: var(--bs-secondary-color);
  line-height: 1;
}
.lc-open:hover { color: var(--bs-primary); }

/* Slim: one row of steps on the Request, Quote and Invoice pages. */
.lc-slim-steps { display: flex; align-items: center; gap: .5rem; overflow-x: auto; }
.lc-slim-step { display: flex; align-items: center; flex: 1 1 0; min-width: 9.5rem; }
.lc-slim-step:not(:last-child)::after {
  content: "";
  flex: none;
  width: .45rem;
  height: .45rem;
  margin: 0 .4rem 0 auto;
  border-top: 2px solid var(--bs-secondary-color);
  border-right: 2px solid var(--bs-secondary-color);
  transform: rotate(45deg);
}
.lc-slim-step.lc-done:not(:last-child)::after { border-color: var(--bs-primary); }
.lc-slim-link { display: flex; align-items: center; gap: .5rem; padding: .35rem .5rem; border-radius: var(--bs-border-radius); color: inherit; min-width: 0; }
a.lc-slim-link:hover { background: rgba(var(--bs-primary-rgb), .08); }
.lc-here .lc-slim-link { background: rgba(var(--bs-primary-rgb), .1); }
.lc-slim-status { font-size: .75rem; }
.lc-slim-step.lc-upcoming .text-heading, .lc-slim-step.lc-skipped .text-heading { color: var(--bs-secondary-color) !important; }

/* Five across only when the card itself is wide enough (it sits beside a sidebar, so the window
   width says little); narrower, the stages wrap into rows of readable cards and lose the arrows. */
.lc-card { container-type: inline-size; }
@container (max-width: 1149.98px) {
  .lc-stages { flex-wrap: wrap; gap: 1rem; }
  .lc-stages > .lc-stage { flex: 1 1 15rem; }
  .lc-dot-stage { margin: 0; }
  .lc-stage::after { display: none; }
}

/* The record the page is on, inside the full card. */
.lc-stage.lc-here { background: rgba(var(--bs-primary-rgb), .04); }

/* Contract card: one line of steps per job. */
.lc-contract-job { padding: .75rem 0; border-bottom: 1px solid var(--bs-border-color); }
.lc-contract-job:last-child { border-bottom: 0; padding-bottom: 0; }
.lc-row .lc-slim-steps { gap: .25rem; }

/* Everything already shows in the compact height (lifecycle-strip.js fitCheck). */
.lc-card.lc-fits .lc-clip { max-height: none; }
.lc-card.lc-fits .lc-clip::after, .lc-card.lc-fits .lc-expander { display: none; }

/* Contract page: a job's visit list opens compact (page-org-contract-visits.js) - about five
   visits around the next one, fading into "Show all", the same way the journey card folds. */
.cv-list.cv-clip {
  max-height: 16.5rem;
  overflow: hidden;
  position: relative;
}
.cv-list.cv-clip::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 4.5rem;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, var(--bs-paper-bg, #fff) 85%);
}
.cv-expander { position: relative; z-index: 1; margin-top: -1.25rem; }

/* Inputs inside a table's cells (invoice lines, commission, expenses...). A table shares its width between columns, and an
   input is 100% of whatever its cell gets, so on a narrow card the Qty and Unit price boxes shrank until their numbers
   were cut off or showed nothing. Inputs keep a readable minimum and the table scrolls sideways in its .table-responsive
   instead; the cells' side padding is tightened so less of that width goes to gaps. */
.table-responsive > .table > tbody > tr > td:has(> .form-control, > .form-select) {
  padding-inline: .375rem;
}

.table-responsive > .table > tbody > tr > td > input.form-control[type="number"] {
  min-width: 5.5rem;
  padding-inline: .5rem;
}

.table-responsive > .table > tbody > tr > td > input.form-control:not([type="number"]),
.table-responsive > .table > tbody > tr > td > .form-select {
  min-width: 8rem;
}

#invoice-lines-table .line-quantity {
  min-width: 4.5rem;
}

#invoice-lines-table .line-price {
  min-width: 6.5rem;
}

#invoice-lines-table .line-description {
  min-width: 11rem;
}

/* No select2 box wider than the place it sits in (Oct 2026: the composer's client picker ran past the card on a phone with a long
   name and address). The chosen text is cut with an ellipsis instead. */
.select2-container {
  max-width: 100%;
}

/* An open list hangs on <body> in a wrapper placed at its field's left edge but as wide as the screen, so on a phone the
   wrapper ran past the right edge (by however far the field sat from the left) and the page slid sideways under the list.
   The list inside is positioned and sized on its own; the wrapper needs no width. Phone only: on a wide screen an
   auto-width list (dropdownAutoWidth) grows to its longest entry within that wrapper. */
@media (max-width: 767.98px) {
  body > .select2-container--open {
    width: 0 !important;
  }
}
.select2-container .select2-selection--single .select2-selection__rendered {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Side panels (Oct 2026, the owner's call): a form, a send/compose dialog or a picker opens as a panel from the side
   instead of a box in the middle. It is still a Bootstrap modal, given class modal-drawer, so every page's script, its
   data-bs-dismiss="modal" buttons, the backdrop, Esc and select2's dropdownParent keep working unchanged; only the
   placement is drawn here. Full height at the inline end, as wide as the modal used to be (so its form keeps the
   columns it was designed with), the full screen on a phone. Confirmations, plan blocks, the map and the convert
   wizard stay centred modals (docs/plans/modals-to-offcanvas.md). */
.modal.modal-drawer {
  --dl-drawer-w: 32rem;
  padding: 0 !important;
}

.modal.modal-drawer .modal-dialog,
.modal.modal-drawer.fade .modal-dialog {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  inset-inline-start: auto;
  display: flex;
  align-items: stretch;
  width: min(var(--dl-drawer-w), 100vw);
  max-width: none;
  height: 100%;
  min-height: 0;
  margin: 0;
  padding: 0;
  transform: translateX(100%);
  transition: transform .3s ease-out;
}

[dir="rtl"] .modal.modal-drawer .modal-dialog {
  transform: translateX(-100%);
}

.modal.modal-drawer.show .modal-dialog,
[dir="rtl"] .modal.modal-drawer.show .modal-dialog {
  transform: none;
}

.modal.modal-drawer .modal-dialog.modal-sm { --dl-drawer-w: 26rem; }
.modal.modal-drawer .modal-dialog.modal-lg { --dl-drawer-w: 50rem; }
.modal.modal-drawer .modal-dialog.modal-xl { --dl-drawer-w: 71.25rem; }

.modal.modal-drawer .modal-content {
  width: 100%;
  height: 100%;
  max-height: none;
  border: 0;
  border-radius: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Sneat nudges the close button half outside a centred box; at the screen's edge that put it off screen. */
.modal.modal-drawer .btn-close,
.modal.modal-drawer .btn-close:hover,
.modal.modal-drawer .btn-close:focus {
  transform: none;
  box-shadow: none;
}

.modal.modal-drawer .modal-simple .btn-close {
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
}

.modal.modal-drawer .modal-header .btn-close {
  inset-block-start: 1.25rem;
  inset-inline-end: 1.25rem;
}

/* A modal-body that is also a .row pulled itself past the panel's edges by the row's negative gutter (the panel scrolls
   sideways where a centred box only clipped it). */
.modal.modal-drawer .modal-body.row {
  margin-inline: 0;
}

/* The buttons stay in reach while a long form scrolls under them. */
.modal.modal-drawer .modal-footer {
  position: sticky;
  inset-block-end: 0;
  z-index: 2;
  background: var(--bs-modal-bg, var(--bs-paper-bg));
}

@media (prefers-reduced-motion: reduce) {
  .modal.modal-drawer .modal-dialog { transition: none; }
}

/* A row of filter buttons wider than a phone (Expenses' statuses, the dispatch queue's kinds and ranges) scrolls inside its
   own box instead of widening the page. Opt-in: a .btn-group holding a dropdown must not clip its menu. */
@media (max-width: 767.98px) {
  .btn-group.dl-btn-group-scroll {
    max-width: 100%;
    overflow-x: auto;
    scrollbar-width: none;
  }

  .btn-group.dl-btn-group-scroll::-webkit-scrollbar {
    display: none;
  }

  .btn-group.dl-btn-group-scroll > .btn {
    flex: 0 0 auto;
    white-space: nowrap;
  }
}
