/* ============================================================
   Mobile Detail Page – Shared Styles
   Prefix: md- (mobile-detail)
   ============================================================ */

/* --- Hide scrollbars on mobile scrollable regions (keeps scroll behavior) --- */
.md-body,
.md-dialog-body,
.md-scroll-region,
.products-list-scroll,
.products-page-content,
.appt-page,
.hide-scrollbar {
    scrollbar-width: none;          /* Firefox */
    -ms-overflow-style: none;       /* IE/Edge legacy */
}
.md-body::-webkit-scrollbar,
.md-dialog-body::-webkit-scrollbar,
.md-scroll-region::-webkit-scrollbar,
.products-list-scroll::-webkit-scrollbar,
.products-page-content::-webkit-scrollbar,
.appt-page::-webkit-scrollbar,
.hide-scrollbar::-webkit-scrollbar {
    display: none;                  /* Chrome/Safari/WebView */
    width: 0;
    height: 0;
}

/* --- Card elevation token (the standard card drop shadow) -------------------
   Single source of truth for card drop shadows. Use `box-shadow: var(--tm-card-shadow)`
   on any card — do NOT use `filter: drop-shadow(...)`: drop-shadow promotes each
   (often animated) card to its own iOS compositing layer and paints an opaque blue
   halo that stacks between densely-packed cards.
   Light mode: a soft periwinkle shadow. Dark mode: the same periwinkle at full
   opacity, which reads as a glow against the dark surface. */
:root {
    --tm-card-shadow: 1px 2px 8px hsl(220deg 40% 70% / 0.55);
    /* Scrims sit on top of arbitrary photography, so they stay dark in both themes on
       purpose — that is what makes white text on a user's photo legible. Named rather
       than inlined so they read as a decision, not as stray literals. */
    --tm-photo-scrim: hsl(222deg 47% 11% / 0.78);
    --tm-photo-scrim-strong: hsl(0deg 0% 0% / 0.5);
}
.mud-theme-dark {
    --tm-card-shadow: 1px 2px 8px hsl(220deg 40% 70%);
}

/* --- CSS custom properties (page-level overrides) --- */
/* --md-page-bg        default: var(--mud-palette-tertiary)
   --md-card-radius    default: 24px (the house glass-pane radius)
   --md-body-pb        default: 190px                          */

/* Dense outlined inputs (Margin.Dense) hardcode their own corner radius in
   MudBlazor.min.css instead of reading --mud-default-borderradius, so the theme's
   control radius (AI.Client.Infrastructure/Settings/Theme.cs DefaultBorderRadius)
   silently has no effect on them. AI.Client/wwwroot/css/utilities.css carries the
   matching fix for the web/browser host, but this file — not utilities.css — is
   what MAUI's native shell actually links (see AI.Client.Mobile/wwwroot/index.html),
   so the override has to be duplicated here too or the field's outline goes round
   while its dense background/margin box stays square. */
.mud-input-control.mud-input-control-margin-dense {
    border-radius: var(--mud-default-borderradius) !important;
}

/* .custom-field / .custom-field-gray (Class="custom-field" on MudTextField etc. — used all
   over AI.Client.Mobile.Shared, e.g. MobileEditAssetDialog's Location field) give an outlined
   control an opaque fill so it stands out on a tinted dialog body. On web, utilities.css pairs
   that fill with the .mud-input-control-margin-dense radius override above, so the two rules
   land on the same element and the box comes out rounded.

   The fill has to sit on .mud-input — the box the outlined border actually traces — NOT on the
   control root that Class="custom-field" lands on. That root also wraps the floating label and
   the helper text, so it is taller than the box, and filling it paints a square slab of surface
   around a round field. Giving the root a matching radius does not save it either: a rounded
   rectangle taller than the one nested inside it still pokes out at the corners. */
.custom-field .mud-input.mud-input-outlined,
.custom-field-gray .mud-input.mud-input-outlined {
    border-radius: var(--mud-default-borderradius);
}

.custom-field .mud-input.mud-input-outlined {
    background: var(--mud-palette-surface);
}

.custom-field-gray .mud-input.mud-input-outlined {
    background: var(--mud-palette-background-gray);
}

/* --- Page container --- */
.md-page {
    display: flex;
    flex-direction: column;
    height: 100%;
    background: var(--md-page-bg, var(--mud-palette-tertiary));
    position: relative;
}

    .md-page .custom-border {
        border-radius: 14px !important;
        /* box-shadow (not filter: drop-shadow) + an alpha colour: drop-shadow
           promotes each animated card to its own iOS compositing layer and paints
           an opaque blue halo that stacks between densely-packed top cards. */
        box-shadow: 1px 2px 8px hsl(220deg 40% 70% / 0.55);
        border: 0px !important;
    }

.md-background-gray {
    background: var(--mud-palette-background-gray);
}

/* --- Scrollable body --- */
.md-body {
    flex: 1;
    overflow-y: auto;
    padding: 16px;
    padding-bottom: var(--md-body-pb, 190px);
    background: var(--md-page-bg, var(--mud-palette-tertiary));
}

/* --- Header (primary-colored) --- */
.md-header {
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    padding: 12px 16px;
    /* The status-bar inset ABSORBS the 12px, it does not add to it — adding pushes the
       header ~12px lower on every real handset. Same rule in .md-dialog-header below
       and in MobileDialogHeader.razor. */
    padding-top: max(12px, var(--tm-safe-area-top, 0px));
    display: flex;
    align-items: flex-start;
    gap: 8px;
}
.md-header-back {
    color: var(--mud-palette-primary-text) !important;
    margin-left: -8px;
    margin-right: -4px;
}
.md-header-content {
    flex: 1;
    min-width: 0;
}
.md-header-top {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 8px;
}
/* Standard mobile page title — sits inside a primary-colored header banner */
.md-page-title {
    padding: 6px 4px 0;
    font-size: 19px;
    font-weight: 600;
    line-height: 1.25;
    letter-spacing: -0.005em;
    color: var(--mud-palette-primary-text);
    white-space: pre-wrap;
}
.md-status-badge {
    background: var(--mud-palette-primary-text);
    color: var(--mud-palette-primary);
    padding: 4px 12px;
    border-radius: 9999px;
    font-size: 0.8125rem;
    font-weight: 600;
    flex-shrink: 0;
}
.md-subtitle {
    display: block;
    font-size: 0.95rem;
    margin-top: 6px;
    opacity: 0.95;
    white-space: pre-wrap;
    line-height: 1.3;
}

.md-form-label {
    font-size: 0.8125rem !important;
    color: var(--mud-palette-text-secondary) !important;
}

/* --- Card --- */
.md-card {
    background: var(--mud-palette-surface);
    /* Default is the 24px house glass-pane radius (was 5px pre-glass). A page can still override
       the token, but no page does — the shared default is the silhouette. */
    border-radius: var(--md-card-radius, 24px);
    padding: 16px;
    margin-bottom: 12px;
    /* The token, not the literal — it carries the dark-mode variant (see :root above). */
    box-shadow: var(--tm-card-shadow);
}

/* Reserves a predictable footprint while deferred data is still loading
   so skeleton -> real content swap doesn't jump the scroll position. */
.md-card--reserve-contacts {
    min-height: 220px;
}
.md-card--reserve-notes {
    min-height: 200px;
}
.md-card--reserve-defects {
    min-height: 72px;
}

.job-details-deferred-slot--reserved {
    min-height: 56px;
}

/* Empty-state container: keeps the card from collapsing to a single line
   when the loaded result has no items. */
.md-empty-stable {
    display: flex;
    align-items: center;
    min-height: 56px;
    padding: 8px 0;
    color: var(--mud-palette-text-secondary);
}
.md-card-title {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}
.md-card-icon {
    color: var(--mud-palette-text-secondary);
    flex-shrink: 0;
    font-size: 1.125rem !important;
}
.md-card-icon.mud-icon-root { font-size: 1.125rem !important; }
.md-card-title-text {
    font-weight: 600;
    font-size: 0.875rem;
    letter-spacing: -0.005em;
    color: var(--mud-palette-text-primary);
}

/* Standardize section header typography across mobile pages and dialogs
   to match the Products section header (0.875rem / 600 weight). */
.md-page .md-card-title .mud-typography,
.md-page .md-card-title-text,
.md-page .cs-head .mud-typography,
.md-page .mud-typography.mud-typography-h5,
.md-page .mud-typography.mud-typography-h6,
.md-page .mud-typography.mud-typography-subtitle1,
.md-page .mud-typography.mud-typography-subtitle2,
.mud-dialog .md-card-title .mud-typography,
.mud-dialog .md-card-title-text,
.mud-dialog-title .mud-typography,
.mud-dialog-title.mud-typography {
    font-size: 0.875rem !important;
    font-weight: 600 !important;
    letter-spacing: -0.005em;
    line-height: 1.3;
}

/* --- Field (label / value pair) --- */
.md-field {
    margin-bottom: 10px;
}
.md-field-label {
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
}
.md-field-value {
    font-size: 0.9375rem;
    color: var(--mud-palette-text-primary);
    margin-top: 1px;
}

/* ============================================================
   Backward-compatible aliases
   MobileMediaGallery & MobileNotes reference these classes.
   ============================================================ */
.property-detail-card {
    background: var(--mud-palette-surface);
    border-radius: 5px;
    padding: 16px;
    margin-bottom: 12px;
    box-shadow: 1px 2px 8px hsl(220deg 40% 70% / 0.55);
    border: 0px !important;
}

/* Mobile card/section corner radius. Use on mobile screens/dialogs for the rounded section look. */
.md-radius {
    border-radius: 14px !important;
}

.property-detail-card-title {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}
.property-detail-card-icon {
    color: var(--mud-palette-text-secondary);
    flex-shrink: 0;
}
.property-detail-card-title-text {
    font-weight: 500;
    font-size: 15px;
    color: var(--mud-palette-text-primary);
}


.mud-dialog:has(.md-dialog-body, .md-page) .mud-dialog-content {
    padding: 0px !important;    
}

/* ============================================================
   Mobile Products page
   Prefix: products-
   ============================================================ */
.products-page-content {
    display: flex;
    flex-direction: column;
    padding-top: var(--tm-mobile-fixed-top-nav-inner-padding);
    background: var(--mud-palette-tertiary);
    height: 100dvh;
    overflow: hidden;
    box-sizing: border-box;
}

/* ---- Top bar ----
   Title, tab strip and refresh on ONE row. They were three stacked rows — title+refresh, then
   the tab strip, then group-by, then scope — so four rows of chrome stood between the app bar
   and the first datum. The shared TopNavBar has no horizontal slack left at 390px (see its own
   note), so the saving is taken here instead: the page's own title row absorbs the tabs and the
   refresh, and the list starts roughly 120px higher. */
.products-topbar {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
    padding: 4px 8px 4px 16px;
}

/* Shrinks before the tabs do. The tab strip is a control the technician uses; the title only
   names where they are, so it is the one that gives up width first on a narrow handset. */
.products-title {
    flex: 0 1 auto;
    min-width: 0;
    margin: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 1.125rem;
    font-weight: 600;
    letter-spacing: 0.0075em;
    color: var(--mud-palette-text-primary);
}

/* The tab strip takes the centre and the remaining width. */
.products-topbar .products-seg-group {
    flex: 1 1 auto;
    min-width: 0;
}

.products-refresh {
    appearance: none;
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    border-radius: 5px;
    background: transparent;
    color: var(--mud-palette-text-secondary);
    cursor: pointer;
}

.products-refresh[disabled] {
    opacity: 0.5;
    cursor: default;
}

.products-refresh-icon {
    font-size: 20px !important;
}

.products-refresh-icon.spin {
    animation: products-spin 900ms linear infinite;
}

@keyframes products-spin {
    to {
        transform: rotate(360deg);
    }
}

/* ---- Context row ----
   ONE row, and it belongs to the tab rather than to the page. Group-by and scope were shown on
   the Van tab too, where neither means anything — a van has no jobs to group by and no week to
   scope to. Required fills this slot with scope + grouping; Van fills it with search. */
.products-context {
    flex: 0 0 auto;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
    padding: 8px 16px;
}

.products-seg-group {
    display: flex;
    background: var(--mud-palette-tertiary);
    border-radius: 5px;
    padding: 4px;
    gap: 0;
}

/* Sentence case, not uppercase: shouting with wide tracking is reserved for status labels, and
   a control that shouts competes with the one orange pill that actually needs to be seen. */
.products-seg {
    flex: 1;
    appearance: none;
    border: 1px solid transparent;
    background: transparent;
    min-height: 44px;
    padding: 8px 4px;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    cursor: pointer;
    border-radius: 4px;
    transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
}

/* Ruled, not floated — the hairline is what separates the active pill from its track. */
.products-seg.active {
    background: var(--mud-palette-surface);
    border-color: var(--mud-palette-lines-default);
    color: var(--mud-palette-text-primary);
    font-weight: 600;
}

/* The outstanding count, on the Required tab only. Built from the pill radius and the primary
   token the screen already uses — the number is the reason to look at that tab, so it carries
   the one accent rather than a second colour. Van is unbadged: "how much is on board" is not a
   count of things to do. */
.products-seg-badge {
    display: inline-block;
    margin-left: 6px;
    min-width: 20px;
    padding: 0 6px;
    border-radius: 6px;
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.38;
    font-variant-numeric: tabular-nums;
}

/* Wraps rather than scrolls. As a horizontal scroller with a suppressed scrollbar, a longer
   localisation pushed a radio option off-screen with nothing to say it was there. */
.products-scope-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.products-scope-chip {
    appearance: none;
    min-height: 44px;
    padding: 8px 16px;
    border-radius: 6px;
    border: 1px solid var(--mud-palette-lines-default);
    background: var(--mud-palette-surface);
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    cursor: pointer;
    white-space: nowrap;
    transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
}

/* No inversion. A solid black fill made the time filter the loudest mark on the screen — louder
   than "Go collect", which is the only state a technician can act on from here. The active mark
   now matches .products-seg.active so all three single-select controls speak at one volume. */
.products-scope-chip.active {
    background: var(--mud-palette-surface);
    border-color: color-mix(in srgb, var(--mud-palette-primary) 45%, var(--mud-palette-surface));
    color: var(--mud-palette-primary);
    font-weight: 600;
}

/* The context row's two pills. Same chip, but each states its CURRENT value and opens a sheet
   rather than being one option among a visible set — three scope options and three groupings
   were nine controls' worth of chrome for two decisions a technician makes once a day. */
.products-scope-chip--pill {
    flex: 1 1 0;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: 4px;
    padding: 8px 8px 8px 12px;
    text-align: left;
}

.products-scope-chip--pill .products-scope-chip-value {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.products-scope-chip--pill .products-scope-chip-chev {
    flex: 0 0 auto;
    font-size: 20px !important;
    color: var(--mud-palette-text-secondary);
}

/* ---- List ---- */
.products-list-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    background-color: var(--mud-palette-tertiary);
    /* 96px, not 170px: the old value was roughly double the bottom nav's real footprint and left
       a screenful of dead scroll at the end of every list. */
    padding: 8px 16px calc(96px + var(--tm-safe-area-bottom, 0px));
}

/* ---- Group card ----
   Ruled, not floated: a 1px hairline does the structural work. The Material double-shadow this
   replaced also rendered as a black smear against the midnight dark theme. */
/* Ruled, not boxed. A card per group meant every group was an isolated container of equal
   weight, and the border was compensating for the fact that nothing else separated them. A
   hairline plus a genuinely generous gap does the same work and lets the page breathe:
   8px inside a row, 16px between sections — tight, tight, gap. */
.products-group {
    background: transparent;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    margin-bottom: 0;
}

.products-group + .products-group {
    margin-top: 16px;
}

/* The open group lifts onto the surface so the expanded lines read as one region. */
.products-group:has(.products-lines) {
    background: var(--mud-palette-surface);
    border-radius: 5px;
    border: 1px solid var(--mud-palette-lines-default);
    overflow: hidden;
}

.products-group-head {
    display: flex;
    align-items: stretch;
    border-bottom: 1px solid transparent;
}

.products-group:has(.products-lines) .products-group-head {
    border-bottom-color: var(--mud-palette-lines-default);
}

/* ---- Section readiness mark ----
   The checkbox column is gone. It asked the technician to re-declare what the system already
   knew — and a hand-drawn tick and a recorded collection then sat in the same box meaning two
   incompatible things. Readiness is derived now, and the only mark left is the green check a
   fully-ready section carries. */
/* It rides WITH the readiness pill rather than in a leading gutter. A leading icon rendered
   only on ready sections would shift those sections' names right of every unready one, and the
   group names would stop forming a column — which is the one thing a list of sections must do.
   On the pill's own line it costs no gutter and reads as what it is: the section's verdict. */
.products-group-check {
    font-size: 16px !important;
    color: var(--mud-palette-success);
    vertical-align: middle;
}

/* ---- Group header (the expander) ----
   It now owns the full left gutter the tick used to provide. */
.products-group-open {
    appearance: none;
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    border: 0;
    background: transparent;
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

/* Ruled, not filled. A tertiary fill read as a blue (actionable) chip in light and vanished into
   the card in dark; the hairline works in both, and the quantity is data, not an action. */
/* Bare and right-aligned, matching .products-line-qty exactly, so the group total and every line
   quantity below it form one numeric rail down the right edge. It was a bordered badge on the
   LEFT, which put the one data type a pick list must scan as a column on opposite edges in
   vertically adjacent rows. Quantity is data; it does not need a box. */
.products-qty-badge {
    flex: 0 0 auto;
    min-width: 34px;
    /* No max-width and no ellipsis: a truncated quantity is not degraded, it is wrong, and wrong
       in a way that looks plausible — "1234.5" rendering as "123…" at the head of the numeric
       rail. .products-group-body carries min-width: 0, so the group NAME absorbs the pressure
       instead. Text may ellipsise; data may not. */
    white-space: nowrap;
    text-align: right;
    color: var(--mud-palette-text-primary);
    font-weight: 600;
    font-size: 0.875rem;
    font-variant-numeric: tabular-nums;
}

.products-group-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

/* Weight before size: the group name outranks its lines at the same 14px. */
.products-group-name {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    line-height: 1.35;
    overflow-wrap: anywhere;
}

.products-group-meta {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 4px;
    font-size: 0.8125rem;
    line-height: 1.35;
    color: var(--mud-palette-text-secondary);
    font-variant-numeric: tabular-nums;
}

/* One icon size across the page — the tick mark and the group's kind icon read as one family. */
.products-group-meta-icon {
    font-size: 16px !important;
    color: var(--mud-palette-text-secondary);
}

/* "2 of 9 ready" — the section's own answer, on its own line beneath the meta. Outline while
   partial, solid once the whole section is ready: the fill is the reward, and it means the
   technician can stop reading the section entirely. Same 6px pill and same 13px as the readiness
   chips on the lines, so the header and its rows speak one vocabulary. */
/* Holds the check and the pill on one line beneath the meta. */
.products-group-readiness {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-top: 4px;
}

.products-group-pill {
    display: inline-block;
    padding: 1px 8px;
    border: 1px solid color-mix(in srgb, var(--mud-palette-success) 35%, var(--mud-palette-surface));
    border-radius: 6px;
    background: transparent;
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.38;
    color: var(--mud-palette-success);
    font-variant-numeric: tabular-nums;
}

.products-group-pill.full {
    background: var(--mud-palette-success);
    border-color: var(--mud-palette-success);
    color: var(--mud-palette-success-text, #fff);
}

/* "0 of 1 ready" in success green reads as partial progress when the honest answer is that
   nothing is ready at all. Same warning token the not-in-stock chips already use — no new
   colour, just the one that matches what the number says. */
.products-group-pill.none {
    border-color: color-mix(in srgb, var(--mud-palette-warning) 35%, var(--mud-palette-surface));
    color: var(--mud-palette-warning);
}

/* The collection line. Present in EVERY grouping and stated even when the answer is "none" —
   "no outstanding collections" is the fact that lets a technician skip a section, and leaving it
   unsaid makes an absence indistinguishable from a section that has not loaded. */
.products-group-collect {
    margin-top: 4px;
    font-size: 0.8125rem;
    line-height: 1.35;
    color: var(--mud-palette-text-secondary);
    font-variant-numeric: tabular-nums;
}

.products-group-collect.due {
    color: var(--mud-palette-primary);
    font-weight: 600;
}

.products-meta-dot {
    width: 3px;
    height: 3px;
    flex: 0 0 3px;
    border-radius: 50%;
    background: var(--mud-palette-text-disabled);
}

.products-chev {
    color: var(--mud-palette-text-secondary);
    transition: transform 180ms cubic-bezier(.4, 0, .2, 1);
    flex: 0 0 auto;
}

.products-chev.open {
    transform: rotate(180deg);
}

/* ---- Expanded lines ---- */
.products-lines {
    background: color-mix(in srgb, var(--mud-palette-tertiary) 50%, var(--mud-palette-surface));
    /* 16px right, matching the group header's own padding — the rest of the rail is reserved on
       .products-line-qty below. */
    padding: 0 16px 4px 0;
    animation: products-reveal 200ms cubic-bezier(.2, .8, .3, 1) both;
}

@keyframes products-reveal {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
}

/* No longer a control. With the checkbox gone the row asserts nothing and toggles nothing — it
   is a fact plus, where one applies, a state to act on. The one thing still tappable on a line
   is the "I already have this" button beneath it, which is a sibling. */
.products-line {
    appearance: none;
    width: 100%;
    display: flex;
    align-items: flex-start;
    gap: 4px;
    min-height: 56px;
    /* 25px against the wrap's 3px edge puts every title on one 28px rule whether or not its row
       is ranked — the accent must never shift the text it is accenting — and indents the lines
       from the 16px their section name starts at, which is the hierarchy the tick column used
       to provide by accident. */
    padding: 8px 0 8px 25px;
    border: 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    border-radius: 0;
    background: transparent;
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

/* The van list only — its rows have no .products-line-wrap around them, so the last row's rule
   has to be cleared here. The Required list's rows get theirs from the wrap. */
.products-lines .products-line:last-child {
    border-bottom: 0;
}

.products-line-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    padding-top: 12px;
}

/* 1.3 rather than 1.35 so a title that wraps to three lines coheres as one object. A real AS 1851
   compliance string wraps to 3+ lines, and at 1.35 the gap between its own lines matched the gap
   to the site name beneath it — the title stopped being a title and became the first three lines
   of a five-line paragraph. */
.products-line-title {
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--mud-palette-text-primary);
    line-height: 1.3;
    overflow-wrap: anywhere;
}

/* 8px opens the boundary between the title and its metadata; the date below then takes 2px, so
   site and date read as one metadata block rather than three evenly-spaced strangers. Net
   vertical cost per row: zero. */
.products-line-sub {
    margin-top: 8px;
    font-size: 0.8125rem;
    line-height: 1.35;
    color: var(--mud-palette-text-secondary);
    overflow-wrap: anywhere;
    font-variant-numeric: tabular-nums;
}

/* One weight step above .products-line-sub at the same size. The two were byte-identical while
   carrying different kinds of fact — where versus when — and Weight-Before-Size separates them
   without spending a pixel. Tabular so a column of dates has a straight left edge. */
.products-line-date {
    margin-top: 2px;
    font-size: 0.8125rem;
    font-weight: 500;
    line-height: 1.35;
    color: var(--mud-palette-text-secondary);
    font-variant-numeric: tabular-nums;
}

.products-line-qty {
    flex: 0 0 auto;
    min-width: 34px;
    padding: 12px 0 0 8px;
    text-align: right;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    font-variant-numeric: tabular-nums;
}

/* Reserves the group header's trailing chevron slot (24px icon + 8px gap) so a line quantity
   lands in the same column as the group total above it — without it the two sat 36px apart, which
   is the one data type a pick list must scan as a column failing to scan as a column. Scoped to
   the wrap so the van's read-only rows, which have no chevron, keep their own edge. */
.products-line-wrap .products-line-qty {
    margin-right: 32px;
}

/* ---- Focus ----
   These are the page's only controls and had no visible focus at all. */
.products-seg:focus-visible,
.products-scope-chip:focus-visible,
.products-refresh:focus-visible,
.products-group-open:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
    border-radius: 5px;
}

