/* ============================================================================
   LAYERING POLICY — read this before adding a rule to this file.

   wwwroot/styles/ has three layers. Put a new rule in exactly one:

     core/         Design tokens, primitives, and the global reset ONLY.
                    "Primitive" means a semantic, reusable class with no
                    knowledge of a specific page or business feature — the
                    kind of thing every Ui*.razor component or a brand-new
                    page could use on day one (`.ui-empty-state`, layout shells
                    like `.app-layout`).
                    No selector here should be named after a page, a domain
                    noun (assignment, contractor, ticket, material, site...),
                    or a feature route. If you can point at the one page that
                    owns a rule, it does not belong in core/.

     features/     Domain layout for one feature area (assignments,
                    contractors, sites, operations, public...), shared by
                    several components inside that area. Follows the existing
                    features/<area>/<file>.css naming; assignments in
                    particular splits further into
                    features/operations/assignments/*.css by concern
                    (shell, layout, editors, controls, map). These are lazy
                    loaded per-route by wwwroot/js/siteNavigation.js
                    (featureStyles / featureStylesForPath) — a rule that must
                    render on first paint regardless of route (e.g. inside
                    Routes.razor, or on a page that predates JS running)
                    cannot rely on this layer.

     <Component>.razor.css   Component-private, via Blazor CSS isolation.
                    Use this once a rule is used by exactly one component AND
                    the selector only ever matches markup written directly in
                    that component's own .razor file. Do NOT isolate a
                    selector that reaches into a *child* component's
                    rendered output (e.g. passed through a `Class` parameter
                    like `<UiPageHeader Class="...">`) — Blazor's scope
                    attribute is not guaranteed safe there without a real
                    build to verify against, and getting it wrong is a
                    silent rendering bug, not a compile error. Move rules
                    like that to features/ instead, or — if neither layer can
                    reach it (ownership boundary, no matching route) — leave
                    it in core/ with a comment explaining why, not silently.

   There is no living exception to this policy today. If a new exception is
   ever unavoidable, comment it in place. CoreStylesheetPolicyTests in
   tests/SnowPlow.Routing.Web.Tests/Architecture/ enforces the policy across
   core/ and keeps the retired legacy layer from returning.

   Core sheets and their cascade positions (App.razor links them in order):
     ui-workspace-base.css     workspace element defaults, before component styles
     ui-workspace-density.css  workspace compact sizing, after component styles
     design-tokens.css         tokens only
     ui-system.css             primitives (this file)
     ui-select.css, ui-search.css, ui-motion.css, ui-data-table.css
     ui-theme.css              palette applied to Bootstrap and element defaults
   ============================================================================ */

/* Shared UI primitives for the redesign. Feature CSS remains responsible for
   feature-specific layout while new work can use these semantic classes. */

.ui-page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-space-4);
    margin-bottom: var(--sp-space-5);
}

.ui-page-header-copy {
    min-width: 0;
}

.ui-page-header .page-subtitle {
    margin: 0;
    color: var(--sp-color-ink-muted);
    font-size: 1rem;
    font-weight: 650;
    line-height: 1.45;
}

/* Authenticated pages show their description in WorkspaceHeader; public
   layouts, which have no workspace shell, keep the UiPageHeader copy. */
.workspace-body .ui-page-header .page-subtitle {
    display: none;
}

.ui-page-header-breadcrumb {
    display: flex;
    min-width: 0;
    align-items: center;
    gap: 0.65rem;
}

.ui-page-header-section {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    color: var(--sp-color-ink-muted);
    font-size: var(--sp-font-size-xs);
    font-weight: 800;
    letter-spacing: 0.08em;
    line-height: 1.2;
    text-overflow: ellipsis;
    text-transform: uppercase;
    white-space: nowrap;
}

.ui-page-header-chevron {
    flex: 0 0 auto;
    color: var(--sp-color-brand);
    font-size: 1.35rem;
    font-weight: 500;
    line-height: 1;
}

.ui-page-header .page-title,
.ui-page-header h1 {
    min-width: 0;
    margin: 0;
    font-size: var(--sp-font-size-h1);
    line-height: 1.08;
}

.ui-page-header-breadcrumb .page-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-page-header h2 {
    margin: 0;
    font-size: var(--sp-font-size-h2);
    line-height: 1.15;
}

.ui-page-header h3 {
    margin: 0;
    font-size: var(--sp-font-size-h3);
    line-height: 1.2;
}

/* Actions may shrink below their one-line width so they wrap inside the
   header; a non-shrinking item would never reach its wrap point and would
   overflow the clipped layout at medium widths. */
.ui-page-header-actions {
    display: flex;
    flex: 0 1 auto;
    min-width: 0;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--sp-space-2);
}

.ui-eyebrow {
    margin: 0 0 var(--sp-space-1);
    color: var(--sp-color-brand-strong);
    font-size: var(--sp-font-size-xs);
    font-weight: 800;
    letter-spacing: 0.08em;
    line-height: var(--sp-line-height-tight);
    text-transform: uppercase;
}

.ui-address-grid {
    display: grid;
    gap: 10px;
    min-width: 0;
    width: 100%;
}

.ui-address-locality-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 100px 120px;
    gap: 10px;
    min-width: 0;
    width: 100%;
}

.ui-address-field {
    display: grid;
    gap: 6px;
    min-width: 0;
}

.ui-address-field > label {
    margin: 0;
    color: var(--sp-semantic-text-muted);
    font-size: 0.77rem;
    font-weight: 800;
    line-height: 1.25;
}

.ui-step-heading {
    display: flex;
    align-items: flex-start;
    justify-content: flex-start;
    gap: 12px;
    padding: var(--sp-panel-padding);
    border-bottom: 1px solid var(--sp-semantic-border-default);
    background: var(--sp-semantic-surface-base);
    color: var(--sp-semantic-text-primary);
}

.ui-step-number {
    display: grid;
    flex: 0 0 auto;
    width: var(--sp-control-height);
    height: var(--sp-control-height);
    place-items: center;
    border-radius: var(--sp-radius-md);
    background: var(--sp-color-brand-soft);
    color: var(--sp-color-brand-text);
    font-size: 0.8rem;
    font-weight: 850;
}

.ui-step-heading-copy {
    display: block;
    min-width: 0;
}

.ui-step-heading :where(h2, h3, h4) {
    margin: 0;
    color: var(--sp-semantic-text-primary);
    font-size: 1rem;
    font-weight: 850;
    line-height: 1.25;
}

.ui-step-heading p {
    margin: 3px 0 0;
    color: var(--sp-semantic-text-muted);
    font-size: 0.78rem;
    line-height: 1.4;
}

.ui-empty-state {
    display: grid;
    place-items: center;
    gap: var(--sp-space-2);
    min-height: 10rem;
    padding: var(--sp-space-8);
    color: var(--sp-color-ink-muted);
    text-align: center;
}

