﻿/* App-level CSS. The MudBlazor theme owns typography and palette;
   everything here is reserved for Blazor framework affordances. */

/* The brand tokens that do NOT move with the theme, mirroring the constants of the same name on
   MediTrackHubTheme. MudBlazor emits CSS variables for palette slots only, so a token with no slot
   has to be declared once here rather than repeated at each use site (UI-Standards §1: never
   hardcode a palette value in a component).
   They belong on a plain :root because they are theme-invariant by design: the sidebar is a deep
   point of the brand family in BOTH palettes, so what sits on it needs one value and not two —
   the same reason NavSections pins its text to fixed white opacities. */
:root {
    --mth-brand: #248F8F;
    --mth-muted-light: #D6F5F5;
    --mth-sidebar-light: #145252;
    --mth-field-gap: 16px;
    /* The two bands that close the page: the drawer's operator badge and the footer. They meet at
       the drawer edge and read as one horizontal band, so their heights cannot be left to whatever
       each happens to contain - an avatar on one side, a line of text on the other, which put a
       5px step exactly on the seam. One declared height, and neither depends on its content. */
    --mth-bottom-band: 49px;
}

html, body {
    margin: 0;
    padding: 0;
}

/* The layout shell. MudMainContent was a plain block, so the footer inside it simply stopped where
   the content stopped: on a short page it floated with empty space beneath it, and it lined up with
   the bottom of the drawer only by coincidence - the drawer is viewport-tall, the footer was not.
   Making the shell a full-height column and letting the content block take the slack pins the
   footer to the bottom when there is room and lets it scroll away when there is not.
   border-box keeps the appbar padding MudBlazor puts on this element inside the 100vh. */
