/* =============================================================================
   IntegratorOS - Global Styles
   Works alongside MudBlazor's built-in styling
   ============================================================================= */

/* Reset & Base
   Font stack matches the token package (vahary.css loads Work Sans); the previous
   'Inter', 'Roboto' stack named two faces this app never loads, so every element outside a
   MudBlazor component fell through to Helvetica/Arial and sat next to Work Sans on the same
   screen. */
html, body {
    margin: 0;
    padding: 0;
    font-family: var(--v-font-body, 'Work Sans'), system-ui, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
}

/* Theme-switch transition. Scoped to the elements that actually change colour — the previous
   universal selector animated every cell of every table on each render, which is what made
   large grids feel sluggish. */
body,
.mud-paper, .mud-card, .mud-appbar, .mud-drawer, .mud-table, .mud-input-control,
.mud-button-root, .mud-chip, .mud-alert, .mud-nav-link {
    transition: background-color 0.3s ease, color 0.15s ease, border-color 0.15s ease;
}

@media (prefers-reduced-motion: reduce) {
    body,
    .mud-paper, .mud-card, .mud-appbar, .mud-drawer, .mud-table, .mud-input-control,
    .mud-button-root, .mud-chip, .mud-alert, .mud-nav-link {
        transition: none;
    }
}

/* ── Utility classes the markup relies on that MudBlazor does not ship ─────────────────────
   Bootstrap used to supply these, but its stylesheet is not linked (and has now been removed),
   so the markup's utility classes silently did nothing. Defining the handful that are actually
   used is far safer than re-adding 160 KB of Bootstrap or editing every call site.

   CORRECTED 2026-08-06 — the original note here claimed "426 uses of fw-bold/text-center/
   text-right did nothing … bold text was not bold". Measured, that was wrong by 61%:

     fw-bold      262 uses  — NEVER broken. `.fw-bold { font-weight: 600 !important; }` was
                              already defined further down this file (it predates the wave that
                              added this block). Bold text was always bold.
     text-center  137 uses  — genuinely dead, genuinely fixed here.
     text-right    16 uses  — genuinely dead, genuinely fixed here.
     text-truncate  3 / w-100 7 / h-100 2 — genuinely dead, genuinely fixed here.
     text-left / align-items-center / justify-content-between — 0 uses; rules kept only because
                              removing them is churn. MudBlazor 9.7 ships none of them (it ships
                              `.d-flex` only), so nothing is being shadowed.

   Real repair count: 165 sites, not 426.

   `.fw-bold` is therefore deliberately NOT declared here: the declaration that used to sit on
   this line was dead code that lost to the `!important` copy below and made the file disagree
   with itself ~250 lines apart. */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.w-100 { width: 100%; }
.h-100 { height: 100%; }
.align-items-center { align-items: center; }
.justify-content-between { justify-content: space-between; }

/* Feature card hover effect */
.feature-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
}

/* Stat card hover effect — clickable dashboard cards */
.stat-card:hover {
    transform: translateY(-3px) scale(1.02);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12) !important;
}

/* `.inventory-card` lived here: a bespoke hover treatment (own radius, own transition curve, a
   gradient ::after overlay) used by exactly one screen — the eight KPI cards on /inventory. Those
   are StatCards now, which carry the shared hover, focus-visible ring and reduced-motion handling,
   so the whole rule set was unreferenced. A one-screen hover treatment is the thing the component
   layer exists to stop, so it is gone rather than parked.
   `.stat-card` and `.feature-card` above are also currently unreferenced but are generically named
   and not this screen's to retire — left alone deliberately. */

/* Scrollbar styling */
::-webkit-scrollbar {
    width: 8px;
    height: 8px;
}

::-webkit-scrollbar-track {
    background: transparent;
}

::-webkit-scrollbar-thumb {
    background-color: rgba(128, 128, 128, 0.4);
    border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
    background-color: rgba(128, 128, 128, 0.6);
}

/* Loading indicator */
.loading-progress {
    position: relative;
    display: block;
    width: 8rem;
    height: 8rem;
    margin: 20vh auto 1rem auto;
}