.ui-empty-state strong {
    color: var(--sp-color-ink);
}

.ui-loading-region {
    position: relative;
    min-width: 0;
}

/* Background refresh: the content stays usable. Only a refresh that outlasts
   the loading delay dims it under a thin progress line; a fast one shows
   nothing, and the content returns to full strength without a delay. */
.ui-loading-region > :not(.ui-loading-region-live, .ui-loading-region-progress) {
    transition: var(--sp-motion-transition-control);
}

.ui-loading-region.is-refreshing > :not(.ui-loading-region-live, .ui-loading-region-progress) {
    opacity: var(--sp-opacity-refreshing);
    transition: var(--sp-motion-transition-pending);
}

.ui-loading-region-progress {
    position: absolute;
    inset: 0 0 auto;
    z-index: 2;
    height: 2px;
    animation: var(--sp-motion-loading-in);
    pointer-events: none;
}

.ui-loading-region-error {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-space-3);
    padding: var(--sp-space-3) var(--sp-space-4);
    border: 1px solid var(--sp-color-danger-border);
    border-radius: var(--sp-radius-md);
    background: var(--sp-color-danger-soft);
    color: var(--sp-color-ink);
}

.ui-loading-region-error-text {
    min-width: min(100%, 16rem);
    flex: 1 1 16rem;
}

.ui-loading-region > .ui-skeleton + .ui-skeleton,
.ui-loading-region-message + .ui-skeleton {
    margin-top: var(--sp-skeleton-gap, 0.75rem);
}

/* Caller-composed placeholder wrappers stack their parts; the parts themselves
   keep the width they were given. */
.ui-loading-region [class*="skeleton"]:not(.ui-skeleton, .ui-skeleton-layout, [class*="ui-skeleton-layout-"]) {
    display: grid;
    gap: var(--sp-skeleton-gap);
    width: 100%;
}

/* Shaped placeholders (UiSkeletonLayout): silhouettes of the content they
   replace, so the region keeps its footprint when the content arrives. */
.ui-skeleton-layout {
    display: grid;
    gap: var(--sp-skeleton-gap);
    width: 100%;
    min-width: 0;
}

.ui-skeleton-layout-toolbar,
.ui-skeleton-layout-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-space-3);
}

.ui-skeleton-layout-actions {
    justify-content: flex-end;
}

.ui-skeleton-layout-rows {
    overflow: hidden;
    border: 1px solid var(--sp-color-border);
    border-radius: var(--sp-radius-sm);
}

.ui-skeleton-layout-row {
    display: grid;
    grid-template-columns: 2fr repeat(calc(var(--ui-skeleton-columns, 4) - 1), minmax(0, 1fr));
    align-items: center;
    gap: var(--sp-space-4);
    min-height: 2.75rem;
    padding: 0 var(--sp-space-3);
    border-top: 1px solid var(--sp-color-border);
}

.ui-skeleton-layout-row.is-header {
    min-height: 2.5rem;
    border-top: 0;
    background: var(--sp-color-surface-raised);
}

.ui-skeleton-layout-item {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: var(--sp-space-3);
}

.ui-skeleton-layout-copy,
.ui-skeleton-layout-field,
.ui-skeleton-layout-heading {
    display: grid;
    gap: var(--sp-space-2);
    min-width: 0;
}

.ui-skeleton-layout-cards {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
}

.ui-skeleton-layout-card {
    display: grid;
    gap: var(--sp-space-2);
    padding: var(--sp-space-4);
    border: 1px solid var(--sp-color-border);
    border-radius: var(--sp-radius-md);
    background: var(--sp-color-surface);
}

.ui-skeleton-layout-form {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
}

.ui-skeleton-layout-form > .ui-skeleton-layout-actions {
    grid-column: 1 / -1;
}

.ui-skeleton-layout-gallery {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 10rem), 1fr));
}

.ui-skeleton-layout-tile {
    display: grid;
    gap: var(--sp-space-2);
    min-width: 0;
}

.ui-skeleton-layout-thumb {
    aspect-ratio: 4 / 3;
    min-height: 0;
    height: auto;
}

.ui-skeleton-layout-panels {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
    gap: var(--sp-space-4);
}

@media (max-width: 700px) {
    .ui-skeleton-layout-row {
        grid-template-columns: minmax(0, 1fr) minmax(0, 0.6fr);
    }

    .ui-skeleton-layout-row > .ui-skeleton:nth-child(n + 3) {
        display: none;
    }
}

.ui-loading-region-live,
.ui-loading-region-message {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.ui-skeleton {
    display: block;
    width: var(--ui-skeleton-width, 100%);
    min-height: 0.85rem;
    height: var(--ui-skeleton-height, var(--sp-skeleton-text-height, 0.85rem));
    border-radius: var(--sp-skeleton-radius, 0.5rem);
    background: linear-gradient(
        100deg,
        var(--sp-skeleton-base) 25%,
        var(--sp-skeleton-highlight) 45%,
        var(--sp-skeleton-base) 65%
    );
    background-color: var(--sp-skeleton-base);
    background-size: 300% 100%;
}

.ui-skeleton-block {
    min-height: var(--sp-skeleton-block-height, 8rem);
}

.ui-skeleton-circle {
    width: var(--ui-skeleton-width, var(--sp-skeleton-circle-size, 2.5rem));
    height: var(--ui-skeleton-height, var(--sp-skeleton-circle-size, 2.5rem));
    border-radius: 50%;
}

.ui-skeleton-text {
    min-height: var(--sp-skeleton-text-height, 0.85rem);
}

.ui-loading-value {
    display: inline-flex;
    /* Reserves the skeleton width without pushing past a container narrower than it. */
    min-width: min(3rem, 100%);
    min-height: 1.25rem;
    align-items: center;
    vertical-align: middle;
}

@media (prefers-reduced-motion: reduce) {
    .ui-skeleton {
        background: var(--sp-skeleton-base);
    }
}

.ui-icon-button {
    display: inline-grid;
    flex: 0 0 2.25rem;
    width: 2.25rem;
    min-width: 2.25rem;
    max-width: 2.25rem;
    height: 2.25rem;
    min-height: 2.25rem;
    max-height: 2.25rem;
    place-items: center;
    padding: 0;
    aspect-ratio: 1;
    border: 1px solid var(--sp-color-border);
    border-radius: var(--sp-radius-md);
    background: var(--sp-color-surface);
    color: var(--sp-color-ink);
    cursor: pointer;
}

.ui-icon-button:hover {
    border-color: var(--sp-color-brand);
    background: var(--sp-color-brand-soft);
}

:where(button, a, input, select, textarea):focus-visible {
    outline: none;
    box-shadow: var(--sp-focus-ring);
}

.navigation-warning-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: auto;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    border-radius: 999px;
    background: var(--sp-color-danger-solid);
    color: var(--sp-color-white);
    font-size: 11px;
    font-weight: 950;
    line-height: 1;
    box-shadow: 0 0 0 1px rgb(var(--sp-rgb-white) / 0.25);
    font-variant-numeric: tabular-nums;
}