@media (prefers-reduced-motion: reduce) {

    .products-lines,
    .products-refresh-icon.spin {
        animation: none;
    }

    .products-chev {
        transition: none;
    }
}

/* ---- Dark ----
   Light-on-dark optically thins stems, and the roles most at risk are the smallest: 13px/400
   metadata in text-secondary on a near-black ground. DESIGN.md's remedy is a touch more leading,
   a touch more tracking and one weight step where the face needs it — this is that, applied only
   to the roles that need it rather than to the whole screen. */
.mud-theme-dark .products-line-sub,
.mud-theme-dark .products-line-date,
.mud-theme-dark .products-group-meta,
.mud-theme-dark .products-answer-sub,
.mud-theme-dark .products-van-restock-sub {
    font-weight: 500;
    letter-spacing: 0.01em;
    line-height: 1.4;
}

/* ---- Alerts ----
   CustomAlert, not MudAlert, so the page's error box stays locked to the app's status tokens. */
.products-alert {
    margin: 4px 0 8px;
}

/* ---- Empty / loading ---- */
/* Ruled ground like everything else. As a bordered surface slab it read as an oversized card in
   a list that no longer uses cards. */
.products-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 48px 16px;
    text-align: center;
}

.products-empty-icon {
    font-size: 48px;
    color: var(--mud-palette-text-disabled);
    margin-bottom: 12px;
}

/* 1.125rem, the same value .products-title uses — 16px exists nowhere in the documented scale,
   which jumps 14 → 18, so the file now has one heading size instead of three. */
.products-empty-title {
    font-weight: 600;
    font-size: 1.125rem;
    color: var(--mud-palette-text-primary);
    margin-bottom: 4px;
}

.products-empty-subtitle {
    font-size: 0.875rem;
    color: var(--mud-palette-text-secondary);
}

.products-loading-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 48px 16px;
    color: var(--mud-palette-text-secondary);
}

.products-loading-label {
    font-size: 0.875rem;
}

/* ---- Van tab ----
   The van list reuses products-line / -line-body / -line-title / -line-sub / -line-qty; only the
   pieces the Required list has no equivalent for are defined here. */

/* The summary row and the search field moved out of this panel — the first became the page's
   pinned answer block, the second its context row — so .products-van-controls and
   .products-van-summary went with them. The Van tab now wears the same shell as Required. */

/* The panel is an action, not an error. The shortfall count carries the severity; a fully
   error-bordered box made a routine top-up read as a fault. */
.products-van-restock {
    margin: 0 0 16px;
    padding: 16px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 5px;
    background: var(--mud-palette-surface);
}

/* The below-minimum section is a disclosure now, so it borrows the Required tab's group card and
   only needs a body. Padding matches the group header's so the list lines up under the count. */
.products-van-group {
    margin-bottom: 16px;
}

.products-van-restock-body {
    padding: 0 16px 16px;
    animation: products-reveal 200ms cubic-bezier(.2, .8, .3, 1) both;
}

.products-van-restock-title {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-error);
}

.products-van-restock-sub {
    margin: 4px 0 16px;
    font-size: 0.8125rem;
    color: var(--mud-palette-text-secondary);
}

/* Exactly what a restock will move, listed before the button that moves it. */
.products-van-shortfall {
    margin: 0 0 12px;
    padding: 0;
    list-style: none;
    border-top: 1px solid var(--mud-palette-lines-default);
}

.products-van-shortfall li {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 8px 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    font-size: 0.875rem;
}

.products-van-shortfall-name {
    flex: 1;
    min-width: 0;
    overflow-wrap: anywhere;
    color: var(--mud-palette-text-primary);
}

.products-van-shortfall-qty {
    flex: 0 0 auto;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--mud-palette-text-primary);
}

/* The incoming-transfer lines reuse the shortfall row's shape so a quantity reads the same
   wherever it appears on this tab. */
.products-van-shortfall-line {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 4px 0;
    font-size: 0.875rem;
}

/* A group whose lines use different units has no honest total to show. */
.products-qty-badge--mixed {
    color: var(--mud-palette-text-disabled);
}

/* A product that has fallen to or below the level the van should carry.

   Warning with a left edge, matching the Required tab's ranked rows exactly — same 3px accent,
   same token, same meaning: a thing to sort out, not a fault. It was error red on the title AND
   the quantity, which made a van with four low lines read as four failures and shouted louder
   than "Not in stock" does on the other tab, where nobody has the part at all.
   The title returns to normal text: the amber quantity is the datum that changed. */
.products-van-low {
    border-left-color: var(--mud-palette-warning) !important;
}

.products-van-low .products-line-qty {
    color: var(--mud-palette-warning);
}

/* Every van row reserves the edge so a low one is not 3px narrower than its neighbours — the
   accent must never move the text it accents. */
.products-list-scroll > .products-lines > div.products-line {
    border-left: 3px solid transparent;
}

.products-van-uom {
    margin-left: 4px;
    font-size: 0.8125rem;
    font-weight: 400;
    color: var(--mud-palette-text-secondary);
}

/* Van rows are read-only, so they carry no tick — and no tick means no 44px column to inset
   against. Restore the leading gutter the Required rows get from the tick. */
/* The van list is the only place .products-lines stands alone rather than inside a group card,
   so it has to bring its own card — otherwise it reads as an unbounded slab on the page ground. */
.products-list-scroll > .products-lines {
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 5px;
    padding: 0 12px 0 0;
    margin-top: 4px;
}

/* Van rows only. Scoped to the van's own standalone list rather than to `div.products-line`:
   the Required rows became divs too when the checkbox was removed, so the bare element selector
   started stripping their 56px height and their body's top padding. */
.products-list-scroll > .products-lines > div.products-line {
    min-height: 0;
    padding: 8px 0 8px 12px;
    cursor: default;
}

    .products-list-scroll > .products-lines > div.products-line .products-line-body,
    .products-list-scroll > .products-lines > div.products-line .products-line-qty {
        padding-top: 0;
    }

/* Material-style ripple press feedback (replaces the old background tint).
   Ripple spreads from the element centre on :active. */
.press {
    position: relative;
    overflow: hidden;
}

.press::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 14px;
    height: 14px;
    margin: -7px 0 0 -7px;
    border-radius: 999px;
    background: var(--mud-palette-primary);
    opacity: 0;
    transform: scale(0);
    pointer-events: none;
}

.press:active::after {
    animation: press-ripple 580ms cubic-bezier(0, 0, 0.2, 1);
}

@keyframes press-ripple {
    0% {
        transform: scale(0);
        opacity: 0.32;
    }

    100% {
        transform: scale(14);
        opacity: 0;
    }
}

/* ============================================================
   Crisp Outline – reusable selection treatment
   Add .crisp-select to any selectable card/row, then toggle the
   .selected class to mark it chosen: a 2px focus ring snaps in over a
   subtle lift and a small tick badge pops into the top-right corner.
   The badge uses ::before so it can coexist with .press (which owns
   ::after for its ripple). Accent follows --mud-palette-primary.
   ============================================================ */
.crisp-select {
    position: relative;
    transition: box-shadow 180ms cubic-bezier(0.4, 0, 0.2, 1),
                transform 180ms cubic-bezier(0.4, 0, 0.2, 1),
                border-color 180ms cubic-bezier(0.4, 0, 0.2, 1);
}

.crisp-select.selected {
    transform: translateY(-2px);
    border-color: transparent !important;
    box-shadow: 0 0 0 2px var(--mud-palette-primary),
                0 3px 6px rgba(0, 0, 0, 0.10),
                0 1px 3px rgba(0, 0, 0, 0.06);
}

.crisp-select.selected::before {
    content: '';
    position: absolute;
    top: 10px;
    right: 10px;
    width: 20px;
    height: 20px;
    border-radius: 999px;
    background-color: var(--mud-palette-primary);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 13l4 4L19 7' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: center;
    background-size: 12px 12px;
    pointer-events: none;
    z-index: 1;
    animation: crisp-pop 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes crisp-pop {
    0% {
        transform: scale(0);
        opacity: 0;
    }

    100% {
        transform: scale(1);
        opacity: 1;
    }
}

/* ============================================================
   Mobile Detail – Utility classes
   ============================================================ */
.md-loading-row {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 16px;
}

.md-text-muted {
    color: var(--mud-palette-text-secondary);
}

.md-hidden-input {
    display: none !important;
}

.md-tag-row {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.md-contact-item {
    background: var(--mud-palette-background);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 5px;
    padding: 12px 16px;
    margin-bottom: 8px;
}
.md-contact-item-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
}
.md-contact-item-icon {
    color: var(--mud-palette-text-secondary);
    flex-shrink: 0;
}
.md-contact-item-name {
    font-weight: 500;
}
.md-contact-link {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--mud-palette-primary);
    text-decoration: none;
    font-size: 0.875rem;
    margin-top: 4px;
}
.md-contact-card {
    background: var(--mud-palette-background);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 5px;
    padding: 12px 16px;
    margin-bottom: 8px;
}
.md-contact-card-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 10px;
}
.md-contact-card-identity {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
}
.md-contact-card-title-group {
    min-width: 0;
}
.md-contact-card-name {
    font-weight: 600;
}
.md-contact-card-role {
    color: var(--mud-palette-text-secondary);
}
.md-contact-card-details {
    display: grid;
    gap: 8px;
}
.md-contact-card-detail {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.md-contact-card-label {
    color: var(--mud-palette-text-secondary);
}
.md-contact-card-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
}
.md-contact-card-secondary-action {
    margin-top: 6px;
    padding-left: 0;
}

.md-scroll-region {
    height: calc(100dvh - 250px);
    overflow-y: auto;
}

/* ============================================================
   Mobile Dialog – Shared Styles
   ============================================================ */
.md-dialog-header {
    /* Fixed dark header, identical in light and dark mode (not the theme primary,
       which is a bright blue that glares against dark surfaces). */
    background: #0B1220;
    color: #ffffff;
    padding: 12px 16px;
    /* Inset absorbs the 12px — see .md-header. */
    padding-top: max(12px, var(--tm-safe-area-top, 0px));
    display: flex;
    align-items: center;
    gap: 8px;
}

.md-dialog-body {
    flex: 1;
    overflow-y: auto;
    padding: 0px;
    padding-bottom: max(16px, env(safe-area-inset-bottom));
    background: var(--mud-palette-tertiary);
}

.md-dialog-subtitle {
    font-size: 0.875rem;
    opacity: 0.9;
    margin-top: 2px;
}

/* ============================================================
   Job Detail — mobile-job-times redesign
   Scoped under .job-detail-dialog so it does not leak to other
   detail pages. Applies to the InProgress body.
   ============================================================ */

/* Timer Hero region (hero card + totals wrapper) */
.job-detail-dialog .md-body-hero {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 4px 16px 0;
}