.loading-progress circle {
    fill: none;
    stroke: var(--v-color-interactive-accent, #5B21B6);
    stroke-width: 0.6rem;
    transform-origin: 50% 50%;
    transform: rotate(-90deg);
}

.loading-progress circle {
    stroke: var(--v-color-interactive-accent, #5B21B6);
}

.loading-progress circle:last-child {
    stroke: var(--v-color-border-hairline, #C6CCCE);
    stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.42), 500%;
    transition: stroke-dasharray 0.05s ease-in-out;
}

.loading-progress-text {
    position: absolute;
    text-align: center;
    padding: 8px;
    inset: 0;
    line-height: 8rem;
    font-size: 1.2rem;
    font-weight: 500;
    color: var(--v-color-interactive-accent, #5B21B6);
}

.loading-progress-text:after {
    content: var(--blazor-load-percentage-text, "Loading");
}

/* Blazor error boundary */
#blazor-error-ui {
    background: var(--mud-palette-error, #DC2626);
    color: white;
    bottom: 0;
    box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.8rem 1.2rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
    border-radius: 8px 8px 0 0;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 1rem;
    top: 0.6rem;
    color: white;
    font-weight: bold;
}

/* Subtle fade-in animation for page content */
@keyframes fadeIn {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: translateY(0); }
}

.mud-main-content > .mud-container {
    animation: fadeIn 0.3s ease-out;
}

/* =============================================================================
   Kanban / Sales Pipeline Styles — Phase 7.5
   ============================================================================= */

/* Kanban container: wraps responsively, scrolls only when truly needed */
.kanban-container {
    padding-bottom: 1rem;
}

/* Kanban board: flexbox layout with wrapping for responsive columns */
.kanban-board {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    min-height: 300px;
}

/* Each Kanban column — responsive widths: 4 per row on large, 3 on medium, 2 on small */
.kanban-column {
    flex: 1 1 calc(25% - 0.75rem);
    min-width: 220px;
    max-width: calc(50% - 0.5rem);
    background: var(--mud-palette-background-gray);
    display: flex;
    flex-direction: column;
}

/* Drop zone inside each column */
.kanban-dropzone {
    flex: 1;
    min-height: 200px;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 0.5rem 0;
}

/* Empty dropzone visual hint */
.kanban-dropzone:empty::after {
    content: "Drop leads here";
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    color: var(--mud-palette-text-secondary);
    font-size: 0.875rem;
    opacity: 0.6;
}

/* Kanban card styling */
.kanban-card {
    background: var(--mud-palette-surface);
    cursor: grab;
    transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}

.kanban-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}

/* Card being dragged */
.kanban-card:active,
.mud-drop-item-preview {
    cursor: grabbing;
    opacity: 0.85;
    transform: scale(1.02);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2) !important;
}

/* Overdue cards have red left border */
.kanban-card-overdue {
    border-left: 3px solid var(--mud-palette-error) !important;
}

/* Drop zone highlight when dragging over */
.mud-drop-zone-drag-over {
    background: rgba(var(--mud-palette-primary-rgb), 0.08) !important;
    border: 2px dashed var(--mud-palette-primary) !important;
    border-radius: 8px;
}

/* Mobile responsiveness: stack columns vertically */
@media (max-width: 768px) {
    .kanban-board {
        flex-direction: column;
        gap: 1rem;
    }

    .kanban-column {
        flex: 0 0 auto;
        min-width: 100%;
        width: 100%;
    }

    .kanban-dropzone {
        min-height: 120px;
    }

    .kanban-container {
        overflow-x: visible;
    }
}

/* Utility: font-weight bold shorthand */
.fw-bold {
    font-weight: 600 !important;
}

/* =============================================================================
   Modern Header & Dashboard Banner — Issue 5 Fix
   Replaces solid baby-blue with gradient, shadows, and modern typography
   ============================================================================= */

/* Clean, modern app bar — surface-colored with a hairline bottom border.
   Drives the background off MudBlazor's own variable so it auto-adapts to dark mode
   (MudBlazor swaps CSS variables, not a .mud-theme-dark class). */
.mud-appbar.app-bar {
    background: var(--mud-palette-appbar-background) !important;
    border-bottom: 1px solid var(--mud-palette-lines-default);
    box-shadow: none !important;
}