.app-layout,
.public-layout {
    font-family: var(--sp-font-sans);
}

.workspace-body {
    min-width: 0;
}

@media (max-width: 700px) {
    .ui-page-header {
        flex-direction: column;
        gap: var(--sp-space-3);
    }

    .ui-page-header-actions {
        width: 100%;
        justify-content: flex-start;
    }

    .ui-step-heading {
        padding: 14px 15px;
    }
}

@media (max-width: 700px) {
    .ui-address-locality-grid {
        grid-template-columns: 1fr;
    }
}

/* Opt-in bottom action bar for a long phone form: its primary action stays on screen instead of thousands of
   pixels down. Wider screens keep the bar in the page flow. While a bar is rendered its height is reserved as
   root scroll padding, so focusClearOfFixedBars keeps a focused field clear of it, and below the last content.
   Mark the element with data-fixed-action-bar as well. */
@media (max-width: 700px), (pointer: coarse) and (max-height: 500px) {
    :root:has(.ui-fixed-action-bar) {
        --ui-fixed-action-bar-height: calc(3rem + 2 * 12px + 1px + env(safe-area-inset-bottom));
        scroll-padding-bottom: calc(var(--ui-fixed-action-bar-height) + 8px);
    }

    body:has(.ui-fixed-action-bar) {
        padding-bottom: var(--ui-fixed-action-bar-height);
    }

    .ui-fixed-action-bar {
        position: fixed;
        inset: auto 0 0;
        z-index: 20;
        padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
        border-top: 1px solid var(--sp-color-border);
        background: var(--sp-color-surface);
    }

    .ui-fixed-action-bar > .btn {
        width: 100%;
        min-height: 3rem;
        font-size: 1rem;
        font-weight: 700;
    }
}

/* Sticky phone action bar for a long create form or inline editor inside the workspace, like the site
   window's footer: on phones it stays at the bottom of the screen while its form is in view and settles
   after the last field. No ancestor between it and the page may be a scroll container (use overflow: clip
   to round corners). The owner sets the inline margins and padding that span its card. Phone toasts rise
   above the bar (ToastHost). */
@media (max-width: 700px), (pointer: coarse) and (max-height: 500px) {
    /* Reserving the bar's height keeps a field focused beneath it in view (focusClearOfFixedBars). */
    :root:has(.ui-sticky-action-bar) {
        scroll-padding-bottom: calc(3rem + 2 * var(--sp-space-3) + 1px + env(safe-area-inset-bottom));
    }

    /* :root outranks table-cell and card-cell surfaces when the bar is a table cell. */
    :root .ui-sticky-action-bar {
        position: sticky;
        bottom: 0;
        z-index: 5;
        padding-top: var(--sp-space-3);
        padding-bottom: calc(var(--sp-space-3) + env(safe-area-inset-bottom));
        border-top: 1px solid var(--sp-color-border);
        background: var(--sp-color-surface);
        box-shadow: 0 -4px 12px rgb(0 0 0 / 0.08);
    }
}
.ui-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 2500;
    display: grid;
    place-items: center;
    padding: var(--sp-space-4, 1rem);
    overflow-y: auto;
    background: rgb(var(--sp-rgb-ink) / 0.58);
    animation: var(--sp-motion-fade-in);
}

.ui-modal {
    max-width: 100%;
    outline: none;
    transform-origin: 50% 42%;
    animation: var(--sp-motion-dialog-in);
}

/* User-supplied tokens (long email addresses, URLs, IDs) wrap inside the
   dialog instead of running past its edge. break-word only breaks a token
   that would overflow, so tables and min-content sizing are unchanged. */
.ui-modal > .modal-body {
    overflow-wrap: break-word;
}

/* A modal dialog is a grid item inside the shared backdrop. Give that item an
   explicit available width so Bootstrap's auto width cannot shrink-wrap a
   dense editor on narrow screens. Its modal-sm/lg/xl max-width still wins on
   larger viewports. */
.ui-modal-backdrop > .modal-dialog {
    width: 100%;
    margin: 0;
}

/* Phones: large editors (modal-lg / modal-xl, the UiModal default) become
   full-screen sheets so the header and footer stay in view and the body is
   the only scroller. Compact dialogs (sm, md, confirmations) keep floating.
   Short touch screens such as a phone in landscape get the same sheet.
   Dynamic viewport units keep the footer above mobile browser toolbars. */
@media (max-width: 575.98px), (max-height: 500px) and (pointer: coarse) {
    .ui-modal-backdrop:has(> .modal-dialog:is(.modal-lg, .modal-xl)) {
        padding: 0;
        place-items: stretch;
    }

    .ui-modal-backdrop > .modal-dialog:is(.modal-lg, .modal-xl) {
        max-width: none;
        height: 100vh;
        height: 100dvh;
    }

    .ui-modal-backdrop > .modal-dialog:is(.modal-lg, .modal-xl) > .ui-modal {
        height: 100%;
        max-height: none;
        border: 0;
        border-radius: 0;
    }

    .ui-modal-backdrop > .modal-dialog:is(.modal-lg, .modal-xl) > .ui-modal > .modal-header {
        padding-top: max(0.75rem, env(safe-area-inset-top));
    }

    .ui-modal-backdrop > .modal-dialog > .ui-modal > .modal-footer {
        flex-wrap: wrap;
        gap: 0.5rem;
        padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
    }

    .ui-modal-backdrop > .modal-dialog > .ui-modal > .modal-footer > * {
        margin: 0;
    }
}

/* An explicit modal-fullscreen dialog fills the viewport at every width; the
   backdrop padding and the shared dialog width must not inset it. */
.ui-modal-backdrop:has(> .modal-dialog.modal-fullscreen) {
    padding: 0;
    place-items: stretch;
}

.ui-modal-backdrop > .modal-dialog.modal-fullscreen {
    width: 100%;
    max-width: none;
    height: 100vh;
    height: 100dvh;
}

.ui-modal-backdrop > .modal-dialog.modal-fullscreen > .ui-modal {
    height: 100%;
    max-height: none;
    border: 0;
    border-radius: 0;
}

/* A header close button never yields width to long header copy beside it.
   Sheets that draw their own header (global search, the salt map) share it. */
.ui-modal-backdrop :is(.modal-header, .global-search-heading, .salt-map-dialog > header) .btn-close {
    flex: 0 0 auto;
}

