/* Web dashboard runtime styling (ADR 0030). The station owns the look — the palette is the
   app's dark/light theme so a dashboard reads the same in the in-app webview and on a remote
   tablet. Theme is driven by data-theme on <html>, resolved on the host (the station's
   preference, never the browser's). Icons render from Fluent UI System Icons (MIT, ADR 0030 §4):
   a bundled MIT font, served via @font-face so the web stays OS-independent and license-clean
   (Segoe could not be served; this MIT set can). User SVG icon assets still render inline as
   <svg>. */

/* Theme comes entirely from tokens.css (ADR 0031 §5): this file reads the canonical
   --color-* tokens directly — no dashboard-private alias set anymore. The reset, the
   typography defaults and the FluentSystemIcons @font-face all live in base.css, which
   is always loaded before this sheet by App.razor, so they are not repeated here. The
   @import keeps the tokens resolvable if this sheet is ever inspected in isolation. */
@import url('tokens.css');

/* The grid fills the viewport and scales — no scroll. The casing that a crossing route paints to
   knock a gap out of the route below it is the STAGE tone: the stage is --color-bg windowed and
   full screen alike, so one declaration covers both and the gap never stops matching what shows
   through it. */
.dash-grid {
    position: absolute;
    inset: 0;
    display: grid;
    width: 100%;
    height: 100%;
    isolation: isolate;
    --dashboard-connection-casing-color: var(--color-bg);
}

.dash-cell {
    --dash-cell-inset: 4px;
    position: relative;
    min-width: 0;
    min-height: 0;
    padding: var(--dash-cell-inset);
}

/* A component paints in two layers with the connection routes running between them (see the
   shared appearance below), and only one stacking context can interleave them: the grid, which
   is what orders a whole dashboard. So the cell must not open one of its own — that would
   collapse both layers back into a single level and bury the routes under the background again.
   The canvases still write the component's layer on the cell; the chrome is what turns it into
   the two bands. */
.dash-cell:has(> .dash-component) {
    z-index: auto !important;
}

.dash-component {
    width: 100%;
    height: 100%;
    overflow: hidden;
}

/* A Symbol's connection anchors sit ON its view-box border, so half of every edge marker falls
   outside this box. The clip that keeps artwork inside the cell lives one level in, on
   .cmp-symbol-image, which occupies the identical rect — artwork stays bounded and the anchor
   overlay beside it is never cut, in edit or in operation. Descendant, not child: the shared
   appearance wraps the view in .dash-component-body, so the artwork hangs one level deeper. */
.dash-component:has(.cmp-symbol) {
    overflow: visible;
}

/* ---- Content follows the cell ------------------------------------------------
   A component is as big as the cell the operator drew it in: every surface below sizes its
   type in container-query height units, so dragging the resize handle grows and shrinks the
   text, the indicator and the button with the box instead of leaving air around a fixed
   15 px. The containers are declared with the shared appearance further down — the body for
   the component's content, the component itself for the corner title.

   --cmp-scale is the knob ComponentChrome writes from the component's own column: the same
   multiplier-inside-the-box the Symbol's artwork transform reads, applied here to type. Each
   surface therefore reads
       clamp(<floor>, calc(<design>cqh * var(--cmp-scale, 1)), <ceiling>cqh)
   where <design> is the share of the box the surface takes at the neutral knob position and
   <ceiling> is the share at which its content exactly fills the box — the ceiling is what stops
   a long text in a wide cell from growing past its own row. 11 px is the readability floor, and
   the value display rule in the next section is what holds it — and what carries the knob the
   rest of the way, onto the size the box and the declaration resolve to together.

   A surface sized as a SHARE of another — the unit beside a number, the answer under a command, a
   label against its readout — reaches neither of those two: the family's clamp was already
   resolved on the parent, so nothing puts the floor under the share. Every share therefore carries
   the floor of its own, as max(11px, <share>). In a short cell the proportion stops holding — the
   part reads larger against its number than the drawing asks — and it reads.

   That size is published a second time under a name of its own: --cmp-type on the component root,
   the type size the whole family scales off, and --display-height on each text surface, where it is
   the tall end of the size the shared rule resolves. Both are registered <length>, so container
   units resolve to px. Ratios between surfaces (a label against its readout) multiply --cmp-type in
   px instead of using em, which would resolve against the very font-size being computed. */

@property --cmp-type {
    syntax: '<length>';
    inherits: true;
    initial-value: 0px;
}

/* ---- Value display (ADR 0034) ------------------------------------------------
   How big a text surface renders follows from what is DECLARED about it and never from the value it
   happens to be showing, and nothing measures text: the two rules below are the whole computation.

   Every surface is in one of two modes and the markup says which. AUTOMATIC (.display-auto) is the
   cell's answer bounded by a declared reserve — --display-reserve, the width the content needs at
   font size 1, computed on the host from a point's declared digits, separator, decimals and unit,
   or from an authored string weighted per character. STATED (.display-stated) is a size in points
   the operator gave, and it is literal: it renders at that size in whatever cell it lands in and is
   never reduced to make content fit. Keeping a stated size legible is the model's job — it clamps
   the points to a legible range — and this sheet does not second-guess it.

   Each surface states the room it has as --display-avail, a container-query width with the family's
   own padding taken off, and the tall end of the automatic answer as --display-height. The
   content-scale knob multiplies the size those two RESOLVE to — never under the readability floor,
   and upward only as far as the room and the family's own ceiling still hold it — so turning it
   moves what is on screen whichever of the two was the binding one. A family holding more than one
   surface publishes that line once as --display-room and each surface takes the share of it it can
   count on; a share stated short costs a hair of type size, where a share stated long costs an
   ellipsis. The size therefore depends only on the box, the declarations, the knob and the
   interface's own face: two cells of the same size holding the same declarations always come out the
   same size, and nothing that moves at the rate of the data is an input to it.

   What exceeds its declaration is drawn by kind, and each mark says exactly one thing. A number
   (.display-num) is never rendered cut: past the digits declared for it the host adds .display-over
   and the surface draws #### — the spreadsheet's and the DCS's mark for a field too narrow for its
   value — with the whole reading in the tooltip. Free text (.display-text) truncates with an
   ellipsis and keeps the whole of itself in the tooltip. Whether the point is READING at all is a
   different fact and keeps its own mark: the markup writes the gap dash as the value and tags the
   surface with data-ganter-quality, the same word the shared appearance takes for the frame it draws
   around the whole component and symbol.js writes on a symbol that lost its point.
   Absence of a reading and absence of room never borrow each other's mark. */

@property --display-reserve {
    syntax: '<number>';
    inherits: false;
    initial-value: 0.01;
}

@property --display-height {
    syntax: '<length>';
    inherits: false;
    initial-value: 0px;
}

/* Registered for the same reason the reserve is, and with the opposite extreme: an unregistered
   length missing from a surface makes the one formula below invalid at computed-value time, which
   drops the whole declaration and leaves the surface at whatever it inherited. Stated wide instead,
   a family that publishes no room of its own falls back on its height candidate — the same courtesy
   the reserve gets, and a legible surface rather than a silent one. */
@property --display-avail {
    syntax: '<length>';
    inherits: false;
    initial-value: 100000px;
}

/* The one formula, and the readability floor with the last word over it. The reserve's registered
   initial value is small and non-zero, so a surface whose family states no reserve keeps a defined
   division and lands far above any candidate height — it falls back on the height alone.

   The knob multiplies what the two candidates RESOLVED to, which is why it stands on the division
   here: the height candidate arrives carrying it already — the family's own clamp, where its ceiling
   is what stops the growth — so the room is the half still to take it. Downward the line gives way
   with everything else, which is what makes the knob visible in a cell whose WIDTH is what limits
   the size; on the height alone it moved nothing there. Upward the room does not give: the division
   IS the width the reading fits in, and past it the number would be drawn cut, which this contract
   never does. */
.display-auto {
    font-size: max(
        11px,
        min(
            var(--display-height),
            calc(var(--display-avail) / var(--display-reserve) * min(var(--cmp-scale, 1), 1))));
}

.display-stated {
    font-size: var(--display-size);
}

/* A NUMBER at a stated size answers the same rule as one in automatic mode: it is never rendered cut.
   The size stays literal — nothing shrinks it — so what gives way is the number itself, replaced by the
   mark for a field that cannot hold its value, with the whole reading in the tooltip.
   The comparison is a length against a length and both are known here: the reserve times the stated
   size is the width the reading needs AT that size, and the box is the room the family states. CSS
   forbids dividing a length by a length in calc, but atan2 takes two lengths and tan undoes it, which
   turns the difference into a plain number of pixels — positive where the field is short. Clamped, that
   is the switch below, exact to the pixel; and still nothing measures text, because both halves of the
   product are declarations. */
/* Inherited, because the mark below is drawn by a pseudo-element and a registered property that does
   not inherit reaches it as its initial value instead of the answer. */
@property --display-cut {
    syntax: '<number>';
    inherits: true;
    initial-value: 0;
}

/* The number gives way to nothing — a size of zero draws neither the digits nor the unit beside them,
   whatever colour either was carrying — and the field keeps the one line the mark needs. Not the line
   the stated size asked for: that line is taller than the cell in exactly the case this rule is for, and
   a mark centred in it is a mark centred below the cell's edge, which is a silent field again. */
.display-num.display-stated {
    position: relative;
    --display-cut: clamp(
        0,
        tan(atan2(var(--display-size) * var(--display-reserve) - var(--display-avail), 1px)),
        1);
    font-size: calc(var(--display-size) * (1 - var(--display-cut)));
    min-height: calc(11px * 1.4 * var(--display-cut));
}

/* The mark, over the field the number left empty — the same mark, at the same readability floor, in the
   same place as the one automatic mode draws, so the operator reads one thing and not two. It collapses
   to nothing while the field can hold its number, rather than standing as an invisible box over a
   reading that is perfectly legible. */
.display-num.display-stated::after {
    content: "####";
    position: absolute;
    left: 0;
    top: 0;
    width: calc(100% * var(--display-cut));
    height: calc(100% * var(--display-cut));
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    font-size: calc(11px * var(--display-cut));
    color: var(--color-text-secondary);
}

.display-num,
.display-text {
    white-space: nowrap;
}

/* A number rests on the RIGHT of the field it reserved, in tabular figures: the separator holds its
   column as the integer part grows, and a column of readings lines up with itself. */
.display-num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.display-num.display-over {
    color: transparent;
    position: relative;
}

.display-num.display-over > * {
    visibility: hidden;
}

/* The mark stands at the readability floor, which is the smallest size a number may be written in —
   it reports the field, not the box. A mark sized off the cell would be sized off the cell's HEIGHT,
   the type size every family scales from, and drawn wider than the field it is standing in. Centred
   in the surface and clipped to it, so a narrow cell shows what room there is rather than spilling
   into the next one. */
.display-num.display-over::after {
    content: "####";
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    font-size: 11px;
    color: var(--color-text-secondary);
}

/* Ellipsis needs a block container over its own inline content — each of these families is a flex
   container, so the text ellipsizes on the inner span it holds (a blockified flex item). */
.cmp-led .state,
.cmp-label .text,
.cmp-command button > .display-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The words take the whole line rather than shrinking to themselves: the line is what gives the
   operator's alignment something to set them in, and the whole-line box is the room the reserve is
   resolved against. The component's own type size is the tall end of that answer. */
.cmp-label .text {
    flex: 1 1 auto;
    min-width: 0;
    --display-height: var(--cmp-type);
    --display-avail: 100cqw;
    text-align: var(--label-align, left);
}

/* ---- Components ------------------------------------------------------------- */

/* The Label is the one surface whose composition is the operator's own: where the words sit in
   the cell, how big they are, what weight and slant they carry and what ink they take. Each
   choice arrives as a class or a custom property LabelView writes, so one rule serves every
   combination and no class has to multiply with another. */
.cmp-label {
    display: flex;
    align-items: var(--label-anchor, safe center);
    width: 100%;
    height: 100%;
    --cmp-type: clamp(11px, calc(64cqh * var(--cmp-scale, 1)), 83cqh);
    font-size: var(--cmp-type);
}

.cmp-label.is-bold {
    font-weight: var(--font-weight-bold);
}

.cmp-label.is-italic {
    font-style: italic;
}

/* The ink is the operator's PAIR — the same shape as the component's background fill: LabelView
   writes both halves inline and the running theme picks one here, so flipping the theme repaints
   without a round trip. A Label that chose no ink of its own carries neither the class nor either
   half, and the interface's own text colour stands. */
.cmp-label.is-inked {
    color: var(--label-ink-light);
}

[data-theme='dark'] .cmp-label.is-inked {
    color: var(--label-ink-dark);
}

.cmp-frame {
    width: 100%;
    height: 100%;
    border: 1px solid var(--color-stroke);
    border-radius: 8px;
    background: transparent;
}

.cmp-frame .frame-title {
    font-size: 12px;
    font-weight: 600;
    color: var(--color-text-secondary);
    padding: 4px 10px;
    border-bottom: 1px solid var(--color-stroke);
}

/* The reading is the whole component, so its ceiling is a whole box: there is no caption row above
   it to leave space for, and a Label the operator placed beside it has a cell of its own. The room
   the reading has across is the cell less this family's own side padding. */
.cmp-value {
    display: flex;
    flex-direction: column;
    justify-content: center;
    width: 100%;
    height: 100%;
    padding: 4px 8px;
    --cmp-type: clamp(11px, calc(70cqh * var(--cmp-scale, 1)), 84cqh);
    --display-room: calc(100cqw - 16px);
    font-size: var(--cmp-type);
}

.cmp-value .readout {
    --display-height: var(--cmp-type);
    --display-avail: var(--display-room);
    font-weight: 600;
    line-height: 1.1;
    /* A line is centred by its BOX, and the box a font gives a line always keeps room under the
       baseline for descenders — room a reading never puts ink in, since figures and the unit
       beside them all sit ON the baseline. Centre the box and the number comes out low, with that
       empty descender room above it reading as a first line nothing was written on. The face here
       rises about 1.08 em over the baseline and hangs 0.25 em under it against figures 0.70 em
       tall, which leaves the ink about 0.06 em below the middle; the margin is twice that,
       because centring splits it between the two sides. What ends up centred is then the ink. */
    margin-bottom: 0.13em;
}

/* A point answering in WORDS — an order, a batch, the name of a profile — reads as free text here:
   the declared width is its characters, and what runs past it truncates with the ellipsis rather than
   the number's mark, whole in the tooltip. It rests where a line of words rests, on the left, which is
   what the sheet already gives it: the right-hand column a separator holds is the number's rule and
   stays scoped to .display-num. */
.cmp-value .readout.display-text {
    overflow: hidden;
    text-overflow: ellipsis;
}

.cmp-value .readout[data-ganter-quality='bad'] {
    color: var(--color-text-secondary);
}

.cmp-value .unit {
    font-size: max(11px, 0.54em);  /* em off the number, so it follows the size the number resolved to */
    font-weight: 400;
    color: var(--color-text-secondary);
    margin-left: 0.15em;
}

/* The word for the state the reading is in, under the number it belongs to. It is a line of the cell
   and not an overlay, so the number above keeps every pixel that is left; it holds its height while
   nothing is named — the number must not grow and shrink as a reading crosses a bound — and
   truncates with an ellipsis, its whole word in the tooltip, because it is a name the operator typed
   and not a reading. Sized off the component's own type scale so it stays a register below the
   reading at every cell size, and never smaller than the family's floor. */
.cmp-value .state {
    flex: 0 0 auto;
    min-height: 1.2em;
    font-size: max(11px, calc(var(--cmp-type) * 0.26));
    line-height: 1.2;
    color: var(--color-text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The indicator is em-sized off the component's type size, so it grows with the cell — and it is
   centred in it, because the indication is all there is: nothing shares the line, so a lamp pinned
   to one edge would be holding room for a caption that never comes. */
.cmp-led {
    display: flex;
    align-items: center;
    justify-content: safe center;
    width: 100%;
    height: 100%;
    padding: 0 8px;
    --cmp-type: clamp(11px, calc(54cqh * var(--cmp-scale, 1)), 76cqh);
    --display-room: calc(100cqw - 16px);
    font-size: var(--cmp-type);
}

/* The lamp is em off the component's type, which comes from the cell's HEIGHT — so in a tall narrow
   cell it would ask for more than the cell is wide. The room between the family's own side padding
   caps it, exactly, rather than a share picked by eye; the same expression on both axes keeps it
   round. */
.cmp-led .dot {
    width: min(1.3em, 100cqw - 16px);
    height: min(1.3em, 100cqw - 16px);
    border-radius: 50%;
    flex: 0 0 auto;
    border: 1px solid var(--color-led-bezel);
}

.cmp-led .dot.hollow {
    background: transparent !important;
    border: 2px dashed var(--color-neutral);
}

.cmp-led .dot.blink {
    animation: led-blink 1s steps(1, end) infinite;
}

@keyframes led-blink {
    50% { opacity: 0.25; }
}

/* The other presentation: the state written out in its own colour. A tinted face with the colour's
   own ink reads in both themes for every entry of the palette, which a filled one would not —
   white on the yellow lamp is not a state anybody reads. */
/* The face hugs its word, which is what makes it a pill and not a bar across the cell — so the sized
   surface is not the pill but the room around it, the whole flex line, the same box the command
   button's caption is given. The pill then inherits the size the line resolved to. The tall end is
   the share of the family type the pill always carried; under it a tall cell can no longer push the
   word past the cell's own width.
   The pill's side padding is drawn INSIDE this line and scales with the size being solved for, so it
   belongs to what the CONTENT needs rather than to the room: the view model adds it to the reserve
   (see LedComponentViewModel.TextReserve), which is the only side of the division where a share of
   the type size can go. Past the room the word still truncates, with the whole state in the tooltip
   the markup gives it. */
.cmp-led.is-label .state-room {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: safe center;
    --display-height: max(11px, calc(var(--cmp-type) * 0.8));
    --display-avail: calc(var(--display-room) - 2px);
}

.cmp-led.is-label .state {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 100%;
    padding: 0.08em 0.55em;
    border: 1px solid var(--led-ink);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--led-ink) 16%, transparent);
    color: var(--led-ink);
    font-weight: var(--font-weight-semibold);
}