/* Dashboard welcome banner — solid Vahary violet hero (brand uses no gradients, UI-IOS-023) */
.dashboard-banner {
    background: var(--v-color-interactive-accent, #5B21B6) !important;
    color: var(--v-color-text-on-accent) !important;
    border-radius: 16px !important;
    box-shadow: 0 10px 30px -8px rgba(79, 70, 229, 0.45), 0 2px 6px rgba(15, 23, 42, 0.08) !important;
    position: relative;
    overflow: hidden;
}

.dashboard-banner::before {
    content: '';
    position: absolute;
    top: -50%;
    right: -20%;
    width: 60%;
    height: 200%;
    background: radial-gradient(ellipse, rgba(255, 255, 255, 0.08) 0%, transparent 70%);
    pointer-events: none;
}

.dashboard-banner .mud-typography {
    color: var(--v-color-text-on-accent) !important;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}

/* Dark mode dashboard banner. The background override is GONE: it named
   `--v-color-interactive-accent-bright`, which is not a real token and never has been, so it was
   painting its raw #8B5CF6 fallback and ignoring the token layer. `--v-color-interactive-accent`
   already carries the dark value (#A78BFA) — that is the point of the token — so the base rule above
   is correct in both themes and only the shadow needs a dark-mode variant. */
.mud-theme-dark .dashboard-banner {
    box-shadow: 0 10px 30px -8px rgba(0, 0, 0, 0.6), 0 1px 4px rgba(0, 0, 0, 0.3) !important;
}

/* ── Card polish: softer, layered shadows + smooth hover lift ── */
.mud-paper:not(.mud-appbar):not(.mud-drawer) {
    transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
}
.mud-card {
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 1px 3px rgba(15, 23, 42, 0.06) !important;
    border: 1px solid var(--mud-palette-lines-default);
}
.mud-theme-dark .mud-card {
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
}

/* Impersonation warning banner */
.impersonation-banner {
    background: var(--v-color-status-attention, #D97706);
    color: white;
    padding: 8px 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    font-weight: 600;
    font-size: 0.875rem;
    letter-spacing: 0.02em;
    box-shadow: 0 2px 8px rgba(239, 108, 0, 0.4);
    z-index: 1100;
    animation: impersonation-pulse 3s ease-in-out infinite;
}

@keyframes impersonation-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.92; }
}

.impersonation-banner .mud-button-root {
    color: white !important;
    border-color: rgba(255,255,255,0.5) !important;
    font-weight: 600;
}

.impersonation-banner .mud-button-root:hover {
    background: rgba(255,255,255,0.15) !important;
    border-color: white !important;
}

/* Auth (login / register / portal login): the bespoke centered-card treatment that lived here
   was replaced by the suite's Monument/Aperture shell — see vahary-auth-shell.css. */

/* Drawer modernization — hairline border driven off the theme variable so it stays
   visible in both light and dark (MudBlazor swaps variables, not a .mud-theme-dark class). */
.mud-drawer {
    border-right: 1px solid var(--mud-palette-lines-default) !important;
    box-shadow: none !important;
}

/* ══════════════════════════════════════════════════════════════════════════════════════════════
   Wave 3 — the shared component layer.

   954 inline `Style` attributes across 108 files, and 214 raw hex values across 28, exist because
   there was nowhere else to put a spacing or a radius decision. Every screen re-decided what the
   product looks like. These few rules are that "somewhere else": PageHeader, StatCard and
   EmptyState render against them, so a spacing change happens once instead of 108 times.

   Everything below derives from MudBlazor's own palette variables, so light/dark and any future
   brand change are handled by the theme rather than duplicated here.
   ══════════════════════════════════════════════════════════════════════════════════════════════ */

:root {
    /* A 4px-based scale. Named by role, not by size, so call sites stop inventing 13px and 18px. */
    --v-space-xs: 4px;
    --v-space-sm: 8px;
    --v-space-md: 16px;
    --v-space-lg: 24px;
    --v-space-xl: 40px;
    --v-radius-sm: 8px;
    --v-radius-md: 10px;   /* matches AppTheme.LayoutProperties.DefaultBorderRadius */
    --v-radius-lg: 12px;
}

/* Atomic strings that must not fold. An invoice number, a formatted date or a rate reads as one
   token; broken over two lines it reads as two. `.text-truncate` is the wrong tool — it hides
   characters — and MudBlazor 9.7 ships no nowrap utility at all (`mud-text-nowrap`, used in
   MainLayout, does not exist in its stylesheet). Nine call sites had inlined the declaration. */
.v-nowrap { white-space: nowrap; }

/* `.text-right` / `.text-center` do NOT work inside a MudSimpleTable, and never have. MudBlazor
   ships `.mud-simple-table table * tr>td, .mud-simple-table table * tr th { text-align: start }` —
   specificity (0,1,3) against the utility's (0,1,0) — so every `class="text-right"` written on a
   simple-table cell is silently ignored while the identical class on a MudTable cell works. That is
   why currency columns in these tables were left-aligned even where the markup asked for right, and
   why the screens that got it right used an inline `style` instead.

   Re-scoped here rather than fixed by adding `!important` to the global utilities: MudBlazor's
   responsive stacking (`.mud-xs-table .mud-table-cell { text-align: start !important }`) relies on
   winning, and an `!important` utility loaded after it would centre the stacked mobile rows of every
   table in the product. MudSimpleTable does not stack, so scoping it here is safe. */