/* Timer Hero card */
.job-detail-dialog .th-wrap { width: 100%; }
.job-detail-dialog .timer-hero {
    background: var(--mud-palette-surface);
    border-radius: 5px;
    padding: 14px 16px 12px;
    border: 1px solid var(--mud-palette-lines-default);
    box-shadow: 0 1px 2px rgba(15, 23, 43, 0.05);
    position: relative;
    overflow: hidden;
}
.job-detail-dialog .timer-hero.is-running {
    border-color: rgba(22, 163, 74, 0.35);
    background:
        radial-gradient(360px 100px at 0% 0%, rgba(22,163,74,0.06), transparent 60%),
        var(--mud-palette-surface);
}
.job-detail-dialog .timer-hero.is-running.is-travel {
    border-color: rgba(30, 85, 196, 0.35);
    background:
        radial-gradient(360px 100px at 0% 0%, rgba(30,85,196,0.06), transparent 60%),
        var(--mud-palette-surface);
}
.job-detail-dialog .timer-hero .th-head {
    display: flex; justify-content: space-between; align-items: center;
    gap: 8px;
    margin-bottom: 4px;
}
.job-detail-dialog .timer-hero .th-label {
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.10em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
    display: inline-flex; align-items: center; gap: 6px;
}
.job-detail-dialog .timer-hero.is-running .th-label { color: #16a34a; }
.job-detail-dialog .timer-hero.is-running.is-travel .th-label { color: #1E55C4; }
.job-detail-dialog .timer-hero .th-live {
    position: relative;
    width: 7px; height: 7px; border-radius: 50%;
    background: #16a34a;
}
/* Radar ping as a composited pseudo-element (transform + opacity) rather than an
   animated box-shadow spread. box-shadow isn't GPU-composited, so the old version
   forced a full 60fps repaint the entire time a timer was running (all day on the
   Job screen) — the standout battery drain on iOS. This ring animates on the
   compositor, so it's near-free. */
.job-detail-dialog .timer-hero .th-live::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: rgba(22,163,74,0.55);
    transform: scale(1);
    animation: jdh-ping-green 1.6s ease-out infinite;
    will-change: transform, opacity;
    pointer-events: none;
}
.job-detail-dialog .timer-hero .th-live.travel {
    background: #1E55C4;
}
.job-detail-dialog .timer-hero .th-live.travel::after {
    background: rgba(30,85,196,0.55);
    animation-name: jdh-ping-blue;
}
@keyframes jdh-ping-green {
    0%   { transform: scale(1);   opacity: 0.9; }
    100% { transform: scale(2.8); opacity: 0; }
}
@keyframes jdh-ping-blue {
    0%   { transform: scale(1);   opacity: 0.9; }
    100% { transform: scale(2.8); opacity: 0; }
}
.job-detail-dialog .timer-hero .th-who {
    font-size: 0.75rem;
    color: var(--mud-palette-text-secondary);
    display: inline-flex; align-items: center; gap: 6px;
    max-width: 55%;
    overflow: hidden;
}
.job-detail-dialog .timer-hero .th-who-name {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.job-detail-dialog .timer-hero .th-avatar {
    width: 22px; height: 22px; border-radius: 50%;
    background: rgba(21, 93, 252, 0.12);
    color: var(--mud-palette-primary);
    font-size: 0.65rem;
    font-weight: 700;
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0;
}
.job-detail-dialog .timer-hero .th-time {
    font-size: 2.6rem;
    font-weight: 500;
    letter-spacing: -0.02em;
    color: var(--mud-palette-text-primary);
    font-variant-numeric: tabular-nums;
    line-height: 1.05;
    margin: 4px 0 2px;
}
.job-detail-dialog .timer-hero .th-time.th-time-stopped { color: var(--mud-palette-text-disabled); }
.job-detail-dialog .timer-hero .th-seconds {
    font-size: 1.55rem;
    color: var(--mud-palette-text-disabled);
    font-weight: 400;
}
.job-detail-dialog .timer-hero .th-sub {
    font-size: 0.78rem;
    color: var(--mud-palette-text-secondary);
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 22px;
}
.job-detail-dialog .timer-hero .th-pill {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 2px 8px 2px 6px; border-radius: 999px;
    font-size: 0.65rem; font-weight: 700;
    letter-spacing: 0.04em; text-transform: uppercase;
}
.job-detail-dialog .timer-hero .th-pill.onsite { background: #dcfce7; color: #166534; }
.job-detail-dialog .timer-hero .th-pill.travel { background: #eff6ff; color: #1e55c4; }
.job-detail-dialog .timer-hero .th-pill-icon .mud-icon-root { font-size: 0.8rem !important; }
.job-detail-dialog .timer-hero .th-since { color: var(--mud-palette-text-secondary); }
.job-detail-dialog .timer-hero .th-stopped-note { color: var(--mud-palette-text-disabled); }
.job-detail-dialog .timer-hero .th-actions {
    display: flex;
    gap: 8px;
    margin-top: 12px;
}
.job-detail-dialog .timer-hero .th-btn {
    flex: 1;
    border-radius: 5px;
    font-weight: 600;
    letter-spacing: 0.02em;
}
.job-detail-dialog .timer-hero .th-btn-success.mud-button-filled {
    background: #16a34a !important;
    color: #fff !important;
}
.job-detail-dialog .timer-hero .th-btn-success.mud-button-filled:hover {
    background: #15803d !important;
}

/* Totals bar */
.job-detail-dialog .job-totals {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    gap: 1px;
    background: var(--mud-palette-lines-default);
    border-radius: 5px;
    overflow: hidden;
    border: 1px solid var(--mud-palette-lines-default);
}
.job-detail-dialog .job-totals .jt-cell {
    background: var(--mud-palette-surface);
    padding: 10px 12px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.job-detail-dialog .job-totals .jt-label {
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
    display: inline-flex; align-items: center; gap: 5px;
}
.job-detail-dialog .job-totals .jt-label-icon .mud-icon-root { font-size: 0.82rem !important; }
.job-detail-dialog .job-totals .jt-swatch {
    width: 8px; height: 8px; border-radius: 2px;
    display: inline-block;
}
.job-detail-dialog .job-totals .jt-swatch.onsite { background: #16a34a; }
.job-detail-dialog .job-totals .jt-swatch.travel { background: #1E55C4; }
.job-detail-dialog .job-totals .jt-value {
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--mud-palette-text-primary);
    letter-spacing: -0.01em;
    font-variant-numeric: tabular-nums;
}
.job-detail-dialog .job-totals .jt-cell.emphasis .jt-value { color: var(--mud-palette-primary); }

/* Compact (sticky collapsed) hero — triggered by JS scroll listener toggling
   .is-compact on the `.md-body-hero` wrapper. */
.job-detail-dialog .md-body-hero {
    transition: padding 0.4s cubic-bezier(0.4, 0, 0.2, 1), gap 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.job-detail-dialog .th-compact-row { display: none; }

.job-detail-dialog .md-body-hero.is-compact {
    padding: 6px 12px 6px;
    gap: 0;
    cursor: pointer;
}
.job-detail-dialog .md-body-hero.is-compact .timer-hero,
.job-detail-dialog .md-body-hero.is-compact .job-totals {
    display: none;
}
.job-detail-dialog .md-body-hero.is-compact .th-compact-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 999px;
    box-shadow: 0 1px 2px rgba(15, 23, 43, 0.05);
    font-variant-numeric: tabular-nums;
    min-height: 36px;
}
.job-detail-dialog .md-body-hero.is-compact .th-compact-row.is-travel {
    border-color: rgba(30, 85, 196, 0.35);
}
.job-detail-dialog .md-body-hero.is-compact .th-compact-row:not(.is-travel) {
    border-color: rgba(22, 163, 74, 0.35);
}
.job-detail-dialog .th-compact-row .thc-dot {
    position: relative;
    width: 8px; height: 8px; border-radius: 50%;
    flex-shrink: 0;
}
/* Composited ping (see .th-live above) — replaces the box-shadow pulse. */
.job-detail-dialog .th-compact-row .thc-dot::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    transform: scale(1);
    animation: jdh-ping-green 1.6s ease-out infinite;
    will-change: transform, opacity;
    pointer-events: none;
}
.job-detail-dialog .th-compact-row .thc-dot.onsite { background: #16a34a; }
.job-detail-dialog .th-compact-row .thc-dot.onsite::after { background: rgba(22,163,74,0.55); }
.job-detail-dialog .th-compact-row .thc-dot.travel { background: #1E55C4; }
.job-detail-dialog .th-compact-row .thc-dot.travel::after {
    background: rgba(30,85,196,0.55);
    animation-name: jdh-ping-blue;
}
.job-detail-dialog .th-compact-row .thc-time {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    letter-spacing: -0.01em;
}
.job-detail-dialog .th-compact-row .thc-pill {
    display: inline-flex; align-items: center;
    padding: 2px 8px; border-radius: 999px;
    font-size: 0.6rem; font-weight: 700;
    letter-spacing: 0.04em; text-transform: uppercase;
    flex-shrink: 0;
}
.job-detail-dialog .th-compact-row .thc-pill.onsite { background: #dcfce7; color: #166534; }
.job-detail-dialog .th-compact-row .thc-pill.travel { background: #eff6ff; color: #1e55c4; }
.job-detail-dialog .th-compact-row .thc-sep {
    width: 3px; height: 3px; border-radius: 50%;
    background: var(--mud-palette-text-disabled);
    flex-shrink: 0;
}
.job-detail-dialog .th-compact-row .thc-total {
    font-size: 0.75rem;
    color: var(--mud-palette-text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.job-detail-dialog .th-compact-row .thc-total strong {
    color: var(--mud-palette-text-primary);
    font-weight: 700;
    margin-left: 2px;
}
.job-detail-dialog .th-compact-row .thc-chevron {
    margin-left: auto;
    color: var(--mud-palette-text-disabled);
    flex-shrink: 0;
}
.job-detail-dialog .th-compact-row .thc-chevron .mud-icon-root {
    font-size: 1.1rem !important;
}

/* Stopped-state compact row: two action buttons inline */
.job-detail-dialog .md-body-hero.is-compact .th-compact-row--stopped {
    padding: 4px;
    gap: 6px;
    background: transparent;
    border: 0;
    box-shadow: none;
    border-radius: 5px;
}
.job-detail-dialog .th-compact-row--stopped .thc-btn.mud-button-root {
    flex: 1 1 auto;
    min-height: 36px;
    padding: 6px 12px;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    border-radius: 5px;
}
.job-detail-dialog .th-compact-row--stopped .thc-btn .mud-button-icon-start .mud-icon-root {
    font-size: 1rem !important;
}
.job-detail-dialog .th-compact-row--stopped .th-btn-success.mud-button-filled {
    background: #16a34a !important;
    color: #fff !important;
}
.job-detail-dialog .th-compact-row--stopped .th-btn-success.mud-button-filled:hover {
    background: #15803d !important;
}
.job-detail-dialog .th-compact-row--stopped .th-btn-success .mud-button-icon-start .mud-icon-root {
    color: #fff !important;
}

/* Job Times section (timeline) */
.job-detail-dialog .job-times-section { margin-top: 12px; }
.job-detail-dialog .job-times-section-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 4px 4px 6px;
}
.job-detail-dialog .job-times-section-header .jts-title {
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    margin: 0;
    display: inline-flex; align-items: center; gap: 7px;
}
.job-detail-dialog .job-times-section-header .jts-title-icon .mud-icon-root { font-size: 1rem !important; color: var(--mud-palette-text-secondary); }
.job-detail-dialog .job-times-paper {
    padding: 0 !important;
    overflow: hidden;
}
.job-detail-dialog .jt-day-divider {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 14px;
    background: var(--mud-palette-background-gray);
    border-bottom: 1px solid var(--mud-palette-lines-default);
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    color: var(--mud-palette-text-secondary);
    min-height: 28px;
}
.job-detail-dialog .jt-day-divider .jt-day-total {
    margin-left: auto;
    color: var(--mud-palette-text-primary);
    font-variant-numeric: tabular-nums;
}
.job-detail-dialog .jt-entry {
    display: grid;
    grid-template-columns: 40px 1fr auto;
    gap: 12px;
    padding: 12px 14px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    align-items: center;
    cursor: pointer;
    transition: background 0.15s ease;
    position: relative;
}
.job-detail-dialog .jt-entry:last-child { border-bottom: 0; }
.job-detail-dialog .jt-entry:hover { background: var(--mud-palette-action-default-hover); }
.job-detail-dialog .jt-entry .jt-rail {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    align-items: center;
    position: relative;
    min-height: 28px;
}
.job-detail-dialog .jt-entry .jt-dot {
    width: 28px; height: 28px;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0;
    position: relative;
    z-index: 1;
}
.job-detail-dialog .jt-entry .jt-dot .mud-icon-root { font-size: 0.95rem !important; }
.job-detail-dialog .jt-entry .jt-dot.onsite {
    background: #dcfce7;
    color: #16a34a;
    box-shadow: 0 0 0 4px rgba(22,163,74,0.06);
}
.job-detail-dialog .jt-entry .jt-dot.travel {
    background: #eff6ff;
    color: #1E55C4;
    box-shadow: 0 0 0 4px rgba(30,85,196,0.06);
}
.job-detail-dialog .jt-entry .jt-line {
    position: absolute;
    top: 28px; bottom: -12px;
    width: 2px;
    background: var(--mud-palette-lines-default);
    border-radius: 1px;
}
.job-detail-dialog .jt-entry .jt-body .jt-type-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 2px;
}
.job-detail-dialog .jt-entry .jt-type {
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}
.job-detail-dialog .jt-entry .jt-type.onsite { color: #16a34a; }
.job-detail-dialog .jt-entry .jt-type.travel { color: #1E55C4; }
.job-detail-dialog .jt-entry .jt-tech {
    font-size: 0.82rem;
    font-weight: 500;
    color: var(--mud-palette-text-primary);
}
.job-detail-dialog .jt-entry .jt-times {
    font-size: 0.78rem;
    color: var(--mud-palette-text-secondary);
    font-variant-numeric: tabular-nums;
    display: inline-flex; align-items: center; gap: 6px;
}
.job-detail-dialog .jt-entry .jt-arrow { color: var(--mud-palette-text-disabled); }
.job-detail-dialog .jt-entry .jt-meta {
    text-align: right;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
}
.job-detail-dialog .jt-entry .jt-dur {
    font-size: 0.98rem;
    font-weight: 700;
    color: var(--mud-palette-text-primary);
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
    line-height: 1;
}
.job-detail-dialog .jt-entry .jt-dur-sub {
    font-size: 0.62rem;
    color: var(--mud-palette-text-disabled);
    letter-spacing: 0.04em;
    font-weight: 600;
}

/* Slim card (Products / Tasks) — header pill */
.job-detail-dialog .card-slim .cs-head {
    display: flex; align-items: center; gap: 8px;
}
.job-detail-dialog .card-slim .cs-count {
    font-size: 0.8125rem;
    color: var(--mud-palette-text-secondary);
    background: var(--mud-palette-background-gray);
    padding: 2px 8px;
    border-radius: 8px;
    font-weight: 600;
    margin-left: 6px;
}
.cs-empty {
    display: flex; align-items: center; gap: 8px;
    color: var(--mud-palette-text-secondary); font-size: 0.875rem;
    padding: 2px 0;
}
.cs-empty .mud-icon-root { color: var(--mud-palette-text-disabled); }

/* Sub-heading separating "added" products from the required list */
.cs-subhead {
    margin-top: 10px; padding-top: 8px;
    border-top: 1px solid var(--mud-palette-lines-default);
    font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
}

/* Product rows */
.job-detail-dialog .product-row-slim {
    display: flex; align-items: center; gap: 10px;
    padding: 8px 0;
    border-top: 1px solid var(--mud-palette-lines-default);
}
.job-detail-dialog .product-row-slim:first-of-type { border-top: 0; padding-top: 4px; }
.job-detail-dialog .product-row-slim .prs-check { flex-shrink: 0; padding: 0; margin: 0; }
.job-detail-dialog .product-row-slim .prs-thumb {
    width: 32px; height: 32px; border-radius: 3px;
    background: var(--mud-palette-background-gray);
    border: 1px solid var(--mud-palette-lines-default);
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--mud-palette-text-secondary);
    flex-shrink: 0;
}
.job-detail-dialog .product-row-slim .prs-thumb .mud-icon-root { font-size: 1rem !important; }
.job-detail-dialog .product-row-slim .prs-info { flex: 1; min-width: 0; }
.job-detail-dialog .product-row-slim .prs-name {
    font-size: 0.88rem;
    color: var(--mud-palette-text-primary);
    font-weight: 500;
    line-height: 1.25;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.job-detail-dialog .product-row-slim .prs-code {
    font-size: 0.7rem;
    color: var(--mud-palette-text-disabled);
    margin-top: 1px;
}
.job-detail-dialog .product-row-slim.is-used .prs-code { color: #16a34a; font-weight: 600; }
.job-detail-dialog .product-row-slim .prs-qty {
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    color: var(--mud-palette-text-primary);
    background: var(--mud-palette-background-gray);
    padding: 2px 8px;
    border-radius: 5px;
    font-weight: 600;
    flex-shrink: 0;
}

/* Next-action hint. Replaces the old `.pulse-hint` on this card, which was host-divergent:
   the live animation lived in AI.Client/wwwroot/css/utilities.css (browser host only), while
   the MAUI host got the commented-out stub in Job.razor and nothing happened. This lives in
   the shared sheet, so both hosts agree. Static ring rather than an infinite pulse — the job
   screen is stared at for an hour at a time. */
.trs-hint-ring {
    box-shadow: var(--tm-card-shadow), 0 0 0 2px var(--mud-palette-primary);
}

/* Task rows */
.job-detail-dialog .task-row-slim {
    padding: 10px 0;
    border-top: 1px solid var(--mud-palette-lines-default);
}
.job-detail-dialog .task-row-slim:first-of-type { border-top: 0; padding-top: 4px; }
.job-detail-dialog .task-row-slim .trs-header {
    display: flex; align-items: flex-start; gap: 10px;
}
/* (.trs-check rules removed — the row renders no checkbox; the done state is carried by
   .job-detail-task-result. They were also the last hand-picked green on this card.) */
.job-detail-dialog .task-row-slim .trs-body { flex: 1; min-width: 0; }
.job-detail-dialog .task-row-slim .trs-label {
    font-size: 0.875rem;
    color: var(--mud-palette-text-secondary);
    font-weight: 400;
    line-height: 1.35;
    /* Clamped: an unclamped long description pushed the action row below the fold, so the
       primary action sat at a different height on every task. */
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
/* When there is no linked asset the description is the row's subject, so it takes the lead role. */
.job-detail-dialog .task-row-slim .trs-label.trs-lead {
    color: var(--mud-palette-text-primary);
    font-weight: 600;
}
.job-detail-dialog .task-row-slim.done .trs-asset,
.job-detail-dialog .task-row-slim.done .trs-label {
    color: var(--mud-palette-text-disabled);
    text-decoration: line-through;
}
/* (.trs-meta removed with the old third-line asset caption — the asset now leads the row
   as .trs-asset. .trs-id went with it; the task ID lives in Task Details' subtitle.) */
.job-detail-dialog .task-row-slim .job-detail-task-actions { margin-top: 8px; }
.job-detail-dialog .task-row-slim .job-detail-task-result { margin-top: 6px; }

/* Complete dock — sticky bottom. Reserves room so content clears the dock; the
   .job-body-bottom-spacer at the end of the body adds the extra scroll-past room. */
.job-detail-dialog .md-body { padding-bottom: 210px; }

/* Fixed rounded "lip" at the top of the scroll area. A sticky pseudo-element pinned to
   the top of the scroller paints the page (navy) colour into two concave corners, so the
   full-bleed cards appear to round off and slip underneath a fixed curved edge as they
   scroll up under the timer bar. Radius matches the cards' 14px custom-border. */
.job-detail-dialog .md-body::before {
    content: "";
    position: sticky;
    top: 0;
    z-index: 4;
    display: block;
    height: 14px;
    margin-bottom: -14px; /* overlay the first 14px of content, don't add layout space */
    pointer-events: none;
    background:
        radial-gradient(14px at 14px 14px, #0000 98%, var(--mud-palette-primary) 100%) top left / 14px 14px no-repeat,
        radial-gradient(14px at 0 14px, #0000 98%, var(--mud-palette-primary) 100%) top right / 14px 14px no-repeat;
        /* radial-gradient(14px at 14px 14px, #0000 98%, var(--mud-palette-background-gray) 100%) top left / 14px 14px no-repeat,
        radial-gradient(14px at 0 14px, #0000 98%, var(--mud-palette-background-gray) 100%) top right / 14px 14px no-repeat; */
}
.job-detail-dialog .complete-dock {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    /* No bottom gap — the white dock-inner panel itself extends to the screen edge
       (its bottom safe-area padding is added below) so there's no background sliver under it. */
    padding: 14px 0px 0px;
    /* Fade into the same colour the job body uses (--mud-palette-primary), not tertiary —
       tertiary is light blue (#d8e2f8) in light theme and leaked a blue sliver around the dock. */
    
    pointer-events: none;
    z-index: 5;
}
.job-detail-dialog .complete-dock .dock-inner {
    background: var(--mud-palette-surface);
    border-radius: 0px;
    padding: 8px 8px 8px 14px;
    box-shadow: 0 8px 24px -8px rgba(15,23,43,0.18), 0 2px 6px rgba(15,23,43,0.06);
    border: 1px solid var(--mud-palette-lines-default);
    display: flex;
    align-items: center;
    gap: 10px;
    pointer-events: auto;
}
.job-detail-dialog .complete-dock .dock-summary { flex: 1; min-width: 0; }
.job-detail-dialog .complete-dock .dock-lbl {
    font-size: 0.6rem;
    letter-spacing: 0.08em;
    color: var(--mud-palette-text-secondary);
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.job-detail-dialog .complete-dock .dock-val {
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--mud-palette-text-primary);
    letter-spacing: -0.005em;
    display: flex; align-items: center; gap: 8px;
}
.job-detail-dialog .complete-dock .dock-live {
    font-size: 0.58rem;
    color: #16a34a;
    font-weight: 700;
    letter-spacing: 0.1em;
}
.job-detail-dialog .complete-dock .dock-btn {
    border-radius: 5px !important;
    padding: 10px 16px !important;
    font-weight: 700;
    letter-spacing: 0.04em;
}
/* Keep dock button labels on a single line — never wrap to two lines */
.job-detail-dialog .complete-dock .dock-btn,
.job-detail-dialog .complete-dock .dock-btn--start,
.job-detail-dialog .complete-dock .dock-btn .mud-button-label,
.job-detail-dialog .complete-dock .dock-btn--start .mud-button-label {
    white-space: nowrap;
}
/* Pending start dock — two equal-width Start Travel / Start Onsite buttons */
.job-detail-dialog .complete-dock .dock-inner--start { padding: 8px; gap: 8px; }
.job-detail-dialog .complete-dock .dock-btn--start {
    flex: 1; min-width: 0;
    padding: 10px 8px !important;
    letter-spacing: 0.01em;
}
/* Tighten the icon gap so the single-line label has more room */
.job-detail-dialog .complete-dock .dock-btn--start .mud-button-label { gap: 6px; }
.job-detail-dialog .complete-dock .dock-btn--start .mud-icon-root { margin-right: 4px; }
/* Offline hint shown above the disabled Start buttons — explains why starting is unavailable */
.job-detail-dialog .complete-dock .dock-offline-hint {
    pointer-events: auto;
    display: flex; align-items: center; justify-content: center;
    gap: 4px;
    margin-bottom: 8px;
    padding: 8px 12px;
    border-radius: 5px;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    box-shadow: 0 8px 24px -8px rgba(15,23,43,0.18), 0 2px 6px rgba(15,23,43,0.06);
    font-size: 0.78rem; font-weight: 600;
    color: var(--mud-palette-text-secondary);
    text-align: center;
}
.job-detail-dialog .complete-dock .dock-offline-hint .mud-icon-root { color: var(--mud-palette-warning); }

/* Complete dock — Job info is a fixed-width secondary action; Complete Job is the
   dominant primary CTA and grows to fill the remaining space. */
.job-detail-dialog .complete-dock .dock-inner--complete { padding: 8px; }
/* Extend every dock variant's white panel down to the bottom screen edge while keeping
   its buttons above the home-indicator safe area. Placed after the variant rules so the
   bottom override wins regardless of which variant is active. */
.job-detail-dialog .complete-dock .dock-inner {
    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
}
.job-detail-dialog .complete-dock .dock-inner--complete > .dock-btn-pulse-wrap { flex: 1; min-width: 0; }
.job-detail-dialog .complete-dock .dock-inner--complete .dock-btn-pulse-wrap .dock-btn { width: 100%; }
.job-detail-dialog .complete-dock .dock-inner--complete > .dock-btn--info {
    flex: 0 0 auto;    
    padding-left: 10px !important;
    padding-right: 10px !important;
}
/* Overflow (⋮) menu — compact fixed-size secondary action left of Complete Job, holding
   alternate completion outcomes (Can't complete job). Matches the asset-filters icon button. */
.job-detail-dialog .complete-dock .dock-more-btn {
    flex: 0 0 auto;
    width: 48px;
    min-width: 48px;
    height: 44px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 5px;
    color: var(--mud-palette-text-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
}
.job-detail-dialog .complete-dock .dock-more-btn .mud-icon-button {
    width: 100%;
    height: 100%;
    border-radius: 5px;
    color: inherit;
}
/* Dock ⋮ menu — finger-friendly touch targets. The menu renders in a body-level popover, so this
   is intentionally NOT scoped under .job-detail-dialog. */
.dock-more-menu .mud-list-item {
    min-height: 52px;
    padding-top: 12px;
    padding-bottom: 12px;
    padding-left: 20px;
    padding-right: 24px;
}
.dock-more-menu .mud-list-item-icon {
    min-width: 40px;
}

/* Lift the FAB so it doesn't overlap the sticky dock */
.job-detail-dialog .fab-menu,
.job-detail-dialog .floating-action-menu,
.job-detail-dialog .mud-fab-container {
    bottom: calc(env(safe-area-inset-bottom, 0px) + 88px) !important;
}

/* Dock "Job info" button (focused in-progress mode) — replaces the LOGGED summary on the left */
.job-detail-dialog .complete-dock .dock-jobinfo {
    flex: 1; min-width: 0;
    display: flex; align-items: center; gap: 10px;
    background: transparent; border: 0; padding: 4px 6px;
    text-align: left; cursor: pointer; color: inherit;
    -webkit-tap-highlight-color: transparent;
}
.job-detail-dialog .complete-dock .dock-jobinfo:active { opacity: 0.6; }
.job-detail-dialog .complete-dock .dock-jobinfo-icon { color: var(--mud-palette-primary); display: flex; flex-shrink: 0; }
.job-detail-dialog .complete-dock .dock-jobinfo-text { min-width: 0; display: flex; flex-direction: column; }
.job-detail-dialog .complete-dock .dock-jobinfo-title {
    font-size: 0.85rem; font-weight: 700; line-height: 1.1;
    color: var(--mud-palette-text-primary);
}
.job-detail-dialog .complete-dock .dock-jobinfo-sub {
    font-size: 0.6rem; letter-spacing: 0.06em; font-weight: 700;
    color: var(--mud-palette-text-secondary);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Focused-mode "Job info" bottom sheet — in-page slide-up overlay, mirrors MobileBottomSheet */
.job-detail-dialog .ji-sheet-backdrop {
    position: absolute; inset: 0;
    background: rgba(15,23,43,0.45);
    opacity: 0; pointer-events: none;
    transition: opacity 220ms cubic-bezier(.4,0,.2,1);
    /* Above the FAB (z-index 1200/1201) so the FAB sits behind the sheet overlay */
    z-index: 1250;
}
.job-detail-dialog .ji-sheet-backdrop.open { opacity: 1; pointer-events: auto; }
.job-detail-dialog .ji-sheet {
    position: absolute; left: 0; right: 0; bottom: 0;
    background: var(--mud-palette-surface);
    border-radius: 5px 5px 0 0;
    max-height: calc(85vh + var(--tm-safe-area-bottom, 0px));
    display: flex; flex-direction: column; overflow: hidden;
    z-index: 1251;
    transform: translateY(100%);
    transition: transform 220ms cubic-bezier(.4,0,.2,1);
    box-shadow: 0 -8px 28px -8px rgba(15,23,43,0.28);
}
.job-detail-dialog .ji-sheet.open { transform: translateY(0); }
.job-detail-dialog .ji-sheet-drag {
    flex-shrink: 0; touch-action: none; cursor: grab;
    -webkit-tap-highlight-color: transparent; user-select: none;
}
.job-detail-dialog .ji-sheet-handle {
    width: 36px; height: 4px;
    background: var(--mud-palette-lines-default);
    border-radius: 2px; margin: 8px auto 4px;
}
.job-detail-dialog .ji-sheet-head {
    display: flex; align-items: center; gap: 8px;
    padding: 4px 16px 8px;
}
.job-detail-dialog .ji-sheet-title {
    flex: 1; font-weight: 600; font-size: 1.125rem; line-height: 1.3;
}
.job-detail-dialog .ji-sheet-content {
    flex: 1 1 auto; min-height: 0;
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    padding: 0 16px calc(var(--tm-safe-area-bottom, env(safe-area-inset-bottom, 0px)) + 16px);
}
/* The job-info sheet reuses the inline page sections, several of which carry [data-reveal]. That
   reveal-on-scroll is driven by an IntersectionObserver bound to the MAIN page scroller (scroll-reveal.js),
   not this sheet's own scroller — so inside the sheet those cards never intersect and stay at opacity:0,
   leaving empty gaps (e.g. Job Details, Property Contacts). Force them fully visible here. */
.job-detail-dialog .ji-sheet-content [data-reveal] {
    opacity: 1;
    transform: none;
    transition: none;
}

/* Primary-job recorded times surfaced at the bottom of the "Combined jobs" sheet. */
.job-detail-dialog .combined-sheet-times {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--mud-palette-lines-default);
}
.job-detail-dialog .combined-sheet-times-label {
    font-size: 0.75rem; font-weight: 600; letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
    margin-bottom: 8px;
}

/* Global safe-area padding for MudBlazor native <DialogActions> on mobile.
   --tm-safe-area-bottom is populated by safeArea.js via the MAUI host. */
.mud-dialog-container .mud-dialog.mud-dialog-fullscreen .mud-dialog-actions {
    padding-bottom: calc(8px + var(--tm-safe-area-bottom, env(safe-area-inset-bottom, 0px)));
}

/* Square off the dialog CONTENT on fullscreen dialogs.
   MudBlazor gives .mud-dialog-content top-left/top-right radii of
   --mud-default-borderradius, and only cancels them when a .mud-dialog-title
   precedes it. Our mobile dialogs run NoHeader = true and draw their own
   MobileDialogHeader inside the content, so the reset never fires: the content
   box clips that dark header at 8px (Theme.cs DefaultBorderRadius), leaving a
   light notch in each top corner.

   NO `>` HERE. This rule used to read `.mud-dialog-fullscreen > .mud-dialog-content`
   and therefore never matched anything: MudBlazor wraps the content in
   `.mud-focus-trap > .mud-focus-trap-child-container`, so .mud-dialog-content is a
   GRANDCHILD (verified against the live DOM over CDP — the content still computed
   `border-radius: 8px 8px 0 0; overflow: hidden`). Keep it a descendant selector. */
.mud-dialog-container .mud-dialog.mud-dialog-fullscreen .mud-dialog-content {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

/* Square the PAPER too. MudBlazor.min.css declares `.mud-dialog-fullscreen{border-radius:0}`
   and then, ~8KB LATER in the same file, `.mud-paper{border-radius:var(--mud-default-borderradius)}`.
   Same specificity (0,1,0), so the later .mud-paper wins and the fullscreen paper is re-rounded
   to 8px; `.mud-dialog-fullscreen` also sets overflow-y:hidden, so it clips as well. That is a
   second, independent source of the same corner notch — the content clip above is the one that
   was actually visible (the notch showed the PAPER's background through it), but fixing only one
   leaves the other. Two classes to beat .mud-paper on specificity — MudBlazor uses no !important
   here. The browser mobile-frame host already does the same (mobile-frame.css). */
.mud-dialog-container .mud-dialog.mud-dialog-fullscreen {
    border-radius: 0;
}

@media (max-width: 959.98px) {
    :where(.mud-dialog-container .mud-dialog .mud-dialog-actions) {
        padding-bottom: calc(8px + var(--tm-safe-area-bottom, env(safe-area-inset-bottom, 0px)));
    }
}

/* ============================================================
   Confirm / question dialog card (shared look)
   Component: Components/ConfirmDialogCard.razor
   Prefix: qd-  (marker class: .confirm-dialog)
   ============================================================ */
.confirm-dialog.mud-dialog {
    border-radius: 5px;
    overflow: hidden;
}

/* Consistent inset so text/actions never sit flush on the card edge. */
.confirm-dialog .mud-dialog-title {
    border-bottom: none !important;
    padding: 20px 20px 4px 20px;
}
.confirm-dialog .mud-dialog-content {
    padding: 0 20px;
}
.confirm-dialog .mud-dialog-actions {
    border-top: none !important;
    justify-content: flex-end;
    gap: 4px;
    padding: 12px 20px 16px 20px;
}

/* Header row: icon chip + bold title. */
.confirm-dialog .qd-head {
    display: flex;
    align-items: center;
    gap: 12px;
}
.confirm-dialog .qd-icon {
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(var(--mud-palette-primary-rgb), 0.12);
    color: var(--mud-palette-primary);
}
.confirm-dialog.is-destructive .qd-icon {
    background: rgba(var(--mud-palette-error-rgb), 0.12);
    color: var(--mud-palette-error);
}
.confirm-dialog .qd-icon .mud-icon-root {
    font-size: 1.4rem !important;
}

/* Beat the global .mud-dialog-title .mud-typography size/weight clamp. */
.confirm-dialog .mud-dialog-title .qd-title.mud-typography {
    font-size: 1.15rem !important;
    font-weight: 700 !important;
    line-height: 1.3;
    color: var(--mud-palette-text-primary);
}
.confirm-dialog .qd-body.mud-typography {
    font-size: 0.9rem;
    color: var(--mud-palette-text-secondary);
    line-height: 1.45;
    /* The restock bodies build a bulleted line list with \n, which HTML was collapsing into one
       run-on paragraph — so the list of what is about to move read as a sentence. pre-line only
       changes rendering where a string actually contains newlines, so bodies without them are
       untouched. */
    white-space: pre-line;
}

/* One optional input under the body. Spaced off it so the control does not read as part of the
   sentence above it. */
.confirm-dialog .qd-body-extra {
    margin-top: 16px;
}

/* Mockup uses UPPERCASE button labels. */
.confirm-dialog .qd-btn.mud-button-root {
    text-transform: uppercase !important;
    border-radius: 8px;
    font-weight: 600;
}

/* ============================================================
   Outlined text-style action button (e.g. "+ ADD")
   Usage: <MudButton Class="md-add-btn" Variant="Variant.Outlined"
                    Size="Size.Small" StartIcon="@Icons.Material.Filled.Add">Add</MudButton>
   ============================================================ */
.md-add-btn.mud-button-root {
    background: var(--mud-palette-surface) !important;
    color: var(--mud-palette-secondary) !important;
    border: 1px solid var(--mud-palette-lines-default) !important;
    border-radius: 5px !important;
    padding: 4px 10px 4px 8px !important;
    min-height: 0 !important;
    min-width: 0 !important;
    font-size: 0.75rem !important;
    font-weight: 600 !important;
    letter-spacing: 0.02em !important;
    text-transform: uppercase !important;
    line-height: 1.2 !important;
    box-shadow: none !important;
    gap: 4px;
}
.md-add-btn.mud-button-root:hover {
    background: rgba(21, 93, 252, 0.06) !important;
    border-color: var(--mud-palette-lines-default) !important;
}
.md-add-btn .mud-button-label { gap: 4px; }
.md-add-btn .mud-icon-root,
.md-add-btn .mud-button-icon-start .mud-icon-root {
    color: var(--mud-palette-secondary) !important;
    font-size: 1rem !important;
    width: 1rem !important;
    height: 1rem !important;
    margin: 0 !important;
}
.md-add-btn .mud-button-icon-start { margin: 0 !important; }


/* ============================================================
   Appointments redesign (mobile) — .appt- prefix
   ============================================================ */
.appt-page {
    padding: 0;
    padding-bottom: calc(var(--tm-mobile-bottom-nav-offset, 72px) + 96px);
    padding-top: var(--tm-mobile-fixed-top-nav-inner-padding);
    background: var(--mud-palette-tertiary);
    min-height: 100vh;
    overflow-x: hidden;
}

    .appt-page .custom-border {
        border-radius: 14px !important;
        /* box-shadow, NOT filter: drop-shadow. Two reasons, both fatal on the glass cards below:
           drop-shadow promotes each card to its own compositing layer and paints an opaque blue
           halo, and — because filter establishes a containing block — it makes the card's own
           backdrop-filter sample an empty backdrop, so the glass renders flat. */
        box-shadow: var(--tm-card-shadow);
        border: 0px !important;
    }

/* Page header */
.appt-pagehead {
    display: flex; justify-content: space-between; align-items: flex-end;
    padding: 16px 20px 12px;
    gap: 12px;
}
.appt-eyebrow {
    font-size: 11.5px; font-weight: 500;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
}
.appt-pagetitle {
    font-size: 24px; font-weight: 600;
    color: var(--mud-palette-text-primary);
    letter-spacing: -0.01em;
    margin-top: 2px;
}
.appt-viewtoggle {
    background: var(--mud-palette-surface) !important;
    border: 1px solid var(--mud-palette-lines-default) !important;
    border-radius: 999px !important;
    padding: 2px !important;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
.appt-viewtoggle .mud-toggle-item {
    border-radius: 999px !important;
    text-transform: none !important;
    font-size: 12px !important;
    font-weight: 600 !important;
    padding: 6px 12px !important;
    min-width: auto !important;
    border: 0 !important;
}

/* Week strip */
.appt-weekstrip {
    display: grid; grid-template-columns: repeat(7, 1fr);
    gap: 6px;
    padding: 4px 12px 12px;
}
.appt-weekday {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding: 10px 0 8px;
    background: transparent; border: 0; border-radius: 12px;
    cursor: pointer; transition: background 0.15s ease;
}
.appt-weekday:hover { background: var(--mud-palette-surface); }
.appt-weekday-dow {
    font-size: 10px; font-weight: 500;
    letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
}
.appt-weekday-num {
    font-size: 17px; font-weight: 600;
    color: var(--mud-palette-text-primary);
}
.appt-weekday-dots { display: flex; gap: 2px; height: 4px; align-items: center; }
.appt-weekday-dot {
    width: 4px; height: 4px; border-radius: 50%;
    background: var(--mud-palette-text-secondary); opacity: 0.6;
}
.appt-weekday.is-selected { background: var(--mud-palette-primary); }
.appt-weekday.is-selected .appt-weekday-dow,
.appt-weekday.is-selected .appt-weekday-num { color: #fff; }
.appt-weekday.is-selected .appt-weekday-dot { background: #fff; opacity: 1; }
.appt-weekday.is-today:not(.is-selected) .appt-weekday-num { color: var(--mud-palette-secondary); }

/* Day summary strip */
.appt-daysummary {
    display: flex !important; align-items: center !important;
    margin: 0 16px 14px;
    background: var(--mud-palette-surface) !important;
    border: 1px solid var(--mud-palette-lines-default) !important;
    border-radius: 10px !important;
    padding: 10px 4px !important;
}
.appt-ds-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.appt-ds-num {
    font-size: 15px; font-weight: 600;
    color: var(--mud-palette-text-primary);
    letter-spacing: -0.01em;
}
.appt-ds-label {
    font-size: 10px; font-weight: 500;
    letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
}
.appt-ds-sep { width: 1px; height: 22px; background: var(--mud-palette-lines-default); }
.appt-ds-warn .appt-ds-num { color: #C26200; }
.appt-ds-route {
    flex: 0 0 auto;
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    background: transparent; border: none; cursor: pointer;
    color: var(--mud-palette-primary); font: inherit;
    padding: 0 10px;
}
.appt-ds-route .appt-ds-label { color: var(--mud-palette-primary); }

/* Agenda list */
.appt-agenda {
    padding: 0 16px;
    display: flex; flex-direction: column; gap: 10px;
}

/* Agenda job card */
.appt-jc {
    display: grid;
    grid-template-columns: 58px 1fr;
    background: var(--mud-palette-surface);    
    padding: 14px;    
    position: relative;
    overflow: hidden;
}
.appt-jc-next {
    border-color: var(--mud-palette-secondary);
    box-shadow: 0 0 0 2px rgba(21,93,252,0.18), 0 1px 2px rgba(0,0,0,0.04);
}
.appt-jc-inprogress { border-left: 3px solid var(--mud-palette-info); }
.appt-jc-overdue { border-left: 3px solid #C26200; }

.appt-jc-rail {
    display: flex; flex-direction: column; align-items: flex-start;
    padding-right: 10px; padding-top: 2px;
}
.appt-jc-time-start {
    font-size: 15px; font-weight: 600;
    color: var(--mud-palette-text-primary); letter-spacing: -0.01em;
}
.appt-jc-time-line {
    flex: 1; width: 2px; background: var(--mud-palette-lines-default);
    margin: 6px 0 6px 8px; min-height: 32px;
    display: flex; align-items: center; justify-content: center;
    position: relative;
}
.appt-jc-time-dur {
    position: absolute;
    left: 50%; transform: translateX(-50%);
    background: var(--mud-palette-background);
    padding: 2px 4px;
    font-size: 10px; font-weight: 600;
    color: var(--mud-palette-text-secondary);
    letter-spacing: -0.01em;
    white-space: nowrap;
}
.appt-jc-time-end {
    font-size: 11px; font-weight: 500;
    color: var(--mud-palette-text-secondary);
}

.appt-jc-body { min-width: 0; position: relative; padding-right: 56px; }
.appt-jc-edit-btn {
    position: absolute;
    top: 0;
    right: 0;
    min-width: 0;
    padding: 2px 10px;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.4;
    border-radius: 6px;
    z-index: 1;
}
.appt-jc-nextbadge {
    display: inline-block;
    background: var(--mud-palette-secondary); color: #fff;
    font-size: 10px; font-weight: 600;
    letter-spacing: 0.06em; text-transform: uppercase;
    padding: 3px 8px; border-radius: 999px;
    margin-bottom: 8px;
}
.appt-jc-headrow {
    display: flex; align-items: center; gap: 6px;
    flex-wrap: wrap; margin-bottom: 8px;
}
.appt-jc-durchip {
    display: inline-flex; align-items: center; gap: 4px;
    background: var(--mud-palette-background-gray);
    border: 1px solid var(--mud-palette-lines-default);
    padding: 2px 8px; border-radius: 999px;
    font-size: 11px; font-weight: 600;
    color: var(--mud-palette-text-secondary);
}
.appt-jc-durchip .mud-icon-root { width: 13px !important; height: 13px !important; font-size: 13px !important; }

.appt-jc-title {
    font-size: 16px; font-weight: 600;
    color: var(--mud-palette-text-primary);
    margin: 0 0 8px;
    letter-spacing: -0.005em;
    line-height: 1.3;
}
.appt-jc-site {
    display: flex; align-items: flex-start; gap: 6px;
    color: var(--mud-palette-text-secondary); font-size: 13px;
    margin-bottom: 8px;
}
.appt-jc-site-text { flex: 1 1 auto; min-width: 0; }
.appt-jc-site .mud-icon-root {
    color: var(--mud-palette-text-secondary);
    width: 15px !important; height: 15px !important; font-size: 15px !important;
    flex-shrink: 0; margin-top: 1px;
}
.appt-jc-sitename { display: block; font-weight: 500; color: var(--mud-palette-text-primary); line-height: 1.35; }
.appt-jc-addr { display: block; color: var(--mud-palette-text-secondary); font-size: 12px; margin-top: 1px; }

.appt-jc-meta {
    display: flex; align-items: center; gap: 6px;
    flex-wrap: wrap;
    font-size: 12px; font-weight: 500;
    color: var(--mud-palette-text-secondary);
    margin-bottom: 10px;
}
.appt-jc-metaitem { display: inline-flex; align-items: center; gap: 4px; }
.appt-jc-metaitem .mud-icon-root { width: 14px !important; height: 14px !important; font-size: 14px !important; color: var(--mud-palette-text-secondary); }

.appt-jc-jobs {
    display: flex; flex-direction: column; gap: 6px;
    margin: 8px -48px 10px 0;
}
.appt-jc-jobrow {
    display: flex; align-items: center; gap: 8px;
    min-height: 44px;
    padding: 8px 10px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 8px;
    background: var(--mud-palette-surface);
    cursor: pointer;
    user-select: none;
}
.appt-jc-jobrow:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}
.appt-jc-jobrow-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.appt-jc-jobrow-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.appt-jc-jobno {
    font-size: 12px; font-weight: 600; letter-spacing: 0.02em;
    color: var(--mud-palette-text-secondary);
    font-family: 'Roboto Mono', ui-monospace, monospace;
    padding: 1px 8px;
    border-radius: 999px;
}
.appt-jc-jobdesc {
    font-size: 12px; color: var(--mud-palette-text-secondary); line-height: 1.35;
    overflow: hidden; text-overflow: ellipsis; display: -webkit-box;
    -webkit-line-clamp: 1; -webkit-box-orient: vertical;
}
.appt-jc-jobrow-chev { color: var(--mud-palette-text-secondary); flex-shrink: 0; }

.appt-mjc-jobs { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
.appt-mjc-jobrow {
    display: flex; align-items: center; gap: 6px;
    min-height: 36px;
    padding: 4px 8px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 6px;
    background: var(--mud-palette-surface);
    cursor: pointer;
}
.appt-mjc-jobrow:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}
.appt-mjc-jobno {
    min-width: 0;
    font-size: 12px; font-weight: 600; letter-spacing: 0.02em;
    color: var(--mud-palette-text-secondary);
    font-family: 'Roboto Mono', ui-monospace, monospace;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    padding: 1px 8px;
    border-radius: 999px;
}
.appt-mjc-jobrow .appt-mjc-jobrow-chev { margin-left: auto; }
.appt-mjc-jobrow-chev { color: var(--mud-palette-text-secondary); flex-shrink: 0; }

.appt-jc-actions { display: flex; align-items: center; gap: 4px; margin-top: 4px; }
.appt-jc-iconbtn {
    border: 1px solid var(--mud-palette-lines-default) !important;
    border-radius: 8px !important;
    background: var(--mud-palette-surface) !important;
}
.appt-jc-directions-btn { text-transform: none; font-weight: 500; flex-shrink: 0; margin-top: -2px; margin-left: auto; }
.appt-jc-jobrow.is-completed,
.appt-mjc-jobrow.is-completed { cursor: default; }

/* "Running" pill — matches the Jobs screen's active-timer chip (pulsing red dot). */
.jobs-running-chip {
    position: relative;
    font-size: 0.6875rem;
    font-weight: 600;
    padding: 3px 10px;
    border-radius: 9999px;
    letter-spacing: 0.02em;
    white-space: nowrap;
    background: #fee2e2;
    color: #b91c1c;
    display: inline-flex;
    align-items: center;
    gap: 5px;
}

.jobs-running-chip::before {
    content: '';
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #b91c1c;
}

/* Composited ping ring over the dot (see .th-live) — replaces the box-shadow pulse
   that repainted every running row at 60fps. Absolutely positioned so it stays out
   of the chip's flex flow; anchored to the ::before dot (left = padding-left). */
.jobs-running-chip::after {
    content: '';
    position: absolute;
    left: 10px;
    top: 50%;
    width: 6px;
    height: 6px;
    margin-top: -3px;
    border-radius: 50%;
    background: rgba(185, 28, 28, 0.5);
    transform: scale(1);
    transform-origin: center;
    animation: jobsLivePing 1.6s ease-out infinite;
    will-change: transform, opacity;
    pointer-events: none;
}

@keyframes jobsLivePing {
    0%   { transform: scale(1);   opacity: 0.8; }
    100% { transform: scale(2.6); opacity: 0; }
}

.appt-endofday {
    display: flex; align-items: center; justify-content: center; gap: 8px;
    color: var(--mud-palette-text-secondary); font-size: 12px; font-weight: 500;
    padding: 16px 0 6px;
}
.appt-endofday .mud-icon-root { color: #007E33; width: 16px !important; height: 16px !important; font-size: 16px !important; }

/* All-day section */
.appt-allday {
    padding: 0 16px;
    margin: 4px 0 10px;
    display: flex; flex-direction: column; gap: 8px;
}
.appt-allday-head {
    display: flex; align-items: center; gap: 6px;
    font-size: 11px; font-weight: 600;
    letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
    padding: 0 2px;
}
.appt-allday-head .mud-icon-root {
    width: 14px !important; height: 14px !important; font-size: 14px !important;
    color: var(--mud-palette-text-secondary);
}
.appt-allday-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 18px; height: 18px; padding: 0 6px;
    background: var(--mud-palette-background-gray);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 999px;
    font-size: 10px; font-weight: 700;
    color: var(--mud-palette-text-secondary);
    margin-left: auto;
}
.appt-allday-list { display: flex; flex-direction: column; gap: 8px; }
.appt-allday-card {
    display: flex; align-items: center; gap: 8px;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-left: 3px solid var(--mud-palette-secondary);
    border-radius: 10px;
    padding: 12px 12px 12px 14px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
    cursor: pointer;
    user-select: none;
}
.appt-allday-card.appt-allday-overdue { border-left-color: #C26200; }
.appt-allday-body { flex: 1; min-width: 0; }
.appt-allday-pill {
    display: inline-flex; align-items: center; gap: 4px;
    background: var(--mud-palette-secondary);
    color: #fff;
    padding: 2px 8px; border-radius: 999px;
    font-size: 10px; font-weight: 700;
    letter-spacing: 0.06em; text-transform: uppercase;
}
.appt-allday-pill .mud-icon-root {
    width: 12px !important; height: 12px !important; font-size: 12px !important;
    color: #fff;
}
.appt-allday-chev { color: var(--mud-palette-text-secondary); flex-shrink: 0; }

/* Empty state */
.appt-empty {
    display: flex; flex-direction: column; align-items: center; gap: 8px;
    padding: 40px 16px;
    color: var(--mud-palette-text-secondary);
}
.appt-empty .mud-icon-root { color: var(--mud-palette-text-disabled); }

/* FAB */
.appt-fab {
    position: fixed !important;
    right: 16px !important;
    bottom: calc(var(--tm-mobile-bottom-nav-offset, 72px) + 16px) !important;
    z-index: 30;
}

/* Month calendar card */
.appt-calcard {
    margin: 4px 16px 12px !important;
    background: var(--mud-palette-surface) !important;
    border: 1px solid var(--mud-palette-lines-default) !important;
    border-radius: 12px !important;
    padding: 14px 12px 10px !important;
}
.appt-cal-head {
    display: flex; align-items: center; justify-content: space-between;
    margin-bottom: 8px;
}
.appt-cal-title { display: flex; align-items: baseline; gap: 6px; }
.appt-cal-month { font-size: 17px; font-weight: 600; color: var(--mud-palette-text-primary); }
.appt-cal-year { font-size: 14px; font-weight: 500; color: var(--mud-palette-text-secondary); }

.appt-cal-dow {
    display: grid; grid-template-columns: repeat(7, 1fr);
    font-size: 10px; font-weight: 500;
    letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
    text-align: center;
    padding-bottom: 4px;
}
.appt-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.appt-cal-blank { aspect-ratio: 1; }
.appt-cal-day {
    aspect-ratio: 1;
    background: transparent; border: 0; border-radius: 10px;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 2px; padding: 0; cursor: pointer;
    position: relative;
    transition: background 0.15s ease;
}
.appt-cal-day:hover { background: var(--mud-palette-background-gray); }
.appt-cal-num { font-size: 14px; font-weight: 500; color: var(--mud-palette-text-primary); }
.appt-cal-dots { display: flex; gap: 2px; height: 4px; align-items: center; }
.appt-cal-dot { width: 4px; height: 4px; border-radius: 50%; background: var(--mud-palette-secondary); }
.appt-cal-day.is-today { background: rgba(21,93,252,0.08); }
.appt-cal-day.is-today .appt-cal-num { color: var(--mud-palette-secondary); font-weight: 600; }
.appt-cal-day.is-selected { background: var(--mud-palette-primary); }
.appt-cal-day.is-selected .appt-cal-num { color: #fff; font-weight: 600; }
.appt-cal-day.is-selected .appt-cal-dot { background: #fff; }
.appt-cal-day.is-overdue .appt-cal-dot { background: #FF8F00; }

.appt-cal-legend {
    display: flex; justify-content: center; gap: 14px;
    margin-top: 10px; padding-top: 10px;
    border-top: 1px solid var(--mud-palette-lines-default);
}

/* Sticky upload progress banner — stays visible while user scrolls .md-body during a FAB-triggered media upload. */
.md-upload-progress-banner {
    position: sticky;
    top: 0;
    z-index: 10;
    margin: -16px -16px 12px;
    padding: 10px 14px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
    box-shadow: 0 2px 6px rgba(15, 23, 43, 0.08);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
.md-upload-progress-banner:active { background: var(--mud-palette-action-default-hover); }
.appt-cal-leg {
    display: inline-flex; align-items: center; gap: 5px;
    font-size: 10.5px; font-weight: 500;
    color: var(--mud-palette-text-secondary);
}
.appt-cal-leg-dot { width: 6px; height: 6px; border-radius: 50%; }
.appt-cal-leg-dot--sched { background: var(--mud-palette-secondary); }
.appt-cal-leg-dot--overdue { background: #FF8F00; }
.appt-cal-leg-dot--today { background: rgba(21,93,252,0.6); }

/* Day head (month view) */
.appt-dayhead {
    display: flex; justify-content: space-between; align-items: center;
    padding: 8px 20px 10px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    margin-bottom: 4px;
}
.appt-dayhead-title { font-size: 16px; font-weight: 600; color: var(--mud-palette-text-primary); }
.appt-dayhead-sub { font-size: 11.5px; font-weight: 500; color: var(--mud-palette-text-secondary); margin-top: 1px; }
.appt-dayhead-filter { text-transform: none !important; border-radius: 999px !important; }

/* Group */
.appt-group { padding: 0 16px; margin-top: 12px; }
.appt-group-head {
    display: flex; align-items: center; gap: 6px;
    font-size: 11px; font-weight: 600;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
    padding: 4px 4px 8px;
}
.appt-group-head .mud-icon-root { color: #FF8F00; width: 15px !important; height: 15px !important; font-size: 15px !important; }
.appt-group-count {
    background: var(--mud-palette-tertiary); color: var(--mud-palette-primary);
    font-size: 10.5px; font-weight: 600;
    padding: 1px 7px; border-radius: 999px;
    letter-spacing: 0;
}
.appt-group-list { display: flex; flex-direction: column; gap: 8px; }

/* Month job card */
.appt-mjc {
    display: grid;
    grid-template-columns: 54px 1fr 28px;
    gap: 12px; align-items: center;
    background: var(--mud-palette-surface);
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 10px;
    padding: 12px 10px 12px 14px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
    cursor: pointer;
}
.appt-mjc-inprogress {
    background: rgba(21,93,252,0.04);
    border-color: rgba(21,93,252,0.25);
}
.appt-mjc-overdue { border-left: 3px solid #C26200; }

.appt-mjc-timecol {
    display: flex; flex-direction: column; gap: 2px;
    border-right: 1px solid var(--mud-palette-lines-default);
    padding-right: 10px; min-height: 40px; justify-content: center;
}
.appt-mjc-time { font-size: 15px; font-weight: 600; color: var(--mud-palette-text-primary); letter-spacing: -0.01em; }
.appt-mjc-dur { font-size: 11px; font-weight: 500; color: var(--mud-palette-text-secondary); }

.appt-mjc-body { min-width: 0; }
.appt-mjc-tagrow { display: flex; gap: 4px; margin-bottom: 4px; flex-wrap: wrap; }
.appt-mjc-title {
    margin: 0 0 4px;
    font-size: 14px; font-weight: 600;
    color: var(--mud-palette-text-primary);
    letter-spacing: -0.005em; line-height: 1.3;
}
.appt-mjc-site {
    display: flex; align-items: center; gap: 5px;
    font-size: 12px; font-weight: 500;
    color: var(--mud-palette-text-secondary);
    margin-bottom: 3px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.appt-mjc-site .mud-icon-root { color: var(--mud-palette-text-secondary); width: 14px !important; height: 14px !important; font-size: 14px !important; flex-shrink: 0; }
.appt-mjc-meta {
    display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
    font-size: 11.5px; font-weight: 500;
    color: var(--mud-palette-text-secondary);
}
.appt-mjc-chev { color: var(--mud-palette-text-secondary); }

/* ============================================================
   Dark mode overrides
   Pastel chip/pill backgrounds (greens/blues/oranges) used for
   status indicators are designed for a white surface. In dark
   mode we replace them with low-opacity tints of the foreground
   colour so they remain visible without washing out the page.
   Body class .mud-theme-dark is applied by MudThemeProvider when
   IsDarkMode is bound true.
   ============================================================ */
.mud-theme-dark .job-detail-dialog .timer-hero .th-pill.onsite,
.mud-theme-dark .job-detail-dialog .th-compact-row .thc-pill.onsite,
.mud-theme-dark .job-detail-dialog .jt-entry .jt-dot.onsite {
    background: rgba(34, 197, 94, 0.18);
    color: #4ADE80;
}
.mud-theme-dark .job-detail-dialog .timer-hero .th-pill.travel,
.mud-theme-dark .job-detail-dialog .th-compact-row .thc-pill.travel,
.mud-theme-dark .job-detail-dialog .jt-entry .jt-dot.travel {
    background: rgba(59, 130, 246, 0.18);
    color: #93C5FD;
}
.mud-theme-dark .job-detail-dialog .jt-entry .jt-type.onsite {
    color: #4ADE80;
}
.mud-theme-dark .job-detail-dialog .jt-entry .jt-type.travel {
    color: #93C5FD;
}
.mud-theme-dark .job-detail-dialog .job-totals .jt-swatch.travel,
.mud-theme-dark .job-detail-dialog .th-compact-row .thc-dot.travel,
.mud-theme-dark .job-detail-dialog .timer-hero .th-live.travel {
    background: #93C5FD;
}
.mud-theme-dark .job-detail-dialog .timer-hero.is-running .th-label,
.mud-theme-dark .job-detail-dialog .product-row-slim.is-used .prs-code,
.mud-theme-dark .job-detail-dialog .complete-dock .dock-live,
.mud-theme-dark .job-detail-dialog .job-offline-strip.ready {
    color: #4ADE80;
}
.mud-theme-dark .job-detail-dialog .timer-hero.is-running.is-travel .th-label,
.mud-theme-dark .job-detail-dialog .job-offline-strip.preparing {
    color: #93C5FD;
}
.mud-theme-dark .job-detail-dialog .job-offline-strip.partial {
    color: #FBBF24;
}

/* Save button (success-tinted) — keep recognisable green but use
   a slightly more saturated tone that reads on a dark surface. */
.mud-theme-dark .job-detail-dialog .timer-hero .th-btn-success.mud-button-filled,
.mud-theme-dark .job-detail-dialog .th-compact-row--stopped .th-btn-success.mud-button-filled {
    background: #15803D !important;
}
.mud-theme-dark .job-detail-dialog .timer-hero .th-btn-success.mud-button-filled:hover,
.mud-theme-dark .job-detail-dialog .th-compact-row--stopped .th-btn-success.mud-button-filled:hover {
    background: #166534 !important;
}

/* Today / overdue accent dots */
.mud-theme-dark .appt-cal-day.is-today { background: rgba(96, 165, 250, 0.18); }
.mud-theme-dark .appt-cal-day.is-today .appt-cal-num { color: #93C5FD; }
.mud-theme-dark .appt-cal-leg-dot--today { background: rgba(96, 165, 250, 0.6); }

/* Month "in progress" job card */
.mud-theme-dark .appt-mjc-inprogress {
    background: rgba(96, 165, 250, 0.10);
    border-color: rgba(96, 165, 250, 0.30);
}

/* Avatar bubble inside hero card (was a primary-tinted swatch) */
.mud-theme-dark .job-detail-dialog .timer-hero .th-avatar {
    background: rgba(96, 165, 250, 0.22);
}

/* Overdue/warning accent — brighten amber for dark surfaces */
.mud-theme-dark .appt-ds-warn .appt-ds-num,
.mud-theme-dark .appt-jc-overdue,
.mud-theme-dark .appt-mjc-overdue {
    border-left-color: #FBBF24;
}
.mud-theme-dark .appt-ds-warn .appt-ds-num { color: #FBBF24; border-left: 0; }
.mud-theme-dark .appt-cal-day.is-overdue .appt-cal-dot,
.mud-theme-dark .appt-cal-leg-dot--overdue,
.mud-theme-dark .appt-group-head .mud-icon-root {
    background: #FBBF24;
}
.mud-theme-dark .appt-group-head .mud-icon-root {
    color: #FBBF24;
    background: transparent;
}
.mud-theme-dark .appt-endofday .mud-icon-root { color: #4ADE80; }

/* Make today's date stand out in MudDatePicker */
.mud-picker-calendar-day.mud-current:not(.mud-selected) {
    border-color: var(--mud-palette-primary) !important;
    color: var(--mud-palette-primary) !important;
    font-weight: 700;
    background-color: var(--mud-palette-primary-hover);
}

/* ============================================================
   My Tasks (mobile) — chips + compact rows
   Adapted from assets/mobile-todo/defex/todo.css
   ============================================================ */
.tdo-chiprow {
    display: flex;
    gap: 8px;
    padding: 12px 16px 8px;
    overflow-x: auto;
    scrollbar-width: none;
    /* Frosted strip, not an opaque bar: it sits over the wash like the Jobs toolbar. */
    background: linear-gradient(135deg, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0.2) 100%);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-bottom: 1px solid rgba(255, 255, 255, 0.6);
}
.tdo-chiprow::-webkit-scrollbar { display: none; }
.tdo-fchip {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: var(--md-inset-fill, rgba(255,255,255,0.45));
    border: 1px solid var(--md-inset-edge, rgba(255,255,255,0.65));
    border-radius: 999px;
    padding: 7px 12px;
    font-size: 13px;
    font-weight: 500;
    color: var(--mud-palette-text-primary);
    cursor: pointer;
    letter-spacing: 0;
    text-transform: none;
    transition: background 120ms, border-color 120ms, color 120ms;
}
.tdo-fchip-count {
    background: var(--mud-palette-background-gray);
    color: var(--mud-palette-text-secondary);
    font-size: 11px;
    font-weight: 500;
    padding: 0 6px;
    border-radius: 999px;
}
.tdo-fchip.is-active {
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text, #fff);
    border-color: var(--mud-palette-primary);
}
.tdo-fchip.is-active .tdo-fchip-count {
    background: rgba(255, 255, 255, 0.18);
    color: #fff;
}

.tdo-meta {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 16px 6px;
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
    letter-spacing: .02em;
}
.tdo-meta-count { font-weight: 500; color: var(--mud-palette-text-primary); }
.tdo-sort {
    margin-left: auto;
    background: transparent;
    border: 0;
    color: var(--mud-palette-secondary);
    font-size: 12px;
    font-weight: 500;
    display: inline-flex;
    align-items: center;
    gap: 2px;
    cursor: pointer;
    padding: 4px 6px;
}

.tdo-list-x { background: var(--mud-palette-background-gray); padding: 8px 12px 4px; }
.tdo-card {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.2) 100%);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid rgba(255, 255, 255, 0.6);
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.04),
        0 20px 40px rgba(14, 165, 233, 0.08),
        inset 0 1px 0 rgba(255, 255, 255, 0.6);
    border-radius: 24px;
    padding: 12px 14px;
    margin-bottom: 10px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
}

.mud-theme-dark .tdo-card {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%);
    border: 1px solid rgba(255, 255, 255, 0.12);
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.35),
        0 20px 40px rgba(14, 165, 233, 0.10),
        inset 0 1px 0 rgba(255, 255, 255, 0.10);
}

/* The red veil survives as a layer UNDER the glass gradient rather than over an opaque fill —
   same construction as .products-shortfall. It is still what marks an overdue task out. */
.tdo-card.is-overdue {
    border-color: rgba(211,47,47,0.35);
    background:
        linear-gradient(135deg,
            rgba(255, 255, 255, 0.50) 0%,
            rgba(255, 255, 255, 0.16) 100%),
        linear-gradient(180deg, rgba(211,47,47,0.10), rgba(211,47,47,0) 40%);
}
.tdo-card-main {
    display: grid;
    grid-template-columns: 24px 1fr;
    gap: 12px;
    align-items: start;
}
.tdo-check {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: 1.5px solid var(--mud-palette-action-disabled);
    background: var(--md-inset-fill, rgba(255,255,255,0.45));
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    margin-top: 1px;
    flex-shrink: 0;
}
.tdo-check .mud-icon-root { width: 16px; height: 16px; color: transparent; }
.tdo-check.is-done { background: var(--mud-palette-success); border-color: var(--mud-palette-success); }
.tdo-check.is-done .mud-icon-root { color: #fff; }

.tdo-card-body { min-width: 0; }
.tdo-row-x-title {
    font-size: 14.5px;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    line-height: 1.3;
    margin-bottom: 4px;
}
.tdo-card.is-done .tdo-row-x-title {
    text-decoration: line-through;
    opacity: 0.6;
    font-weight: 400;
}
.tdo-loc {
    font-size: 12px;
    color: var(--mud-palette-text-secondary);
    display: flex;
    gap: 4px;
    align-items: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.tdo-loc .mud-icon-root { width: 14px; height: 14px; flex-shrink: 0; }
.tdo-loc-sep { opacity: .6; }
.tdo-loc .tdo-due-overdue { color: #B02525; font-weight: 600; }
.tdo-loc .tdo-due-today { color: var(--mud-palette-secondary); font-weight: 600; }

.tdo-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 8px;
}
.tdo-chip {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-size: 11.5px;
    font-weight: 600;
    line-height: 1;
    padding: 4px 8px;
    border-radius: 999px;
    background: var(--md-inset-fill, rgba(255,255,255,0.45));
    color: var(--mud-palette-text-secondary);
    white-space: nowrap;
}
.tdo-chip .mud-icon-root { width: 13px; height: 13px; }
.tdo-chip-recur { background: rgba(124,58,237,0.10); color: #7C3AED; }
.tdo-chip.p-critical { background: rgba(211,47,47,0.12); color: #D32F2F; }
.tdo-chip.p-high { background: rgba(255,143,0,0.14); color: #C46A00; }
.tdo-chip-source { background: rgba(10,102,51,0.10); color: #0a6633; }
button.tdo-chip { border: 0; font-family: inherit; cursor: default; }
button.tdo-chip.is-clickable { cursor: pointer; }

.tdo-foot {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 4px;
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid var(--mud-palette-lines-default);
}
.tdo-foot-btn {
    background: transparent;
    border: none;
    padding: 4px 8px;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: var(--mud-palette-text-secondary);
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    border-radius: 8px;
}
.tdo-foot-btn:active { background: var(--mud-palette-action-default-hover); }
.tdo-foot-btn .mud-icon-root { font-size: 1.05rem; }
.tdo-foot-count { line-height: 1; }
.tdo-foot-count-badge {
    background: var(--mud-palette-secondary);
    color: #fff;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 999px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10.5px;
}

.tdo-empty {
    padding: 60px 32px;
    text-align: center;
    color: var(--mud-palette-text-secondary);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
}
.tdo-empty .mud-icon-root {
    width: 42px;
    height: 42px;
    color: var(--mud-palette-action-disabled);
}
.tdo-empty-title {
    font-size: 15px;
    font-weight: 500;
    color: var(--mud-palette-text-primary);
}
.tdo-empty-body {
    font-size: 13px;
    max-width: 220px;
    line-height: 1.45;
}

/* ============================================================
   Scroll-triggered section reveal (scroll-reveal.js)
   Elements marked [data-reveal] start hidden + nudged down and
   fade/rise into place the first time they scroll into their
   container's viewport (an IntersectionObserver adds .is-revealed).
   Echoes the web GSAP "rise" entrance. Shared across the mobile
   detail/list screens (Job, Property, Asset, Jobs).
   ============================================================ */
[data-reveal] {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity .5s ease, transform .5s cubic-bezier(.22, .61, .36, 1);
    will-change: opacity, transform;
}
[data-reveal].is-revealed {
    opacity: 1;
    transform: none;
}
@media (prefers-reduced-motion: reduce) {
    [data-reveal] {
        opacity: 1;
        transform: none;
        transition: none;
    }

    /* Kill the always-running timer/"live" ping rings. iOS users often enable Reduce
       Motion specifically to save battery, so honouring it here also cuts the
       compositor work. The dots themselves stay (they carry the on-site/travel/live
       cue via colour); only the animated ring pseudo-element is suppressed. */
    .job-detail-dialog .timer-hero .th-live::after,
    .job-detail-dialog .th-compact-row .thc-dot::after,
    .jobs-running-chip::after {
        animation: none;
        opacity: 0;
    }
}

/* ----------------------------------------------------------------------------
   Dark-mode marker class neutraliser.

   The mobile pages signal dark mode with a `mud-theme-dark` class on a wrapper
   (the <body> in the MAUI app, the .mobile-frame-screen in the web preview) so
   their `.mud-theme-dark <descendant>` overrides can fire — MudThemeProvider only
   swaps CSS variables and never adds a theme class itself.

   MudBlazor ships one bare utility rule, `.mud-theme-dark { color/background:
   var(--mud-palette-dark[-text]) !important }`, which would otherwise paint that
   wrapper grey. We only use the class as a descendant hook, so cancel the fill and
   restore the real theme surface/text.
   ---------------------------------------------------------------------------- */
body.mud-theme-dark,
.mobile-frame-screen.mud-theme-dark {
    background-color: var(--mud-palette-background) !important;
    color: var(--mud-palette-text-primary) !important;
}


/* ── MobileMediaGallery ──────────────────────────────────────────────────────
   Hoisted out of the component's inline <style>, which duplicated into the DOM
   once per instance. Colours resolve from the theme palette rather than literal
   hex: the previous #dc2626 / #eff6ff / #1e3a8a set had no dark counterpart and
   glared on the dark surface. */
.mobile-media-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
    gap: 8px;
    padding: 8px 0;
}
.mobile-media-thumb {
    position: relative;
    aspect-ratio: 1;
    border-radius: 5px;
    overflow: hidden;
    background: var(--mud-palette-background-gray);
    cursor: pointer;
    border: 1px solid var(--mud-palette-lines-default);
}
.mobile-media-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.mobile-media-thumb-pending img {
    filter: brightness(0.72);
}
.mobile-media-thumb-failed {
    border-color: var(--mud-palette-error);
    box-shadow: 0 0 0 1px var(--mud-palette-error) inset;
}
/* Flat token surfaces, not gradients: the old gradients ran from a theme variable to a
   fixed slate, so they inverted in dark mode and stranded the white label on a light fill. */
.mobile-media-thumb-video {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    background: var(--mud-palette-dark);
    color: var(--mud-palette-dark-text);
    padding: 10px;
}
.mobile-media-thumb-placeholder {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    background: var(--mud-palette-background-gray);
    color: var(--mud-palette-text-secondary);
    padding: 10px;
}
.mobile-media-thumb-title {
    font-size: 0.75rem;
    line-height: 1.2;
    word-break: break-word;
}
.mobile-media-thumb-delete {
    background: var(--tm-photo-scrim-strong) !important;
    color: var(--mud-palette-dark-text) !important;
    min-width: 28px !important;
    width: 28px !important;
    height: 28px !important;
    padding: 0 !important;
}
.mobile-media-thumb-badge {
    position: absolute;
    left: 6px;
    bottom: 6px;
    background: var(--tm-photo-scrim);
    color: var(--mud-palette-dark-text);
    border-radius: 999px;
    padding: 2px 8px;
    font-size: 0.75rem;
    line-height: 1.2;
}
.mobile-media-thumb-status {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}
.mobile-media-thumb-status-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 8px;
    border-radius: 999px;
    font-size: 0.75rem;
    line-height: 1.2;
}
.mobile-media-thumb-status-chip.pending {
    background: var(--mud-palette-secondary);
    color: var(--mud-palette-secondary-text);
}
.mobile-media-thumb-status-chip.failed {
    background: var(--mud-palette-error);
    color: var(--mud-palette-error-text);
}
/* Sync banners: tinted from the palette hue via color-mix so one rule serves both themes. */
.mobile-media-sync-banner {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border-radius: 6px;
    margin: 6px 0 2px 0;
    font-size: 0.875rem;
}
.mobile-media-sync-banner.pending {
    background: color-mix(in srgb, var(--mud-palette-info) 14%, var(--mud-palette-surface));
    color: color-mix(in srgb, var(--mud-palette-info) 70%, var(--mud-palette-text-primary));
}
.mobile-media-sync-banner.failed {
    background: color-mix(in srgb, var(--mud-palette-error) 14%, var(--mud-palette-surface));
    color: color-mix(in srgb, var(--mud-palette-error) 70%, var(--mud-palette-text-primary));
}
.mobile-media-sync-banner .spacer { flex: 1; }

/* =========================================================================
   ScanCodeField — label + rounded code input + filled scan button
   Prefix: md-scan-

   Hoisted here rather than into the component's inline <style> so it does not
   duplicate into the DOM per instance, and because the MAUI host does not link
   utilities.css (which is why .custom-field/.custom-field-gray do nothing on a
   real device). Palette variables only, so both themes are covered without a
   .mud-theme-dark override.
   ========================================================================= */
.md-scan-label {
    display: block;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    margin-bottom: 6px;
}

.md-scan-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

.md-scan-field {
    flex: 1;
    min-width: 0;
}
/* Kill the dense field's top margin so the input and the 44px button share a baseline. */
.md-scan-field.mud-input-control {
    margin-top: 0;
}
.md-scan-field .mud-input.mud-input-outlined {
    background: var(--mud-palette-background-gray);
    border-radius: 8px;
}
.md-scan-field .mud-input.mud-input-outlined .mud-input-outlined-border {
    border-color: var(--mud-palette-lines-default);
    border-radius: 8px;
}

.md-scan-btn.mud-icon-button {
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    border-radius: 8px;
    background: var(--mud-palette-primary) !important;
    color: var(--mud-palette-primary-text, #fff) !important;
}
.md-scan-btn.mud-icon-button:hover {
    background: var(--mud-palette-primary-darken) !important;
}
.md-scan-btn.mud-icon-button:disabled {
    background: var(--mud-palette-action-disabled-background) !important;
    color: var(--mud-palette-action-disabled) !important;
}

/* --- Required-products readiness and collection -------------------------------------------
   The pick list used to show a tick that meant nothing beyond "I looked at this". These
   surface the real reservation state per line, and the per-job Collect action. */

/* --- The answer line ----------------------------------------------------------------------
   What the technician came for, above the list. Ruled rather than filled: it is the quietest
   thing on screen when everything is sorted, and the list below it is the detail. */
/* Pinned above the scroll region and deliberately NOT shaped like a row. It used to be a
   bordered card on a surface fill, 10px above an identically-bordered group card — so the one
   sentence the technician came for read as the list's first row rather than its headline.
   Ruled, not boxed: a hairline beneath it and nothing else. */
.products-answer {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0;
    padding: 12px 16px;
    background: var(--mud-palette-surface);
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.products-answer-icon {
    flex: 0 0 auto;
    font-size: 20px !important;
    color: var(--mud-palette-primary);
}

.products-answer.settled .products-answer-icon {
    color: var(--mud-palette-success);
}

/* Back to the body size. 15px against a 14px body is a 1.07 ratio — below the threshold at which
   the eye reads a size change at all — so it bought an orphan value in the scale and delivered
   nothing. The answer already leads by pinned position, its own surface, a primary-coloured icon
   and the hairline beneath it. */
.products-answer-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.products-answer-text {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

/* The context the headline is true OF. "3 parts not ready yet" is only actionable once you know
   it is about today's four jobs and not the whole month — and that scope is now chosen behind a
   pill rather than displayed as a row of chips, so the answer has to restate it. */
.products-answer-sub {
    margin-top: 2px;
    font-size: 0.8125rem;
    line-height: 1.35;
    color: var(--mud-palette-text-secondary);
    font-variant-numeric: tabular-nums;
}

.products-answer-action {
    appearance: none;
    flex: 0 0 auto;
    min-height: 44px;
    padding: 8px 16px;
    border: 1px solid var(--mud-palette-primary);
    border-radius: 5px;
    background: transparent;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-primary);
    cursor: pointer;
}

/* The answer's continuation: what the technician can do about the part nobody is holding.
   Shares the answer's geometry exactly — same padding, same gap, same hairline — so the two read
   as one pinned block rather than as a headline with a banner stuck under it. Warning-tinted
   surface rather than a filled alert: nobody having the part is a problem to solve, not a fault,
   and the same restraint the readiness chips use applies at block scale. */
.products-shortfall {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 16px;
    background: color-mix(in srgb, var(--mud-palette-warning) 8%, var(--mud-palette-surface));
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.products-shortfall-icon {
    flex: 0 0 auto;
    font-size: 20px !important;
    color: var(--mud-palette-warning);
}

.products-shortfall-text {
    flex: 1;
    min-width: 0;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--mud-palette-text-primary);
}

/* Same 44px glove target and the same shape as the answer's own action, so the two actions in
   the pinned block read as the same kind of thing. */
.products-shortfall-action {
    appearance: none;
    flex: 0 0 auto;
    min-height: 44px;
    padding: 8px 16px;
    border: 1px solid var(--mud-palette-primary);
    border-radius: 5px;
    background: var(--mud-palette-surface);
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-primary);
    cursor: pointer;
}

.products-shortfall-action:disabled {
    opacity: 0.6;
    cursor: default;
}

/* --- Readiness ----------------------------------------------------------------------------
   Four states, not the reservation model's seven. Sentence case rather than the system's
   uppercase status-label role on purpose: a list can carry forty of these at once, and forty
   shouting chips is exactly what the Shouting Reserve rule exists to prevent. A covered line
   gets no chip at all — on a healthy screen only the things needing something speak. */
.products-ready {
    display: inline-block;
    padding: 1px 8px;
    border: 1px solid transparent;
    border-radius: 6px;
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.38;
    vertical-align: middle;
}

/* --- The ranking -------------------------------------------------------------------------
   The four states used to be four tinted chips of identical weight, which is a list with four
   equally-loud things on it and therefore a list with no priority at all. They are ranked now:
   NOT IN STOCK outranks GO COLLECT outranks ON ORDER, and a ready part says nothing.

   Rank is carried by WEIGHT — solid fill plus a coloured row edge for the two that need a
   decision, quiet text for the two that only need to be known — not by escalating to a new
   colour. The design specifies error red for "not in stock"; this file's own note below the
   warning token is why it stays warning, and the brief forbids new colours besides. If red is
   wanted, --mud-palette-warning becomes --mud-palette-error in .products-ready-short and
   .products-line-wrap--short and nothing else changes. */

/* Solid warning. The top of the ranking: nobody within reach is holding this part, which is the
   only state on the screen that cannot be resolved by driving somewhere. */
.products-ready-short {
    background: var(--mud-palette-warning);
    border-color: var(--mud-palette-warning);
    color: var(--mud-palette-warning-text, #fff);
}

/* Solid primary. Second: there is a part with this technician's name on it somewhere they can
   reach, and the whole trip is the action. Blue because it is actionable — the One Blue Rule
   working for us rather than against us. */
.products-ready-collect {
    background: var(--mud-palette-primary);
    border-color: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
}

/* Below the fold of the ranking: nothing to do, and specifically nothing to ask for again. Text,
   not a chip — a state that needs no decision must not compete with the two that do. Success
   rather than blue for the same reason as before: the technician asked, and the asking worked. */
.products-ready-incoming {
    padding: 0;
    border-color: transparent;
    background: transparent;
    font-weight: 500;
    color: var(--mud-palette-success);
}

.products-ready-order {
    padding: 0;
    border-color: transparent;
    background: transparent;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
}

/* Reserved, but sitting on a vehicle — nothing to collect and nowhere to drive. Quiet text for
   the same reason as the two above: it needs to be known, not decided, and a solid chip here
   would compete with the two states that do need a decision.

   Success rather than secondary because the part IS accounted for; this is the good outcome,
   not a neutral one. Deliberately a separate rule from .products-ready-incoming despite matching
   it today — "on a van" and "coming to a van" are different facts, and sharing a class called
   "incoming" for a thing that has already arrived is the kind of tidiness that costs an hour
   later. No new colour either way, which the ranking note above requires. */
.products-ready-onvan {
    padding: 0;
    border-color: transparent;
    background: transparent;
    font-weight: 500;
    color: var(--mud-palette-success);
}

/* --- The row edge -------------------------------------------------------------------------
   The chip says what the state is; the edge is what makes the ranked rows findable while
   scrolling past thirty lines of them. Transparent by default so a ranked row and an unranked
   one are the same width and their titles land on the same rule. */
.products-line-wrap {
    border-left: 3px solid transparent;
}

.products-line-wrap--short {
    border-left-color: var(--mud-palette-warning);
}

.products-line-wrap--collect {
    border-left-color: var(--mud-palette-primary);
}

/* Where a covered line came from. Quiet — it is reassurance, not a state to act on. */
/* The on-order readiness rendered as a button rather than a span. Reset to look exactly like the
   span it replaces — the affordance is the underline, not a second visual treatment. */
.products-ready-link {
    appearance: none;
    border: 0;
    background: transparent;
    padding: 0;
    font: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}

.products-ready-source {
    color: var(--mud-palette-success);
    font-weight: 500;
}

/* "2 of 3" — how much of what the line needs is actually reserved. It is the number that changes
   the decision, so it gets the documented small role rather than 11px, a real token colour rather
   than an opacity dimming already-muted text a second time, and tabular figures like every other
   quantity on the screen. The old 11px was also the one absolute size here, so it alone ignored
   the reader's browser text-size preference. */
.products-readiness-count {
    margin-left: 8px;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    font-variant-numeric: tabular-nums;
}

.products-group-action {
    padding: 0 16px 16px;
}

/* --- The line wrap -------------------------------------------------------------------------
   Binds a row to the secondary action beneath it, and carries the hairline the row itself no
   longer draws. */
.products-line-wrap {
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.products-line-wrap.last,
.products-lines .products-line-wrap:last-child {
    border-bottom: 0;
}

.products-line-wrap .products-line {
    border-bottom: 0;
}

/* Sibling of the row-checkbox, never nested inside it — a button in a button is invalid, and
   unreachable by keyboard. Full width so it is findable through a glove. */
/* Borderless and on the 48px text rule. A dashed full-width box was the most textured element in
   the list and repeated under every line, so roughly half the list's area was secondary action —
   a technician scanning "what still needs sorting" had to skip a non-datum between every pair of
   facts. The wrap's hairline already binds it to its row; it needs no outline of its own. */
.products-line-action {
    appearance: none;
    display: block;
    width: 100%;
    min-height: 40px;
    margin: 0 0 8px;
    /* On the lines' own 28px text rule, not the old 48px tick rule. */
    padding: 8px 16px 8px 25px;
    border: 0;
    background: transparent;
    text-align: left;
    /* Control Label, not Field Label — this is a button, and it was the only one on the screen
       set at the quieter 13px metadata size. */
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--mud-palette-primary);
    cursor: pointer;
}

.products-line-action:disabled {
    opacity: 0.5;
    cursor: default;
}

/* The same action sitting at the END of a flex row rather than on its own line. Without this the
   base class's width:100% claims the whole row, the flex sibling collapses to flex-basis 0 with
   nothing left to grow into, and the supplier's name wraps one character per line. */
.products-line-action--inline {
    width: auto;
    flex: none;
    margin: 0;
    padding: 8px 0 8px 12px;
    text-align: right;
}

.products-answer-action:focus-visible,
.products-shortfall-action:focus-visible,
.products-line-action:focus-visible,
.products-sheet-option:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
    border-radius: 5px;
}

/* --- Scope + grouping sheet ---------------------------------------------------------------
   The two decisions the context row's pills stand for, as full-width rows rather than as chips.
   Chips in a row are sized by their label; a sheet has the whole width to spend, and a
   technician choosing with a glove on should not be aiming at a word. */
.products-sheet-label {
    margin: 8px 0 4px;
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--mud-palette-text-secondary);
}

.products-sheet-option {
    appearance: none;
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 48px;
    padding: 8px 12px;
    border: 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    background: transparent;
    text-align: left;
    font-size: 0.875rem;
    font-weight: 500;
    color: var(--mud-palette-text-primary);
    cursor: pointer;
}

.products-sheet-option:last-child {
    border-bottom: 0;
}

.products-sheet-option.active {
    color: var(--mud-palette-primary);
    font-weight: 600;
}

.products-sheet-option-text {
    flex: 1;
    min-width: 0;
}

/* How much each scope is actually hiding. A scope switch is a guess without it — "this week"
   could mean four more parts or forty, and the technician finds out only after committing. */
.products-sheet-option-count {
    flex: 0 0 auto;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    font-variant-numeric: tabular-nums;
}

.products-sheet-option-check {
    flex: 0 0 auto;
    font-size: 20px !important;
    color: var(--mud-palette-primary);
}

/* --- Collect sheet ------------------------------------------------------------------------
   Taller rows than the scope sheet's, because each carries two facts — the place and the job —
   and the place is the one being chosen. */
.products-sheet-source {
    appearance: none;
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 56px;
    padding: 12px;
    border: 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    background: transparent;
    text-align: left;
    cursor: pointer;
}

.products-sheet-source:last-child {
    border-bottom: 0;
}

.products-sheet-source-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

/* The place leads, because what is being chosen is a drive. */
.products-sheet-source-name {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    overflow-wrap: anywhere;
}

.products-sheet-source-sub {
    margin-top: 2px;
    font-size: 0.8125rem;
    color: var(--mud-palette-text-secondary);
    overflow-wrap: anywhere;
}

.products-sheet-source-count {
    flex: 0 0 auto;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    font-variant-numeric: tabular-nums;
}

.products-sheet-source-chev {
    flex: 0 0 auto;
    font-size: 20px !important;
    color: var(--mud-palette-text-secondary);
}

/* Standing caveats and explanations inside a sheet. Quiet — they qualify the choice above rather
   than competing with it. */
.products-sheet-note {
    margin: 12px 0 4px;
    font-size: 0.8125rem;
    line-height: 1.45;
    color: var(--mud-palette-text-secondary);
}

.products-sheet-list {
    margin: 4px 0 8px;
    padding-left: 20px;
    font-size: 0.8125rem;
    line-height: 1.45;
    color: var(--mud-palette-text-secondary);
}

.products-sheet-list li {
    margin-bottom: 6px;
}

/* --- Offline strip ------------------------------------------------------------------------
   Stated ONCE, directly under the app bar, rather than repeated on each control it disables.
   Warning-tinted rather than error-filled, matching the shortfall block exactly: no signal is a
   condition to work around, not a fault. Tappable, because "you're offline" raises the obvious
   question and the sheet is the answer. */
.products-offline-strip {
    appearance: none;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 44px;
    padding: 8px 16px;
    border: 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    background: color-mix(in srgb, var(--mud-palette-warning) 8%, var(--mud-palette-surface));
    text-align: left;
    cursor: pointer;
}

.products-offline-icon {
    flex: 0 0 auto;
    font-size: 20px !important;
    color: var(--mud-palette-warning);
}

.products-offline-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.products-offline-title {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

.products-offline-sub {
    font-size: 0.8125rem;
    color: var(--mud-palette-text-secondary);
    font-variant-numeric: tabular-nums;
}

.products-offline-action {
    flex: 0 0 auto;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-primary);
}

/* --- Stale-list note ----------------------------------------------------------------------
   Sits between the error and the list it qualifies. Two facts, both load-bearing: how old this
   is, and that acting on it is switched off. */
.products-stale-note {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: 0 0 12px;
    padding: 8px 12px;
    border-left: 3px solid var(--mud-palette-warning);
    font-size: 0.8125rem;
    line-height: 1.45;
    color: var(--mud-palette-text-secondary);
}

/* --- Empty-state action -------------------------------------------------------------------
   The step out to the next scope. Outlined, not filled: it is a suggestion about where to look,
   not the thing the technician came to do. */
.products-empty-action {
    appearance: none;
    margin-top: 16px;
    min-height: 44px;
    padding: 8px 16px;
    border: 1px solid var(--mud-palette-primary);
    border-radius: 5px;
    background: transparent;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-primary);
    cursor: pointer;
}

/* --- Loading skeleton ---------------------------------------------------------------------
   The shape of what is coming, not a spinner. A centred spinner said "wait" and nothing else;
   these say how much is arriving and where it will sit, so the list fills in rather than
   replacing something. */
.products-skeleton {
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding-top: 8px;
}

.products-skeleton-group {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.products-skeleton-line {
    height: 12px;
    border-radius: 5px;
    background: color-mix(in srgb, var(--mud-palette-text-disabled) 25%, var(--mud-palette-surface));
    animation: products-skeleton-pulse 1400ms ease-in-out infinite;
}

.products-skeleton-line--title { width: 62%; }
.products-skeleton-line--meta  { width: 40%; }
.products-skeleton-line--pill  { width: 28%; }

@keyframes products-skeleton-pulse {
    50% { opacity: 0.45; }
}

/* --- Collect bar --------------------------------------------------------------------------
   Fixed above the bottom nav, in every grouping. Collecting used to be reachable only under
   By job AND only from inside a section body, so on a job with more than a handful of parts the
   action scrolled out of reach exactly as the technician finished reading what it would collect.
   Absent rather than disabled when there is nothing to collect — a bar that permanently says
   "no" is not worth 64px of a 390px screen. */
.products-collect-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--tm-mobile-bottom-nav-offset, 72px) + 16px);
    z-index: 1099;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 0 12px;
    box-sizing: border-box;
    pointer-events: none;
}

.products-collect-bar > * {
    pointer-events: all;
}

/* Filled primary: this is the screen's one committing action, and the One Blue Rule spends its
   loudest mark here rather than on a filter. */
.products-collect-primary {
    appearance: none;
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-height: 56px;
    padding: 8px 16px;
    border: 0;
    border-radius: 5px;
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    text-align: left;
    cursor: pointer;
    box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}

.products-collect-primary:disabled {
    opacity: 0.6;
    cursor: default;
}

.products-collect-primary-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.products-collect-primary-label {
    font-size: 0.875rem;
    font-weight: 600;
}

/* The places, under the count. One stop or three is the difference between doing this now and
   doing it after lunch, and it should not need a tap to find out. */
.products-collect-primary-sub {
    margin-top: 2px;
    font-size: 0.8125rem;
    opacity: 0.85;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.products-collect-primary-icon {
    flex: 0 0 auto;
    font-size: 20px !important;
}

/* The bar's second action. Outlined on a surface fill, not filled: recording a purchase is
   always available but is not what most technicians open this screen to do, and two filled
   buttons stacked would make the bar shout twice. */
.products-collect-secondary {
    appearance: none;
    width: 100%;
    min-height: 48px;
    padding: 8px 16px;
    border: 1px solid var(--mud-palette-primary);
    border-radius: 5px;
    background: var(--mud-palette-surface);
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-primary);
    cursor: pointer;
    box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}

/* ---- Record purchase screen ----
   Reuses the products- shell wholesale; only the pieces the pick list has no equivalent for are
   defined here. */
.products-purchase-section {
    margin-bottom: 16px;
    padding: 16px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 5px;
    background: var(--mud-palette-surface);
}

.products-purchase-heading {
    margin-bottom: 12px;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

.products-purchase-label {
    margin-bottom: 8px;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
}

.products-purchase-empty {
    padding: 8px 0 4px;
    font-size: 0.875rem;
    color: var(--mud-palette-text-secondary);
}

.products-purchase-line {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 0;
    border-bottom: 1px solid var(--mud-palette-lines-default);
}

.products-purchase-line:last-of-type {
    border-bottom: 0;
}

.products-purchase-line-body {
    flex: 1;
    min-width: 0;
}

/* Quantity and unit cost side by side under the name. Cost is the wider of the two: it is the
   number being corrected against a docket, whereas quantity is usually already right. */
/* What stays on the order after a short pickup. Warning, not error, and the same token the
   Required tab's shortfall uses: being short at a trade counter is routine, and the remainder is
   simply still owed. */
.products-po-short {
    color: var(--mud-palette-warning);
    font-weight: 500;
}

/* ---- Van stocktake: what a counted line says back ----
   Both only ever render AFTER a count has been entered — the system's figure is deliberately
   withheld until then, so that counting is counting rather than agreeing with a number already on
   the screen. */

/* Agreement. Muted rather than success-green: a line that matches is the boring, expected outcome,
   and colouring every one of forty lines green would drown the handful that actually differ. */
.products-stocktake-match {
    color: var(--mud-palette-text-secondary);
}

/* Disagreement. Warning rather than error, and for the same reason as .products-po-short: finding
   a difference is what a stocktake is FOR. Treating it as an error tells the technician they did
   something wrong by counting properly. */
.products-stocktake-variance {
    color: var(--mud-palette-warning);
    font-weight: 500;
}

/* ---- Mobile purchase order: the three-step progress ----
   Ticks and labels only. No connecting rail, no numbers: three steps do not need a diagram, and
   the web page's full stepper is the thing this screen exists to be smaller than. */
.products-po-step {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 0;
}

.products-po-step-icon {
    flex: 0 0 auto;
    font-size: 20px !important;
    color: var(--mud-palette-text-disabled);
}

.products-po-step-icon.done {
    color: var(--mud-palette-success);
}

.products-po-step-label {
    font-size: 0.875rem;
    color: var(--mud-palette-text-secondary);
}

/* Weight, not colour: the current step is where the order IS, which is a fact rather than an
   action, so it must not compete with the collect bar below. */
.products-po-step-label.current {
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

/* A supplier order that is not ready yet — listed so the technician can see the part IS on order,
   but plainly not something to drive for. */
.products-sheet-source:disabled {
    opacity: 0.55;
    cursor: default;
}

.products-purchase-line-fields {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin-top: 8px;
}

/* One field and its label, stacked, inside a row of them.
   The label-above rule (DESIGN.md → Inputs / Fields) needs somewhere to put the label when two
   fields sit side by side — without this wrapper the labels become flex siblings of the inputs
   and the row lays out as label, field, label, field across. */
.products-purchase-field {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* The label inside a field stack carries no bottom margin: the field's own dense margin already
   provides the gap, and 8px on top of it breaks the label/value rhythm the rule is protecting. */
.products-purchase-field .products-purchase-label {
    margin-bottom: 2px;
}

.products-purchase-qty {
    flex: 0 0 88px;
}

.products-purchase-cost {
    flex: 1 1 auto;
    min-width: 0;
}

.products-purchase-line-total {
    flex: 0 0 auto;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-text-primary);
    font-variant-numeric: tabular-nums;
}

.products-purchase-line-remove {
    appearance: none;
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
}

.products-purchase-receipt {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* Capped rather than full-bleed: it is proof the photo was taken, not something to read. */
.products-purchase-receipt-img {
    max-width: 100%;
    max-height: 220px;
    object-fit: contain;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 5px;
    background: var(--mud-palette-tertiary);
}

/* The number the technician checks against the docket in their other hand, so it sits at the
   quantity weight rather than the label weight. */
.products-purchase-total {
    flex: 0 0 auto;
    font-size: 1.125rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.products-collect-secondary:focus-visible,
.products-purchase-line-remove:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
    border-radius: 5px;
}

.products-collect-note {
    appearance: none;
    width: 100%;
    min-height: 40px;
    padding: 8px 16px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 5px;
    background: var(--mud-palette-surface);
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    text-align: left;
    cursor: pointer;
}

/* The bar overlays the list, so the list has to end above it. */
.products-list-scroll--barred {
    padding-bottom: calc(176px + var(--tm-safe-area-bottom, 0px));
}

.products-offline-strip:focus-visible,
.products-empty-action:focus-visible,
.products-collect-primary:focus-visible,
.products-collect-note:focus-visible,
.products-sheet-source:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
    border-radius: 5px;
}

@media (prefers-reduced-motion: reduce) {
    .products-skeleton-line {
        animation: none;
    }
}

/* ============================================================
   Glass surfaces — EXPERIMENTAL
   Prefix: md-glass-

   Shared by the mobile Home and Jobs screens. This file is linked by BOTH hosts
   (AI.Client.Mobile/wwwroot/index.template.html and AI.Client/wwwroot/index.template.html),
   so one definition covers the MAUI app and the /mobile browser host.

   Two pieces that only work together:
     .md-glass-wash    on the PAGE — soft colour blooms behind the cards.
     .md-glass-surface on each CARD — a translucent pane that lets the wash through.

   Without the wash the panes have nothing to pick up and read as flat white cards, and
   backdrop-filter is a no-op: blurring a single flat colour returns that same colour.

   Every property is !important, and the selectors are deliberately repeated to raise
   specificity to (0,3,0). These fight page-level rules that are themselves !important at
   (0,2,0) — e.g. `.jobs-page-content .custom-border { border-radius: 14px !important }` —
   and page <style> blocks render in the body, so they win over this file on ties. The
   repetition is buying specificity, not papering over a mistake.
   ============================================================ */

.md-glass-surface.md-glass-surface.md-glass-surface,
.md-glass-card.md-glass-card.md-glass-card {
    /* 1. Surface */
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.2) 100%) !important;
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);

    /* 2. Faint solid border — the glass edge */
    border: 1px solid rgba(255, 255, 255, 0.6) !important;

    /* 3 & 4. Layered shadows: physical depth, a faint ambient colour glow, then a crisp
       inner highlight along the top edge. */
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.04),
        0 20px 40px rgba(14, 165, 233, 0.08),
        inset 0 1px 0 rgba(255, 255, 255, 0.6) !important;
}

/* Radius is a separate concern: Jobs stacks cards into joined groups whose corners must
   stay square where they meet, so it opts into the surface alone and sets its own radii. */
.md-glass-card.md-glass-card.md-glass-card {
    border-radius: 24px !important;
}

/* Mud's default divider is a dark hairline, which reads as a seam cutting the pane rather
   than an edge within it. On glass the rule matches the border. */
.md-glass-surface .mud-divider,
.md-glass-card .mud-divider {
    border-color: rgba(255, 255, 255, 0.55);
}

/* The wash. Put this on the element that grows to the FULL CONTENT HEIGHT, not on a
   viewport-height wrapper — otherwise the background stops at the fold and everything
   below falls through to the dark host background, with the glass panes turning into
   unreadable grey rectangles.
   NOTE ON SIZING: radial-gradient percentages are relative to the element they paint. Jobs puts
   this on a fixed 100dvh container, so the blooms stay compact; Home puts it on a paper that grows
   to the full content height, where the SAME percentages become huge in absolute pixels. A bloom
   that reads as a tasteful corner tint on Jobs can wash half of Home. Keep the palette narrow so
   the two screens can't drift apart: blues and cyans only — an off-hue bloom (violet was tried)
   is invisible on Jobs and dominates Home. */
.md-glass-wash.md-glass-wash.md-glass-wash {
    background:
        radial-gradient(60% 25% at 15% 4%,  rgba(14, 165, 233, 0.30), transparent 70%),
        radial-gradient(50% 20% at 90% 18%, rgba(56, 189, 248, 0.24), transparent 70%),
        radial-gradient(70% 30% at 40% 55%, rgba(37, 99, 235, 0.20), transparent 70%),
        radial-gradient(60% 25% at 80% 88%, rgba(14, 165, 233, 0.24), transparent 70%),
        var(--mud-palette-tertiary) !important;
}

/* Dark theme: a 55%-white pane reads as a light card dropped on a dark page rather than as
   glass. Same construction inverted — a thin white veil, a brighter edge instead of a
   lighter fill, and a deeper drop so the card still lifts. The wash goes dimmer and cooler;
   at the light-mode alphas it reads as smudges. */
.mud-theme-dark .md-glass-surface.md-glass-surface.md-glass-surface,
.mud-theme-dark .md-glass-card.md-glass-card.md-glass-card {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%) !important;
    border: 1px solid rgba(255, 255, 255, 0.12) !important;
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.35),
        0 20px 40px rgba(14, 165, 233, 0.10),
        inset 0 1px 0 rgba(255, 255, 255, 0.10) !important;
}

.mud-theme-dark .md-glass-surface .mud-divider,
.mud-theme-dark .md-glass-card .mud-divider {
    border-color: rgba(255, 255, 255, 0.10);
}

.mud-theme-dark .md-glass-wash.md-glass-wash.md-glass-wash {
    background:
        radial-gradient(60% 25% at 15% 4%,  rgba(14, 165, 233, 0.16), transparent 70%),
        radial-gradient(70% 30% at 60% 60%, rgba(37, 99, 235, 0.12), transparent 70%),
        var(--mud-palette-tertiary) !important;
}

/* ------------------------------------------------------------
   Glass chips — pill badges sitting ON a glass pane
   Prefix: md-glass-chip

   Same construction as .md-glass-card at chip scale: blurred translucent pane, a bright
   1px edge, and a small drop plus an inner top highlight. A flat pastel chip on a glass
   card reads as a sticker — the blur is what puts it on the same material.

   WHY THE TINT IS NOT HERE: CustomChip writes `background-color: … !important` INLINE, and
   an inline !important beats an author-stylesheet !important — no selector can win. The tint
   is therefore emitted inline by the component (see GlassBackground in the MOBILE copy of
   Components/CustomChip.razor, which adds this class to every Light-variant chip). This class
   carries only the properties nothing sets inline — and the ones that need a dark-theme
   inversion, which a computed inline string can't express. Applying it by hand is only needed
   for chips that aren't CustomChips.

   The doubled selector buys specificity against the page <style> blocks, which render in the
   body and so beat this file on ties — the same reason the glass card rules are tripled. */
.md-glass-chip.md-glass-chip {
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border: 1px solid rgba(255, 255, 255, 0.55) !important;
    box-shadow:
        0 3px 10px rgba(0, 0, 0, 0.05),
        inset 0 1px 0 rgba(255, 255, 255, 0.55) !important;
}

.mud-theme-dark .md-glass-chip.md-glass-chip {
    border: 1px solid rgba(255, 255, 255, 0.14) !important;
    box-shadow:
        0 3px 10px rgba(0, 0, 0, 0.30),
        inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
}

/* ------------------------------------------------------------
   Glass buttons
   Prefix: md-glass-button

   Two treatments, both raised rather than flush — a button has to read as pressable, so the
   drop shadow is heavier than a card's and there is a sharp reflection edge along the top.

     .md-glass-button          neutral elevated glass — standard actions.
     .md-glass-button-primary  blue coloured pane — the affirmative action.
     .md-glass-button-success  the same pane in green — a second coloured action beside it.

   Applied to Start Travel (success) / Start Onsite (primary) in Pages/Mobile/Job.razor. The
   coloured pair is identical in construction and differs only in hue, so neither outranks the
   other — they are two equal choices, not a primary and its fallback.

   ON SPECIFICITY: these are (0,2,0) !important, which is enough to beat MudBlazor's
   .mud-button-filled-* fills at (0,1,0) but NOT a page/section rule at (0,3,0). That is
   deliberate — e.g. `.job-detail-dialog .complete-dock .dock-btn--start { padding: … }`
   keeps its own padding so the two dock buttons still fit side by side on a narrow phone.
   Colour, edge and shadow come from here; layout stays with whoever owns the layout.

   :disabled is excluded throughout — otherwise a disabled Start button (offline, or a start
   already in flight) keeps the full lit treatment and reads as tappable. */
.md-glass-button.md-glass-button:not(:disabled) {
    background: rgba(255, 255, 255, 0.7) !important;
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.8) !important;
    border-radius: 12px !important;
    padding: 10px 20px;
    box-shadow:
        0 4px 12px rgba(0, 0, 0, 0.08),
        inset 0 1px 0 rgba(255, 255, 255, 0.5) !important;
    color: #1c1c1e !important;
    font-weight: 600;
}

/* COLOUR IN THE PANE — no white film over it. Two earlier passes are worth not repeating:
     1. The Apple idiom, a 15% blue wash with #007aff text. Read washed out: the wash sits on an
        already-pale dock, the blur returns nothing because that dock is opaque, and the label
        landed ~3.5:1 — under AA for its size.
     2. A high-alpha blue pane with a white sheen gradient over it. Correct weight, but the sheen
        is a veil, and a veil over colour reads as HAZE rather than glass. It also lightened the
        pane exactly where the label sits, pulling white text to ~3.9:1.

   So the depth is a hue gradient, not an alpha or white one, and the glass is carried by the
   pieces that stay crisp: the bright 1px edge, the inner highlight LINE along the top, and the
   coloured ambient shadow. White on this reads 5.3:1.

   When tuning: change the two blues. Adding any rgba(255,255,255,…) fill layer brings the haze
   straight back. */
.md-glass-button-primary.md-glass-button-primary:not(:disabled) {
    background: linear-gradient(160deg, rgba(19, 116, 231, 0.96) 0%, rgba(8, 86, 190, 0.96) 100%) !important;
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.45) !important;
    border-radius: 12px !important;
    padding: 14px 24px;
    color: #FFFFFF !important;
    font-weight: 600;
    text-align: center;
    box-shadow:
        0 6px 16px rgba(10, 111, 224, 0.32),
        inset 0 1px 0 rgba(255, 255, 255, 0.45) !important;
}

/* The green sibling. Identical construction to -primary; only the hue moves, so the pair reads
   as one control type in two colours.
   NOT the MudBlazor success green (#4CAF50 / #16A34A) — white on either is ~3.3:1, well under
   AA. Green has to go noticeably darker than blue to carry white text at the same weight, which
   is why these values look heavier than the blue ones side by side on a swatch. They don't on
   screen: 4.9:1 at the light end, 6.3:1 at the dark. */
.md-glass-button-success.md-glass-button-success:not(:disabled) {
    background: linear-gradient(160deg, rgba(21, 124, 60, 0.96) 0%, rgba(9, 88, 42, 0.96) 100%) !important;
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.45) !important;
    border-radius: 12px !important;
    padding: 14px 24px;
    color: #FFFFFF !important;
    font-weight: 600;
    text-align: center;
    box-shadow:
        0 6px 16px rgba(21, 124, 60, 0.32),
        inset 0 1px 0 rgba(255, 255, 255, 0.45) !important;
}

/* Mud paints the icon from its own colour variable, so it stays palette-green/pink while the
   label turns ink or white. Pull both from the button's own colour. */
.md-glass-button:not(:disabled) .mud-icon-root,
.md-glass-button-primary:not(:disabled) .mud-icon-root,
.md-glass-button-success:not(:disabled) .mud-icon-root {
    color: inherit !important;
}

/* Dark theme: a 70%-white pane is a white button on a dark sheet, not glass. Same
   construction inverted — dark veil, edge carries the lift, label goes light. The blue pane
   drops its sheen (a bright top highlight on a dark page reads as a seam) and loses a little
   alpha so the page shows through. */
.mud-theme-dark .md-glass-button.md-glass-button:not(:disabled) {
    background: rgba(255, 255, 255, 0.10) !important;
    border: 1px solid rgba(255, 255, 255, 0.18) !important;
    box-shadow:
        0 4px 12px rgba(0, 0, 0, 0.35),
        inset 0 1px 0 rgba(255, 255, 255, 0.10) !important;
    color: #F3F4F6 !important;
}

.mud-theme-dark .md-glass-button-primary.md-glass-button-primary:not(:disabled) {
    background: linear-gradient(160deg, rgba(30, 136, 245, 0.90) 0%, rgba(13, 99, 199, 0.90) 100%) !important;
    border: 1px solid rgba(255, 255, 255, 0.22) !important;
    box-shadow:
        0 6px 16px rgba(0, 0, 0, 0.40),
        inset 0 1px 0 rgba(255, 255, 255, 0.16) !important;
    color: #FFFFFF !important;
}

.mud-theme-dark .md-glass-button-success.md-glass-button-success:not(:disabled) {
    background: linear-gradient(160deg, rgba(34, 152, 78, 0.90) 0%, rgba(16, 108, 54, 0.90) 100%) !important;
    border: 1px solid rgba(255, 255, 255, 0.22) !important;
    box-shadow:
        0 6px 16px rgba(0, 0, 0, 0.40),
        inset 0 1px 0 rgba(255, 255, 255, 0.16) !important;
    color: #FFFFFF !important;
}

/* ============================================================
   Icon tiles on glass — EXPERIMENTAL
   Prefix: md-icon-tile

   Generalised from the mobile Home screen's .home-nav-icon so the Jobs screen (and anything
   else that lands on glass) can use the same treatment instead of re-deriving it. Home still
   carries its own copy of the rules — it is the reference implementation and is deliberately
   left untouched; new work should use these classes.

   THE TECHNIQUE, in one sentence: an icon on a glass pane sits in a soft tinted tile that
   casts an AMBIENT COLOURED shadow in its own hue, so it reads as a lit object on the pane
   rather than a sticker on top of it. A neutral grey shadow over a translucent surface reads
   as dirt; a hue-matched one reads as depth.

   Three variables carry a whole variant, so a new hue is one line:
     --mit-bg    the tile fill (a pale wash of the hue)
     --mit-fg    the glyph colour (the saturated hue)
     --mit-glow  the same hue at ~45% — the shadow tint
   Geometry (two shadow layers: a wide diffuse bloom + a tight contact shadow, both with a
   negative spread so they stay tucked under the tile instead of haloing past its edges)
   lives once, here.

   Sizing is a second axis, set by --mit-size / --mit-radius / --mit-icon. Radius is held at
   roughly 32% of the tile at every size so a 22px inline glyph carries the same corner
   language as a 44px header tile — that ratio, not an absolute radius, is what makes the
   family read as one thing.
   ============================================================ */

/* .md-icon-tile-host is for components that render their own button and give you no hook on it
   — MudMenu being the case that forced it. Put the host class (plus a size and a hue) on a
   wrapper: the sizing/hue custom properties inherit down to the button, which the second
   selector then shapes. A real .md-icon-tile inside such a host is unaffected, because its own
   classes set the same variables directly on itself and win over the inherited values. */
.md-icon-tile,
.md-icon-tile-host .mud-icon-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    box-sizing: border-box;
    width: var(--mit-size, 36px);
    height: var(--mit-size, 36px);
    border-radius: var(--mit-radius, 12px);
    background: var(--mit-bg, #eef1f5);
    color: var(--mit-fg, #475569);
    /* The un-hued fallback is the periwinkle the house card shadow uses, so a tile with no hue
       class still looks native. It lives in the var() fallback rather than as a declaration
       here on purpose: DECLARING it would set the property on this element and so beat the
       value inherited from a .md-icon-tile-host wrapper, silently greying every hosted tile's
       glow while its fill and glyph stayed correctly hued. */
    box-shadow:
        0 8px 18px -6px var(--mit-glow, hsl(220deg 40% 55% / 0.40)),
        0 3px 8px -4px var(--mit-glow, hsl(220deg 40% 55% / 0.40));
}

/* Mud sizes its SVG icons by font-size with width/height at 1em, but callers routinely pin
   width/height directly — so pin all three here or a caller's leftover Size="Size.Small"
   wins and the glyph rattles around inside the tile. */
.md-icon-tile .mud-icon-root,
.md-icon-tile-host .mud-icon-button .mud-icon-root {
    font-size: var(--mit-icon, 20px) !important;
    width: 1em !important;
    height: 1em !important;
    padding: 0 !important;
    margin: 0 !important;
    background: none !important;
    border-radius: 0 !important;
    color: var(--mit-fg, #475569) !important;
}

/* Sizes. Default (no modifier) is the 36px medium above. */
.md-icon-tile--lg { --mit-size: 44px; --mit-radius: 14px; --mit-icon: 24px; }
.md-icon-tile--sm { --mit-size: 28px; --mit-radius: 9px;  --mit-icon: 16px; }
.md-icon-tile--xs { --mit-size: 22px; --mit-radius: 7px;  --mit-icon: 14px; }

/* At the two inline sizes the tight contact layer is larger than the tile itself and just
   reads as a smudge under the text baseline. The diffuse bloom alone carries the lift. */
.md-icon-tile--sm,
.md-icon-tile--xs,
.md-icon-tile-host.md-icon-tile--sm .mud-icon-button,
.md-icon-tile-host.md-icon-tile--xs .mud-icon-button {
    box-shadow: 0 4px 10px -4px var(--mit-glow, hsl(220deg 40% 55% / 0.40));
}

/* Hues. Values are Home's palette verbatim where one already existed, so the two screens
   can't drift. */
.md-icon-tile-blue   { --mit-bg: #e3f2fd; --mit-fg: #1565c0; --mit-glow: rgba(21, 101, 192, 0.45); }
.md-icon-tile-indigo { --mit-bg: #e8eaf6; --mit-fg: #3949ab; --mit-glow: rgba(57, 73, 171, 0.45); }
.md-icon-tile-violet { --mit-bg: #f3e8ff; --mit-fg: #7e22ce; --mit-glow: rgba(126, 34, 206, 0.42); }
.md-icon-tile-red    { --mit-bg: #ffebee; --mit-fg: #d32f2f; --mit-glow: rgba(211, 47, 47, 0.42); }
.md-icon-tile-amber  { --mit-bg: #fff4e5; --mit-fg: #b45309; --mit-glow: rgba(180, 83, 9, 0.40); }
.md-icon-tile-green  { --mit-bg: #e8f5e9; --mit-fg: #2e7d32; --mit-glow: rgba(46, 125, 50, 0.42); }
.md-icon-tile-teal   { --mit-bg: #e0f2f1; --mit-fg: #00838f; --mit-glow: rgba(0, 131, 143, 0.42); }
.md-icon-tile-pink   { --mit-bg: #fce4ec; --mit-fg: #ad1457; --mit-glow: rgba(173, 20, 87, 0.42); }
.md-icon-tile-slate  { --mit-bg: #eef1f5; --mit-fg: #475569; --mit-glow: rgba(71, 85, 105, 0.32); }

/* Dark theme. A pale wash fill turns into a bright chip punched out of a dark pane, so the
   fill inverts to a translucent veil of the hue and the glyph lightens to stay legible on it.
   The contact layer goes entirely: with no light surface beneath it there is nothing for it
   to darken, and it only muddies the tile. The bloom survives and now reads as an actual
   glow, which is the effect the light theme is imitating in the first place. */
.mud-theme-dark .md-icon-tile,
.mud-theme-dark .md-icon-tile-host .mud-icon-button {
    box-shadow: 0 8px 22px -6px var(--mit-glow, hsl(220deg 40% 55% / 0.40));
}

.mud-theme-dark .md-icon-tile--sm,
.mud-theme-dark .md-icon-tile--xs,
.mud-theme-dark .md-icon-tile-host.md-icon-tile--sm .mud-icon-button,
.mud-theme-dark .md-icon-tile-host.md-icon-tile--xs .mud-icon-button {
    box-shadow: 0 5px 12px -5px var(--mit-glow, hsl(220deg 40% 55% / 0.40));
}

.mud-theme-dark .md-icon-tile-blue   { --mit-bg: rgba(21, 101, 192, 0.22);  --mit-fg: #90caf9; }
.mud-theme-dark .md-icon-tile-indigo { --mit-bg: rgba(57, 73, 171, 0.22);   --mit-fg: #9fa8da; }
.mud-theme-dark .md-icon-tile-violet { --mit-bg: rgba(126, 34, 206, 0.22);  --mit-fg: #ce93d8; }
.mud-theme-dark .md-icon-tile-red    { --mit-bg: rgba(211, 47, 47, 0.20);   --mit-fg: #ff8a80; }
.mud-theme-dark .md-icon-tile-amber  { --mit-bg: rgba(180, 83, 9, 0.22);    --mit-fg: #fcd34d; }
.mud-theme-dark .md-icon-tile-green  { --mit-bg: rgba(46, 125, 50, 0.22);   --mit-fg: #a5d6a7; }
.mud-theme-dark .md-icon-tile-teal   { --mit-bg: rgba(0, 131, 143, 0.22);   --mit-fg: #80deea; }
.mud-theme-dark .md-icon-tile-pink   { --mit-bg: rgba(173, 20, 87, 0.22);   --mit-fg: #f48fb1; }
.mud-theme-dark .md-icon-tile-slate  { --mit-bg: rgba(148, 163, 184, 0.18); --mit-fg: #cbd5e1; }

/* ============================================================
   Calendar (Appointments) on glass — EXPERIMENTAL
   Prefix: .appt-

   The screen now uses the same three pieces as Home and Jobs: .md-glass-wash on the page,
   .md-glass-card on every surface, .md-icon-tile on every section-leading icon. Only the
   things those classes deliberately flatten are restated here.

   WHY THIS BLOCK IS AT THE END OF THE FILE: .md-glass-card sets background / border /
   box-shadow with !important at (0,3,0). Every rule below either matches or exceeds that
   specificity, so source order is what decides the ties — these must come after the glass
   section, not next to the rest of the .appt- rules up at ~2240.
   ============================================================ */

/* --- Accent edges -------------------------------------------------------------
   .md-glass-card's `border: 1px solid …!important` overwrites all four sides, which wipes the
   3px left rails that carry "overdue" / "in progress" / "all day". They're the only state a
   card shows at a glance, so they're restored as longhand at (0,3,0) — the doubled class is
   buying specificity, not guarding against a duplicate. */
.appt-jc.appt-jc-overdue.appt-jc-overdue,
.appt-mjc.appt-mjc-overdue.appt-mjc-overdue,
.appt-allday-card.appt-allday-overdue.appt-allday-overdue {
    border-left: 3px solid #C26200 !important;
}

.appt-jc.appt-jc-inprogress.appt-jc-inprogress,
.appt-mjc.appt-mjc-inprogress.appt-mjc-inprogress {
    border-left: 3px solid var(--mud-palette-info) !important;
}

.appt-allday-card.appt-allday-card.appt-allday-card {
    border-left: 3px solid var(--mud-palette-secondary) !important;
}

/* "Up next" was a blue ring drawn with box-shadow, which the glass shadow stack replaces.
   Re-stated as the full stack plus the ring so the card keeps its depth AND its highlight. */
.appt-jc.appt-jc-next.appt-jc-next {
    box-shadow:
        0 0 0 2px rgba(21, 93, 252, 0.28),
        0 12px 32px rgba(0, 0, 0, 0.04),
        0 20px 40px rgba(14, 165, 233, 0.08),
        inset 0 1px 0 rgba(255, 255, 255, 0.6) !important;
}

.mud-theme-dark .appt-jc.appt-jc-next.appt-jc-next {
    box-shadow:
        0 0 0 2px rgba(126, 166, 255, 0.35),
        0 12px 32px rgba(0, 0, 0, 0.35),
        0 20px 40px rgba(14, 165, 233, 0.10),
        inset 0 1px 0 rgba(255, 255, 255, 0.10) !important;
}

/* --- Lines inside a pane ------------------------------------------------------
   Same reasoning as .md-glass-card .mud-divider: Mud's dark hairline reads as a seam cutting
   the pane rather than a rule drawn on it. Every internal divider on this screen matches the
   glass edge instead. */
.appt-daysummary .appt-ds-sep,
.appt-mjc .appt-mjc-timecol,
.appt-calcard .appt-cal-legend,
.appt-jc .appt-jc-time-line {
    border-color: rgba(255, 255, 255, 0.55);
}

.appt-daysummary .appt-ds-sep,
.appt-jc .appt-jc-time-line {
    background: rgba(255, 255, 255, 0.55);
}

.mud-theme-dark .appt-daysummary .appt-ds-sep,
.mud-theme-dark .appt-mjc .appt-mjc-timecol,
.mud-theme-dark .appt-calcard .appt-cal-legend,
.mud-theme-dark .appt-jc .appt-jc-time-line {
    border-color: rgba(255, 255, 255, 0.12);
}

.mud-theme-dark .appt-daysummary .appt-ds-sep,
.mud-theme-dark .appt-jc .appt-jc-time-line {
    background: rgba(255, 255, 255, 0.12);
}

/* The day head sits directly on the wash rather than on a pane, so its underline follows the
   wash's own hairline weight instead of the pane edge. */
.appt-dayhead {
    border-bottom-color: rgba(148, 163, 184, 0.35);
}
.mud-theme-dark .appt-dayhead {
    border-bottom-color: rgba(255, 255, 255, 0.10);
}

/* --- Controls on the wash -----------------------------------------------------
   The view toggle, the week strip and the FAB float on the wash with no pane under them. Left
   solid they were the only opaque objects on the screen; they get the same translucent-pane
   construction as the cards, at control scale. */
.appt-viewtoggle.appt-viewtoggle {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.2) 100%) !important;
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid rgba(255, 255, 255, 0.6) !important;
    box-shadow:
        0 6px 16px rgba(0, 0, 0, 0.04),
        inset 0 1px 0 rgba(255, 255, 255, 0.6) !important;
}

.mud-theme-dark .appt-viewtoggle.appt-viewtoggle {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%) !important;
    border: 1px solid rgba(255, 255, 255, 0.12) !important;
    box-shadow:
        0 6px 16px rgba(0, 0, 0, 0.35),
        inset 0 1px 0 rgba(255, 255, 255, 0.10) !important;
}

/* A hover fill of --mud-palette-surface is opaque, so hovering a day punched a solid rectangle
   through the wash. A white veil is the glass equivalent. */
.appt-weekday:hover,
.appt-cal-day:hover {
    background: rgba(255, 255, 255, 0.35);
}
.mud-theme-dark .appt-weekday:hover,
.mud-theme-dark .appt-cal-day:hover {
    background: rgba(255, 255, 255, 0.08);
}

/* The selected day is the one thing on the screen that should read as SOLID — it is the page's
   subject. It gets the icon-tile treatment instead of the pane treatment: an ambient shadow in
   its own hue, so it sits on the glass the way a Home icon tile does. */
.appt-weekday.is-selected,
.appt-cal-day.is-selected {
    box-shadow:
        0 8px 18px -6px hsl(220deg 40% 55% / 0.45),
        0 3px 8px -4px hsl(220deg 40% 55% / 0.45);
}
.mud-theme-dark .appt-weekday.is-selected,
.mud-theme-dark .appt-cal-day.is-selected {
    box-shadow: 0 8px 22px -6px hsl(220deg 40% 55% / 0.45);
}

/* Same hue-matched glow, keyed to the FAB's own colour. */
.mud-fab.appt-fab {
    box-shadow:
        0 10px 22px -6px rgba(21, 93, 252, 0.45),
        0 4px 10px -4px rgba(21, 93, 252, 0.45) !important;
}

/* Count badges sat on --mud-palette-background-gray / --tertiary, both opaque. */
.appt-allday-count,
.appt-group-count {
    background: rgba(255, 255, 255, 0.45);
    border-color: rgba(255, 255, 255, 0.6);
}
.mud-theme-dark .appt-allday-count,
.mud-theme-dark .appt-group-count {
    background: rgba(255, 255, 255, 0.10);
    border-color: rgba(255, 255, 255, 0.12);
}

/* The month card's in-progress tint was a background wash, which the glass surface replaces —
   the state now lives entirely in the left rail restated above. Nothing to declare here; the
   note stays so the missing rule doesn't read as an oversight. */

/* ============================================================
   Products on glass — EXPERIMENTAL
   Prefix: .products-

   Same three pieces as Home / Jobs / Calendar: .md-glass-wash on the page, .md-glass-card on
   each surface, .md-icon-tile on each leading icon. The page is a fixed 100dvh flex column, so
   like Jobs (and unlike Home) the wash's radial percentages stay compact.

   Two things are specific to this screen:

   1. THE PINNED CHROME IS A BAR, NOT A CARD. Topbar, context row, answer and the warning strips
      are flush-stacked full-width blocks. They take the glass SURFACE without the 24px radius —
      one frosted header region divided by the glass hairline — because a stack of five rounded
      panes reads as debris rather than as a header.
   2. THE GROUPS BECOME PANES. The list was deliberately "ruled, not carded" (see the notes on
      .products-group), and putting each section on glass reverses that decision. That is the
      point of the change: a pane is what the rest of the mobile app now uses for a section, and
      a ruled list among carded screens is the odd one out.

   Placed at the end of the file for the same cascade reason as the calendar block: .md-glass-card
   is !important at (0,3,0), so anything that has to survive it must come after it.
   ============================================================ */

/* The wash lives on .products-page-content; every ground between it and the panes has to get out
   of the way or it paints over the blooms. */
.products-list-scroll {
    background-color: transparent;
}

.products-lines {
    background: transparent;
}

/* --- The frosted header stack ------------------------------------------------------------- */
.products-topbar,
.products-context,
.products-answer {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.20) 100%);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-bottom: 1px solid rgba(255, 255, 255, 0.6);
}

.mud-theme-dark .products-topbar,
.mud-theme-dark .products-context,
.mud-theme-dark .products-answer {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%);
    border-bottom-color: rgba(255, 255, 255, 0.12);
}

/* The two warning-tinted members of the same stack. The tint survives as a veil UNDER the glass
   gradient rather than as an opaque fill — it is still what marks them out from the answer above,
   just at glass weight. */
.products-shortfall,
.products-offline-strip {
    background:
        linear-gradient(135deg,
            rgba(255, 255, 255, 0.42) 0%,
            rgba(255, 255, 255, 0.14) 100%),
        color-mix(in srgb, var(--mud-palette-warning) 16%, transparent);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-bottom: 1px solid rgba(255, 255, 255, 0.6);
}

.mud-theme-dark .products-shortfall,
.mud-theme-dark .products-offline-strip {
    background:
        linear-gradient(135deg,
            rgba(255, 255, 255, 0.10) 0%,
            rgba(255, 255, 255, 0.04) 100%),
        color-mix(in srgb, var(--mud-palette-warning) 20%, transparent);
    border-bottom-color: rgba(255, 255, 255, 0.12);
}

/* --- Controls floating on the wash --------------------------------------------------------- */
/* The tab track and the two scope pills. Opaque fills here punched solid rectangles through the
   frosted header they sit on. */
.products-seg-group {
    background: rgba(255, 255, 255, 0.28);
}

.products-seg.active {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.70) 0%,
        rgba(255, 255, 255, 0.35) 100%);
    border-color: rgba(255, 255, 255, 0.75);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.products-scope-chip {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.20) 100%);
    border-color: rgba(255, 255, 255, 0.6);
}

.mud-theme-dark .products-seg-group {
    background: rgba(255, 255, 255, 0.06);
}

.mud-theme-dark .products-seg.active {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.16) 0%,
        rgba(255, 255, 255, 0.06) 100%);
    border-color: rgba(255, 255, 255, 0.18);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.30);
}

.mud-theme-dark .products-scope-chip {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%);
    border-color: rgba(255, 255, 255, 0.12);
}

/* --- Section panes ------------------------------------------------------------------------- */
/* The pane's 24px corner has to clip its contents, and the header's 16px of padding sat too close
   to that curve — 18px is what the corner needs, the same adjustment Home made when its cards went
   from 14px to 24px. */
.products-group {
    overflow: hidden;
}

.products-group + .products-group {
    margin-top: 12px;
}

.products-group .products-group-open {
    padding: 12px 18px;
}

/* Every rule INSIDE a pane matches the glass edge. Mud's hairline is a dark line, which on a
   translucent surface reads as a seam cutting the pane rather than a rule drawn on it. */
.products-group:has(.products-lines) .products-group-head,
.products-group .products-line,
.products-group .products-line-wrap,
.products-list-scroll > .products-lines .products-line {
    border-bottom-color: rgba(255, 255, 255, 0.55);
}

.mud-theme-dark .products-group:has(.products-lines) .products-group-head,
.mud-theme-dark .products-group .products-line,
.mud-theme-dark .products-group .products-line-wrap,
.mud-theme-dark .products-list-scroll > .products-lines .products-line {
    border-bottom-color: rgba(255, 255, 255, 0.12);
}

/* The skeleton stands in for the panes that are coming, so its separators follow them. */
.products-skeleton-group {
    border-bottom-color: rgba(255, 255, 255, 0.55);
}

.mud-theme-dark .products-skeleton-group {
    border-bottom-color: rgba(255, 255, 255, 0.12);
}

/* --- The collect bar ----------------------------------------------------------------------- */
/* The primary stays a solid blue button — it is the screen's one committing action and glass would
   take exactly the weight that makes it findable. It gets the icon-tile treatment instead: an
   ambient shadow in its OWN hue rather than the neutral grey drop it had, which over a translucent
   page read as dirt under the bar. */
.products-collect-primary {
    box-shadow:
        0 12px 24px -8px rgba(21, 93, 252, 0.50),
        0 4px 10px -4px rgba(21, 93, 252, 0.45);
}

.products-collect-secondary,
.products-collect-note {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.65) 0%,
        rgba(255, 255, 255, 0.32) 100%);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-color: rgba(255, 255, 255, 0.7);
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.06),
        inset 0 1px 0 rgba(255, 255, 255, 0.6);
}

.mud-theme-dark .products-collect-secondary,
.mud-theme-dark .products-collect-note {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.12) 0%,
        rgba(255, 255, 255, 0.05) 100%);
    border-color: rgba(255, 255, 255, 0.16);
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.35),
        inset 0 1px 0 rgba(255, 255, 255, 0.10);
}

/* The shortfall's own action sits ON the warning strip, so it takes the same treatment or it is
   the one opaque chip in the header stack. */
.products-shortfall-action {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.65) 0%,
        rgba(255, 255, 255, 0.32) 100%);
    border-color: rgba(255, 255, 255, 0.7);
}

.mud-theme-dark .products-shortfall-action {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.12) 0%,
        rgba(255, 255, 255, 0.05) 100%);
    border-color: rgba(255, 255, 255, 0.16);
}

/* The empty state and the van's restock panel stand on the wash as panes of their own; the restock
   panel takes .md-glass-card in the markup, and the empty state stays unboxed on the wash with its
   icon promoted to a tile. Nothing to declare for either. */

/* The van's standalone list is a pane too, so its 24px corners have to clip the first and last
   rows — without this the rows square off the top and bottom of the glass. */
.products-list-scroll > .products-lines {
    overflow: hidden;
}

/* --- The purchase-flow sections ------------------------------------------------------------
   The stock screens that are not the Products hub — raise an order, collect one at a counter,
   record a card purchase, count the van — build their bodies out of .products-purchase-section
   rather than .products-group. They already carried .md-glass-wash on the page, which made them
   WORSE than before the glass work rather than better: the blooms were painting behind sections
   that were still an opaque --mud-palette-surface fill with a 5px radius and a dark hairline
   border, so the wash only ever showed in the gaps between flat white cards.

   So the section becomes the same pane the Products hub's groups are. Same construction as
   .md-glass-card, spelled out here rather than added to five markup files, because the class is
   already the shared section wrapper across all of them — the same argument the bottom-sheet block
   below makes for doing it by selector. */
.products-purchase-section {
    /* 18px, not the 16px it had: a 24px corner needs the extra or the heading crowds the curve.
       Same adjustment .products-group-open made for the same reason. */
    padding: 18px;
    border-radius: 24px;
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.20) 100%);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid rgba(255, 255, 255, 0.6);
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.04),
        0 20px 40px rgba(14, 165, 233, 0.08),
        inset 0 1px 0 rgba(255, 255, 255, 0.6);
}

.mud-theme-dark .products-purchase-section {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%);
    border-color: rgba(255, 255, 255, 0.12);
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.35),
        inset 0 1px 0 rgba(255, 255, 255, 0.10);
}

/* Rules INSIDE the pane match its edge, for the reason given above the .products-group rules:
   Mud's dark hairline on a translucent surface reads as a seam cutting the pane rather than a
   line drawn on it. The last row drops its rule entirely — on a pane the corner is the ending,
   and a hairline floating above it is a rule to nowhere. */
.products-purchase-section .products-purchase-line,
.products-purchase-section .products-po-step {
    border-bottom-color: rgba(255, 255, 255, 0.55);
}

.products-purchase-section .products-purchase-line:last-child {
    border-bottom: 0;
}

.mud-theme-dark .products-purchase-section .products-purchase-line,
.mud-theme-dark .products-purchase-section .products-po-step {
    border-bottom-color: rgba(255, 255, 255, 0.12);
}

/* The docket/receipt thumbnail sits inside a pane, so its corner follows the pane's rather than
   staying the pre-glass 5px — a square-ish photo in a 24px pane is the one hard corner on screen. */
.products-purchase-receipt-img {
    border-radius: 14px;
    border-color: rgba(255, 255, 255, 0.6);
}

.mud-theme-dark .products-purchase-receipt-img {
    border-color: rgba(255, 255, 255, 0.12);
}

/* Deliberately NOT declared here: .products-sheet-note and .products-empty-state. Both are
   page-level commentary standing on the wash rather than content inside a pane, and boxing them
   would make an aside read as another section. They were already transparent — this note exists
   so the next reader knows that is a decision and not an omission. */
/* ============================================================
   Bottom sheets on glass — EXPERIMENTAL
   Prefix: .mobile-bottom-sheet (MobileBottomSheet.razor)

   ONE BLOCK COVERS EVERY SHEET. MobileBottomSheet is the shell for ~22 dialogs, each of which
   brings its own bespoke row and card classes, so the glass is applied here by selector rather
   than by adding md-glass-card to two dozen files. Adding it to one more sheet later means adding
   its row class to the pane list below — nothing else.

   THE SHEET IS THE PAGE. It carries .md-glass-wash in the component markup (not a translucent
   surface over the scrim): the wash is what the panes inside sample, and without it every card in
   here would be backdrop-filtering the darkened page behind the dialog, which is unreadable over
   arbitrary content. So a sheet reads exactly like Home — blooms behind, panes on top.

   WHY THE .mobile-bottom-sheet PREFIX ON EVERYTHING: each sheet's own rules live in a page style
   block, and page style blocks render in the body where they win source-order ties against this
   linked file. The prefix buys a specificity step (0,2,0) so these win outright; the doubled-class
   selectors buy a second step where the sheet's own rule is itself (0,2,0), e.g. a
   .mud-theme-dark override.
   ============================================================ */

/* The sheet's top edge; the surface itself is the wash. */
.mobile-bottom-sheet.mobile-bottom-sheet {
    border-top: 1px solid rgba(255, 255, 255, 0.6);
    box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.12);
}

.mud-theme-dark .mobile-bottom-sheet.mobile-bottom-sheet {
    border-top-color: rgba(255, 255, 255, 0.12);
    box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.45);
}

/* The drag handle reads as the glass edge rather than as Mud's dark hairline. */
.mobile-bottom-sheet .mbs-handle {
    background: rgba(255, 255, 255, 0.75);
}

.mud-theme-dark .mobile-bottom-sheet .mbs-handle {
    background: rgba(255, 255, 255, 0.22);
}

/* --- Panes --------------------------------------------------------------------------------
   Every card- or row-shaped child across the sheets. 18px rather than the pages' 24px: these are
   48–56px rows on a narrow surface, and a 24px corner on a 48px row is most of the way to a
   stadium.

   !important throughout for the same reason .md-glass-card uses it — several sheets set their
   surface with an inline style attribute, which nothing but !important can beat. */
.mobile-bottom-sheet .mbs-content .mud-paper,
.mobile-bottom-sheet .mp-card,
.mobile-bottom-sheet .form-card,
.mobile-bottom-sheet .gs-result,
.mobile-bottom-sheet .task-details-card,
.mobile-bottom-sheet .plan-picker-row,
.mobile-bottom-sheet .plan-asset-picker-row,
.mobile-bottom-sheet .plan-asset-actions-row,
.mobile-bottom-sheet .ca-actions-row,
.mobile-bottom-sheet .au-actions-row,
.mobile-bottom-sheet .jas-row,
.mobile-bottom-sheet .atd-row,
.mobile-bottom-sheet .njs-body,
.mobile-bottom-sheet .products-sheet-option,
.mobile-bottom-sheet .products-sheet-source {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.20) 100%) !important;
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid rgba(255, 255, 255, 0.6) !important;
    border-radius: 18px !important;
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.04),
        0 20px 40px rgba(14, 165, 233, 0.08),
        inset 0 1px 0 rgba(255, 255, 255, 0.6) !important;
}

.mud-theme-dark .mobile-bottom-sheet .mbs-content .mud-paper,
.mud-theme-dark .mobile-bottom-sheet .mp-card,
.mud-theme-dark .mobile-bottom-sheet .form-card,
.mud-theme-dark .mobile-bottom-sheet .gs-result,
.mud-theme-dark .mobile-bottom-sheet .task-details-card,
.mud-theme-dark .mobile-bottom-sheet .plan-picker-row,
.mud-theme-dark .mobile-bottom-sheet .plan-asset-picker-row,
.mud-theme-dark .mobile-bottom-sheet .plan-asset-actions-row,
.mud-theme-dark .mobile-bottom-sheet .ca-actions-row,
.mud-theme-dark .mobile-bottom-sheet .au-actions-row,
.mud-theme-dark .mobile-bottom-sheet .jas-row,
.mud-theme-dark .mobile-bottom-sheet .atd-row,
.mud-theme-dark .mobile-bottom-sheet .njs-body,
.mud-theme-dark .mobile-bottom-sheet .products-sheet-option,
.mud-theme-dark .mobile-bottom-sheet .products-sheet-source {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%) !important;
    border: 1px solid rgba(255, 255, 255, 0.12) !important;
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.35),
        0 20px 40px rgba(14, 165, 233, 0.10),
        inset 0 1px 0 rgba(255, 255, 255, 0.10) !important;
}

/* Nothing is needed to clear the `+ row { border-top }` separators these lists used, or
   .gs-result's / .atd-row's bottom rule: the pane's own `border` shorthand above is !important and
   writes all four sides, so a hairline between two floating panes can't survive it. Noted because
   its absence otherwise reads as an oversight. */

/* Rows authored as full-bleed list items carry almost no horizontal padding, which on a pane puts
   their text against the curve. The gap between panes is the same 8px the pages use. */
.mobile-bottom-sheet .ca-actions-row,
.mobile-bottom-sheet .au-actions-row,
.mobile-bottom-sheet .jas-row,
.mobile-bottom-sheet .plan-asset-actions-row,
.mobile-bottom-sheet .atd-row,
.mobile-bottom-sheet .njs-body,
.mobile-bottom-sheet .products-sheet-option,
.mobile-bottom-sheet .products-sheet-source {
    padding-left: 14px;
    padding-right: 14px;
    margin-bottom: 8px;
}

.mobile-bottom-sheet .ca-actions-row:last-child,
.mobile-bottom-sheet .au-actions-row:last-child,
.mobile-bottom-sheet .jas-row:last-child,
.mobile-bottom-sheet .plan-asset-actions-row:last-child,
.mobile-bottom-sheet .atd-row:last-child,
.mobile-bottom-sheet .products-sheet-option:last-child,
.mobile-bottom-sheet .products-sheet-source:last-child {
    margin-bottom: 0;
}

/* --- Icon tiles ----------------------------------------------------------------------------
   The action sheets' 36px circles are icon tiles in all but name, so they take the tile treatment:
   an AMBIENT shadow in the tile's OWN hue. A neutral grey shadow over a translucent pane reads as
   dirt; a hue-matched one reads as depth.

   These all set `color` to their hue and fill with a pale wash of it, so currentColor IS the hue
   and one rule covers every variant — including .danger, which recolours both. */
.mobile-bottom-sheet [class*="actions-icon"] {
    box-shadow:
        0 8px 18px -6px color-mix(in srgb, currentColor 45%, transparent),
        0 3px 8px -4px color-mix(in srgb, currentColor 45%, transparent);
}

/* Dark: the tight contact layer has no light surface beneath it to darken and only muddies the
   tile. The wide bloom alone carries the lift. */
.mud-theme-dark .mobile-bottom-sheet [class*="actions-icon"],
.mud-theme-dark .mobile-bottom-sheet .atd-icon,
.mud-theme-dark .mobile-bottom-sheet .form-card-icon {
    box-shadow: 0 8px 22px -6px var(--mbs-glow, hsl(220deg 40% 55% / 0.45));
}

/* The running-timer circles are a solid fill with a white glyph, so currentColor is white and the
   hue has to be named. Same two values the fills use. */
.mobile-bottom-sheet .atd-icon.travel { --mbs-glow: rgba(234, 88, 12, 0.45); }
.mobile-bottom-sheet .atd-icon.onsite { --mbs-glow: rgba(22, 163, 74, 0.45); }
.mobile-bottom-sheet .form-card-icon  { --mbs-glow: rgba(94, 53, 177, 0.42); }

.mobile-bottom-sheet .atd-icon,
.mobile-bottom-sheet .form-card-icon {
    box-shadow:
        0 8px 18px -6px var(--mbs-glow),
        0 3px 8px -4px var(--mbs-glow);
}

/* Not tiled on purpose: .gs-result-icon takes its colour from an inline style per result row, so
   there is no hue for a rule here to key a glow to, and .asset-suggestion-icon is a photo rather
   than a glyph. Both keep their existing treatment. */

/* --- Controls on the sheet ------------------------------------------------------------------
   The search row's field is a solid box by design (see MobileBottomSheet's own note) — over a wash
   it becomes the same translucent pane as everything else. */
.mobile-bottom-sheet .mbs-search .mud-input.mud-input-outlined {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.65) 0%,
        rgba(255, 255, 255, 0.32) 100%);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
}

.mud-theme-dark .mobile-bottom-sheet .mbs-search .mud-input.mud-input-outlined {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.12) 0%,
        rgba(255, 255, 255, 0.05) 100%);
}

/* Stragglers: three more sheets whose child surfaces don't share any of the class names above.
   .property-detail-card is a shared class (the Property page uses it too), so it is deliberately
   scoped to the sheet rather than promoted globally. */
.mobile-bottom-sheet .property-detail-card,
.mobile-bottom-sheet .asset-suggestion-row {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.20) 100%) !important;
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid rgba(255, 255, 255, 0.6) !important;
    border-radius: 18px !important;
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.04),
        0 20px 40px rgba(14, 165, 233, 0.08),
        inset 0 1px 0 rgba(255, 255, 255, 0.6) !important;
}

