/* ============================================================================
   shared.css — styling for the shared component library + Template A layout
   (ADR 0031 §4). Every value resolves through var(--…) from tokens.css; no themed
   literal appears here. Component CSS isolation (.razor.css) could host these, but a
   single sheet keeps the foundation legible while the library is small.
   ============================================================================ */

@import url('tokens.css');

/* ---- Popup layer (PopupLayer / PopupContent) --------------------------------
   The one level the app's own popups are drawn on, a child of the page rather than of the control
   that opened them. Everything here is placed in WINDOW coordinates, and an element only keeps the
   window as its containing block while nothing above it carries a transform, a filter, a
   perspective or CSS containment (container-type is the everyday one) — hence the level, high
   enough in the tree that there is nothing above it to shift the maths.

   So: NOTHING in these two rules may grow such a property, and neither may they take a z-index.
   With no stacking context of its own, each popup's own z-index still counts against the page, so
   a list opened inside a dialog stands above it, exactly as when it was drawn in place. Out of
   flow and zero-sized, the layer is a place in the DOM and never a box on the page. */
.ui-popup-layer {
    position: absolute;
    top: 0;
    left: 0;
    width: 0;
    height: 0;
}

/* One popup's slot, keyed by its control so two open at once never trade content. It generates no
   box either: what it holds is already fixed to the window. */
.ui-popup-slot {
    display: contents;
}

/* ---- Shared page masthead (PageHeader) -------------------------------------- */
/* Both scaffolds wear the same header: a permanent full-width band pinned to the top of
   the pane — title at the dense scale, one-line description, hairline divider — with the
   body (rail + content, or tree + detail) carried below it. */
.ui-ph { flex: 0 0 auto; }
.ui-ph .page-title { font-size: var(--font-size-page-title-dense); line-height: 1.15; }
.ui-ph .page-description { margin-top: var(--space-1); }
.ui-ph-divider { margin: var(--space-3) 0; }

/* ---- Template A page layout ------------------------------------------------- */
/* A column: the shared masthead on top, then a [rail | content] row. The "On this page"
   mini-rail sits on the left below the header — the same left-hand navigation side as
   Template B's tree and the AppShell rail. */
.ui-page {
    display: flex;
    flex-direction: column;
    height: 100vh;
    overflow: hidden;
    background: var(--color-bg);
    padding: var(--space-5) var(--space-7) var(--space-4);
}

.ui-page-body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
}

.ui-page-main {
    flex: 1 1 auto;
    min-width: 0;
    overflow-y: auto;
    position: relative;
}

.ui-page-content {
    max-width: var(--content-max-width);
    margin: 0 auto;
    padding: var(--space-6) var(--space-8) 96px;
}

/* A page that declares WideContent takes the pane's whole width: the form measure above is
   right for a column of fields and wrong for a list of sentences, which it would break over
   three lines each. */
.ui-page-content.wide {
    max-width: none;
}

.ui-scroll-section {
    scroll-margin-top: var(--space-6);
    margin-bottom: var(--space-8);
}

/* A section that opens a new rail group (the eyebrow-led one) gets generous separation,
   so the column's rhythm carries the GENERAL / DATA / SERVER … grouping the rail lists —
   not just the small eyebrow. Top margins collapse with the prior section's 32px bottom,
   so the larger value wins: ~56px between groups against 32px within one. */
.ui-scroll-section.is-group-start {
    margin-top: calc(var(--space-8) + var(--space-7));
}

/* …but the first section sits tight under the masthead, not pushed down by that gap. */
.ui-page-content > .ui-scroll-section:first-child {
    margin-top: 0;
}

.ui-section-eyebrow {
    margin: 0 0 var(--space-2) var(--space-1);
}

.ui-scroll-section .section-title {
    margin-bottom: var(--space-4);
}