/* Nothing to read: the dashed face is the label's hollow lamp, and the gap is already the text. */
.cmp-led.is-label .state.no-reading {
    border-style: dashed;
    border-color: var(--color-neutral);
    background: transparent;
    color: var(--color-text-secondary);
}

.cmp-led.is-label .state.blink {
    animation: led-blink 1s steps(1, end) infinite;
}

/* The third thing the indicator can be: reading, but reporting a code no declared state claims. It
   is not lit — a colour would name a state the declaration does not have — and it is not the dashed
   lamp either, which is the point saying nothing at all. A solid neutral ring is the answer to that
   pair: the point ANSWERED, and what it answered has no name here. The fill is overridden the way
   the hollow lamp overrides it, because the state's colour rides in on the element's own style. */
.cmp-led .dot.unclaimed {
    background: transparent !important;
    border: 2px solid var(--color-neutral);
}

/* The same, written: the pill keeps the solid edge that says it is reading, drops the state tint it
   has no state for, and holds the reading itself instead of a word nobody declared for it. */
.cmp-led.is-label .state.unclaimed {
    border-color: var(--color-neutral);
    background: transparent;
    color: var(--color-text-secondary);
}

/* A state may name a glyph, and then the glyph is the lamp. It is a mark and not writing — there is
   no readability floor under it, exactly as there is none under the lamp it replaces — so it is
   sized by the same expression the lamp is: em off the component's type, which comes from the
   cell's HEIGHT, capped by the room the family's own side padding leaves across it.
   A cell too narrow for the mark makes it small; it never makes it wider than the cell. */
.cmp-led .glyph {
    font-family: 'FluentSystemIcons-Resizable';
    font-weight: var(--font-weight-regular);
    line-height: 1;
    flex: 0 0 auto;
    font-size: min(1.3em, 100cqw - 16px);
}

.cmp-led .glyph.blink {
    animation: led-blink 1s steps(1, end) infinite;
}

.cmp-chart {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
}

/* Which mode the chart is drawing in — whether what is on screen is the measurement or a reduction
   of it, and which reduction. It sits in the corner opposite the toolbar so the two never meet, and
   it stays a mark rather than a badge: it is a fact about the drawing, not a condition to act on.
   The whole sentence, with the counts behind it, rides the title attribute.
   The corner it stands in is the left end of the row the chart's own title is drawn in, and the title
   gives that room up — this width, the offset it stands at and a clearance beside it — instead of
   being written under the mark (EChartsOptionBuilder.StateMarkRoomPx). The width is capped here for
   exactly that reason: a state named longer than the room ellipsizes, and the whole sentence is on
   the title attribute either way, rather than the mark growing back over the line the title was
   fitted to. */
.chart-mode {
    position: absolute;
    top: 4px;
    left: 6px;
    z-index: 5;
    max-width: 132px;
    padding: 0 4px;
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text-secondary);
    font-size: 10px;
    line-height: 15px;
    letter-spacing: 0.01em;
    opacity: 0.75;
    pointer-events: auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Bar and pie: the same column as the time chart, because both hang a note under the drawing and
   both give up exactly the room that note takes. They sit inside the same .chart-wrap, so the one
   shared toolbar can find them — told to draw only the few tools a comparison can honour, since
   there is no time axis to walk and no zoom to reset. */
.cmp-categorical {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
}

/* What a component says when a ceiling stood between the retention it was configured with and the
   one the station granted. It is the runtime twin of the editor's .edit-hint.is-capped and keeps
   that register: one quiet tinted line under the drawing, never a banner over it, because the
   component still works — it just keeps less than it was asked to. The full sentence rides the
   title attribute, so a narrow cell truncates without losing it. */
.cmp-cap-note {
    flex: 0 0 auto;
    margin-top: 2px;
    padding: 1px 4px;
    border-radius: var(--radius-sm);
    background: var(--color-caution-bg);
    color: var(--color-caution);
    font-size: 11px;
    line-height: 1.35;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---- Occurrence episodes --------------------------------------------------- */

.cmp-occurrences {
    width: 100%;
    height: 100%;
    overflow: auto;
    border: 1px solid var(--color-stroke);
    background: var(--color-surface);
}

.cmp-occurrences .occ-header {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 4px 8px;
    border-bottom: 1px solid var(--color-stroke);
    background: var(--color-surface);
    font-size: 12px;
    font-weight: 600;
}

.cmp-occurrences .occ-row {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 4px 8px;
    border-bottom: 1px solid var(--color-stroke);
}

.cmp-occurrences .occ-row.active {
    border-left: 3px solid var(--color-accent);
    padding-left: 5px;
}

.cmp-occurrences .occ-title-line,
.cmp-occurrences .occ-clocks {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
}

.cmp-occurrences .occ-clocks {
    flex-wrap: wrap;
    column-gap: var(--space-3);
    row-gap: var(--space-2);
}

.cmp-occurrences .occ-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
}

.cmp-occurrences .occ-status {
    flex: 0 0 auto;
    color: var(--color-text-secondary);
    font-size: 11px;
}

.cmp-occurrences .occ-status.active {
    color: var(--color-accent-text);
}

.cmp-occurrences .occ-clock,
.cmp-occurrences .occ-signal {
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.cmp-occurrences .occ-clock {
    color: var(--color-text-secondary);
}

.cmp-occurrences .occ-signal {
    padding: 1px 4px;
    border: 1px solid var(--color-caution);
    border-radius: var(--radius-sm);
    color: var(--color-text);
    background: var(--color-caution-bg);
}

.cmp-occurrences .occ-empty {
    padding: var(--space-3);
    color: var(--color-text-secondary);
    font-size: var(--font-size-eyebrow);
    line-height: 1.4;
}

.cmp-occurrences .occ-message {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--color-text-secondary);
    font-size: 11px;
}

/* The chart's <div class="echart"> has no intrinsic height, so it collapses to ECharts'
   100px default. Force it to fill the cell; the resize tick (echart-behaviors.js / ganterChart)
   then sizes the canvas to match. A chart takes the room its cell leaves instead of the whole
   cell, because a capped retention adds a line under it; the same tick re-sizes the canvas. */
.cmp-chart > .echart,
.cmp-categorical > .echart {
    flex: 1 1 auto;
    min-height: 0;
    width: 100% !important;
}

/* The dial takes the room its cell leaves rather than the whole of it, because the state the bands
   name is written on a line under the arc; the same resize tick sizes the canvas to match. */
.cmp-gauge > .echart {
    flex: 1 1 auto;
    min-height: 0;
    width: 100% !important;
}

/* ---- KPI card (value + mini-chart over a rolling window) --------------------- */

.cmp-kpi {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    padding: 4px 8px;
    --display-room: calc(100cqw - 16px);
}

/* A card that draws no mini-chart is its reading, so the text takes the box the drawing left
   instead of leaving air under it. */
.cmp-kpi.no-mini .kpi-text {
    display: flex;
    flex-direction: column;
    justify-content: center;
    flex: 1;
    min-height: 0;
}

/* The card's metrics are the card's own and not the cell's — a KPI reads as one register whatever
   room it was given — so the content knob meets them here, on the candidate itself, where the other
   families meet it in the cqh clamp they publish. */
.cmp-kpi .readout {
    --display-height: calc(22px * var(--cmp-scale, 1));
    --display-avail: var(--display-room);
    font-weight: 600;
    line-height: 1.1;
}

.cmp-kpi .readout[data-ganter-quality='bad'] {
    color: var(--color-text-secondary);
}

.cmp-kpi .unit {
    font-size: max(11px, 0.54em);  /* em off the number, so it follows the size the number resolved to */
    font-weight: 400;
    color: var(--color-text-secondary);
    margin-left: 0.15em;  /* the gap the reserve charges for the unit */
}

/* The window's three statistics as one line. Their candidate height IS the readability floor, so at
   the neutral knob the line reads at 11 px in every cell and the reserve has nothing to take off it;
   the gap is em-based so it stays proportional to that line. The row is what they share with the
   state below, so the room stated here is the half of it they can count on. */
.cmp-kpi .kpi-stats {
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    gap: 0.9em;
    --display-height: calc(11px * var(--cmp-scale, 1));
    --display-avail: calc(var(--display-room) * 0.55);
    color: var(--color-text-secondary);
    white-space: nowrap;
    overflow: hidden;
}

/* The row the two of them share: the word for the state the card is in, and the window's statistics
   beside it. Two boxes and not one, because the statistics stand at the floor — they have nowhere to
   shrink to, only the mark to stand down behind — and a word inside them would take three good
   numbers behind that mark. */
.cmp-kpi .kpi-stats-row {
    display: flex;
    align-items: baseline;
    gap: 0.9em;
    min-width: 0;
}

/* The word holds the row's height while nothing is named, so the mini-chart under it does not grow
   and shrink as a reading crosses a bound. It is also what gives way when the row runs short — by a
   wide margin, and never past a share of the line — because a name cut short still reads and can be
   had whole from its tooltip, where a statistic cut short would be a number shown wrong. It is a
   name and not a reading, so it carries the family's floor as its own size. */
.cmp-kpi .kpi-state {
    flex: 0 8 auto;
    min-width: 0;
    max-width: 45%;
    min-height: 1.2em;
    line-height: 1.2;
    font-size: 11px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--color-text-secondary);
}

/* The line the difference stands on, and the carrier of what it is measured against: a number that
   overran its field draws the #### mark over its own box, so the box around it is where the markup
   names the reference and has it survive. */
.cmp-kpi .kpi-delta-row {
    min-width: 0;
}

/* The difference from the reference reads with the value, not with the window statistics: a
   register above them, still secondary to the reading it belongs to. */
.cmp-kpi .kpi-delta {
    --display-height: calc(13px * var(--cmp-scale, 1));
    --display-avail: var(--display-room);
    color: var(--color-text-secondary);
    white-space: nowrap;
    overflow: hidden;
}

.cmp-kpi .kpi-mini {
    position: relative;
    flex: 1;
    min-height: 0;
}

/* Hidden rather than removed: the view releases the chart instance by naming this element, and an
   element already out of the document could not be named. */
.cmp-kpi.no-mini .kpi-mini {
    display: none;
}

.cmp-kpi .kpi-mini .echart {
    position: absolute;
    inset: 0;
}

/* What the distribution counted. Quiet and last, so it explains the bars without competing with
   the reading above them. */
.cmp-kpi .kpi-mini-note {
    flex: 0 0 auto;
    font-size: 10px;
    line-height: 1.3;
    color: var(--color-text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ---- Progress (one bounded reading, drawn here and not by a chart engine) ---- */
/* Everything below is fractions of the scale the view model already placed: --len is how much of
   the track something takes, --at is where on it something sits, and these rules only decide which
   edge each one is measured from. That is what lets one set of markup serve a bar across the cell
   and a level column up it, in either direction. */

.cmp-progress {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 0.25em;
    width: 100%;
    height: 100%;
    padding: 4px 8px;
    /* The reading labels the indicator, it is not the whole of it, so its growth is capped where a
       bare readout's is not: what a bigger cell buys here is a longer track. */
    --cmp-type: clamp(11px, calc(30cqh * var(--cmp-scale, 1)), 30px);
    --display-room: calc(100cqw - 16px);
    font-size: var(--cmp-type);
    /* The fill's ink arrives as the operator's pair, written inline by the view; the running theme
       picks the half here, and a track nobody accented takes the interface's indication ink. */
    --progress-ink: var(--progress-ink-light, var(--color-accent));
}

[data-theme='dark'] .cmp-progress {
    --progress-ink: var(--progress-ink-dark, var(--color-accent));
}

.cmp-progress .progress-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5em;
    min-width: 0;
}

/* The pair on the head never negotiates (ADR 0034): the number takes the width it reserved, the
   caption takes what is left and ellipsizes, and neither is sized from the other. Each is given a
   share of the head as its room — the number the larger one, since it is the reading — and a share
   stated short costs a hair of type size where a share stated long would cost the caption its tail. */
.cmp-progress .label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    --display-height: max(11px, calc(var(--cmp-type) * 0.72));
    --display-avail: calc(var(--display-room) * 0.38);
    color: var(--color-text-secondary);
}

.cmp-progress .readout {
    flex: 0 0 auto;
    --display-height: var(--cmp-type);
    --display-avail: calc(var(--display-room) * 0.62);
    font-weight: 600;
    line-height: 1.1;
}

/* The field is exactly the width that was reserved for it: 1em here is the size the rule above
   resolved, so the box holds the declared digits, the separator, the decimals and the unit — and
   holds still as the reading ticks. A stated size states no reserve and takes the room its own
   content needs, which is what makes it literal. */
.cmp-progress .readout.display-auto {
    width: calc(var(--display-reserve) * 1em);
}

.cmp-progress .readout[data-ganter-quality='bad'] {
    color: var(--color-text-secondary);
}

.cmp-progress .unit {
    font-size: max(11px, 0.54em);  /* em off the number, so it follows the size the number resolved to */
    font-weight: 400;
    color: var(--color-text-secondary);
    margin-left: 0.15em;
}

/* The word for the state the track is in, on the head between the caption and the reading. It is
   sized as a share of the component's type and so carries the readability floor itself, and it is
   what gives way when the head runs short — by a wide margin over the caption beside it, and never
   past a share of the line, so the caption is only reached once the name has given up nearly all of
   its own width. The reading never shrinks at all. It holds a line while nothing is named, which is
   what the standing head needs so the column below it does not move as a reading crosses a bound. */
.cmp-progress .state {
    flex: 0 8 auto;
    min-width: 0;
    max-width: 45%;
    min-height: 1.2em;
    font-size: max(11px, calc(var(--cmp-type) * 0.72));
    line-height: 1.2;
    color: var(--color-text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cmp-progress .progress-body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    min-height: 0;
}

.cmp-progress .progress-track {
    position: relative;
    box-sizing: border-box;
    flex: 0 0 auto;
    height: clamp(8px, calc(24cqh * var(--cmp-scale, 1)), 28px);
    border-radius: var(--radius-sm);
    background: var(--color-overlay-grid);
    overflow: hidden;
}

/* Nothing to read: no fill at all, and a dashed outline in its place. An empty bar would be read
   as a measured zero, which is the one thing this component must never say. */
.cmp-progress .progress-track.no-reading {
    background: transparent;
    border: 1px dashed var(--color-neutral);
}

/* Horizontal, forward — the scale starts at the left edge and everything is measured from it. */
.cmp-progress .progress-fill {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: var(--len);
    background: var(--progress-ink);
}

.cmp-progress .progress-span {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--at);
    width: var(--len);
    background: var(--span-ink, var(--color-neutral));
    opacity: 0.5;
}

.cmp-progress .progress-needle,
.cmp-progress .progress-mark {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--at);
    width: 3px;
    margin-left: -1.5px;
    border-radius: 1px;
}

.cmp-progress .progress-needle {
    background: var(--progress-ink);
}

/* The reference is not a reading, so it is drawn in the text ink with a hairline of the surface
   around it: it must stay legible over a fill and over a band alike. */
.cmp-progress .progress-mark {
    background: var(--color-text);
    box-shadow: 0 0 0 1px var(--color-surface);
}

/* Horizontal, reversed — the scale starts at the right edge instead. */
.cmp-progress.is-reverse .progress-fill {
    left: auto;
    right: 0;
}

.cmp-progress.is-reverse .progress-span {
    left: auto;
    right: var(--at);
}

.cmp-progress.is-reverse .progress-needle,
.cmp-progress.is-reverse .progress-mark {
    left: auto;
    right: var(--at);
    margin-left: 0;
    margin-right: -1.5px;
}

/* Vertical — the track runs up the cell and the scale starts at the bottom. */
.cmp-progress.is-vertical {
    justify-content: flex-start;
    /* Standing up, the head is a header over the column, and the room it has is the cell's WIDTH:
       a tall cell must not print a title as tall as the level it labels. */
    --cmp-type: clamp(11px, calc(18cqw * var(--cmp-scale, 1)), 30px);
}

.cmp-progress.is-vertical .progress-head {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
}

/* Standing up, the head is a stack of lines and nothing shares one, so each has the whole width. */
.cmp-progress.is-vertical .label,
.cmp-progress.is-vertical .readout {
    --display-avail: var(--display-room);
}

.cmp-progress.is-vertical .progress-body {
    flex: 1 1 auto;
    flex-direction: row;
    gap: 4px;
}

.cmp-progress.is-vertical .progress-track {
    flex: 0 0 auto;
    height: auto;
    width: clamp(8px, calc(24cqw * var(--cmp-scale, 1)), 28px);
}

.cmp-progress.is-vertical .progress-fill {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    width: auto;
    height: var(--len);
}

.cmp-progress.is-vertical .progress-span {
    top: auto;
    bottom: var(--at);
    left: 0;
    right: 0;
    width: auto;
    height: var(--len);
}

.cmp-progress.is-vertical .progress-needle,
.cmp-progress.is-vertical .progress-mark {
    top: auto;
    bottom: var(--at);
    left: 0;
    right: 0;
    width: auto;
    height: 3px;
    margin: 0 0 -1.5px;
}

