/* ============================================================
   DESIGN D — "Glass Cohesion"
   Source: Claude Design project b7a2734f-5d21-4eba-970b-d7dfc6691764,
   "Property Screen - Glass Cohesion.dc.html", Direction D ("B + glass bar").

   The shape of the system, because it is easy to get backwards:

     GLASS  — only three surfaces. The app bar, the nav menu, and the page-header
              chrome panel. Translucent; the app bar and nav menu also blur.
     SOLID  — every content card is #ffffff with NO blur. Its lift comes from an 18px
              radius, a WHITE rim and a large soft shadow. On the blue ground a white
              rim reads as a highlight, not a border: the edge is meant to nearly
              vanish and the shadow does the separating.
     GROUND — a blue gradient on <body> (see the .main-container block for why it must
              be body and not .mud-main-content).

   Light theme only for now. The dark palette still renders, but these white-alpha
   fills have no dark counterparts yet — see --defex-card-edge, the glass tokens and
   the body gradient.
   ============================================================ */
:root {
    /* Radius scale. Before this there was no general-purpose family — radius was spread
       across the Mud token (5px), .round (14px), .custom-radius-2 (12px), --wgt-radius-*
       and ~33 literals in this file. */
    --defex-radius-panel: 18px;    /* cards, nav menu, chrome panel */
    --defex-radius-inner: 10px;    /* panels nested inside a card */
    --defex-radius-control: 24px;   /* buttons, inputs, tabs */
    --defex-radius-nav: 9px;       /* nav rows */
    --defex-radius-tile: 12px;     /* entity icon tile */
    --defex-radius-pill: 999px;

    --defex-glass-bar: rgba(255, 255, 255, 0.62);
    --defex-glass-panel: rgba(255, 255, 255, 0.66);
    --defex-glass-edge: rgba(255, 255, 255, 0.85);

    --defex-card-bg: #ffffffc7;
    --defex-card-edge: rgba(255, 255, 255, 0.90);
    --defex-card-shadow: 0 16px 44px rgba(15, 23, 43, 0.10);
    --defex-panel-shadow: 0 10px 32px rgba(15, 23, 43, 0.07);
    --defex-menu-shadow: 0 16px 44px rgba(15, 23, 43, 0.12);

    --defex-inner-bg: #F4F6FA;
    --defex-inner-edge: #E4E9F3;
    --defex-ghost-edge: #DEE2ED;
    --defex-rule: rgba(15, 23, 43, 0.08);

    /* Design D's stops are #dde7f8 → #e9eff9 40% → #f2f5fb 100%. The final stop is lifted here
       from #f2f5fb to #e7edf9 and pushed out to 100% via a later midpoint, because D's value goes
       essentially white: with background-attachment:fixed the gradient box is the VIEWPORT, so on
       a tall window that near-white stop arrives around mid-screen and the lower half loses all
       colour. Deviation from the design, made deliberately to keep the wash continuous. */
    --defex-page-ground: linear-gradient(165deg, #dde7f8 0%, #e6edfa 45%, #e7edf9 100%);
}

/* THE GROUND. On <body>, NOT on .mud-main-content — MudBlazor margin-lefts the main
   content by the drawer width, so the strip behind the fixed drawer would fall through
   to body's white. The nav menu's backdrop-filter would then sample flat white, and
   blurring a flat colour returns that colour, so the menu would render as a plain white
   slab. Same failure mode documented at mobile-detail.css:4295. */
/* The blooms are BACKGROUND LAYERS on body, not pseudo-elements. The design draws them as two
   absolutely-positioned circles, but as layers they need no stacking-context reasoning at all —
   as pseudo-elements they must carry z-index:-1, because ::after is body's last child and
   .mud-layout is z-index:auto, so at z-index 0 tree order wins and the bloom paints as a blue
   film OVER the whole UI.

   background-attachment: fixed applies to every layer, so the whole ground is viewport-anchored
   and stays put while content scrolls — matching D, where the blooms sit on a non-scrolling shell.

   D anchors both blooms to the TOP. A third, low one is added here: with a viewport-sized
   gradient box, two top blooms plus a pale end stop leave the bottom of a tall window with no
   colour at all. Keep the palette to blues — an off-hue bloom reads as a stain, not as light. */
body {
    background:
        radial-gradient(380px 380px at 270px 120px, rgba(21, 93, 252, 0.13), rgba(21, 93, 252, 0)),
        radial-gradient(320px 320px at calc(100% - 210px) 160px, rgba(150, 180, 240, 0.16), rgba(150, 180, 240, 0)),
        /* Bottom lift. A single LINEAR ramp, not radial blooms: two low radials (the first
           attempt) peaked near the bottom edge and overlapped across the width, which read as a
           distinctly bluer band with a visible ramp rather than an even wash. A linear fade to
           transparent at 55% carries colour to the bottom with no locus and no edge. */
        linear-gradient(to top, rgba(21, 93, 252, 0.09) 0%, rgba(21, 93, 252, 0) 55%),
        var(--defex-page-ground);
    background-attachment: fixed;
}

/* ============================================================
   DARK MODE for the Design D surface system.

   Everything above this block is a LIGHT literal — the glass pass gave the app a
   hand-painted ground, card fills and rim colours instead of MudBlazor's palette, and
   wrote no dark counterparts. MudThemeProvider still swaps --mud-palette-* correctly,
   so in dark mode text went to #F8FAFC while every surface stayed pale: white-on-white.

   The marker is stamped on .mud-layout (MainLayout.razor), NOT on <body> — Blazor can
   only render inside the app root, and .mud-layout is the outermost element it owns that
   still contains the appbar, the drawer and the main content.

   The ground is repainted on .mud-layout rather than left on <body> because body cannot
   carry the marker. .mud-layout is full-viewport and opaque here, so body's light ground
   never shows through, and — crucially — the nav drawer is a DESCENDANT of .mud-layout,
   so the panel's backdrop-filter samples this dark ground. Painting the ground any deeper
   (e.g. on .mud-main-content) reintroduces the original bug the body rule was written to
   avoid: MudBlazor margin-lefts the main content by the drawer width, leaving the strip
   behind the fixed drawer unpainted, and blurring a flat colour returns that colour.
   ============================================================ */
.defex-theme-dark {
    /* Mirrors the light ground's 165deg three-stop shape, walked down to the dark
       palette's background (#0B1220) and surface (#16233B) rather than to flat black —
       a pure-black ground makes the cards above it read as floating grey rectangles. */
    --defex-page-ground: linear-gradient(165deg, #0B1220 0%, #101B31 45%, #0D1626 100%);

    /* The blooms stay blue and stay ON: a cobalt glow over dark navy is what keeps the
       ground from reading as flat, exactly as the pale wash does in light. Only the
       alphas lift, because additive light needs more presence over a dark base. */
    --defex-glass-bar: rgba(22, 35, 59, 0.62);
    --defex-glass-panel: rgba(22, 35, 59, 0.66);
    --defex-glass-edge: rgba(255, 255, 255, 0.14);

    /* Alpha matched to the light card's #ffffffc7 (~0.78) so cards keep the same degree
       of translucency over the ground, and the blooms still read through them. */
    --defex-card-bg: rgba(22, 35, 59, 0.78);
    --defex-card-edge: rgba(255, 255, 255, 0.10);
    /* Shadows go to black, not to the light set's navy: a navy shadow over a navy ground
       is invisible, so the cards would lose their lift entirely. */
    --defex-card-shadow: 0 16px 44px rgba(0, 0, 0, 0.45);
    --defex-panel-shadow: 0 10px 32px rgba(0, 0, 0, 0.38);
    --defex-menu-shadow: 0 16px 44px rgba(0, 0, 0, 0.50);

    /* Nested panels go DARKER than the card, inverting the light system where they go
       lighter — the cue is "recessed", and recessed reads as less light in both themes. */
    --defex-inner-bg: #111B2E;
    --defex-inner-edge: #1F2E4A;
    --defex-ghost-edge: #243350;
    --defex-rule: rgba(255, 255, 255, 0.10);
}

.defex-theme-dark.mud-layout {
    /* min-height, because .mud-layout is sized by its children — on a short page it would
       otherwise stop above the fold and leak body's light ground below it. */
    min-height: 100vh;
    background:
        radial-gradient(380px 380px at 270px 120px, rgba(21, 93, 252, 0.20), rgba(21, 93, 252, 0)),
        radial-gradient(320px 320px at calc(100% - 210px) 160px, rgba(90, 130, 220, 0.18), rgba(90, 130, 220, 0)),
        linear-gradient(to top, rgba(21, 93, 252, 0.12) 0%, rgba(21, 93, 252, 0) 55%),
        var(--defex-page-ground);
    background-attachment: fixed;
}

/* Remove drop shadow from all buttons globally */
.mud-button,
.mud-button-root,
.mud-icon-button,
.mud-icon-button-root {
    box-shadow: none !important;
}

/* Tabs, Design D: a row of top-rounded pills under a faint rule, the active one lifted on
   a white fill with a blue inset underline. The rule drops from the hard table-lines colour
   to --defex-rule; a hard hairline reads as a seam cutting the chrome panel. */
.mud-tab {
    /* 12px horizontal, not D's 14: the joined tab bar spends width on its left inset, and
       entity pages run to 9 tabs plus 5 utility icons. The 2px per tab buys that back and
       keeps the strip out of overflow-scroll. */
    padding: 12px 20px !important;
    border-radius: var(--defex-radius-control) var(--defex-radius-control) 0 0 !important;
    color: #4B5563;
    text-transform: none;
}

.mud-tab:hover {
    background: rgba(15, 23, 43, 0.04);
}

/* The tab strip is literal-coloured above (#4B5563 / #0F172B) — dark navy labels that
   vanish against the dark ground. Same three states, walked to the dark palette. */
.defex-theme-dark .mud-tab {
    color: #9FB0CE;
}

.defex-theme-dark .mud-tab:hover {
    background: rgba(255, 255, 255, 0.06);
}

.defex-theme-dark .mud-tab.mud-tab-active {
    color: #F8FAFC;
}

/* No fill on the active tab — it reads purely through the darker label and the underline.
   Design D lifts it on a white pill, but on our joined chrome panel that pill sat on an
   already-white surface and only muddied the seam. */
.mud-tab.mud-tab-active {
    color: #0F172B;
    background: transparent;
}

.mud-tabs {
    border-top: 1px solid var(--defex-rule) !important;
}

/* Utility tab strip (EntityUtilityTabs): right-align the trailing icon-only tabs
   (Notes/Files/Conversation/To Do/Activity) and tighten their padding.
   Two facts about the MudTabs DOM drive this:
   1. The .mud-tab button is NOT the flexbox child — MudBlazor wraps each tab in a
      .mud-tooltip-root (because ToolTip is set) and that wrapper is the flex item.
   2. .mud-tabs-tabbar-wrapper is shrink-wrapped to its tabs, so it has no internal free
      space for an auto margin. Stretch it to fill its (wider) container first, THEN the
      auto margin on the first utility tab's wrapper pushes the group to the right edge.
   Scoped with :has() so only pages that actually host the utility strip are affected. */
.mud-tabs-tabbar-wrapper:has(.ai-utility-tab--first) {
    width: 100%;
}

.mud-tabs-tabbar-wrapper > .mud-tooltip-root:has(.ai-utility-tab--first),
.mud-tabs-tabbar-wrapper > .ai-utility-tab--first {
    margin-left: auto !important;
}

.ai-utility-tab {
    padding: 6px 12px !important;
}

/* Utility tab icons default to a lighter grey; the active tab keeps the primary colour. */
.ai-utility-tab .mud-icon-root {
    color: var(--mud-palette-text-secondary) !important;
}

.ai-utility-tab.mud-tab-active .mud-icon-root {
    color: var(--mud-palette-primary) !important;
}

/* MudBlazor positions its active-tab underline slider by summing panel widths and rendering
   a percentage — it can't see the auto-margin gap we inject for the utility group, so the
   slider desyncs on these strips. Hide it and draw the underline directly on the active tab
   (inset box-shadow = no layout shift), which follows real layout and stays aligned. */
.mud-tabs-tabbar-wrapper:has(.ai-utility-tab--first) .mud-tab-slider {
    display: none !important;
}

.mud-tabs-tabbar-wrapper:has(.ai-utility-tab--first) .mud-tab.mud-tab-active {
    box-shadow: inset 0 -2px 0 0 var(--mud-palette-primary);
}


/* Add extra padding to the toggles in small size */
.mud-toggle-item-size-small {
    padding: 4px 8px !important;
}

/* MudToggleGroup rounds the SHELL (border-radius + overflow:hidden) but hard-resets every
   item to border-radius:0, so the end segments stay square inside the 24px-rounded shell and
   the group background shows through as a crescent past the first/last segment. Let the end
   items inherit the shell's corners, and pad the rounded side (beating the size-small
   !important above) so the label doesn't sit inside the curve. */
.mud-toggle-group.mud-toggle-group-horizontal > .mud-toggle-item:first-child {
    border-start-start-radius: inherit;
    border-end-start-radius: inherit;
    padding-inline-start: 14px !important;
}

.mud-toggle-group.mud-toggle-group-horizontal > .mud-toggle-item:last-child {
    border-start-end-radius: inherit;
    border-end-end-radius: inherit;
    padding-inline-end: 14px !important;
}

/* Reduce padding in mud datagrid tables (excluding child content rows) */
.mud-table-body > .mud-table-row > .mud-table-cell,
.mud-table-head > .mud-table-row > .mud-table-cell {
    padding: 4px 24px !important;
}

/* reduce margin on in put controls */
/* This hits EVERY dense input in the app and hardcodes a literal over the theme's
   DefaultBorderRadius, so it has to move with it or the token change does nothing visible. */
.mud-input-control.mud-input-control-margin-dense {
    margin: 0px !important;
    border-radius: var(--defex-radius-control) !important;
}

/* Font weight utilities */
.font-medium    { font-weight: 500 !important; }
.font-semibold  { font-weight: 600 !important; }
.font-bold      { font-weight: 700 !important; }

/* Grey text - theme-aware (adapts to dark mode) */
.text-grey      { color: var(--mud-palette-text-secondary) !important; }

/* Grey text - fixed shades (for when a specific grey is needed) */
.text-grey-500  { color: #6B7280 !important; }
.text-grey-600  { color: #475569 !important; }

/* Form field label: smaller, muted, theme-aware (tracks --mud-palette-text-secondary) */
.form-label {
    font-size: 0.8125rem !important; /* 13px */
    color: var(--mud-palette-text-secondary) !important;
}

/* Form guidance copy sitting under a section header. Body size (0.875rem) so it
   stays above the 14px floor, but one colour step back and measure-capped, so a
   sentence of instruction reads as guidance rather than competing with the field
   values beside it. The cap matters in wide register dialogs, where an uncapped
   paragraph runs past 120 characters per line. */
.form-hint {
    color: var(--mud-palette-text-secondary) !important;
    /* 60ch, not 68: `ch` is the width of "0", which in Roboto runs ~15% wider than the
       average lowercase glyph, so 68ch measured as 78 characters per line. 60ch lands
       at ~69 — inside the 45-75 band. */
    max-width: 60ch;
}

/* Inline field error rendered outside MudBlazor's own helper slot (e.g. an async
   uniqueness check). Sits at the documented field-label step rather than dropping
   to MudBlazor's 0.75rem caption, and takes its colour from the shared status
   token so it matches .status-pill and CustomAlert in every theme. */
.form-error-text {
    font-size: 0.8125rem !important; /* 13px — the field-label step */
    font-weight: 500 !important;
    color: var(--status-error-fg) !important;
}

/* Outlined text field with a solid (non-transparent) background, e.g. inline
   composers sitting on a tinted swimlane. Tracks the theme surface colour. */
.form-field-white .mud-input.mud-input-outlined {
    background-color: var(--mud-palette-surface) !important;
    border-radius: var(--mud-default-borderradius);
}

/* The mandatory-field asterisk (RequiredIndicator.razor). A class rather than an inline
   style so the rule below can find it with :has(). */
.required-indicator {
    color: var(--mud-palette-error);
}

/* Required-but-empty field: a soft error-toned wash that clears the moment the field has
   a value, so an unfinished form reads at a glance. Deliberately lighter than
   --status-error-bg: this is "still to do", not "you got it wrong".

   Driven entirely by RequiredIndicator stamping --empty on itself, so no page markup is
   involved; a field opts in by giving the indicator (or its HelpLabel) a For expression.

   Anchoring on the MudItem rather than using a sibling combinator is deliberate. The input
   is NOT reliably a sibling of the label: LookupPopover wraps its trigger in two nested
   divs, so ZoneLookup and LookupDropdown sit a couple of levels down. Matching the item and
   then descending reaches every field wrapper uniformly.

   The child combinator on the label matters — without it a nested item's label would wash
   its parent's inputs too. Consequence to know: an item holding two inputs washes both, so
   keep one field per MudItem (which the forms already do).

   Some fields put the label in a flex row alongside trailing actions (AI assist, snippet
   insert), which pushes .help-label-root a level below the item and out of reach of that
   child combinator. Such a row opts back in with .field-label-row — an explicit marker
   rather than a relaxed combinator, so the sibling-bleed guarded against above still holds.

   Read-mode sections render a MudText instead of an input, so this is inert there. */
.mud-grid-item:has(> :is(.help-label-root, .form-label, .field-label-row) .required-indicator--empty) .mud-input-control .mud-input {
    background-color: var(--field-required-bg);
    border-radius: var(--mud-default-borderradius);
}

.section-header {
    font-size: 18px;
    font-weight: 500;
}

/* Two-line section heading: a tinted icon tile beside a title with a quieter
   subtitle underneath. The tile's bg/fg/border are supplied inline by the caller
   (e.g. the Job information card tints them from the job type's --status-* tokens
   so the header reads as the same object as the Job Type chip). */
.section-header-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 10px;
    flex-shrink: 0;
}

.section-header-titles {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
    min-width: 0;
}

/* Sits at the same step as .form-label so the caption under a section title reads
   as the same class of text as the field labels below it. */
.section-header-subtitle {
    font-size: 0.8125rem !important; /* 13px */
    font-weight: 400;
    color: var(--mud-palette-text-secondary) !important;
}

/* Planner drag-and-drop highlight */
.planner-drop-highlight {
    background: color-mix(in srgb, var(--mud-palette-primary) 15%, transparent) !important;
    border-color: var(--mud-palette-primary) !important;
    transition: background 0.15s ease, border-color 0.15s ease;
}

/* Multi-select highlight for backlog job cards (Job Planner Day rail).
   1px to match the resting card: a thicker selected border reflows the card's
   content and makes the whole list jitter as you tick boxes. The fill plus the
   accented left stripe carry the state instead. */
.backlog-job-card-selected {
    border: 1px solid #1E88E5 !important;
    background: color-mix(in srgb, #1E88E5 8%, var(--mud-palette-surface)) !important;
}

/* Prompt drag-and-drop reorder styles */
.prompt-drop-zone .prompt-row {
    transition: margin-top 0.2s ease;
    position: relative;
}

.prompt-drop-zone .prompt-row:hover {
    background: var(--mud-palette-action-default-hover) !important;
}

.prompt-drop-zone .prompt-drop-indicator {
    margin-top: 50px;
}

.prompt-drop-zone .prompt-drop-indicator::before {
    content: '';
    position: absolute;
    top: -28px;
    left: 40px;
    right: 16px;
    height: 4px;
    border-radius: 2px;
    background: var(--mud-palette-primary);
}

.prompt-drop-zone .prompt-drop-indicator::after {
    content: '';
    position: absolute;
    top: -32px;
    left: 32px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--mud-palette-primary);
}

/* Item drag-and-drop reorder styles (QuoteTemplate Items tab) */
.item-drop-zone .item-row {
    transition: margin-top 0.2s ease;
    position: relative;
}

.item-drop-zone .item-row:hover {
    background: var(--mud-palette-action-default-hover) !important;
}

.item-drop-zone .item-drop-indicator {
    margin-top: 50px;
}

.item-drop-zone .item-drop-indicator::before {
    content: '';
    position: absolute;
    top: -28px;
    left: 40px;
    right: 16px;
    height: 4px;
    border-radius: 2px;
    background: var(--mud-palette-primary);
}

.item-drop-zone .item-drop-indicator::after {
    content: '';
    position: absolute;
    top: -32px;
    left: 32px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--mud-palette-primary);
}

/* Give outlined form inputs a surface background so they stand out on gray containers */
.mud-input-outlined .mud-input-slot {
    background-color: var(--mud-palette-surface);
    border-radius: var(--mud-default-borderradius);
}

/* ===== Status color tokens (single source of truth) =====
   Consumed by .status-pill, CustomAlert, the outlined snackbar and CustomChip so
   all four shapes stay visually locked together.

   Each severity is ONE fixed hue, mixed against the live surface and text colours.
   That split is the whole design:

   - The HUE is a constant. Severity means the same thing in every theme, so it is
     not derived from --mud-palette-error/warning/success and it deliberately does
     NOT follow the red tenant-operator theme. (--status-info-* used to derive from
     the info slot, which is why the OPTIONAL badge turned alarm-red for platform
     operators. That is fixed here.)
   - The LIGHTNESS adapts, because --mud-palette-surface and -text-primary are
     swapped by MudThemeProvider on :root. One declaration therefore resolves
     correctly in light, dark and red without a marker class.

   The marker class matters: on the web host MudThemeProvider only swaps variables
   and never stamps `.mud-theme-dark` (MainLayout stamps `defex-theme-dark` on
   .main-container instead). Scoping these to that class would miss MudDialog and
   MudSnackbar entirely — both providers render outside .main-container — which
   would break the pill/alert lock in dark mode rather than complete it. Deriving
   from :root-level palette variables reaches portalled content too.

   Ratios are 12% / 30% / 66%, chosen so every severity clears WCAG AA (>= 4.5:1)
   on its own background in BOTH themes; worst case is dark error at 4.51:1.
   Changing them without re-checking that will silently drop a pill below AA. */
:root {
    --status-error-hue: #C92324;
    --status-warning-hue: #EA580C;
    --status-success-hue: #007E33;
    --status-info-hue: #2563EB;

    --status-error-bg: color-mix(in srgb, var(--status-error-hue) 12%, var(--mud-palette-surface));
    --status-error-border: color-mix(in srgb, var(--status-error-hue) 30%, var(--mud-palette-surface));
    --status-error-fg: color-mix(in srgb, var(--status-error-hue) 66%, var(--mud-palette-text-primary));

    /* Wash for a required field that is still empty (see .field-required-empty). Sits
       below the 12% pill step on purpose — it must stay quiet enough to sit behind
       input text in both themes without reading as a validation failure. */
    --field-required-bg: color-mix(in srgb, var(--status-error-hue) 8%, var(--mud-palette-surface));

    --status-warning-bg: color-mix(in srgb, var(--status-warning-hue) 12%, var(--mud-palette-surface));
    --status-warning-border: color-mix(in srgb, var(--status-warning-hue) 30%, var(--mud-palette-surface));
    --status-warning-fg: color-mix(in srgb, var(--status-warning-hue) 66%, var(--mud-palette-text-primary));

    --status-success-bg: color-mix(in srgb, var(--status-success-hue) 12%, var(--mud-palette-surface));
    --status-success-border: color-mix(in srgb, var(--status-success-hue) 30%, var(--mud-palette-surface));
    --status-success-fg: color-mix(in srgb, var(--status-success-hue) 66%, var(--mud-palette-text-primary));

    --status-info-bg: color-mix(in srgb, var(--status-info-hue) 12%, var(--mud-palette-surface));
    --status-info-border: color-mix(in srgb, var(--status-info-hue) 30%, var(--mud-palette-surface));
    --status-info-fg: color-mix(in srgb, var(--status-info-hue) 66%, var(--mud-palette-text-primary));

    /* Soft info, kept as its own token name because CustomChip and .status-icon-btn
       consume it. It now resolves identically to the info pill above: maintaining two
       near-identical pale blues was itself part of the blue sprawl this block exists
       to prevent. Alias rather than a second set of literals. */
    --status-info-soft-bg: var(--status-info-bg);
    --status-info-soft-border: var(--status-info-border);
    --status-info-soft-fg: var(--status-info-fg);
}

/* ===== Glass chips (CustomChip, Light variant) =====
   Prefix: defex-glass-chip. The web counterpart of the mobile app's .md-glass-chip
   (mobile-detail.css), kept as a separate class rather than reusing that one because the two
   hosts theme differently: the mobile rule inverts off `.mud-theme-dark`, which the web host
   never stamps (see the note in the status-token block above). Everything here therefore
   resolves from :root palette variables so one declaration works in light, dark and red.

   Only the blur, the rim and the lift live here. The TINT is emitted INLINE by
   Shared/Components/CustomChip.razor, because MudChip writes `background-color: … !important`
   inline and an inline !important cannot be beaten from a stylesheet — only by a later
   declaration inside the same style attribute.

   The blur only does visible work where there is something behind the chip to sample: the
   blue page ground and the translucent chrome panel. Design D's content cards are solid
   white, so a chip inside one reads as tint + rim + lift alone — which is the intended
   result, not a bug to chase with a heavier blur.

   The doubled selector buys specificity over the page <style> blocks, which render in the
   body and so beat this file on ties. */
.defex-glass-chip.defex-glass-chip {
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    /* Rim and top highlight derive from --mud-palette-surface rather than a white literal:
       in light theme that IS white (the same rim as mobile), and in dark it resolves to the
       raised surface grey, which still sits above the page behind it and reads as an edge
       instead of the bright white line a hard-coded rgba(255,255,255,…) would draw. */
    border: 1px solid color-mix(in srgb, var(--mud-palette-surface) 75%, transparent) !important;
    box-shadow:
        0 3px 10px rgba(15, 23, 43, 0.06),
        inset 0 1px 0 color-mix(in srgb, var(--mud-palette-surface) 55%, transparent) !important;
}

/* ===== Glass buttons (outlined variant) =====
   NOT HERE. The outlined "ghost" button — fill, rim, hover, and the Design-1e tint
   layered on top of them — is styled in the inline style block of
   Shared/MainLayout.razor, because that block renders in the body and carries
   `!important` on the rim, so no rule in this file can reach it. A copy here silently
   loses the border and the hover while appearing to work on the fill. Edit it there. */

/* ===== Screen-reader-only text =====
   Available to assistive technology, removed from the visual layout without being
   hidden from it (display:none and visibility:hidden are both skipped by screen
   readers). Use when state is carried visually by colour, position or shape alone
   and the text equivalent would otherwise never be rendered — e.g. a status dot
   whose label exists in the model but is never printed. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ===== Color utilities ===== */
.colour-blue {
    color: #1570EF;
}

.colour-green {
    color: #16A34A;
}

.colour-green-light {
    color: #DCFCE7;
}

.colour-orange {
    color: #EA580C;
}

.colour-orange-light {
    color: #FFF7ED;
}

.colour-yellow {
    color: yellow;
}

.colour-red {
    color: #DC2626;
}

.colour-red-light {
    color: #FEE2E2;
}

.colour-purple {
    color: purple;
}

.colour-pink {
    color: hotpink;
}

/* ===== Pill / badge utilities ===== */
.pill {
    display: flex;
    height: 24px;
    padding: 4px 8px;
    justify-content: flex-end;
    align-items: center;
    border-radius: 8px;
    border: 0 solid;
}

.pill.white {
    border: 1px solid var(--mud-palette-table-lines);
}

.pill.red {
    background: var(--colour-red-light);
}

.pill.red > span {
    color: var(--colour-red);
}

.pill.green {
    background: var(--colour-green-light);
}

.pill.green > span {
    color: var(--colour-green);
}

.pill.orange {
    background: var(--colour-orange-light);
}

.pill.orange > span {
    color: var(--colour-orange);
}

/* Neutral */
.pill.grey {
    background: var(--mud-palette-background-gray);
}

/* ===== Status pill labels =====
   Reusable version of the HelpfulTipsPanel section-header look: light tinted
   background, colored bold uppercase label, leading icon, rounded corners.
   Variants consume the shared status tokens above. */
.status-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 12px;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    border-radius: 6px;
    border: 1px solid transparent;
}

.status-pill--error {
    color: var(--status-error-fg);
    background-color: var(--status-error-bg);
    border-color: var(--status-error-border);
}

.status-pill--warning {
    color: var(--status-warning-fg);
    background-color: var(--status-warning-bg);
    border-color: var(--status-warning-border);
}

.status-pill--success {
    color: var(--status-success-fg);
    background-color: var(--status-success-bg);
    border-color: var(--status-success-border);
}

.status-pill--info {
    color: var(--status-info-fg);
    background-color: var(--status-info-bg);
    border-color: var(--status-info-border);
}

/* Full-width variant (e.g. the collapsible COMPLETED header). */
.status-pill--block {
    display: flex;
    width: 100%;
    justify-content: flex-start;
    cursor: pointer;
}

/* ===== CustomAlert: dense =====
   MudBlazor's Dense only shrinks .mud-alert's own box (6px 16px -> 0 12px). The message and
   icon carry fixed padding of their own that Dense never reaches — .mud-alert-message is
   9px 0 and .mud-alert-icon is 7px 0 around a 22px glyph — so a "dense" one-line alert still
   stood ~47px tall. Roughly half the CustomAlert call sites pass Dense="true", so they were
   asking for compact and getting close to the full height. These rules finish the job.

   Scoped to .custom-alert.mud-dense: alerts that did NOT opt into Dense are untouched. */
/* Vertical breathing room is set here rather than left to whatever padding the alert happens to
   carry: MudBlazor's .mud-alert.mud-dense zeroes it, and the py-1 utility CustomAlert used to add
   is !important. Three classes so this beats both. */
.mud-alert.custom-alert.mud-dense {
    padding-top: 6px !important;
    padding-bottom: 6px !important;
}

.custom-alert.mud-dense .mud-alert-message {
    padding: 2px 0;
}

.custom-alert.mud-dense .mud-alert-icon {
    padding: 0;
    font-size: 16px;
    align-items: center;
    /* Beats MudBlazor's own !important margin-inline-end on .mud-alert-icon. */
    margin-right: 8px !important;
    margin-inline-end: 8px !important;
}

/* The optional leading icon CustomAlert renders itself for the Icon parameter. Its mr-3/mt-1
   utilities are !important, so overriding them needs the same weight. */
.custom-alert.mud-dense .custom-alert__icon {
    font-size: 16px;
    width: 16px;
    height: 16px;
    margin-right: 8px !important;
    margin-top: 0 !important;
}

/* Title sits directly above its message rather than a full line apart. mb-1 is !important. */
.custom-alert.mud-dense .custom-alert__title {
    margin-bottom: 2px !important;
}

/* ===== Scheduling flag =====
   The compact inline variant of SchedulingWarningList (Flag="true"): one accented line per
   warning, sitting directly against the field that caused it. Deliberately NOT a CustomAlert —
   that renders a bold title line above the message inside a fully-bordered MudAlert box, which
   costs ~4x the height for the same sentence. Here the severity is carried by the icon and the
   left accent, and only the message is shown, so a warning reads as an annotation on the form
   rather than an interruption of it. Uses the same status tokens as .status-pill above, so the
   tints stay locked to the pills and stay AA in both themes. */
.scheduling-flag {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    padding: 8px 10px;
    font-size: 0.75rem;
    line-height: 1.4;
    border-left: 2px solid transparent;
    border-radius: 0 var(--mud-default-borderradius) var(--mud-default-borderradius) 0;
}

.scheduling-flag__icon {
    flex: none;
    margin-top: 1px;
    /* MudIcon's Size.Small is 20px; the design's flag icon is 16px. */
    font-size: 16px !important;
    width: 16px;
    height: 16px;
}

.scheduling-flag--error {
    color: var(--status-error-fg);
    background-color: var(--status-error-bg);
    border-left-color: var(--status-error-hue);
}

.scheduling-flag--warning {
    color: var(--status-warning-fg);
    background-color: var(--status-warning-bg);
    border-left-color: var(--status-warning-hue);
}

.scheduling-flag--success {
    color: var(--status-success-fg);
    background-color: var(--status-success-bg);
    border-left-color: var(--status-success-hue);
}

.scheduling-flag--info {
    color: var(--status-info-fg);
    background-color: var(--status-info-bg);
    border-left-color: var(--status-info-hue);
}

/* ===== Periodic pricing ↔ Periodic Invoicing link (ServiceAgreement Routine Pricing tab) =====
   Tints the Periodic-priced rows in the Current Prices table and accents the Periodic
   Invoicing card below it, so the scope of the invoicing settings is unmistakable. Uses the
   shared soft-info tokens + the primary accent to stay locked to the theme. */
.periodic-price-row td {
    background-color: var(--status-info-soft-bg) !important;
}

.periodic-price-row td:first-child {
    box-shadow: inset 3px 0 0 var(--mud-palette-secondary);
}

.periodic-invoicing-card {
    border-top: 3px solid var(--mud-palette-secondary) !important;
}

/* Dashed connector ("tether") bridging the Current Prices table to the Periodic Invoicing
   card, spelling out that the one schedule governs every Periodic price above (design 2a). */
.periodic-tether {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding-left: 56px;
    margin: -12px 0;
}

.periodic-tether__line {
    height: 14px;
    border-left: 2px dashed var(--mud-palette-secondary);
    margin-left: 9px;
}

.periodic-tether__pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: var(--mud-palette-secondary);
    color: var(--mud-palette-secondary-text);
    font-size: 0.6875rem;
    font-weight: 500;
    padding: 5px 12px;
    border-radius: 999px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, .2);
}