/* Touch screens and narrow windows get the shared 44px close target. */
@media (pointer: coarse), (max-width: 700px) {
    .ui-modal-backdrop :is(.modal-header, .global-search-heading, .salt-map-dialog > header) .btn-close {
        box-sizing: content-box;
        width: 1rem;
        height: 1rem;
        padding: 0.875rem;
        margin: -0.75rem -0.75rem -0.75rem auto;
    }
}

.ui-modal-backdrop.ui-modal-closing {
    pointer-events: none;
    animation: var(--sp-motion-fade-out);
}

.ui-modal-closing .ui-modal {
    animation: var(--sp-motion-dialog-out);
}

/* Keep older hand-authored dialogs on the same visual language while they
   are migrated to UiModal. Visible Bootstrap modals only are targeted. */
.modal.d-block:not(.ui-modal-backdrop) {
    animation: var(--sp-motion-fade-in);
}

.modal.d-block:not(.ui-modal-backdrop) > .modal-dialog > .modal-content,
.modal.d-block:not(.ui-modal-backdrop) > .modal-dialog > section {
    transform-origin: 50% 42%;
    animation: var(--sp-motion-dialog-in);
}

body.ui-modal-open {
    overflow: hidden;
}

/* Fallback shown by AppErrorBoundary when a page's render or event handler
   throws. Deliberately plain: it renders while something in the page is
   already broken, so it depends on nothing but the alert primitive. */
.app-error-boundary {
    padding: var(--sp-space-4) 0;
}

.app-error-boundary-reference code {
    overflow-wrap: anywhere;
    font-weight: 800;
}

.brand-logo {
    aspect-ratio: 268 / 90;
    object-fit: contain;
    max-width: 100%;
    border: 1px solid var(--sp-color-border);
    border-radius: var(--sp-radius-sm);
}

html:not([data-theme="dark"]) .brand-logo.brand-logo-dark {
    display: none;
}

html[data-theme="dark"] .brand-logo.brand-logo-light {
    display: none;
}

html[data-theme="dark"] .brand-logo.brand-logo-dark {
    display: block;
}

/* FocusOnNavigate moves focus programmatically to the heading. */
h1:focus {
    outline: none;
    box-shadow: none;
}

/* One control silhouette across admin, public, and feature-specific buttons.
   Important keeps legacy and isolated radius rules from fragmenting this token. */
:root :is(button, .btn, [role="button"], input[type="button"], input[type="submit"], input[type="reset"]) {
    border-radius: var(--sp-control-radius) !important;
}

/* Shared compact actions win over historical feature widths and Bootstrap utilities.
   A toolbar that pairs one with taller text controls sets --ui-icon-button-size on the
   button instead of fighting this rule's importance. */
:root .ui-button.ui-button-icon-only {
    position: relative;
    display: inline-flex;
    flex: 0 0 auto !important;
    align-items: center;
    justify-content: center;
    width: var(--ui-icon-button-size, 2rem) !important;
    min-width: var(--ui-icon-button-size, 2rem) !important;
    max-width: var(--ui-icon-button-size, 2rem) !important;
    height: var(--ui-icon-button-size, 2rem) !important;
    min-height: var(--ui-icon-button-size, 2rem) !important;
    padding: 0 !important;
    margin: 0;
    gap: 0 !important;
    border: 1px solid transparent;
    background: transparent;
    color: var(--bs-secondary-color);
    box-shadow: none;
    line-height: 1;
    white-space: nowrap;
    vertical-align: middle;
}

:root .ui-button.ui-button-icon-only:is(:hover, :focus-visible):not(:disabled):not([aria-disabled="true"]) {
    border-color: var(--bs-border-color);
    background: var(--bs-tertiary-bg);
    color: var(--bs-emphasis-color);
}

:root
    .ui-button.ui-button-icon-only:is(.btn-danger, .btn-outline-danger):is(:hover, :focus-visible):not(:disabled):not(
        [aria-disabled="true"]
    ) {
    border-color: var(--bs-danger-border-subtle);
    background: var(--bs-danger-bg-subtle);
    color: var(--bs-danger-text-emphasis);
}

:root .ui-button.ui-button-icon-only:disabled {
    opacity: 0.4;
}

/* Captioned compact action: same quiet row-action look, but the icon carries a
   short visible word because the glyph alone does not say what it does. */
:root .ui-button.ui-button-icon-only.ui-button-captioned {
    width: auto !important;
    max-width: none !important;
    padding: 0 0.45rem !important;
    gap: 0.3rem !important;
    font-size: 0.8125rem;
    font-weight: 500;
}

.ui-button-caption {
    line-height: 1;
}

/* Phone-captioned compact action: icon-only on wider screens; on phones, where it sits in a pinned
   action bar, it shows its caption as a full-size labelled button that shares the bar's width. */
.ui-button-phone-captioned > .ui-button-caption {
    display: none;
}

@media (max-width: 700px), (pointer: coarse) and (max-height: 500px) {
    .ui-button-phone-captioned > .ui-button-caption {
        display: inline;
    }

    :root .ui-button.ui-button-icon-only.ui-button-phone-captioned {
        flex: 1 1 0 !important;
        width: auto !important;
        max-width: none !important;
        padding: 0 0.875rem !important;
        gap: 0.375rem !important;
        border-color: var(--bs-border-color);
        color: var(--bs-emphasis-color);
        font-size: 1rem;
        font-weight: 600;
    }
}

:root .directory-row-actions,
:root .ui-row-actions {
    display: inline-flex !important;
    flex-direction: row !important;
    flex-wrap: nowrap !important;
    align-items: center;
    justify-content: flex-start;
    gap: 0.25rem !important;
    width: max-content;
    min-width: 0;
    max-width: none;
    vertical-align: middle;
}

:root :is(.directory-row-actions, .ui-row-actions) > .btn {
    width: auto;
    /* Text actions (such as a final Confirm Delete) share the icon actions'
       32px fine-pointer target instead of the narrow-screen field height. */
    min-height: 2rem;
    white-space: nowrap;
}

/* Same narrow-screen breakpoint as text actions (UiButton.razor.css), so one action row keeps one 44px height. */
@media (max-width: 767.98px), (pointer: coarse) {
    :root .ui-button.ui-button-icon-only {
        width: 2.75rem !important;
        min-width: 2.75rem !important;
        max-width: 2.75rem !important;
        height: 2.75rem !important;
        min-height: 2.75rem !important;
    }

    :root .ui-button.ui-button-icon-only.ui-button-captioned {
        width: auto !important;
        max-width: none !important;
    }

    :root :is(.directory-row-actions, .ui-row-actions) > .btn {
        min-height: 2.75rem;
    }

    /* A text link beside the row buttons (a Source link) shares their 44px target. */
    :root :is(.directory-row-actions, .ui-row-actions) > a {
        display: inline-flex;
        align-items: center;
        min-height: 2.75rem;
        padding-inline: 0.5rem;
    }
}