/* ---- "On this page" mini-rail ----------------------------------------------- */
.ui-page-rail {
    flex: 0 0 var(--rail-width);
    width: var(--rail-width);
    border-right: 1px solid var(--color-stroke-subtle);
    padding: var(--space-6) var(--space-5) var(--space-8);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.ui-rail-group {
    margin: var(--space-4) 0 var(--space-2);
}

.ui-rail-group:first-child {
    margin-top: 0;
}

.ui-rail-item {
    display: block;
    font-size: var(--font-size-description);
    color: var(--color-text-secondary);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    border-left: 2px solid transparent;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ui-rail-item:hover {
    background: var(--color-overlay-hover);
    color: var(--color-text);
    text-decoration: none;
}

.ui-rail-item.active {
    color: var(--color-accent-text);
    border-left-color: var(--color-accent);
    font-weight: var(--font-weight-semibold);
}

/* ---- Cards ------------------------------------------------------------------ */
.ui-card {
    background: var(--color-surface);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-lg);
    padding: var(--space-5);
    box-shadow: var(--shadow-card);
}

.ui-card + .ui-card {
    margin-top: var(--space-4);
}

/* A card that presents itself RECESSED: the well tone and no lift, for a block that belongs to the
   card around it — a result the page produced, an aside the operator reads — rather than standing
   beside it as a peer. The named modifier exists so that intent lives in the stylesheet with the
   rest of the depth contract; a page must never reach for an inline surface override. */
.ui-card.ui-card-well {
    background: var(--color-surface-alt);
    box-shadow: none;
}

.ui-panel-card-head {
    margin-bottom: var(--space-4);
}

.ui-panel-card-head .field-description {
    margin-top: var(--space-1);
}

.ui-card.disabled,
.ui-card[aria-disabled='true'] {
    opacity: 0.55;
    pointer-events: none;
}

/* ---- Setting rows ----------------------------------------------------------- */
.ui-setting-row {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    padding: var(--space-3) 0;
}

.ui-setting-row + .ui-setting-row {
    border-top: 1px solid var(--color-stroke-subtle);
}

.ui-setting-row-text {
    flex: 1 1 auto;
    min-width: 0;
}

.ui-setting-row-text .field-description {
    margin-top: var(--space-1);
}

.ui-setting-row-control {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

/* ---- The legend of a field, and the effect of the value in force (FieldLegend) ----------------
   Two different sentences used to arrive under a field as the same grey line: what the field
   decides, and what the value chosen right now produces. Nothing told them apart, so an
   explanation would change under the operator the moment he chose. The legend keeps
   `.field-description` — it is the explanation, and it is fixed. What the value produces is drawn
   as another kind of thing entirely: pulled in behind a mark of its own, in the full ink of a
   reading rather than the quiet grey of prose. Not louder, just plainly not an explanation. */
.field-effect {
    display: flex;
    gap: var(--space-2);
    margin-left: var(--space-2);
    font-size: var(--font-size-description);
    line-height: 1.4;
    color: var(--color-text);
}

/* The mark: a rule as tall as whatever it is marking, so a sentence that wraps to three lines is
   still visibly the one line of effect and not the start of a paragraph. */
.field-effect::before {
    content: '';
    flex: 0 0 auto;
    width: 2px;
    border-radius: 1px;
    background: var(--color-accent);
}

/* ---- Inputs (text / textarea / select) -------------------------------------- */
.ui-input,
.ui-select,
.ui-textarea {
    font-family: var(--font-sans);
    font-size: var(--font-size-body);
    color: var(--color-text);
    background: var(--color-surface);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    padding: var(--space-3) var(--space-4);
    width: 100%;
    box-sizing: border-box;
}

/* Fluent reveal focus: the box border stays quiet and a 2px accent bar lights the bottom
   edge (an inset shadow, so the field never shifts by the 1px a thicker border would add).
   Selects and textareas inherit it. */
.ui-input:focus,
.ui-select:focus,
.ui-textarea:focus {
    outline: none;
    box-shadow: inset 0 -2px 0 0 var(--color-accent);
}

.ui-input:disabled,
.ui-select:disabled,
.ui-textarea:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.ui-input[readonly] {
    background: var(--color-surface-alt);
    color: var(--color-text-secondary);
}

.ui-textarea {
    resize: vertical;
    line-height: 1.4;
}

.ui-input.mono {
    font-family: var(--font-mono);
}

.ui-select {
    min-width: 200px;
    cursor: pointer;
}

/* ---- ComboBox (shared DOM-drawn single-select, ADR 0031 §3) ------------------
   The closed control reuses .ui-select's box; .ui-combobox lays out the value + chevron and the
   open list is a themed DOM popup layered above any stacking context (like the context menu),
   so the whole picker follows the theme on the CEF host and a remote browser — no native popup. */
.ui-combobox {
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    text-align: left;
    line-height: 1.3;
}

.ui-combobox-value {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-combobox-chevron {
    flex: 0 0 auto;
    color: var(--color-text-secondary);
    transition: transform var(--dur-quick) var(--ease-out);
}

.ui-combobox[aria-expanded='true'] .ui-combobox-chevron {
    transform: rotate(180deg);
}

/* The popup layer is always in the DOM (like the old <select>'s options); it only becomes
   visible + interactive while open. Scrim catches the outside click; the list is fixed + JS-placed.
   The layer is drawn on the page's shared popup level (PopupLayer), and it generates no box of its
   own there — only the scrim and the list, both taken out of flow. */
.ui-combobox-layer {
    display: none;
}

.ui-combobox-layer.open {
    display: contents;
}

.ui-combobox-scrim {
    position: fixed;
    inset: 0;
    z-index: var(--z-popover);
}

.ui-combobox-pop {
    position: fixed;
    z-index: var(--z-popover);
    margin: 0;
    padding: var(--space-1);
    list-style: none;
    max-height: min(320px, 60vh);
    overflow-y: auto;
    /* Wheeling past the last option must not scroll the panel behind the open list. */
    overscroll-behavior: contain;
    background: var(--color-surface);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-popover);
    font-family: var(--font-sans);
    font-size: var(--font-size-body);
    color: var(--color-text);
}

.ui-combobox-option {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    cursor: pointer;
    white-space: nowrap;
    color: inherit;
}

/* The value mark of a suggestion (what the point holds), between the tick and the label. It stays
   neutral: colour is reserved for semantics, and a type is not one. */
.ui-combobox-option-glyph {
    flex: 0 0 auto;
    font-size: 14px;
}

.ui-combobox-option-label {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
}

.ui-combobox-option:hover:not(.disabled) {
    background: var(--color-overlay-hover);
}

.ui-combobox-option.active:not(.disabled) {
    background: var(--color-accent-bg);
}

.ui-combobox-option.disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.ui-combobox-option[aria-selected='true'] {
    font-weight: var(--font-weight-medium);
}

.ui-combobox-tick {
    flex: 0 0 auto;
    width: 14px;
    text-align: center;
    color: var(--color-accent);
}

.ui-combobox-option[aria-selected='true'] .ui-combobox-tick::before {
    content: '\2713';  /* check mark */
}

/* ---- ChoiceMark — the drawing of an option, beside the name for it (ChoiceMark.razor) ------- */

/* The drawing an option produces, beside the name for it. The box is fixed and is its OWN query
   container: the two indicators hand it their runtime markup, whose geometry is written against the
   component's cell, and without a container of its own that geometry would resolve against the
   window and arrive the size of the screen. */
.choice-mark {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 20px;
    container-type: size;
    color: var(--color-text-secondary);
}

.choice-mark > svg {
    width: 100%;
    height: 100%;
    overflow: visible;
}

/* Only the padding a component keeps for its cell is taken back; everything that says what the
   drawing IS still comes from the one stylesheet that paints it on the canvas. */
.choice-mark .cmp-progress,
.choice-mark .cmp-led {
    padding: 0;
}

/* The Label's box, likewise: where the words sit is the component's own rule, resolved from the two
   custom properties LabelView writes. Only the type is taken down, and only here — at the size the
   canvas would give it a single line fills the mark, and top, middle and bottom would be one
   picture. The words are set in the interface's own ink, so a mark reads as the row it sits in. */
.choice-mark .cmp-label {
    --cmp-type: 8px;
    line-height: 1;
}

/* The traces are strokes and the solid shapes are fills — one rule each rather than an attribute
   per element, so a mark inherits the ink of the row it sits in. */
.choice-mark .mark-trace {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.choice-mark .mark-area {
    fill: currentColor;
    fill-opacity: 0.2;
    stroke: none;
}

.choice-mark .mark-bar,
.choice-mark .mark-legend > rect {
    fill: currentColor;
}

.choice-mark .mark-plot {
    fill: none;
    stroke: currentColor;
    stroke-width: 1;
    opacity: 0.5;
}

.choice-mark .mark-track {
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    opacity: 0.35;
}

.choice-mark .mark-fill {
    fill: none;
    stroke: var(--color-accent);
    stroke-width: 2;
    stroke-linecap: round;
}

.choice-mark .mark-needle {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
}

/* The mark riding the rim of an arc. Heavier than a needle and squared off, because a pointer that
   is a short segment has only its weight to say it is not a tick of the scale. */
.choice-mark .mark-pin {
    fill: none;
    stroke: currentColor;
    stroke-width: 2.4;
}

/* The measurement a drawing is a reduction OF — drawn under the line and quiet enough that the line
   is what is read. It is what makes a reduction legible at all: without the data behind it, "keeps
   the peaks" and "flattens them" are two curves with nothing to be true or false about. */
.choice-mark .mark-ghost {
    fill: none;
    stroke: currentColor;
    stroke-width: 1;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity: 0.22;
}

/* One drawn sample. It rides the line, so it is the line's ink and not the plot's. */
.choice-mark .mark-dot {
    fill: currentColor;
    opacity: 0.75;
}

/* What the drawing does rather than what it IS: a leader line, a handle over the plot, the hands of
   a clock, the origin an elapsed axis counts from. Never load-bearing on its own — every mark still
   reads with these removed. */
.choice-mark .mark-hint {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.2;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity: 0.7;
}

/* A ruler: the line an axis is drawn as, and the ticks along it. */
.choice-mark .mark-axis {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.1;
    stroke-linecap: round;
    opacity: 0.55;
}

/* An outlined shape that is not a plot — a key, a closed list, the round thing inside a picture that
   says whether the picture was stretched. */
.choice-mark .mark-outline {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.2;
    opacity: 0.8;
}

/* Something written: a row of a banner, a chip of a count, a reading over its bar, the number on a
   card. A mark never writes real text — at this size it would be a smear — so a written thing is the
   block of ink it occupies. */
.choice-mark .mark-chip {
    fill: currentColor;
    opacity: 0.7;
}

/* An area the drawing occupies without being the subject of it: the bed of a track, the body of a
   picture, the stretch of a navigator. */
.choice-mark .mark-pane {
    fill: currentColor;
    opacity: 0.18;
}

/* The part of a track the reading has reached — the same accent the dial's arc fills in, so a
   filled thing is one colour across the whole vocabulary. */
.choice-mark .mark-lit {
    fill: var(--color-accent);
}

/* A ring, and the slice taken out of it. The two share a width so the slice sits ON the ring rather
   than beside it, and butt caps are what keep a slice's ends radial. */
.choice-mark .mark-ring {
    fill: none;
    stroke: currentColor;
    stroke-width: 4.6;
    opacity: 0.28;
}

.choice-mark .mark-ring-slice {
    fill: none;
    stroke: currentColor;
    stroke-width: 4.6;
    opacity: 0.7;
}

/* The same slice out of a solid body. */
.choice-mark .mark-slice {
    fill: currentColor;
    stroke: none;
    opacity: 0.55;
}

/* A toolbox button is drawn at the size it is drawn on the bar, so the row shows the key and not an
   enlargement of it. */
.choice-mark-tool {
    width: 20px;
    height: 16px;
}

/* A profile segment rides a grid cell rather than a property rail, so it is drawn narrow: the three
   shapes are one plateau wide and stay legible with the name still fitting beside them. */
.choice-mark-profile {
    width: 30px;
}

/* A page is taller than it is wide, so the sheet mark is drawn narrow rather than floating in a box
   sized for a plot. */
.choice-mark-sheet {
    width: 24px;
}

/* ---- Buttons ----------------------------------------------------------------
   Every variant paints its OWN opaque fill and no hover gives that up: a translucent tint goes on
   background-image, layered over the fill, never on background-color. On background-color it would
   replace the fill, and whatever sits behind the button — a caution banner, a flagged row — would
   show through and recolour it. Layering keeps hover colour part of the vocabulary instead of an
   accident of the container the button was dropped in. */
.ui-btn {
    font-family: var(--font-sans);
    font-size: var(--font-size-body);
    font-weight: var(--font-weight-medium);
    padding: var(--space-3) var(--space-5);
    border-radius: var(--radius-md);
    border: 1px solid var(--color-stroke);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    white-space: nowrap;
    transition: transform var(--dur-press) var(--ease-out);
}

.ui-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Press feedback — a small dip confirms the click landed (never below 0.95). */
.ui-btn:active:not(:disabled),
.ui-icon-btn:active:not(:disabled),
.ui-toolbar-btn:active:not(:disabled) {
    transform: scale(0.97);
}

.ui-btn-default {
    background-color: var(--color-surface);
    color: var(--color-text);
}

.ui-btn-default:hover:not(:disabled) {
    background-image: linear-gradient(var(--color-overlay-hover), var(--color-overlay-hover));
}

.ui-btn-accent {
    background: var(--color-accent);
    color: var(--color-on-accent);
    border-color: var(--color-accent);
}

.ui-btn-accent:hover:not(:disabled) {
    background: var(--color-accent-hover);
    border-color: var(--color-accent-hover);
}

.ui-btn-danger {
    background: var(--color-critical-fill);
    color: var(--color-text-on-accent);
    border-color: var(--color-critical-fill);
}

/* Darken on hover (never lighten): a lighter red fill would push white below 4.5:1. */
.ui-btn-danger:hover:not(:disabled) {
    filter: brightness(0.92);
}

/* The outline variant: the card surface with a neutral stroke, so only the red label and the hover
   tint mark it as destructive. */
.ui-btn-danger-subtle {
    background-color: var(--color-surface);
    color: var(--color-critical);
    border-color: var(--color-stroke);
}

.ui-btn-danger-subtle:hover:not(:disabled) {
    background-image: linear-gradient(var(--color-critical-bg), var(--color-critical-bg));
    border-color: var(--color-critical);
}

/* ---- Switch ----------------------------------------------------------------- */
.ui-switch {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    cursor: pointer;
    user-select: none;
}

.ui-switch input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.ui-switch-track {
    width: 40px;
    height: 22px;
    border-radius: var(--radius-pill);
    background: var(--color-neutral);
    position: relative;
    transition: background var(--dur-quick) var(--ease-out);
    flex: 0 0 auto;
}

.ui-switch.on .ui-switch-track {
    background: var(--color-accent);
}

.ui-switch-knob {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--color-surface);
    transition: transform var(--dur-quick) var(--ease-out);
}

.ui-switch.on .ui-switch-knob {
    transform: translateX(18px);
}

/* Keyboard focus shows on the track: the real <input> is visually hidden (0×0), so the
   global :focus-visible ring would have nothing to paint. */
.ui-switch input:focus-visible + .ui-switch-track {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: var(--focus-ring-offset);
}

.ui-switch.disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.ui-switch-label {
    font-size: var(--font-size-body);
    color: var(--color-text);
}

/* The same switch standing on a command bar (ToolbarSwitch): it keeps its own drawing and takes the
   bar's label size, so the state control reads as one row with the verbs beside it. */
.ui-toolbar-switch { display: inline-flex; align-items: center; padding: 0 var(--space-2); }
.ui-toolbar-switch .ui-switch-label { font-size: var(--font-size-description); }

/* ---- Password field --------------------------------------------------------- */
.ui-password {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    width: 100%;
}

.ui-password-reveal {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--font-size-description);
    color: var(--color-text-secondary);
    cursor: pointer;
    user-select: none;
}

/* ---- Radio group ------------------------------------------------------------ */
.ui-radio-group {
    display: flex;
    gap: var(--space-7);
    flex-wrap: wrap;
}

.ui-radio {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
    font-size: var(--font-size-body);
    color: var(--color-text);
}

.ui-radio input:disabled + span,
.ui-radio.disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

/* ---- Hyperlink (warnings count) ---------------------------------------------
   A button that reads as a word in the sentence around it: it keeps the inherited type
   (base.css) and gives up every other button affordance. */
.ui-link {
    color: var(--color-accent-text);
    cursor: pointer;
    text-decoration: underline;
    background: none;
    border: none;
    padding: 0;
}

/* ---- State dot -------------------------------------------------------------- */
.ui-state-dot {
    display: inline-block;
    border-radius: 50%;
    flex: 0 0 auto;
    background: var(--color-neutral);
    vertical-align: middle;
}

.ui-state-dot.ui-state-success { background: var(--color-success); }
.ui-state-dot.ui-state-caution { background: var(--color-caution); }
.ui-state-dot.ui-state-critical { background: var(--color-critical); }
.ui-state-dot.ui-state-attention { background: var(--color-accent); }
.ui-state-dot.ui-state-neutral { background: var(--color-neutral); }

/* ---- Banner ----------------------------------------------------------------- */
/* Almost every notice in the app is an observation of status, so the banner reads as one:
   the neutral recessed surface, and a thin rule down the left edge as the only mark of
   the tone. Strong ink is spent on the few conditions that carry a consequence, and
   those ask for it by name (.ui-banner-alarm) — a screen the operator leaves open all
   day must not be a wall of coloured boxes. The rule is drawn as an inset shadow so it
   clips with the radius instead of bending around the corners, the same way the client
   rows in Settings draw theirs. */
.ui-banner {
    display: flex;
    align-items: flex-start;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    background: var(--color-surface-alt);
    border: 1px solid var(--color-stroke-subtle);
    box-shadow: inset 3px 0 0 var(--color-neutral);
    margin-bottom: var(--space-4);
}

.ui-banner-text { flex: 1 1 auto; min-width: 0; }

/* The title sits in the body's own weight: the rule already says what kind of notice this
   is, so the heading only has to be a heading. Size and ink separate it from the body. */
.ui-banner-title {
    font-size: var(--font-size-body);
    font-weight: var(--font-weight-regular);
    color: var(--color-text);
    text-wrap: pretty;
}

.ui-banner-text .field-description { margin-top: var(--space-1); }
.ui-banner-action { flex: 0 0 auto; }

.ui-banner-caution { box-shadow: inset 3px 0 0 var(--color-caution); }
.ui-banner-critical { box-shadow: inset 3px 0 0 var(--color-critical); }
.ui-banner-success { box-shadow: inset 3px 0 0 var(--color-success); }
.ui-banner-info { box-shadow: inset 3px 0 0 var(--color-neutral); }

/* The declared loud treatment, applied by name and never by severity: a condition whose
   consequence is already running — a live external stop that ends runs, or a licence
   state that is taking the station down to a halt. It keeps the full semantic field, and
   its title goes back to a heading's weight. A caution that merely reports status must
   never reach here; that is what the rule above is for. */
.ui-banner-alarm {
    background: var(--color-caution-bg);
    border-color: var(--color-caution);
    box-shadow: none;
}

.ui-banner-alarm.ui-banner-critical {
    background: var(--color-critical-bg);
    border-color: var(--color-critical);
}

.ui-banner-alarm .ui-banner-title { font-weight: var(--font-weight-semibold); }

/* RegionBoundary — the per-region render parachute. The fallback keeps the failed region's
   footprint calm and scrollable instead of inheriting whatever half-drawn box the failure left;
   the Banner inside carries the tone. */
.ui-region-fault {
    padding: var(--space-4);
    min-width: 0;
    overflow: auto;
}

/* The Settings Legal section's inline document reader: the offline plain-text projection,
   scrollable and selectable, in the quiet description ink. */
.as-legal-document {
    margin-top: var(--space-2);
    max-height: 420px;
    overflow: auto;
    white-space: pre-wrap;
    user-select: text;
}

/* Station operation mode: one compact, shared notice above each engineering work area.
   The read-only wrapper preserves layout while native inert blocks custom and native controls. */
.engineering-lock-notice {
    flex: 0 0 auto;
    padding-block: var(--space-3);
}

.engineering-readonly-surface {
    display: contents;
}

/* Held read-only, a control reads as unavailable wherever the lock reached it: inside the wrapped
   surface, and inside the master-detail card's pinned header, which sits in a band of its own above
   the body and so wears the attributes on itself rather than under a wrapper. */
:is(.engineering-readonly-surface, .ui-panel-head)[aria-disabled="true"] :is(input, select, textarea, button, .ui-input, .ui-btn, .ui-toolbar-btn) {
    opacity: var(--opacity-disabled);
    cursor: not-allowed;
}

/* ---- Confirm dialog --------------------------------------------------------- */
.ui-modal-scrim {
    position: fixed;
    inset: 0;
    background: var(--color-scrim);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: var(--z-scrim);
}

.ui-modal {
    width: min(420px, 92vw);
    background: var(--color-surface);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-popover);
    padding: var(--space-6);
}