.periodic-tether__pill .mud-icon-root {
    font-size: 1rem;
}

/* ===== Snackbar (outlined variant) tinted with status tokens =====
   SnackbarVariant=Outlined renders .mud-snackbar.mud-alert-outlined-{severity}.
   Re-skin the white surface with the shared status palette so toasts match
   .status-pill / CustomAlert. Scoped to .mud-snackbar to avoid touching MudAlert. */
.mud-snackbar.mud-alert-outlined-success {
    background-color: var(--status-success-bg);
    border-color: var(--status-success-border);
    color: var(--status-success-fg);
}

.mud-snackbar.mud-alert-outlined-error {
    background-color: var(--status-error-bg);
    border-color: var(--status-error-border);
    color: var(--status-error-fg);
}

.mud-snackbar.mud-alert-outlined-warning {
    background-color: var(--status-warning-bg);
    border-color: var(--status-warning-border);
    color: var(--status-warning-fg);
}

.mud-snackbar.mud-alert-outlined-info {
    background-color: var(--status-info-soft-bg);
    border-color: var(--status-info-soft-border);
    color: var(--status-info-soft-fg);
}

/* Soft circular icon button using the pastel info tokens, matching the
   status chips/pills (e.g. the page-header help button). Pair with
   Color="Color.Inherit" so the glyph picks up the token foreground. */