.ui-button[aria-expanded="true"] .ui-action-icon[data-icon="expand"] {
    transform: rotate(180deg);
}

.ui-button-hint {
    position: fixed;
    z-index: 1090;
    max-width: min(18rem, calc(100vw - 1rem));
    padding: 0.375rem 0.5rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--sp-control-radius);
    background: var(--bs-body-bg);
    color: var(--bs-body-color);
    box-shadow: 0 2px 8px rgb(0 0 0 / 12%);
    font-size: 0.75rem;
    line-height: 1.4;
    overflow-wrap: anywhere;
}

@media (max-width: 767.98px) {
    .ui-table-cards > tbody > tr > td[data-label="Actions"] {
        grid-template-columns: minmax(0, 1fr);
    }

    :root .ui-table-cards :is(.directory-row-actions, .ui-row-actions) {
        max-width: 100%;
        overflow-x: auto;
        padding: 0.2rem;
    }
}

/* Compact search affordance; the label remains available to assistive technology. */
.ui-search-field {
    position: relative;
}
.ui-search-field > .form-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
.ui-search-field > .ui-search-field-icon {
    position: absolute;
    inset-inline-start: 0.7rem;
    top: 0.55rem;
    display: flex;
    color: var(--bs-secondary-color);
    pointer-events: none;
}
.ui-field.ui-search-field > input.form-control[type="search"] {
    padding-inline-start: 2.25rem !important;
}
.ui-search-field:focus-within > .ui-search-field-icon {
    color: var(--bs-primary);
}
/* Password fields: the Show password toggle sits inside the input's end edge,
   which reserves room for it so typed characters never run underneath. */
.ui-password-input {
    --ui-password-toggle-size: var(--sp-control-height);
    position: relative;
}
.ui-password-input > input.form-control {
    padding-inline-end: calc(var(--ui-password-toggle-size) + 0.25rem);
}
/* Edge draws its own reveal eye (and a clear button) inside password inputs. A UiTextField password
   input always has one labelled reveal control, its own toggle or the caller's (the W-9 tax ID
   fields), so the native one would be a second eye. One rule per pseudo-element: a browser that
   does not know one of them drops the whole rule it appears in. */
.ui-field input.form-control::-ms-reveal {
    display: none;
}
.ui-field input.form-control::-ms-clear {
    display: none;
}
.ui-password-toggle {
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--ui-password-toggle-size);
    padding: 0;
    border: 0;
    border-radius: 0 var(--sp-radius-sm) var(--sp-radius-sm) 0;
    background: transparent;
    color: var(--sp-color-ink-muted);
    cursor: pointer;
}
.ui-password-toggle:hover:not(:disabled),
.ui-password-toggle[aria-pressed="true"] {
    color: var(--sp-color-ink-strong);
}
.ui-password-toggle:focus-visible {
    outline: 2px solid var(--sp-color-brand);
    outline-offset: -2px;
}
.ui-password-toggle:disabled {
    cursor: default;
    opacity: 0.5;
}
@media (pointer: coarse), (max-width: 700px) {
    .ui-password-input {
        --ui-password-toggle-size: var(--sp-control-height-touch);
    }
}
/* Typed electronic signature (signup and secure-document W-9): the typed name is drawn in a
   larger script face; the placeholder keeps the UI font and size. The element-qualified selector
   outranks the public field size (`.public-layout .ui-field > .form-control`) and page field rules;
   touch phones still get the 16px anti-zoom size below. */
.ui-field > input.form-control.ui-signature-input {
    font-family: var(--sp-font-signature);
    font-size: var(--sp-font-size-signature);
}
.ui-field > input.form-control.ui-signature-input::placeholder {
    font-family: var(--sp-font-sans);
    font-size: 16px;
}
/* Currency fields (Currency="true"): a leading $ sits inside the input's start
   edge, in the value's font and baseline, which reserves room for it so typed
   digits never run underneath. The inset follows the control's own padding. */
.ui-currency-input {
    /* The admin density sheet pads every input 9px, compact ones included. */
    --ui-currency-inset: 9px;
    position: relative;
    display: grid;
    min-width: 0;
}
.public-layout .ui-currency-input {
    --ui-currency-inset: 12px;
}
/* Important like the search field's icon inset: the admin density input rule outranks any class selector. */
.ui-currency-input > input.form-control {
    min-width: 0;
    padding-inline-start: calc(var(--ui-currency-inset) + 1.5ch) !important;
}
.ui-currency-symbol {
    position: absolute;
    inset-block: 0;
    inset-inline-start: 1px;
    display: flex;
    align-items: center;
    padding-inline-start: var(--ui-currency-inset);
    color: var(--sp-color-ink-muted);
    /* The admin density sheet sizes every input with this token; elsewhere fields use 13px. */
    font-size: var(--compact-control-font-size, 13px);
    line-height: 1.25;
    pointer-events: none;
}
.public-layout .ui-currency-symbol {
    font-size: 16px;
}
@media (pointer: coarse), (max-width: 700px) {
    .ui-currency-symbol {
        font-size: 16px;
    }
}
/* Swatch color inputs keep a fixed swatch width; inside a shrink-to-fit flex row
   the field wrapper otherwise collapses the input until its padding hides the color. */
.ui-field > input.form-control.form-control-color {
    width: 5rem;
    min-width: 5rem;
}
/* Touch: the swatch matches the 44px text fields beside it. */
@media (pointer: coarse), (max-width: 700px) {
    .ui-field > input.form-control.form-control-color {
        height: var(--sp-control-height-touch);
    }
}
.ui-confirm-backdrop {
    z-index: 2900;
    backdrop-filter: blur(3px);
}
.ui-confirm-size {
    max-width: 27rem;
}
.ui-confirm-dialog {
    border: 1px solid var(--bs-border-color-translucent);
    border-radius: 1rem;
    box-shadow: 0 24px 80px rgb(0 0 0 / 0.25);
    background: var(--bs-body-bg);
    overflow: hidden;
}
.ui-confirm-dialog .ui-confirm-header {
    gap: 0.75rem;
    padding: 1.25rem 1.25rem 0.75rem;
    border: 0;
}
.ui-confirm-dialog .modal-title {
    flex: 1;
    font-weight: 650;
    line-height: 1.35;
}
.ui-confirm-dialog .btn-close {
    flex-shrink: 0;
    margin: 0;
}
.ui-confirm-symbol {
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    flex-shrink: 0;
    border-radius: 0.8rem;
    color: var(--bs-danger-text-emphasis);
    background: var(--bs-danger-bg-subtle);
    animation: var(--sp-motion-scale-in);
}
.ui-confirm-dialog--primary .ui-confirm-symbol {
    color: var(--bs-primary-text-emphasis);
    background: var(--bs-primary-bg-subtle);
}
.ui-confirm-dialog .ui-confirm-body {
    padding: 0.25rem 1.25rem 1.25rem;
}
.ui-confirm-message {
    color: var(--bs-secondary-color);
    line-height: 1.6;
    overflow-wrap: anywhere;
}
/* Only the plain-text Message keeps its line breaks. Inherited pre-line turned the whitespace
   between component markup in ChildContent into blank line boxes, which stretched form
   controls such as the date picker and left an empty band above the footer. */