/* Vertical, reversed — the scale starts at the top. */
.cmp-progress.is-vertical.is-reverse .progress-fill {
    bottom: auto;
    top: 0;
}

.cmp-progress.is-vertical.is-reverse .progress-span {
    bottom: auto;
    top: var(--at);
}

.cmp-progress.is-vertical.is-reverse .progress-needle,
.cmp-progress.is-vertical.is-reverse .progress-mark {
    bottom: auto;
    top: var(--at);
    margin: -1.5px 0 0;
}

/* The bargraph: the same fill in steps, so a glance reads how many rather than how far. The cells
   run the way the scale does, which is the one thing the flex direction has to say. */
.cmp-progress .progress-track.is-segmented {
    display: flex;
    gap: 2px;
    background: transparent;
}

.cmp-progress .progress-cell {
    flex: 1 1 0;
    min-width: 0;
    min-height: 0;
    border-radius: 1px;
    background: var(--color-overlay-grid);
}

.cmp-progress .progress-cell.lit {
    background: var(--progress-ink);
}

.cmp-progress.is-reverse .progress-track.is-segmented {
    flex-direction: row-reverse;
}

.cmp-progress.is-vertical .progress-track.is-segmented {
    flex-direction: column-reverse;
}

.cmp-progress.is-vertical.is-reverse .progress-track.is-segmented {
    flex-direction: column;
}

/* The two ends of the scale, written where they sit: first child is the near end, so the vertical
   column reverses to put it at the bottom. */
.cmp-progress .progress-scale {
    display: flex;
    flex: 0 0 auto;
    justify-content: space-between;
    gap: 0.5em;
    font-size: clamp(9px, calc(14cqh * var(--cmp-scale, 1)), 13px);
    line-height: 1.2;
    color: var(--color-text-secondary);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow: hidden;
}

.cmp-progress.is-vertical .progress-scale {
    flex-direction: column-reverse;
    align-items: flex-start;
}

/* The travel of the fill is the reading moving, not the interface decorating itself, so it keeps
   its own duration under the system's reduced-motion preference — beside the alarm blink and the
   indeterminate progress base.css already exempts for the same reason. */
.cmp-progress .progress-fill,
.cmp-progress .progress-cell,
.cmp-progress .progress-needle,
.cmp-progress .progress-mark {
    transition-property: width, height, top, bottom, left, right, background-color;
    transition-duration: var(--dur-base) !important;
    transition-timing-function: var(--ease-out);
}

/* Unit live chart stack (one ChartView per plot group): vertical stack, each chart a
   fixed height, the page scrolls when there are more groups than fit. */
.unit-charts-page {
    position: fixed;
    inset: 0;
    padding: 8px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 8px;
    overflow-y: auto;
}

.unit-chart {
    flex: 0 0 auto;
    height: 240px;
}

.cmp-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
}

.cmp-icon .glyph {
    font-family: 'FluentSystemIcons-Resizable';
    line-height: 1;
}

/* The operator's own SVG mark, in the box the component's Size field states — the same field the
   font glyph is set at, so one panel answers for both. The box carries the ink; the drawing takes it
   wherever it declares no paint of its own, which leaves a coloured artwork alone and lets a
   monochrome one be tinted. */
.cmp-icon .glyph-svg {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 0;
}

.cmp-icon .glyph-svg svg {
    width: 100%;
    height: 100%;
    fill: currentColor;
}

.cmp-image {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}

.cmp-image img {
    max-width: 100%;
    max-height: 100%;
}

/* Fitted inside the cell whole, taking the room the shape allows — the mark beside the name draws
   the picture entire, inside the box, and a small picture grows to it. */
.cmp-image img.uniform {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* At the size the picture was made, whatever the cell is: a small one stays small and a large one is
   clipped by the cell, which is the mark drawn beside that name. */
.cmp-image img.none {
    max-width: none;
    max-height: none;
    width: auto;
    height: auto;
}

.cmp-image img.fill {
    width: 100%;
    height: 100%;
    object-fit: fill;
}

.cmp-image img.uniformfill {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Command components render read-only until commanding is enabled (ADR 0030 §7). The control and
   the reserved answer line are em-sized off one type size, so the whole stack — not just its
   text — follows the cell. Every kind of the family draws those same two rows and no more: the
   control the operator's hand goes to, and under it the line that answers the touch. That is what
   lets one type size serve all of them, and why the ceiling is the share at which the two rows
   exactly fill the box — the way to a bigger command is a taller cell, never a stack that leaves
   the one the operator drew, so the knob mostly tunes downward. Centring is the safe kind: in a
   cell too short for even the floor, the stack hangs from the top and stays reachable instead of
   being cut at both ends. */
.cmp-command {
    display: flex;
    flex-direction: column;
    justify-content: safe center;
    gap: 0.43em;
    width: 100%;
    height: 100%;
    padding: 0.29em 0.57em;
    --cmp-type: clamp(11px, calc(22cqh * var(--cmp-scale, 1)), 23cqh);
    /* The line the two rows are drawn on: the cell across, less this family's own side padding. Its
       padding is em off the type size, which is already resolved in px here. */
    --display-room: calc(100cqw - var(--cmp-type) * 1.14);
    font-size: var(--cmp-type);
}

/* Whatever the kind puts on the control row — a key, a field row, a switch — that row is what the
   cell is for, and it never gives way. The answer line under it is what yields when the two rows
   want more than the operator drew. */
.cmp-command > button,
.cmp-command .input-row,
.cmp-command .toggle-switch {
    flex: 0 0 auto;
}

/* What the two controls share is their metrics — one type size and one box. What they ARE is drawn
   apart right below, because a field and a key must be told apart before either is read. */
.cmp-command button,
.cmp-command input {
    font-size: 1em;
    padding: 0.43em 0.86em;
    border: 1px solid var(--color-stroke);
    color: var(--color-text);
}

/* The field is a WELL: the recessed fill and the inset shadow every text surface in the app is
   drawn with, so it reads sunken in both themes without either being named here — in light the
   fill steps down from the surface, in dark it steps up and the deeper inset carries the recess.
   Its corner is tighter than the key's: a slot is cut into the surface, a key sits on it. And it
   must be allowed to shrink — an <input> asks for its own default width, which is more than a
   narrow cell has. */
.cmp-command input {
    min-width: 0;
    border-radius: var(--radius-sm);
    background: var(--color-surface-alt);
    box-shadow: var(--shadow-inset-well);
    cursor: text;
}

/* Focus lights the bottom edge over the well rather than thickening the border, so the field never
   shifts by the pixel a heavier border would add (the app's reveal focus). */
.cmp-command input:focus {
    outline: none;
    box-shadow: var(--shadow-inset-well), inset 0 -2px 0 0 var(--color-accent);
}

/* The key is the opposite reading: its own face, a caption in the weight a command is read at, and
   a rounder corner. The face itself is not set here for the Button kind — it carries the
   operator's colour pair below — so this rule stays at the family's specificity and never takes
   that choice away. */
.cmp-command button {
    border-radius: var(--radius-md);
    background: var(--color-surface);
    font-weight: var(--font-weight-semibold);
    cursor: pointer;
}

/* …and it stands off the surface, which is the other half of the reading. The switch's track is a
   button too and is left out of it: the track is the slot its knob runs in, not something that
   comes up to meet the finger. Nothing here paints a background — a rule this specific would beat
   the operator's own face. */
.cmp-command button:not(.track) {
    box-shadow: var(--shadow-card);
    transition: transform var(--dur-press) var(--ease-out);
}

/* Hover tints the face without replacing it: on background-color the operator's colour would be
   thrown away and whatever sits behind the button would show through — the rule every button in
   the app follows. */
.cmp-command button:not(.track):hover:not(:disabled) {
    background-image: linear-gradient(var(--color-overlay-hover), var(--color-overlay-hover));
}

/* The press lands: a small dip, never below 0.95. */
.cmp-command button:not(.track):active:not(:disabled) {
    transform: scale(0.97);
}

/* The caption is its own block inside the key, which is what gives the ellipsis an inline box to cut.
   Its room is the key's: the family's line less the key's own padding and its edge. */
.cmp-command button > .display-text {
    display: block;
    --display-height: var(--cmp-type);
    --display-avail: calc(var(--display-room) - var(--cmp-type) * 1.72 - 2px);
}

/* The Button kind is the one command control that carries a look of its own: a colour PAIR — one
   half per running theme, the same shape as the component background above — and an optional mark
   from the app's icon library. The class and the four halves always travel together from
   ButtonView, and both halves are always there because a button is born holding the product
   colour; the theme picks its two here, so flipping it repaints without a round trip. The edge is
   one small step of the face's own ink rather than the neutral control stroke, which would ring a
   saturated face with a stray hairline — the step is small enough that at the product colour the
   edge lands on the control stroke the neighbouring controls draw. Nothing below is sized in px:
   the glyph and the gap are em off .cmp-command's type size, so the painted button follows the
   cell like the rest of the family. */
.cmd-faced {
    --cmd-face: var(--cmd-face-light);
    --cmd-ink: var(--cmd-ink-light);
    --cmd-edge: color-mix(in srgb, var(--cmd-ink) 14%, var(--cmd-face));
}

[data-theme='dark'] .cmd-faced {
    --cmd-face: var(--cmd-face-dark);
    --cmd-ink: var(--cmd-ink-dark);
}

.cmd-faced button {
    display: flex;
    align-items: center;
    gap: 0.5em;
    background: var(--cmd-face);
    color: var(--cmd-ink);
    border-color: var(--cmd-edge);
}

/* The caption takes the whole line beside the mark rather than hugging its own words, so the
   operator's key reads as one face and not as a word adrift on it. */
.cmd-faced button > .display-text {
    flex: 1 1 auto;
    min-width: 0;
}

/* A mark on the key takes its own width off what the caption is left: the glyph and the gap between
   the two, both em off the family's type size. */
.cmd-faced button:has(> .glyph) > .display-text {
    --display-avail: calc(var(--display-room) - var(--cmp-type) * 3.47 - 2px);
}

.cmd-faced button > .glyph {
    flex: 0 0 auto;
    font-family: 'FluentSystemIcons-Resizable';
    font-size: 1.25em;
    line-height: 1;
}

/* One row, whatever the cell: the field and the key side by side. Each asks for the width its job
   needs — the field for the most, since it is what the row is for, and the key for exactly its own
   word — so what a wider cell has to give goes to the box being typed in. */
.cmp-command .input-row {
    display: flex;
    align-items: center;
    gap: 0.57em;
    min-width: 0;
}

.cmp-command .input-row > input {
    flex: 2 1 5em;
}

.cmp-command .input-row > button {
    flex: 0 0 auto;   /* the command never shrinks under its own word */
}

/* Over a two-state point the row carries a key per state instead of a box to type in, and those
   two ARE the row: they split what the field would have taken and give way with it, because a pair
   that never shrank would carry the cell out with them. */
.cmp-command .input-row > button.flag {
    flex: 1 1 0;
    min-width: 0;
}

/* Blocked is not blank. A command the operator may not press is still drawing the point's state, and
   the state is why the component is on the screen at all — so only the FACE the hand goes to steps
   back, and only a step. What says the press will not land is the cursor here and the lock the shared
   appearance draws on the frame, never a fade over the whole control. */
.cmp-command button:disabled,
.cmp-command input:disabled {
    opacity: 0.7;
    cursor: not-allowed;
}

/* …and these are not faces at all: the switch's track, the option in force, the closed picker naming
   the mode the plant is in and the travel the handle stands on each ARE the reading, drawn by the
   control itself. They keep their full strength whoever is signed in — a lit track gone grey reads as
   a switch that is off, which is the screen lying about the plant. */
.cmp-command button.track:disabled,
.cmp-command button.option.active:disabled,
.cmp-command .ui-combobox:disabled,
.cmp-command input.travel:disabled {
    opacity: 1;
}

/* A key standing in a set of them is its OWN box, and no share of the LINE can say how wide that box
   came out: how many keys the row carries is the point's business, not the stylesheet's. So the key
   states ITSELF as the container its word is sized against, and 100cqw below is then the width the
   row actually gave it.
   The air around the word is drawn by the CAPTION rather than by the key, which is what makes it a
   share of the very size being solved for — and the reserve is the only side of that division where a
   share of the type size can stand, so the host adds the two sides there (see
   SelectorComponentViewModel.TextReserve and the key reserve InputView publishes). Drawn by the key it
   would be a fixed share of the family type instead: most of a narrow key, with no legible size left
   to stand beside it. Past the key's own box the word still truncates, with an ellipsis. */
.cmp-command .input-row > button.flag,
.cmp-selector .selector-row > .option {
    container-type: inline-size;
    padding-inline: 0;
}

/* …and on a key the row left narrow, the AIR is what yields. Air is negotiable and the word is not:
   at the readability floor a full share of air is most of a small key, and a word cut so that its own
   padding could stay whole is the wrong thing to have kept. Bounded by a share of the key, the two
   sides give way exactly as the key gets narrow, and never take more than the reserve accounted for —
   so the size the sheet resolves stays a size the word fits in, or truncates at the floor because no
   size would have held it. */
.cmp-command .input-row > button.flag > .display-text,
.cmp-selector .selector-row > .option > .display-text {
    padding-block: 0;
    padding-inline: min(0.86em, 8cqw);
    --display-avail: 100cqw;
}

/* The answer to the touch: one line sized off the component's own type, reserved while empty so a
   result never shifts the control above it, and the first thing to give way when the cell is
   shorter than the two rows want — what the operator presses has to be there whole, and half an
   answer is still worth the room it has. One line only: a long message truncates instead of
   growing rows the cell does not have. A share of the type is a share that carries the readability
   floor itself; the line box follows the size that comes out of it. */
.cmp-command .feedback {
    flex: 0 1 auto;
    height: 1.27em;
    min-height: 0;
    overflow: hidden;
    font-size: max(11px, calc(var(--cmp-type) * 0.79));
    color: var(--color-text-secondary);
    white-space: nowrap;
    text-overflow: ellipsis;
}

.cmp-command .feedback.error {
    color: var(--color-critical);
}

/* The caption a command draws beside or over its control is a text surface like any other
   (ADR 0034): the family's own type size is the tall end of the answer and the line the family
   publishes is the room it is measured against, so the caption grows with the cell instead of
   standing at one size, and what runs past the line ellipsizes with the whole of itself in the
   tooltip. */
.cmp-command .label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--color-text-secondary);
    --display-height: var(--cmp-type);
    --display-avail: var(--display-room);
}

/* The switch is drawn in em so it follows .cmp-command's type size — a toggle in a taller cell is
   a bigger toggle, not a small one adrift in the middle of it. */
.toggle-switch {
    display: inline-flex;
    align-items: center;
    gap: 0.57em;
    min-width: 0;
}

.toggle-switch .track {
    width: 2.86em;
    height: 1.57em;
    flex: 0 0 auto;
    border-radius: var(--radius-pill);
    background: var(--color-neutral);
    position: relative;
    transition: background var(--dur-quick) var(--ease-out);
}

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

.toggle-switch .track .knob {
    position: absolute;
    top: 0.14em;
    left: 0.14em;
    width: 1.29em;
    height: 1.29em;
    border-radius: 50%;
    background: var(--color-text-on-accent);
    transition: transform var(--dur-quick) var(--ease-out);
}

.toggle-switch .track.on .knob {
    transform: translateX(1.29em);
}

/* Alarm banner + procedure tables */
.cmp-table {
    width: 100%;
    height: 100%;
    overflow: auto;
    font-size: 13px;
}

.cmp-table table {
    width: 100%;
    border-collapse: collapse;
}

.cmp-table th,
.cmp-table td {
    text-align: left;
    padding: 4px 8px;
    border-bottom: 1px solid var(--color-stroke);
}

/* ---- Run comments -----------------------------------------------------------
   The timeline is a record, so every recorded line carries the two gestures that keep it honest:
   rewrite it where it stands, or remove it. They ride the trailing edge of the line they act on,
   the removal in the danger ink and last, and the line itself is what gives way when the cell is
   narrow — a cluster that shrank would put the verbs somewhere new on every row.
   Removing asks first and asks in the row, so what is being answered is the very line the eye is
   already on: the question in the quiet ink, and only the key that removes wearing the danger. */
.cmp-comments .comment-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px 6px;
    padding: 2px 0;
    min-width: 0;
}

.cmp-comments .comment-said {
    flex: 1 1 55%;
    min-width: 0;
    overflow-wrap: anywhere;
}

.cmp-comments .comment-at { color: var(--color-text-secondary); }

.cmp-comments .comment-phase { margin: 0 4px; }

.cmp-comments .comment-gestures {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 4px;
    margin-left: auto;
}

/* The question takes what the row has left and wraps under the line rather than pushing the two
   keys off it: the answer must stay reachable at every cell width. */
.cmp-comments .comment-confirm {
    flex: 1 1 auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 4px;
    min-width: 0;
}

.cmp-comments .comment-confirm-said {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--color-text-secondary);
    font-size: 11px;
}

/* A row verb is read beside its line rather than pressed like the command a cell is for, so it takes
   the family's key and steps it down: the list's own small type, a tighter corner and no lift, since
   what stands off the surface here is the cell's command and not a verb over one line of it. The
   family's fill, edge and greyed-out refusal are inherited untouched, and a mistaken press costs
   nothing because the removal only arms. */
.cmp-comments .comment-gestures button,
.cmp-comments .comment-confirm button {
    flex: 0 0 auto;
    padding: 1px 6px;
    border-radius: var(--radius-sm);
    box-shadow: none;
    font-size: 11px;
}

/* Being rewritten, the comment IS the row: the field takes the whole line, in the same box the add
   row below it is typed in, so one habit serves both. */