.status-icon-btn {
    /* The glyph reads as the action blue, not the muted info-pill foreground: this button
       sits a few pixels from the Ask DEX pill, and the two blues have to be the same blue.
       Resolving from the palette also keeps it tracking the tenant-operator red. */
    color: var(--mud-palette-secondary) !important;
    background-color: var(--status-info-soft-bg) !important;
}

.status-icon-btn:hover {
    background-color: var(--status-info-soft-border) !important;
}

/* ===== Layout containers ===== */
.main-page {
    max-width: 1200px;
    margin: 0 auto;
}

.main-container {
    padding-left: 0px !important;
    padding-right: 0px !important;
    /* The fixed MudAppBar is already cleared by .mud-main-content's padding-top
       (= --mud-appbar-height). A top padding here would stack on top of that and
       show as a gap below the app-bar, so keep it flush at 0. */
    padding-top: 0px !important;
    overflow: auto;
    /* Reserve the scrollbar's width even while content is short (loading bar,
       skeletons), so the page doesn't shift horizontally when the scrollbar
       appears after the real content renders. */
    scrollbar-gutter: stable;
    /* Subtract the same app-bar offset that .mud-main-content pads by, otherwise
       the container overflows the viewport by the difference. */
    height: calc(100dvh - var(--app-bar-offset, 60px)) !important;
    display: flex !important;
    flex-direction: column !important;
    /* Transparent so body's gradient shows through — see the ground block at the top. */
    background-color: transparent !important;
    position: relative;
    z-index: 1;

    /* 199 inline `background: var(--mud-palette-background-gray)` styles across 156 pages
       repaint a flat section ground over the gradient. Rather than edit all of them,
       redefine the variable for everything inside the content area: each of those inline
       styles resolves through it, so they all become transparent at once.

       If a panel genuinely needs a filled grey (an inner well, a zebra swimlane), give it
       .defex-solid-ground below rather than reaching for a literal — that keeps the opt-out
       greppable and themeable. */
    --mud-palette-background-gray: transparent;
}

/* The page-root MudPaper. Every top-level page is scaffolded as a full-bleed
   <MudPaper Elevation="0"> (see DESIGN.md / CLAUDE.md), and MudBlazor gives .mud-paper an
   opaque --mud-palette-surface fill — so without this the page root paints white edge to
   edge and the body gradient is only ever visible in the shell gutters.

   Matched by DEPTH rather than by wrapper class: some pages nest the root paper under an
   animation wrapper (.content-reveal), others don't, and Property wraps it in a flex row.
   One or two levels covers every scaffold in the app.

   Cards are unaffected even when they sit at these depths — `.custom-border:not()...` is
   (0,3,0) and re-asserts its own white over this rule's (0,2,0). */
.main-container > .mud-paper,
.main-container > * > .mud-paper {
    background-color: transparent;
}

/* Opt back in to a real filled section ground. */
.defex-solid-ground {
    --mud-palette-background-gray: var(--defex-inner-bg);
}

@media (max-width: 599px) {
    .main-container {
        height: calc(100dvh - 20px) !important;
    }
}

/* Override MudMainContent padding for all media sizes */
/* Mobile (default) */
.mud-main-content {
    padding-top: 56px !important;
    /* Transparent, so the body gradient reaches from behind the drawer across the content. */
    background-color: transparent !important;
    min-height: 100dvh;
    position: relative;
    z-index: 1;
}

/* Small screens and up: clear the fixed MudAppBar so flush-top content
   (global note banners, impersonation banner) isn't hidden behind it.
   --app-bar-offset is the bar's true rendered height (the 64px theme variable
   over-reports it by ~4px); keep this and .main-container's height in sync. */
@media (min-width: 600px) {
    .mud-main-content {
        padding-top: var(--app-bar-offset, 60px) !important;
    }
}

/* ===== Text / interaction utilities ===== */
.hovercard:hover {
    box-shadow: 0px 4px 6px -1px rgba(0,0,0,0.1), 0px 2px 4px -1px rgba(0,0,0,0.06);
}

/* Material-style ripple press feedback (replaces the old :active background tint).
   Ripple spreads from the element centre on :active. */
.press {
    position: relative;
    overflow: hidden;
    transition: border-color 0.2s ease;
}

.press:hover {
    border-color: #1976d2;
}

.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;
    }
}