.ui-modal-title { margin-bottom: var(--space-3); }
.ui-modal-body { margin-bottom: var(--space-6); white-space: pre-line; }

.ui-modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-3);
}

/* Generic Dialog (the transient editor modals): a wider variant + a scrollable body +
   a footer action row. */
.ui-modal-wide { width: min(560px, 94vw); }

.ui-modal-content { margin-bottom: var(--space-5); }
.ui-modal-content.scroll { max-height: 60vh; overflow-y: auto; padding-right: var(--space-2); }

.ui-modal-footer {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-stroke-subtle);
}

/* ---- Local-user sign-in (ADR 0029) — the station instrument face shared by the view login and
   the title-bar switch-user dialog (SignInPanel + PinPad). One depth strategy: surface shift +
   inset (the readout well is recessed; keys are the surface + a hairline; no key shadows). ----- */

/* Signed-in header (switch-user dialog only): who holds the station now + sign out. */
.signin-current {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding-bottom: var(--space-3);
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--color-stroke-subtle);
}
.signin-current-name { font-size: var(--font-size-body); color: var(--color-text-secondary); }

/* Operator tag — a two-letter engraved token standing in for an avatar. */
.signin-token {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: var(--radius-md);
    background: var(--color-surface-alt);
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0.02em;
}
.signin-token-accent { background: var(--color-accent); color: var(--color-on-accent); }

/* Operator roster (the list step) — a shift board: one row per local user. */
.signin-roster { display: flex; flex-direction: column; gap: var(--space-1); }

.signin-roster-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    text-align: left;
    padding: var(--space-2) var(--space-3);
    background: transparent;
    border: none;
    border-radius: var(--radius-md);
    cursor: pointer;
    color: var(--color-text);
    transition: background var(--dur-quick) var(--ease-out);
}
.signin-roster-row:hover { background: var(--color-overlay-hover); }
.signin-roster-name {
    flex: 1;
    min-width: 0;
    font-size: var(--font-size-body);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.signin-roster-afford { flex: 0 0 auto; color: var(--color-text-disabled); }

.signin-error { color: var(--color-critical); font-size: var(--font-size-small); }

/* The offline recovery door, under the keypad and on the primary administrator only. It is the way
   out of a forgotten PIN, not a second way in, so it reads as a quiet link rather than a button
   competing with the keypad's own confirm. */
.signin-recovery-link { align-self: flex-start; font-size: var(--font-size-small); }

/* PIN step — the selected operator, then the keypad. */
.signin-pin { display: flex; flex-direction: column; gap: var(--space-3); }

.signin-op-row { display: flex; align-items: center; gap: var(--space-3); }
.signin-op-name {
    flex: 1;
    min-width: 0;
    font-size: var(--font-size-body);
    font-weight: var(--font-weight-medium);
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.signin-back {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--font-size-description);
    color: var(--color-text-secondary);
    transition: color var(--dur-quick) var(--ease-out);
}
.signin-back:hover { color: var(--color-text); }

/* ---- PinPad — the panel-mount PIN keypad (ADR 0029). ---------------------------------------- */
.pinpad { display: flex; flex-direction: column; gap: var(--space-3); outline: none; }

/* Masked readout — a recessed well: nothing is typed into it directly, so it takes the well tone
   the rest of the app's read-only fields take, one step under the card and the keys in both themes.
   One dot per entered digit, never the digits themselves. Clicking it focuses the pad, and the pad's
   focus lights an accent ring + a blinking caret so it reads as an active field. */
.pinpad-readout {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    padding: 0 var(--space-4);
    cursor: text;
    background: var(--color-surface-alt);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-inset-well);
    transition: border-color var(--dur-quick) var(--ease-out), box-shadow var(--dur-quick) var(--ease-out);
}
.pinpad:focus-within .pinpad-readout {
    border-color: var(--color-accent);
    box-shadow: var(--shadow-inset-well), 0 0 0 3px color-mix(in srgb, var(--color-accent) 20%, transparent);
}
.pinpad:focus-within .pinpad-readout-hint { display: none; }

.pinpad-readout-hint { font-size: var(--font-size-body); color: var(--color-text-disabled); }
.pinpad-dot {
    flex: 0 0 auto;
    width: 9px;
    height: 9px;
    border-radius: var(--radius-pill);
    background: var(--color-text);
}

/* Caret — the insertion cue; only shown (and blinking) while the pad holds focus. */
.pinpad-caret {
    flex: 0 0 auto;
    width: 2px;
    height: 20px;
    border-radius: var(--radius-pill);
    background: var(--color-accent);
    visibility: hidden;
}
.pinpad:focus-within .pinpad-caret {
    visibility: visible;
    animation: pinpad-blink 1.1s steps(1, end) infinite;
}
@keyframes pinpad-blink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
    .pinpad:focus-within .pinpad-caret { animation: none; }   /* steady caret still marks the spot */
}