.mud-simple-table table * tr > td.text-right,
.mud-simple-table table * tr th.text-right { text-align: right; }

.mud-simple-table table * tr > td.text-center,
.mud-simple-table table * tr th.text-center { text-align: center; }

/* A table row or list row that behaves as a button. Screens that made rows clickable gave them an
   inline `cursor: pointer` and nothing else — so the row had role="button", tabindex and a keydown
   handler, but tabbing onto it produced no visible change at all. Colour-free feedback (a tint on
   hover, a ring on focus) is the part that makes the keyboard path usable. */
.v-row-button {
    cursor: pointer;
}

.v-row-button:hover {
    background-color: var(--mud-palette-action-default-hover);
}

.v-row-button:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: -2px;
}

/* Text sitting on a filled palette surface (`mud-theme-primary` and friends). Screens hardcoded
   `color: rgba(255,255,255,0.7)` for these, which is white-on-white the moment the palette behind
   them is light — and in the dark theme Primary IS light (#A78BFA). Opacity de-emphasises without
   naming a colour, so the contrast text the theme chose keeps winning. */
.v-onfill-label { opacity: 0.75; }
.v-onfill-value { opacity: 0.9; }
.v-onfill-divider { border-color: currentColor !important; opacity: 0.3; }
/* Same reasoning for an outlined control on a filled surface — a chip or button whose border was
   drawn in `rgba(255,255,255,0.6)`. currentColor keeps it tied to whatever contrast colour the
   theme picked for the fill behind it. */
.v-onfill-outline { border-color: currentColor !important; opacity: 0.85; }

/* A unit after a figure ("/mo", "/yr") reads as an annotation on the number, not part of it. */
.v-unit-suffix {
    font-size: 0.6em;
    font-weight: 400;
    opacity: 0.8;
}

/* ── PageHeader ──────────────────────────────────────────────────────────────────────────── */
.v-page-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--v-space-md);
    margin-bottom: var(--v-space-lg);
}

.v-page-header__text {
    min-width: 0; /* lets a long title truncate instead of forcing the row wide */
}

.v-page-header__title {
    display: flex;
    align-items: center;
    gap: var(--v-space-sm);
    font-weight: 700;
    /* Replaces a hand-written `vertical-align: middle` on the icon at every call site, which only
       approximated alignment because the icon and the text have different optical centres. */
}

.v-page-header__icon {
    flex: 0 0 auto;
    color: var(--mud-palette-primary);
}

/* The supporting line under a page title.
   Colour lives here rather than on the component, because MudBlazor's `Color="Color.Secondary"`
   emits `!important` and would beat any container that needs to override it — which is exactly what
   happened inside `.v-portal-hero`, where the subtitle rendered grey on violet at 1.76:1 (light)
   and 1.06:1 (dark). `text-secondary` is the palette's actual muted TEXT colour, not the brand
   secondary, and the hero rule below can now win on specificity as intended. */
.v-page-header__subtitle {
    margin-top: var(--v-space-xs);
    color: var(--mud-palette-text-secondary);
}

/* Actions WRAP under the title rather than clipping. The old hand-rolled headers pushed secondary
   buttons off-canvas at 768px — on /jobs/{id} "Critical Path" was half off screen and "Generate
   Invoice" was gone entirely. */
.v-page-header__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--v-space-sm);
}

@media (max-width: 600px) {
    .v-page-header__actions {
        width: 100%;
    }
}

/* ── StatCard ────────────────────────────────────────────────────────────────────────────── */
/* Fill the grid cell, so a row of KPI cards has ONE height.
   Without this the single card carrying a Caption ("· 78 total") is taller than its siblings and the
   row renders crooked — and a shared primitive that renders crooked reads worse than the four
   bespoke cards it replaced, because the inconsistency now looks systemic rather than incidental. */
.v-stat {
    height: 100%;
}

.v-stat__avatar {
    border-radius: var(--v-radius-lg);
    flex: 0 0 auto;   /* never let a long value squeeze the icon out of round */
}

.v-stat__value {
    margin-bottom: 0;
    font-variant-numeric: tabular-nums;
    /* Tabular figures so a column of KPIs does not jitter as values change width. */
}

.v-stat__text {
    min-width: 0;
}

/* Optional trailing graphic (sparkline). Never allowed to push the figure out of the card. */
.v-stat__extra {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
}

/* Interactive ONLY when the card actually goes somewhere. A card that lifts under the cursor and
   does nothing is worse than a flat one. */
.v-stat--interactive {
    cursor: pointer;
    transition: box-shadow 120ms ease, transform 120ms ease;
}