.mud-theme-dark .mobile-bottom-sheet .property-detail-card,
.mud-theme-dark .mobile-bottom-sheet .asset-suggestion-row {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%) !important;
    border: 1px solid rgba(255, 255, 255, 0.12) !important;
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.35),
        0 20px 40px rgba(14, 165, 233, 0.10),
        inset 0 1px 0 rgba(255, 255, 255, 0.10) !important;
}

.mobile-bottom-sheet .asset-suggestion-row {
    padding-left: 14px;
    padding-right: 14px;
    margin-bottom: 8px;
}

/* A pinned controls strip, not a card: on the wash its solid fill was an opaque band across the
   top of the sheet. It has no business being a pane either — it holds chips. */
.mobile-bottom-sheet .job-list-sheet-controls {
    background: transparent;
}

/* ============================================================
   Detail pages on glass — EXPERIMENTAL
   Prefix: .md-card / .md-page .custom-border / .job-detail-

   Same three pieces as Home / Jobs / Calendar / Products: .md-glass-wash on the body,
   a glass surface on every section pane, .md-icon-tile on leading icons.

   THE MOBILE APP HAS TWO CARD IDIOMS, and this block converts both:

     .md-card               — Job Detail, Proposal(s), the job section components  (~104 uses)
     .md-page .custom-border — Property, Asset, Defect, Appointment, Assets, Defects (~139 uses)

   They are converted together on purpose. They were never meaningfully different — one is a
   class, the other is a MudPaper wearing a class — and leaving one solid while the other went
   glass is what made Jobs list → Job detail read as a jump. Anything still solid after this
   is solid because it was DECIDED, not because it was missed.

   Per-screen exceptions:

   1. THE JOB DETAIL HEADER AND TIMER BAR STAY DARK. They are that page's one deliberately-opaque
      region — an anchored chrome block the body scrolls under — and frosting them would leave
      the screen with no fixed edge at all. Only the tab strip below them goes glass, because it
      is sticky over the wash and an opaque fill would scroll a hard band across the blooms.

   Placed at the end of the file for the same cascade reason as the calendar and products
   blocks: .md-glass-card is !important at (0,3,0), so anything that must survive it comes after.
   ============================================================ */