.mud-main-content {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

.mud-main-content > .app-content {
    flex: 1 1 auto;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

.blazor-error-boundary::after {
    content: "An error has occurred.";
}


/* Single width for every grid-toolbar search field, so otherwise identical toolbars cannot drift.
   Replaces the per-page users-search / systems-search classes, which were applied but never
   defined in any stylesheet. Shrinks rather than overflowing on narrow toolbars. */
.entity-search {
    width: 320px;
    max-width: 100%;
    flex: 0 1 320px;
}

/* The permissions workspace: a band of controls above, the drawing below, and nothing beside the drawing.
   This is also the element that goes full screen, which is why it owns a background of its own — a
   fullscreened element is painted over a black backdrop, and inheriting a transparent background would put
   the drawing on black whatever the theme says. */
.permissions-workspace {
    display: flex;
    flex-direction: column;
    gap: 12px;
    background-color: var(--mud-palette-surface);
}

/* Two sections rather than one row: the filters are always there, the value selector is there only when
   something is selected. `flex-wrap` is what makes the pair stack on a narrow window instead of squeezing
   both into unreadable columns. */
.permissions-band {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    gap: 12px;
}

/* Sized by its content, and allowed to wrap rather than to stretch: a toggle row that grows to fill the
   band would put centimetres between four short words. */
.permissions-kind-filter {
    flex: 0 0 auto;
    align-self: center;
}

.permissions-filters {
    display: flex;
    flex-direction: column;
    gap: 8px;
    flex: 1 1 22rem;
    min-width: 0;
}

/* Visibly a section of its own, per the shape agreed with the user: separated from the filters rather than
   mixed in with them. It takes the width its content needs and yields it back the moment the selection is
   cleared. */
.permissions-selector {
    flex: 1 1 26rem;
    min-width: 0;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    background-color: var(--mud-palette-surface);
}

.permissions-selector-body {
    padding: 12px 16px;
}

/* The strip: identity, the three values, the outcome — in a row while there is room, stacked when there is
   not. Deliberately baseline-free `center` alignment: the three parts have different heights and aligning
   their first lines would leave the radios floating above their own labels. */
.permissions-selector-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 20px;
}

.permissions-selector-identity {
    display: flex;
    flex-direction: column;
    flex: 1 1 12rem;
    min-width: 0;
}

/* MudRadioGroup wraps its radios in an inline container; the row layout has to be asked for explicitly, and
   wrapping has to be allowed or three values overflow the strip on a narrow band. */
.permissions-selector-values .mud-input-control-boolean-input,
.permissions-selector-values > div {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
}

.permissions-selector-outcome {
    display: flex;
    flex-direction: column;
    flex: 1 1 14rem;
    min-width: 0;
}

/* The frame holds the border and the controls; the host inside it holds the scrolling. Separating the two
   is what keeps the controls still while the drawing moves under them. */
.permission-graph-frame {
    position: relative;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    background-color: var(--mud-palette-surface);
    overflow: hidden;
}

/* Top right, over the drawing, on a translucent bed so the controls stay legible whatever is underneath.
   No transition on anything here: this corner enters every visual baseline, and a control that animates is
   a control that captures differently depending on when the screenshot was taken. */
.permission-graph-toolbar {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 2;
    display: flex;
    gap: 2px;
    padding: 2px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    background-color: var(--mud-palette-surface);
    opacity: 0.92;
}

.permission-graph-toolbar .mud-button-root {
    transition: none;
}

/* The two fullscreen buttons are both rendered and the state picks one. `:fullscreen` is the browser's own
   answer to "is this element full screen", which means Esc, F11 and the button all agree without anything
   having to be told. */
.permission-graph-fullscreen-exit {
    display: none;
}

.permissions-workspace:fullscreen .permission-graph-fullscreen-exit {
    display: inline-flex;
}

.permissions-workspace:fullscreen .permission-graph-fullscreen-enter {
    display: none;
}

/* A fullscreened element is laid out against the screen, not against the page: the workspace has to claim
   that height, and the drawing has to take what the band leaves rather than the 70vh cap it uses in the
   page. The subtraction is generous on purpose — the band is one row tall with nothing selected and two or
   three when the diagnostics have something to say — and the workspace scrolls if it is not enough. */
.permissions-workspace:fullscreen {
    height: 100%;
    padding: 12px;
    overflow: auto;
}

.permissions-workspace:fullscreen .permission-graph-host {
    height: calc(100vh - 14rem);
    min-height: 20rem;
}

/* The permissions graph. Laid out by Graphviz and drawn as SVG, which is what lets the state below be
   CSS classes rather than a stylesheet object inside a JavaScript module. The drawing carries a viewBox
   and no width or height, so it scales to the host; the host scrolls when the drawing is larger. */
/* Height follows the window rather than a constant. The drawing's proportions come from the catalogue and
   not from the viewport, so the host cannot simply be given an aspect ratio; but a fixed 620px wasted the
   lower half of a tall screen and cut the drawing in two on a short one. The floor keeps it usable on a
   laptop in a split window. */
/* The border now belongs to the frame around it; the host only scrolls. */
.permission-graph-host {
    width: 100%;
    height: min(70vh, 760px);
    min-height: 380px;
    overflow: auto;
    background-color: var(--mud-palette-surface);
}

/* The toolbar floats over this box, so the drawing is given the band it occupies instead of starting
   underneath it. Measured rather than guessed: at 1920 the controls covered the top right node of the Users
   cluster, and a translucent bed over a label is still a label nobody can read. Padding rather than a margin
   because it belongs to the scrolled content — scroll down and the drawing does pass under the controls,
   which is the correct trade: the initial view, and the view every baseline captures, is unobstructed. */
.permission-graph-frame > .permission-graph-host {
    padding-top: 2.75rem;
}

/* Over the host rather than above it. The host keeps its height whether or not anything is drawn in it,
   so the frame does not change size when the graph is seeded and the page does not jump under the
   operator who just pressed the button. Starts below the floating toolbar band, which stays reachable. */
.permission-graph-empty {
    position: absolute;
    inset: 2.75rem 0 0 0;
    z-index: 1;
    display: flex;

    /* Near the top, not centred. The host is min(70vh, 760px) tall whether or not anything is drawn in
       it, so centring put the message halfway down that box and pushed its button below the fold on a
       900px screen — an empty state whose only action cannot be seen without scrolling. */
    align-items: flex-start;
    justify-content: center;
    padding-top: 72px;
    background-color: var(--mud-palette-surface);
}

/* The drawing can be dragged, so the host says so. `grabbing` only while a pan is actually running: a
   cursor that changes on press for a gesture that turns out to be a click is a flicker. */
.permission-graph-host {
    cursor: grab;
}

.permission-graph-host.panning {
    cursor: grabbing;
}

/* `width` is set inline by the module — `scale × 100%` — and this is the value it starts from. Height stays
   automatic: the viewBox fixes the ratio, so scaling the width scales both. */
.permission-graph-host svg {
    display: block;
    width: 100%;
    height: auto;

    /* Without this, dragging across the drawing selects the labels instead of panning. The keys stay
       selectable where they are meant to be read and copied — in the value selector strip. */
    user-select: none;
}

/* The two channels, and never one. FILL is what the subject SAYS, BORDER is what RESULTS once precedence
   and the whole prerequisite closure have been applied — they differ exactly in the cases somebody opens
   this screen for.

   The hues are the theme's semantic tokens, and they used to be three hand-picked hexes. The reasoning
   for picking them — a Deny is a decision, not a failure, so it must not be painted like an error — was
   sound and is kept, but it was answered in the wrong currency: what it needs is a quieter Allow and a
   quieter Deny, not colours from outside the palette. The old fill was `#2e7d32`, which was the light
   Primary of the retired Mira theme and by the end belonged to nothing in the application, while the
   tri-state radio a few hundred pixels away was already drawing the same Allow in Color.Success. Two
   greens for one concept, on one screen, is what reads as neglect.

   So: the tokens, darkened. The labels inside these boxes are white, and every one of the four tokens
   fails against white on its own — Success 2.24:1 light and 2.51:1 dark, Error 3.68:1 and 3.40:1 — which
   is exactly why the original values were hand-darkened rather than taken from the palette. Mixing 60%
   of the token with black keeps the hue, answers the "a Deny is not a failure" argument in the right
   currency (a quieter red, not a different one), and clears AA in both themes: the worst of the four is
   5.63:1. Toward black rather than toward the surface, because the light surface is white and mixing
   into it would lighten the very fills the white labels sit on. */
.permission-graph-host g.node .node-box {
    stroke-width: 1.6px;
    transition: fill 120ms ease, stroke 120ms ease;
}

.permission-graph-host g.node.opinion-allow .node-box { fill: color-mix(in srgb, var(--mud-palette-success) 60%, #000); }
.permission-graph-host g.node.opinion-deny .node-box { fill: color-mix(in srgb, var(--mud-palette-error) 60%, #000); }

/* Inherit is the absence of an opinion, so it takes the palette's neutral rather than a fourth hue:
   there is nothing being said, and nothing to say it in. `gray-darker` is one value in both themes,
   which suits a node that means the same thing in both — 6.19:1 under the white label. */
.permission-graph-host g.node.opinion-inherit .node-box { fill: var(--mud-palette-gray-darker); }

/* The verdict is a border, so it is read against the fill it surrounds and can afford the token at
   full strength. */
.permission-graph-host g.node.verdict-allow .node-box { stroke: var(--mud-palette-success); }
.permission-graph-host g.node.verdict-deny .node-box { stroke: var(--mud-palette-error); stroke-dasharray: 4 2.5; }

/* A pending change is announced before it is saved, or the batch is invisible until it is too late to
   reconsider it. */
.permission-graph-host g.node.pending .node-box {
    stroke-width: 3.2px;
    stroke-dasharray: none;
}

/* Selection is an outer ring rather than a border override, so it does not paint over the verdict or the
   staging marker — on the one node being looked at, of all of them. */
.permission-graph-host g.node.selected .node-box {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 3px;
}

.permission-graph-host g.node:focus-visible {
    outline: none;
}

.permission-graph-host g.node:focus-visible .node-box {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 3px;
}

.permission-graph-host g.node text {
    fill: #ffffff;
    font-family: Helvetica, Arial, sans-serif;
    pointer-events: none;
    user-select: none;
}

.permission-graph-host g.node {
    cursor: pointer;
}

/* Graphviz paints the edges after the nodes, so they sit on top and swallow clicks on any node they pass
   over — a node behind an edge simply cannot be selected. Nothing clicks an edge, so they take themselves
   out of the way. */
.permission-graph-host g.edge {
    pointer-events: none;
}

.permission-graph-host g.edge .edge-line,
.permission-graph-host g.edge .edge-head {
    stroke: var(--mud-palette-text-disabled);
}

/* Belt and braces with the `fill="none"` the module preserves: a connector is a line, and the SVG default
   for an unfilled path is black. A long curve that encloses area would otherwise render as a solid blob. */
.permission-graph-host g.edge .edge-line {
    fill: none;
}

.permission-graph-host g.edge .edge-head {
    fill: var(--mud-palette-text-disabled);
}

.permission-graph-host g.cluster path,
.permission-graph-host g.cluster polygon {
    stroke: var(--mud-palette-lines-default);
    fill: none;
}

.permission-graph-host g.cluster text {
    fill: var(--mud-palette-text-secondary);
    font-family: Helvetica, Arial, sans-serif;
}

/* A ghost is in the drawing but not yet part of the answer: it holds the position it will have once it is
   revealed, so revealing it moves nothing. Outline only and no label — enough to say something is there
   and to be clicked, not enough to be mistaken for a value the operator has seen. */
.permission-graph-host g.node.ghost .node-box {
    fill: transparent;
    stroke: var(--mud-palette-lines-default);
    stroke-width: 1px;
    stroke-dasharray: 2 3;
}

.permission-graph-host g.node.ghost text {
    display: none;
}

.permission-graph-host g.node.ghost:hover .node-box {
    stroke: var(--mud-palette-primary);
}

.permission-graph-host g.dimmed {
    opacity: 0.15;
}

.permission-graph-host g.edge.dimmed {
    opacity: 0.08;
}

@media (prefers-reduced-motion: reduce) {
    .permission-graph-host g.node .node-box {
        transition: none;
    }
}

/* The prerequisite lists inside the inspector's alerts. */
.permission-prerequisite-list {
    margin: 4px 0 0;
    padding-left: 20px;
    font-size: 0.8125rem;
}

/* Acquisition counters. Right-aligned and tabular so the digits line up as they climb: a column of
   numbers that shifts sideways while a run advances is harder to read than one that does not. */
.mth-import-count {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* The page toolbar.

   The band that opens every entity and admin page: breadcrumbs on the left, the button bar on the
   right. Its surface is the MudPaper around it, so the MudToolBar inside must not paint one of its
   own or the border would sit around a second, differently-coloured rectangle.

   These rules live here rather than in a scoped stylesheet for the same reason the grid toolbar's do:
   the element whose height has to change is MudBlazor's own toolbar, and ::deep only reaches
   downwards from the component root. */
.entity-toolbar .mud-toolbar {
    background-color: transparent;
    padding-top: 4px;
    padding-bottom: 4px;
}

/* The trail and the permalink, as one group. The group takes the width the crumbs used to take, so the
   copy control stays beside the path it names instead of drifting towards the buttons. */
.entity-toolbar-location {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: 1 1 auto;
    min-width: 0;
}

/* The crumbs give up their width before the buttons do: an action the operator cannot reach is worse
   than a path they have to read across two lines. */
.entity-toolbar-crumbs {
    flex: 0 1 auto;
    min-width: 0;
}

/* The permalink control is drawn small so the band does not grow a row taller than the trail it sits
   on, but §8 asks for a 44px target and a 30px icon is not one. The overlay gives it the target
   without giving it the box: the button keeps its size in the layout and answers to a finger over the
   whole 44 square. Its only neighbour is the last crumb, which is inert, so the few pixels of overlap
   cost nothing. */
.entity-toolbar-permalink {
    position: relative;
}

.entity-toolbar-permalink::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

/* The link in a grid's identifying cell. Deliberately not MudBlazor's default link: primary-blue and
   underlined would put a column of blue on every list page in the product and change what a grid looks
   like. It takes the colour of the row and reveals itself on hover, so what tells an operator this one
   is different is the pointer and the address in the status bar. */
.entity-row-link {
    font: inherit;
    color: inherit;
}

/* The anchor answers for the whole cell, and does it without taking the whole cell's box. The grid
   hands the identifying cell to the browser and stops handling clicks on it, so anything the anchor
   does not cover is dead — on a short value in a wide column, the middle of the cell is exactly where
   an operator aims and it sat outside the text. Widening the anchor instead was the first attempt and
   it cost the mobile layout: a block anchor consumes the row, so the value stopped lining up with
   every other value on the card. An overlay keeps the text where it was and makes the target a cell
   rather than a word. */
td:has(> .entity-row-link) {
    position: relative;
}

.entity-row-link::after {
    content: "";
    position: absolute;
    inset: 0;
}

/* The reference grids of a detail page — the systems of a model, the sites of a customer — lead to
   the record each row names, and the whole row is the target. It is still one anchor, in the cell
   that identifies the record, so the middle click, the Ctrl-click and "copy link address" work and
   nothing handles the click a second time; the overlay only widens it from the cell to the row. The
   other links a row carries (a telephone number, an e-mail, directions) are lifted above it. */
tr:has(.entity-row-anchor) {
    position: relative;
    cursor: pointer;
}

.entity-row-anchor {
    font: inherit;
    color: inherit;
}

.entity-row-anchor::after {
    content: "";
    position: absolute;
    inset: 0;
}

tr:has(.entity-row-anchor) a:not(.entity-row-anchor) {
    position: relative;
    z-index: 1;
}

/* Same reasoning as the grid's row link: a tree of primary-blue underlined labels would be a new
   visual language on a panel that is read far more often than it is clicked. */
.entity-tree-link {
    font: inherit;
    color: inherit;
}

/* Off-screen rather than hidden: the copy fallback has to select the text, and a display:none or
   hidden element cannot be selected. Fixed positioning keeps it from scrolling the page on focus. */
.mth-clipboard-shim {
    position: fixed;
    top: -1000px;
    left: -1000px;
    opacity: 0;
}

.entity-toolbar-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
}

/* Below sm the band becomes a column. A breadcrumb trail and three labelled buttons stop fitting side
   by side well before the drawer collapses, and stacking is the only arrangement that keeps the path
   readable and every button pressable at the 44px target size. */
@media (max-width: 599.98px) {
    .entity-toolbar .mud-toolbar {
        flex-direction: column;
        align-items: stretch;
    }

    .entity-toolbar-actions {
        justify-content: flex-end;
    }
}

/* The entity grid's toolbar.

   MudBlazor lays a table toolbar out as a single non-wrapping flex row of fixed height, so at 390px
   the title and the search field were drawn on top of one another, the search clipped to two
   characters. It held four children until the page band took New and "Delete selected" off it; the
   two that remain are the ones that describe the table rather than act on the entity. These rules
   live here rather than in a scoped stylesheet because the element that has to change height is
   MudBlazor's own toolbar, which is an ancestor of anything the component could scope to, and ::deep
   only reaches downwards. */
.entity-list-grid .mud-table-toolbar {
    height: auto;
    min-height: 64px;
    padding-top: 8px;
    padding-bottom: 8px;
}

.entity-grid-toolbar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 12px;
    width: 100%;
}

/* The search takes what is left over, down to a width where it still shows a word or two, and wraps
   to its own line rather than shrinking past that. */
.entity-grid-toolbar .entity-search {
    flex: 1 1 220px;
}

/* Below sm the row becomes a column: the title and a usable search box stop fitting side by side well
   before the drawer collapses, and stacking is the only arrangement that keeps both legible. */
@media (max-width: 599.98px) {
    .entity-grid-toolbar {
        flex-direction: column;
        align-items: stretch;
    }

    /* The basis above is a width, and flex-basis follows the main axis: left alone, the same
       declaration reserves 220px of HEIGHT once the container becomes a column, which drew the search
       field with a 200px hole under it. */
    .entity-grid-toolbar .entity-search {
        flex: 0 0 auto;
    }
}

/* The summary band's toggle.

   One copy of the tiles serves both layouts: the collapsed class is the only thing that hides them,
   and from sm up it stops applying, so the band is a band again with nothing duplicated. Not a native
   `details`: Chromium hides a closed details' body through `::details-content`, which a rule on the
   children cannot override, so the tiles stayed invisible on a wide page. */
.entity-summary-tiles-collapsed {
    display: none;
}

.entity-summary-toggle {
    width: 100%;
    font: inherit;
    color: inherit;
    text-align: left;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    cursor: pointer;
    padding: 12px 16px;
    margin-bottom: 16px;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: var(--mud-default-borderradius);
    background-color: var(--mud-palette-surface);

    /* The default triangle is replaced by the chevron, which is the affordance the rest of the
       application uses for the same gesture. */
    list-style: none;
}

.entity-summary-chevron.expanded {
    transform: rotate(180deg);
}

@media (min-width: 600px) {
    .entity-summary-toggle {
        display: none;
    }

    /* From sm up the collapse does not apply at all, whatever the toggle was left on. */
    .entity-summary-tiles-collapsed {
        display: flex;
    }
}

/* An address read as one value: its lines, and "navigate to" beside them. */
.postal-address {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
}

.postal-address-lines {
    min-width: 0;
}

/* The title of a dashboard widget. Stronger than the caption it used to be, at the customer's request:
   bold and a step larger, so a chart says what it is before the operator reads its axes. One class for
   every widget, so no title can drift from the others. */
.widget-title {
    font-size: 0.95rem;
    font-weight: 700;
    line-height: 1.4;
}

/* Fleet map pins (dashboard widget SystemsMap). The map package draws pins and clusters outside any
   component scope, so their colours live here; the map applies the class a status names
   (SystemStatusSeverity) to a pin, and to any cluster whose most severe member it is. The hues are
   the palette's semantic ones, the same EntityChipColors gives the status chip. */
.systems-map-pin--online {
    --sts-map-pin-color: var(--mud-palette-success);
}

.systems-map-pin--warning {
    --sts-map-pin-color: var(--mud-palette-warning);
}

.systems-map-pin--offline {
    --sts-map-pin-color: var(--mud-palette-error);
}

.systems-map-pin--unknown {
    --sts-map-pin-color: var(--mud-palette-text-disabled);
}