.text-truncate {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.scroller {
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}

.scroller::-webkit-scrollbar {
    display: none;
}

/* Keep the data grid's vertical scrollbar pinned to the viewport's right
   edge. Without min-width:0 the flex-child default of min-width:auto lets
   the scroll container grow to its content width, pushing the scrollbar
   off-screen when columns are wider than the viewport. */
.mud-data-grid {
    min-width: 0;
    max-width: 100%;
}

/* Make the container itself the horizontal (and vertical) scroller, and cap it
   at the viewport width so BOTH scrollbars land on the visible edges.

   The `!important` on overflow is load-bearing: for grids that declare a sticky
   (StickyLeft/StickyRight) column, MudBlazor inlines `overflow: clip` on this
   element and instead relies on the OUTER .mud-data-grid wrapper to scroll
   horizontally (see MudDataGrid.razor.cs TableStyle). But we cap this container
   at max-width:100%, so it never reaches its `width:max-content`, the outer
   wrapper never sees an overflowing child, and the horizontal scrollbar simply
   never appears — the extra columns are clipped and unreachable. Overriding the
   inline `overflow: clip` back to `auto` restores scrolling on the container
   itself; sticky columns still pin correctly against a scrolling container.
   Use the `overflow` shorthand (not overflow-x) so we don't leave overflow-y
   clipped and kill the FixedHeader vertical scroll. */
.mud-data-grid .mud-table-container {
    overflow: auto !important;
    width: 100%;
    max-width: 100%;
}

/* Allow the inner table to grow past the container width when columns are
   resized wider, while still filling the container when content is narrow.
   Without this override MudBlazor's `.mud-table-root { width: 100% }` locks
   the table to the container, so column-resize has nowhere to take width.
   (Descendant selector — MudDataGrid wraps the table in MudDropContainer
   when DragDropColumnReordering is enabled, so a child selector won't match.) */
.mud-data-grid .mud-table-container .mud-table-root {
    width: max-content;
    min-width: 100%;
}

/* ===== Sticky (StickyLeft/StickyRight) column cells: solid background =====
   Sticky column cells overlap the horizontally-scrolled columns, so they must
   be OPAQUE or the scrolled content bleeds through. The page markup sets
   `background:transparent` inline on these cells (so they used to show the row
   colour through), which is why we override with !important here. Match each row
   state MudBlazor paints on the <tr> (surface / striped / hover) so the sticky
   column still looks seamless with its row. z-index keeps the cell above its
   non-positioned siblings (matters for StickyLeft, which paints before them). */
.mud-data-grid .mud-table-body .mud-table-row .mud-table-cell.sticky-left,
.mud-data-grid .mud-table-body .mud-table-row .mud-table-cell.sticky-right,
.mud-data-grid .mud-table-head .mud-table-cell.sticky-left,
.mud-data-grid .mud-table-head .mud-table-cell.sticky-right {
    background-color: var(--mud-palette-surface) !important;
    z-index: 2;
}

/* Striped/hover row tints (--mud-palette-table-striped / -hover) are TRANSLUCENT
   (rgba ~0.02/0.04), so setting them as the cell background alone lets scrolled
   columns bleed through on those rows. Layer the translucent tint over an opaque
   surface base so the cell stays solid while still matching the row colour. */
.mud-data-grid.mud-table-striped .mud-table-body .mud-table-row:nth-of-type(odd) .mud-table-cell.sticky-left,
.mud-data-grid.mud-table-striped .mud-table-body .mud-table-row:nth-of-type(odd) .mud-table-cell.sticky-right {
    background-color: var(--mud-palette-surface) !important;
    background-image: linear-gradient(var(--mud-palette-table-striped), var(--mud-palette-table-striped)) !important;
}

@media (hover: hover) and (pointer: fine) {
    .mud-data-grid.mud-table-hover .mud-table-body .mud-table-row:hover .mud-table-cell.sticky-left,
    .mud-data-grid.mud-table-hover .mud-table-body .mud-table-row:hover .mud-table-cell.sticky-right {
        background-color: var(--mud-palette-surface) !important;
        background-image: linear-gradient(var(--mud-palette-table-hover), var(--mud-palette-table-hover)) !important;
    }
}

/* ===== Always-visible scrollbars for MudDataGrid scroll containers ===== */
.mud-data-grid .mud-table-container {
    scrollbar-width: auto;
    scrollbar-color: var(--mud-palette-lines-default) transparent;
}

.mud-data-grid .mud-table-container::-webkit-scrollbar {
    width: 15px;
    height: 15px;
}

.mud-data-grid .mud-table-container::-webkit-scrollbar-track {
    background: transparent;
}

.mud-data-grid .mud-table-container::-webkit-scrollbar-thumb {
    background-color: var(--mud-palette-lines-default);
    border-radius: 6px;
    border: 2px solid transparent;
    background-clip: content-box;
}

.mud-data-grid .mud-table-container::-webkit-scrollbar-thumb:hover {
    background-color: var(--mud-palette-action-default);
    background-clip: content-box;
}

.mud-data-grid .mud-table-container::-webkit-scrollbar-corner {
    background: transparent;
}

.no-uppercase {
    text-transform: none !important;
}

/* Remove underline/border from MudSelect or MudInput */
.no-underline .mud-input-underline::before,
.no-underline .mud-input-underline::after {
    border-bottom: none !important;
}

.no-underline .mud-input-slot {
    text-align: right;
    margin-top: 2px;
}

/* Target the label text inside MudMenu for no-uppercase - more aggressive selectors */
.no-uppercase .mud-menu-activator,
.no-uppercase .mud-menu-activator *,
.no-uppercase .mud-menu-activator .mud-typography,
.no-uppercase .mud-menu-activator .mud-typography *,
.no-uppercase .mud-menu-activator .mud-text,
.no-uppercase .mud-menu-activator .mud-text *,
.no-uppercase button,
.no-uppercase button *,
/* Universal selector for any MudMenu with no-uppercase class */
.no-uppercase * {
    text-transform: none !important;
}

/* Additional specificity for MudButton inside no-uppercase containers */
.no-uppercase .mud-button,
.no-uppercase .mud-button-root,
.no-uppercase .mud-button-label {
    text-transform: none !important;
}

/* ===== MudTable body: use body1 typography instead of default body2 ===== */
.mud-table-body .mud-table-cell {
    font-family: var(--mud-typography-body1-family) !important;
    font-size: var(--mud-typography-body1-size) !important;
    font-weight: var(--mud-typography-body1-weight) !important;
    line-height: var(--mud-typography-body1-lineheight) !important;
    letter-spacing: var(--mud-typography-body1-letterspacing) !important;
}

/* ===== Constrain auto-generated checkbox column in multi-select tables ===== */
.mud-table th:first-child:has(.mud-checkbox),
.mud-table td:first-child:has(.mud-checkbox) {
    width: 50px !important;
    min-width: 50px !important;
    max-width: 50px !important;
}

/* ===== Responsive column min-width system using clamp(min, preferred, max) ===== */
.col-width-tiny {
    min-width: clamp(50px, 4vw, 90px);
}

.col-width-small {
    min-width: clamp(100px, 8vw, 180px);
}

.col-width-numeric {
    min-width: clamp(120px, 10vw, 200px);
}

.col-width-status {
    min-width: clamp(98px, 8.4vw, 168px);
}

/* Property Assets grid: Asset No + Status hold short values (e.g. "CA/FHDV2",
   an "Active" chip), so narrow their columns without affecting other tables. */
.property-assets-grid .col-width-status {
    min-width: clamp(72px, 6vw, 110px);
}

/* Let the table grow past the container so the columns keep their min-widths
   and the grid's overflow-x scrollbar appears. min-width:100% still fills the
   full width (no empty gap) when the columns comfortably fit. */
.property-assets-grid .mud-table-root {
    width: max-content;
    min-width: 100%;
}

/* Property Jobs grid: same overflow behaviour as the Assets grid above — let the
   table grow past the container so columns keep their min-widths and the grid's
   own horizontal scrollbar appears. */
.property-jobs-grid .mud-table-root {
    width: max-content;
    min-width: 100%;
}

.col-width-field {
    min-width: clamp(150px, 12vw, 250px);
}

.col-width-type {
    min-width: clamp(105px, 8.4vw, 175px);
}

.col-width-date {
    min-width: clamp(140px, 12vw, 220px);
}

.col-width-name {
    min-width: clamp(126px, 10.5vw, 224px);
}

.col-width-property {
    min-width: clamp(180px, 14vw, 300px);
}

.col-width-client {
    min-width: clamp(180px, 14vw, 300px);
}

.col-width-description {
    min-width: clamp(250px, 20vw, 450px);
}

.code-width {
    min-width: clamp(120px, 12vw, 240px);
}

.description-width {
    width: clamp(250px, 20vw, 450px);
}

/* Text truncation support for MudHighlighter components */
.col-width-small .mud-highlighter,
.col-width-numeric .mud-highlighter,
.col-width-status .mud-highlighter,
.col-width-field .mud-highlighter,
.col-width-type .mud-highlighter,
.col-width-date .mud-highlighter,
.col-width-name .mud-highlighter,
.col-width-property .mud-highlighter,
.col-width-client .mud-highlighter,
.col-width-description .mud-highlighter,
.code-width .mud-highlighter,
.description-width .mud-highlighter {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
}

/* ===== Border utilities ===== */
/* THE CARD. Design D: solid #ffffff, 18px radius, a WHITE rim and a large soft shadow.
   No blur — cards are not glass in D; only the app bar, nav menu and page header are.

   The white rim is not a mistake. Against the blue ground it reads as a highlight along
   the card's edge; against the card itself it is invisible. The shadow, not the border,
   is what separates the card from the page.

   --defex-card-edge is deliberately NOT --mud-palette-table-lines: that token also draws
   DataGrid row separators, which need to stay legible at full strength.

   The nesting reset matters — cards routinely contain sub-panels carrying the same class
   (e.g. Property.razor's expansion panels and inner papers). Without it every nested panel
   casts its own shadow inside its parent and the card fills with soft grey rectangles. */
.custom-border {
    border: 1px solid var(--defex-card-edge);
}

/* Popovers and menus are excluded rather than reset: they carry MudBlazor's own elevation,
   and a `box-shadow: none` override here would out-specify and erase it.

   The radius lives HERE and not on .round, because .round is opt-in and ~65 of the 206 cards
   never took it — every list screen (Clients, Propertys, Leads …) carries a bare
   `ma-4 custom-border` and so fell through to the theme's 8px DefaultBorderRadius, sitting
   visibly squarer than the 18px PageHeader directly above it. A card's radius is part of what
   makes it a card, so it belongs on the card class. .round is now redundant on any element
   that also has .custom-border, but it stays: it is still the lever for the handful of
   rounded surfaces that are not cards.

   Nested panels keep their tighter --defex-radius-inner — the rule below is (0,4,0) and
   out-specifies this one. */
.custom-border:not(.mud-popover-paper):not(.mud-menu-paper) {
    background-color: var(--defex-card-bg);
    border-radius: var(--defex-radius-panel);
    box-shadow: var(--defex-card-shadow);
}

/* A card is now translucent (--defex-card-bg carries alpha), so anything inside it that paints
   an opaque --mud-palette-surface punches a solid white block through it — on list screens the
   DataGrid did exactly that, leaving the card's padding ring reading pale blue around a hard
   white rectangle. These inherit the card's own fill instead.

   Scoped to descendants of .custom-border: the same elements outside a card still need their
   own surface. Nested .custom-border panels are excluded — they are inner surfaces with a
   deliberate grey fill, handled by the rule below. */
.custom-border .mud-table,
.custom-border .mud-data-grid,
.custom-border .search-filter-bar {
    background-color: transparent;
}

/* Nested panels are INNER surfaces, not cards: filled grey, tighter radius, hard hairline,
   no shadow of their own. Must out-specify the rule above (0,3,0), hence the repeated
   :not()s — drop them and this silently loses the cascade. */
.custom-border .custom-border:not(.mud-popover-paper):not(.mud-menu-paper) {
    background-color: var(--defex-inner-bg);
    border-color: var(--defex-inner-edge);
    border-radius: var(--defex-radius-inner);
    box-shadow: none;
}

.custom-border-bottom {
    border-bottom: 1px solid var(--mud-palette-table-lines);
}

/* ===== Full-screen list overlay =====
   Expands a list container (toolbar + grid) to fill the whole app viewport. Reusable across
   list screens: toggle this class on the list wrapper and give the grid a taller height.

   z-index is kept BELOW MudBlazor's popover layer (~1201) so the columns panel, filter menus,
   export menu and row context menu opened from inside the overlay still appear on top. The app
   forces the drawer to z-index 1300 and the app bar to 1250 (nav-menu.css), which would paint
   over the overlay, so instead of out-z-indexing them we hide them while a list is full screen
   (body:has rule below). That covers the whole screen without needing any JS interop. */
.list-fullscreen {
    /* !important beats the wrapper's inline `position: relative` (inline styles outrank class rules). */
    position: fixed !important;
    inset: 0;
    z-index: 1150;
    margin: 0 !important;
    /* !important because the card radius now comes from .custom-border (0,3,0) and .round
       sits later in the file — a plain `0` here loses both ways and leaves rounded corners
       floating over the four screen edges. */
    border-radius: 0 !important;
    overflow: auto;
    background: var(--mud-palette-surface);
    padding: 16px;
    animation: listFsIn 160ms cubic-bezier(0, 0, 0.2, 1);
}

/* Drop the top app bar and left nav drawer so the overlay truly fills the viewport. */
body:has(.list-fullscreen) .mud-appbar,
body:has(.list-fullscreen) .mud-drawer {
    display: none !important;
}

@keyframes listFsIn {
    from { opacity: 0.6; }
    to { opacity: 1; }
}

.custom-border-top {
    border-top: 1px solid var(--mud-palette-table-lines);
}

.custom-border-right {
    border-right: 1px solid var(--mud-palette-table-lines);
}

.custom-border-left {
    border-left: 1px solid var(--mud-palette-table-lines);
}

/* The house card radius. Paired with .custom-border on ~150 pages, so this is the single
   lever that moves every page card to Design D's 18px. */
.round {
    border-radius: var(--defex-radius-panel);
}

/* Target MudMenu dropdown popover */
.mud-popover-paper.custom-border,
.mud-menu-paper.custom-border {
    border: 1px solid var(--mud-palette-table-lines) !important;
}

.no-radius {
    border-radius: 0 !important;
}

/* ===== Smart Import — entity rail + relationship badge ===== */
.ent-rail-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 8px;
    cursor: pointer;
    border-left: 3px solid transparent;
    transition: background-color 120ms ease, border-color 120ms ease;
}