.v-stat--interactive:hover {
    transform: translateY(-1px);
    box-shadow: var(--mud-elevation-6);
}

.v-stat--interactive:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
}

/* A StatCard used as a single-selection filter — the renewal-pipeline buckets. Those five cards
   each carried an inline `border: 2px solid var(--mud-palette-…)` in a different hue, so one
   channel was made to carry two meanings at once: the bucket's severity AND which bucket you had
   selected. Selection now has a single colour plus a raised elevation, so it survives greyscale
   and colour blindness, and the ring is always present (transparent when off) so selecting a
   bucket never shifts the row. */
.v-stat--selectable {
    border: 2px solid transparent;
}

.v-stat--selected {
    border-color: var(--mud-palette-primary);
    box-shadow: var(--mud-elevation-4);
}

@media (prefers-reduced-motion: reduce) {
    .v-stat--interactive {
        transition: none;
    }
    .v-stat--interactive:hover {
        transform: none;
    }
}

/* ── EmptyState ──────────────────────────────────────────────────────────────────────────── */
.v-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--v-space-sm);
    padding: var(--v-space-xl) var(--v-space-lg);
    text-align: center;
    border: 1px dashed var(--mud-palette-lines-default);
    border-radius: var(--v-radius-md);
    background: var(--mud-palette-background-gray);
    /* -gray, not -grey. MudBlazor 9.7 emits `gray`; the `grey` spelling is dead and rendered these
       panels with no background at all in 14 places before Wave 0 fixed them. */
}

.v-empty__icon {
    opacity: 0.7;
}

.v-empty__title {
    /* Same cap as the body below it. Without this the title was the one part of the panel with no
       measure at all, so on a wide table a long title ran the full width while the sentence under
       it wrapped at 46ch — the two lines of the same block disagreeing about where the edge is.
       The class was referenced here from the start and defined nowhere, which styles nothing and
       reports nothing; see check-classes.py. */
    max-width: 46ch;
}

.v-empty__body {
    max-width: 46ch; /* keeps the explanation readable rather than stretched across a wide table */
}

.v-empty__actions {
    margin-top: var(--v-space-sm);
    display: flex;
    flex-wrap: wrap;
    gap: var(--v-space-sm);
    justify-content: center;
}

/* ── Customer portal ─────────────────────────────────────────────────────────────────────────
   The white-label portal is the only surface a dealer's own customer ever sees, and its page
   hero was re-declared inline on eight screens: the same two properties on the MudPaper, and
   then the SAME colour again on every MudText inside it, because a MudText renders its own
   `color` and does not inherit the paper's. That is ~30 inline declarations saying one thing.
   One class carries it now, and the descendant rule means children need no copy at all.
   Both palette entries are theme-aware (light: violet on off-white text; dark: light violet on
   near-black text), so the hero reads correctly in both modes without a second rule.

   `.v-hero` is the same device outside the portal — the operator dashboard's welcome card was
   doing this by hand in literal `white` and `rgba(255,255,255,0.7)`, which is the exact failure
   this class exists to prevent: Primary in the DARK theme is light violet (#A78BFA), so white on
   it is white-on-light. Same rules, no "portal" in the name, because the device was never
   portal-specific. */
.v-portal-hero,
.v-hero {
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
}

.v-portal-hero .mud-typography,
.v-portal-hero .mud-icon-root,
.v-portal-hero .mud-button-root,
.v-hero .mud-typography,
.v-hero .mud-icon-root,
.v-hero .mud-button-root,
.v-hero .mud-chip {
    color: var(--mud-palette-primary-text);
}

/* Supporting line inside a hero. Held at 0.85 rather than a second colour, because no palette
   hue clears AA against the primary fill in both themes. */
.v-portal-hero__lead,
.v-portal-hero .v-page-header__subtitle {
    opacity: 0.85;
}

/* A PageHeader nested in a hero owns the paper, so its own bottom margin would read as a second
   pad. The hero paper's `pa-*` is the spacing. */
.v-portal-hero .v-page-header {
    margin-bottom: 0;
}

/* Portal cards sit in MudGrid rows of two and three and were each sized by their own content,
   so a row of "recommendations" or sites stepped up and down like a bar chart. */
.v-portal-card {
    height: 100%;
}

/* A card the customer picks from — request types, appointment slots, the site selector. Three
   screens each carried their own inline `border: 2px solid …; transition: all 0.2s`, and NONE of
   the three could be operated by keyboard: a customer without a pointer could not choose what
   kind of service to request. The ring is always present (transparent when off) so choosing does
   not shift the row, and :focus-visible draws a real one. */
.v-portal-choice {
    border: 2px solid transparent;
    cursor: pointer;
    transition: border-color 120ms ease, box-shadow 120ms ease;
}