.cmp-comments .comment-edit {
    flex: 1 1 auto;
    min-width: 0;
    margin-top: 0;
}

/* The alarm banner's three presentations share one box: the summary sentence is always there, the
   severity chips ride under it, and only the list asks for the room a table takes. The box itself
   does not scroll — the rows do, between a head that keeps the sentence and Ack all in place and
   the answer line at the foot. On a full-screen station that is the whole point: the command that
   clears an alarm can never be scrolled out of reach of the finger that needs it. A summary in a
   one-row cell has no rows to scroll and centres in the cell instead of hanging from the top. */
.cmp-alarms {
    /* The size of an answer target, declared once here and raised by whichever signal says the
       finger is the pointer (see the two blocks at the end of this section). */
    --alarm-ack-width: 3.6em;
    --alarm-ack-height: 2em;
    --alarm-ack-row-padding: 2px 8px;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.cmp-alarms.is-summary {
    justify-content: center;
}

.cmp-alarms .alarm-head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex: 0 0 auto;
    padding-right: 8px;
}

.cmp-alarms .alarm-summary {
    flex: 1 1 auto;
    min-width: 0;
    padding: 4px 8px;
    font-weight: var(--font-weight-semibold);
}

.cmp-alarms .alarm-rows {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
}

.cmp-alarms .alarm-severities {
    display: flex;
    flex-wrap: wrap;
    flex: 0 0 auto;
    gap: 4px;
    padding: 0 8px 4px;
}

.cmp-alarms .alarm-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 5px;
    padding: 1px 8px;
    border-radius: var(--radius-pill);
    font-size: 11px;
    line-height: 1.5;
    white-space: nowrap;
}

.cmp-alarms .alarm-chip .count {
    font-weight: var(--font-weight-semibold);
    font-variant-numeric: tabular-nums;
}

/* What still awaits an answer, said quietly beside the count rather than in a second colour: the
   chip already carries the severity, and two inks in one chip would compete over which is the
   meaning. */
.cmp-alarms .alarm-chip .unacked {
    opacity: 0.75;
}

.cmp-alarms .alarm-chip.critical { color: var(--color-critical); background: var(--color-critical-bg); }
.cmp-alarms .alarm-chip.high { color: var(--color-caution); background: var(--color-caution-bg); }
.cmp-alarms .alarm-chip.medium { color: var(--color-accent-text); background: var(--color-accent-bg); }
.cmp-alarms .alarm-chip.low { color: var(--color-text-secondary); background: var(--color-neutral-bg); }

/* A row still awaiting an acknowledge is the one the operator is being asked to answer, so it is
   marked at the edge instead of being repainted — the row's own text stays as readable as its
   neighbours'. */
.cmp-alarms tr.unacked td:first-child {
    border-left: 3px solid var(--color-critical);
    padding-left: 5px;
}

/* An answered alarm whose condition had already cleared has left the reading; the row holds its
   place for a moment so the list does not close the gap under the finger that answered it, and it
   is drawn as what it now is — on its way out, not something still waiting. */
.cmp-alarms tr.settling {
    opacity: 0.5;
}

/* The two answer targets, drawn as targets and not as links: a box of their own, a word inside it
   and one width per column. The row's box keeps that width whether it holds the button or the mark
   of an answer already given, so a column of them reads as a checklist and never jumps as rows are
   answered. The ink is the accent rather than the alarm's own red — what clears an alarm is not a
   second alarm. */
.cmp-alarms .alarm-ack-all,
.cmp-alarms td.alarm-ack > * {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--alarm-ack-width);
    min-height: var(--alarm-ack-height);
    padding: 2px 10px;
    border-radius: 6px;
    font-weight: var(--font-weight-semibold);
    white-space: nowrap;
}

.cmp-alarms .alarm-ack-all,
.cmp-alarms .alarm-ack button {
    flex: 0 0 auto;   /* the head's summary yields room; the command never shrinks under its word */
    border: 1px solid var(--color-stroke);
    background: var(--color-surface);
    color: var(--color-accent-text);
    cursor: pointer;
}

.cmp-alarms .alarm-ack-all:hover:not(:disabled),
.cmp-alarms .alarm-ack button:hover:not(:disabled) {
    border-color: var(--color-accent);
    background: var(--color-accent-bg);
}

/* Blocked, not answered: the box stays where it is and goes quiet. Which of the two a row is
   showing has to be readable at a glance — this one still asks to be pressed and cannot be. */
.cmp-alarms .alarm-ack-all:disabled,
.cmp-alarms .alarm-ack button:disabled {
    color: var(--color-text-disabled);
    cursor: not-allowed;
}

/* Answered: the mark alone, no box and no edge, in the room the button would have taken. */
.cmp-alarms .alarm-ack .acked {
    border: 1px solid transparent;
    color: var(--color-text-secondary);
}

.cmp-alarms td.alarm-ack {
    width: 1%;
    padding: var(--alarm-ack-row-padding);
    text-align: right;
}

/* The answer to a press, on the component's own box (ADR 0020 §E/§F): reserved while empty so a
   result never shifts the rows, and outside the scrolling region so it is read where the press
   happened. It is drawn only by a banner that offers the acknowledge — an informational one keeps
   the whole cell for what it reports. */
.cmp-alarms .feedback {
    flex: 0 0 auto;
    padding: 0 8px 3px;
    font-size: 11px;
    line-height: 1.4;
    min-height: 1.4em;
    color: var(--color-text-secondary);
}

.cmp-alarms .feedback.error {
    color: var(--color-critical);
}

/* Two signals say the finger is the pointer, and either one is enough — a panel PC with a
   maintenance mouse plugged in answers "hover: hover" and would otherwise keep the desk-sized
   target in the very mode the operator works in. So the touch query is one of them and the
   full-screen dashboard, which is how a machine runs, is the other. Both raise the answer targets to
   the 44px minimum (WCAG 2.5.5) and leave the banner's type where it is: what a finger needs is a
   bigger target, not bigger text. Placed after the base declarations so the touch query wins at
   equal specificity; the full-screen anchor outweighs both. */
@media (hover: none) {
    .cmp-alarms {
        --alarm-ack-width: 64px;
        --alarm-ack-height: 44px;
        --alarm-ack-row-padding: 4px 8px;
    }
}

.dnav-page.fullscreen .cmp-alarms {
    --alarm-ack-width: 64px;
    --alarm-ack-height: 44px;
    --alarm-ack-row-padding: 4px 8px;
}

/* The point table: several readings as one list, sharing the table box above. It is a SCROLLING
   list, which is one of the shapes the size-follows-the-cell rule leaves out — the cell decides how
   many rows are visible, not how big each one is — so the rows take the table's own type and no row
   reserves a width of its own.
   The box itself does not scroll — the rows do, so the sentence about what the ceiling held back
   stays at the foot of the cell where it can be read rather than scrolling away under the rows it
   is talking about. */
.cmp-points {
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* A size stated for the list is the list's, in any cell (ADR 0034), and it is the whole list that
   rises: what it has to beat is the table type above, which is why it is said again here. */
.cmp-points.display-stated {
    font-size: var(--display-size);
}

.cmp-points .point-rows {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
}

/* Fixed layout: the columns are declared here instead of following the readings, so a number that
   gains a digit cannot make the whole table re-flow ten times a second, and the name is the only
   column that has to yield room. */
.cmp-points table {
    table-layout: fixed;
}

.cmp-points .point-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The reading, cut with an ellipsis wherever it runs past the column: wider than the column the
   table declared for it, the head would otherwise be drawn over the name beside it and the tail
   clipped away by the scroll box, with neither readable. Cut, it says so with the ellipsis and keeps
   the whole of itself in the tooltip the markup gives every row alike, word or number. */
.cmp-points .point-value {
    width: 45%;
    font-weight: var(--font-weight-semibold);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A NUMBER rests on the right of the column in tabular figures, so a column of them lines up on the
   decimal point and no row jitters as its value ticks (ADR 0034 §5). Scoped to the numeric family: a
   row answering in words is free text instead (ADR 0034 §2) and rests where the table's own cells
   already rest — on the left, which .cmp-table td gives it for nothing. */
.cmp-points .point-value.display-num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* A reading the row does not have is the gap, and the gap is quiet: it is the absence of a
   reading, not an alarm about one. */
.cmp-points .point-value[data-ganter-quality='bad'] {
    color: var(--color-text-secondary);
}

.cmp-points .point-unit {
    width: 4.5em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--color-text-secondary);
}

.cmp-points .point-empty {
    padding: var(--space-3);
    color: var(--color-text-secondary);
    font-size: var(--font-size-eyebrow);
    line-height: 1.4;
}

/* ---- The three adjustments ---------------------------------------------------
   Selector, Setpoint slider and Step keys are members of the command family above: they sit inside
   .cmp-command, so they take its type size off the cell, its field-and-key paints, its disabled
   treatment and its one answer line. What each one adds here is only its own control row — and each
   row is declared 0 0 auto for the family's reason: what the operator's hand goes to is what the
   cell is for, and the line under it is what yields when the two rows want more than he drew. */

.cmp-selector .selector-row,
.cmp-slider .slider-row,
.cmp-step .step-row {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: 0.57em;
    min-width: 0;
}

/* The keys ARE the row: they split what is available and give way together, like the two the Input
   draws over a flag point — a set that never shrank would carry the cell out with it. Their words
   sit on the block span the family's own rule gives every key. */
.cmp-selector .selector-row > .option {
    flex: 1 1 0;
    min-width: 0;
}

/* The option in force is lit — and it is lit off the READING, never off the touch: the key the
   operator pressed goes on looking like every other one until the point reports the mode it asked
   for. The accent is the interface's own, so nothing here holds a palette of its own. */
.cmp-selector .selector-row > .option.active {
    border-color: var(--color-accent);
    background: var(--color-accent-bg);
    color: var(--color-accent-text);
}

.cmp-selector .no-options {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    color: var(--color-text-secondary);
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The picker takes the room the keys would have had, so the list presentation reads at the same
   width as the keys it replaces. */
.cmp-selector .selector-row > .ui-combobox {
    flex: 2 1 5em;
    min-width: 0;
}

/* The travel is what the slider's row is for, so it takes the spare width ahead of the keys; the
   keys never shrink under their own words. */
.cmp-slider .slider-row > .travel {
    flex: 3 1 5em;
    min-width: 0;
}

/* The two keys take the width their captions need and no more — the travel is what the row is for. */
.cmp-slider .slider-row > .release,
.cmp-slider .slider-row > .send {
    flex: 0 0 auto;
}

/* Their captions read at the family's type size and carry its floor themselves: a key whose width is
   its own caption states no room for a reserve to resolve against. The Set key's caption holds a
   value, so its width really does move — it truncates at a share of the line rather than stretching
   the row it shares with the travel. */
.cmp-slider .slider-row > .release > .key,
.cmp-slider .slider-row > .send > .key {
    display: block;
    max-width: 8em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: max(11px, var(--cmp-type));
}

/* The handle is drawn in em off the family's type size, so a slider in a taller cell is a bigger
   slider and not a small one adrift in the middle of it. The track carries the app's own recess and
   the handle the app's own accent; the browser's default paint is dropped so both themes are drawn
   from the tokens rather than from whatever the platform picked. */
.cmp-slider input[type='range'] {
    appearance: none;
    height: 1.6em;
    padding: 0;
    border: 0;
    background: transparent;
    box-shadow: none;
    cursor: pointer;
}

/* The keyboard is how this control is reached without a pointer — the arrow keys move the handle a
   step at a time and each press arms exactly as a drag does — so the ring that says where the focus
   is has to survive the family's quiet field treatment. The pointer keeps its quiet field: the ring
   is drawn for the focus a keyboard put there. */
.cmp-slider input[type='range']:focus-visible {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: var(--focus-ring-offset);
}

.cmp-slider input[type='range']::-webkit-slider-runnable-track {
    height: 0.36em;
    border-radius: var(--radius-pill);
    background: var(--color-surface-alt);
    box-shadow: var(--shadow-inset-well);
}

.cmp-slider input[type='range']::-moz-range-track {
    height: 0.36em;
    border-radius: var(--radius-pill);
    background: var(--color-surface-alt);
    box-shadow: var(--shadow-inset-well);
}

.cmp-slider input[type='range']::-webkit-slider-thumb {
    appearance: none;
    width: 1.1em;
    height: 1.1em;
    margin-top: -0.37em;
    border: 2px solid var(--color-surface);
    border-radius: 50%;
    background: var(--color-accent);
    box-shadow: var(--shadow-card);
}

.cmp-slider input[type='range']::-moz-range-thumb {
    width: 1.1em;
    height: 1.1em;
    border: 2px solid var(--color-surface);
    border-radius: 50%;
    background: var(--color-accent);
    box-shadow: var(--shadow-card);
}

/* Armed: the handle is the operator's hand and no longer the plant's position, and the caution ink
   is the whole of what says so — calmly, because nothing has been written yet and the key beside it
   already names what would be. */
.cmp-slider.is-armed input[type='range']::-webkit-slider-thumb {
    background: var(--color-caution);
}

.cmp-slider.is-armed input[type='range']::-moz-range-thumb {
    background: var(--color-caution);
}

/* Nothing to draw at: no reading and nothing armed is no position, so the handle is not drawn at
   all and the track states the absence the way every other surface does. A thumb parked at the
   minimum would be a setpoint the screen invented. */
.cmp-slider.no-handle input[type='range']::-webkit-slider-thumb {
    visibility: hidden;
}

.cmp-slider.no-handle input[type='range']::-moz-range-thumb {
    visibility: hidden;
}

.cmp-slider.no-handle input[type='range']::-webkit-slider-runnable-track {
    background: transparent;
    box-shadow: none;
    border: 1px dashed var(--color-neutral);
}

.cmp-slider.no-handle input[type='range']::-moz-range-track {
    background: transparent;
    box-shadow: none;
    border: 1px dashed var(--color-neutral);
}

/* The two keys ARE the row: they split it and give way with it, like the two the Input draws over
   a flag point — and a − and a + are the two widest-reaching targets on a touch panel, so the row
   the cell gave is theirs whole. */
.cmp-step .step-row > .step {
    flex: 1 1 0;
    min-width: 0;
}

/* A sign asks for its own width off the family's type size, and nothing else asks for room in
   there. */
.cmp-step .step-row > .step > .display-text {
    --display-avail: calc(var(--cmp-type) * 0.68);
}

.cmp-status {
    display: flex;
    flex-direction: column;
    justify-content: center;
    height: 100%;
    padding: 4px 10px;
    gap: 4px;
}

.badge {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 10px;
    font-size: 12px;
    font-weight: 600;
}

.badge.ok { background: var(--color-success-bg); color: var(--color-success); }
.badge.nok { background: var(--color-critical-bg); color: var(--color-critical); }
.badge.neutral { background: var(--color-overlay-grid); color: var(--color-text-secondary); }

.cmp-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    color: var(--color-text-secondary);
    font-size: 12px;
}

/* A component this build cannot render. It states what is in the cell and what to do about it,
   in the same quiet register as an empty state — the screen is intact, one piece of it is not
   available, and nothing about that is an error. */
.cmp-unsupported {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    padding: var(--space-2);
    overflow: hidden;
    border: 1px dashed var(--color-stroke);
    border-radius: var(--radius-md);
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
    line-height: 1.4;
    text-align: center;
}

.cmp-unsupported strong {
    min-width: 0;
    color: var(--color-text);
    font-weight: var(--font-weight-semibold);
    overflow-wrap: anywhere;
}

/* ---- Editor workbench: real canvas flanked by page-level task tools -------- */
/* The host the stage draws into, on every page that mounts the runtime or the editor: it is markup
   of the shared canvas, so it is dressed here beside the rest of what that canvas draws rather than
   in the stylesheet of one page that happens to mount it. */
.dnav-canvas-host {
    flex: 1 1 auto;
    /* Without this the flex item's min-height is its content — the runtime grid holding the
       chart canvases at their last measured pixels — so a window made smaller could never take
       height back from the stage. Zero frees the shrink; the canvases refit right after. */
    min-height: 0;
    position: relative;
    width: 100%;
    overflow: hidden;
    background: var(--color-bg);
}

.dashboard-editor-stage {
    min-width: 0;
    min-height: 0;
    position: relative;
    z-index: 0;
    isolation: isolate;
}

.edit-canvas-host {
    width: 100%;
    height: 100%;
    position: relative;
    overflow: hidden;
}

.edit-grid {
    background-image:
        linear-gradient(to right, var(--color-overlay-grid) 1px, transparent 1px),
        linear-gradient(to bottom, var(--color-overlay-grid) 1px, transparent 1px);
    /* background-size is set inline per the dashboard's column/row count. */
}

.edit-cmp {
    cursor: move;
    border: 1px solid transparent;
    border-radius: 4px;
    touch-action: none;
}

.editor-selection-overlay {
    pointer-events: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 var(--focus-ring-width) var(--color-accent);
}

.editor-selection-overlay .resize-grip {
    pointer-events: auto;
}

/* Clicks select/drag the cell, never reach the rendered component. */
.edit-pointerless {
    pointer-events: none;
}

/* The press lands on the background layer rather than falling through to the cell: the cell sits
   below every component band, so only a component layer can answer in the order the operator
   sees. The terminal handles keep winning — they ride the content band, above this one. */
.edit-cmp > .dash-component-chrome {
    pointer-events: auto;
}

.symbol-anchor-guide-note {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin: var(--space-3) 0;
    padding-left: var(--space-3);
    border-left: 2px solid var(--color-accent);
}

.symbol-anchor-guide-note strong {
    color: var(--color-text);
    font-weight: var(--font-weight-semibold);
}

.resize-grip {
    position: absolute;
    right: -4px;
    bottom: -4px;
    width: 14px;
    height: 14px;
    background: var(--color-accent);
    border: 2px solid var(--color-surface);
    border-radius: 3px;
    cursor: nwse-resize;
}

.dashboard-tools-splitter {
    min-width: 0;
    overflow: hidden;
}

/* Collapsed there is no pane width to drag, so the gutter is spacing only: the splitter keeps its
   grid track — the tool rail stands off the stage by the same air as the open pane — and gives up
   its hairline, its grip and the pointer. */
.dashboard-tools-splitter.collapsed {
    pointer-events: none;
    visibility: hidden;
}

.dashboard-tools {
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-lg);
    background: var(--color-surface-alt);
}