.ent-rail-item:hover {
    background-color: var(--mud-palette-background-gray);
}

.ent-rail-item.is-active {
    background-color: var(--mud-palette-background-gray);
    border-left-color: var(--mud-palette-primary);
}

.ent-rail-ico {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 8px;
    flex-shrink: 0;
    color: var(--mud-palette-primary);
    background-color: color-mix(in srgb, var(--mud-palette-primary) 12%, transparent);
}

.ent-rail-ico.ent-tone-client { color: #155DFC; background-color: color-mix(in srgb, #155DFC 12%, transparent); }
.ent-rail-ico.ent-tone-property { color: #0a6633; background-color: color-mix(in srgb, #0a6633 12%, transparent); }
.ent-rail-ico.ent-tone-contact { color: #8a4a00; background-color: color-mix(in srgb, #8a4a00 12%, transparent); }
.ent-rail-ico.ent-tone-asset { color: #6d28d9; background-color: color-mix(in srgb, #6d28d9 12%, transparent); }

.ent-rail-text {
    min-width: 0;
    flex: 1;
}

.ent-rail-name {
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.rel-badge {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-radius: 8px;
    background-color: var(--mud-palette-background-gray);
}

.custom-radius {
    border-radius: var(--defex-radius-control) !important;
}

.custom-radius-2 {
    border-radius: var(--defex-radius-tile) !important;
}

/* ============================================================
   Page header surface — takes the mobile glass LANGUAGE, not the mobile glass MECHANISM.

   No backdrop-filter here, on purpose. PageHeader renders above the page's grey section
   wrapper, so it sits on --mud-palette-background (white). Blurring a flat colour returns
   that colour, so a translucent pane there would render as a plain white box and the blur
   would cost a compositing layer for nothing. The header is SELF-LIT instead: it carries its
   own faint blooms, the same blues as the nav drawer's wash, so the two ends of the shell
   read as one material even though only one of them is really glass.

   Shadow is the house token (hue-matched); never filter: drop-shadow — see
   mobile-detail.css:29 for why. */
/* THE CHROME PANEL. One of D's three glass surfaces — but translucent WITHOUT blur: the
   design feeds it `panelBlur`, which Direction B sets to none. It reads as glass because
   the page gradient shows through it, not because it blurs.

   The self-lit bloom gradient this rule used to carry is gone: the body ground now supplies
   the colour, so painting our own would double it. */
.page-header {
    background: var(--defex-glass-panel) !important;
    border: 1px solid var(--defex-glass-edge);
    border-radius: var(--defex-radius-panel) !important;
    box-shadow: var(--defex-panel-shadow) !important;
    /* Design D's chrome panel is `16px 12px 0`, with the 14px gap above the tab rule supplied
       by a title-row wrapper INSIDE the panel. We have no such wrapper — our tab bar is a
       separate element joined below — so that 14px moves onto the panel's own bottom padding.
       Net spacing above the tabs is the same; the previous pa-2 gave a cramped 8px all round. */
    padding: 16px 12px 14px;
}

/* The header's entity tile. Design D gives it a flat gradient fill and a tinted rim with
   NO glow — the bloom from the previous pass is deliberately removed. Size and radius are
   D's (46px / 12px); the COLOUR still comes from NavigatorIcon's categorical token rather
   than D's single amber, because D only ever shows a Property and the categorical set has
   to cover every entity type (see the rationale in NavigatorIcon.razor). */
.page-header .navigator-icon-container {
    width: 46px !important;
    height: 46px !important;
    min-width: 46px !important;
    border-radius: var(--defex-radius-tile);
}

/* The category trail that AssetType renders on its subtitle line. MudBreadcrumbs ships with
   list padding and body-size text; both are wrong here, where the trail has to read as the
   caption row it replaced and add no height to the header. Scoped to the header so ordinary
   page-level breadcrumbs keep their own sizing. */
.page-header .page-header__crumbs.mud-breadcrumbs {
    padding: 0;
    margin: 0;
    font-size: 0.75rem;
    line-height: 1.4;
}

.page-header .page-header__crumbs .mud-breadcrumb-item,
.page-header .page-header__crumbs .mud-breadcrumb-item a,
.page-header .page-header__crumbs .mud-breadcrumb-separator {
    font-size: 0.75rem;
    line-height: 1.4;
}

.page-header .page-header__crumbs .mud-breadcrumb-separator .mud-icon-root {
    font-size: 0.9rem;
}

/* When the left rail carries a SECOND control under the back arrow — the Relationships
   toggle (TitleAction) on Client, Property and Contract, or the destinations chevron
   (TitleMenu) on Job and JobCard — the rail is two 30px small icon buttons tall, 14px
   deeper than the 46px tile beside it. That left the tile floating with the second
   button's glyph hanging below its bottom edge.

   Growing the tile to the rail's own 60px puts the two on one bottom line and, as a
   side effect, gives the 35px Size.Large glyph the breathing room it never had at 46px
   (76% of the tile, against a comfortable 58% now). Kept square: a 46x60 tile would
   read as a stretched chip rather than an entity tile.

   Scoped with :has() rather than a per-page flag — headers with only the back arrow
   keep D's 46px tile, which is still the common case. */
.page-header .page-header__rail:has(> :nth-child(2)) + .navigator-icon-container {
    width: 60px !important;
    height: 60px !important;
    min-width: 60px !important;
}

/* ===== View switcher INSIDE the header panel (PageHeader TabsContent) =====
   Option G from the page-header tab exploration. For list screens whose tabs are two or three
   whole-page views (List / Dashboard), the pills stack under the title inside the one chrome
   panel instead of riding a joined tab bar below it. That removes the seam and ~23px of chrome;
   the page renders its own bodies, because MudTabs cannot live in the header (it owns its panels
   as well as its bar — see the join rules below, which these pages never match).

   The negative left margin pulls the first pill’s LABEL, not its box, onto the title’s left edge:
   without it the 14px pill padding pushes the row 14px right of the title and the two never line
   up. 3px of top margin is the optical gap — the title’s 1.43 line-height already supplies the
   rest. */
.page-header__tabs {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-top: 3px;
    margin-left: -2px; /* net -14px against the pill padding, after the ml-3 the row carries */
}

.page-header__tab {
    padding: 6px 14px;
    border: 1px solid transparent;
    border-radius: var(--defex-radius-pill);
    background: transparent;
    color: #4B5563;
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
    line-height: 1.43;
    cursor: pointer;
    transition: background-color .15s ease, color .15s ease;
}

.page-header__tab:hover:not(.page-header__tab--active) {
    background: rgba(15, 23, 43, 0.04);
    color: #0F172B;
}

/* The selected pill is a SOLID white chip on the panel’s translucent glass — the same
   figure/ground move the segmented controls make, and the only thing separating it from the
   filter chips in the card below is that it is filled rather than outlined. */
.page-header__tab--active {
    background: #ffffff;
    border-color: var(--defex-glass-edge);
    box-shadow: 0 2px 8px rgba(15, 23, 43, 0.08);
    color: #0F172B;
}

.page-header__tab:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
}

/* Two lines of content now sit beside the tile, so it grows to cover both — the same 46 → 60px
   move the rail rule above makes for a two-button rail, and for the same reason: at 46px the tile
   floats against the title with the pill row hanging below its bottom edge. */
.page-header:has(.page-header__tabs) .navigator-icon-container {
    width: 60px !important;
    height: 60px !important;
    min-width: 60px !important;
}

/* ===== Header + tab strip as ONE panel (Design D) =====
   D draws the title block and the tab row inside a single chrome panel. MudTabs can't be
   nested inside PageHeader — it renders the tab bar AND the panels, so nesting it would put
   every page's entire content inside the header. Instead the two elements are joined
   visually: the header squares its bottom, and .mud-tabs-tabbar (the bar alone — it is the
   first child of .mud-tabs, with .mud-tabs-panels as its sibling) becomes the panel's
   lower half.

   Selected with :has() rather than a per-page flag: PageHeader's wrapper and MudTabs are
   adjacent siblings, so the relationship is already expressible in CSS and the 42 paired
   pages need no markup change. Two shapes exist in the wild — header directly followed by
   the tabs, and header wrapped in a div. Anything ELSE between them (a banner, an alert)
   simply doesn't match and keeps the unjoined look, which is the intended degradation.

   Measured before writing: header [592,68,986,99] margin 8px 12px 0; tabbar [580,168,1010,71]
   margin 0. They already abut vertically (167 → 168); only the 12px side margins differ. */
.page-header:has(+ .mud-tabs),
:has(> .page-header):has(+ .mud-tabs) > .page-header {
    border-bottom-left-radius: 0 !important;
    border-bottom-right-radius: 0 !important;
    border-bottom: 0;
    /* Casts UPWARD only. The panel's downward shadow belongs to the tab bar below; leaving
       the original `0 10px 32px` here would drop a shadow across the seam and draw the very
       line this join exists to remove. */
    box-shadow: 0 -6px 24px rgba(15, 23, 43, 0.05) !important;
}

.page-header + .mud-tabs > .mud-tabs-tabbar,
:has(> .page-header) + .mud-tabs > .mud-tabs-tabbar {
    /* Tracks PageHeader's ml-4/mr-4 — the bar is the same panel, so the two must agree. */
    margin: 0 16px;
    /* Inset so the first tab's active pill clears the rounded bottom-left corner instead of
       overhanging it. Kept asymmetric and modest: this padding comes straight out of the
       space available to the tabs, and a symmetric 18px was enough to tip a 9-tab page into
       MudBlazor's overflow-scroll mode — arrows appear and swallow the utility icons, which
       is a worse outcome than a flush edge. The tab padding below is trimmed to pay for it. */
    padding-left: 16px;
    padding-right: 8px;
    background-color: var(--defex-glass-panel);
    border: 1px solid var(--defex-glass-edge);
    border-top: 1px solid var(--defex-rule);
    border-bottom-left-radius: var(--defex-radius-panel);
    border-bottom-right-radius: var(--defex-radius-panel);
    box-shadow: var(--defex-panel-shadow);
}

/* The global .mud-tabs top rule would run the FULL width of .mud-tabs — 12px proud of the
   joined panel on each side — so the seam rule moves onto the bar itself, above.

   margin-top matters just as much: ~275 pages put `mt-2` (8px) on their MudTabs, which opens a
   gap between the header and the bar and breaks the join. Property happens not to, which is why
   it looked seamless while Contact and most list screens showed a seam. Zeroed here rather than
   swept across those pages — the join is a styling concern, and the margin is meaningless once
   the two elements are one panel. */
.page-header + .mud-tabs,
:has(> .page-header) + .mud-tabs {
    border-top: 0 !important;
    margin-top: 0 !important;
}

/* No margin here. The first card inside the panels already carries its own mt-4, so adding
   16px on the container stacked to a 33px gap under the tab bar while every other gap on the
   page was 16 — the panels area must stay flush and let the cards space themselves. */
.page-header + .mud-tabs > .mud-tabs-panels,
:has(> .page-header) + .mud-tabs > .mud-tabs-panels {
    margin-top: 0;
}

/* ===== Header + toolbar strip as ONE panel =====
   The same join as the tab strip above, for pages whose header is followed by a control bar
   rather than MudTabs — JobPlanner's date/view/actions toolbar. Opted into with a class
   rather than a structural :has(+ …) selector, because a toolbar is just a MudPaper and is
   indistinguishable from any other paper a page might place under its header.

   Standalone first: JobPlanner can collapse its header away, and when it does the bar is the
   page's only chrome panel and has to round all four corners on its own. The joined overrides
   follow and only apply while a header is actually its previous sibling. */
.page-header-bar {
    margin: 4px 16px 0;
    padding: 6px 12px;
    background: var(--defex-glass-panel) !important;
    border: 1px solid var(--defex-glass-edge);
    border-radius: var(--defex-radius-panel) !important;
    box-shadow: var(--defex-panel-shadow) !important;
}

.page-header:has(+ .page-header-bar),
:has(> .page-header):has(+ .page-header-bar) > .page-header {
    border-bottom-left-radius: 0 !important;
    border-bottom-right-radius: 0 !important;
    border-bottom: 0;
    /* Upward-only, for the reason given on the tab-strip join: a downward cast would draw a
       shadow line straight across the seam we're trying to erase. */
    box-shadow: 0 -6px 24px rgba(15, 23, 43, 0.05) !important;
}

.page-header + .page-header-bar,
:has(> .page-header) + .page-header-bar {
    /* Flush against the header, and on its side edges — the two are one panel now. */
    margin-top: 0;
    border-top: 1px solid var(--defex-rule);
    border-top-left-radius: 0 !important;
    border-top-right-radius: 0 !important;
}

/* ===== Shadow utilities ===== */
/* THE GLASS BAR. Design D takes its bar from Direction A: translucent white, blurred and
   saturated, a white bottom rule instead of a shadow line, and a wide diffuse cast rather
   than the old hard 20%-black drop. The translucency comes from the palette
   (Theme.cs AppbarBackground); the blur and edge live here. */
.appbar-shadow {
    backdrop-filter: blur(20px) saturate(1.4);
    -webkit-backdrop-filter: blur(20px) saturate(1.4);
    /* D specifies `1px solid rgba(255,255,255,.6)` here. Dropped deliberately: every other white
       hairline in this design sits on a ROUNDED panel edge, where it reads as a glass rim. The app
       bar is full-bleed and square, so the same value renders as a hard white rule 1600px wide
       across the whole app. The diffuse cast below already separates the bar from the content. */
    box-shadow: 0 1px 24px rgba(15, 23, 43, 0.06);
}

/* Bare `aside` is the MudDrawer element. Excluded here because .nav-menu-drawer paints its
   own hue-matched shadow (nav-menu.css) — a neutral 20%-black shadow cast by a translucent
   glass surface reads as dirt on the page behind it, not as depth. Other asides keep the
   original treatment.

   .wf-rail is excluded for the same reason: it is a page-level <aside> that already carries D's
   SOLID card treatment (learn.css), and this !important rule out-ranks its --defex-card-shadow,
   leaving one card on the screen with a hard 20%-black drop while its neighbours have the diffuse
   one. Any future <aside> that takes a card treatment belongs on this list too.

   .ct-deck-rail is excluded because it is not a surface at all: the deck's notes rail is a
   transparent column that holds its own cards, so a shadow cast by nothing rendered as a shaded
   panel sitting behind them. It also animates its width open and closed, which dragged the drop
   along with it. */
aside:not(.nav-menu-drawer):not(.wf-rail):not(.ct-deck-rail) {
    box-shadow: 0px 3px 8px rgb(0 0 0 / 20%) !important;
}

.custom-shadow {
    box-shadow: var(--tm-card-shadow, 1px 2px 8px hsl(220deg 40% 70% / 0.55));
}

/* ===== Misc utilities ===== */
.floating-container {
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 1000;
}

.floating-button {
    margin-bottom: 10px;
}

.error-cell {
    background-color: #FF8D8D;
}

.asset-type-icon {
    width: 24px;
    height: 24px;
    filter: brightness(0.3);
}

/* Richer PNG asset-type image (admin/detail screens). Larger than the SVG icon and unfiltered (keeps full colour). */
.asset-type-image {
    width: 40px;
    height: 40px;
    object-fit: contain;
}

/* Grid-row variant: the list needs the image at icon scale, but still unfiltered. */
.asset-type-image-sm {
    width: 28px;
    height: 28px;
}

/* The fill goes on .mud-input — the box the outlined border traces — not on the control root
   that Class="custom-field" lands on. The root also wraps the floating label and helper text,
   so it is taller than the box and a fill there paints a square slab of surface around a round
   field. Kept in sync with the same pair of rules in
   AI.Client.Mobile.Shared/wwwroot/css/mobile-detail.css, which is what the MAUI host links. */
.custom-field .mud-input.mud-input-outlined,
.custom-field-gray .mud-input.mud-input-outlined {
    border-radius: var(--defex-radius-control);
}

.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);
}

/* Editable fields in a register modal are white wells, so they read as "type here"
   against the modal's tinted ground and its near-white section cards. The outlined
   border is what separates a field from the card it sits on now that both are light,
   so it is nudged up a step below.

   --mud-palette-surface, never a literal: a dialog renders outside .main-container,
   so neither .defex-theme-dark nor .mud-theme-dark reaches it (see the theming note
   at the top of this file) and a palette var is the ONLY thing here that follows dark
   mode. It resolves white in light and the raised dark surface in dark.

   DISABLED controls keep the muted ground fill — "you can't edit this" has to stay
   readable at a glance once every live field went white.

   Do NOT extend that exception to [readonly]. MudSelect, MudDatePicker and
   MudTimePicker are all MudInput underneath with ReadOnly="@(!Editable)", and
   Editable is false by default — so every dropdown and picker in the app carries
   `readonly` on its inner input as the mechanism that stops you typing into it, not
   as a statement about editability. A [readonly] exception here greys out exactly
   the controls it looks like it should spare.

   The fill goes on the input container, NOT the control: the control's box also covers
   the helper-text slot underneath, which painted the helper line as a white brick hanging
   off the bottom of the field. The container stops at the input, so the helper sits back on
   the card. Radius matches the control's (--defex-radius-control, set on the dense rule
   above) so the fill still lands exactly under the rounded outline.

   DO NOT close the comment before this paragraph. A terminator sat on the "should spare"
   line once (commit 92e55e8db added this continuation and left the old one in place), and
   the effect is invisible rather than loud: CSS error recovery starts a qualified rule at
   the first stray token, swallows everything up to the next `{`, and DROPS the rule it
   lands on — so the white fill below silently vanished while the disabled rule beside it
   kept working, leaving disabled fields looking MORE filled than editable ones.
   StylesheetCommentLintTests now fails the build on a stray terminator (and note that a
   terminator written inside comment prose, even in backticks, closes the comment too —
   which is why this paragraph spells it out in words).

   .defex-dialog-fullbleed gets the same treatment: dialogs.css paints those dialogs the
   same Design D ground (see "THE CLIP" / the full-bleed body rule there), so their fields
   have the identical problem — an unfilled outlined control reads as a hole in the gradient
   rather than as somewhere to type. .defex-palette is excluded: CommandPaletteDialog draws
   its own edge-to-edge search rule in command-palette.css, and a rounded well inside it
   would cut a pill out of that rule. */
.register-modal-content .mud-input-control:not(.mud-input-control-boolean-input) > .mud-input-control-input-container,
.mud-dialog.defex-dialog-fullbleed:not(.defex-palette) .mud-input-control:not(.mud-input-control-boolean-input) > .mud-input-control-input-container {
    background: var(--mud-palette-surface);
    border-radius: var(--defex-radius-control);
}

.register-modal-content .mud-input-control:not(.mud-input-control-boolean-input):has(.mud-disabled) > .mud-input-control-input-container,
.register-modal-content .mud-input-control:not(.mud-input-control-boolean-input):has(.mud-input-root[disabled]) > .mud-input-control-input-container,
.mud-dialog.defex-dialog-fullbleed:not(.defex-palette) .mud-input-control:not(.mud-input-control-boolean-input):has(.mud-disabled) > .mud-input-control-input-container,
.mud-dialog.defex-dialog-fullbleed:not(.defex-palette) .mud-input-control:not(.mud-input-control-boolean-input):has(.mud-input-root[disabled]) > .mud-input-control-input-container {
    background: var(--mud-palette-background-gray);
}

/* Field definition. A white field on a near-white card leans entirely on its border,
   so the outlined rim gets one step of weight back.

   Mixed off --mud-palette-text-primary rather than a --defex-* literal: those are
   light-theme constants (see --defex-rule at the top of this file), and this rule has
   no dark-mode escape hatch inside a dialog. Mixing against the live text colour
   inverts for free — a dark line on a light field, a light line on a dark one — the
   same trick the status tokens use. */
.register-modal-content .mud-input-control:not(.mud-input-control-boolean-input) .mud-input-outlined-border {
    border-color: color-mix(in srgb, var(--mud-palette-text-primary) 20%, transparent);
}


/* Helper text belongs to the card, not to the field well — no fill of its own. */
.register-modal-content .mud-input-control-helper-container {
    margin-top: 0px !important;
    background: transparent !important;
}

/* Dashboard grid: ensure content is constrained for chart scaling */
.grid-stack .grid-stack-item-content {
    overflow: hidden;
}

/* Dashboard widget spacing */
.widget-card {
    overflow-y: hidden;
    display: flex;
    flex-direction: column;
}

/* Grid-stack widgets are absolutely positioned inside their cell content. */
.grid-stack .widget-card {
    position: absolute;
    top: 6px;
    right: 6px;
    bottom: 6px;
    left: 6px;
}

.widget-card > *:not(.widget-edit-overlay) {
    flex: 1;
    min-height: 0;
}

/* Chart widgets: ensure ECharts scales to container instead of overflowing */
.echart-container {
    min-height: 0;
    overflow: hidden;
}

/* Analytics Explorer: give chart sufficient height so it doesn't look squished */
.analytics-explorer-chart {
    height: 420px;
}

.analytics-explorer-chart .echart-container {
    height: 100%;
}

/* Widget wrappers that contain charts - flex column so chart fills remaining space */
.widget-chart-container {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

.widget-chart-container .echart-container {
    flex: 1;
}

/* Chart fill: takes remaining space in flex layouts (e.g. MudStack with toggle + chart) */
.widget-chart-fill {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.widget-chart-fill .echart-container {
    flex: 1;
}

/* Dashboard edit mode - disable widget content interaction so GridStack handles drag/resize */
.grid-stack-editing > .grid-stack-item > .grid-stack-item-content > .widget-card > *:not(.widget-edit-overlay):not(.widget-drag-header) {
    pointer-events: none;
}

/* Widget drag header - full-width grab strip across the top of the widget */
.widget-drag-header {
    display: none;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 36px;
    z-index: 1;
    cursor: grab;
}

.widget-drag-header:active {
    cursor: grabbing;
}

.widget-card:hover > .widget-drag-header,
.grid-stack-editing .widget-drag-header {
    display: block;
}

/* Widget edit overlay - hidden by default */
.widget-edit-overlay {
    display: none;
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 2;
    gap: 4px;
    align-items: center;
}

.widget-card:hover > .widget-edit-overlay {
    display: flex;
}

.grid-stack-editing .widget-edit-overlay {
    display: flex;
    pointer-events: auto !important;
}

.widget-edit-btn {
    background-color: var(--mud-palette-surface) !important;
    border: 1px solid var(--mud-palette-table-lines) !important;
    opacity: 0.8;
}

.widget-edit-btn:hover {
    opacity: 1;
    background-color: var(--mud-palette-error) !important;
    color: white !important;
}

.widget-edit-move {
    color: var(--mud-palette-text-secondary);
    opacity: 0.6;
    cursor: grab;
}

/* Widget settings menu - hidden by default, shown on hover next to the edit overlay */
.widget-settings-menu {
    display: none;
    position: absolute;
    top: 8px;
    right: 76px;
    z-index: 2;
}

.widget-card:hover .widget-settings-menu {
    display: block;
}

.grid-stack-editing .widget-settings-menu {
    display: block;
}

/* Add widget dropzone - hidden by default, shown in edit mode */
.widget-add-dropzone {
    display: none;
    margin: 6px;
    padding: 24px;
    border: 2px dashed var(--mud-palette-table-lines);
    border-radius: 12px;
    cursor: pointer;
    transition: border-color 0.2s, background-color 0.2s;
}

.widget-add-dropzone:hover {
    border-color: var(--mud-palette-primary);
    background-color: var(--mud-palette-primary-hover);
}

.grid-stack-editing ~ .widget-add-dropzone {
    display: block;
}

.widget-add-dropzone-inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-height: 100px;
}

/* Responsive dashboard grid */
@media (max-width: 768px) {
    .grid-stack {
        min-width: 0 !important;
    }
    .grid-stack > .grid-stack-item > .grid-stack-item-content {
        overflow-y: auto;
    }
}

/* Help labels */
/* The root is block-level so the field value below (link, chip, switch, text) starts on its own
   line, matching the old block MudText labels. Inside flex rows it behaves as a flex item. */
.help-label-root {
    display: block;
}

.help-label-container {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    position: relative;
}

    .help-label-container .help-label-text {
        cursor: pointer;
        transition: color 0.15s ease, text-decoration-color 0.15s ease;
        text-decoration: underline;
        text-decoration-color: transparent;
        text-underline-offset: 3px;
    }

        .help-label-container .help-label-text:hover {
            color: var(--mud-palette-primary);
            text-decoration-color: var(--mud-palette-primary);
        }

    .help-label-container .help-icon {
        transition: opacity 0.2s ease, color 0.15s ease;
        border: 1px solid currentColor;
        border-radius: 50%;
        padding: 1px;
        width: 14px;
        height: 14px;
        font-size: 0.875rem;
        opacity: 0.45;
        color: var(--mud-palette-text-secondary);
    }

    .help-label-container .help-icon-button {
        display: inline-flex;
        align-items: center;
        cursor: pointer;
    }

        .help-label-container .help-icon-button:focus-visible {
            outline: 2px solid var(--mud-palette-primary);
            outline-offset: 2px;
            border-radius: 50%;
        }

        .help-label-container .help-icon-button:hover .help-icon,
        .help-label-container .help-icon-button:focus-visible .help-icon {
            opacity: 1;
            color: var(--mud-palette-primary);
        }

/*
   Popover renders in MudPopoverProvider (#popovercontent-*). Scoped CSS often misses it;
   max-height/overflow are set on MudPopover Style (inline on that node). Keep z-index here.
*/
.mud-popover.help-label-popover {
    z-index: 1301;
}

/* Drag handle icon lives inside portaled popover, next to the close button */
.mud-popover.help-label-popover .help-label-popover-drag-handle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 6px;
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
    opacity: 0.5;
    touch-action: none;
}

/* Make sure the grab cursor shows on the inner MudIcon/svg as well */
.mud-popover.help-label-popover .help-label-popover-drag-handle * {
    cursor: inherit;
}

.mud-popover.help-label-popover .help-label-popover-drag-handle:hover {
    opacity: 0.85;
}

.mud-popover.help-label-popover .help-label-popover-drag-handle:active,
.mud-popover.help-label-popover .help-label-popover-drag-handle:active * {
    cursor: grabbing;
}

/* 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);
}

/* Glow hint — pulsing blue box-shadow used to draw attention to the next action.
   Mirrors AI.Client.Mobile.Shared/Pages/Mobile/Job.razor lines 176-187. */
.pulse-hint {
    border-radius: 4px;
    animation: pulseHintBlue 1.6s ease-in-out infinite;
}

@keyframes pulseHintBlue {
    0%, 100% {
        box-shadow: 0 0 0 0 rgba(37, 150, 255, 0.85), 0 0 0 0 rgba(37, 150, 255, 0.55);
    }
    50% {
        box-shadow: 0 0 0 6px rgba(37, 150, 255, 0.45), 0 0 18px 10px rgba(37, 150, 255, 0);
    }
}

/* ===== Get Started onboarding hub: overall progress ring ===== */
.onboarding-ring {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.onboarding-ring .mud-progress-circular {
    width: 90px !important;
    height: 90px !important;
}

.onboarding-ring__label {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    line-height: 1;
}

.onboarding-ring__pct {
    font-weight: 700;
    line-height: 1;
}

.onboarding-ring__sign {
    font-size: 0.6em;
    margin-left: 1px;
}

.onboarding-step {
    cursor: pointer;
    border-radius: 6px;
    padding-left: 4px;
    padding-right: 4px;
    transition: background-color 0.12s ease;
}

.onboarding-step:hover {
    background-color: var(--mud-palette-action-default-hover);
}

.onboarding-step__action {
    opacity: 0;
    transition: opacity 0.12s ease;
}

.onboarding-step:hover .onboarding-step__action {
    opacity: 1;
}

.onboarding-step--done {
    cursor: default;
}

.onboarding-step--done:hover {
    background-color: transparent;
}

/* ===== Read-only rich-text display =====
   Echoes RichTextField content (e.g. invoice/quote payment terms) outside the
   editor. The editable .rtf-editor forces vertical-align:top + sized columns on
   its layout tables; this mirrors that for the plain MarkupString render so the
   columns line up the same way (cells otherwise default to vertical-align:middle
   and the shorter column drifts out of alignment). Borderless, like the report. */
.rtf-display table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
    margin: 4px 0;
}

.rtf-display td {
    vertical-align: top;
    padding: 4px 8px;
}

.rtf-display img {
    max-width: 100%;
    height: auto;
}

.rtf-display a {
    color: #1570EF;
    text-decoration: underline;
}

/* ===== Defect template product cards =====
   Compact "product row in a card" used by the Products section of a Defect
   Template. Written as plain flex rather than nested MudStacks because the
   MudStack version could not ellipsis correctly: a flex child defaults to
   min-width:auto, so the name box refused to shrink below its content and the
   chip/actions squeezed the name down to a few characters ("Pres...") while
   whitespace sat unused to the right. The min-width:0 chain below is what
   actually lets the name take the leftover width and truncate only at the end. */
.dt-product-card {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 12px 8px 12px 14px;
    border: 1px solid var(--mud-palette-table-lines);
    border-radius: 14px;
    background-color: var(--mud-palette-surface);
    transition: border-color 150ms ease, box-shadow 150ms ease;
}

.dt-product-card:hover {
    border-color: color-mix(in srgb, var(--mud-palette-primary) 35%, var(--mud-palette-table-lines));
    box-shadow: 0 1px 6px rgb(16 24 40 / 0.07);
}

.defex-theme-dark .dt-product-card:hover,
.mud-theme-dark .dt-product-card:hover {
    box-shadow: 0 1px 6px rgb(0 0 0 / 0.35);
}

/* Required products carry a left accent stripe so the ordering (required first)
   is legible at a glance without reading every chip. */
.dt-product-card--required {
    border-left: 3px solid var(--mud-palette-info);
    padding-left: 12px;
}

/* Rounded-square tile instead of a solid dark avatar: the avatar was the
   heaviest element on the card and pulled the eye away from the product name. */
.dt-product-card__icon {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 10px;
    background-color: color-mix(in srgb, var(--mud-palette-primary) 10%, var(--mud-palette-surface));
    color: var(--mud-palette-primary);
}

.defex-theme-dark .dt-product-card__icon,
.mud-theme-dark .dt-product-card__icon {
    background-color: color-mix(in srgb, var(--mud-palette-primary) 26%, var(--mud-palette-surface));
    color: var(--mud-palette-primary-text);
}

.dt-product-card__body {
    flex: 1 1 auto;
    min-width: 0;
    padding-top: 1px;
}

.dt-product-card__name {
    font-size: 0.9375rem;
    font-weight: 500;
    line-height: 1.35;
    color: var(--mud-palette-text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dt-product-card__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 3px;
    min-width: 0;
    font-size: 0.8125rem;
    color: var(--mud-palette-text-secondary);
}

.dt-product-card__part {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
    font-variant-numeric: tabular-nums;
}

.dt-product-card__sep {
    flex: 0 0 auto;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background-color: currentColor;
    opacity: 0.55;
}

.dt-product-card__qty {
    flex: 0 0 auto;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.dt-product-card__qty b {
    font-weight: 600;
    color: var(--mud-palette-text-primary);
}

/* Actions never shrink and never steal width from the name. Muted at rest so
   the card reads as content first; full strength on hover/focus. */
.dt-product-card__actions {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 0;
    margin-left: 4px;
    opacity: 0.6;
    transition: opacity 150ms ease;
}

.dt-product-card:hover .dt-product-card__actions,
.dt-product-card:focus-within .dt-product-card__actions {
    opacity: 1;
}

.dt-product-card__actions .mud-icon-button {
    color: var(--mud-palette-text-secondary);
}

.dt-product-card__actions .mud-icon-button:hover {
    color: var(--mud-palette-text-primary);
}

/* Touch devices have no hover: keep the actions at full strength there. */
@media (hover: none) {
    .dt-product-card__actions {
        opacity: 1;
    }
}

/* ===== Feature Switches register (/platform/features) =====
   One ruled sheet rather than a card per category: the category is a break INSIDE
   the ledger, so the eye runs down a single Status column instead of restarting at
   each group. Colours come from the shared palette/status tokens only, so the sheet
   holds in dark and in the red tenant-operator theme. */
.feature-switch-table td,
.feature-switch-table th {
    padding: 10px 24px;
    vertical-align: middle;
}

/* Section break. Quiet by design — 500 weight and muted ink, no uppercase
   (The Shouting Reserve Rule keeps tracking-plus-caps for status labels alone). */
.feature-switch-table tr.feature-switch-group td {
    padding: 14px 24px 6px;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--mud-palette-text-secondary);
    background-color: transparent;
    border-bottom: 1px solid var(--mud-palette-table-lines);
}

.feature-switch-table tr.feature-switch-group:first-child td {
    padding-top: 8px;
}

/* Hover belongs to the rows you can act on, not to the section breaks. */
.feature-switch-table tbody tr.feature-switch-group:hover {
    background-color: transparent !important;
}

.feature-switch-name {
    font-size: 0.875rem;
    font-weight: 500;
}

.feature-switch-desc {
    font-size: 0.875rem;
    color: var(--mud-palette-text-secondary);
    max-width: 62ch;
}

/* Fixed widths stop the Status and switch columns from reflowing as pills appear
   and disappear — a column that jitters on every toggle reads as unreliable. */
.feature-switch-col-status { width: 190px; }
.feature-switch-col-switch { width: 130px; }
.feature-switch-col-action { width: 170px; text-align: right; }
.feature-switch-table td.feature-switch-col-action { text-align: right; }

/* Tenant record → Features tab. Wider than the platform sheet's switch column because the
   control here is a three-state Inherit/On/Off group, not a single switch. */
.tenant-feature-col-state { width: 270px; }

/* The resting state carries no colour: on a sheet where nothing is switched off,
   the only chromatic elements are the switches themselves. */
.feature-switch-default {
    font-size: 0.875rem;
    color: var(--mud-palette-text-secondary);
}

/* ===== Mobile app version register (/platform/mobile-version) =====
   Two ruled sheets, one per store. Flat at rest and bordered rather than boxed, so the pair
   reads as one register rather than two floating cards. Every colour resolves from the theme
   or the shared status tokens, so it holds in dark and in the red tenant-operator theme. */
.mv-section {
    display: flex;
    flex-direction: column;
    height: 100%;
}

.mv-section__head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding-bottom: 12px;
    margin-bottom: 16px;
    border-bottom: 1px solid var(--mud-palette-table-lines);
}

/* Nothing is blocked: the resting state is muted text, not a green pill. Saturated colour on
   this screen is earned only by a gate that is actually stopping someone from working. */
.mv-section__quiet {
    font-size: 0.875rem;
    color: var(--mud-palette-text-secondary);
}

/* Flows with the rest of the sheet rather than being pinned to the bottom: every helper and
   error message on this screen is one line, so the two platforms' rows — switch included —
   land on the same baselines whether or not one side is showing an error. */
.mv-section__force {
    padding-top: 4px;
}

.mv-section__hint {
    font-size: 0.875rem;
    color: var(--mud-palette-text-secondary);
    max-width: 62ch;
    /* Clears MudSwitch's own leading box so the sentence lines up under its label. */
    margin-left: 4px;
}

/* Tenant override list inside the dialog. */
.feature-switch-tenants {
    max-height: 46vh;
    overflow-y: auto;
}

.feature-switch-tenants td {
    padding: 6px 12px;
    vertical-align: middle;
}

.feature-switch-tenant-name {
    font-size: 0.875rem;
}

/* Property > Assets tab action row.
   The routine prompt alert shares the row with the Import/Bulk/Create buttons.
   Without this the alert steals the row's width, the buttons shrink, their
   labels wrap onto two lines and the whole row grows taller. Keep the buttons
   at their natural size and let the alert absorb whatever is left. */
.asset-actions-row .routine-asset-prompt {
    flex: 1 1 auto;
    min-width: 0;
}

.asset-actions-row > .mud-button-root {
    flex: 0 0 auto;
    white-space: nowrap;
}

/* ── Inventory dashboard widgets (/inventory/dashboard) ──────────────────────
   .inventory-widget is the SOLID-tier card; these rules cover the states and the
   text-overflow cases the happy-path markup didn't. Theme-aware throughout —
   the --status-* and --mud-palette-* tokens already resolve for dark and for the
   red tenant-operator theme, so nothing here hard-codes a colour. */

/* A long item or location name must not push the status chip off the row.
   Flex children default to min-width:auto, which refuses to shrink below their
   content — the chip is what loses, and it's the part carrying the severity. */
.inventory-widget__title,
.inventory-widget .mud-stack > .mud-stack {
    min-width: 0;
}

.inventory-widget__title .mud-typography,
.inventory-widget__body .mud-typography-subtitle2 {
    overflow-wrap: anywhere;
}

/* Header actions (the expiry-window select) keep their size; on a narrow card the
   header wraps and the control drops below the title, rather than both being
   squeezed into one row and wrapping their own labels to two lines. */
.inventory-widget__header {
    flex-wrap: wrap;
    row-gap: 8px;
}

.inventory-widget__actions {
    flex: 0 0 auto;
}

@media (max-width: 599px) {
    .inventory-widget__actions {
        flex: 1 1 100%;
    }
}

/* Failure state. Distinct from empty by ink, icon and the presence of a retry —
   never by wording alone, because the two states are read at a glance. */
.inventory-widget__error {
    color: var(--status-error-fg);
    padding: 4px 0;
}

.inventory-widget__error .mud-icon-root {
    color: var(--status-error-fg);
    margin-top: 2px;
    flex: 0 0 auto;
}

.inventory-widget__error-body {
    min-width: 0;
}

/* The retry sits under its message, tucked back to the text's left edge so the
   two read as one block rather than as a stray control. */
.inventory-widget__error-body .mud-button-root {
    margin-left: -8px;
    margin-top: 2px;
    min-height: 36px;
}

/* Secondary metadata. Deliberately NOT Color.Secondary: that resolves to
   signal-blue, which is this system's action colour, and painting inert text in
   it promises a link that isn't there. */
.inventory-widget__meta {
    color: var(--mud-palette-text-secondary);
}

.inventory-widget__meta-row {
    margin-top: 12px;
    padding-top: 8px;
    border-top: 1px solid var(--mud-palette-divider);
}

/* Keeps the "Updated …" stamp on the right when the truncation note is absent. */
.inventory-widget__meta-row > .mud-typography:only-child {
    margin-left: auto;
}

.inventory-widget__footer-actions {
    margin-top: 8px;
}

/* Definition icon beside a widget title. Sits at the weight of metadata, not of the
   heading — it answers a question the reader may not have, and shouldn't compete with
   the title for the ones who already know the term. */
.inventory-widget__hint {
    color: var(--mud-palette-text-secondary);
    cursor: help;
}

/* ── Inventory dashboard page layout (/inventory/dashboard) ──────────────────
   Multi-column, not a row grid. A row grid ties every row's height to its tallest
   card, which is what left ~40% of the first screen empty when a six-row card sat
   beside a one-line card. Columns pack: each card takes its own height only.

   `columns: <width> <count>` is self-responsive — as many columns as fit at 520px
   minimum, capped at three. One column on a phone, two at a typical laptop width,
   three on a wide monitor, with no breakpoint list to maintain. */
.inventory-dashboard__masonry {
    columns: 520px 3;
    column-gap: 24px;
}

/* The headline value strip, above the exception masonry.

   A grid rather than the masonry's columns, because these tiles are peers that should sit
   side by side and stay equal width — the masonry deliberately packs cards of unequal
   height, which is the opposite of what a row of summary figures wants.

   auto-fit + minmax means the tiles reflow to one per row on a phone without a breakpoint
   list, matching how the masonry below already behaves. */
.inventory-dashboard__valuestrip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 16px;
}

.inventory-dashboard__cell {
    /* Without this a card can be split across a column boundary mid-row. */
    break-inside: avoid;
    /* Chrome honours break-inside on a block only when it isn't a lone anonymous
       box; the inline-block establishes the fragmentation context reliably. */
    display: inline-block;
    /* Inline-block baseline alignment would otherwise leave a few pixels of descender
       space under every card. */
    vertical-align: top;
    width: 100%;
    margin-bottom: 24px;
}

/* Hidden, not unmounted: dropping the component from the tree would dispose it and
   re-fetch the moment its feed had something to report again. */
.inventory-dashboard__cell.is-quiet {
    display: none;
}

/* The all-clear strip. Ruled, not boxed — the cards above already carry the boxes,
   and this is the one region on the page that is explicitly NOT work. */
.inventory-dashboard__allclear {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin-top: 8px;
    padding-top: 16px;
    border-top: 1px solid var(--mud-palette-divider);
    color: var(--mud-palette-text-secondary);
}

.inventory-dashboard__allclear > .mud-icon-root {
    color: var(--status-success-fg);
    flex: 0 0 auto;
    margin-top: 1px;
}

.inventory-dashboard__allclear-items {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 24px;
    min-width: 0;
}

.inventory-dashboard__allclear-item {
    margin: 0;
    font-size: 0.8125rem;
    line-height: 1.43;
}

.inventory-dashboard__allclear-title {
    font-weight: 500;
    color: var(--mud-palette-text-primary);
    margin-right: 6px;
}

@media (max-width: 599px) {
    .inventory-dashboard__allclear-items {
        gap: 8px 0;
    }

    .inventory-dashboard__allclear-item {
        flex: 1 1 100%;
    }
}

/* ── Inventory dashboard rows ────────────────────────────────────────────────
   Ruled rows, not nested cards. Each exception used to arrive as its own bordered,
   rounded, shadowed MudPaper inside the widget's bordered, rounded, shadowed
   MudPaper — three containers deep to show one line. The rule and the alignment do
   that work now, which is both quieter and what the design system actually asks for. */
.inv-rows > .inv-row {
    border-bottom: 1px solid var(--mud-palette-divider);
}

.inv-rows > .inv-row:last-child {
    border-bottom: none;
    padding-bottom: 0;
}

.inv-row {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 12px;
    padding: 10px 0;
}

/* The wash goes on the row itself, not a ::before overlay. A positioned pseudo-element
   with a background paints above non-positioned in-flow content, so the overlay version
   washed over the row's own text. It also lines the wash up with the rules above and
   below, which span the same content width. */
.inv-row {
    transition: background-color 120ms ease-out;
}

.inv-row:hover,
.inv-row:focus-within {
    background-color: var(--mud-palette-action-default-hover);
}

.inv-row__body {
    flex: 1 1 12rem;
    min-width: 0;
}

.inv-row__link,
.inv-row__title {
    display: block;
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.43;
    color: var(--mud-palette-text-primary);
    text-decoration: none;
    overflow-wrap: anywhere;
}

.inv-row__link:hover {
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* Stretched link: the anchor's own box stays around the title for a sensible focus
   ring, while this overlay makes the entire row clickable. Actions raise themselves
   above it below, so no interactive element ends up nested inside another. */
.inv-row__link::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
}

.inv-row__link:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
    border-radius: 2px;
}