.v-portal-choice:hover {
    border-color: var(--mud-palette-lines-default);
}

.v-portal-choice--selected,
.v-portal-choice--selected:hover {
    border-color: var(--mud-palette-primary);
}

.v-portal-choice:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 2px;
}

.v-portal-choice--unavailable {
    opacity: 0.5;
    cursor: default;
}

.v-portal-choice--unavailable:hover {
    border-color: transparent;
}

@media (prefers-reduced-motion: reduce) {
    .v-portal-choice {
        transition: none;
    }
}

/* The big glyph on a portal outcome screen — request submitted, appointment booked, payment
   taken or cancelled, feedback thanks. Six screens, six different inline sizes: 4rem, 5rem,
   64px, 80px, and Size.Large twice. They are the same moment in the same product. */
.v-portal-outcome-icon {
    font-size: 4rem;
}

/* Legend swatches. The scheduling calendar hand-wrote `width:12px;height:12px;border-radius:50%`
   three times and the site map hand-wrote a 16px square five times; only the colour ever
   differed, and the colour is the one thing a legend is actually for. */
.v-legend-dot {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    flex: 0 0 auto;
}

.v-legend-swatch {
    width: 16px;
    height: 16px;
    border-radius: 3px; /* not --v-radius-sm: 8px on a 16px chip is a pill, not a swatch */
    flex: 0 0 auto;
}

/* The 0–10 score echoed back on the feedback thank-you screen. It was one 400-character inline
   style, rebuilt eleven times per render, and it tinted the chosen number success / warning /
   error by NPS band — telling the respondent that their own honest 6 was a failure. The form
   itself already refuses to do that ("the scale must not tell the respondent what their answer
   means"); the confirmation now agrees with it. */
.v-nps-bubble {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    border: 2px solid var(--mud-palette-lines-default);
    opacity: 0.4;
}

.v-nps-bubble--selected {
    background: var(--mud-palette-primary);
    color: var(--mud-palette-primary-text);
    border-color: var(--mud-palette-primary);
    opacity: 1;
}

/* The six-digit access code on the portal sign-in screen. The spaced, oversized treatment was
   written as an inline Style on the MudTextField — which lands on the control WRAPPER, and
   MudBlazor sets `font-size` on `.mud-input-root` itself, so the rule never reached the input:
   the code rendered at body size in a normal stack. Targeting the input is what makes it real. */
.v-otp-field input {
    letter-spacing: 0.5em;
    font-size: 1.5rem;
    text-align: center;
}

/* The portal's mobile bottom navigation. `portal-bottom-nav` was already on the element and had
   never been declared anywhere — the positioning lived in a parallel inline style, and each of the
   four buttons repeated the same three declarations. */
.portal-bottom-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1100;
}

.portal-bottom-nav .mud-button-root {
    flex-direction: column;
    min-width: 60px;
    font-size: 0.65rem;
}

/* Clearance for whichever fixed bar is at the bottom of the portal.
   MudMainContent carried a flat `padding-bottom: 16px`, which is shorter than the dense footer
   app bar (48px) AND shorter than the mobile navigation (~56px) — so the last element of every
   portal page rendered underneath one of them. On a phone that was both at once, since the two
   are stacked in the same strip until the footer is hidden below sm. */
.portal-main {
    padding-bottom: 64px;
}

@media (max-width: 599.98px) {
    /* Below sm the footer is hidden, so only the navigation needs clearing — plus the iOS home
       indicator, which otherwise overlaps the bottom row of buttons on a notched phone. */
    .portal-main {
        padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
    }

    .portal-bottom-nav {
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }
}

/* A select sitting inside an EmptyState's action row — e.g. the design picker that replaced the
   dead end on the cable-schedule and BOM pages. Bounded so it does not stretch to the full width of
   a centred empty state, which is the one place a full-width select looks broken. */
.v-picker-select {
    max-width: 360px;
    min-width: 220px;
}

/* Tabular figures for identifiers and counts that stack in a column — the BOM's schedule tags, for
   one. Proportional digits make "C-101, C-102" and "C-98, C-99" fail to line up, which defeats the
   only reason to print the tags under each other. */
.v-tabular {
    font-variant-numeric: tabular-nums;
}

/* A panel recessed into the page rather than raised off it: the summary strip above a detail
   record, a sub-card inside a tab, a dialog's read-only context block. Twenty-three call sites had
   each inlined `background: var(--mud-palette-background-gray)` on a MudPaper, MudCard or
   MudToolBar, which is the right variable but re-decided twenty-three times — and re-decided
   nowhere when the recessed surface eventually needs a border or a different tint in dark mode.
   `background` (not `background-color`) is deliberate: it matches what the inline declaration did,
   so a paper that had a gradient from somewhere else keeps losing it exactly as before. */