/* Keypad — a 3-column panel of flat keys; the ✓ key is the accent commit. */
.pinpad-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-2); }

.pinpad-key {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 46px;
    background: var(--color-surface);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: var(--font-size-readout);
    color: var(--color-text);
    transition: background var(--dur-quick) var(--ease-out), transform var(--dur-press) var(--ease-out);
}
.pinpad-key:hover:not(:disabled) { background: var(--color-overlay-hover); }
.pinpad-key:active:not(:disabled) { transform: scale(0.97); }
.pinpad-key:disabled { opacity: 0.4; cursor: default; }
.pinpad-key-util { color: var(--color-text-secondary); }

/* Confirm key — a quiet accent-tinted key with an accent ✓ glyph (readable both themes); the commit
   is signalled by colour, not a loud fill. */
.pinpad-key-go {
    background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface));
    border-color: color-mix(in srgb, var(--color-accent) 30%, var(--color-stroke));
}
.pinpad-key-go:hover:not(:disabled) {
    background: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface));
}

/* Crash-consent payload (ADR 0028): a header bar with a Copy button sits above the read-only
   crash-detail box so the operator can lift the whole report to search it or send it to support.
   The bar carries the top spacing; the box tucks right under it. */
.ui-crash-log {
    margin-top: var(--space-4);
}

.ui-crash-logbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

.ui-crash-logbar-label {
    font-size: var(--font-size-description);
    color: var(--color-text-secondary);
}

/* Inside the bar+box group the box hugs the bar, so it overrides the standalone top margin. */
.ui-crash-log .ui-crash-detail {
    margin-top: var(--space-2);
}

/* Crash-consent payload (ADR 0028): the read-only monospace crash detail box inside the web
   consent modal. It is a
   <pre>, so zero the browser's default 1em block margin (the surrounding spacing is
   token-driven via margin-top) and cap the height so a long log scrolls inside this
   evidence box rather than pushing the footer out of the modal. */
.ui-crash-detail {
    margin: var(--space-4) 0 0;
    padding: var(--space-3);
    border: 1px solid var(--color-stroke-subtle);
    border-radius: var(--radius-sm);
    background: var(--color-surface-alt);
    font-family: var(--font-mono);
    font-size: var(--font-size-description);
    line-height: 1.45;
    color: var(--color-text-secondary);
    white-space: pre;
    overflow: auto;
    max-height: 280px;
    overscroll-behavior: contain;
}

/* ---- QR --------------------------------------------------------------------- */
.ui-qr {
    image-rendering: pixelated;
    border-radius: var(--radius-sm);
    background: var(--color-qr); /* QR must stay black-on-white to scan; not a themed surface */
}

/* ---- Sessions table (Clients) ----------------------------------------------- */
.ui-sessions-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--font-size-description);
    max-height: 240px;
}

.ui-sessions-table th,
.ui-sessions-table td {
    text-align: left;
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--color-stroke-subtle);
}

.ui-sessions-table th {
    font-weight: var(--font-weight-semibold);
    color: var(--color-text-secondary);
}

.ui-sessions-scroll {
    max-height: 240px;
    overflow-y: auto;
}

/* ---- Misc layout helpers used by the Settings page -------------------------- */
.ui-grid-2 {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-5);
}

.ui-grid-3 {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    gap: var(--space-5);
}

.ui-field-stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
}

/* Fixed-width field utilities. min-width:0 lets the fixed width win over a control's own
   min-width (e.g. .ui-select's 200px) inside a flex field. One
   scale shared by every page. */
.w-50  { width: 50px;  min-width: 0; }
.w-60  { width: 60px;  min-width: 0; }
.w-70  { width: 70px;  min-width: 0; }
.w-80  { width: 80px;  min-width: 0; }
.w-90  { width: 90px;  min-width: 0; }
.w-110 { width: 110px; min-width: 0; }
.w-120 { width: 120px; min-width: 0; }
.w-130 { width: 130px; min-width: 0; }
.w-140 { width: 140px; min-width: 0; }
.w-150 { width: 150px; min-width: 0; }
.w-160 { width: 160px; min-width: 0; }
.w-170 { width: 170px; min-width: 0; }
.w-180 { width: 180px; min-width: 0; }
.w-200 { width: 200px; min-width: 0; }
.w-240 { width: 240px; min-width: 0; }

/* The titleless member of the banner family — an inline note beside the field or control
   it judges (validation hints, limit-fault messages). Same neutral surface and same thin
   rule, so a Logic panel and a Process panel say the same thing the same way. Shared
   across pages; carries a default top margin, callers add their own spacing if needed. */
.ui-note-caution {
    margin-top: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    background: var(--color-surface-alt);
    border: 1px solid var(--color-stroke-subtle);
    box-shadow: inset 3px 0 0 var(--color-caution);
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
}

/* The counterpart of the note above: a standing statement about how the area works, true whatever
   the screen currently shows and never turned off. It carries no severity ink for that reason — a
   neutral well behind a grey rule, so it reads as the page's own voice rather than as something
   that just happened. Shared across pages; the vertical margin is part of the well, so callers add
   no spacing of their own. An opening <strong> lead is optional and takes the full text ink. */
.ui-principle {
    margin: var(--space-5) 0;
    padding: var(--space-3) var(--space-4);
    border-left: 3px solid var(--color-stroke);
    background: var(--color-surface-alt);
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
    line-height: 1.5;
    text-wrap: pretty;
}

.ui-principle strong {
    color: var(--color-text);
    font-weight: var(--font-weight-semibold);
}

/* Closing a card, the well leans on that card's padding instead of doubling it. */
.ui-principle:last-child { margin-bottom: 0; }

.ui-inline {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.ui-row-end {
    margin-left: auto;
}

.ui-row-grow {
    flex: 1 1 auto;
    min-width: 0;
}

.ui-empty-text {
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
}

/* Centered empty-state for page panels (no selection / no items). The caption reuses
   the shared .ui-empty-text, capped narrower when it sits inside the state. Shared by
   every page. */
.ui-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-4);
    padding: var(--space-8) var(--space-5);
    text-align: center;
    color: var(--color-text-secondary);
}
.ui-empty-state .ui-glyph { color: var(--color-text-disabled); }
.ui-empty-state .ui-empty-text { max-width: 420px; }

/* ---- Asset library grid (AssetCardGrid / AssetOriginFilter) -----------------
   The one paged card grid every asset library browses through — the Logic categories and the
   Dashboard editor's symbol picker alike, which is why it belongs to the shared library rather
   than to either page sheet. A card leads with the thing itself, so scanning is visual and a
   screen holds many more entries than a row list does; only the paged slice is ever built, which
   is what keeps a catalog of thousands cheap to browse. A library sets its card width through
   --ui-asset-card-min and adds only the marks its own cards wear. */
.ui-asset-filters { display: flex; align-items: flex-end; flex-wrap: wrap; gap: var(--space-3); margin-bottom: var(--space-3); }
.ui-asset-filter {
    display: flex;
    min-width: 0;
    flex-direction: column;
    gap: var(--space-1);
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
    font-weight: var(--font-weight-medium);
}
.ui-asset-filter .ui-input, .ui-asset-filter .ui-select { width: 100%; min-width: 0; }
.ui-asset-filter-search { flex: 1 1 220px; }
.ui-asset-filter-origin { flex: 0 0 auto; min-width: 140px; }
.ui-asset-results { display: flex; justify-content: space-between; min-height: 24px; margin-bottom: var(--space-2); color: var(--color-text-secondary); font-size: var(--font-size-description); }
.ui-asset-range { flex: 0 0 auto; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.ui-asset-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--ui-asset-card-min, 168px)), 1fr));
    gap: var(--space-3);
    min-width: 0;
}
.ui-asset-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
    padding: var(--space-3);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    text-align: left;
    cursor: pointer;
}
@media (hover: hover) {
    .ui-asset-card:hover { border-color: var(--color-text-disabled); background: var(--color-overlay-hover); }
}
.ui-asset-card.selected { border-color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }
.ui-asset-card:focus-visible { outline: var(--focus-ring-width) solid var(--color-accent); outline-offset: var(--focus-ring-offset); }
.ui-asset-card-figure {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    aspect-ratio: 4 / 3;
    min-width: 0;
    overflow: hidden;
    border-radius: var(--radius-sm);
    background: var(--color-surface-alt);
    color: var(--color-text);
    pointer-events: none;
}
.ui-asset-card-figure img { max-width: 100%; max-height: 100%; object-fit: contain; }
.ui-asset-card-name {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    min-width: 0;
    min-height: 2.6em;
    overflow: hidden;
    overflow-wrap: anywhere;
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-semibold);
    line-height: 1.3;
}
.ui-asset-card-meta {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    min-width: 0;
    color: var(--color-text-secondary);
    font-size: var(--font-size-eyebrow);
}
.ui-asset-card-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-asset-card-origin { flex: 0 0 auto; }
.ui-asset-pagination { display: flex; align-items: center; justify-content: center; gap: var(--space-4); margin-top: var(--space-4); color: var(--color-text-secondary); font-size: var(--font-size-description); }
/* Touch: the card and the page buttons keep a finger-sized target. */
@media (hover: none) {
    .ui-asset-card, .ui-asset-pagination .ui-btn { min-height: 44px; }
}

/* ---- Collection layout (CollectionLayout) -----------------------------------
   A list of one kind of thing and the row picked in it, side by side: the collection column,
   the inspector column beside it in one proportion for every page, and the editor that takes the
   collection's column while the inspector stays. The list deliberately gets more room than the
   inspector, and the inspector keeps its place as the selection moves, so nothing below the
   list reflows under the operator's eyes. Where the page gives the layout a height to fill,
   each column scrolls on its own; where it does not, both grow with their content and the
   panel scrolls as one. */