.ui-confirm-message-text {
    white-space: pre-line;
}
.ui-confirm-dialog .ui-confirm-actions {
    display: flex;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 0.5rem;
    padding: 0.85rem 1.25rem;
    background: var(--bs-tertiary-bg);
    border-top: 1px solid var(--bs-border-color-translucent);
}
.ui-confirm-actions > .btn {
    margin: 0;
    min-height: 2.5rem;
}

@media (prefers-reduced-motion: reduce) {
    .ui-confirm-backdrop {
        backdrop-filter: none;
    }
}
@media (max-width: 480px) {
    .ui-confirm-actions > .btn {
        flex: 1;
        min-height: 2.75rem;
    }
}

/* Native map/rail searches and autocomplete pickers share the same quiet glyph.
   Date and time pickers are dialog comboboxes with their own calendar icon. */
input[type="search"]:not(.ui-search-field > input):not(.operations-search > input),
input[role="combobox"]:not([aria-haspopup="dialog"]) {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23748193' stroke-width='1.75' stroke-linecap='round'%3E%3Ccircle cx='10' cy='10' r='7'/%3E%3Cpath d='m15 15 6 6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 0.7rem center;
    padding-inline-start: 2.25rem;
}
/* The global search sits beside the header action rail, so it uses the rail's
   box (WorkspaceHeader.razor.css) instead of the shared 2rem icon-only size. */
.workspace-header.workspace-header-minimal .workspace-global-search.btn {
    --workspace-search-size: 38px;
    flex: 0 0 var(--workspace-search-size) !important;
    width: var(--workspace-search-size) !important;
    min-width: var(--workspace-search-size) !important;
    max-width: var(--workspace-search-size) !important;
    height: var(--workspace-search-size) !important;
    min-height: var(--workspace-search-size) !important;
    padding: 0;
}
/* The phone shell condition from MainLayout.razor.css: narrow windows and short touch screens. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
    .workspace-header.workspace-header-minimal .workspace-global-search.btn {
        --workspace-search-size: 36px;
    }
}
@media (max-width: 700px) and (pointer: coarse), (max-height: 500px) and (pointer: coarse) {
    .workspace-header.workspace-header-minimal .workspace-global-search.btn {
        --workspace-search-size: 2.75rem;
    }
}

/* iOS Safari zooms the page into any focused field whose text is smaller than
   16px and does not zoom back out. The compact legacy field sizes (14px) sit
   below that, so touch phones, in portrait and landscape, get 16px field text.
   !important is needed to outrank the legacy attribute-heavy input selectors. */
@media (max-width: 767.98px) and (pointer: coarse), (max-height: 500px) and (pointer: coarse) {
    input:not(
        [type="checkbox"],
        [type="radio"],
        [type="range"],
        [type="color"],
        [type="file"],
        [type="button"],
        [type="submit"],
        [type="reset"],
        [type="image"]
    ),
    select,
    textarea {
        font-size: max(1rem, 16px) !important;
    }
}

/* Shared Leaflet controls are used by both the admin maps and public portal. */
.leaflet-control-container .leaflet-top.leaflet-left {
    top: 10px;
    left: 10px;
}

.leaflet-control-zoom {
    overflow: hidden;
    border: 1px solid var(--sp-color-border-strong);
    border-radius: 0.65rem;
    background: var(--sp-color-surface-raised);
    box-shadow: 0 0.35rem 0.9rem rgb(var(--sp-rgb-black) / 0.18);
}

.leaflet-control-zoom a {
    display: block;
    box-sizing: border-box;
    width: 2.25rem;
    height: 2.25rem;
    line-height: 2.15rem;
    text-align: center;
    text-decoration: none;
    color: var(--sp-color-ink);
    background: var(--sp-color-surface-raised);
    border: 0;
    border-bottom: 1px solid var(--sp-color-border);
    font-family: Arial, Helvetica, sans-serif;
    font-size: 1.4rem;
    font-weight: 500;
    transition: var(--sp-motion-transition-control);
}

.leaflet-control-zoom a:last-child {
    border-bottom: none;
}

.leaflet-control-zoom a:hover,
.leaflet-control-zoom a:focus-visible {
    color: var(--sp-color-brand-strong);
    background: var(--sp-color-brand-soft);
}

.leaflet-control-zoom a:focus-visible {
    position: relative;
    z-index: 1;
    outline: 2px solid var(--sp-color-brand);
    outline-offset: -2px;
}

.leaflet-control-zoom a.leaflet-disabled {
    color: var(--sp-color-ink-faint);
    background: var(--sp-color-surface);
    cursor: not-allowed;
}

.snow-map-bearing-control {
    display: flex;
    align-items: center;
    overflow: hidden;
    border: 1px solid var(--sp-color-border-strong);
    border-radius: 0.65rem;
    background: var(--sp-color-surface-raised);
    color: var(--sp-color-ink);
    box-shadow: 0 0.35rem 0.9rem rgb(var(--sp-rgb-black) / 0.18);
}

.snow-map-bearing-button {
    display: grid;
    width: 2.25rem;
    height: 2.25rem;
    place-items: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font:
        700 1.2rem/1 Arial,
        sans-serif;
    cursor: pointer;
    transition: var(--sp-motion-transition-control);
}

.snow-map-bearing-button:hover,
.snow-map-bearing-button:focus-visible {
    background: var(--sp-color-brand-soft);
    color: var(--sp-color-brand-strong);
}

.snow-map-bearing-button:focus-visible {
    outline: 2px solid var(--sp-color-brand);
    outline-offset: -2px;
}

.snow-map-bearing-button > .snow-map-tool-icon {
    width: 1.1rem;
    height: 1.1rem;
}

.snow-map-bearing-compass {
    font-size: 0.875rem;
}

.snow-map-bearing-compass.is-rotated {
    color: var(--sp-color-brand-strong);
}

.snow-map-bearing-degrees {
    min-width: 2.5rem;
    text-align: center;
    font:
        700 0.72rem/1 Arial,
        sans-serif;
    font-variant-numeric: tabular-nums;
}

@media (pointer: coarse) {
    .snow-map-bearing-button {
        width: 2.75rem;
        height: 2.75rem;
    }
}