.dashboard-tools.collapsed {
    border-radius: var(--radius-md);
}

.dashboard-tools-header {
    flex: 0 0 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-2);
    border-bottom: 1px solid var(--color-stroke);
}

.dashboard-tools-tabs {
    flex: 1 1 auto;
    min-width: 0;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-1);
}

.dashboard-tools-tabs.collapsed {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-2);
}

.dashboard-tools-tab {
    min-width: 0;
    min-height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-2);
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
    cursor: pointer;
}

.dashboard-tools-tab span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dashboard-tools-tab:hover {
    background: var(--color-overlay-hover);
    color: var(--color-text);
}

.dashboard-tools-tab.active {
    border-color: var(--color-stroke);
    background: var(--color-surface);
    color: var(--color-accent-text);
    font-weight: var(--font-weight-semibold);
}

.dashboard-tools-tab:focus-visible,
.dashboard-tools-collapse:focus-visible {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: calc(-1 * var(--focus-ring-offset));
}

.dashboard-tools-collapse {
    flex: 0 0 40px;
}

.dashboard-tools-content {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: var(--space-4);
}

/* The region of a task panel that ANSWERS something, held apart from the heading above it. It
   repeats the panel's own column: the fields inside stack exactly as they did under it. */
.dashboard-task-form {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.dashboard-task-panel {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.dashboard-task-heading {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

.dashboard-task-heading h2 {
    margin: 0;
    color: var(--color-text);
    font-size: var(--font-size-card-title);
    font-weight: var(--font-weight-semibold);
}

.dashboard-task-heading h2:focus-visible {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: var(--focus-ring-offset);
    border-radius: var(--radius-sm);
}

.dashboard-task-heading p {
    margin: var(--space-1) 0 0;
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
    line-height: 1.4;
    text-wrap: pretty;
}

/* The Properties header names what is being edited: the kind's palette mark and name, and under
   them the component's own name — or, when it has none, whatever already identifies it. It rides
   the heading's own line, flushed to the far end, so it reads as an answer to "Properties of what"
   rather than as a second title under it, and takes no height off the panel it heads. It is also
   the side that gives way: the heading keeps its word and a long name ellipsizes below. */
.dashboard-properties-identity {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex: 0 1 auto;
    min-width: 0;
    margin-left: auto;
}

.dashboard-properties-identity-icon {
    flex: 0 0 auto;
    color: var(--color-text-secondary);
}

.dashboard-properties-identity-text {
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    text-align: right;
}

.dashboard-properties-identity-type,
.dashboard-properties-identity-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dashboard-properties-identity-type {
    color: var(--color-text);
    font-size: var(--font-size-description);
    font-weight: var(--font-weight-semibold);
}

.dashboard-properties-identity-name {
    color: var(--color-text-secondary);
    font-size: var(--font-size-eyebrow);
}

.dashboard-task-count {
    flex: 0 0 auto;
    min-width: 24px;
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--color-surface);
    color: var(--color-text-secondary);
    font-size: var(--font-size-micro);
    font-variant-numeric: tabular-nums;
    text-align: center;
}

.dashboard-task-filters {
    display: grid;
    grid-template-columns: minmax(0, 1.35fr) minmax(112px, .65fr);
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.dashboard-palette-list,
.dashboard-component-list,
.dashboard-properties-stack {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.dashboard-component-search {
    margin: 0 0 var(--space-3);
}

/* Glyph + text, like Add: the icon keeps a fixed column and identification, type
   and grid address stack beside it — never as a third text line above them. */
.dashboard-component-row {
    width: 100%;
    min-height: 56px;
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background: transparent;
    color: var(--color-text);
    text-align: left;
    cursor: pointer;
}

.dashboard-component-row:hover {
    background: var(--color-overlay-hover);
}

.dashboard-component-row.selected {
    border-color: var(--color-stroke);
    background: var(--color-surface);
    box-shadow: inset 3px 0 0 var(--color-accent);
}

.dashboard-component-row:focus-visible {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: calc(-1 * var(--focus-ring-offset));
}

.dashboard-component-row-main {
    min-width: 0;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.dashboard-component-row-main strong,
.dashboard-component-row-main span,
.dashboard-component-location {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dashboard-component-row-main strong {
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-semibold);
}

.dashboard-component-row-main span,
.dashboard-component-location {
    color: var(--color-text-secondary);
    font-size: var(--font-size-eyebrow);
}

.dashboard-component-location {
    width: 100%;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    text-align: left;
}

.dashboard-task-empty {
    margin-top: var(--space-3);
    padding: var(--space-5);
    border: 1px dashed var(--color-stroke);
    border-radius: var(--radius-md);
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
    line-height: 1.5;
    text-align: center;
}

.dashboard-task-empty.prominent {
    margin-top: var(--space-5);
}

.edit-section-title {
    font-size: 14px;
    font-weight: 600;
    margin: 12px 0 6px;
}

.edit-section-sub {
    font-size: 12px;
    font-weight: 600;
    color: var(--color-text-secondary);
    margin: 10px 0 4px;
}

.palette-category {
    font-size: 11px;
    font-weight: 600;
    color: var(--color-text-secondary);
    margin: 8px 0 2px;
}

.palette-item {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    cursor: grab;
    border: 1px solid transparent;
    background: transparent;
    color: var(--color-text);
    text-align: left;
}

.palette-item:hover {
    background: var(--color-overlay-grid);
    border-color: var(--color-stroke);
}

.palette-item .pal-name { font-size: 13px; }
.palette-item .pal-desc { font-size: 11px; color: var(--color-text-secondary); }

.edit-field {
    display: block;
    font-size: 12px;
    color: var(--color-text-secondary);
    margin: 8px 0 0;
}

.edit-field input,
.edit-field select,
.edit-field textarea {
    display: block;
    width: 100%;
    margin-top: 3px;
    padding: 6px 8px;
    border-radius: 6px;
    border: 1px solid var(--color-stroke);
    background: var(--color-surface);
    color: var(--color-text);
    font-size: 13px;
    box-sizing: border-box;
}

.edit-row {
    display: flex;
    gap: 6px;
    align-items: center;
    margin-top: 6px;
}

/* A row left with only its handles — the ordering arrows of a band whose scale has no state to word
   — keeps them at the end of the line they have always sat at. Nothing stretches to push them there
   any more, and at the left edge a pair of arrows reads as the row's subject instead of its grip. */
.edit-row.edit-row-end {
    justify-content: flex-end;
}

.edit-row input,
.edit-row select { flex: 1 1 0; min-width: 0; padding: 6px 8px; border-radius: 6px;
    border: 1px solid var(--color-stroke); background: var(--color-surface); color: var(--color-text); font-size: 13px; }

/* The shared ComboBox in the editor: block-level (drops below its caption) and sized like the
   fields beside it; in a row two share the width just as the old <select>s did. */
.edit-field .ui-combobox,
.edit-row .ui-combobox {
    display: flex;
    width: 100%;
    min-width: 0;
    padding: 6px 8px;
    font-size: 13px;
}
.edit-field .ui-combobox { margin-top: 3px; }
.edit-row .ui-combobox { flex: 1 1 0; }

.edit-row button,
.editor-property-section-body > button,
.dashboard-danger-zone button {
    padding: 6px 12px;
    border-radius: 6px;
    border: 1px solid var(--color-stroke);
    background: var(--color-surface);
    color: var(--color-text);
    cursor: pointer;
    font-size: 13px;
}

/* The single home of the plain red button. Wherever a dashboard surface draws a bare button that
   removes something — the editor's property panels, its danger zone, the run comments — it asks for
   this class and wears the critical ink on its edge and its text, keeping whatever shape its own
   context already gave it, so a tone that moves moves once, here. The button says it, rather than an
   ancestor saying it for a class it happens to contain: a red that is inherited from where a button
   sits reaches whatever else lands in that box. It carries the same weight as the neutral chrome
   declared above, so it has to stay below it to win the edge. */
button.dash-btn-danger {
    border-color: var(--color-critical);
    color: var(--color-critical);
}

/* A generous gap, not the tight one lists use: each section already reads as a card of its own
   — its own border and fill — and the space between confirms it, so the stack reads as the
   categories it holds instead of one continuous run of fields. */
.editor-property-section {
    margin: 0 0 var(--space-6);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    overflow: hidden;
}

.editor-property-section:not([open]) > :not(summary) {
    display: none;
}

.editor-property-section.has-issue {
    border-color: var(--color-caution);
}

.editor-property-section > summary,
.editor-property-section-summary {
    min-height: 40px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    color: var(--color-text);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-semibold);
    cursor: pointer;
    user-select: none;
    list-style: none;
}

.editor-property-section > summary::-webkit-details-marker {
    display: none;
}

/* A section IS a disclosure and has to look like one, or the bar passes for a card's title and the
   body underneath is only found by accident. The chevron points right while the section is closed
   and down while it is open — the same mark and the same turn as the shared .ui-disclosure — and it
   trails the row so the title stays flush with the fields it heads. */
.editor-property-section > summary::after {
    content: "";
    flex: 0 0 auto;
    width: 7px;
    height: 7px;
    border-right: 1.5px solid var(--color-text-secondary);
    border-bottom: 1.5px solid var(--color-text-secondary);
    transform: rotate(-45deg);
    transition: transform var(--dur-press) var(--ease-out);
}

.editor-property-section[open] > summary::after {
    transform: rotate(45deg);
}

.editor-property-section > summary:hover::after {
    border-color: var(--color-text);
}

.editor-property-section > summary:hover,
.editor-property-section-summary:hover {
    background: var(--color-overlay-hover);
}

.editor-property-section > summary:focus-visible,
.editor-property-section-summary:focus-visible {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: calc(-1 * var(--focus-ring-offset));
}

.editor-property-section-body {
    padding: 0 var(--space-4) var(--space-4);
    border-top: 1px solid var(--color-stroke-subtle);
}

/* The auto margin takes the whole gap, which leaves the badge sitting against the chevron instead
   of adrift between the title and it. */
.editor-section-status {
    margin-left: auto;
    color: var(--color-caution);
    font-size: var(--font-size-micro);
    font-weight: var(--font-weight-semibold);
}

.editor-layer-actions {
    align-items: stretch;
}

.editor-layer-actions button {
    flex: 1 1 0;
}

.editor-actions-section .editor-property-section-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-3);
    padding-top: var(--space-4);
}

.editor-field-error {
    margin-top: var(--space-2);
    color: var(--color-critical);
    font-size: var(--font-size-description);
}

.dashboard-visibility-title {
    margin-top: var(--space-6);
}

.edit-field.edit-toggle.compact {
    margin-top: var(--space-2);
}

.dashboard-danger-zone {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    margin-top: var(--space-7);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-critical);
}

.dashboard-danger-zone > div {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.dashboard-danger-zone strong {
    color: var(--color-critical);
    font-size: var(--font-size-small);
}

.dashboard-danger-zone span {
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
}

/* Icon picker grid (editor) — a scrollable grid of glyph buttons. */
.icon-grid {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 2px;
    max-height: 150px;
    overflow-y: auto;
    margin: 4px 0 2px;
    border: 1px solid var(--color-stroke);
    border-radius: 6px;
    padding: 4px;
}

.icon-grid .icon-cell {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 5px;
    cursor: pointer;
    color: var(--color-text);
}

.icon-grid .icon-cell:hover { background: var(--color-overlay-grid); }
.icon-grid .icon-cell.sel { border-color: var(--color-accent); background: var(--color-overlay-grid); }
.icon-grid .icon-cell .glyph { font-family: 'FluentSystemIcons-Resizable'; font-size: 18px; line-height: 1; }

/* The button a repeated row opens that grid from — the grid is far taller than a row, so the row
   carries the mark it named and the picker unfolds under it. It is a finger's target first: the
   mark is drawn at the cell's own size, which is what gives the button its height and width, and
   the name it stands for only appears when the catalog carries no mark for it — truncated there,
   rather than pushing the row wide. The card's own gesture rule (.edit-card-body .edit-row > button)
   keeps it from stretching. */
.icon-grid-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 32px;
    max-width: 45%;
}

.icon-grid-trigger > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.icon-grid-trigger .ui-glyph { font-size: 18px; color: var(--color-text); }

/* Open, the button is the way back out of the grid under it, so it says so. */
.icon-grid-trigger[aria-expanded="true"] { border-color: var(--color-accent); }

.edit-hint {
    font-size: 11px;
    color: var(--color-text-secondary);
    margin-top: 10px;
    line-height: 1.4;
}

/* A hint that reports a real consequence — a retention the station could not grant whole. It stays
   a hint: the tint is what makes it findable, not a banner, because the chart still works. */
.edit-hint.is-capped {
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--color-caution-bg);
    color: var(--color-caution);
}

/* What the bound point reads right now, under the address field. It confirms a binding, so it
   sits close to the field and stays quieter than it — the value is the only part in the text
   ink, and it never wraps the panel wider than the field above. */
.editor-address-reading {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    min-width: 0;
    margin-top: var(--space-1);
    font-size: var(--font-size-eyebrow);
    line-height: 1.4;
    color: var(--color-text-secondary);
}

.editor-address-reading-lead {
    flex: 0 0 auto;
}

.editor-address-reading-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.editor-address-reading.is-value .editor-address-reading-text {
    color: var(--color-text);
    font-family: var(--font-mono);
}

/* No value to show is information, not an alarm: a soft caution tint, no icon, no weight. */
.editor-address-reading.is-gap .editor-address-reading-text {
    color: var(--color-caution);
}

/* The paint beside the hex — what actually confirms a colour binding. It carries a hairline of its
   own because a colour close to the panel's surface would otherwise have no edge at all, and it
   centres on the line rather than sitting on its baseline, which an empty box has no business
   having. */
.editor-address-reading-swatch {
    flex: 0 0 auto;
    align-self: center;
    width: 12px;
    height: 12px;
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sm);
}

/* A presentation literal is drawn rather than written: the code point in the station's own icon
   font, a user icon's artwork, the picture an asset id names. Each is held to the height of the
   line, so the confirmation stays one quiet row under the field instead of becoming a second
   panel. */
.editor-address-reading-glyph {
    flex: 0 0 auto;
    align-self: center;
    font-family: 'FluentSystemIcons-Resizable';
    font-size: 16px;
    line-height: 1;
    color: var(--color-text);
}

.editor-address-reading-artwork {
    display: inline-flex;
    flex: 0 0 auto;
    align-self: center;
    color: var(--color-text);
}

.editor-address-reading-artwork svg {
    width: 16px;
    height: 16px;
}

/* The thumbnail keeps the picture's own proportion inside a fixed box and shows the recessed fill
   around it, so a transparent or narrow image reads as the image it is and not as a load that
   failed. */
.editor-address-reading-thumb {
    flex: 0 0 auto;
    align-self: center;
    width: 28px;
    height: 20px;
    object-fit: contain;
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sm);
    background: var(--color-surface-alt);
}

/* Why there is no drawing, beside the value that produced it. It takes the gap's own tint because
   it is the same kind of statement — something is missing — while the value keeps the text ink,
   since seeing what the point actually holds is what tells the operator which point he meant. */
.editor-address-reading-note {
    flex: 0 0 auto;
    color: var(--color-caution);
}

.edit-field.edit-toggle {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    margin-top: var(--space-4);
    color: var(--color-text);
}

.edit-field.edit-toggle > input {
    flex: 0 0 auto;
    width: auto;
    margin: 1px 0 0;
}

/* One row of a repeated list — a series, a band, a state, an option, a point — as a card of its
   own. Stacked as bare fields the rows run into each other: the same captions repeat down the rail
   and nothing says where one ends. So each row is boxed and headed, and the head is what names it,
   opens it and removes it. The border is what makes two of them countable at a glance; the fields
   under it are exactly the ones that were there before. */
.edit-card {
    margin-top: var(--space-4);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    overflow: hidden;
}

/* The row's own ink down the leading edge. The dot in the head is a detail at the end of a line and
   is read only once the eye is already on that line; the rule is the width of the whole card, so two
   stacked rows are told apart before either name is read — which is what a list of series needs when
   the canvas beside it draws them in exactly these colours. A row nobody gave a colour keeps the
   plain edge, for the same reason its dot is a ring: the drawing takes the palette's next turn
   there, and a rule painted in a stand-in would be a colour the canvas is not using. */
.edit-card.has-ink {
    border-left: 3px solid var(--edit-card-ink);
}

/* A band rather than a hairline between one card and the next: the head carries a fill of its own,
   so where a row ends and the following one begins survives a long card whose own fields already
   fill the panel's width. */
.edit-card-head {
    display: flex;
    align-items: stretch;
    background: var(--color-surface-alt);
}

/* The chart's own structure, carded like the rows that reference it: same box, same head, same fold
   — a plot and a ruler are read exactly the way a series is. What they are not is rows somebody
   added, so they carry no ink of their own; the family reads as one in the accent instead, which is
   what separates "the stage" from "the series drawn on it" at a glance. */
.edit-card-axis {
    border-left: 3px solid var(--color-accent);
}