/* --- The section panes, both idioms -------------------------------------------------------
   Radius is NOT set here — .md-card reads --md-card-radius (default 24px) and .custom-border
   sets its own below, so the silhouette stays where each idiom already decided it. */
.md-card,
.md-page .custom-border {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.2) 100%);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid rgba(255, 255, 255, 0.6) !important;
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.04),
        0 20px 40px rgba(14, 165, 233, 0.08),
        inset 0 1px 0 rgba(255, 255, 255, 0.6);
}

.mud-theme-dark .md-card,
.mud-theme-dark .md-page .custom-border {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%);
    border: 1px solid rgba(255, 255, 255, 0.12) !important;
    box-shadow:
        0 12px 32px rgba(0, 0, 0, 0.35),
        0 20px 40px rgba(14, 165, 233, 0.10),
        inset 0 1px 0 rgba(255, 255, 255, 0.10);
}

/* .custom-border's own rule up at ~62 is `border: 0 !important` plus a 14px radius and the
   pre-glass periwinkle contact shadow. The border is why the two declarations above carry
   !important — without it the glass edge is wiped and the panes lose their rim. Radius comes
   up to the house 24px here, and the old shadow is superseded by the stack above. */
.md-page .custom-border {
    border-radius: 24px !important;
}

/* MudPaper paints --mud-palette-surface at (0,1,0) and .mud-paper-outlined adds its own border;
   both would show through the translucent fill as a solid underlay. */