/* Full screen (top left) and the rotation control (top right) need about 21rem side by side. In a
   narrower map, such as a phone-width Home or Snow Depth map, Full screen moves below the rotation row
   so neither covers the other. A feature map may keep its own narrow layout in its scoped styles. */
.leaflet-container:has(.snow-map-bearing-control) {
    container-type: inline-size;
}

@container (max-width: 24rem) {
    .leaflet-container .snow-map-fullscreen-control {
        margin-top: 3.75rem;
    }
}

/* Shared map commands, including fullscreen on every map, use one appearance
   in both themes. Captions are added by the shared tool renderer. */
.snow-map-tool-group {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid var(--sp-color-border-strong);
    border-radius: 0.65rem;
    background: var(--sp-color-surface-raised);
    color: var(--sp-color-ink);
    box-shadow: 0 0.35rem 0.9rem rgb(var(--sp-rgb-black) / 0.18);
}

.snow-map-tool-button {
    position: relative;
    display: grid;
    width: 2.25rem;
    height: 2.25rem;
    flex: 0 0 auto;
    place-items: center;
    margin: 0;
    padding: 0;
    border: 0;
    border-bottom: 1px solid var(--sp-color-border);
    border-radius: 0;
    background: transparent;
    color: inherit;
    cursor: pointer;
    transition: var(--sp-motion-transition-control);
}

.snow-map-tool-button:last-child {
    border-bottom: 0;
}

.snow-map-tool-button[hidden] {
    display: none;
}

.snow-map-tool-button:hover,
.snow-map-tool-button:focus-visible {
    background: var(--sp-color-brand-soft);
    color: var(--sp-color-brand-strong);
}

.snow-map-tool-button:focus-visible {
    z-index: 1;
    outline: 2px solid var(--sp-color-brand);
    outline-offset: -2px;
}

.snow-map-tool-button[aria-pressed="true"],
.snow-map-tool-button.is-active {
    background: var(--sp-color-brand-soft);
    color: var(--sp-color-brand-strong);
    box-shadow: inset 3px 0 0 var(--sp-color-brand);
}

.snow-map-tool-button:disabled {
    color: var(--sp-color-ink-faint);
    cursor: not-allowed;
}

.snow-map-tool-icon,
.snow-map-tool-button > .ui-action-icon {
    width: 1.15rem;
    height: 1.15rem;
}

@media (pointer: coarse) {
    .snow-map-tool-button {
        width: 2.75rem;
        height: 2.75rem;
    }
}

/* Map view tools carry a short visible caption to the right of the icon, so a
   tool says what it does without a hover. Zoom +/- stays a bare Leaflet glyph. */
.snow-map-tool-caption {
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
}

/* Full screen (top left) and the rotation compass (top right) need about 21rem
   side by side. In a narrower map, such as the phone ticket map, Full screen
   keeps its icon, accessible name and tooltip but drops the visible caption,
   so neither control covers the other. The Site overhead view keeps its own
   phone layout, which moves Full screen below the compass row instead. */
/* Touch screens move maps with two fingers; a one-finger swipe shows this hint
   over the map while the page scrolls (snowMapCore.applyTouchGesturePolicy). */
.snow-map-touch-hint {
    position: absolute;
    inset: 0;
    z-index: 1000;
    display: grid;
    place-items: center;
    padding: var(--sp-space-4);
    background: rgb(var(--sp-rgb-ink) / 0.72);
    color: var(--sp-color-white);
    font-size: 1rem;
    font-weight: 700;
    text-align: center;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--sp-motion-duration-base) var(--sp-motion-ease-standard);
}
.snow-map-touch-hint.is-visible {
    opacity: 1;
}

.leaflet-container:has(.snow-map-fullscreen-control) {
    container-type: inline-size;
}

@container (max-width: 24rem) {
    .snow-map-fullscreen-control .snow-map-tool-caption {
        display: none;
    }

    /* Without its caption the button is just the centred icon. */
    :root .snow-map-fullscreen-control .snow-map-tool-button.has-caption {
        justify-content: center;
        padding-inline: 0;
    }
}

:root .snow-map-tool-button.has-caption {
    display: flex;
    width: 100%;
    min-width: 2.25rem;
    align-items: center;
    justify-content: flex-start;
    gap: 0.4rem;
    padding: 0 0.6rem 0 0.55rem;
}

:root .snow-map-bearing-button:has(.snow-map-tool-caption),
:root .snow-map-bearing-compass {
    display: inline-flex;
    width: auto;
    align-items: center;
    gap: 0.25rem;
    padding: 0 0.5rem;
}

/* A tool whose caption is hidden in a narrow map keeps the 44px touch target. */
@media (pointer: coarse) {
    :root .snow-map-tool-button.has-caption {
        min-width: 2.75rem;
    }
}

/* On phones the 15-degree steps and their readout give way to two-finger
   rotation; the compass keeps the north reset. Every map shares this rule. */
@media (max-width: 700px) {
    :root .snow-map-bearing-button:not(.snow-map-bearing-compass),
    .snow-map-bearing-degrees {
        display: none;
    }
}

/* The same applies to a narrow map on a wide screen, such as the 200px ticket detail preview at
   1024px: the full rotation row (about 207px) would be wider than the map and clipped by it. */
@container (max-width: 16rem) {
    :root .snow-map-bearing-button:not(.snow-map-bearing-compass),
    .snow-map-bearing-degrees {
        display: none;
    }
}

/* A map workspace in fullscreen stays in the document and is pinned over the
   shell: above the navigation drawer (2100) and its toggles (2200), below
   dialogs (2500), confirmations (2900) and toasts, so those stay usable. */
.is-map-workspace-fullscreen {
    position: fixed !important;
    inset: 0 !important;
    z-index: 2300 !important;
    width: auto !important;
    max-width: none !important;
    height: 100vh !important;
    height: 100dvh !important;
    max-height: none !important;
    margin: 0 !important;
    box-sizing: border-box;
    overflow: hidden;
    background: var(--sp-color-surface-muted);
    padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}

html.has-map-workspace-fullscreen,
html.has-map-workspace-fullscreen body {
    overflow: hidden;
    overscroll-behavior: none;
}

/* A phone page with a fixed bottom action bar (marked data-fixed-action-bar, such as PhoneRecipientPicker's 55px
   bar plus its safe-area padding) ends its workspace above the bar, so the last content can scroll into view. The
   bar's height is also root scroll padding, so a control scrolled or focused into view (focusClearOfFixedBars)
   stops above the bar instead of under it. */
@media (max-width: 700px), (max-height: 500px) and (pointer: coarse) {
    .app-content.workspace-body:has([data-fixed-action-bar]) {
        padding-bottom: calc(18px + 55px + max(10px, env(safe-area-inset-bottom)));
    }

    :root:has(.app-content.workspace-body [data-fixed-action-bar]) {
        scroll-padding-bottom: calc(55px + max(10px, env(safe-area-inset-bottom)) + 8px);
    }
}