.ui-collection { display: grid; --collection-list: minmax(0, 3fr); --collection-inspector: minmax(280px, 2fr); grid-template-columns: var(--collection-list) 11px var(--collection-inspector); gap: var(--space-5) var(--space-4); flex: 1 1 auto; min-width: 0; min-height: 0; }
/* One declared proportion, stated here because how wide the two columns stand is the layout's own
   answer and not a page's: the list over the inspector 3:2, both growing with the window, and no
   fixed pixel column anywhere. The 11px middle track is the divider the operator drags where the
   proportion does not fit: the script (master-detail.js) puts the dragged width on
   --collection-inspector-width and marks the element inspector-sized, and only then does the fixed
   track take over — without a width the grid stays purely proportional. Only the tracks move, so
   the narrow rules below, which set the template outright, stack both modes and ignore a kept
   width. */
.ui-collection.inspector-sized { --collection-list: minmax(0, 1fr); --collection-inspector: minmax(280px, var(--collection-inspector-width, 2fr)); }
.ui-collection-list { min-width: 0; min-height: 0; overflow-y: auto; }
/* The divider between the columns — the same dressing as the tree splitter: a hairline at rest
   that thickens into a 3px accent bar on hover/drag (the thicken rides transform, so nothing
   relayouts). Its hairline is the layout's one rule between the columns, so the inspector draws
   no border of its own beside it. */
.ui-collection-splitter {
    position: relative;
    cursor: col-resize;
    display: flex;
    align-items: stretch;
    justify-content: center;
}
.ui-collection-splitter-line {
    width: 3px;
    background: var(--color-stroke);
    transform: scaleX(0.3333);
    transition: transform var(--dur-quick) var(--ease-out), background var(--dur-quick) var(--ease-out);
}
.ui-collection-splitter:hover .ui-collection-splitter-line,
.ui-collection-splitter:focus-visible .ui-collection-splitter-line,
.ui-collection-splitter.dragging .ui-collection-splitter-line {
    transform: scaleX(1);
    background: var(--color-accent);
}
/* One inspector column everywhere, with its own scroll; the hairline that tells it apart from the
   list beside it is the splitter's. */
.ui-collection-inspector {
    min-width: 0;
    min-height: 0;
    overflow-y: auto;
}
.ui-collection-no-selection { min-height: 280px; }

/* The column names the row it is showing, and carries whatever verb acts on that row's own list. */
.ui-collection-inspector-head {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--space-3); margin-bottom: var(--space-4);
}
.ui-inspector-title {
    font-size: var(--font-size-card-title);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text);
    min-width: 0;
    overflow-wrap: anywhere;
}

/* What a column holds: every field of the picked row, each one over the short thing that helps
   decide it — a projected value, an example, a mark of what the option draws. The stack is the
   shared shape so the next screen is born with it instead of inventing a fifth. */
.ui-inspector { display: flex; min-width: 0; flex-direction: column; gap: var(--space-4); }
.ui-inspector-field { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
.ui-inspector-field > .field-description { margin-top: calc(var(--space-1) * -1); }
/* The effect of the value belongs to the legend over it, so it closes the same gap the legend
   closed against the control: the pair reads as one thing said about this field. */
.ui-inspector-field > .field-effect { margin-top: calc(var(--space-1) * -1); }
/* Two fields answering one question (a window's two ends, a band's two sides) stand together over
   the single line that says what they let through. */
.ui-inspector-pair { display: flex; flex-wrap: wrap; gap: var(--space-4); min-width: 0; }
/* An eyebrow parting one group of fields from the next takes its own line and its own air. */
.ui-inspector + .eyebrow { display: block; margin-top: var(--space-5); margin-bottom: var(--space-3); }
/* Create/edit mode has one job and one content column. A page either hands it the whole card or
   stands it in the list's column, where it scrolls on its own height the way that column does. */
.ui-collection-editor { min-width: 0; padding: var(--space-5); border: 1px solid var(--color-stroke); border-radius: var(--radius-lg); background: var(--color-surface); }
.ui-collection > .ui-collection-editor { min-height: 0; overflow-y: auto; }

/* Narrow: one column, the divider folds away with the tracks it parted (setting the template
   outright is also what keeps a kept width from applying here), and the hairline becomes the rule
   over the second column. Stacked, side-by-side scrollers would be two short windows on top of
   each other, so the columns hand their overflow back and whatever contains them is the single
   scroller again.
   Two questions reach the same answer, and CSS cannot ask them as one. A page that declares its
   pane a query container is answered on the pane's own width, which is what a library browsing
   inside a folded tree needs; a page that lays the layout in its flow declares no container, so
   only the window is left to read. The window threshold stays low enough that a pane inside a
   window that narrow has already answered yes on its own width (the chrome around a pane is worth
   more than the 120px between the two), so no page can stack by one question and not the other. */
@container (max-width: 840px) {
    .ui-collection { grid-template-columns: 1fr; }
    .ui-collection > .ui-collection-splitter { display: none; }
    .ui-collection-inspector { padding-top: var(--space-5); border-top: 1px solid var(--color-stroke); }
    .ui-collection-list,
    .ui-collection-inspector,
    .ui-collection > .ui-collection-editor { min-height: auto; overflow: visible; }
}

@media (max-width: 960px) {
    .ui-collection { grid-template-columns: 1fr; }
    .ui-collection > .ui-collection-splitter { display: none; }
    .ui-collection-inspector { padding-top: var(--space-5); border-top: 1px solid var(--color-stroke); }
    .ui-collection-list,
    .ui-collection-inspector,
    .ui-collection > .ui-collection-editor { min-height: auto; overflow: visible; }
}

/* ---- Split model screen (StackedAreas) -----------------------------------
   Two halves of one subject and the settings of what is picked in them: on the left the
   model-level definition over the per-unit part, each a boxed area — the well-pane outline the
   workspace tree wears, one declaration further down (.ui-md-tree) — and on the right one settings
   column the height of the page, in the same box. The 11px tracks are the master-detail dividers:
   the stack one turned on its side (the script puts the dragged height on --stack-top-height and
   marks the stack top-sized), and the collection one between the areas and the column (the dragged
   width lands on --collection-inspector-width and marks the split inspector-sized — the very
   contract .ui-collection states above, reused so one script serves both geometries). Without a
   kept size the areas share the stack evenly and the columns keep the collection's declared
   proportion. Both areas keep a floor tall enough for a head and a few rows (the script clamps the
   drag to the same floors), so neither half can be dragged or squeezed away; the split
   deliberately keeps their sum as its minimum height, so a viewport too short for both hands the
   overflow to the detail body, which scrolls the screen as a whole instead of collapsing an area
   under its floor. */
.ui-split {
    --collection-list: minmax(0, 3fr);
    --collection-inspector: minmax(280px, 2fr);
    --stack-top-floor: 200px;
    --stack-bottom-floor: 240px;
    display: grid;
    grid-template-columns: var(--collection-list) 11px var(--collection-inspector);
    /* Basis zero, not auto: sized from the body's share and never from what the areas hold, which
       is the whole point — content taller than an area scrolls inside it. The explicit floor is
       what keeps the short-viewport fallback: a body too short for both floors overflows and
       scrolls the screen whole instead of squeezing an area away. */
    flex: 1 1 0;
    min-width: 0;
    min-height: calc(var(--stack-top-floor) + var(--stack-bottom-floor) + 11px);
}
.ui-split.inspector-sized { --collection-list: minmax(0, 1fr); --collection-inspector: minmax(280px, var(--collection-inspector-width, 2fr)); }

/* The two areas over each other, parted by the row the divider rides in. The floors are inherited
   from the split above, where the whole screen's minimum is computed from them. */
.ui-stack {
    --stack-top: minmax(var(--stack-top-floor), 1fr);
    display: grid;
    grid-template-rows: var(--stack-top) 11px minmax(var(--stack-bottom-floor), 1fr);
    min-width: 0;
    min-height: 0;
}
.ui-stack.top-sized { --stack-top: minmax(var(--stack-top-floor), var(--stack-top-height, 1fr)); }

/* An area is a box with a head band over a scrolling body; the settings column is the third wearer
   of the same box (the shared well-pane rule carries the outline and the flex column). What is
   added here is only that a grid item must be allowed to shrink under its content. */
.ui-stack-area,
.ui-split-settings { min-height: 0; }

/* The band an area or the settings column stands its head in: pinned over the body, ruled off from
   it the same way the tree pane's footer rules itself off from the list. */
.ui-stack-area-head,
.ui-split-settings-head {
    flex: 0 0 auto;
    min-width: 0;
    padding-bottom: var(--space-2);
    margin-bottom: var(--space-3);
    border-bottom: 1px solid var(--color-stroke-subtle);
}

/* The settings head is a row: what the column is, and whatever verb acts on the list it holds —
   the same pair the collection inspector heads itself with. On the left the two lines stack: the
   fixed word naming the area, which stands there with nothing picked, over the line saying where
   the picked item came from and which item it is. */
.ui-split-settings-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
}
.ui-split-settings-name {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}
.ui-split-settings-origin {
    font-size: var(--font-size-description);
    color: var(--color-text-secondary);
    min-width: 0;
    overflow-wrap: anywhere;
}

/* The bodies scroll on their own height, which is what keeps a whole table on screen while its
   area holds still; sideways overflow belongs to the grid inside, which scrolls itself. */
.ui-stack-area-body,
.ui-split-settings-body {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    overflow-y: auto;
}

/* With nothing picked, the calm prompt takes the column's height, so it stands where the fields
   will instead of hanging at the top of an empty box. */
.ui-split-settings > .ui-empty-state { flex: 1 1 auto; }

/* The divider between the areas — the collection splitter's dressing on its side: a hairline at
   rest that thickens into a 3px accent bar on hover/drag (the thicken rides transform, so nothing
   relayouts). It never folds away: a vertical stack fits any width. */