.md-page .custom-border.mud-paper,
.md-page .custom-border.mud-paper-outlined {
    background-color: transparent;
}

/* --- Insets ON a pane ---------------------------------------------------------------------
   Same reasoning as the Jobs list: on glass, white is the one colour the pane can never be, so
   an opaque inner block is the loudest thing on the card. These are lighter than the pane's own
   0.55 because they sit ON it — at the pane's alpha they'd out-white their own parent.

   Declared on .md-page (not per screen) so every detail page reads the same two numbers. */
.md-page,
.job-detail-dialog {
    --md-inset-fill: rgba(255, 255, 255, 0.45);
    --md-inset-edge: rgba(255, 255, 255, 0.65);
}

.mud-theme-dark .md-page,
.mud-theme-dark .job-detail-dialog {
    --md-inset-fill: rgba(255, 255, 255, 0.08);
    --md-inset-edge: rgba(255, 255, 255, 0.12);
}

.job-detail-dialog .job-detail-time-card,
.job-detail-dialog .job-detail-asset-card,
.job-detail-dialog .job-detail-timer-strip,
.job-detail-dialog .job-eta-chip {
    background: var(--md-inset-fill);
    border: 1px solid var(--md-inset-edge);
}

/* --- Lines inside a pane ------------------------------------------------------------------
   Mud's dark hairline reads as a seam cutting the pane rather than a rule drawn on it. */