/* Leaflet's own zoom and layer controls are 30px on touch devices. Every map
   shares them, so the 44px touch target lives here beside the other shared
   touch rules. The container class outranks Leaflet's lazily loaded sheet. */
@media (pointer: coarse) {
    .leaflet-container.leaflet-touch .leaflet-bar a {
        width: 2.75rem;
        height: 2.75rem;
        line-height: 2.75rem;
    }

    .leaflet-container.leaflet-touch .leaflet-control-layers-toggle {
        width: 2.75rem;
        height: 2.75rem;
    }

    /* An embedded map without one-finger dragging (snowMapCore.embeddedMapTouchOptions) leaves one-finger pans to
       page scrolling and two-finger gestures to the map. Leaflet would set this through leaflet-touch-zoom, but
       leaflet-rotate replaces touchZoom with its own two-finger handler. */
    .leaflet-container.leaflet-touch:not(.leaflet-touch-drag) {
        touch-action: pan-x pan-y;
    }
}

/* Bootstrap check and radio controls are 1em. On narrow screens and touch
   pointers the control grows to 1.25em (a switch to a 2.5rem track) and the
   label becomes the 44px tap target. The control is centred on a one-line
   label instead of hanging from Bootstrap's float, so it stays level with
   its text; a long-label row keeps the control at its first line. Feature
   rows laid out with d-flex keep their own geometry. */
@media (max-width: 767.98px), (pointer: coarse) {
    .form-check:not(.form-switch) > .form-check-input {
        width: 1.25em;
        height: 1.25em;
    }

    .form-check.form-switch > .form-check-input {
        width: 2.5rem;
        height: 1.375rem;
    }

    .form-check:not(.d-flex, .ui-check-row) {
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding-left: 0;
    }

    .form-check.form-check-inline:not(.d-flex, .ui-check-row) {
        display: inline-flex;
    }

    .form-check:not(.d-flex, .ui-check-row) > .form-check-input {
        float: none;
        flex: 0 0 auto;
        margin: 0;
    }

    .form-check:not(.d-flex) > .form-check-label {
        display: flex;
        align-items: center;
        min-height: var(--sp-control-height-touch);
    }

    .form-check.ui-check-row > .form-check-input {
        margin-top: calc((var(--sp-control-height-touch) - 1.25em) / 2);
    }

    .form-check.form-switch.ui-check-row > .form-check-input {
        margin-top: calc((var(--sp-control-height-touch) - 1.375rem) / 2);
    }

    /* UiRadioOption renders its own (flex) label around the radio; that label
       is the 44px row. */
    label.ui-radio-option {
        min-height: var(--sp-control-height-touch);
    }

    /* A check without a visible label (UiCheckField renders its container as the
       label) centres the control in a 44px box that toggles it. */
    .form-check.ui-check-bare:not(.form-switch) {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 2.75rem;
        min-height: 2.75rem;
        padding-left: 0;
    }

    .form-check.ui-check-bare:not(.form-switch) > .form-check-input {
        float: none;
        margin: 0;
    }
}

/* UiCheckField with a long or structured label: the control sits beside the
   wrapped label instead of floating into Bootstrap's hanging indent. */
.form-check.ui-check-row {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    padding-left: 0;
}

.form-check.ui-check-row > .form-check-input {
    float: none;
    flex: 0 0 auto;
    margin: 0.2em 0 0;
}

/* Thin progress track. Indeterminate progress animates one segment across the
   track with transform only; determinate callers size the bar themselves. */
.ui-progress {
    position: relative;
    display: block;
    width: 100%;
    height: 3px;
    overflow: hidden;
    background: color-mix(in srgb, var(--sp-color-brand) 16%, transparent);
}

.ui-progress-bar {
    display: block;
    width: 40%;
    height: 100%;
    border-radius: var(--sp-radius-pill);
    background: var(--sp-color-brand);
}

/* Navigation feedback is controlled locally, before a server round trip: a
   progress bar pins to the top of the viewport and the page being left dims and
   stops taking input. Both wait for the loading delay, so a fast navigation
   swaps content without a flash; the page keeps its layout throughout. */
.route-navigation-loading {
    position: fixed;
    inset: 0 0 auto;
    z-index: 2400;
    display: none;
    pointer-events: none;
}

html[data-route-pending] .route-navigation-loading {
    display: block;
}

html[data-route-pending] .workspace-body {
    pointer-events: none;
    opacity: var(--sp-opacity-refreshing);
    transition: var(--sp-motion-transition-pending);
}

/* Directory filters share the reference rhythm; feature tables own columns and
   row styling. The scoped toolbar's touch token remains the source of sizing. */
:root .ui-toolbar-directory {
    --toolbar-control-height: 34px;
    padding: 10px 12px;
    gap: 8px;
}
:root .ui-toolbar-directory .ui-toolbar-left {
    flex-basis: 20rem;
    gap: 8px;
}
:root .ui-toolbar-directory .ui-field {
    flex: 0 1 auto;
    min-width: 0;
}
:root .ui-toolbar-directory .ui-toolbar-search {
    flex: 1 1 220px;
    max-width: 380px;
}
:root .ui-toolbar-directory .form-label {
    font-size: 11px;
}
@media (pointer: coarse) {
    :root .ui-toolbar-directory {
        --toolbar-control-height: 44px;
    }
}

/* Page-level background progress (UiProgressNotice): a compact pill above the
   bottom edge, so streaming batches never push the page around. It waits for
   the loading delay, then fades in; fast work shows nothing. */
.ui-progress-notice-body {
    position: fixed;
    inset-block-end: max(var(--sp-space-4), env(safe-area-inset-bottom));
    inset-inline-start: 50%;
    z-index: 1080;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-space-2);
    max-width: min(32rem, calc(100vw - 2rem));
    padding: var(--sp-space-2) var(--sp-space-4);
    border: 1px solid var(--sp-color-border);
    border-radius: var(--sp-radius-pill);
    background: var(--sp-color-surface);
    box-shadow: var(--sp-shadow-md);
    color: var(--sp-color-ink);
    font-size: 0.8125rem;
    translate: -50% 0;
    animation: var(--sp-motion-loading-in);
    pointer-events: none;
}

.ui-progress-notice.is-inline {
    display: inline-flex;
    min-width: 0;
}

.ui-progress-notice.is-inline .ui-progress-notice-body {
    position: static;
    z-index: auto;
    max-width: 100%;
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
    color: var(--sp-color-ink-muted);
    translate: none;
}

.ui-progress-notice-body > .spinner-border {
    flex: none;
    color: var(--sp-color-brand);
}