.inv-row__meta {
    margin: 2px 0 0;
    font-size: 0.75rem;
    line-height: 1.43;
    color: var(--mud-palette-text-secondary);
    overflow-wrap: anywhere;
}

.inv-row__status,
.inv-row__actions {
    position: relative;
    z-index: 1;
    flex: 0 0 auto;
}

.inv-row__status {
    margin-left: auto;
}

.inv-row__actions {
    display: flex;
    align-items: center;
    gap: 2px;
}

/* Touch targets. The old text buttons measured 31px tall — under the 44px minimum,
   and these are read in a warehouse, often one-handed. */
.inv-row__actions .mud-button-root,
.inv-row__actions .mud-icon-button {
    min-height: 44px;
}

.inv-row__actions .mud-icon-button {
    min-width: 44px;
}

@media (max-width: 599px) {
    .inv-row__body {
        flex: 1 1 100%;
    }

    .inv-row__status {
        margin-left: 0;
    }

    .inv-row__actions {
        margin-left: auto;
    }
}

/* ── Inventory dashboard status chips ────────────────────────────────────────
   One ladder — neutral, positive, warning, critical — built on the --status-*
   token trio, which is already colour-mixed against --mud-palette-surface and so
   resolves for dark and for the red tenant-operator theme with no hex here.

   All four share one shape and one weight. Severity is the tint alone, because the
   previous mix of filled and outlined variants meant the same severity could arrive
   in two different treatments on the same screen. Filled is gone entirely: six
   saturated red blocks stacked down a card read as an alarm wall, where six tinted
   pills read as six facts and let a genuinely critical one stand out. */