.edit-card-axis > .edit-card-head {
    background: var(--color-accent-bg);
}

/* The head IS the disclosure, minus the removal: the target a finger has to find is the whole row
   and not a chevron at the end of it. */
.edit-card-toggle {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 34px;
    padding: var(--space-2) var(--space-3);
    border: 0;
    background: transparent;
    color: var(--color-text);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-semibold);
    text-align: left;
    cursor: pointer;
}

.edit-card-toggle:hover {
    background: var(--color-overlay-hover);
}

.edit-card-toggle:focus-visible {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: calc(-1 * var(--focus-ring-offset));
}

/* The same mark and the same turn as the property section above it — a card is that gesture one
   level down, not a new one. */
.edit-card-toggle::after {
    content: "";
    flex: 0 0 auto;
    margin-left: auto;
    width: 6px;
    height: 6px;
    border-right: 1.5px solid var(--color-text-secondary);
    border-bottom: 1.5px solid var(--color-text-secondary);
    transform: rotate(45deg);
    transition: transform var(--dur-press) var(--ease-out);
}

.edit-card.is-collapsed .edit-card-toggle::after {
    transform: rotate(-45deg);
}

/* The ink the row is drawn in, so a list is scanned by colour before it is read. It carries a
   hairline of its own, because a colour close to the panel's surface would have no edge at all. */
/* A row whose colour nobody chose says so: the drawing takes the palette's next turn there, and a
   filled dot would give two fresh rows one colour while the canvas draws them apart. */
.edit-card-dot.is-unset {
    background: none;
    border: 1px solid var(--color-stroke);
}

.edit-card-dot {
    flex: 0 0 auto;
    width: 10px;
    height: 10px;
    border: 1px solid var(--color-stroke);
    border-radius: 50%;
}

.edit-card-name {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* What the row holds, beside what it is called — and the half that gives way first, by a wide margin
   and never past half the head, because the name is what the list is scanned by and the address
   behind it is only the confirmation. Sharing the shrink evenly is what cut "Voltage" to "Volta…"
   beside an address written out in full. */
.edit-card-detail {
    flex: 0 8 auto;
    max-width: 50%;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--color-text-secondary);
    font-size: var(--font-size-micro);
    font-weight: var(--font-weight-regular);
}

.edit-card-remove {
    flex: 0 0 auto;
    min-width: 34px;
    padding: 0 var(--space-3);
    border: 0;
    border-left: 1px solid var(--color-stroke);
    background: transparent;
    color: var(--color-critical);
    font-size: 13px;
    cursor: pointer;
}

.edit-card-remove:hover {
    background: var(--color-overlay-hover);
}

.edit-card-remove:focus-visible {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: calc(-1 * var(--focus-ring-offset));
}

.edit-card-body {
    padding: var(--space-3);
    border-top: 1px solid var(--color-stroke-subtle);
}

/* Folded away rather than unmounted, so a half-typed field is still there when the card reopens. */
.edit-card.is-collapsed > .edit-card-body {
    display: none;
}

.edit-card-body > .edit-row:first-child,
.edit-card-body > .edit-field:first-child {
    margin-top: 0;
}

/* A row's gestures stay square beside the field they act on instead of splitting the row evenly
   with it, and a gesture the row at the end of the list cannot make says so. */
.edit-card-body .edit-row > button {
    flex: 0 0 auto;
    min-width: 30px;
}

.edit-card-body .edit-row > button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Inside a card the colour pair sits under its own caption like any other field, so the swatch must
   not claim the caption's indent the standalone accent pair uses. */
.edit-card-body .edit-color-row {
    gap: var(--space-2);
    margin-top: 3px;
}

.edit-card-body .edit-field.edit-toggle {
    margin-top: var(--space-2);
    align-items: center;
    font-size: 12px;
    color: var(--color-text-secondary);
}

/* The one switch in a card that decides whether any of the fields above it reach the canvas. As one
   more quiet caption at the end of a row it is the last thing found and the first thing wanted, so
   it takes a bar of its own across the foot of the card: full width, the card's own text ink, and a
   fill that separates it from the fields it governs. */
.edit-card-body .edit-field.edit-toggle.edit-card-switch {
    margin-top: var(--space-3);
    padding: var(--space-2) var(--space-3);
    gap: var(--space-3);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sm);
    background: var(--color-surface-alt);
    color: var(--color-text);
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-medium);
}

/* A series switched off is still being edited, so nothing dims the fields the operator is typing
   into: what goes quiet is the ink, the part of the card that stands for the drawing, which is how
   "not on the canvas" is readable down the rail without costing the row any legibility. */
.edit-card.is-off {
    border-left-color: var(--color-stroke);
}

.edit-card.is-off .edit-card-dot {
    opacity: 0.3;
}

/* The gauge's named starts share the row evenly: the choice between them is one choice, and none of
   the three is the main one. Each is its arc over its name — the shape is what the three differ by,
   so it leads and the word underneath confirms it. */
.edit-presets button {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-2) var(--space-1);
}

.edit-presets button > span {
    max-width: 100%;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.edit-presets .choice-mark {
    width: 100%;
    max-width: 44px;
}

/* Background colour — one row per theme, both on screen at once, because the value IS the pair.
   Each row is a swatch to pick with and the hex to read, type or paste. */
.edit-color-pair {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.edit-color-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: 12px;
    color: var(--color-text-secondary);
}

.edit-color-row > span {
    flex: 0 0 2.6rem;
}

.edit-color-swatch {
    flex: 0 0 auto;
    width: 34px;
    height: 28px;
    padding: 2px;
    border: 1px solid var(--color-stroke);
    border-radius: 6px;
    background: var(--color-surface);
    cursor: pointer;
}

/* A swatch standing on a colour nobody chose: the palette's turn, shown so the cell agrees with the
   drawing instead of asserting one waiting colour for every undeclared channel. The colour inside
   stays exact — it is the thing the cell exists to report — so what carries "nobody chose this" is
   the frame: dashed, the same stroke width, so the swatch neither grows nor shifts when a colour is
   picked and the frame closes. */
.edit-color-swatch.is-dealt {
    border-style: dashed;
    border-color: var(--color-text-secondary);
}

.edit-color-hex {
    flex: 1 1 0;
    min-width: 0;
    padding: 6px 8px;
    border: 1px solid var(--color-stroke);
    border-radius: 6px;
    background: var(--color-surface);
    color: var(--color-text);
    font-size: 13px;
    box-sizing: border-box;
}

/* Symbol gallery — shared by the Dashboard property panel and the full Assets
   manager. The cards, the result line and the paging are the shared asset-library grid
   (.ui-asset-* in shared.css); what lives here is the four-field filter row a narrow tools pane
   needs, and what a symbol card shows beyond the shared anatomy. It stays visually quiet like an
   equipment catalog: neutral artwork and surfaces, with the signal blue reserved for the
   operator's current selection. */
.symbol-gallery {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-width: 0;
}

/* The gallery is a flex column that already spaces its own rows, so the grid's stand-off margins
   would stack on that gap and leave the symbol library looser than its four siblings. */
.symbol-gallery .ui-asset-results { margin-bottom: 0; }
.symbol-gallery .ui-asset-pagination { margin-top: 0; }

.symbol-gallery-filters {
    display: grid;
    grid-template-columns: minmax(220px, 1.35fr) minmax(160px, .9fr) minmax(150px, .8fr) minmax(104px, .55fr);
    gap: var(--space-3);
    align-items: end;
}

/* The grid sizes every track, so the Origin field must not also claim a floor of its own —
   in a narrow tools pane that floor is what would overflow the row. */
.symbol-gallery-filters .ui-asset-filter-origin {
    min-width: 0;
}

.symbol-gallery-library-note {
    display: grid;
    grid-template-columns: minmax(120px, max-content) minmax(0, 1fr);
    gap: var(--space-3);
    align-items: baseline;
    padding: var(--space-3) var(--space-4);
    border-left: 2px solid var(--color-accent);
    background: var(--color-neutral-bg);
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
}

.symbol-gallery-library-note strong {
    color: var(--color-text);
    font-weight: var(--font-weight-semibold);
}

/* A symbol card is wider than a glyph or a swatch: the artwork is judged in it, and it carries
   the library eyebrow and the I/O rail besides. */
.symbol-gallery-grid {
    --ui-asset-card-min: 176px;
}

.symbol-preview {
    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-color: var(--color-surface-alt);
    background-image: radial-gradient(circle, var(--color-symbol-stage-grid) 1px, transparent 1px);
    background-size: 12px 12px;
    color: var(--color-text-secondary);
    pointer-events: none;
}

.symbol-preview > svg {
    display: block;
    width: calc(100% - var(--space-4));
    height: calc(100% - var(--space-4));
    max-width: calc(100% - var(--space-4));
    max-height: calc(100% - var(--space-4));
}

.symbol-preview-empty {
    color: var(--color-text-disabled);
    font-size: var(--font-size-section);
}

/* The library the symbol belongs to, as a quiet eyebrow above the artwork — it takes the accent
   with the card so the selection reads as one mark, not two. */
.symbol-gallery-library {
    min-width: 0;
    overflow: hidden;
    color: var(--color-text-secondary);
    font-family: var(--font-mono);
    font-size: var(--font-size-micro);
    font-weight: var(--font-weight-medium);
    letter-spacing: .04em;
    line-height: 1.2;
    text-overflow: ellipsis;
    text-transform: uppercase;
    white-space: nowrap;
}

.ui-asset-card.selected .symbol-gallery-library {
    color: var(--color-accent-text);
}

/* A small terminal-like footer makes the symbol's declared I/O surface discoverable without
   animating every preview or spending semantic alarm colours as decoration. */
.symbol-gallery-slot-rail {
    padding-top: var(--space-2);
    border-top: 1px solid var(--color-stroke-subtle);
    color: var(--color-text-secondary);
    font-family: var(--font-mono);
    font-size: var(--font-size-micro);
    font-variant-numeric: tabular-nums;
}

.symbol-gallery-empty {
    min-height: 160px;
    gap: var(--space-3);
    border: 1px dashed var(--color-stroke);
    border-radius: var(--radius-md);
    background: var(--color-surface-alt);
}

.symbol-gallery.compact .symbol-gallery-filters {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

.symbol-gallery.compact .symbol-gallery-search {
    grid-column: 1 / -1;
}

.symbol-gallery.compact .symbol-gallery-grid {
    --ui-asset-card-min: 140px;
}

@media (max-width: 640px) {
    .symbol-gallery-filters {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }

    .symbol-gallery-search {
        grid-column: 1 / -1;
    }

    .symbol-gallery-library-note {
        grid-template-columns: 1fr;
        gap: var(--space-1);
    }
}

/* Gauge (ADR 0030 §4) shares the chart host, over the line that names what it is reading. */
.cmp-gauge {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
}

/* The word for the state the dial is in. It is a line of the cell and not an overlay on the arc, so
   the number the engine writes in the arc's own gap keeps every pixel it had. The line holds its
   height while nothing is named — the arc above it must not grow and shrink as a reading crosses a
   bound — and truncates with an ellipsis, its whole word in the tooltip, because it is a name the
   operator typed and not a measurement. */
.cmp-gauge .gauge-state {
    flex: 0 0 auto;
    min-height: 1.2em;
    font-size: clamp(11px, calc(11cqh * var(--cmp-scale, 1)), 14px);
    line-height: 1.2;
    text-align: center;
    color: var(--color-text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Animated SVG process symbol (ADR 0030 §4). The root does NOT clip: it holds the artwork and the
   anchor overlay as siblings, and the anchors sit on the view-box border. Clipping belongs to the
   artwork wrapper below, which fills the same rect. */
.cmp-symbol {
    position: relative;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    container-type: size;
}

/* Scaled or oversized artwork stays inside the component cell — the anchor overlay does not. */
.cmp-symbol-image {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    container-type: size;
}

.symbol-runtime-surface,
.cmp-symbol-anchor-layer {
    --symbol-layout-width: 100cqw;
    --symbol-layout-height: 100cqh;
    flex: 0 0 auto;
    transform-origin: center;
}

.symbol-runtime-surface.symbol-rotation-quarter,
.cmp-symbol-anchor-layer.symbol-rotation-quarter {
    /* Rotate inside a swapped envelope so a 6×2 linear symbol remains fully visible at 90°/270°. */
    --symbol-layout-width: 100cqh;
    --symbol-layout-height: 100cqw;
}

.symbol-fit-contain,
.symbol-fit-stretch {
    width: var(--symbol-layout-width);
    height: var(--symbol-layout-height);
}

.symbol-fit-stretch-x {
    width: var(--symbol-layout-width);
    height: auto;
    max-height: var(--symbol-layout-height);
    aspect-ratio: var(--symbol-viewbox-aspect, auto);
}

.symbol-fit-stretch-y {
    width: auto;
    max-width: var(--symbol-layout-width);
    height: var(--symbol-layout-height);
    aspect-ratio: var(--symbol-viewbox-aspect, auto);
}

.symbol-runtime-surface > svg,
.cmp-symbol-anchor-layer > svg {
    display: block;
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
}

.cmp-symbol-anchor-host {
    position: absolute;
    z-index: 4;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: visible;
    pointer-events: none;
    container-type: size;
}

.cmp-symbol-anchor-layer > svg {
    overflow: visible;
}

.symbol-anchor {
    --symbol-anchor-color: var(--color-accent);
    --symbol-anchor-fill: var(--color-surface);
    --symbol-anchor-halo-fill: var(--color-surface);
    --symbol-anchor-mark: var(--color-text-on-accent);
}

.symbol-anchor-halo,
.symbol-anchor-dot,
.symbol-anchor-occupied-core {
    vector-effect: non-scaling-stroke;
    pointer-events: none;
}

.symbol-anchor-halo {
    fill: var(--symbol-anchor-halo-fill);
    stroke: transparent;
    stroke-width: 2px;
    opacity: .96;
}

.symbol-anchor-dot {
    fill: var(--symbol-anchor-fill);
    stroke: var(--symbol-anchor-color);
    stroke-width: 2px;
}

/* ---- Typed connection network ---------------------------------------------- */
.dashboard-connection-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.dashboard-connection-layer > svg {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
}

/* The two surfaces bracket the components. The routes run in the gap the shared appearance leaves
   between every component's background band (below) and its content band (above): a route stays
   readable across the cards a screen is built from and still disappears behind the equipment it
   reaches. The controls stay over everything, so a handle is always reachable without painting
   route strokes across the artwork. */
.dashboard-connection-base {
    z-index: 1000000000;
}

.dashboard-connection-controls {
    z-index: 2147483647;
}

.dashboard-connection-casing,
.dashboard-connection-stroke,
.dashboard-connection-internal,
.dashboard-connection-preview,
.dashboard-connection-hit {
    fill: none;
    vector-effect: non-scaling-stroke;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.dashboard-connection-casing {
    stroke: var(--dashboard-connection-casing-color);
    stroke-width: calc(var(--connection-width) + 5px);
}

.dashboard-connection-stroke {
    stroke: var(--connection-color);
    stroke-width: var(--connection-width);
}

.dashboard-connection-internal {
    pointer-events: none;
    fill: none;
    stroke: var(--connection-color);
    stroke-width: var(--connection-width);
    vector-effect: non-scaling-stroke;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.dashboard-connection-network.is-undefined .dashboard-connection-casing {
    stroke-width: 5px;
}

.dashboard-connection-network.is-undefined .dashboard-connection-stroke {
    stroke: var(--color-text-disabled);
    stroke-width: 1.5px;
    stroke-dasharray: 7 5;
}

.dashboard-connection-network.is-undefined .dashboard-connection-internal {
    stroke: var(--color-text-disabled);
    stroke-width: 1.5px;
    stroke-dasharray: 7 5;
}

.dashboard-connection-segment.needs-adjustment .dashboard-connection-stroke {
    filter: drop-shadow(0 0 2px var(--color-caution));
}

.dashboard-connection-hit {
    pointer-events: stroke;
    cursor: grab;
    stroke: transparent;
    stroke-width: max(18px, calc(var(--connection-width) + 10px));
    touch-action: none;
}

.dashboard-connection-hit:focus-visible,
.dashboard-connection-segment.is-selected .dashboard-connection-stroke {
    filter: drop-shadow(0 0 3px var(--color-accent));
}

.dashboard-connection-preview {
    pointer-events: none;
    stroke: var(--color-accent);
    stroke-width: 2px;
    stroke-dasharray: 6 5;
}

.dashboard-connection-branch-handle {
    display: none;
    pointer-events: all;
    cursor: crosshair;
    touch-action: none;
    fill: var(--color-surface);
    stroke: var(--connection-color);
    stroke-width: 2px;
    vector-effect: non-scaling-stroke;
}

.dashboard-connection-segment.is-selected .dashboard-connection-branch-handle {
    display: block;
}

.dashboard-connection-node {
    color: var(--connection-color);
}

.dashboard-connection-layer.is-editable .dashboard-connection-node {
    pointer-events: all;
    cursor: move;
    touch-action: none;
}

.dashboard-connection-node-halo {
    fill: var(--color-surface);
    stroke: transparent;
}

.dashboard-connection-node-dot {
    fill: var(--connection-color);
    stroke: var(--color-surface);
    stroke-width: 1.5px;
    vector-effect: non-scaling-stroke;
}

.dashboard-connection-node.connectionpoint .dashboard-connection-node-dot {
    fill: var(--color-surface);
    stroke: var(--connection-color);
    stroke-width: 2px;
}

.dashboard-connection-node.openend .dashboard-connection-node-dot {
    fill: var(--color-caution-bg);
    stroke: var(--color-caution);
}

.dashboard-connection-open-mark {
    fill: none;
    stroke: var(--color-caution);
    stroke-width: 1.5px;
    vector-effect: non-scaling-stroke;
}

.dashboard-connection-node.is-selected .dashboard-connection-node-halo {
    stroke: var(--color-accent);
    stroke-width: 2px;
    vector-effect: non-scaling-stroke;
}

.dashboard-connection-warning {
    pointer-events: none;
}

.dashboard-connection-warning circle {
    fill: var(--color-critical-bg);
    stroke: var(--color-critical);
    stroke-width: 1px;
    vector-effect: non-scaling-stroke;
}

.dashboard-connection-warning text {
    fill: var(--color-critical);
    font-size: 11px;
    font-weight: var(--font-weight-bold);
}

.symbol-terminal-action,
.symbol-terminal-action * {
    pointer-events: all;
    touch-action: none;
}

.symbol-terminal-action.is-keyboard-source .symbol-anchor-halo,
.symbol-anchor-action.is-keyboard-source .symbol-anchor-halo,
.symbol-terminal-action.is-keyboard-source .symbol-terminal-halo {
    stroke: var(--color-accent);
    stroke-width: 3px;
}

.symbol-anchor-dot.is-measurement-only {
    opacity: 0;
    pointer-events: none;
}

.symbol-anchor-hit {
    fill: transparent;
    stroke: none;
    pointer-events: fill;
}

.symbol-anchor-action {
    cursor: pointer;
    pointer-events: all;
}

.symbol-anchor-action:focus-visible {
    outline: none;
}

.symbol-anchor-action:focus-visible .symbol-anchor-halo {
    stroke: var(--color-accent);
    stroke-width: 3px;
}

.symbol-anchor.is-active {
    --symbol-anchor-fill: var(--color-accent);
    --symbol-anchor-color: var(--color-surface);
}

.symbol-anchor.is-active .symbol-anchor-halo {
    stroke: var(--color-accent);
}

.symbol-anchor.is-compatible {
    --symbol-anchor-color: var(--color-success);
}

.symbol-anchor.is-compatible .symbol-anchor-halo {
    fill: var(--color-success-bg);
    stroke: var(--color-success);
}

/* "Release here to connect": the hovered drop target during a link/reconnect drag. A grown, glowing
   green halo reads as a positive affordance; the caution variant flags a type mismatch that will
   still connect (as an undefined network) so the operator is not surprised. */
.symbol-anchor.is-drop-target {
    --symbol-anchor-color: var(--color-success);
    --symbol-anchor-fill: var(--color-success);
    --symbol-anchor-halo-fill: var(--color-success-bg);
}

.symbol-anchor.is-drop-target .symbol-anchor-halo {
    stroke: var(--color-success);
    stroke-width: 3px;
    opacity: 1;
    transform-box: fill-box;
    transform-origin: center;
    transform: scale(1.3);
    filter: drop-shadow(0 0 4px var(--color-success));
}

.symbol-anchor.is-drop-target .symbol-anchor-dot {
    fill: var(--color-success);
    stroke: var(--color-surface);
}

.symbol-anchor.is-drop-target.is-drop-mismatch {
    --symbol-anchor-color: var(--color-caution);
    --symbol-anchor-fill: var(--color-caution);
    --symbol-anchor-halo-fill: var(--color-caution-bg);
}

.symbol-anchor.is-drop-target.is-drop-mismatch .symbol-anchor-halo {
    stroke: var(--color-caution);
    filter: drop-shadow(0 0 4px var(--color-caution));
}

.symbol-anchor.is-drop-target.is-drop-mismatch .symbol-anchor-dot {
    fill: var(--color-caution);
}

/* "Anchored here": a solid marker in the connected network's own colour (the editor passes it as
   --symbol-anchor-accent), over a surface halo that separates it from the line it terminates and
   from the artwork underneath. A muted grey would read as unavailable, the opposite of what a
   landed connection means. */
.symbol-anchor.is-occupied {
    --symbol-anchor-color: var(--symbol-anchor-accent, var(--color-accent));
    --symbol-anchor-fill: var(--symbol-anchor-accent, var(--color-accent));
    --symbol-anchor-halo-fill: var(--color-surface);
}

.symbol-anchor-occupied-core {
    fill: var(--color-surface);
    stroke: none;
}

.symbol-anchor.is-forced {
    --symbol-anchor-color: var(--color-caution);
    --symbol-anchor-halo-fill: var(--color-caution-bg);
    --symbol-anchor-mark: var(--color-caution);
}

.symbol-anchor.is-forced .symbol-anchor-halo,
.symbol-anchor.is-incompatible .symbol-anchor-halo {
    stroke: var(--symbol-anchor-color);
    stroke-dasharray: 3 2;
}

.symbol-anchor.is-forced .symbol-anchor-halo {
    fill: var(--color-caution-bg);
}

.symbol-anchor.is-incompatible {
    --symbol-anchor-color: var(--color-critical);
    --symbol-anchor-fill: var(--color-surface);
    --symbol-anchor-halo-fill: var(--color-critical-bg);
    --symbol-anchor-mark: var(--color-critical);
}

.symbol-anchor.is-incompatible .symbol-anchor-halo {
    fill: var(--color-critical-bg);
}

.symbol-anchor-warning-mark {
    fill: none;
    stroke: var(--symbol-anchor-mark);
    stroke-linecap: round;
    stroke-width: 1.5px;
    vector-effect: non-scaling-stroke;
    pointer-events: none;
}

.symbol-anchor-warning-dot {
    fill: var(--symbol-anchor-mark);
    pointer-events: none;
}

.symbol-anchor-action.is-disabled {
    cursor: not-allowed;
    opacity: .6;
}

.symbol-orientation-options {
    align-items: center;
    flex-wrap: wrap;
    color: var(--color-text-secondary);
    font-size: var(--font-size-description);
}

.symbol-orientation-options label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-height: 28px;
}

/* ---- Shared chart toolbar (ADR 0026): custom HTML overlay, replaces the ECharts toolbox ---- */
.chart-wrap {
    position: relative;
    width: 100%;
    height: 100%;
}

.chart-wrap:fullscreen {
    background: var(--color-surface);
    padding: 8px;
}

.chart-toolbar {
    position: absolute;
    top: 6px;
    right: 8px;
    z-index: 6;
    display: flex;
    gap: 2px;
    opacity: 0;
    transition: opacity var(--dur-quick) var(--ease-out);
}

/* An open disclosure holds the bar on screen: a panel faded out is still pressable, so a bar that
   went with the pointer would leave an invisible surface over the plot. */
.chart-wrap:hover .chart-toolbar,
.chart-toolbar:focus-within,
.chart-toolbar:has(.menu-pop) {
    opacity: 1;
}

/* Touch surfaces (remote tablets) have no hover — keep the toolbar visible there. */
@media (hover: none) {
    .chart-toolbar { opacity: 1; }
}

.chart-toolbar button {
    width: 26px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--color-stroke);
    border-radius: 5px;
    background: var(--color-surface);
    color: var(--color-text-secondary);
    cursor: pointer;
}

.chart-toolbar button:hover {
    color: var(--color-text);
    background: var(--color-overlay-grid);
}

.chart-toolbar button.active {
    color: var(--color-accent);
    border-color: var(--color-accent);
}

.chart-toolbar svg {
    width: 16px;
    height: 16px;
}

.chart-toolbar .menu {
    position: relative;
    display: flex;
}

.chart-toolbar .menu-pop {
    position: absolute;
    top: 30px;
    right: 0;
    min-width: 150px;
    max-height: 220px;
    overflow-y: auto;
    padding: 4px;
    background: var(--color-surface);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-popover);
    z-index: 10;
}

.chart-toolbar .menu-pop label {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 3px 6px;
    font-size: 12px;
    color: var(--color-text);
    cursor: pointer;
    white-space: nowrap;
}

.chart-toolbar .menu-pop label:hover {
    background: var(--color-overlay-grid);
    border-radius: 4px;
}

.chart-toolbar .menu-empty {
    padding: 4px 6px;
    font-size: 12px;
    color: var(--color-text-secondary);
}

/* The manual scale's popover: two axes, each a heading with its own "Automatic" and a pair of
   fields under it, then the shortcut that fills all four from the frame on screen. It is a form and
   not a list, so it overrides the series popover's nowrap column and states its own width. */
.chart-toolbar .chart-scale {
    min-width: 232px;
    max-height: none;
    overflow: visible;
}

.chart-toolbar .chart-scale-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 4px 4px 2px;
    font-size: 11px;
    color: var(--color-text-secondary);
}

.chart-toolbar .chart-scale-row {
    display: flex;
    gap: 6px;
    padding: 0 4px 6px;
}

.chart-toolbar .chart-scale-row input {
    flex: 1 1 0;
    min-width: 0;
    padding: 3px 6px;
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sm);
    font-size: 12px;
}

/* The two buttons of the form read as text, not as more icons: the icon row above is the chart's
   tools, and these act on the fields under them. */
.chart-toolbar .chart-scale-auto,
.chart-toolbar .chart-scale-load {
    width: auto;
    height: auto;
    padding: 3px 8px;
    font-size: 11px;
    white-space: nowrap;
}

.chart-toolbar .chart-scale-load {
    width: calc(100% - 8px);
    margin: 2px 4px 4px;
}

.chart-toolbar .chart-scale-auto:disabled {
    color: var(--color-text-disabled);
    cursor: default;
}

.chart-toolbar .chart-scale-auto:disabled:hover {
    background: none;
    color: var(--color-text-disabled);
}

/* The window's statistic: one line saying what the window is, then a row per series. It is a table
   of readings, so the numbers are right-aligned and tabular — three columns of digits that do not
   line up are three columns nobody can compare. */
.chart-toolbar .chart-stats {
    min-width: 268px;
    max-height: 260px;
}

.chart-toolbar .chart-stats-head {
    padding: 4px 4px 6px;
    font-size: 11px;
    line-height: 1.35;
    color: var(--color-text-secondary);
}

.chart-toolbar .chart-stats-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
    color: var(--color-text);
}