.md-card .mud-divider,
.md-card hr,
.md-page .custom-border .mud-divider,
.md-page .custom-border hr {
    border-color: rgba(255, 255, 255, 0.55);
}

.mud-theme-dark .md-card .mud-divider,
.mud-theme-dark .md-card hr,
.mud-theme-dark .md-page .custom-border .mud-divider,
.mud-theme-dark .md-page .custom-border hr {
    border-color: rgba(255, 255, 255, 0.10);
}

/* --- The bottom dock ----------------------------------------------------------------------
   A frosted bar, like the tab strip at the other end of the body. It keeps its top border and
   drops the drop shadow: the shadow existed to lift an opaque panel off an opaque body, and on
   glass it just muddies the wash under it. */
.job-detail-dialog .complete-dock .dock-inner {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.55) 0%,
        rgba(255, 255, 255, 0.2) 100%);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid rgba(255, 255, 255, 0.6);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
}

.mud-theme-dark .job-detail-dialog .complete-dock .dock-inner {
    background: linear-gradient(135deg,
        rgba(255, 255, 255, 0.10) 0%,
        rgba(255, 255, 255, 0.04) 100%);
    border: 1px solid rgba(255, 255, 255, 0.12);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
}

/* ============================================================
   Dialogs on glass — EXPERIMENTAL
   Prefix: .md-dialog-

   The mobile app has THREE dialog hosts, and this block is the third of them:

     MobileBottomSheet   — already glass (see "Bottom sheets on glass" above)   ~20 dialogs
     .md-dialog-body     — this block                                           ~43 dialogs
     bespoke shells      — own block each (search, notifications, assistant)    ~35 dialogs

   .md-dialog-body was a flat --mud-palette-tertiary field, which is the same lavender the
   wash uses as ITS base — so the conversion is simply letting the blooms happen. Nothing
   about the layout changes.

   THE DARK HEADER STAYS. .md-dialog-header is a fixed #0B1220 bar, deliberately identical in
   light and dark mode. It is the same call as the Job Detail header: it's the anchored chrome
   the body scrolls under, and frosting it leaves the sheet with no fixed edge.
   ============================================================ */