.inv-chip {
    display: inline-flex;
    align-items: center;
    padding: 3px 10px;
    border: 1px solid transparent;
    border-radius: 6px;
    font-size: 0.7rem;
    font-weight: 700;
    line-height: 1.43;
    letter-spacing: 0.04em;
    white-space: nowrap;
}

/* A quantity is not a severity. "Move 8" used to be painted in the action colour,
   which promised something clickable and spent signal-blue on a number. */
.inv-chip--neutral {
    background-color: var(--mud-palette-background-grey);
    border-color: var(--mud-palette-divider);
    color: var(--mud-palette-text-secondary);
}

/* Green is demoted on purpose. A filled green "Healthy" block was the loudest mark
   on a screen with nothing wrong, which is backwards: the register should be quiet
   until something is due. */
.inv-chip--positive {
    background-color: var(--status-success-bg);
    border-color: var(--status-success-border);
    color: var(--status-success-fg);
}

.inv-chip--warning {
    background-color: var(--status-warning-bg);
    border-color: var(--status-warning-border);
    color: var(--status-warning-fg);
}

.inv-chip--critical {
    background-color: var(--status-error-bg);
    border-color: var(--status-error-border);
    color: var(--status-error-fg);
}

/* The widget header icons were seven identical Color.Primary marks — the action
   colour spent on decoration, seven times, competing with the chips that actually
   need to be seen. Muted ink here is what lets one critical chip carry across a
   workshop. */