.v-inset {
    background: var(--mud-palette-background-gray);
}

/* The page-level standing state — access denied, not found, nothing configured yet. Thirteen
   screens wrote `min-height: 60vh` next to the same `d-flex … align-center justify-center`, which
   is the vertical-centring half of the pattern doing nothing without a height to centre within.
   60vh rather than 100vh on purpose: the app bar and page padding are already above it, so a full
   viewport pushes the message below the fold on a laptop. */
.v-page-state {
    min-height: 60vh;
}

/* An icon set inline with heading text, not in its own slot. MudBlazor renders icons as `svg`,
   which sits on the text baseline and hangs the glyph a few pixels low next to a MudText — the
   reason ten call sites reached for `vertical-align: middle` and none of them for a flex row. */
.v-inline-icon {
    vertical-align: middle;
}

/* Free text the user typed, played back as they typed it: customer notes, job notes, a ticket
   description. Without this the newlines they entered collapse and three paragraphs render as one,
   which reads as data loss even though nothing was lost. */
.v-preserve-lines {
    white-space: pre-wrap;
}

/* ── Sizes that eleven screens each decided for themselves ───────────────────────────────────
   Everything below replaces a cluster of inline widths that named the same ROLE at different
   numbers. The numbers were not disagreements anyone had — they are what happens when the only
   place to put a size is the call site. Each rule states the one value and says why it is that
   value, so the next screen inherits the decision instead of re-taking it. */

/* The trailing column of icon buttons on a table. Eleven tables declared it at 80, 100, 120, 140
   and 150px, with no relation to how many buttons they hold — the 150px ones held two and the
   100px ones held three. 120px fits three Size.Small icon buttons (3 × 30px) inside
   .mud-table-cell's 16px side padding exactly; the one table with four buttons still fits,
   because MudBlazor's table-layout is `auto` and a column that needs more simply takes it. The
   150px columns were the real cost: 30px stolen from the data beside them on every row. */
.v-col-actions {
    width: 120px;
}

/* The search or filter field above a list. Eight of them, at 280, 300, 300, 300, 300, 300, 320
   and 400px. 300px is what six of them already were and fits the longest placeholder in the
   product ("Search by SKU, name…") without truncation. */
.v-filter-field {
    max-width: 300px;
}

/* A table cell holding free text that must not set the column width — a risk description, an
   audit-log payload, a ticket subject. Eight cells inlined some subset of the four declarations
   this needs, and three of them inlined only `max-width` alongside `.text-truncate`, which is the
   same four declarations written in two places. */