.md-dialog-body {
    background:
        radial-gradient(60% 25% at 15% 4%,  rgba(14, 165, 233, 0.30), transparent 70%),
        radial-gradient(50% 20% at 90% 18%, rgba(56, 189, 248, 0.24), transparent 70%),
        radial-gradient(70% 30% at 40% 55%, rgba(37, 99, 235, 0.20), transparent 70%),
        radial-gradient(60% 25% at 80% 88%, rgba(14, 165, 233, 0.24), transparent 70%),
        var(--mud-palette-tertiary);

    /* Same two numbers every other glass surface uses, so a dialog's own local classes have
       something to adopt instead of each one inventing an alpha. */
    --md-inset-fill: rgba(255, 255, 255, 0.45);
    --md-inset-edge: rgba(255, 255, 255, 0.65);
}

.mud-theme-dark .md-dialog-body {
    background:
        radial-gradient(60% 25% at 15% 4%,  rgba(14, 165, 233, 0.16), transparent 70%),
        radial-gradient(70% 30% at 60% 60%, rgba(37, 99, 235, 0.12), transparent 70%),
        var(--mud-palette-tertiary);
    --md-inset-fill: rgba(255, 255, 255, 0.08);
    --md-inset-edge: rgba(255, 255, 255, 0.12);
}

/* A bare MudPaper inside a dialog body is the generic "block of content" these dialogs reach
   for; left opaque it is the same white-sticker problem the Jobs list had. Scoped to the body
   so Mud's own internals (menus, pickers, overlays) are untouched. */
.md-dialog-body > .mud-paper,
.md-dialog-body > * > .mud-paper {
    background-color: var(--md-inset-fill);
    border: 1px solid var(--md-inset-edge);
}

/* The chip rows the filter-style dialogs are built from. They read as pills stamped out of
   white card stock; on the wash they become pills cut into it. */
.md-dialog-body .jf-chip {
    background: var(--md-inset-fill);
    border-color: var(--md-inset-edge);
}

/* --- What deliberately stays solid --------------------------------------------------------
   Three groups are NOT on glass, and the note is here so their absence doesn't read as an
   oversight the next time someone audits this:

   1. MODAL PROMPTS AND POPOVERS — confirm/delete/unsaved-changes, the timesheet date picker,
      the small pickers. Glass works because there is a WASH BEHIND IT to bloom through. A modal
      floats over a dimmed scrim, so a translucent fill samples the scrim and the dimmed page
      under it: it gets muddier and harder to read, not lighter. A modal is a card over the app,
      not a page, and it should keep saying so.
   2. MEDIA VIEWERS — image/photo/carousel/annotate dialogs. The surface IS the photo; a pane
      around it is chrome competing with the content.
   3. ANCHORED CHROME — .md-dialog-header, .md-header, the Job Detail header and timer bar, the
      notification appbar. Every screen needs one fixed opaque edge or the whole thing floats.
   ------------------------------------------------------------------------------------------ */


/* ============================================================
   .md-glass-flat — glass WITHOUT backdrop-filter, for rows that scroll
   ============================================================
   ANDROID WEBVIEW BUG. .md-glass-surface / .md-glass-card carry backdrop-filter: blur(20px).
   That is fine for a handful of static panes, and wrong for a long virtualized list where
   every row is one. Three failures compound on Android (WKWebView on iOS absorbs them, which
   is why this only ever reproduced on the Jobs screens):

   1. SCALED BLUR SNAPSHOT. A transform animation on (or above) an element that owns a
      backdrop-filter makes Chromium capture the backdrop ONCE and stretch that cached bitmap
      for the animation's duration. On a slow GPU the last frame never invalidates it, so a
      smeared, upscaled snapshot of the whole page is what stays on screen. Both animations
      the Jobs list runs are transforms: [data-reveal].is-revealed on group headers, and
      .job-card-exiting on a completed card.
   2. BACKDROP-ROOT ESCALATION. Each backdrop-filter samples everything painted below it. Ten
      visible glass rows plus the glass toolbar plus the wash overruns WebView's layer budget
      and it falls back to filtering the ROOT layer — the entire page blurs, not the backdrop
      of one card.
   3. LAYER CHURN. <Virtualize> recycles rows as you scroll, re-promoting a blur layer each
      time. WebView routinely leaves the stale composited tile behind, which reads as ghosting
      that survives until something forces a full repaint.

   The fix costs nothing visually: the thing these panes blur is .md-glass-wash, four soft
   radial gradients. Blurring a low-frequency gradient returns very nearly the same gradient —
   the same reasoning the block at the top of the glass section already spells out for a flat
   colour. So the background, border and shadows are left exactly as .md-glass-surface sets
   them and ONLY the filter is dropped.

   Keep the blur on panes that are static and few — the toolbar, the summary card, dialogs and
   sheets. Reach for .md-glass-flat on anything inside a scroller, a <Virtualize>, or an
   element that animates transform.

   AT THE END OF THE FILE ON PURPOSE: this has to null a property that .md-glass-surface sets
   at (0,3,0). The doubled class matches that specificity, so source order is what decides the
   tie — and several screen-specific blocks above also restate glass at (0,3,0).
   ============================================================ */
.md-glass-flat.md-glass-flat.md-glass-flat {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
}


/* ============================================================
   ANDROID WEBVIEW GLASS — SECOND PASS (the .md-glass-flat block above was necessary,
   not sufficient)
   ============================================================
   The block above put .md-glass-flat on three elements: the Jobs group header and the two
   rail-card idioms. Everything else on those screens kept its blur, and the three failure
   modes it documents are all COUNT-driven, not element-driven — so the screen still went
   smeared/washed on Android. What was left behind:

   1. THE CHIPS. The mobile copy of CustomChip adds .md-glass-chip (backdrop-filter: blur(14px))
      to EVERY Light-variant chip, and a Jobs row carries three to five of them (breach,
      priority, job type, period, note indicators). Ten virtualized rows is thirty-plus blur
      layers inside one scroller — on its own that overruns the layer budget and trips
      BACKDROP-ROOT ESCALATION, which is the whole-page wash in the reporter's second capture.
   2. THE PANES INSIDE THE SCROLLER. .jobs-summary-card and .jobs-breach-alert are plain
      .md-glass-surface and live INSIDE .jobs-list-scroll, so they are blur layers that travel
      under a composited scroll — SCALED BLUR SNAPSHOT, the progressive top-to-bottom fade in
      the first capture.
   3. THE DETAIL SECTIONS. .md-card owns blur(20px) and the Job/Asset/Property detail sections
      that carry [data-reveal] ARE .md-card — a transform animation directly on an element that
      owns a backdrop-filter, failure mode 1 verbatim.

   The rule is therefore stated as an invariant rather than as another hand-applied class:
   NOTHING that animates a transform, and nothing inside a virtualized scroller, owns a
   backdrop-filter. What blurs is still the wash — four soft radial gradients — so dropping the
   filter costs essentially nothing visually, exactly as the .md-glass-flat block argues. The
   background, border and shadow are untouched everywhere; only the filter goes.

   Deliberately NOT flattened: .jobs-toolbar and the dialog/sheet panes. Those are static, few,
   and outside the scroller — the one or two layers WebView handles fine, and they are where the
   frosted look actually reads.
   ============================================================ */
[data-reveal],
[data-reveal] *,
.md-glass-chip.md-glass-chip,
.jobs-list-scroll * {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
}

/* will-change is a promise, not a hint: left on permanently it keeps every revealed section on
   its own composited layer for the life of the page, which is the memory pressure half of the
   same problem. scroll-reveal.js reveals once and unobserves, so once .is-revealed lands the
   element will never animate again and the layer can be released. */
[data-reveal].is-revealed {
    will-change: auto;
}