.ui-stack-splitter {
    position: relative;
    cursor: row-resize;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
}
.ui-stack-splitter-line {
    height: 3px;
    background: var(--color-stroke);
    transform: scaleY(0.3333);
    transition: transform var(--dur-quick) var(--ease-out), background var(--dur-quick) var(--ease-out);
}
.ui-stack-splitter:hover .ui-stack-splitter-line,
.ui-stack-splitter:focus-visible .ui-stack-splitter-line,
.ui-stack-splitter.dragging .ui-stack-splitter-line {
    transform: scaleY(1);
    background: var(--color-accent);
}
/* A detail body hosting a split screen hands it the height instead of scrolling itself: the areas
   and the settings column are the scrollers now. Scoped by :has, so every other screen's body
   scrolls exactly as before, and the body keeps its own overflow for the short-viewport case
   above, where the split refuses to shrink under the areas' floors and the body is what scrolls.
   The read-only surface between them is display:contents, so the split answers to the body's flex
   directly. */
.ui-md-detail-body:has(.ui-split) { display: flex; flex-direction: column; }

/* ============================================================================
   Template B — MasterDetailLayout. Two panes over the page: a tree column under its
   own masthead, foldable to a rail, and a detail column with an optional contextual
   toolbar strip, parted by a hairline splitter (accent-on-hover).

   BOTH panes are the same thing in the depth contract: a WELL — the working trough of
   the workspace, one step under the card tone. Neither is a card, and neither is a step
   away from the other; what rises is what the page LAYS IN them, the .ui-card blocks
   inside the detail body, which take the card tone and the card's lift against this
   floor. That is the whole ladder Template B uses: page → well pane → cards.
   ============================================================================ */
.ui-md {
    display: flex;
    flex-direction: column;
    height: 100vh;
    overflow: hidden;
    background: var(--color-bg);
    padding: var(--space-5) var(--space-7) var(--space-4);
}

.ui-md-body {
    --tree-pane-width: 320px;
    /* The width the task rail of an editing surface opens at, stated here and on the pane itself
       (the splitter script seeds its remembered "open" width off the element), so the two never
       disagree over the number. The active width is what the column set reads: folding the rail
       overrides only that, leaving the dragged width where the drag put it. */
    --editor-pane-width: 380px;
    --editor-pane-active-width: var(--editor-pane-width);
    --editor-splitter-width: 11px;
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    grid-template-columns: var(--tree-pane-width) 11px minmax(0, 1fr);
}

/* Master pane — the well the tree is sunk into, outlined so the column reads as a pane. The two
   areas of a split model screen and its settings column (StackedAreas, above) wear the very same
   box: one declaration, so the outline that makes a pane read as a pane cannot drift apart
   between the screens that use it. */
.ui-md-tree,
.ui-stack-area,
.ui-split-settings {
    min-width: 0;
    overflow: hidden;
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-lg);
    padding: var(--space-3);
    background: var(--color-surface-alt);
    display: flex;
    flex-direction: column;
}

/* ---- The column's masthead, its footer, and the rail it folds to ----------------------------
   Naming the column, commanding what it lists and putting it away belong to every page wearing the
   scaffold, not to one of them, so the strip that heads the pane, the strip that foots it, the
   command that folds it and the rail it leaves behind are dressed here with the rest of Template B.
   MasterDetailLayout draws them for Connector, Logic, Process and Users; the Home page — which lays
   the scaffold out by hand so full screen can override it — wears these same classes rather than a
   twin of its own. */

/* The pane's masthead: the column's name and the command that puts the column away. The page's own
   commands are not here — they foot the column, where the same glyph row answers on every screen. */
.ui-md-tree-header {
    flex: 0 0 auto;
    min-height: 40px;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0 var(--space-1) var(--space-2) var(--space-2);
    color: var(--color-text);
}

/* The column's name in both states — the masthead above and the vertical label that stands in for
   it on the collapsed rail — takes its weight and size from this one declaration, so the rail
   never reads heavier than the header it replaces. */
.ui-md-tree-header,
.ui-md-tree-rail-label {
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-semibold);
}

/* The name takes the slack, so the command that folds the column holds the trailing end however
   short it runs — and an overlong one ellipsises inside the card instead of pushing it out. */
.ui-md-tree-title {
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The pane's footer: the page's own commands for what the column lists, ruled off from the list
   and pinned under it however far the list scrolls. Making and filing lead; opening and closing the
   whole column hold the trailing corner on every screen; reordering, where a column offers it,
   sits directly ahead of that pair rather than past it. */
.ui-md-tree-footer {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding-top: var(--space-3);
    margin-top: var(--space-3);
    border-top: 1px solid var(--color-stroke-subtle);
}

.ui-md-tree-footer-commands,
.ui-md-tree-footer-view,
.ui-md-tree-footer-ordering { display: flex; align-items: center; gap: var(--space-2); }

/* Expand all / collapse all are not a second way to create: the "+" beside them writes something
   into the column, and these two only change how much of what is already there is on screen. So
   they are pushed off the leading cluster to the far end of the strip rather than seated in it,
   the same reading the detail toolbar gives its leading and trailing clusters, and they hold that
   corner on every screen. Where a column also reorders, the reordering cluster is the one that
   eats the free space instead (it is marked up ahead of the pair), landing the pair against the
   corner with reordering just to its left. */
.ui-md-tree-footer-ordering,
.ui-md-tree-footer-commands + .ui-md-tree-footer-view { margin-left: auto; }

/* The chevron points at the edge the column folds into; on the rail it points back out. */
.ui-md-tree-collapse .ui-glyph { transform: rotate(180deg); }

/* Collapsed the pane keeps its gutter: the rail stands off the detail by the same air as the
   open pane, so the workspace never reads as two surfaces pushed together. Only the track is
   overridden — the dragged width stays on --tree-pane-width, so reopening restores it. */
.ui-md-body.tree-collapsed { grid-template-columns: 44px 11px minmax(0, 1fr); }

/* Collapsed there is no pane width to drag, so the gutter is spacing only: the splitter keeps
   its grid track and gives up its hairline and its grip. */
.ui-md-body.tree-collapsed .ui-md-splitter { visibility: hidden; }

/* ---- The body handed to an editing surface (MasterDetailLayout.DetailColumns) ----------------
   A page that assembles instead of reading needs two tracks past the stage: the hairline that
   resizes the task rail, and the rail itself. The page states the mode on the body and the surface
   contributes the three columns; the folded rail floats its open panel over the stage, which is
   what the positioning is for. Both column sets are stated so putting the tree column away
   composes with the mode instead of overriding it back to three tracks. */
.ui-md-body.editing {
    position: relative;
    grid-template-columns:
        var(--tree-pane-width)
        11px
        minmax(0, 1fr)
        var(--editor-splitter-width)
        var(--editor-pane-active-width);
}

.ui-md-body.editing.tree-collapsed {
    grid-template-columns:
        44px
        11px
        minmax(0, 1fr)
        var(--editor-splitter-width)
        var(--editor-pane-active-width);
}

/* Narrow, the rail gives ground first: the stage is what is being drawn on. */
@media (max-width: 1100px) {
    .ui-md-body.editing { --editor-pane-active-width: min(var(--editor-pane-width), 300px); }
}

@media (max-width: 720px) {
    .ui-md-body.editing { --editor-pane-active-width: min(var(--editor-pane-width), 260px); }
}

/* The mode reaches past the bar: the stage is outlined in the accent while editing, so the boundary
   of "the screen I am assembling" stays visible in peripheral vision. Only the stage carries it —
   the task rail is an <aside>, not a ui-md-detail. */
.ui-md-body.editing .ui-md-detail { border-color: var(--color-accent); }

/* Collapsed, the card is the rail: its single control fills it corner to corner, so the card
   carries no padding of its own and the hover wash reaches every edge. */
.ui-md-tree.collapsed {
    padding: 0;
    border-radius: var(--radius-md);
}

.ui-md-tree-rail {
    width: 100%;
    height: 100%;
    min-height: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    border: 0;
    border-radius: inherit;
    background: transparent;
    color: var(--color-text-secondary);
    cursor: pointer;
}

/* The rail's only label, so it has to read as app typography, not as a graphic. The type is the
   app's because controls inherit it (base.css); what this rule owes is vertical *layout* rather
   than a rotated box. A `transform: rotate()` hands the text to Chromium's transformed-paint
   path, which rasterises it off the normal glyph path and — at a fractional device ratio
   (display scale × desktop zoom) — resamples it into soft, skewed letterforms. writing-mode
   types the run sideways on the ordinary path: `sideways-lr` reads bottom-to-top, up the rail,
   and the `vertical-rl` declaration ahead of it is the same run top-to-bottom — the fallback a
   renderer that doesn't know the sideways values lands on. */
.ui-md-tree-rail-label {
    writing-mode: vertical-rl;
    writing-mode: sideways-lr;
}

/* Lit while hovered, and while the rail is the thing holding the column's list open. */
.ui-md-tree-rail:hover,
.ui-md-tree-rail[aria-expanded="true"] {
    background: var(--color-overlay-hover);
    color: var(--color-accent-text);
}

/* The rail and the icon commands heading and footing the column sit against the card's edges, and
   the card clips what overflows it — so their ring is drawn inside rather than around them.
   Anything else the page seats in either strip keeps the app-wide ring (base.css). */
.ui-md-tree-rail:focus-visible,
.ui-md-tree-header .ui-icon-btn:focus-visible,
.ui-md-tree-footer .ui-icon-btn:focus-visible {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: calc(-1 * var(--focus-ring-offset));
}

/* Draggable divider — a hairline at rest that thickens into a 3px accent bar on
   hover/drag (the only colored affordance in the scaffold). */
.ui-md-splitter {
    position: relative;
    cursor: col-resize;
    display: flex;
    align-items: stretch;
    justify-content: center;
}

/* A 3px bar scaled to a 1px hairline at rest, thickening to full on hover/drag — the
   thicken rides transform (GPU-composited), not width, so nothing relayouts. */
.ui-md-splitter-line {
    width: 3px;
    background: var(--color-stroke);
    transform: scaleX(0.3333);
    transition: transform var(--dur-quick) var(--ease-out), background var(--dur-quick) var(--ease-out);
}

/* Keyboard focus reads the same as the pointer over it: the divider is about to move, whichever
   gesture is about to move it. The app-wide ring still frames it (base.css). */
.ui-md-splitter:hover .ui-md-splitter-line,
.ui-md-splitter:focus-visible .ui-md-splitter-line,
.ui-md-splitter.dragging .ui-md-splitter-line {
    transform: scaleX(1);
    background: var(--color-accent);
}

/* Detail column — the same well as the tree beside it, carrying its pinned head on top.
   It is the surface the page's cards are laid ON, never a card itself. */
.ui-md-detail {
    min-width: 0;
    /* The height twin of min-width above: a grid item's min-height is otherwise its content,
       and a chart canvas holds the pixels of the PREVIOUS layout — the detail could grow with
       the window but never shrink back, which is a stage taller than the viewport until a
       reload. Zero lets the row shrink first, and the charts then refit to the smaller cell. */
    min-height: 0;
    overflow: hidden;
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-lg);
    background: var(--color-surface-alt);
    display: flex;
    flex-direction: column;
}