.inventory-widget__title .mud-icon-root {
    color: var(--mud-palette-text-secondary);
}

/* …except on the crash notice, where the header icon IS the message. Muting it there
   would make a broken panel look like an ordinary quiet one. */
.inventory-widget__title--error .mud-icon-root {
    color: var(--status-error-fg);
}

/* ===== Data Health (platform) =====
   Prefix: health-. Used by /platform/health and /platform/health/issues.

   The KPI figures earn their colour from a non-zero count: a tenant with nothing
   wrong renders the whole tile row in ink, so a single red numeral is legible at a
   glance (DESIGN.md -> The Colour-Means-Something Rule). Severity itself is carried
   by .status-pill--* rather than a bespoke hue, so these screens stay locked to the
   same severity vocabulary as pills and alerts everywhere else. */
.health-figure {
    font-size: 2.125rem;
    font-weight: 600;
    line-height: 1.235;
}

.health-figure--critical,
.health-figure--error {
    color: var(--status-error-fg);
}

.health-figure--warning {
    color: var(--status-warning-fg);
}

/* Secondary metadata under a figure or beside a rule name. Field-label role. */
.health-caption {
    font-size: 0.8125rem;
    line-height: 1.43;
    color: var(--mud-palette-text-secondary);
}

/* The machine rule key (Rel.JobAsset.AssetId.Orphan) sitting under its display name. */
.health-rulekey {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.8125rem;
    color: var(--mud-palette-text-secondary);
    word-break: break-all;
}

/* Run rows navigate to that run's issues. */
.health-row-link {
    cursor: pointer;
}

/* ===== App-bar global search =====
   The bordered pill is the .search-box div itself; the MudTextField inside must draw
   nothing, or the control shows a second, inner outline. MudBlazor renders the outlined
   border as a <fieldset>, and the global 24px control radius rounds it into a visible
   inner pill, so kill the fieldset outright rather than just recolouring it. */
.search-box .search-box-input fieldset,
.search-box .mud-input.mud-input-outlined > .mud-input-outlined-border {
    /* MudBlazor's own rule is more specific, hence !important. */
    display: none !important;
}

.search-box .search-box-input .mud-input,
.search-box .search-box-input .mud-input-slot {
    border: none;
    outline: none;
    box-shadow: none;
    background-color: transparent;
}

/* ===== Proposal property cards (Proposal.razor, Details tab) =====
   Three ruled bands: identity, captured scope, and a footer ledger line. The footer is
   pushed to the bottom of the card rather than following the last chip, so every card in a
   row terminates on the same line however much scope each one carries.

   That is the fix, and it is two faults at once: "N assets" used to sit wherever the chips
   left it, and "Add Asset Type" shared the chips' wrap row — so the one action on the card
   landed at a different position on every card in the row, and moved every time an asset
   type was added. Data wraps; the action holds still. */
.pp-card {
    height: 100%;
    display: flex;
    flex-direction: column;
}

.pp-card-head {
    display: flex;
    align-items: flex-start;
    gap: 8px;
}

/* Without min-width:0 a long address refuses to wrap and shoves the row actions off the card. */
.pp-card-identity {
    min-width: 0;
    flex: 1 1 auto;
}

/* 16px, not h6's 20px: a card title must not outrank the "Properties" section header (18px)
   that owns it. Weight carries the step instead — the Weight-Before-Size Rule. */
.pp-card-name {
    font-size: 1rem;
    font-weight: 600;
    line-height: 1.4;
    overflow-wrap: anywhere;
}

.pp-card-address {
    color: var(--mud-palette-text-secondary);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Pulls the icon buttons' own hit padding back so the glyphs align to the card's inner edge
   and the title's cap height, instead of floating inboard and low. */
.pp-card-actions {
    display: flex;
    flex: 0 0 auto;
    margin: -6px -8px 0 0;
}

.pp-card-scope {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--mud-palette-table-lines);
}

.pp-card-scope-empty {
    color: var(--mud-palette-text-secondary);
}

/* margin-top:auto is what aligns the footers across a row of unequal cards. */
.pp-card-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-top: auto;
    padding-top: 12px;
    border-top: 1px solid var(--mud-palette-table-lines);
}

.pp-card-count {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--mud-palette-text-secondary);
}

/* Hides a MudTabs tab bar when only one panel is rendered - used by pages hosted inside a setup
   dialog (Users in Embedded mode), where a lone "List" tab is noise. */
.hide-tab-header .mud-tabs-toolbar {
    display: none;
}

/* ===== PageHeader title: one line, never wrapped =====
   The title block and the setup strip are both flex children of the header panel with the
   default shrink factor, so a long entity name (a client called "Rozet Group 2 (setup contract
   before property)") was being squeezed and wrapped onto two or three lines while the setup
   strip beside it kept its full width.

   The documented shrink order is: setup strip first, actions never (they are flex-shrink-0).
   The rules below enforce it, and the title text itself carries nowrap + ellipsis as the
   backstop for the genuinely-too-narrow viewport. */
.page-header__titleblock {
    flex: 0 1 auto;
    min-width: 0;
}

/* The setup strip takes the whole gap between the title and the actions (setup-banner.css
   gives it flex-basis 0 + grow), so the MudSpacer that used to fill that gap must stop
   growing — otherwise the two split it and the strip gets half the room it could have had. */
.page-header:has(.page-header__setup) > .flex-grow-1 {
    flex-grow: 0 !important;
}

.page-header__title-row {
    min-width: 0;
}

.page-header__title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