.chart-toolbar .chart-stats-table th,
.chart-toolbar .chart-stats-table td {
    padding: 2px 4px;
    text-align: right;
    font-weight: 400;
    white-space: nowrap;
}

.chart-toolbar .chart-stats-table thead th {
    font-size: 11px;
    color: var(--color-text-secondary);
    border-bottom: 1px solid var(--color-stroke);
}

.chart-toolbar .chart-stats-table tbody td {
    font-variant-numeric: tabular-nums;
}

.chart-toolbar .chart-stats-table th[scope="row"] {
    display: flex;
    align-items: center;
    gap: 6px;
    text-align: left;
}

.chart-toolbar .chart-stats-name {
    max-width: 108px;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chart-toolbar .chart-stats-ink {
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 2px;
    background: var(--color-text-secondary);
}

/* Reference lines: what is drawn, each row with the ✕ that removes it, then the small form that
   draws the next one. Same register as the manual scale beside it — a form, not a list of icons. */
.chart-toolbar .chart-refs {
    min-width: 244px;
    max-height: 300px;
}

.chart-toolbar .chart-refs-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 4px 4px 2px;
    font-size: 11px;
    color: var(--color-text-secondary);
}

.chart-toolbar .chart-refs-list {
    margin: 0;
    padding: 0 4px 4px;
    list-style: none;
}

.chart-toolbar .chart-refs-list li {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 2px 0;
    font-size: 12px;
    color: var(--color-text);
}

.chart-toolbar .chart-refs-ink {
    flex: 0 0 auto;
    width: 8px;
    height: 8px;
    border-radius: 2px;
}

.chart-toolbar .chart-refs-at {
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
}

.chart-toolbar .chart-refs-text {
    flex: 1 1 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--color-text-secondary);
}

.chart-toolbar .chart-refs-row {
    display: flex;
    gap: 6px;
    padding: 0 4px 6px;
}

.chart-toolbar .chart-refs-row input,
.chart-toolbar .chart-refs-row select {
    flex: 1 1 0;
    min-width: 0;
    padding: 3px 6px;
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-stroke);
    border-radius: var(--radius-sm);
    font-size: 12px;
}

/* The colour is a swatch and not a field: it holds no text to make room for. */
.chart-toolbar .chart-refs-row input[type="color"] {
    flex: 0 0 auto;
    width: 32px;
    padding: 1px 2px;
}

.chart-toolbar .chart-refs-drop,
.chart-toolbar .chart-refs-clear,
.chart-toolbar .chart-refs-add {
    width: auto;
    height: auto;
    padding: 3px 8px;
    font-size: 11px;
    white-space: nowrap;
}

.chart-toolbar .chart-refs-drop {
    padding: 0 4px;
    border: none;
    background: none;
}

.chart-toolbar .chart-refs-add {
    width: calc(100% - 8px);
    margin: 2px 4px 4px;
}

.chart-toolbar .chart-refs-clear:disabled,
.chart-toolbar .chart-refs-add:disabled {
    color: var(--color-text-disabled);
    cursor: default;
}

.chart-toolbar .chart-refs-clear:disabled:hover,
.chart-toolbar .chart-refs-add:disabled:hover {
    background: none;
    color: var(--color-text-disabled);
}

/* Touch (remote tablets, no hover): grow the toolbar tap targets to the 44px minimum
   (WCAG 2.5.5). Placed after the base rules so it wins at equal specificity; desktop
   (hover: hover) keeps the compact 26px controls untouched. */
@media (hover: none) {
    .chart-toolbar { gap: 6px; }
    .chart-toolbar button { width: 44px; height: 44px; }
    .chart-toolbar svg { width: 20px; height: 20px; }
    .chart-toolbar .menu-pop { top: 48px; }
    .chart-toolbar .menu-pop label { min-height: 44px; padding: 8px 10px; }

    /* The scale's own controls stay text-sized: a 44px-square "Automatic" would be a word in a
       box the size of an icon. They grow in height only, which is what a finger needs. */
    .chart-toolbar .chart-scale-auto,
    .chart-toolbar .chart-scale-load { width: auto; min-height: 44px; }
    .chart-toolbar .chart-scale-load { width: calc(100% - 8px); }
    .chart-toolbar .chart-scale-row input { min-height: 44px; }

    /* The two analysis panels follow the same rule: their controls grow in height, and the ✕ that
       removes a line grows in both, because it is a target and not a word. */
    .chart-toolbar .chart-refs-clear,
    .chart-toolbar .chart-refs-add { width: auto; min-height: 44px; }
    .chart-toolbar .chart-refs-add { width: calc(100% - 8px); }
    .chart-toolbar .chart-refs-row input,
    .chart-toolbar .chart-refs-row select { min-height: 44px; }
    .chart-toolbar .chart-refs-drop { width: 44px; height: 44px; }
}

/* ---- Detached chart window (ADR 0026): a bare full-window chart opened via
   window.open. Fixed to the viewport so it fills the window regardless of body height, with a
   thin title/sync bar above a flex-filled chart body. ---- */
.detached-chart {
    position: fixed;
    inset: 0;
    display: flex;
    flex-direction: column;
    background: var(--color-surface);
}

.detached-chart-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 8px 14px;
    border-bottom: 1px solid var(--color-stroke);
}