.v-cell-truncate {
    max-width: 250px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A progress bar reading as a figure inside a row, not as a page-width meter. Seven of them at
   56, 60, 70 and 80px; 80px is the width three separate screens landed on and the shortest that
   still shows a 5% fill as a visible sliver rather than a rounding artefact. */
.v-inline-meter {
    width: 80px;
}

/* ── Two shapes that mean something ──────────────────────────────────────────────────────────
   Every other inline `border-radius` in the client has been deleted rather than moved: the web
   theme sets `DefaultBorderRadius = "10px"` (VaharyAppTheme.BuildWeb), so a screen writing 8, 10
   or 12px was overriding the product's own radius with a value no one chose. These two are not
   that — they are shapes doing work, so they get names. */

/* The floating support-chat panel. Larger than a card because it is not on the page, it is over
   it; `overflow: hidden` is what clips the header's corners, so the header does not need a radius
   of its own. */
.v-panel-floating {
    border-radius: 16px;
    overflow: hidden;
}

/* A message bubble in a chat transcript. */
.v-chat-bubble {
    border-radius: 16px;
}

/* Fully rounded ends — a compose field and a send button read as a pill, not as a card corner.
   9999px rather than the 20px that was inlined: 20px happens to equal half the height of a dense
   control, so it only looked like a pill by coincidence and stopped being one the moment the
   control grew.

   The second selector is not belt-and-braces. A MudTextField's `Style`/`Class` lands on
   `.mud-input-control`, but the rounded thing you can see is the `.mud-input-outlined-border`
   fieldset inside it, which MudBlazor sets from `--mud-default-borderradius` at specificity
   (0,3,0). So the chat composer's inline `border-radius: 20px` never applied at all — it rendered
   at the theme's 10px for as long as it has existed. Matching MudBlazor's specificity and winning
   on source order is what makes the declaration real. */
.v-pill,
.mud-input-control.v-pill .mud-input-outlined-border {
    border-radius: 9999px;
}

/* ── A coloured edge marking what a card is about ────────────────────────────────────────────
   Eight cards drew `border-left: 4px solid var(--mud-palette-…)` in five different palette
   colours, and two of them drew it on the TOP edge instead — the same device, the direction
   picked per screen rather than per meaning. Split into a shape and a colour so a call site
   chooses the meaning and inherits the shape: the width, the style and the edge stop being
   nine independent decisions.

   The colour travels as a custom property rather than a full border declaration per variant, so
   `--top` does not have to be written out once for every colour. */
.v-accent-edge {
    border-left: 4px solid var(--v-accent-color, var(--mud-palette-primary));
}

/* Top edge instead of leading edge — for a centred outcome card, where a left edge reads as a
   list item that wandered out of a list. */
.v-accent-edge--top {
    border-left: 0;
    border-top: 4px solid var(--v-accent-color, var(--mud-palette-primary));
}

.v-accent-primary { --v-accent-color: var(--mud-palette-primary); }
.v-accent-secondary { --v-accent-color: var(--mud-palette-secondary); }
.v-accent-tertiary { --v-accent-color: var(--mud-palette-tertiary); }
.v-accent-success { --v-accent-color: var(--mud-palette-success); }
.v-accent-warning { --v-accent-color: var(--mud-palette-warning); }

/* A panel at the app's raised surface colour, as opposed to `.v-inset`'s recessed one. Both were
   being inlined; naming only one of the pair is how the other drifts. */
.v-surface {
    background: var(--mud-palette-surface);
}

/* A table that must not paint a surface of its own because it is already inside a paper that
   painted one. Four MudSimpleTables inlined `background: transparent` for this; MudBlazor gives
   the table `--mud-palette-surface` by default, which reads as a second card inside the first. */
.v-table-flush {
    background: transparent;
}

/* The large icon standing above an empty-state message. Five of them at 36px, 48px, 64px and
   80px — four sizes for one role, none of them chosen against the others. 64px is the middle and
   matches the `4rem` that the two most recently written call sites had already landed on. */
.v-icon-empty {
    font-size: 64px;
}

/* A decorative oversized glyph inside a hero, there as texture rather than as information. The
   opacity — not a hardcoded `rgba(255,255,255,0.2)` — is what keeps it legible when the fill
   behind it changes with the theme. */
.v-hero-watermark {
    font-size: 6rem;
    opacity: 0.2;
}

/* The stock-status glyph in the product sidebar's list rows. Three icons in the same slot at two
   different sizes (8px for the in-stock dot, 12px for the two warnings), so the healthy state
   read as a speck next to its own siblings. */
.v-list-status-icon {
    font-size: 12px;
}

/* =============================================================================
   Branded boot state (login Wave 1)
   -----------------------------------------------------------------------------
   Painted from the static HTML in App.razor before any script runs, and removed by the
   afterWebAssemblyStarted initializer. Every rule here has a literal fallback: vahary.css is a
   separate stylesheet, and on a cold cache this element can paint before the token sheet has
   applied — a boot mark that depends on the thing it is covering for would defeat itself.
   ============================================================================= */
#vahary-boot {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 20px;
    background: var(--v-color-surface-base, #F2F3F1);
    opacity: 1;
    transition: opacity 0.25s ease;
}

/* The theme script sets data-v-theme before first paint; these keep the fallbacks honest for the
   window where vahary.css has not applied yet. */
[data-v-theme="dark"] #vahary-boot {
    background: var(--v-color-surface-base, #12161A);
}

.vahary-boot__mark {
    width: 44px;
    height: 44px;
    /* Steel, not violet: this is a wayfinding mark, not a call to action, and the aperture
       cut-out reads better in a muted ink at 44px. */
    color: var(--v-color-text-muted, #8B949C);
    animation: vahary-boot-pulse 1.6s ease-in-out infinite;
}

.vahary-boot__label {
    font-family: var(--v-font-body, 'Work Sans'), system-ui, sans-serif;
    font-size: 0.8125rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--v-color-text-muted, #8B949C);
}

#vahary-boot.vahary-boot--done {
    opacity: 0;
    pointer-events: none;
}

@keyframes vahary-boot-pulse {
    0%, 100% { opacity: 0.45; }
    50%      { opacity: 1; }
}

/* Stills, not motion — the mark still reads at full strength. */
@media (prefers-reduced-motion: reduce) {
    .vahary-boot__mark {
        animation: none;
        opacity: 1;
    }
    #vahary-boot {
        transition: none;
    }
}