/* The card's head: the selected thing's header, and under it the bar of what can be done to it.
   Both stay put while the body scrolls, which is the whole point of the strip — the name of what
   you are editing is the last thing that should leave the screen. One rule closes the head, at its
   bottom edge, so however many rows it holds it reads as one thing. */
.ui-md-head {
    flex: 0 0 auto;
    border-bottom: 1px solid var(--color-stroke);
}

/* Nothing selected: no name to carry, and the page has taken its verbs away with the selection.
   There is no head left to draw, so the card opens straight into its own empty state instead of
   under a rule holding nothing up. Asked as "no element inside", not as :empty, which counts text
   nodes too and would come undone the day the strip carries a stray one. */
.ui-md-head:not(:has(*)) { display: none; }

/* Pinned, the header keeps the body's own gutter, so the title stands directly over the cards it
   names. Its inline divider is dropped by the component: the head's rule is the one below it. */
.ui-md-head > .ui-panel-head { padding: var(--space-4) var(--space-5); }

.ui-md-toolbar {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--color-stroke);
}

/* In the head the bar keeps the head's closing rule as its own bottom edge, and under a header it
   draws the hairline that parts the two rows: where you are, then what you can do here. The same
   markup laid on a stage of its own (the dashboard editor, Home) keeps its own edge. */
.ui-md-head > .ui-md-toolbar { border-bottom: 0; }
.ui-md-head > .ui-panel-head + .ui-md-toolbar { border-top: 1px solid var(--color-stroke); }

.ui-md-toolbar-left {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

/* The bar says what is open and then what can be done with it, and those are two things. Left on the
   strip's own button gap the name reads as the first button of the row, so the title takes a group
   of its own: its mark and any state pill travel with it, and a wider gap plus a hairline part it
   from the first verb. Every bar of this shape marks its title with this class, so none of them
   drifts back to spelling the spacing itself. */
.ui-md-toolbar-title {
    flex: 0 1 auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    margin-inline-end: var(--space-2);
    padding-inline-end: var(--space-4);
    border-inline-end: 1px solid var(--color-stroke);
}

.ui-md-toolbar-right {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.ui-md-detail-body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: var(--space-5);
}

/* Frameless operator mode on the shared scaffold (Full screen / Expand). Home's own
   fullscreen class hides the stage bar and substitutes a caption, an exit button and a
   flyout; this scaffold has none of those three, so it KEEPS the pinned head — the bar
   already names the surface and carries the two commands that leave the mode. */
.ui-md.frameless { padding: 0; }
.ui-md.frameless .ui-ph,
.ui-md.frameless .ui-ph-divider,
.ui-md.frameless .ui-md-tree,
.ui-md.frameless .ui-md-splitter { display: none; }
.ui-md.frameless .ui-md-body { grid-template-columns: 1fr; }
.ui-md.frameless .ui-md-detail {
    margin: var(--space-3);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-lg);
    background: transparent;
}

/* ---- Panel header (PanelHeader) ----
   What a detail panel opens with, on every page wearing the scaffold: an icon tile, the title with
   its trailing marks, an optional right-aligned action, and a one-line sub saying what the selected
   thing is. Inside the scaffold it is drawn in the pinned head above; anywhere else it is drawn
   where it is written, and then it brings the hairline that parts it from the cards below. The tile
   stays neutral/monochrome (no per-area tint): color is reserved for semantics, which is what the
   marks beside the title carry — state dots, run-state chips, severity pills. */
.ui-panel-head { display: flex; align-items: flex-start; gap: var(--space-4); }
.ui-panel-head-icon {
    flex: 0 0 auto; width: 34px; height: 34px; border-radius: var(--radius-lg);
    display: flex; align-items: center; justify-content: center;
    background: var(--color-surface-alt); color: var(--color-text-secondary);
}
.ui-panel-head-text { flex: 1 1 auto; min-width: 0; }
.ui-panel-head-action { flex: 0 0 auto; display: flex; align-items: center; gap: var(--space-2); }
.ui-panel-titleline { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.ui-panel-title { font-size: var(--font-size-section); font-weight: var(--font-weight-semibold); color: var(--color-text); }
.ui-panel-sub { font-size: var(--font-size-description); color: var(--color-text-secondary); margin-top: var(--space-1); }
.ui-panel-divider { margin: var(--space-4) 0 var(--space-5); }

/* ---- Toolbar buttons ---------- */
.ui-toolbar-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-sans);
    font-size: var(--font-size-description);
    color: var(--color-text);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    cursor: pointer;
    white-space: nowrap;
}

/* The wash composes over the fill (the Buttons rule above): a page state that paints one — a
   recording toggle, a pending command — keeps it under the pointer, which is when it is read. */
.ui-toolbar-btn:hover:not(:disabled) {
    background-image: linear-gradient(var(--color-overlay-hover), var(--color-overlay-hover));
}
/* One refusal, one grey: a command held by the button's own disabled state and one held by
   aria-disabled (the import label, which is not a button) stand side by side on the same bar. */
.ui-toolbar-btn:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
.ui-toolbar-btn[aria-disabled="true"] { opacity: var(--opacity-disabled); cursor: not-allowed; }
.ui-toolbar-btn.danger { color: var(--color-critical); }
.ui-toolbar-btn.danger:hover:not(:disabled) { background: var(--color-critical-bg); }
/* Toggled state (e.g. the Dashboards Edit button): accent the glyph + label when on. */
.ui-toolbar-btn.active { color: var(--color-accent); }

/* A toggle that STAYS pressed: the same accent, over the soft fill and edge that hold the state
   with nothing pointing at it. The fill is set as a colour so the hover wash still composes on top
   (background-image, the rule above). */
.ui-toolbar-btn[aria-pressed="true"] {
    color: var(--color-accent);
    background-color: var(--color-accent-bg);
    border-color: var(--color-accent);
}

/* ---- Fluent glyph -----------------------------------------------------------
   The weight is pinned because the icon face ships only ONE: inside a semibold surface
   (a pane header, a flagged status entry) an inherited 600 has no bold face to switch to,
   so Chromium synthesizes one and thickens every stroke. */
.ui-glyph {
    font-family: 'FluentSystemIcons-Resizable';
    font-weight: var(--font-weight-regular);
    line-height: 1;
    display: inline-block;
    color: var(--color-text-secondary);
}

/* ---- Icon value: the operator's own SVG mark (IconValue.razor) ---------------
   The box states the size the caller asked for and the drawing fills it, so a mark authored at any
   size reads at the one it was placed at. `--iconvalue-fill` is the caller's tint, and it reaches
   only what declares no paint of its own: a coloured artwork stays as drawn, a monochrome one takes
   the tint. */
.ui-iconvalue {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 0;
}

.ui-iconvalue svg {
    width: 100%;
    height: 100%;
    fill: var(--iconvalue-fill, currentColor);
}

/* ============================================================================
   TreeView / TreeNode — the shared recursive tree.
   ============================================================================ */
.ui-tree {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    outline: none;
}

.ui-tree-node { display: block; }

/* The optional caption over a run of roots (a page that names its groups). It is a label, not a
   row: no hover, no selection, no indent of its own — the same weight the rail's group headers
   carry, so a titled group reads alike wherever the app draws one. */
.ui-tree-group {
    padding: var(--space-4) var(--space-3) var(--space-2);
    font-size: var(--font-size-eyebrow);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text-secondary);
}

.ui-tree-row {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-2) var(--space-3) var(--space-2) 0;
    border-radius: var(--radius-md);
    margin: 1px var(--space-1);
    cursor: pointer;
    user-select: none;
}

.ui-tree-row:hover { background: var(--color-overlay-hover); }
.ui-tree-row.selected { background: var(--color-surface); box-shadow: inset 0 0 0 1px var(--color-stroke); }

.ui-tree-toggle {
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    cursor: pointer;
    color: var(--color-text-secondary);
    padding: 0;
}

.ui-tree-toggle-spacer { flex: 0 0 auto; width: 18px; }

/* The .ui-glyph icon face, on the toggle button — same single weight, pinned for the same reason. */
.ui-tree-chevron {
    font-family: 'FluentSystemIcons-Resizable';
    font-weight: var(--font-weight-regular);
    font-size: var(--font-size-small);
    line-height: 1;
    display: inline-block;
}

.ui-tree-content { flex: 1 1 auto; min-width: 0; }

/* Naming a row in place (the folder create/rename gesture): the field stands exactly where the
   row's label does, sized by the row so the column never jumps around the edit. */
.ui-tree-rename {
    display: block;
    width: 100%;
    min-width: 0;
    box-sizing: border-box;
    padding: 0 var(--space-2);
    border: 1px solid var(--color-accent);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font: inherit;
    font-size: var(--font-size-small);
}

/* The standard row layout shared by every node template: icon, label, trailing info. */
.ui-tree-rowline {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
}