.detached-chart-title {
    font-size: 14px;
    font-weight: 600;
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.detached-chart-sync {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
    font-size: 12px;
    color: var(--color-text-secondary);
    cursor: pointer;
    user-select: none;
}

.detached-chart-body {
    flex: 1;
    min-height: 0;
    padding: 8px;
}

/* The unit window stacks one chart per plot group, each sharing the height equally. */
.detached-unit-stack {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 100%;
    height: 100%;
}

.detached-unit-chart {
    flex: 1;
    min-height: 0;
    position: relative;
}

/* The editable ComboBox behind the address fields sizes exactly like the plain input it replaced:
   its wrapper takes the field's own block spacing (so the overlaid chevron lines up with the box,
   not with the gap above it) and flexes like a field when it shares a row. */
.edit-field .ui-combobox-editable {
    margin-top: 3px;
}

.edit-row .ui-combobox-editable {
    flex: 1 1 0;
    min-width: 0;
}

.edit-field .ui-combobox-editable > input,
.edit-row .ui-combobox-editable > input {
    margin-top: 0;
}

/* The row's own button box (the ✕ beside a chart series) must not reach the chevron inside the
   field — it is an affordance drawn on the box, not a control beside it. */
.edit-row .ui-combobox-editable > .ui-combobox-open {
    padding: 0 var(--space-3);
    border: 0;
    background: none;
    color: var(--color-text-secondary);
}

/* The canvas is a real focus target — Delete and Escape act only while it holds the focus — and its
   host clips overflow, so the app-wide keyboard ring is pulled inside the grid instead of being cut
   off outside it. Pointer focus still paints nothing (:focus-visible). */
.edit-grid:focus-visible {
    outline-offset: calc(-1 * var(--focus-ring-width));
}

/* The Add list and the Components locator read as glyph + text so a component is
   recognized before it is read: the icon keeps its own fixed column and the
   name/description (or identification/type/address) stack beside it. */
.palette-item {
    flex-direction: row;
    align-items: center;
    gap: var(--space-3);
}

.palette-item .pal-icon,
.dashboard-component-row .pal-icon {
    flex: 0 0 auto;
    width: 20px;
    text-align: center;
}

.palette-item .pal-text {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

/* The Connection point sits with Symbol but is clicked, never dragged onto a cell. */
.dashboard-add-connection-point {
    cursor: pointer;
}

/* ---- Editor tool rail: one-line tab strip, leading commands, temporary overlay --------------
   Height is what the canvas competes for, so the expanded strip lays its four areas out as a
   single wrapping row: they fit one line at the rail's usual width and take a second only when it
   is dragged near its 320 px minimum or a narrow viewport caps it. The tabs are words here —
   the glyphs stay on the collapsed rail, where there is room for nothing else — which is what
   buys the line. */
.dashboard-tools-tabs:not(.collapsed) {
    display: flex;
    flex-wrap: wrap;
}

.dashboard-tools-tabs:not(.collapsed) > .dashboard-tools-tab {
    flex: 1 1 auto;
}

/* The panel's own commands own the header's LEADING edge. At the trailing one they sit after the
   last tab, where anything pointing outward reads as scrolling the strip rather than as retracting
   the pane; the tabs start after them and keep the rest of the line. */
.dashboard-tools-header > .dashboard-tools-tabs {
    margin-left: var(--space-2);
}

.dashboard-tools-pin,
.dashboard-tools-close {
    flex: 0 0 32px;
}

.dashboard-tools-pin:focus-visible,
.dashboard-tools-close:focus-visible,
.dashboard-tools-tabs.collapsed:focus-visible {
    outline: var(--focus-ring-width) solid var(--color-accent);
    outline-offset: calc(-1 * var(--focus-ring-offset));
}

/* Collapsed, an area command floats the whole panel over the canvas instead of pinning the pane
   back open — the right-hand mirror of the dashboards pane's dnav-navigation-overlay, laid out
   against the same workspace. It leaves the grid, so the rail's 44 px track stays exactly where it
   was, and grows inward from that outer edge at the width the operator last gave the pane. The
   canvas stage is a stacking context of its own, so being positioned is what puts the panel above
   it — and the same z-index pair the dashboards overlay uses keeps both below the popover layer.
   Floating also changes what it is made of: pinned, the panel is a well the tool cards rise out
   of; hovering over the stage it is a popover, so it drops the inherited well fill for the card
   tone the dashboards overlay opposite it wears. */
.dashboard-tools.overlay {
    position: absolute;
    z-index: 20;
    top: 0;
    right: 0;
    bottom: 0;
    width: min(var(--editor-pane-width, 380px), 72vw);
    border-radius: var(--radius-lg);
    background: var(--color-surface);
    box-shadow: var(--shadow-popover);
}

/* The outside click that retracts it: transparent, below the panel, over everything else. */
.dashboard-tools-scrim {
    position: absolute;
    inset: 0;
    z-index: 19;
    border: 0;
    background: transparent;
    cursor: default;
}

/* ---- Choosing the symbol before it lands -----------------------------------------------------
   Symbol is the one Add row that opens something instead of dropping something, so it loses the
   grab cursor the draggable rows carry: the pointer has to promise the library, not the cell. */
.dashboard-add-symbol {
    cursor: pointer;
}

.dashboard-add-symbol-back {
    align-self: flex-start;
    margin-bottom: var(--space-3);
}

/* Properties opens with the symbol this instance IS — artwork, name and where it came from — so the
   panel reads as the component's even before the sections below it. */
.editor-symbol-identity {
    display: grid;
    grid-template-columns: 84px minmax(0, 1fr);
    gap: var(--space-3);
    align-items: center;
    min-width: 0;
}

.editor-symbol-identity .symbol-preview {
    aspect-ratio: 1 / 1;
}

.editor-symbol-identity-text {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.editor-symbol-identity-text strong {
    overflow-wrap: anywhere;
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-semibold);
}

.editor-symbol-identity-text span {
    overflow-wrap: anywhere;
    color: var(--color-text-secondary);
    font-size: var(--font-size-eyebrow);
}

.editor-symbol-change {
    width: 100%;
    margin-top: var(--space-3);
}

/* The terminal roster answers what this symbol can be wired to, in the panel, without spending the
   room a second gallery would. */
.editor-symbol-terminals {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin: 0;
}

.editor-symbol-terminals > div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    min-width: 0;
}

.editor-symbol-terminals dt {
    min-width: 0;
    overflow: hidden;
    color: var(--color-text);
    font-family: var(--font-mono);
    font-size: var(--font-size-micro);
    text-overflow: ellipsis;
    white-space: nowrap;
}

.editor-symbol-terminals dd {
    flex: 0 0 auto;
    margin: 0;
    color: var(--color-text-secondary);
    font-size: var(--font-size-micro);
}

/* One declared symbol input: its name, what it accepts, and which of the two sources feeds it.
   The expectation sits on the caption line so it is read before the field, and the source pair is
   a segmented choice — the active half is marked, so no stored value is ever the silent loser. */
.symbol-slot {
    margin-top: var(--space-5);
}

.symbol-slot-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    min-width: 0;
}

.symbol-slot-name {
    min-width: 0;
    overflow: hidden;
    color: var(--color-text);
    font-family: var(--font-mono);
    font-size: var(--font-size-micro);
    text-overflow: ellipsis;
    white-space: nowrap;
}

.symbol-slot-expects {
    flex: 0 0 auto;
    color: var(--color-text-secondary);
    font-size: var(--font-size-micro);
}

.symbol-slot-source {
    display: flex;
    gap: var(--space-1);
    margin: var(--space-2) 0;
}

.symbol-slot-source .ui-btn {
    flex: 1 1 0;
    min-width: 0;
    justify-content: center;
    padding: 3px var(--space-3);
    font-size: var(--font-size-micro);
}

.symbol-slot-source .ui-btn.is-selected {
    border-color: var(--color-accent);
    color: var(--color-accent);
    background: var(--color-surface-alt);
}

/* A boolean input's fixed value is a switch. Its real checkbox is visually hidden (absolutely
   positioned, 0×0), so the field-level input box must not repaint it as an ordinary text box. */
.symbol-slot .ui-switch {
    margin-top: var(--space-1);
}

.symbol-slot .ui-switch > input {
    width: 0;
    padding: 0;
    border: 0;
}

/* ---- The appearance every component shares ----------------------------------
   Frame, background with its colour pair, and corner title (ADR 0020), each a per-component
   value: this is what turns loose readouts into the structured cards a SCADA screen is read as.
   Frame, background and title are off by default, so a screen shows a box only where the
   integrator asked for one; the colour a background paints starts at the product tone.
   The component is a flex column — the title takes a real row and the body keeps the rest, so a
   caption never sits over a value.
   Frame and background live on a cell-sized sibling layer instead of on the component box: the
   connection routes run between the two (ADR 0030), so each half needs its own level in the
   grid's stacking context. ComponentChrome writes both z-indexes from the component's own layer,
   which is what keeps the operator's ordering between components unchanged. */
.dash-component-chrome {
    position: absolute;
    inset: var(--dash-cell-inset);
    border-radius: var(--radius-lg);
    pointer-events: none;
}

/* The outline's weight is the component's own: a hairline is the card's edge, and a heavier stroke
   is how one group of components is told from those around it. ComponentChrome writes the width
   only once it is off the hairline, so an ordinary frame declares nothing. */
.dash-component-chrome.framed {
    border: var(--dash-frame-width, 1px) solid var(--color-stroke);
}

/* The fill is the component's own colour PAIR — ComponentChrome writes both halves inline and
   the running theme picks one here, so flipping the theme repaints without a round trip. The
   pair starts at the CARD tone (--color-surface, spelled out in DashboardComponent's default
   constants because a face is row data, not CSS): the stage under it is the page backdrop in
   every mounting, so a filled component reads as a card on a page — the same relation an app
   card has, in both themes. */
.dash-component-chrome.filled {
    background: var(--dash-fill-light);
}

[data-theme='dark'] .dash-component-chrome.filled {
    background: var(--dash-fill-dark);
}

/* Two size containers, because the two rows answer to different boxes: the corner title sizes
   itself against the whole component, and the content against the room the title leaves it. Both
   boxes come from the cell, never from what is inside them, so the sizing above resolves in one
   pass and cannot chase itself. */
.dash-component {
    position: relative;
    display: flex;
    flex-direction: column;
    container-type: size;
}

/* No paint of its own — the clip that keeps the content inside the corner the chrome layer
   rounds. */
.dash-component.framed,
.dash-component.filled {
    border-radius: var(--radius-lg);
}

.dash-component-body {
    flex: 1 1 auto;
    position: relative;
    min-width: 0;
    min-height: 0;
    container-type: size;
}

.dash-component.framed > .dash-component-body,
.dash-component.filled > .dash-component-body {
    padding: var(--space-2) var(--space-3);
}

/* ---- A bound point that is not reading --------------------------------------
   One mark for one fact, drawn HERE and nowhere else, so it comes out identical on every kind: a
   component reports that a point it is bound to went silent and the shared appearance draws it,
   which is what lets a kind added later wear the mark without implementing it. An operator learns
   it once and reads it across the whole interface.

   A restrained dashed frame in the caution tone with a small "!" seal — the cue repeats in SHAPE and
   not in colour alone, and it stands over the component's own drawing rather than repainting it, so
   an authored process state still reads as itself. Both halves come from theme tokens, so the two
   themes are answered by the palette. In a general loss of communication many frames light at once;
   that is the truth of the screen at that moment, and the reason each one stays this light.

   It lights only where a point is BOUND and silent: a component nobody addressed yet stays quiet, so
   a screen under construction is not a screen of warnings, and a kind that reads no point at all
   (a label, a frame, an icon on a fixed glyph) never lights.

   The gap dash a readout writes in place of its value stays beside it — the frame says something on
   this component is wrong, the dash says this number is missing — and neither of them is the mark
   for a reading past its declared width (ADR 0034 §4). Three facts, three marks, no borrowing.

   The z-indexes are local to the component's own stacking context (ComponentChrome gives the content
   layer one), and are set above anything the component draws inside it. The second selector is the
   Logic symbol inspector, a preview stage with no component around it: the artwork there says the
   same thing through the attribute its runtime writes, and wears the same mark for it. */
.dash-component[data-ganter-quality='bad']::before,
.lg-symbol-inspector-stage:has(> [data-ganter-quality='bad'])::before {
    content: '';
    position: absolute;
    z-index: 100;
    inset: var(--space-1);
    border: 1px dashed var(--color-caution);
    border-radius: var(--radius-sm);
    pointer-events: none;
}

.dash-component[data-ganter-quality='bad']::after,
.lg-symbol-inspector-stage:has(> [data-ganter-quality='bad'])::after {
    content: '!';
    position: absolute;
    z-index: 101;
    top: var(--space-2);
    right: var(--space-2);
    display: grid;
    width: 16px;
    height: 16px;
    place-items: center;
    border: 1px solid var(--color-caution);
    border-radius: var(--radius-pill);
    background: var(--color-surface);
    color: var(--color-caution);
    font-family: var(--font-mono);
    font-size: var(--font-size-eyebrow);
    font-weight: var(--font-weight-bold);
    line-height: 1;
    pointer-events: none;
}

/* The seal stands in the top-right corner, which is where a corner title's line ends. A title long
   enough to fill that line would run under it — and a caption whose last word is hidden reads as a
   different caption. So the row gives the seal its width while the mark is up: the caption ellipsizes
   a little earlier, which is the truthful answer, instead of ending under a badge. (The corner is the
   seal's for the other side of the same rule: a reading rests to the RIGHT of its field, ADR 0034 §5,
   so the bottom-right belongs to numbers.) */
.dash-component[data-ganter-quality='bad'] > .dash-component-title {
    --display-seal: calc(var(--space-2) + 16px);
    padding-right: calc(var(--space-3) + var(--display-seal));

    /* The room the line is SIZED against has to lose the same width the padding took, or the type is
       fitted to a line wider than the one it is drawn in and runs under the seal anyway. */
    --display-avail: calc(100cqw - var(--space-3) * 2 - var(--display-seal));
}

/* ---- A command this station may not press -----------------------------------
   The second fact the shared appearance marks, in the grammar of the first: the component reports
   that a press on it is blocked for whoever is signed in, and the mark is drawn HERE — one lock,
   learned once, worn by every kind that commands, including the ones added later. The whole reason
   travels in the component's own tooltip, so the corner stays a mark and the sentence stays
   reachable instead of taking a row of the cell away from what the operator came to read.

   Quiet by decision — the secondary ink, a glyph and nothing around it. A station not signed in for
   commanding sees this on every command on the screen at once, and a plant running normally must not
   come out looking like a screen of warnings over a permission the operator can simply sign in for.
   What is blocked is the ACTION and never the reading: the lit track, the option in force and the
   handle at the point's own position all stay at full strength, and this mark with the not-allowed
   cursor is the whole of what says the hand cannot move them.

   It hangs off the BODY because the component box's two pseudo-elements are the not-reading mark's,
   and both facts can be true at once — a station that may not command, looking at a point that
   stopped answering. Their corners are apart for the same reason: the seal keeps the top-right, the
   lock keeps the bottom-left, and neither takes the bottom-right a reading rests in (ADR 0034 §5).
   The body is drawn whether or not the component carries a frame, so a bare component wears the mark
   exactly as a card does. */
.dash-component[data-ganter-locked] {
    --dash-lock-size: 13px;
}

.dash-component[data-ganter-locked] > .dash-component-body::after {
    content: '\EB82';
    position: absolute;
    z-index: 101;
    bottom: var(--space-2);
    left: var(--space-2);
    color: var(--color-text-secondary);
    font-family: 'FluentSystemIcons-Resizable';

    /* The icon face has one weight: inheriting a bolder one from the component under it would leave
       the browser to synthesize the difference, and a smeared lock is not the quiet mark. */
    font-weight: var(--font-weight-regular);
    font-size: var(--dash-lock-size);
    line-height: 1;
    pointer-events: none;
}

/* The answer line gives the mark its corner while it is up, the way the caption row gives the seal
   its own: the line stands empty for as long as nothing can be pressed, and an answer still on
   screen from the press before the permission narrowed must not run under the lock. */
.dash-component[data-ganter-locked] .feedback {
    padding-left: calc(var(--dash-lock-size) + var(--space-3));
}

/* The title grows with the component like everything else, but it is a header over the content and
   not the content: the tall end of its size is a small multiple of the eyebrow size, so a tall card
   gets a readable caption rather than a caption that outgrows what it captions. Case belongs to
   whoever typed the caption — "Process Value" is shown as it was written — so the tracking is the
   hair a small semibold line wants in mixed case, not the wide setting upper case asks for.
   Both answers here are the operator's to overrule, and each arrives as a property ComponentChrome
   writes only when he did. A STATED caption size is literal (ADR 0034): it takes the place of the
   cell's answer at exactly the points he named, which is what makes the captions of a screen agree
   across cards of different sizes. His own ink takes the place of the interface's tone the same way,
   one half per theme. */
.dash-component-title {
    flex: 0 0 auto;
    overflow: hidden;
    padding: var(--space-2) var(--space-3) 0;
    color: var(--dash-title-ink-light, var(--color-text-secondary));
    --display-height: clamp(
        var(--font-size-eyebrow),
        calc(18cqh * var(--cmp-scale, 1)),
        calc(var(--font-size-eyebrow) * 2.2));
    --display-avail: calc(100cqw - var(--space-3) * 2);
    /* The ROW is the cell's — the strut below holds it open — and the caption's own LINE is the size
       that came out of the rule. The two are separate on purpose, because each answers a different
       question. This row's own height feeds the body under it, and that height is the cqh every
       sibling sizes against: a row following the resolved type would put the caption's own answer
       into the boxes above it, and they would stop coming from the cell alone. But a LINE held open
       at the tall end around a caption resolved smaller is exactly the empty band between a title and
       the thing it titles. So the height stays the cell's and the leading goes. --display-height is
       deliberately non-inheriting — every surface reads its own — so the reserve travels to the strut
       as a property of its own. */
    --dash-title-reserve: calc(var(--display-height) * 1.2);
    line-height: 1.2;
    font-weight: var(--font-weight-semibold);
    letter-spacing: 0.01em;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A stated caption is the row: it renders at the points the operator named in any cell, so that is
   the height the body under it is measured from. */
.dash-component-title.display-stated {
    --dash-title-reserve: calc(var(--display-size) * 1.2);
}

/* The reserve, as a zero-width strut aligned to the FOOT of the line: the row keeps the height the
   cell gives it, and the caption sits at the bottom of that room, against what it heads. Whatever a
   caption resolved smaller leaves over is left above it, under the card's own edge, where it reads as
   the top margin of the card instead of as a gap under its title. */
.dash-component-title::before {
    content: "";
    display: inline-block;
    width: 0;
    height: var(--dash-title-reserve);
    vertical-align: bottom;
}

/* The dark half of the caption's ink pair — the same shape the fill's pair takes, so the theme
   flips in the browser without a re-render. A caption nobody recoloured declares neither half and
   the theme's own tone above stands. */
[data-theme='dark'] .dash-component-title {
    color: var(--dash-title-ink-dark, var(--color-text-secondary));
}

/* The frame's title reads as the card's header, so it gets the separating rule the standalone
   Frame kind already draws; a bare title (no box) stays a quiet caption over open grid. */
.dash-component.framed > .dash-component-title,
.dash-component.filled > .dash-component-title {
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--color-stroke-subtle);
}

/* The body must not clip the anchors either — the same exception .dash-component already makes
   above, restated one level in, where the artwork now hangs. */
.dash-component:has(.cmp-symbol) > .dash-component-body {
    overflow: visible;
}

/* Ctrl multi-selection: the primary member keeps the overlay ring and the resize grip (a
   single-component gesture), and every member carries its own accent outline — the operator
   must see exactly which components a batch edit is about to touch. The outline rides over the
   whole content band, so an overlapped member still shows. */
.edit-cmp.selected::after {
    content: "";
    position: absolute;
    z-index: 2000000000;
    inset: -1px;
    border: 1px solid var(--color-accent);
    border-radius: 4px;
    pointer-events: none;
}

.editor-mixed-value {
    margin-left: auto;
    color: var(--color-text-secondary);
    font-size: var(--font-size-eyebrow);
    text-transform: uppercase;
}