.ui-tree-rowline .ui-tree-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-tree-rowline .ui-tree-info {
    flex: 0 0 auto;
    font-size: var(--font-size-description);
    color: var(--color-text-secondary);
}

.ui-tree-rowline .ui-tree-error {
    flex: 0 0 auto;
    color: var(--color-critical);
}

/* Where a row came from — the mark on a definition or a device the station ships. Provenance is
   not state, so it takes the faintest ink on the row: it answers a question the operator asks
   once, and must never compete with what the row is or how it is doing. */
.ui-tree-rowline .ui-tree-origin {
    flex: 0 0 auto;
    color: var(--color-text-disabled);
}

/* ============================================================================
   Context menu (right-click on a tree row).
   ============================================================================ */
.ui-ctx-scrim {
    position: fixed;
    inset: 0;
    z-index: var(--z-popover);
}

.ui-ctx-menu {
    position: fixed;
    min-width: 180px;
    background: var(--color-surface);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-popover);
    padding: var(--space-2);
}

.ui-ctx-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    font-family: var(--font-sans);
    font-size: var(--font-size-body);
    color: var(--color-text);
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    cursor: pointer;
    text-align: left;
}

.ui-ctx-item:hover:not(:disabled) { background: var(--color-overlay-hover); }
.ui-ctx-item:disabled { opacity: 0.45; cursor: not-allowed; }
.ui-ctx-item.danger { color: var(--color-critical); }
.ui-ctx-glyph { color: inherit; }

.ui-ctx-separator {
    height: 1px;
    background: var(--color-stroke-subtle);
    margin: var(--space-2) 0;
}

/* One level of submenu: the row that opens a list keeps the item shape and adds the chevron that
   says so; the list itself is a menu panel of its own, laid beside the row and nudged back inside
   the window by ganterContextMenu.clampSubmenu. Both themes read it off the same tokens as the
   parent panel, so there is nothing here that can drift from it. */
.ui-ctx-sub { position: relative; }

.ui-ctx-item.ui-ctx-parent.open:not(:disabled) { background: var(--color-overlay-hover); }

.ui-ctx-chevron {
    margin-left: auto;
    color: var(--color-text-secondary);
}

.ui-ctx-panel {
    position: absolute;
    left: 100%;
    top: 0;
    z-index: 1;
    min-width: 180px;
    max-height: 60vh;
    overflow-y: auto;
    background: var(--color-surface);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-popover);
    padding: var(--space-2);
}

/* ============================================================================
   DataGrid (over <Virtualize>).
   ============================================================================ */
/* A table's job is to show what is configured at a glance, so a column is never dropped to make
   the rest fit: past the width its columns add up to (--grid-min-width, published by the grid from
   the columns themselves) the GRID scrolls sideways. The head and the body carry that same floor,
   which is what keeps the header travelling with the rows and standing over them instead of
   staying behind while they slide. The body scrolls only downward: it is never narrower than its
   rows, so it has nothing of its own to hide. */
.ui-grid {
    --grid-cols: auto;
    --grid-min-width: 0;
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    overflow-x: auto;
    overflow-y: hidden;
    background: var(--color-surface);
}

.ui-grid-head,
.ui-grid-row {
    display: grid;
    grid-template-columns: var(--grid-cols);
    align-items: center;
}

.ui-grid-head,
.ui-grid-body { min-width: var(--grid-min-width); }

.ui-grid-head {
    background: var(--color-surface-alt);
    border-bottom: 1px solid var(--color-stroke);
}

.ui-grid-body { overflow-x: hidden; overflow-y: auto; }

.ui-grid-row { border-bottom: 1px solid var(--color-stroke-subtle); }

/* The pointer washes a row; the picked row is tinted and ruled at its leading edge. The wash is a
   background-IMAGE, so it composes over the tint instead of replacing it (the Colour tokens note):
   the picked row stays picked under the pointer, and stays picked while the pointer is on another.
   The tint is hued and the wash is neutral, so the difference survives even side by side. */
.ui-grid-row:hover {
    background-image: linear-gradient(var(--color-overlay-hover), var(--color-overlay-hover));
}

/* The mark is the row's, never one cell's: the whole row carries it, and the rule runs its height. */
.ui-grid-row.selected {
    background-color: var(--color-overlay-selected);
    box-shadow: inset 3px 0 0 0 var(--color-accent);
}

/* Only a grid the page made selectable writes aria-selected, so it is also what says the row
   answers a click. */
.ui-grid-row[aria-selected] { cursor: pointer; }

.ui-grid-cell {
    padding: var(--space-2) var(--space-3);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: var(--font-size-body);
}

.ui-grid-cell.end { text-align: right; justify-self: end; }

.ui-grid-th {
    font-size: var(--font-size-description);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text-secondary);
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.ui-grid-th.sortable { cursor: pointer; }
.ui-grid-th.sortable:hover { color: var(--color-text); }
.ui-grid-sort { font-size: var(--font-size-micro); }

.ui-grid-group-head {
    grid-column: 1 / -1;
    padding: var(--space-3) var(--space-3) var(--space-1);
    font-size: var(--font-size-description);
    font-weight: var(--font-weight-semibold);
    background: var(--color-surface-alt);
    border-bottom: 1px solid var(--color-stroke-subtle);
}

.ui-grid-group-count { color: var(--color-text-secondary); font-weight: var(--font-weight-regular); }

.ui-grid-empty {
    padding: var(--space-5);
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
    text-align: center;
}

.ui-grid.compact .ui-grid-cell { padding: var(--space-1) var(--space-3); font-size: var(--font-size-description); }

/* Inline grid cell editors (the tag grid). */
.ui-grid-cell .ui-input,
.ui-grid-cell .ui-select {
    padding: var(--space-1) var(--space-2);
    min-width: 0;
    width: 100%;
}

.ui-grid-cell .ui-select { min-width: 0; }
.ui-grid-cell.disabled-cell, .disabled-cell { opacity: 0.4; pointer-events: none; }
.ui-grid-cell.mono { font-family: var(--font-mono); }

/* Two fields that answer one question (a window's two ends, a band's two sides) in a single cell:
   the pair reads as one column, each half named by the caption in front of it. */
.ui-grid-pair { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.ui-grid-pair > .ui-input, .ui-grid-pair > .ui-select { min-width: 0; flex: 1 1 0; }
.ui-grid-pair-label {
    flex: 0 0 auto;
    font-size: var(--font-size-eyebrow);
    color: var(--color-text-secondary);
}

/* A cell that reads rather than edits: what the row's own settings add up to, in the grid's
   secondary ink so it is plainly not a field. */
.ui-grid-readout {
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-grid-actions {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    justify-content: flex-end;
}

/* ---- Icon button (row actions / header) ------------------------------------- */
.ui-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    cursor: pointer;
    padding: 0;
}

.ui-icon-btn:hover:not(:disabled) { background: var(--color-overlay-hover); }
.ui-icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }

/* ---- Checkbox --------------------------------------------------------------- */
.ui-checkbox {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    cursor: pointer;
    user-select: none;
    font-size: var(--font-size-body);
}

.ui-checkbox input { width: 16px; height: 16px; accent-color: var(--color-accent); cursor: pointer; }
.ui-checkbox.disabled { opacity: 0.5; cursor: not-allowed; }
.ui-checkbox-label { color: var(--color-text); }
.ui-checkbox-label.bold { font-weight: var(--font-weight-semibold); }

/* ---- Tabs — underline tab strip (Shared/Navigation). Global, not scoped — the embedded host
   serves no *.styles.css. ---- */
.ui-tabs { display: flex; flex-direction: column; }

.ui-tabs-strip {
    display: flex;
    flex-wrap: wrap;
    border-bottom: 1px solid var(--color-stroke-subtle);
}

.ui-tab {
    appearance: none;
    background: transparent;
    border: 0;
    border-bottom: 2px solid transparent;
    margin: 0 2px 0 0;
    padding: var(--space-3) var(--space-4);
    font-family: var(--font-sans);
    font-size: var(--font-size-body);
    font-weight: var(--font-weight-regular);
    color: var(--color-text-secondary);
    cursor: pointer;
    border-top-left-radius: var(--radius-sm);
    border-top-right-radius: var(--radius-sm);
}

.ui-tab:hover:not(.disabled):not(.active) {
    background: var(--color-overlay-hover);
    color: var(--color-text);
}

.ui-tab.active {
    border-bottom-color: var(--color-accent);
    color: var(--color-text);
    font-weight: var(--font-weight-semibold);
}

.ui-tab.disabled {
    color: var(--color-text-disabled);
    cursor: default;
}

.ui-tabs-content { margin-top: var(--space-5); }

/* ---- ComboBox — editable (free-text) mode -----------------------------------
   The field the operator may type anything into (a Logic address), with the same DOM-drawn list
   anchored to it. The wrapper only overlays the chevron on the box's trailing edge, so the text box
   keeps whatever field styling its page already gives inputs — the control reads as the field it
   replaced, not as a picker. */
.ui-combobox-editable {
    position: relative;
    display: block;
}

.ui-combobox-editable > .ui-combobox-input {
    width: 100%;
    /* Room for the chevron: its two paddings plus the glyph. */
    padding-right: calc(2 * var(--space-3) + 12px);
}

.ui-combobox-open {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    display: flex;
    align-items: center;
    padding: 0 var(--space-3);
    border: 0;
    background: none;
    color: var(--color-text-secondary);
    cursor: pointer;
}

.ui-combobox-open:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

.ui-combobox-editable > input[aria-expanded='true'] + .ui-combobox-open .ui-combobox-chevron {
    transform: rotate(180deg);
}

/* The tail row of a capped list: what is still behind the cap, so the list does not read as
   complete when it is not. Not an option — it is not selectable and carries no role. */
.ui-combobox-more {
    padding: var(--space-2) var(--space-3);
    font-size: var(--font-size-description);
    color: var(--color-text-secondary);
    white-space: nowrap;
}
