/* ============================================================
   HydraSafe UI layer — component patterns and motion.

   hydrasafe-design.css is the token layer (a port of the design system, kept
   diffable against it). This file is the app's own layer built ON those tokens:
   severity rails, the appbar, motion, skeletons, the node hero, the command
   palette. Nothing here invents a colour — every value resolves to a token.

   Linked after hydrasafe-design.css in App.razor.
   ============================================================ */

/* ============================================================
   0. Page shell: pages fill the viewport.

   Several pages sized their scroll area with a hardcoded `height: 88vh`, which
   only lines up at one window height — everywhere else it left a dead strip at
   the bottom (or overflowed). A page is a flex column that fills its slot, and
   exactly one child inside it is the part that scrolls.
   ============================================================ */
/* .content is a stretched flex item, which is not a *definite* height for
   percentage resolution — so a child asking for `height: 100%` fell back to its
   content and grew the page instead of fitting it. (That is why the old code
   reached for vh units: they resolve against something definite.) Pinning it to
   100% of .body-content, which does have a definite height, makes every
   percentage below it resolve. No overflow:hidden here on purpose: each page
   owns its own scroll container, and clipping here would hide their content. */
.content {
	height: 100%;
	min-height: 0;
}

.hs-page {
	display: flex;
	flex-direction: column;
	height: 100%;
	min-height: 0;
	width: 100%;
}

/* A plain block scroll container, NOT a flex column. As a flex column it gave
   the data grid inside a full-height box, and a <table> with a definite height
   hands the spare height to its rows — so a grid holding one alarm rendered
   that alarm as a single 380px-tall row with the text floating in the middle. */
.hs-page-body {
	flex: 1 1 auto;
	min-height: 0;
	overflow: auto;
	background: var(--hs-pane);
}

/* Belt and braces: the grid sizes to its rows and the container takes up the
   slack, whatever height the surrounding stylesheets try to give it. */
.hs-page-body > .fluent-data-grid,
.hs-page-body table.fluent-data-grid {
	height: auto !important;
}

/* Toolbars and other fixed-height page furniture must not be squeezed by the
   body claiming the space. */
.hs-page-bar {
	flex: 0 0 auto;
}

/* A plot fills the remainder and never scrolls — a chart that scrolls is a chart
   that was sized wrong. The small top inset is breathing room from the toolbar,
   which the plot used to sit hard against (and overlap). */
.hs-plot-body {
	flex: 1 1 auto;
	min-height: 0;
	overflow: hidden;
	padding-top: var(--space-2);
	/* Page-level side breathing room. Deliberately here and not inside TrendView:
	   the same TrendView fills a panel tile edge-to-edge. */
	padding-inline: var(--space-3);
	box-sizing: border-box;
}

/* Panel plot tiles (trend, live bar): the same frame as the value/gauge tiles.
   The frame is a box-shadow, not a border, for the same reason as on
   .hs-value-tile: the chart sizes itself to the tile's client box, and a
   border would shrink that box without firing its resize observer. No accent
   bar any more: every tile in the app wore a 3px one, and on a panel of twelve
   tiles that is twelve accent bars, which stops meaning anything. The frame
   carries the tile; the accent is kept for the value, the active nav item and
   the primary action. */
.hs-plot-tile {
	position: relative;
	height: 100%;
	width: 100%;
	overflow: hidden;
	background: var(--hs-tile);
	border-radius: var(--radius-lg);
	box-shadow: inset 0 0 0 1px var(--border-default), var(--elevation-1);
}

/* Inside a tile the chart's own top margin (8px) put the topmost axis label
   right against the tile's frame — visibly glued to it in light mode. The
   inset is invisible as such: the canvas paints the same surface as the tile
   behind it. */
.hs-plot-tile .hs-plot-box,
.hs-node-tile .hs-plot-box {
	padding-top: 10px;
	box-sizing: border-box;
}

/* Plot title — ONE style for every plot title: trend tile plots and live bar
   tiles alike, and typographically IDENTICAL to the single-node tile titles
   (.hs-tile-label): uppercase overline, letter-spaced, bold, --text-secondary,
   no chip. Only the positioning differs — it floats top-centre INSIDE a
   position: relative box, above the chart canvas (a positioned sibling that
   paints later, hence the z-index). pointer-events off so it never eats the
   chart's pan/zoom gestures. */
.hs-plot-label {
	position: absolute;
	top: 0;
	left: 50%;
	transform: translateX(-50%);
	z-index: 3;
	pointer-events: none;
	max-width: calc(100% - 56px); /* keep clear of the chart's legend button */
	padding: 8px var(--space-3) 6px;
	font-size: var(--text-overline);
	letter-spacing: var(--ls-overline);
	text-transform: uppercase;
	font-weight: var(--fw-bold);
	color: var(--text-secondary);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* ============================================================
   0b. Shared surface tokens for this layer.

   --hs-tile is the app's card surface. Light is plain white on the faintly
   blue page; dark is one step up the near-black scale (hydrasafe-design.css).
   ============================================================ */
:root {
	--hs-tile: #FFFFFF;
	/* A bar surface for toolbars, tile title strips and table footers: one
	   opaque step down from the tile. It used to be a translucent accent tint,
	   which picked up whatever happened to be painted behind it. */
	--hs-tile-bar: #F2F5F9;
	/* Opaque surface for things content scrolls UNDER — sticky grid headers
	   above all. A translucent tint there let every row show through the
	   header while scrolling, which read as broken scrolling. */
	--hs-bar-solid: #F2F5F9;
	/* Row separator inside a grid: a step lighter than --border-default, so
	   400 rows do not read as 400 rules. */
	--hs-hairline: #EDF1F6;
	/* Accent TEXT needs its own value. --color-accent is steel #4682B4, which is
	   only 3.8:1 on a lightly tinted surface — fine for a rail or an icon (3:1
	   for UI components) but a fail for label text. The navy end of the ramp
	   clears AA comfortably in both modes. */
	--hs-accent-ink: var(--navy-700);
	/* The big number: value tiles, the node hero. */
	--hs-value-ink: var(--navy-700);
	/* The selected/active fill and its ink: the current nav item, the active
	   palette row, the pressed segment. An opaque step rather than a
	   translucent accent wash — a wash over the sidebar picked up whatever was
	   behind it. */
	--hs-accent-soft: #EAF0F9;
	--hs-accent-soft-ink: var(--navy-700);
	/* The chrome (appbar, toolbars), the pane a grid sits on, and the stroke of
	   a toolbar control: white / white / a step darker than the hairline in
	   light; three steps of the scale in dark. */
	--hs-chrome: #FFFFFF;
	--hs-pane: #FFFFFF;
	--hs-control-stroke: var(--border-strong);
	/* A list column standing beside a pane (the plugin rail): a shade off the
	   pane in light — not the sunken grey, which beside a white pane reads as a
	   well rather than as a list — and the deepest step of the scale in dark. */
	--hs-aside: #FAFBFD;
}

body.theme-dark {
	--hs-chrome: var(--hs-n-150);
	--hs-pane: var(--hs-n-100);
	--hs-control-stroke: var(--hs-n-400);
	/* --hs-tile was color-mix(--neutral-layer-1 84%, --navy-700): a navy wash
	   over Fluent's grey, which was the workaround for the grey. The scale is
	   cool now, so the wash is not needed and the tile is simply a step up. */
	--hs-tile: var(--hs-n-200);
	--hs-tile-bar: var(--hs-n-150);
	--hs-aside: var(--hs-n-000);
	--hs-bar-solid: #10161D;
	--hs-hairline: var(--hs-n-150);
	--hs-accent-ink: #A9BBD1;
	--hs-value-ink: #8FB4E6;
	/* --hs-accent-soft itself comes from the token layer (#16243A). */
	--hs-accent-soft-ink: #C3DAF7;
}

/* ============================================================
   1. Severity: rail + pill, not a flood.

   The old pattern painted the whole row in a saturated hue, which is why the
   grids read as a legacy SCADA panel. Here the row surface stays neutral, a
   4px rail carries the hue, and a pill carries hue + icon + text so severity
   is never encoded by colour alone.

   --sev-* are set per row by the .sev-<state> classes below; everything
   downstream reads those, so a new state means one new class.
   ============================================================ */
.sev-critical,
.sev-unack {
	--sev: var(--hs-error);
	--sev-bg: var(--hs-error-bg);
	--sev-ink: var(--hs-error-darken);
}

.sev-gone-unack {
	--sev: var(--hs-warning);
	--sev-bg: var(--hs-warning-bg);
	--sev-ink: var(--hs-warning-darken);
}

.sev-ack {
	--sev: var(--hs-info);
	--sev-bg: var(--hs-info-bg);
	--sev-ink: var(--hs-info-darken, #10303C);
}

.sev-ok,
.sev-normal {
	--sev: var(--hs-success);
	--sev-bg: var(--hs-success-bg);
	--sev-ink: var(--hs-success-darken);
}

.sev-user {
	--sev: var(--navy-500);
	--sev-bg: #EAF0F9;
	--sev-ink: var(--navy-700);
}

.sev-io {
	--sev: var(--steel-500);
	--sev-bg: var(--steel-100);
	--sev-ink: var(--steel-700);
}

.sev-init {
	--sev: var(--grey-500);
	--sev-bg: var(--grey-100);
	--sev-ink: var(--hs-ink-2);
}

/* Dark mode keeps the hue identity and carries it in the ink as well. The
   pills used to inherit --text-primary here, so all five states came out the
   same ink and the hue was carried by the tint alone; at this contrast the hue
   belongs in the text. */
body.theme-dark .sev-critical,
body.theme-dark .sev-unack {
	--sev-ink: #FFB4B9;
}

body.theme-dark .sev-gone-unack {
	--sev-ink: #F5C77A;
}

body.theme-dark .sev-ack {
	--sev-ink: #9AD1EC;
}

body.theme-dark .sev-ok,
body.theme-dark .sev-normal {
	--sev-ink: #93DDB0;
}

body.theme-dark .sev-user {
	--sev-ink: #B6D0F2;
	--sev-bg: #111C2C;
}

body.theme-dark .sev-io {
	--sev-ink: #A9CBE0;
	--sev-bg: #0E1E28;
}

body.theme-dark .sev-init {
	--sev-ink: #B4BEC9;
	--sev-bg: #161B21;
}

/* ---- the row itself ----
   Every selector here is qualified with [role="row"], and that is load-bearing.

   FluentDataGrid stamps RowClass onto the <tr> AND onto every <td> inside it.
   So a bare `.hs-sev-row` rule hits the row plus each cell: the 6% tint stacked
   per cell, and `.hs-sev-row > :first-child` matched the first element inside
   *each* cell — putting a rail at every column boundary and making the table
   look striped. Cells carry role="gridcell", so keying on role="row" selects
   the row and nothing else, in Table and Grid display modes alike. */
/* Mixed against the OPAQUE surface, not against `transparent`.
   A translucent tint takes on whatever happens to be painted underneath — the
   grid's own zebra striping, a hover fill from the component library, the page
   behind it — so hovering shifted the row off-hue instead of just brightening
   it. Mixing with --hs-tile yields one opaque colour per state: hover is then
   guaranteed to be the same hue, only stronger. */
.hs-sev-row[role="row"] {
	/* Under 12% on white: a stronger mix becomes a colour block. */
	background: color-mix(in srgb, var(--sev, transparent) 8%, var(--hs-pane)) !important;
	transition: background var(--dur-fast) var(--ease-standard);
}

/* The 22%/36% mixes above were tuned against Fluent's #333. On near-black the
   same numbers wash the row out to a solid colour block — exactly the legacy
   SCADA panel the rail pattern was introduced to avoid — so dark gets less. */
body.theme-dark .hs-sev-row[role="row"] {
	background: color-mix(in srgb, var(--sev, transparent) 11%, var(--hs-pane)) !important;
}

.hs-sev-row[role="row"] > :first-child {
	box-shadow: inset 3px 0 0 0 var(--sev, transparent);
}

/* Same hue, ~1.5× the mix. */
.hs-sev-row[role="row"]:hover {
	background: color-mix(in srgb, var(--sev, transparent) 12%, var(--hs-pane)) !important;
}

body.theme-dark .hs-sev-row[role="row"]:hover {
	background: color-mix(in srgb, var(--sev, transparent) 18%, var(--hs-pane)) !important;
}

/* The cells must stay clear so the row's colour is the only one showing. The
   grid stamps RowClass onto every <td> and the library adds its own hover fill
   to cells; either one painting over the row is what produced the odd tone. */
.hs-sev-row[role="row"] > [role="gridcell"],
.hs-sev-row[role="row"]:hover > [role="gridcell"] {
	background: transparent !important;
}

/* ---- new rows arrive, they do not just appear ----
   Applied by each grid to rows it has not rendered before (see the seen-key
   tracking in AlarmGrid/EventsDisplay). One-shot: the class is handed out once
   per row, so this never replays on a refresh tick. */
/* Two animations, deliberately split.

   The cells carry only the slide and fade. [role="row"] qualification matters:
   RowClass lands on the <tr> AND on every <td>, so an unqualified
   `.hs-row-enter > *` also animated the contents of each cell individually —
   every span sliding in on its own.

   No fill-mode on either: `both` holds the final keyframe forever, and since the
   row's rail is itself a box-shadow on the first cell, a lingering box-shadow
   from the animation would erase the rail permanently. */
.hs-row-enter[role="row"] > * {
	animation: hs-row-in var(--dur-slow) var(--ease-decelerate);
}

/* The flash goes on the row, as a glow rather than a background-color: the row's
   background is an !important opaque mix, and an important author declaration
   outranks an animation, so animating the background would simply be ignored.
   A box-shadow on a table-row paints per cell, which here is exactly what we
   want — the whole row lights up. */
.hs-row-enter[role="row"] {
	animation: hs-row-flash var(--dur-slow) var(--ease-decelerate);
}

@keyframes hs-row-in {
	0% {
		opacity: 0;
		transform: translateX(-12px);
	}

	55%, 100% {
		opacity: 1;
		transform: none;
	}
}

@keyframes hs-row-flash {
	0% {
		box-shadow: inset 0 0 0 2000px color-mix(in srgb, var(--sev, var(--color-accent)) 45%, transparent);
	}

	100% {
		box-shadow: inset 0 0 0 2000px transparent;
	}
}

/* Anything still needing acknowledgement gets a slow breathing rail. This is
   the one place motion means "act on me". */
.hs-sev-row[role="row"].hs-needs-ack > :first-child {
	animation: hs-rail-pulse 2.4s var(--ease-standard) infinite;
}

@keyframes hs-rail-pulse {
	0%, 100% {
		box-shadow: inset 3px 0 0 0 var(--sev);
	}

	50% {
		box-shadow: inset 3px 0 0 0 color-mix(in srgb, var(--sev) 30%, transparent);
	}
}

/* ---- the pill ---- */
.hs-pill {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 2px 10px 2px 8px;
	border-radius: var(--radius-pill);
	background: var(--sev-bg, var(--grey-100));
	color: var(--sev-ink, var(--text-primary));
	border: 1px solid color-mix(in srgb, var(--sev, var(--grey-300)) 40%, transparent);
	font-size: var(--text-caption);
	font-weight: var(--fw-semibold);
	line-height: 1.6;
	white-space: nowrap;
}

.hs-pill .hs-pill-dot {
	width: 7px;
	height: 7px;
	border-radius: 50%;
	background: var(--sev, var(--grey-400));
	flex: 0 0 auto;
}

.hs-pill svg {
	width: 14px;
	height: 14px;
	flex: 0 0 auto;
}

/* A pill that still needs a human: the dot pulses, the pill does not. */
.hs-pill.hs-needs-ack .hs-pill-dot {
	animation: hs-pulse 1.6s var(--ease-standard) infinite;
}

/* ---- priority badge: rank, not severity ---- */
.hs-prio {
	display: inline-block;
	padding: 1px 8px;
	border-radius: var(--radius-sm);
	font-size: var(--text-overline);
	font-weight: var(--fw-bold);
	letter-spacing: var(--ls-wide);
	text-transform: uppercase;
	background: var(--grey-100);
	color: var(--text-secondary);
	border: 1px solid var(--border-default);
}

body.theme-dark .hs-prio {
	background: var(--surface-sunken);
}

.hs-prio-critical {
	background: var(--hs-error-bg);
	color: var(--hs-error-darken);
	border-color: color-mix(in srgb, var(--hs-error) 45%, transparent);
}

.hs-prio-high {
	background: var(--hs-warning-bg);
	color: var(--hs-warning-darken);
	border-color: color-mix(in srgb, var(--hs-warning) 45%, transparent);
}

/* `body.theme-dark .hs-prio` outranks the bare `.hs-prio-high`, so the ranked
   badges lost their hue in dark mode and came out the same navy as Low/Medium.
   Restating the background here keeps the amber/red identity in both modes. */
body.theme-dark .hs-prio-critical {
	background: var(--hs-error-bg);
	color: #FFB4B9;
}

body.theme-dark .hs-prio-high {
	background: var(--hs-warning-bg);
	color: #F5C77A;
}

/* ============================================================
   1b. Source coloration — the Nodes grid.

   Same visual family as the severity rows (tint + rail) but deliberately much
   weaker: on the Nodes page the colour groups rows by which provider owns them,
   which is orientation, not a warning. Severity earns 22%; provenance gets 9%,
   and a 3px rail instead of 5px.

   The hues are assigned by the caller (GetSourceClass) from the provider name, so
   every Simulator row shares one colour for as long as the name is stable.
   ============================================================ */
.hs-src-a { --src: var(--steel-500); }
.hs-src-b { --src: var(--navy-500); }
.hs-src-c { --src: var(--hs-info); }
.hs-src-d { --src: var(--hs-success); }
.hs-src-e { --src: var(--hs-warning); }
.hs-src-f { --src: var(--grey-500); }

/* ---- Nodes grid row treatment ----
   Static, and that is the whole point: no per-row classes, no per-row inline styles,
   nothing for Blazor to diff per row. A virtualized grid on a Server circuit cannot
   afford data-driven row attributes (see the note in Nodes.razor) — so the rows get
   the same *structure* as the event rows (hairline separators, a clear hover) while
   the source colour is carried by the swatch in the Source column. */
.hs-node-grid tbody tr[role="row"] > td {
	/* 34px, not the 44px the grid ships: a row of one id, one number and four
	   flags never needed it. Nodes.razor sets the matching ItemSize. */
	height: 34px;
	/* Exactly 34px, loaded and placeholder rows alike. Fluent's 6px of vertical
	   padding under the star icon's line box made a loaded row 38.7px while a
	   placeholder row stayed at the 34px ItemSize says; Blazor's Virtualize measures
	   the rows it rendered, and with two heights in one viewport it re-rendered in a
	   loop that cancelled every load — after any jump of the scrollbar the grid
	   stayed blank. */
	padding-top: 0;
	padding-bottom: 0;
	border-bottom: 1px solid var(--hs-hairline) !important;
	font-size: 12.5px;
}

.hs-node-grid tbody tr[role="row"]:hover > td {
	background: var(--hs-tile-bar) !important;
}

/* The two flag columns: their uppercase, letter-spaced headers need 124px, and
   in table display mode the column's Width parameter is not applied, so the
   width is set here (column 4 and 5: Retained, Writable). */
.hs-node-grid th:nth-child(4),
.hs-node-grid th:nth-child(5) {
	width: 124px;
	min-width: 124px !important;
}

/* The star and the open-page columns: Fluent's 100px minimum on every header
   made the eight columns wider than the pane and gave the grid a horizontal
   scrollbar for two icon columns. */
.hs-node-grid th:first-child,
.hs-node-grid td:first-child {
	width: 56px;
	min-width: 56px !important;
}

.hs-node-grid th:last-child,
.hs-node-grid td:last-child {
	width: 72px;
	min-width: 72px !important;
}

/* Values, ids, timestamps and counts: mono and tabular, so digits line up
   down the column and a changing value does not reflow its neighbours. This
   is the single change that makes a process table look like an instrument
   rather than a spreadsheet. */
.hs-mono,
.hs-node-grid td .hs-node-id,
.hs-feed-id,
.hs-feed-time,
.hs-about-stat > b {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 12px;
}

/* A small swatch next to the source name — the row's source colour. */
.hs-src-tag {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	min-width: 0;
}

.hs-src-tag::before {
	content: "";
	flex: 0 0 auto;
	/* Larger than a dot: it is now the only carrier of the source colour, so it has
	   to be legible as a colour at a glance down the column. */
	width: 10px;
	height: 10px;
	border-radius: 3px;
	background: var(--src, var(--grey-400));
	box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--src, var(--grey-400)) 55%, black);
}

/* ---- alarm LED in the Id column ----
   A node with an alarm gets an LED ahead of its id, coloured by the sev-* class
   on the same element. Same one-element-per-row budget as the source swatch (no
   RowClass on this grid — see the note in Nodes.razor); the slot renders on
   every row so ids stay aligned, and with no --sev both mixes below resolve to
   transparent, so an alarm-free slot is invisible rather than grey. The halo
   ring is what lifts it past subtle; needs-ack breathes, matching the pill dot. */
.hs-node-id {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	min-width: 0;
}

.hs-alarm-led {
	flex: 0 0 auto;
	width: 9px;
	height: 9px;
	border-radius: 50%;
	background: var(--sev, transparent);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--sev, transparent) 22%, transparent);
}

/* Dark surfaces swallow the halo at 22%, same as the row tints above. */
body.theme-dark .hs-alarm-led {
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--sev, transparent) 32%, transparent);
}

.hs-alarm-led.hs-needs-ack {
	animation: hs-pulse 1.6s var(--ease-standard) infinite;
}

/* The event kind, as a small uppercase word in the kind's own ink (the row's
   rail already carries the hue; this makes it readable). */
.hs-type {
	font-size: 11px;
	font-weight: var(--fw-semibold);
	letter-spacing: .04em;
	text-transform: uppercase;
	color: var(--sev-ink, var(--text-secondary));
}

/* The Events grid's message cell, one line: the state pill and the priority
   badge keep their size, the text beside them (node, alarm, message) gives way
   with an ellipsis. An ellipsis on the flex row itself did nothing, and a narrow
   column cut the badge off at the Source column instead. */
.hs-event-msg {
	display: flex;
	align-items: center;
	gap: 10px;
	white-space: nowrap;
	overflow: hidden;
}

.hs-event-msg > .hs-pill,
.hs-event-msg > .hs-prio {
	flex-shrink: 0;
}

.hs-event-msg > :not(.hs-pill, .hs-prio) {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* Boolean flags in the Nodes grid: a tick in the success hue, a dash in the
   disabled ink — the eye is looking for the exceptions. */
.hs-flag-yes {
	color: var(--hs-success);
	font-weight: var(--fw-semibold);
}

.hs-flag-no {
	color: var(--text-disabled);
}

/* ---- one empty state, used by every grid ----
   The app had two competing styles side by side: a bold centred "No data to
   show!" and an icon-plus-sentence. This is the single version. */
.hs-grid-empty {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--space-3);
	padding: var(--space-10) var(--space-6);
	text-align: center;
	color: var(--text-secondary);
	font-size: var(--text-body2);
}

/* ---- log entries (plugin dashboard) — same rail treatment ----
   Time first, then the line; a 2px rail in the severity, on the bar surface. */
.hs-log-entry {
	display: flex;
	align-items: baseline;
	gap: 8px;
	padding: 4px 8px;
	border-radius: 5px;
	border-left: 2px solid var(--sev, var(--border-default));
	background: var(--hs-tile-bar);
	font-size: 10.5px;
	color: var(--text-primary);
	min-width: 0;
	flex: 0 0 auto;
}

.hs-log-entry > time {
	color: var(--text-secondary);
	font-family: var(--font-mono);
	font-size: 10px;
	white-space: nowrap;
	flex: 0 0 auto;
}

.hs-log-entry > span {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* ============================================================
   2. Appbar: flat chrome.

   The gradient plus the animated strand tangle was 80px of decoration above
   every screen in the product, and the single loudest thing in the app. The
   bar is chrome now: 52px, one flat surface, one hairline, the wordmark, the
   breadcrumb, the search field and the controls. Brand identity moves to the
   wordmark and the accent, which is where it can be afforded on every page.
   (The canvas and neural-net.js went with the gradient.)
   ============================================================ */
header.header {
	height: 52px !important;
	min-height: 52px !important;
	background: var(--hs-chrome) !important;
	border-bottom: 1px solid var(--neutral-stroke-divider-rest);
	box-shadow: none !important;
}

body.theme-dark header.header {
	border-bottom-color: var(--hs-n-300);
}

/* Fluent gives the inner gutters div its own relative context and 20px
   margins; the bar is tighter than that. !important because Fluent's own rule
   for this class wins on specificity order. min-width (here and on the stack
   inside): the bar shrinks to the window, its breadcrumb and search field give
   (both can); at the content's own width it ran 14px past the right edge on a
   1024px window, the account button off screen and the page scrolling sideways. */
header.header > div.header-gutters {
	position: static !important;
	margin-left: 14px !important;
	margin-right: 14px !important;
	min-width: 0;
}

header.header .header {
	gap: 14px;
	min-width: 0;
}

/* ---- wordmark ---- */
.hs-brand {
	display: flex;
	align-items: center;
	gap: 8px;
	flex: 0 0 auto;
	min-width: 0;
}

/* The wordmark asset is a single white path drawn for the old navy bar. As a
   mask over currentColor it takes the bar's ink instead: navy on white, white
   on near-black. 750:153 is the SVG's aspect ratio. */
.hs-logo {
	display: block;
	width: 127px;
	height: 26px;
	flex: 0 0 auto;
	background: currentColor;
	color: var(--navy-700);
	-webkit-mask: url(/logo.svg) no-repeat left center / contain;
	mask: url(/logo.svg) no-repeat left center / contain;
}

body.theme-dark .hs-logo {
	color: #FFFFFF;
}

.hs-wordmark-text {
	font-family: var(--font-display);
	font-size: 19px;
	line-height: 1;
	color: #6E8CB0;
	user-select: none;
}

body:not(.theme-dark) .hs-wordmark-text {
	color: #7B90AC;
}

.hs-header-sep {
	width: 1px;
	height: 20px;
	background: var(--neutral-stroke-divider-rest);
	flex: 0 0 auto;
}

/* ---- breadcrumb: section / page ---- */
.hs-crumbs {
	display: flex;
	align-items: center;
	gap: 7px;
	min-width: 0;
	font-size: 12.5px;
	color: var(--text-secondary);
	white-space: nowrap;
}

.hs-crumb-sep {
	color: var(--text-disabled);
}

.hs-crumb-current {
	color: var(--text-primary);
	font-weight: var(--fw-semibold);
	overflow: hidden;
	text-overflow: ellipsis;
	max-width: 32vw;
}

.hs-crumb-current.hs-mono {
	font-size: 12.5px;
}

/* ---- the search field: the palette's front door ----
   With the appbar reduced to chrome, this is the thing in it. A button dressed
   as a field: clicking it opens the Ctrl+K palette. */
.hs-header-search {
	display: flex;
	align-items: center;
	gap: 8px;
	width: min(430px, 100%);
	height: 30px;
	padding: 0 10px;
	box-sizing: border-box;
	border-radius: 8px;
	border: 1px solid var(--hs-control-stroke);
	background: var(--hs-tile);
	color: var(--text-secondary);
	font: inherit;
	font-size: 12.5px;
	cursor: text;
	text-align: left;
	transition: border-color var(--dur-fast) var(--ease-standard);
}

.hs-header-search:hover {
	border-color: var(--border-strong);
}

.hs-header-search:focus-visible {
	outline: none;
	border-color: var(--color-accent);
	box-shadow: var(--shadow-focus-ring);
}

.hs-header-search-text {
	flex: 1 1 auto;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-header-search .hs-kbd {
	font-size: 10px;
	padding: 0 5px;
	border-bottom-width: 1px;
	color: var(--text-secondary);
}

.hs-header-tools {
	display: flex;
	align-items: center;
	gap: 2px;
	flex: 0 0 auto;
}

/* ---- header controls: monochrome and quiet at rest ----
   FluentButton's neutral appearance paints a filled surface, which put six
   filled chips in the bar. */
header.header :is(fluent-button, fluent-menu-button) {
	background: transparent !important;
	border-color: transparent !important;
	color: var(--hs-ink-2) !important;
	height: 30px !important;
	min-width: 30px !important;
	border-radius: 7px !important;
	padding: 0 8px !important;
}

header.header :is(fluent-button, fluent-menu-button):hover {
	background: var(--hs-tile-bar) !important;
	color: var(--text-primary) !important;
}

body.theme-dark header.header :is(fluent-button, fluent-menu-button):hover {
	background: var(--hs-n-250) !important;
}

/* The theme toggle keeps a faint fill, so the current mode has a button. */
header.header .hs-theme-btn {
	background: var(--hs-accent-soft) !important;
	color: var(--hs-accent-soft-ink) !important;
}

body.theme-dark header.header .hs-theme-btn {
	background: var(--hs-n-250) !important;
}

/* ---- nav toggle, only shown where the sidebar collapses ---- */
.hs-navtoggle {
	display: none;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	margin-right: 4px;
	border-radius: var(--radius-md);
	cursor: pointer;
	color: var(--text-secondary);
	transition: background var(--dur-fast) var(--ease-standard);
}

.hs-navtoggle:hover {
	background: var(--hs-tile-bar);
}

/* The checkbox lives in the sidebar and the label in the header, so they are not
   siblings — :has() on the root is what bridges them. Keyboard users tab to the
   (off-screen) checkbox and the ring shows up on the visible control. */
body:has(.navmenu-toggle-input:focus-visible) .hs-navtoggle {
	outline: 2px solid var(--color-accent);
	outline-offset: 1px;
}

/* Shown only where the search field has folded away. */
.hs-show-sm {
	display: none !important;
}

@media (max-width: 600px) {
	.hs-navtoggle {
		display: inline-flex;
	}

	.hs-show-sm {
		display: inline-flex !important;
	}

	/* Secondary controls fold away first so the primary ones stay reachable
	   instead of overflowing past the viewport edge. */
	.hs-hide-sm {
		display: none !important;
	}

	header.header .header {
		gap: 4px;
	}

	header.header > div.header-gutters {
		margin-left: 8px !important;
		margin-right: 8px !important;
	}

	.hs-wordmark-text,
	.hs-header-sep {
		display: none;
	}

	.hs-logo {
		width: 93px;
		height: 19px;
	}

	/* At 375px the six controls have to share ~170px: tighter chips, no gaps. */
	.hs-header-tools {
		gap: 0;
	}

	header.header :is(fluent-button, fluent-menu-button) {
		min-width: 28px !important;
		padding: 0 4px !important;
	}
}

/* ============================================================
   3. Motion: value changes.

   Two distinct signals. A flash means "this number just changed" (fires on
   every update, must be cheap and short). A count-up means "this number is
   travelling to a new value" (gauges only, where the movement is the point).
   ============================================================ */
/* Two identical animations under different names, alternated on each change.
   Re-adding the same class does not restart a CSS animation — the computed
   animation-name is unchanged — and keying the element instead would tear down
   and rebuild the node on every tick. Swapping between -a and -b changes
   animation-name, which does restart it, and costs nothing. */
.hs-flash-a {
	animation: hs-flash-a var(--dur-slow) var(--ease-decelerate);
}

.hs-flash-b {
	animation: hs-flash-b var(--dur-slow) var(--ease-decelerate);
}

@keyframes hs-flash-a {
	0% {
		background-color: color-mix(in srgb, var(--color-accent) 26%, transparent);
	}

	100% {
		background-color: transparent;
	}
}

@keyframes hs-flash-b {
	0% {
		background-color: color-mix(in srgb, var(--color-accent) 26%, transparent);
	}

	100% {
		background-color: transparent;
	}
}

/* ---- shared tile header ----
   The value tile and the gauge are interchangeable renderers for the same thing
   (one node), so they have to look like siblings. The gauge used to draw its own
   title inside the SVG at a different size and baseline from the value tile's,
   and the value tile had lost its name altogether — side by side they lined up
   with nothing. Both now emit this one header, so the names sit on the same line
   whichever renderer a tile uses. */
.hs-tile-label {
	flex: 0 0 auto;
	padding: 9px 13px 0;
	font-size: 10px;
	letter-spacing: .11em;
	text-transform: uppercase;
	font-weight: var(--fw-bold);
	color: var(--hs-ink-3);
	text-align: left;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* The renderer fills whatever is left under the header. */
.hs-tile-body {
	flex: 1 1 auto;
	min-height: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
}

/* Live value tile: was a bare --neutral-fill-secondary block with no frame.
   The frame is a box-shadow, NOT a border, and that is deliberate:
   ValueDisplay auto-sizes its text by shrinking the font until it fits
   parent.clientHeight, driven by a ResizeObserver on this element. A border
   shrinks the client box without changing the element's size, so the observer
   never fires, the font stays at its old value and the number gets clipped.
   box-shadow paints inside the same box and costs no layout. */
.hs-value-tile {
	box-sizing: border-box;
	border-radius: var(--radius-lg);
	background: var(--hs-tile);
	box-shadow: inset 0 0 0 1px var(--border-default),
				var(--elevation-1);
	transition: box-shadow var(--dur-base) var(--ease-standard);
}

/* In light the tiles are white on a tinted page and the frame does the
   separating, not a shadow; the shadow is for near-black, where a border
   alone was all a card had. */
body:not(.theme-dark) .hs-value-tile,
body:not(.theme-dark) .hs-plot-tile {
	box-shadow: inset 0 0 0 1px var(--border-default);
}

body:not(.theme-dark) .hs-node-tile {
	box-shadow: none;
}

.hs-value-tile:hover {
	box-shadow: inset 0 0 0 1px var(--border-strong),
				var(--elevation-4);
}

/* The number is the one thing on a value tile that keeps the colour: mono and
   tabular so it does not jitter as it changes, in the value ink. */
.hs-value-tile .hs-tile-body {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-weight: var(--fw-medium);
	letter-spacing: -.02em;
	color: var(--hs-value-ink);
	padding: 0 8px;
}

/* Footer line: the node id (when the title is a name of its own) and the last
   change, signed and coloured. */
.hs-tile-foot {
	flex: 0 0 auto;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
	padding: 0 13px 9px;
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 10.5px;
	color: var(--text-secondary);
	min-width: 0;
}

.hs-tile-foot > span:first-child {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* The value tile's stack: label, number, footer. A one-row tile is about 70px high, and under
   the label and the footer line the number kept 11px of it — the fit script then shrank it to
   5px. The tile is a size container, so a short one moves the footer up beside the label and
   gives the number the rest. The tile's size never depends on its content (GridStack or the
   page sets it), so size containment costs nothing. */
.hs-value-readout {
	container: hs-readout / size;
}

.hs-value-stack {
	height: 100%;
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	grid-template-rows: auto minmax(0, 1fr) auto;
	grid-template-areas:
		"label label"
		"body body"
		"foot foot";
}

.hs-value-stack > .hs-tile-label { grid-area: label; }
.hs-value-stack > .hs-tile-body { grid-area: body; }
.hs-value-stack > .hs-tile-foot { grid-area: foot; }

@container hs-readout (max-height: 100px) {
	.hs-value-stack {
		grid-template-rows: auto minmax(0, 1fr);
		grid-template-areas:
			"label foot"
			"body body";
	}

	.hs-value-stack > .hs-tile-foot {
		max-width: 60cqw;
		padding: 9px 13px 0 0;
	}
}

.hs-tile-delta.up {
	color: var(--hs-success);
}

.hs-tile-delta.down {
	color: var(--hs-error);
}

/* Directional tick: rising values glow success, falling values glow error.
   Applied to the hero readout, where a bare flash would lose the sign. */
.hs-tick-up {
	animation: hs-tick-up-kf var(--dur-slow) var(--ease-decelerate);
}

.hs-tick-down {
	animation: hs-tick-down-kf var(--dur-slow) var(--ease-decelerate);
}

@keyframes hs-tick-up-kf {
	0% {
		color: var(--hs-success);
		transform: translateY(-2px);
	}

	100% {
		color: inherit;
		transform: none;
	}
}

@keyframes hs-tick-down-kf {
	0% {
		color: var(--hs-error);
		transform: translateY(2px);
	}

	100% {
		color: inherit;
		transform: none;
	}
}

/* ---- gauge ----
   The needle rotates about the hub and the fill is revealed by dash offset, so
   both are plain transitionable properties. transform-box/transform-origin are
   what let an SVG child rotate about a point in user units. */
.hs-gauge-needle {
	transform-box: view-box;
	transform-origin: 100px 110px;
	transition: transform var(--dur-slow) var(--ease-standard);
}

.hs-gauge-fill {
	transition: stroke-dashoffset var(--dur-slow) var(--ease-standard);
}

/* Gauge typography. All four labels were previously one 14px/9px pair coloured
   with a focus-ring token; they now form a proper hierarchy — value, unit, the
   two scale ends, and the title. */
.hs-gauge-value {
	fill: var(--hs-accent-ink);
	font-family: var(--font-sans);
	font-size: 26px;
	font-weight: 600;
	letter-spacing: -0.5px;
}

.hs-gauge-unit {
	fill: var(--text-secondary);
	font-family: var(--font-sans);
	font-size: 11px;
	font-weight: 500;
}

.hs-gauge-limit {
	fill: var(--text-secondary);
	font-family: var(--font-mono);
	font-size: 8px;
	opacity: .8;
}

.hs-gauge-title {
	fill: var(--text-secondary);
	font-family: var(--font-sans);
	font-size: 9px;
	font-weight: 600;
	letter-spacing: .06em;
	text-transform: uppercase;
}

/* ============================================================
   4. Cards: hover lift.
   The elevation ramp existed unused; this is what it was for.
   ============================================================ */
.hs-lift,
.fluent-card.hs-lift {
	transition: box-shadow var(--dur-base) var(--ease-standard),
				transform var(--dur-base) var(--ease-standard);
	will-change: transform;
}

.hs-lift:hover {
	box-shadow: var(--elevation-8);
	transform: translateY(-2px);
}

/* ============================================================
   5. Skeletons — shown while a panel has no data yet.
   ============================================================ */
.hs-skeleton {
	position: relative;
	overflow: hidden;
	background: var(--surface-sunken);
	border-radius: var(--radius-md);
}

.hs-skeleton::after {
	content: "";
	position: absolute;
	inset: 0;
	transform: translateX(-100%);
	background: linear-gradient(90deg,
			transparent,
			color-mix(in srgb, var(--text-primary) 8%, transparent),
			transparent);
	animation: hs-shimmer 1.4s var(--ease-standard) infinite;
}

@keyframes hs-shimmer {
	100% {
		transform: translateX(100%);
	}
}

.hs-skeleton-stack {
	display: flex;
	flex-direction: column;
	gap: 10px;
	padding: 16px;
	height: 100%;
	box-sizing: border-box;
}

.hs-skeleton-line {
	height: 12px;
}

.hs-skeleton-line.w-40 {
	width: 40%;
}

.hs-skeleton-line.w-60 {
	width: 60%;
}

.hs-skeleton-line.w-80 {
	width: 80%;
}

.hs-skeleton-block {
	flex: 1 1 auto;
	min-height: 60px;
}

/* ============================================================
   6. Panel edit mode.

   Edit mode used to look identical to view mode. Now the grid shows through,
   every tile gets a dashed brand outline, a drag handle and a resize corner,
   and a hint bar explains the gestures.
   ============================================================ */
/* Floating rather than in flow: the panel's cell height is measured once from the
   container minus the toolbar, so a bar that took up layout height would push the
   grid into an overflow scroll the moment edit mode turned on.

   Two things keep it out of the way of the work it describes:

   * pointer-events: none — the resize corner of a bottom-row tile sits exactly
     where this bar floats, and an overlay there swallowed the drag. Nothing in
     the bar is interactive, so it can be made transparent to the mouse outright.
   * it retires on its own after a few seconds. The instructions are only needed
     while you read them, and holding the space permanently is what made it feel
     like it was covering the tiles. Re-entering edit mode re-creates the element,
     so the animation runs again each time. */
.hs-edit-hint {
	position: fixed;
	left: 50%;
	bottom: 20px;
	transform: translateX(-50%);
	z-index: 900;
	pointer-events: none;
	display: flex;
	align-items: center;
	gap: 10px;
	max-width: min(760px, calc(100vw - 32px));
	padding: 10px 18px;
	border-radius: var(--radius-pill);
	background: var(--surface-card);
	border: 1px solid var(--navy-200);
	box-shadow: var(--elevation-8);
	color: var(--text-primary);
	font-size: var(--text-caption);
	animation: hs-hint-life 7s var(--ease-standard) forwards;
}

body.theme-dark .hs-edit-hint {
	border-color: var(--border-strong);
}

@keyframes hs-hint-life {
	0% {
		opacity: 0;
		transform: translate(-50%, 12px);
	}

	/* In and readable for the bulk of the life... */
	6%, 80% {
		opacity: 1;
		transform: translateX(-50%);
	}

	/* ...then out, and out of the way. */
	100% {
		opacity: 0;
		transform: translate(-50%, 8px);
		visibility: hidden;
	}
}

/* The grid itself, revealed only while editing. */
.hs-panel-editing {
	background-image: linear-gradient(color-mix(in srgb, var(--color-accent) 16%, transparent) 1px, transparent 1px),
	linear-gradient(90deg, color-mix(in srgb, var(--color-accent) 16%, transparent) 1px, transparent 1px);
	background-size: 40px 40px;
	border-radius: var(--radius-lg);
	transition: background-color var(--dur-base) var(--ease-standard);
}

/* The tiles are opaque, so the grid behind them only shows in the gutters — the
   dashed ring is what has to carry "this is editable". At 1px/55% alpha it was
   invisible against a white tile, hence the solid 2px dash and the accent wash.

   A real BORDER, not an inset outline: an outline with a negative offset is
   painted inside the content box, and a tile whose content fills it edge-to-edge
   with an opaque positioned canvas (the trend plot) painted straight over it —
   the trend tile showed no dashes at all. Children cannot cover a border ring. */
.hs-panel-editing .grid-stack-item-content {
	border: 2px dashed var(--color-accent);
	box-sizing: border-box;
	border-radius: var(--radius-lg);
	box-shadow: inset 0 0 0 100vmax color-mix(in srgb, var(--color-accent) 7%, transparent);
	transition: border-color var(--dur-fast) var(--ease-standard),
				box-shadow var(--dur-base) var(--ease-standard);
}

.hs-panel-editing .grid-stack-item-content:hover {
	border-style: solid;
	box-shadow: var(--elevation-8),
				inset 0 0 0 100vmax color-mix(in srgb, var(--color-accent) 12%, transparent);
	cursor: grab;
}

.hs-panel-editing .grid-stack-item.ui-draggable-dragging .grid-stack-item-content {
	cursor: grabbing;
	border: 2px solid var(--color-accent);
	box-shadow: var(--elevation-8);
	opacity: .92;
}

/* Drag handle — a grip that appears on hover, top-left of each tile. */
.hs-panel-editing .grid-stack-item-content::before {
	content: "⠿";
	position: absolute;
	top: 4px;
	left: 8px;
	z-index: 5;
	font-size: 15px;
	line-height: 1;
	color: var(--color-primary-contrast);
	background: var(--color-accent);
	border-radius: var(--radius-sm);
	padding: 3px 5px;
	/* Visible at rest so the gesture is discoverable without hunting, full
	   strength on hover. Hover-only handles are invisible affordances. */
	opacity: .55;
	pointer-events: none;
	transition: opacity var(--dur-fast) var(--ease-standard);
}

/* Resize corner — mirrors the handle, bottom-right. */
.hs-panel-editing .grid-stack-item-content::after {
	content: "";
	position: absolute;
	right: 3px;
	bottom: 3px;
	z-index: 5;
	width: 12px;
	height: 12px;
	pointer-events: none;
	opacity: .55;
	transition: opacity var(--dur-fast) var(--ease-standard);
	background: linear-gradient(135deg, transparent 45%, var(--color-accent) 45%, var(--color-accent) 60%, transparent 60%, transparent 72%, var(--color-accent) 72%, var(--color-accent) 87%, transparent 87%);
}

.hs-panel-editing .grid-stack-item-content:hover::before,
.hs-panel-editing .grid-stack-item-content:hover::after {
	opacity: 1;
}

/* gridstack's own placeholder, restyled to the brand instead of its default. */
.hs-panel-editing .grid-stack-placeholder > .placeholder-content {
	border: 2px dashed var(--color-accent) !important;
	background: color-mix(in srgb, var(--color-accent) 10%, transparent) !important;
	border-radius: var(--radius-lg) !important;
}

/* ============================================================
   7. Node detail — hero readout.
   The current value used to be a table cell. It is the reason the page exists,
   so it gets the top of the page and the biggest type on it.
   ============================================================ */
/* No border, no background, no shadow: the hero is a section of its tile, not a
   card of its own. It only becomes a card when used standalone (the dialog),
   which opts in with .hs-card. */
.hs-node-hero {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--space-5);
	padding: 16px 16px 14px;
}

.hs-node-hero.hs-card {
	border-radius: var(--radius-lg);
	background: var(--hs-tile);
	border: 1px solid var(--border-default);
}

.hs-node-hero-main {
	display: flex;
	flex-direction: column;
	gap: var(--space-2);
	min-width: 0;
}

.hs-node-hero-label {
	font-size: var(--text-overline);
	letter-spacing: var(--ls-overline);
	text-transform: uppercase;
	color: var(--text-secondary);
	font-weight: var(--fw-semibold);
}

.hs-node-hero-value {
	display: flex;
	align-items: baseline;
	gap: var(--space-2);
	font-variant-numeric: tabular-nums;
	line-height: 1;
}

/* The value is where the eye lands, so it carries the brand colour rather than
   being one more piece of near-black text. */
.hs-node-hero-number {
	font-family: var(--font-mono);
	font-size: var(--text-display);
	font-weight: var(--fw-medium);
	letter-spacing: -.02em;
	color: var(--hs-value-ink);
}

.hs-node-hero-unit {
	font-size: var(--text-h6);
	font-weight: var(--fw-medium);
	color: var(--hs-accent-ink);
}

.hs-node-hero-delta {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	font-size: var(--text-body2);
	font-weight: var(--fw-medium);
	font-variant-numeric: tabular-nums;
	color: var(--text-secondary);
}

.hs-node-hero-delta.up {
	color: var(--hs-success);
}

.hs-node-hero-delta.down {
	color: var(--hs-error);
}

.hs-node-hero-id {
	font-family: var(--font-mono);
	font-size: var(--text-caption);
	color: var(--text-secondary);
	overflow-wrap: anywhere;
}

/* Provenance, next to the status: the same swatch the Nodes grid uses. */
.hs-node-hero-src {
	font-family: var(--font-mono);
	font-size: var(--text-overline);
	color: var(--text-secondary);
}

.hs-node-hero-side {
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: 7px;
}

/* Inline sparkline strip under the hero number. */
.hs-sparkline {
	display: block;
	width: 100%;
	max-width: 260px;
	height: 34px;
	overflow: visible;
}

.hs-sparkline path {
	fill: none;
	stroke: var(--color-accent);
	stroke-width: 1.75;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.hs-sparkline .hs-spark-fill {
	fill: color-mix(in srgb, var(--color-accent) 16%, transparent);
	stroke: none;
}

/* Tiles on the node page. They used to be bare --neutral-fill-secondary blocks
   with 4px of padding and no title, so the four quadrants read as one grey mass. */
.hs-node-tile {
	display: flex;
	flex-direction: column;
	height: 100%;
	width: 100%;
	box-sizing: border-box;
	overflow: hidden;
	background: var(--hs-tile);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-lg);
	box-shadow: var(--elevation-1);
	transition: box-shadow var(--dur-base) var(--ease-standard);
	/* The tile is the ONLY card on this page. Anything nested inside it — the
	   hero, the property list, a grid's own container — is flattened below, so
	   the node view stops reading as boxes inside boxes inside boxes. */
	position: relative;
}

.hs-node-tile:hover {
	box-shadow: var(--elevation-4);
}

/* Tile title: a label does not need a surface of its own, so the tinted strip
   is gone; the hairline underneath is what separates it from the content.
   Optional meta on the right (a count, a window) in mono. */
.hs-tile-title {
	margin: 0;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 10px;
	padding: 10px 14px 8px;
	font-size: 10px;
	letter-spacing: .11em;
	text-transform: uppercase;
	font-weight: var(--fw-bold);
	color: var(--text-secondary);
	border-bottom: 1px solid var(--border-default);
	flex: 0 0 auto;
	white-space: nowrap;
	overflow: hidden;
}

/* The label names the card and is two words; the meta beside it is a node id that
   can be any length. The meta used to be rigid, so a long id ellipsised the label
   that identifies the card instead of itself. The label holds, the meta gives. */
.hs-tile-title > span:first-child {
	flex: 0 0 auto;
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
}

.hs-tile-meta {
	font-family: var(--font-mono);
	font-size: 10.5px;
	font-weight: var(--fw-medium);
	letter-spacing: 0;
	text-transform: none;
	color: var(--text-secondary);
	flex: 0 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* A count badge in the title: alarms on a tile. */
.hs-tile-count {
	font-family: var(--font-mono);
	font-size: 10.5px;
	font-weight: var(--fw-semibold);
	letter-spacing: 0;
	padding: 1px 7px;
	border-radius: var(--radius-pill);
	background: var(--sev-bg, var(--hs-tile-bar));
	color: var(--sev-ink, var(--text-secondary));
	border: 1px solid color-mix(in srgb, var(--sev, var(--border-default)) 40%, transparent);
	flex: 0 0 auto;
}

/* Untitled tiles (the hero, the chart) still need their content inset. */
.hs-node-tile > :not(.hs-tile-title) {
	padding: 0;
}

/* The child that holds the actual content is the one that scrolls, or the tile
   grows past its grid cell. */
.hs-node-tile > :last-child:not(.hs-tile-title) {
	flex: 1 1 auto;
	min-height: 0;
	overflow: auto;
}

/* ---- flattening the nested surfaces ----
   .events-container paints --neutral-fill-secondary. Standalone that is correct;
   inside a tile it became a second, differently-shaded box floating in the
   first — the most visible offender in dark mode. */
.hs-node-tile .events-container,
.hs-node-tile .alarm-list-container {
	background: transparent !important;
	height: 100%;
}

/* The node body, in a tile or in the dialog: full-bleed sections — the hero,
   the alarm chips, the write control, the property list — separated by
   hairlines rather than padded blocks floating in a padded box. */
.hs-node-body {
	display: flex;
	flex-direction: column;
	gap: 0;
	height: 100%;
	min-height: 0;
	overflow-y: auto;
}

.hs-node-body > * + * {
	border-top: 1px solid var(--border-default);
}

.hs-node-section {
	padding: 13px 16px;
}

/* ---- event dialog hero ----
   Same shape as the node hero, but the headline is prose rather than a number, and
   the severity rail ties it to the row the user clicked to get here. */
.hs-event-hero {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--space-4);
	padding: var(--space-4) var(--space-4) var(--space-4) var(--space-3);
	border-radius: var(--radius-lg);
	border: 1px solid var(--border-default);
	border-left: 5px solid var(--sev, var(--border-default));
	background: color-mix(in srgb, var(--sev, transparent) 12%, var(--hs-tile));
}

body.theme-dark .hs-event-hero {
	background: color-mix(in srgb, var(--sev, transparent) 20%, var(--hs-tile));
}

.hs-event-hero-main {
	display: flex;
	flex-direction: column;
	gap: 4px;
	min-width: 0;
	flex: 1 1 auto;
}

.hs-event-hero-title {
	font-size: var(--text-h6);
	font-weight: var(--fw-semibold);
	line-height: var(--lh-heading);
	color: var(--text-primary);
	overflow-wrap: anywhere;
}

/* ---- humanised property list, replacing the key/value table ---- */
/* Borderless by default — inside a tile it would be a box in a box. .hs-card
   puts the frame back for standalone use. */
.hs-proplist {
	display: grid;
	grid-template-columns: 150px minmax(0, 1fr);
	gap: 0;
	overflow: hidden;
	margin: 0;
}

.hs-proplist.hs-card {
	border-radius: var(--radius-lg);
	border: 1px solid var(--border-default);
	background: var(--hs-tile);
}

.hs-proplist > dt,
.hs-proplist > dd {
	display: flex;
	align-items: center;
	min-height: 34px;
	padding: 0 16px;
	margin: 0;
	box-sizing: border-box;
	border-top: 1px solid var(--hs-hairline);
	min-width: 0;
}

/* The first row's rule is the section separator above it. */
.hs-proplist > dt:first-of-type,
.hs-proplist > dt:first-of-type + dd {
	border-top: 0;
}

.hs-proplist > dt {
	color: var(--text-secondary);
	font-size: 11.5px;
}

.hs-proplist > dd {
	color: var(--text-primary);
	font-size: 12px;
	overflow-wrap: anywhere;
}

.hs-proplist code {
	font-family: var(--font-mono);
	font-size: var(--text-caption);
	color: var(--text-primary);
}

/* ============================================================
   Forms — labelled fields, used by the dialogs.
   ============================================================ */
.hs-field {
	display: flex;
	flex-direction: column;
	gap: 5px;
	min-width: 0;
}

.hs-field-label {
	font-size: var(--text-overline);
	letter-spacing: var(--ls-overline);
	text-transform: uppercase;
	font-weight: var(--fw-bold);
	color: var(--hs-accent-ink);
}

.hs-field-hint {
	font-size: var(--text-caption);
	color: var(--text-secondary);
	line-height: var(--lh-body);
}

.hs-event-form {
	display: flex;
	flex-direction: column;
	gap: var(--space-5);
	min-width: min(420px, 84vw);
}

.hs-event-form-meta {
	display: flex;
	align-items: center;
	gap: var(--space-3);
	padding: var(--space-3);
	border-radius: var(--radius-md);
	border: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
}

/* ---- write-value control ----
   Grouped and labelled, with the unit adjacent to the field, so a write to a
   live signal reads as a deliberate action rather than a stray input box. */
.hs-write {
	display: flex;
	flex-direction: column;
	gap: 6px;
	padding: var(--space-3);
	border-radius: var(--radius-md);
	border: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
}

.hs-write-label {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	font-size: var(--text-overline);
	letter-spacing: var(--ls-overline);
	text-transform: uppercase;
	font-weight: var(--fw-bold);
	color: var(--hs-accent-ink);
}

.hs-write-row {
	display: flex;
	align-items: center;
	gap: var(--space-2);
}

.hs-write-row .hs-write-field {
	flex: 1 1 auto;
	min-width: 0;
}

.hs-write-unit {
	font-size: var(--text-body2);
	font-weight: var(--fw-medium);
	color: var(--text-secondary);
	flex: 0 0 auto;
}

/* Boolean chip — replaces the raw "False" the table used to print. */
.hs-bool {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	font-size: var(--text-caption);
	font-weight: var(--fw-medium);
	color: var(--text-secondary);
}

.hs-bool.yes {
	color: var(--hs-success);
}

/* ============================================================
   8. Command palette (Ctrl+K) and the shortcuts overlay.
   ============================================================ */
.hs-overlay {
	position: fixed;
	inset: 0;
	z-index: 2000;
	display: flex;
	justify-content: center;
	/* A plain dark scrim: a navy-tinted one over a near-black page read as a
	   tint bug rather than a modal. */
	background: rgba(2, 5, 9, .66);
	backdrop-filter: blur(3px);
	animation: hs-fade-in var(--dur-fast) var(--ease-decelerate);
}

.hs-overlay.hs-overlay-top {
	align-items: flex-start;
	padding-top: 12vh;
}

.hs-overlay.hs-overlay-center {
	align-items: center;
}

@keyframes hs-fade-in {
	0% {
		opacity: 0;
	}

	100% {
		opacity: 1;
	}
}

.hs-palette {
	width: min(620px, 92vw);
	max-height: 66vh;
	display: flex;
	flex-direction: column;
	border-radius: var(--radius-xl);
	background: var(--hs-tile);
	border: 1px solid var(--border-strong);
	box-shadow: var(--elevation-8);
	overflow: hidden;
	animation: hs-pop-in var(--dur-base) var(--ease-decelerate);
}

/* The input row: search glyph, then the field. */
.hs-palette-head {
	display: flex;
	align-items: center;
	gap: 11px;
	padding: 0 16px;
	border-bottom: 1px solid var(--border-default);
	color: var(--text-secondary);
}

.hs-palette-head > svg {
	flex: 0 0 auto;
}

@keyframes hs-pop-in {
	0% {
		opacity: 0;
		transform: translateY(-12px) scale(.98);
	}

	100% {
		opacity: 1;
		transform: none;
	}
}

.hs-palette-input {
	flex: 1 1 auto;
	min-width: 0;
	box-sizing: border-box;
	padding: 15px 0;
	border: 0;
	background: transparent;
	color: var(--text-primary);
	font-family: var(--font-sans);
	font-size: 15px;
	outline: none;
}

.hs-palette-input::placeholder {
	color: var(--text-disabled);
}

.hs-palette-list {
	overflow-y: auto;
	padding: 6px;
	margin: 0;
	list-style: none;
}

/* Results grouped by kind — pages, then configurations, then nodes. */
.hs-palette-section {
	padding: 10px 12px 4px;
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .12em;
	text-transform: uppercase;
	color: var(--text-secondary);
	list-style: none;
}

.hs-palette-item.hs-mono-item .hs-palette-label {
	font-family: var(--font-mono);
	font-size: 12.5px;
}

.hs-palette-item {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 9px 12px;
	border-radius: var(--radius-md);
	cursor: pointer;
	color: var(--text-primary);
	font-size: var(--text-body2);
}

.hs-palette-item .hs-palette-kind {
	margin-left: auto;
	font-size: var(--text-overline);
	letter-spacing: var(--ls-wide);
	text-transform: uppercase;
	color: var(--text-secondary);
	flex: 0 0 auto;
}

.hs-palette-item .hs-palette-label {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-palette-item.active,
.hs-palette-item:hover {
	background: var(--hs-accent-soft);
	color: var(--hs-accent-soft-ink);
}

.hs-palette-empty {
	padding: var(--space-6);
	text-align: center;
	color: var(--text-secondary);
	font-size: var(--text-body2);
}

.hs-palette-foot {
	display: flex;
	align-items: center;
	gap: var(--space-4);
	padding: 9px var(--space-4);
	border-top: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
	color: var(--text-secondary);
	font-size: var(--text-overline);
}

.hs-palette-scope {
	margin-left: auto;
	font-family: var(--font-mono);
	white-space: nowrap;
}

/* ---- keyboard shortcut chrome ---- */
kbd.hs-kbd {
	display: inline-block;
	min-width: 18px;
	padding: 1px 6px;
	border-radius: var(--radius-sm);
	border: 1px solid var(--border-strong);
	border-bottom-width: 2px;
	background: var(--surface-sunken);
	color: var(--text-primary);
	font-family: var(--font-mono);
	font-size: var(--text-overline);
	text-align: center;
	line-height: 1.5;
}

.hs-shortcuts {
	width: min(620px, 92vw);
	max-height: 76vh;
	overflow-y: auto;
	padding: 0;
	border-radius: var(--radius-xl);
	background: var(--hs-tile);
	border: 1px solid var(--border-strong);
	box-shadow: var(--elevation-8);
	animation: hs-pop-in var(--dur-base) var(--ease-decelerate);
}

/* Same 44px header strip as every dialog. */
.hs-shortcuts-head {
	display: flex;
	align-items: center;
	gap: 10px;
	height: 44px;
	padding: 0 18px;
	border-bottom: 1px solid var(--border-default);
	color: var(--text-primary);
	font-size: 14px;
	font-weight: var(--fw-semibold);
}

.hs-shortcuts-head > span {
	flex: 1 1 auto;
}

/* Actions on the left, the g-then-key jumps in their own column. */
.hs-shortcuts-cols {
	display: grid;
	grid-template-columns: 1fr 1fr;
}

.hs-shortcuts-col {
	display: flex;
	flex-direction: column;
	padding: 14px 18px 18px;
	min-width: 0;
}

.hs-shortcuts-col + .hs-shortcuts-col {
	border-left: 1px solid var(--border-default);
}

.hs-shortcuts-col > .hs-section-label {
	padding-bottom: 10px;
}

.hs-shortcut-row {
	display: flex;
	align-items: center;
	gap: 12px;
	height: 32px;
	font-size: 12.5px;
	color: var(--text-secondary);
}

.hs-shortcut-keys {
	display: flex;
	gap: 4px;
	flex: 0 0 auto;
	white-space: nowrap;
}

@media (max-width: 520px) {
	.hs-shortcuts-cols {
		grid-template-columns: 1fr;
	}

	.hs-shortcuts-col + .hs-shortcuts-col {
		border-left: 0;
		border-top: 1px solid var(--border-default);
	}
}

/* Overline label used by the dialogs and the shortcut columns. */
.hs-section-label {
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .12em;
	text-transform: uppercase;
	color: var(--hs-accent-ink);
}

/* ============================================================
   8b. Colour across the shell.

   The token layer is brand-navy but almost nothing consumed it below the header,
   so every page came out white-on-grey. These are the places where a tint costs
   nothing in legibility and tells you which application you are in: grid header
   rows, page toolbars, the active nav item, and section titles.
   ============================================================ */

/* ---- data grid header ----
   MUST be opaque (--hs-bar-solid, not --hs-tile-bar): these header cells are
   position: sticky, and rows scroll underneath them. With a translucent tint
   the rows showed through the header.

   An 11px uppercase rule rather than a tinted band of 13px semibold body text,
   and a hairline underneath instead of the 2px accent rule — a 2px accent line
   under the header of every table was a lot of colour spent on a line. */
.fluent-data-grid th[cell-type="columnheader"],
.fluent-data-grid thead th {
	height: 32px !important;
	background: var(--hs-bar-solid) !important;
	color: var(--hs-ink-3) !important;
	font-size: 10px !important;
	font-weight: var(--fw-bold) !important;
	letter-spacing: .11em !important;
	text-transform: uppercase !important;
	border-bottom: 1px solid var(--border-default) !important;
}

.fluent-data-grid th[cell-type="columnheader"] {
	padding-top: 0 !important;
	padding-bottom: 0 !important;
}

body:not(.theme-dark) .fluent-data-grid th[cell-type="columnheader"],
body:not(.theme-dark) .fluent-data-grid thead th {
	border-bottom-color: var(--border-strong) !important;
}

/* The column the grid is sorted by carries the accent ink. */
.fluent-data-grid th[aria-sort="ascending"],
.fluent-data-grid th[aria-sort="descending"] {
	color: var(--hs-accent-ink) !important;
}

/* A sortable header renders its title inside a subtle button. The button is a
   web component whose :host carries Fluent's own type ramp and ink, so the host
   itself is given the header's type, and the title inside inherits it. */
.fluent-data-grid th fluent-button[col-sort-button] {
	font-size: 10px !important;
	font-weight: var(--fw-bold) !important;
	letter-spacing: .11em !important;
	text-transform: uppercase !important;
	color: inherit !important;
	line-height: 1 !important;
}

.fluent-data-grid th [col-title-text],
.fluent-data-grid th [col-sort-button] [col-title-text] {
	color: inherit !important;
	font-size: inherit !important;
	font-weight: inherit !important;
	letter-spacing: inherit !important;
	text-transform: inherit !important;
	line-height: inherit !important;
}

/* The button itself is invisible: no fill, no stroke, no 32px slab that pushed
   the header row past its height. Hover brightens the ink, nothing else. */
.fluent-data-grid th fluent-button[col-sort-button] {
	background: transparent !important;
	border-color: transparent !important;
	height: 24px !important;
	min-height: 0 !important;
	padding: 0 4px !important;
	margin: 0 -4px !important;
}

.fluent-data-grid th fluent-button[col-sort-button]:hover {
	color: var(--text-primary) !important;
}

/* ---- page toolbars ----
   Flat, 44px, one hairline: the tinted strip competed with the appbar above
   it. Controls inside are 30px chips; groups are separated by a hairline
   (.hs-tool-sep) instead of whitespace alone. The pages set
   --neutral-fill-secondary inline, so this needs the same specificity route: a
   class the pages opt into. */
.hs-toolbar {
	background: var(--hs-chrome) !important;
	border-bottom: 1px solid var(--border-default);
	box-sizing: border-box;
	min-height: 44px;
	padding: 6px 14px !important;
	column-gap: 8px !important;
	row-gap: 6px;
	flex-wrap: wrap;
}

body.theme-dark .hs-toolbar {
	border-bottom-color: var(--hs-n-300);
}

.hs-toolbar :is(fluent-button, fluent-menu-button, fluent-anchor-button),
.hs-toolbar fluent-text-input::part(root) {
	height: 30px !important;
	min-height: 30px !important;
	border-radius: 8px !important;
}

.hs-toolbar :is(fluent-button, fluent-menu-button, fluent-anchor-button) {
	font-size: 12.5px;
}

/* A bordered group of icon buttons: one chip, hairlines between. */
.hs-tool-group {
	display: inline-flex;
	align-items: center;
	height: 30px;
	border: 1px solid var(--hs-control-stroke);
	border-radius: 8px;
	background: var(--hs-tile);
	overflow: hidden;
	flex: 0 0 auto;
}

.hs-tool-group fluent-button {
	background: transparent !important;
	border: 0 !important;
	border-radius: 0 !important;
	height: 28px !important;
	min-height: 28px !important;
	min-width: 30px !important;
	color: var(--text-secondary) !important;
}

.hs-tool-group fluent-button:hover {
	background: var(--hs-tile-bar) !important;
	color: var(--text-primary) !important;
}

.hs-tool-group > * + * {
	border-left: 1px solid var(--border-default);
}

.hs-tool-sep {
	width: 1px;
	height: 18px;
	background: var(--border-default);
	margin: 0 2px;
	flex: 0 0 auto;
}

.hs-tool-count {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 11px;
	color: var(--text-secondary);
	white-space: nowrap;
	flex: 0 0 auto;
}

.hs-tool-spacer {
	flex: 1 1 auto;
}

/* A run of related controls that also carries a tour id, so it needs a real box. */
.hs-tool-cluster {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	row-gap: 6px;
}

/* Segmented control: the time span presets, UTC/local. Buttons, so it is
   keyboard-reachable; aria-pressed carries the state. */
.hs-seg {
	display: inline-flex;
	align-items: center;
	height: 30px;
	padding: 2px;
	box-sizing: border-box;
	gap: 1px;
	border: 1px solid var(--hs-control-stroke);
	border-radius: 8px;
	background: var(--hs-tile);
	flex: 0 0 auto;
}

.hs-seg > button {
	height: 24px;
	padding: 0 7px;
	border: 0;
	border-radius: 6px;
	background: transparent;
	color: var(--text-secondary);
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 11.5px;
	line-height: 1;
	cursor: pointer;
	white-space: nowrap;
	transition: background var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}

.hs-seg > button:hover {
	color: var(--text-primary);
	background: var(--hs-tile-bar);
}

.hs-seg > button[aria-pressed="true"] {
	background: var(--hs-accent-soft);
	color: var(--hs-accent-soft-ink);
	font-weight: var(--fw-semibold);
}

.hs-seg > button:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

/* A toggle that is ON — Follow, Auto refresh: accent-soft fill and a breathing
   dot, rather than a fully filled accent button that read as a primary action. */
.hs-toggle-on {
	background: var(--hs-accent-soft) !important;
	border-color: color-mix(in srgb, var(--color-accent) 40%, transparent) !important;
	color: var(--hs-accent-soft-ink) !important;
	font-weight: var(--fw-semibold);
}

.hs-live-dot {
	display: inline-block;
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--color-accent);
	margin-right: 7px;
	vertical-align: middle;
	animation: hs-pulse 2s var(--ease-standard) infinite;
}

/* A labelled control group in a toolbar: "Scale  min … max  Auto". One control on the
   Live toolbar and on a trend card, so the two scales read and work alike. */
.hs-tool-field {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	height: 30px;
	padding: 0 4px 0 11px;
	box-sizing: border-box;
	border: 1px solid var(--hs-control-stroke);
	border-radius: 8px;
	background: var(--hs-tile);
	font-size: 12px;
	color: var(--text-secondary);
	white-space: nowrap;
	flex: 0 0 auto;
}

/* The scale fields - text fields that take the number in the page's language (HsNumberInput),
   so no spinner - at the toolbar's 12.5px and 104px of width, so a value like -0.0321 or
   12345.6 fits with room instead of being clipped at the edge. */
.hs-tool-field fluent-field:has(> fluent-text-input) {
	width: 104px;
}

.hs-tool-field fluent-text-input::part(control) {
	font-size: 12.5px;
	padding: 0 8px;
}

.hs-tool-field fluent-text-input::part(root) {
	height: 24px !important;
	min-height: 24px !important;
	border-radius: 5px !important;
}

.hs-tool-field .hs-tool-ellipsis {
	color: var(--text-disabled);
}

/* ---- sidebar: 34px rows, a real active state, section labels, a rail ----
   Plain markup (NavMenu.razor). At 34px the whole list fits without the old
   Settings group having to collapse, which is what made "where is Signal
   Builder" a two-click question. The rail (body.hs-nav-collapsed) keeps the
   icons and the alarm dot and drops the labels, the counts and the footer. */
.navmenu {
	width: 236px;
	flex: 0 0 auto;
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	min-height: 0;
	box-sizing: border-box;
	background: var(--neutral-fill-stealth-rest) !important;
	border-right: 1px solid var(--neutral-stroke-divider-rest);
	transition: width var(--dur-base) var(--ease-standard);
}

body.theme-dark .navmenu {
	border-right-color: var(--hs-n-300);
}

.hs-nav {
	display: flex;
	flex-direction: column;
	min-height: 0;
	padding: 6px 0 8px;
	overflow-y: auto;
	overflow-x: hidden;
}

.hs-nav-collapse {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	margin: 2px 8px 4px 10px;
	border: 0;
	border-radius: 7px;
	background: transparent;
	color: var(--hs-ink-3);
	cursor: pointer;
	flex: 0 0 auto;
}

.hs-nav-collapse:hover {
	background: var(--hs-tile-bar);
	color: var(--text-primary);
}

body.theme-dark .hs-nav-collapse:hover {
	background: var(--hs-n-250);
}

.hs-nav-link {
	position: relative;
	display: flex;
	align-items: center;
	gap: 10px;
	height: 34px;
	margin: 2px 8px 0;
	padding: 0 10px;
	box-sizing: border-box;
	width: calc(100% - 16px);
	border: 0;
	border-radius: 7px;
	background: transparent;
	color: var(--hs-ink-2);
	font: inherit;
	font-size: 13px;
	text-align: left;
	text-decoration: none;
	white-space: nowrap;
	cursor: pointer;
	flex: 0 0 auto;
	transition: background var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}

.hs-nav-icon {
	display: flex;
	flex: 0 0 auto;
	width: 16px;
	height: 16px;
	color: var(--hs-ink-4);
}

.hs-nav-icon svg {
	width: 16px !important;
	height: 16px !important;
	fill: currentColor !important;
}

.hs-nav-text {
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}

.hs-nav-link:hover {
	background: var(--hs-tile-bar);
	color: var(--text-primary);
}

body.theme-dark .hs-nav-link:hover {
	background: var(--hs-n-250);
}

/* The rail stays (it is the clearest "you are here" in a list of fourteen)
   but at 2px, on a tinted fill that is an opaque step of the scale rather
   than a translucent wash. */
.hs-nav-link.active {
	background: var(--hs-accent-soft);
	box-shadow: inset 2px 0 0 0 var(--color-accent);
	color: var(--hs-accent-soft-ink);
	font-weight: var(--fw-semibold);
}

.hs-nav-link.active .hs-nav-icon {
	color: var(--color-accent);
}

.hs-nav-link:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

.hs-nav-badge {
	margin-left: auto;
	padding-left: 8px;
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 10.5px;
	font-weight: var(--fw-semibold);
	color: var(--text-secondary);
	flex: 0 0 auto;
}

/* The alarm count is a pill in the severity's own colours. */
.hs-nav-badge.hs-nav-badge-alarm {
	padding: 1px 7px;
	border-radius: var(--radius-pill);
	background: var(--sev-bg);
	color: var(--sev-ink);
	border: 1px solid color-mix(in srgb, var(--sev) 40%, transparent);
}

/* Section labels. */
.hs-nav-section {
	padding: 14px 18px 5px;
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .14em;
	text-transform: uppercase;
	color: var(--hs-ink-3);
	flex: 0 0 auto;
}

.hs-nav-collapse + .hs-nav-section {
	padding-top: 4px;
}

/* One hairline between the monitoring sections and the tray of tools. */
.hs-nav-rule {
	height: 1px;
	margin: 14px 16px 2px;
	background: var(--neutral-stroke-divider-rest);
	flex: 0 0 auto;
}

/* Favourites: a group with a chevron; its rows indent under the icon column. */
.hs-nav-group {
	display: flex;
	flex-direction: column;
	flex: 0 0 auto;
}

.hs-nav-chevron {
	margin-left: auto;
	flex: 0 0 auto;
	color: var(--hs-ink-4);
	transition: transform var(--dur-fast) var(--ease-standard);
}

.hs-nav-group.open .hs-nav-chevron {
	transform: rotate(90deg);
}

/* Panels: a group whose head is a link to its own page, so the chevron is a
   separate control beside it rather than part of the row. The link gives up the
   chevron's width; together they still measure one .hs-nav-link. */
.hs-nav-grouprow {
	display: flex;
	align-items: center;
	flex: 0 0 auto;
}

.hs-nav-grouprow > .hs-nav-link {
	width: auto;
	flex: 1 1 auto;
	min-width: 0;
	margin-right: 0;
}

.hs-nav-twist {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 22px;
	height: 34px;
	margin: 2px 8px 0 0;
	padding: 0;
	border: 0;
	border-radius: 0 7px 7px 0;
	background: transparent;
	color: var(--hs-ink-4);
	cursor: pointer;
	flex: 0 0 auto;
}

.hs-nav-twist:hover {
	background: var(--hs-tile-bar);
	color: var(--text-primary);
}

body.theme-dark .hs-nav-twist:hover {
	background: var(--hs-n-250);
}

.hs-nav-twist:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

.hs-nav-twist svg {
	transition: transform var(--dur-fast) var(--ease-standard);
}

.hs-nav-group.open .hs-nav-twist svg {
	transform: rotate(90deg);
}

.hs-nav-group-items {
	display: flex;
	flex-direction: column;
}

.hs-nav-group-items .hs-nav-link {
	height: 30px;
	padding-left: 36px;
	font-size: 12.5px;
}

.hs-nav-group-items .hs-nav-link .hs-nav-icon {
	display: none;
}

.hs-nav-empty {
	display: block;
	padding: 4px 36px 6px;
	font-size: 12px;
	color: var(--text-disabled);
}

/* The footer: About by the build stamp, and the update chip when there is one.
   The version was 14px body text in the app's main ink; it is a build stamp. */
.hs-nav-foot {
	display: flex;
	align-items: center;
	gap: 10px;
	margin: 0 16px;
	padding: 10px 0;
	border-top: 1px solid var(--neutral-stroke-divider-rest);
	font-size: 12.5px;
	flex: 0 0 auto;
	min-width: 0;
}

.hs-nav-foot-link {
	display: flex;
	align-items: center;
	gap: 6px;
	flex: 1 1 auto;
	min-width: 0;
	color: var(--text-secondary);
	text-decoration: none;
}

.hs-nav-foot-link:hover {
	color: var(--text-primary);
}

.hs-nav-foot-link.active {
	color: var(--hs-accent-soft-ink);
	font-weight: var(--fw-semibold);
}

.hs-nav-foot-icon {
	display: none;
	width: 16px;
	height: 16px;
}

.hs-nav-foot-icon svg {
	width: 16px;
	height: 16px;
	fill: currentColor;
}

.hs-nav-version {
	font-family: var(--font-mono);
	font-size: 10.5px;
	color: var(--hs-ink-4);
	white-space: nowrap;
	flex: 0 0 auto;
}

.hs-nav-update {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 2px 8px;
	border-radius: var(--radius-pill);
	background: var(--hs-success-bg);
	color: var(--hs-success-darken);
	border: 1px solid color-mix(in srgb, var(--hs-success) 45%, transparent);
	font-size: 10.5px;
	font-weight: var(--fw-semibold);
	text-decoration: none;
	white-space: nowrap;
	flex: 0 0 auto;
}

body.theme-dark .hs-nav-update {
	color: #93DDB0;
}

.hs-nav-update-dot {
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--hs-success);
	animation: hs-pulse 2.4s var(--ease-standard) infinite;
}

/* ---- the rail ---- */
@media (min-width: 601px) {
	body.hs-nav-collapsed .navmenu {
		width: 56px;
	}

	body.hs-nav-collapsed .hs-nav-text,
	body.hs-nav-collapsed .hs-nav-chevron,
	body.hs-nav-collapsed .hs-nav-twist,
	body.hs-nav-collapsed .hs-nav-version,
	body.hs-nav-collapsed .hs-nav-group-items,
	body.hs-nav-collapsed .hs-nav-badge:not(.hs-nav-badge-alarm) {
		display: none;
	}

	/* A section boundary becomes a hairline; the first one, under the toggle, nothing. */
	body.hs-nav-collapsed .hs-nav-section {
		height: 1px;
		padding: 0;
		margin: 10px 14px 6px;
		background: var(--neutral-stroke-divider-rest);
		font-size: 0;
		color: transparent;
	}

	body.hs-nav-collapsed .hs-nav-collapse + .hs-nav-section {
		display: none;
	}

	body.hs-nav-collapsed .hs-nav-rule {
		margin: 10px 14px 4px;
	}

	body.hs-nav-collapsed .hs-nav-link {
		width: 36px;
		margin: 2px 10px 0;
		padding: 0;
		justify-content: center;
	}

	/* With the chevron gone the group's head is a plain rail icon again. */
	body.hs-nav-collapsed .hs-nav-grouprow > .hs-nav-link {
		flex: 0 0 auto;
	}

	body.hs-nav-collapsed .hs-nav-link.active {
		box-shadow: none;
	}

	/* The alarm count becomes a dot in the icon's corner. */
	body.hs-nav-collapsed .hs-nav-badge.hs-nav-badge-alarm {
		position: absolute;
		top: 5px;
		right: 5px;
		width: 7px;
		height: 7px;
		padding: 0;
		margin: 0;
		border: 0;
		border-radius: 50%;
		background: var(--sev);
		font-size: 0;
	}

	body.hs-nav-collapsed .hs-nav-foot {
		flex-direction: column;
		gap: 8px;
		margin: 0 10px;
	}

	body.hs-nav-collapsed .hs-nav-foot-link {
		flex: 0 0 auto;
	}

	body.hs-nav-collapsed .hs-nav-foot-icon {
		display: flex;
	}

	body.hs-nav-collapsed .hs-nav-update {
		padding: 3px;
	}
}

@media (max-width: 600px) {
	.hs-nav-collapse {
		display: none;
	}
}

/* ============================================================
   8b2. Page headers, settings pages, stat chips, filter rows.

   Alarms, Events, Plugins and the four Settings pages share one page head: a
   title, an optional one-line purpose, the primary action on the right. The
   settings pages add a tab strip underneath — Users, Permissions, Roles and
   Display are four tabs of one Settings destination rather than four nav
   links to it.
   ============================================================ */
.hs-page-header {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-end;
	justify-content: space-between;
	gap: 14px 20px;
	padding: 18px 20px 14px;
	border-bottom: 1px solid var(--border-default);
	flex: 0 0 auto;
}

/* The tab-strip variant carries its own rule (the tabs' baseline). */
.hs-page-header.hs-page-header-tabs {
	flex-direction: column;
	align-items: stretch;
	border-bottom: 0;
	padding-bottom: 0;
}

/* A page head with a filter row underneath (Alarms, Events): the row carries the
   rule, the head only closes the gap to it. */
.hs-page-header.hs-page-header-filters {
	border-bottom: 0;
	padding-bottom: 12px;
}

.hs-page-headrow {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-end;
	justify-content: space-between;
	gap: 14px 20px;
}

.hs-page-heading {
	display: flex;
	flex-direction: column;
	gap: 5px;
	min-width: 0;
}

.hs-page-header h1 {
	margin: 0;
	font-size: 22px;
	font-weight: var(--fw-semibold);
	letter-spacing: -.01em;
	line-height: 1.2;
	color: var(--text-primary);
}

.hs-page-sub {
	font-size: 12.5px;
	color: var(--text-secondary);
}

.hs-page-actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px;
	flex: 0 0 auto;
}

.hs-page-actions :is(fluent-button, fluent-menu-button, fluent-anchor-button),
.hs-page-actions fluent-text-input::part(root) {
	height: 32px !important;
	min-height: 32px !important;
	border-radius: 8px !important;
}

/* Tally line in a page head: "2 running · 1 degraded". */
.hs-page-counts {
	display: flex;
	align-items: center;
	gap: 18px;
	font-size: 11.5px;
	color: var(--text-secondary);
	white-space: nowrap;
}

.hs-page-counts b {
	font-family: var(--font-mono);
	font-weight: var(--fw-semibold);
	color: var(--text-primary);
}

.hs-page-counts b.ok {
	color: var(--sev-ink);
}

/* ---- stat chips: the situation at a glance, before reading a row ---- */
.hs-stats {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px;
}

.hs-stat {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	padding: 4px 10px;
	border-radius: 7px;
	border: 1px solid var(--border-default);
	background: var(--hs-tile);
	color: var(--text-primary);
	font-size: 11.5px;
	white-space: nowrap;
}

.hs-stat::before {
	content: "";
	width: 7px;
	height: 7px;
	border-radius: 2px;
	background: var(--sev, var(--text-secondary));
	flex: 0 0 auto;
}

.hs-stat b {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-weight: var(--fw-semibold);
}

.hs-stat.hs-stat-sev {
	background: var(--sev-bg);
	color: var(--sev-ink);
	border-color: color-mix(in srgb, var(--sev) 40%, transparent);
}

/* ---- filter row under a page head ---- */
.hs-filter-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 10px;
	padding: 0 20px 14px;
	border-bottom: 1px solid var(--border-default);
	flex: 0 0 auto;
}

.hs-filter-row :is(fluent-button, fluent-menu-button),
.hs-filter-row fluent-text-input::part(root) {
	height: 30px !important;
	min-height: 30px !important;
	border-radius: 8px !important;
}

/* Type chips: one per event kind, its dot in the kind's colour. */
.hs-chips {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 6px;
}

.hs-chip {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	height: 28px;
	padding: 0 11px;
	border-radius: var(--radius-pill);
	border: 1px solid var(--border-default);
	background: transparent;
	color: var(--text-secondary);
	font: inherit;
	font-size: 12px;
	font-weight: var(--fw-medium);
	cursor: pointer;
	white-space: nowrap;
	transition: background var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}

.hs-chip::before {
	content: "";
	width: 7px;
	height: 7px;
	border-radius: 2px;
	background: var(--sev, var(--text-secondary));
	flex: 0 0 auto;
}

/* A chip that stands for neither a kind nor a severity — a trend tile's time frame — has no
   colour to carry, so it drops the dot. It is also a single choice rather than a toggle in a
   set, and takes the accent fill for it: one of these is always on, and the tile-bar fill the
   toggles use is too quiet to say which. */
.hs-chip-plain::before {
	display: none;
}

.hs-chip-plain[aria-pressed="true"] {
	background: var(--hs-accent-soft);
	border-color: var(--color-accent);
	color: var(--hs-accent-soft-ink);
}

.hs-chip:hover {
	color: var(--text-primary);
	background: var(--hs-tile-bar);
}

.hs-chip[aria-pressed="true"] {
	background: var(--hs-tile-bar);
	color: var(--text-primary);
	border-color: var(--border-strong);
	font-weight: var(--fw-semibold);
}

.hs-chip:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

/* A count inside a chip - the alarm page's state chips lead with their tally. */
.hs-chip b {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-weight: var(--fw-semibold);
	color: var(--text-primary);
}

/* ---- settings tab strip ---- */
.hs-tabs {
	display: flex;
	align-items: flex-end;
	gap: 2px;
	margin-top: 4px;
}

.hs-tabs a {
	padding: 7px 13px 9px;
	font-size: 12.5px;
	font-weight: var(--fw-medium);
	color: var(--text-secondary);
	text-decoration: none;
	white-space: nowrap;
	box-shadow: inset 0 -1px 0 0 var(--border-default);
	transition: color var(--dur-fast) var(--ease-standard);
}

.hs-tabs a:hover {
	color: var(--text-primary);
}

.hs-tabs a.active {
	color: var(--text-primary);
	font-weight: var(--fw-semibold);
	box-shadow: inset 0 -2px 0 0 var(--color-accent);
}

.hs-tabs .hs-tabs-fill {
	flex: 1 1 auto;
	height: 1px;
	background: var(--border-default);
}

/* Kept for the settings pages' scroll body. */
.hs-settings-body {
	flex: 1 1 auto;
	min-height: 0;
	overflow: auto;
	padding: 16px 20px 20px;
}

/* The table sits in a card: frame, header rule, optional footer strip. */
.hs-table-card {
	border: 1px solid var(--border-default);
	border-radius: var(--radius-lg);
	background: var(--hs-tile);
	overflow: hidden;
}

.hs-table-card > .fluent-table,
.hs-settings-body .fluent-table {
	border: 0;
	border-radius: 0;
	background: transparent;
}

.hs-settings-body .fluent-table th {
	height: 32px;
	padding: 0 16px;
	background: var(--hs-bar-solid) !important;
	color: var(--text-secondary) !important;
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .11em;
	text-transform: uppercase;
	border-bottom: 1px solid var(--border-default);
}

.hs-settings-body .fluent-table td {
	height: 42px;
	padding: 0 16px;
	color: var(--text-primary);
	border-bottom: 1px solid var(--hs-hairline) !important;
	font-size: 12.5px;
}

.hs-settings-body .fluent-table tbody tr:hover td {
	background: var(--hs-tile-bar);
}

.hs-table-foot {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 6px 16px;
	padding: 10px 16px;
	border-top: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
	font-size: 11px;
	color: var(--text-secondary);
}

.hs-table-foot > span:first-child {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* Users: initials avatar, role badge. */
.hs-avatar {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 24px;
	height: 24px;
	border-radius: 50%;
	background: var(--hs-accent-soft);
	color: var(--hs-accent-soft-ink);
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .02em;
	flex: 0 0 auto;
	user-select: none;
}

.hs-user-cell {
	display: flex;
	align-items: center;
	gap: 10px;
	min-width: 0;
}

.hs-user-cell > span:last-child {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-role {
	display: inline-block;
	padding: 2px 9px;
	border-radius: 5px;
	border: 1px solid var(--border-default);
	background: var(--hs-tile);
	color: var(--text-secondary);
	font-size: 10.5px;
	font-weight: var(--fw-semibold);
	letter-spacing: .04em;
	white-space: nowrap;
}

.hs-role.hs-role-admin {
	background: var(--hs-accent-soft);
	color: var(--hs-accent-soft-ink);
	border-color: color-mix(in srgb, var(--color-accent) 30%, transparent);
}

/* ============================================================
   8b-2. Plugins page: a rail of plugins beside the one you picked.

   Was a grid of 400px cards, one per plugin. Every plugin got the same box
   whatever it had to show: the QuestDB card — metrics, a log, the history
   tables and the projection — folded itself into tabs inside a 340px column,
   while a simulation provider with one log line was given the same 400px of
   height and left most of it empty. Picking one plugin and giving it the pane
   lets each pane be the shape of what it holds; the rail keeps every plugin
   and its state visible at once, which is what the grid of cards was for.
   ============================================================ */
.hs-plugins {
	flex: 1 1 auto;
	min-height: 0;
	display: grid;
	grid-template-columns: 268px minmax(0, 1fr);
}

/* ---- the rail: one row per plugin, grouped by what the plugin is ---- */
.hs-plugin-rail {
	min-width: 0;
	display: flex;
	flex-direction: column;
	overflow: hidden;
	background: var(--hs-aside);
	border-right: 1px solid var(--border-default);
}

.hs-plugin-rail-head {
	flex: 0 0 auto;
	padding: 12px 14px 10px;
	border-bottom: 1px solid var(--hs-hairline);
}

.hs-plugin-rail-head fluent-text-input::part(root) {
	height: 30px !important;
	min-height: 30px !important;
	border-radius: 8px !important;
	width: 100%;
}

.hs-plugin-group {
	padding: 12px 8px 5px;
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .14em;
	text-transform: uppercase;
	color: var(--text-secondary);
}

.hs-plugin-rail-body {
	flex: 1 1 auto;
	min-height: 0;
	overflow: auto;
	padding: 6px 8px 10px;
}

.hs-plugin-row {
	display: flex;
	align-items: center;
	gap: 9px;
	width: 100%;
	box-sizing: border-box;
	margin-bottom: 2px;
	padding: 8px 10px;
	border: 0;
	border-radius: 8px;
	background: transparent;
	color: inherit;
	font: inherit;
	text-align: left;
	cursor: pointer;
	min-width: 0;
	transition: background var(--dur-fast) var(--ease-standard);
}

.hs-plugin-row:hover {
	background: var(--hs-tile-bar);
}

/* The selected row carries the accent the way the nav item does: a soft fill
   and a 2px rail, not a filled accent block. */
.hs-plugin-row.on,
.hs-plugin-row.on:hover {
	background: var(--hs-accent-soft);
	box-shadow: inset 2px 0 0 0 var(--color-accent);
}

.hs-plugin-row:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

.hs-plugin-dot {
	width: 7px;
	height: 7px;
	border-radius: 50%;
	background: var(--sev);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--sev) 20%, transparent);
	flex: 0 0 auto;
}

.hs-plugin-row-text {
	flex: 1 1 auto;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 1px;
}

.hs-plugin-row-name {
	font-family: var(--font-mono);
	font-size: 11.5px;
	font-weight: var(--fw-medium);
	color: var(--text-primary);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-plugin-row.on .hs-plugin-row-name {
	color: var(--hs-accent-soft-ink);
}

/* The newest line the plugin logged. One line, clipped: the rail says what it
   last said, the pane says the rest. */
.hs-plugin-row-meta {
	font-size: 10.5px;
	color: var(--text-secondary);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-plugin-flag {
	flex: 0 0 auto;
	padding: 1px 7px;
	border-radius: var(--radius-pill);
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 10px;
	font-weight: var(--fw-semibold);
	background: var(--sev-bg);
	color: var(--sev-ink);
	border: 1px solid color-mix(in srgb, var(--sev) 40%, transparent);
}

/* ---- the pane ---- */
.hs-plugin-detail {
	min-width: 0;
	display: flex;
	flex-direction: column;
	overflow: hidden;
}

.hs-plugin-detail-head {
	flex: 0 0 auto;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 10px 16px;
	padding: 14px 20px 12px;
	border-bottom: 1px solid var(--hs-hairline);
}

.hs-plugin-ident {
	display: flex;
	flex-direction: column;
	gap: 4px;
	min-width: 0;
}

.hs-plugin-identline {
	display: flex;
	align-items: center;
	gap: 10px;
	min-width: 0;
}

.hs-plugin-title {
	margin: 0;
	font-family: var(--font-mono);
	font-size: 17px;
	font-weight: var(--fw-medium);
	letter-spacing: -.01em;
	color: var(--text-primary);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-plugin-pill {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	flex: 0 0 auto;
	padding: 2px 9px;
	border-radius: var(--radius-pill);
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .08em;
	text-transform: uppercase;
	background: var(--sev-bg);
	color: var(--sev-ink);
	border: 1px solid color-mix(in srgb, var(--sev) 40%, transparent);
}

.hs-plugin-pill::before {
	content: "";
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--sev);
}

.hs-plugin-sub {
	font-size: 12px;
	color: var(--text-secondary);
}

.hs-plugin-detail-body {
	flex: 1 1 auto;
	min-height: 0;
	overflow: auto;
	display: flex;
	flex-direction: column;
	gap: 16px;
	padding: 16px 20px 24px;
	background: var(--surface-page);
}

/* The pane scrolls; its sections keep their own height. Without this the column
   shrinks every box to fit the viewport instead of overflowing it, and the tier
   table and the metrics — the two tallest — collapse to nothing. */
.hs-plugin-detail-body > * {
	flex: 0 0 auto;
}

/* ---- the health strip: the numbers worth reading before the log ---- */
.hs-plugin-stats {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
	gap: 10px;
}

.hs-plugin-stat {
	display: flex;
	flex-direction: column;
	gap: 3px;
	min-width: 0;
	padding: 11px 13px 12px;
	border-radius: var(--radius-lg);
	background: var(--hs-tile);
	border: 1px solid var(--border-default);
	box-shadow: inset 3px 0 0 0 var(--sev, var(--color-accent));
}

.hs-plugin-stat-label {
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--text-secondary);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* The unit drops under the number rather than being clipped by it: a WAL rate of
   nine digits fills the tile on its own, and the number is the part worth keeping. */
.hs-plugin-stat-value {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0 5px;
	min-width: 0;
}

.hs-plugin-stat-value > b {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 25px;
	font-weight: var(--fw-medium);
	line-height: 1.1;
	color: var(--sev-ink, var(--text-primary));
}

.hs-plugin-stat-value > span {
	font-size: 11px;
	color: var(--text-secondary);
}

.hs-plugin-stat-note {
	font-size: 10.5px;
	color: var(--hs-ink-4);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* ---- the boxes the pane is made of ---- */
.hs-plugin-box {
	display: flex;
	flex-direction: column;
	min-width: 0;
	border-radius: var(--radius-lg);
	background: var(--hs-tile);
	border: 1px solid var(--border-default);
	overflow: hidden;
}

.hs-plugin-box-head {
	flex: 0 0 auto;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 10px;
	padding: 10px 14px;
	background: var(--hs-tile-bar);
	border-bottom: 1px solid var(--border-default);
	font-size: 12px;
	font-weight: var(--fw-semibold);
	color: var(--text-primary);
}

.hs-plugin-box-note {
	font-size: 10.5px;
	font-weight: var(--fw-regular);
	color: var(--text-secondary);
}

.hs-plugin-box-body {
	display: flex;
	flex-direction: column;
	gap: 14px;
	padding: 14px;
	min-width: 0;
}

.hs-plugin-box-foot {
	flex: 0 0 auto;
	padding: 9px 14px;
	border-top: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
	font-size: 10.5px;
	color: var(--text-secondary);
	text-wrap: pretty;
}

/* Storage beside its log, the registration facts beside theirs: a wide box and
   a narrow one. Both collapse to one column when the pane is too narrow to
   keep the log readable next to them. */
.hs-plugin-split {
	display: grid;
	grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
	gap: 16px;
	align-items: start;
}

.hs-plugin-split.hs-plugin-split-facts {
	grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr);
}

/* ---- the log ---- */
.hs-plugin-logs {
	flex: 1 1 auto;
	min-height: 0;
	max-height: 452px;
	overflow: auto;
	display: flex;
	flex-direction: column;
	gap: 4px;
	padding: 8px;
}

/* In the pane there is room for the whole line, so it wraps instead of being
   clipped — the card had to clip it, which is how a message that mattered was
   only ever half readable. */
.hs-plugin-logs .hs-log-entry {
	padding: 7px 9px;
	border-radius: var(--radius-md);
	font-size: 11.5px;
}

.hs-plugin-logs .hs-log-entry > time {
	padding-top: 1px;
}

.hs-plugin-logs .hs-log-entry > span {
	overflow: visible;
	white-space: normal;
	text-wrap: pretty;
}

.hs-plugin-logs .hs-log-entry strong {
	margin-left: 6px;
	font-family: var(--font-mono);
	font-size: 10px;
	color: var(--text-secondary);
}

/* ---- disk headroom: what is on the disk now, and what it grows into ---- */
.hs-disk-line {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 10px;
	font-size: 11.5px;
	color: var(--hs-ink-2);
}

.hs-disk-line b {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-weight: var(--fw-semibold);
	color: var(--text-primary);
}

.hs-disk-bar {
	position: relative;
	height: 12px;
	border-radius: var(--radius-md);
	background: var(--surface-sunken);
	overflow: hidden;
}

.hs-disk-now,
.hs-disk-growth,
.hs-disk-mark {
	position: absolute;
	top: 0;
	bottom: 0;
}

.hs-disk-now {
	left: 0;
	background: var(--color-accent);
}

/* Hatched, because it has not happened yet. */
.hs-disk-growth {
	background: repeating-linear-gradient(135deg, var(--navy-200) 0 4px, var(--navy-100) 4px 8px);
}

.hs-disk-mark {
	width: 2px;
	background: var(--hs-ink-4);
}

.hs-disk-legend {
	display: flex;
	flex-wrap: wrap;
	gap: 6px 16px;
	font-size: 10.5px;
	color: var(--text-secondary);
}

.hs-disk-legend > span {
	display: inline-flex;
	align-items: center;
	gap: 6px;
}

.hs-disk-legend i {
	width: 10px;
	height: 10px;
	border-radius: 2px;
	flex: 0 0 auto;
}

/* ---- the history tables, in the pane ---- */
.hs-plugin-scroll {
	overflow: auto;
	max-height: 420px;
}

.hs-plugin-box .hs-storage-table {
	margin: 0;
	font-size: 11.5px;
}

.hs-plugin-box .hs-storage-table th {
	position: sticky;
	top: 0;
	z-index: 1;
	padding: 7px 12px;
	background: var(--hs-tile);
	border-bottom: 1px solid var(--border-default);
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .08em;
	text-transform: uppercase;
	color: var(--text-secondary);
}

.hs-plugin-box .hs-storage-table td {
	padding: 7px 12px;
	border-bottom: 1px solid var(--hs-hairline);
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
}

.hs-plugin-box .hs-storage-table td:first-child {
	font-family: var(--font-sans);
}

/* A retired tier is still on the disk but no longer fed: the row steps back
   rather than leaving the table, where it would stop explaining the disk. */
.hs-plugin-box .hs-storage-table tr.retired td {
	background: var(--hs-tile-bar);
	color: var(--text-secondary);
}

.hs-plugin-box .hs-storage-table tr.total td {
	background: var(--hs-tile-bar);
	font-weight: var(--fw-semibold);
	border-bottom: 0;
}

.hs-plugin-drop {
	height: 24px;
	padding: 0 9px;
	border-radius: var(--radius-md);
	border: 1px solid color-mix(in srgb, var(--hs-error) 40%, transparent);
	background: var(--hs-error-bg);
	color: var(--hs-error-darken);
	font-family: var(--font-sans);
	font-size: 10.5px;
	font-weight: var(--fw-semibold);
	cursor: pointer;
	white-space: nowrap;
}

body.theme-dark .hs-plugin-drop {
	color: #FFB4B9;
}

.hs-plugin-drop:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

/* ---- metrics: grouped boxes, or one flat table ---- */
.hs-metric-groups {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
	gap: 12px;
	align-items: start;
	padding: 14px;
}

.hs-metric-group {
	display: flex;
	flex-direction: column;
	min-width: 0;
	border-radius: 8px;
	border: 1px solid var(--border-default);
	overflow: hidden;
}

.hs-metric-group > header {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 7px 11px;
	background: var(--hs-tile-bar);
	border-bottom: 1px solid var(--border-default);
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .1em;
	text-transform: uppercase;
	color: var(--hs-ink-2);
}

.hs-metric-group > header::before {
	content: "";
	width: 6px;
	height: 6px;
	border-radius: 2px;
	background: var(--sev, var(--color-accent));
	flex: 0 0 auto;
}

.hs-metric-row {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 12px;
	min-width: 0;
	padding: 7px 11px;
	border-top: 1px solid var(--hs-hairline);
}

.hs-metric-row > span:first-child {
	font-size: 11.5px;
	color: var(--hs-ink-2);
	min-width: 0;
	text-wrap: pretty;
}

.hs-metric-row > span:last-child {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 12px;
	font-weight: var(--fw-medium);
	white-space: nowrap;
	color: var(--sev-ink, var(--text-primary));
}

.hs-metric-table {
	width: 100%;
	border-collapse: collapse;
	font-size: 11.5px;
}

.hs-metric-table th {
	padding: 7px 14px;
	border-bottom: 1px solid var(--border-default);
	font-size: 10px;
	font-weight: var(--fw-bold);
	letter-spacing: .08em;
	text-transform: uppercase;
	color: var(--text-secondary);
	text-align: left;
}

.hs-metric-table td {
	padding: 7px 14px;
	border-bottom: 1px solid var(--hs-hairline);
	color: var(--hs-ink-2);
}

.hs-metric-table td:first-child {
	font-size: 10.5px;
	letter-spacing: .06em;
	text-transform: uppercase;
	color: var(--hs-ink-4);
	white-space: nowrap;
}

.hs-metric-table th:last-child,
.hs-metric-table td:last-child {
	text-align: right;
}

.hs-metric-table td:last-child {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-weight: var(--fw-medium);
	white-space: nowrap;
	color: var(--sev-ink, var(--text-primary));
}

/* ---- nothing to show ---- */
.hs-plugin-blank {
	flex: 1 1 auto;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: var(--space-10) var(--space-6);
	text-align: center;
	font-size: 12.5px;
	color: var(--text-secondary);
}

/* Narrow panes: the log goes under what it belongs to rather than being
   squeezed into a column of single words. */
@media (max-width: 1280px) {
	.hs-plugin-split,
	.hs-plugin-split.hs-plugin-split-facts {
		grid-template-columns: minmax(0, 1fr);
	}
}

/* Narrower still: the rail goes above the pane, so neither is a sliver. */
@media (max-width: 900px) {
	.hs-plugins {
		grid-template-columns: minmax(0, 1fr);
		grid-template-rows: minmax(0, 200px) minmax(0, 1fr);
	}

	.hs-plugin-rail {
		border-right: 0;
		border-bottom: 1px solid var(--border-default);
	}
}

/* ============================================================
   8c. Panel type picker.

   Was a row of five same-looking chip buttons over a bare editor, so choosing a
   panel type meant reading five words and guessing. Cards carry an icon and a
   one-line description, and the selected one is unambiguous.
   ============================================================ */
/* Type picker left, settings right. Stacked, the picker took the width and the
   editor was left as a cramped strip along the bottom of the dialog. */
/* Fills the dialog rather than declaring its own width — the dialog's width is set
   where it is opened (PanelDisplay / Panel). A fixed width here overflowed whatever
   Fluent's default happened to be and clipped the settings column. */
.hs-panel-dialog {
	display: grid;
	grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
	gap: var(--space-5);
	align-items: start;
	width: 100%;
}

/* One column when there is not width for two — below this the side-by-side split
   would make both halves too narrow to be worth it. */
@media (max-width: 900px) {
	.hs-panel-dialog {
		grid-template-columns: 1fr;
	}
}

.hs-panel-dialog-types,
.hs-panel-dialog-settings {
	display: flex;
	flex-direction: column;
	gap: 10px;
	min-width: 0;
}

/* The editors are loose stacks of fields; framing them gives the right-hand column
   a definite edge so it reads as a panel rather than as leftover space. */
.hs-panel-editor {
	padding: var(--space-4);
	border-radius: var(--radius-lg);
	border: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
	min-height: 140px;
}

/* The editors' own controls stack rather than run off the column edge. */
.hs-panel-editor .fluent-stack-horizontal {
	flex-wrap: wrap;
	row-gap: 10px;
}

.hs-panel-editor fluent-field:has(> fluent-text-input) {
	width: 100%;
}

.hs-picker {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
	gap: var(--space-3);
}

.hs-picker-card {
	display: flex;
	flex-direction: column;
	gap: 6px;
	padding: var(--space-4);
	border-radius: var(--radius-lg);
	border: 1px solid var(--border-default);
	background: var(--hs-tile);
	cursor: pointer;
	text-align: left;
	font: inherit;
	color: var(--text-primary);
	transition: border-color var(--dur-fast) var(--ease-standard),
				background var(--dur-fast) var(--ease-standard),
				box-shadow var(--dur-base) var(--ease-standard),
				transform var(--dur-base) var(--ease-standard);
}

.hs-picker-card:hover {
	border-color: var(--color-accent);
	box-shadow: var(--elevation-2);
	transform: translateY(-1px);
}

.hs-picker-card[aria-pressed="true"] {
	border-color: var(--color-accent);
	border-width: 2px;
	padding: calc(var(--space-4) - 1px);
	background: var(--hs-tile-bar);
	box-shadow: var(--elevation-2);
}

.hs-picker-card:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

.hs-picker-name {
	font-weight: var(--fw-semibold);
	font-size: var(--text-body2);
}

.hs-picker-card[aria-pressed="true"] .hs-picker-name {
	color: var(--hs-accent-ink);
}

.hs-picker-desc {
	font-size: var(--text-caption);
	color: var(--text-secondary);
	line-height: var(--lh-body);
}

.hs-picker-icon {
	color: var(--color-accent);
}

/* ============================================================
   8d. Compact feed — alarms and events inside a panel tile.

   A full data grid in a 300px tile is unusable: sortable headers it has no room
   for, five columns that overflow, and a horizontal scrollbar. A panel tile is
   for reading at a glance, so ordering and filtering are dropped and each entry
   becomes one line.

   The reflow uses container queries, not media queries: what matters is how wide
   the TILE is, not the window. Same panel can be wide on one dashboard and
   narrow on another.
   ============================================================ */
.hs-feed {
	container-type: inline-size;
	display: flex;
	flex-direction: column;
	height: 100%;
	min-height: 0;
	overflow-y: auto;
	overflow-x: hidden;
}

.hs-feed-item {
	display: flex;
	align-items: center;
	gap: var(--space-3);
	padding: 7px var(--space-3) 7px calc(var(--space-3) - 1px);
	border-left: 3px solid var(--sev, transparent);
	border-bottom: 1px solid var(--hs-hairline);
	background: color-mix(in srgb, var(--sev, transparent) 8%, var(--hs-tile));
	min-width: 0;
	cursor: pointer;
	transition: background var(--dur-fast) var(--ease-standard);
}

body.theme-dark .hs-feed-item {
	background: color-mix(in srgb, var(--sev, transparent) 12%, var(--hs-tile));
}

.hs-feed-item:hover {
	background: color-mix(in srgb, var(--sev, transparent) 12%, var(--hs-tile));
}

body.theme-dark .hs-feed-item:hover {
	background: color-mix(in srgb, var(--sev, transparent) 20%, var(--hs-tile));
}

/* Identity block: takes the slack and truncates rather than pushing siblings out. */
.hs-feed-main {
	display: flex;
	flex-direction: column;
	gap: 1px;
	min-width: 0;
	flex: 1 1 auto;
}

.hs-feed-title {
	font-size: var(--text-body2);
	font-weight: var(--fw-medium);
	color: var(--text-primary);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.hs-feed-sub {
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: var(--text-caption);
	color: var(--text-secondary);
	min-width: 0;
	/* Without this the mono id and the priority badge could still push the item
	   wider than the tile at very small sizes, reintroducing the scrollbar this
	   whole layout exists to remove. */
	overflow: hidden;
}

.hs-feed-sub > .hs-feed-id {
	font-family: var(--font-mono);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* The source is the least important of the three, so it truncates first: given a
   fixed shrink factor the browser takes width from here before the node id. */
.hs-feed-src {
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
	flex: 0 1 auto;
	min-width: 0;
	opacity: .85;
}

.hs-feed-sep {
	flex: 0 0 auto;
	opacity: .45;
}

.hs-feed-time {
	font-family: var(--font-mono);
	font-size: var(--text-caption);
	color: var(--text-secondary);
	white-space: nowrap;
	flex: 0 0 auto;
}

/* Status block: pill plus whatever action the row offers. */
.hs-feed-side {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	flex: 0 0 auto;
}

/* ---- the reflow ----
   Below 340px of tile width the two blocks stop competing for the same line and
   stack, which is what stops the horizontal scrollbar. */
@container (max-width: 340px) {
	.hs-feed-item {
		align-items: flex-start;
		flex-direction: column;
		gap: 5px;
	}

	.hs-feed-side {
		width: 100%;
		justify-content: space-between;
	}
}

/* Narrower still: the pill's text is dropped and only its dot and icon remain,
   so a very small tile still shows severity without wrapping. */
@container (max-width: 210px) {
	.hs-feed-side .hs-pill > span:last-child {
		display: none;
	}

	.hs-feed-side .hs-pill {
		padding: 2px 6px;
	}

	/* Last resort at tile widths this small: the secondary line is allowed to
	   wrap rather than force the item wider than its container. */
	.hs-feed-sub {
		flex-wrap: wrap;
		row-gap: 2px;
	}

	.hs-feed-item {
		padding-left: 8px;
		padding-right: 8px;
	}
}

.hs-feed-empty {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--space-2);
	flex: 1 1 auto;
	padding: var(--space-6);
	text-align: center;
	color: var(--text-secondary);
	font-size: var(--text-caption);
}

/* A page whose view list is empty (Live, Trend, Panel): what is missing, and the two ways to
   fill it — generate the starter views from the plant, or add one by hand. */
.hs-view-empty {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--space-3);
	flex: 1 1 auto;
	min-height: 240px;
	padding: var(--space-6);
	text-align: center;
	color: var(--text-secondary);
}

.hs-view-empty-actions {
	display: inline-flex;
	gap: var(--space-2);
	flex-wrap: wrap;
	justify-content: center;
}

/* Newly arrived feed entries get the same one-shot treatment as grid rows. */
.hs-feed-item.hs-row-enter {
	animation: hs-feed-in var(--dur-slow) var(--ease-decelerate);
}

@keyframes hs-feed-in {
	0% {
		opacity: 0;
		transform: translateX(-12px);
		border-left-width: 14px;
	}

	100% {
		opacity: 1;
		transform: none;
		border-left-width: 3px;
	}
}

/* ============================================================
   9. Density.
   A body-level class so one toggle reaches every grid at once.
   ============================================================ */
/* .hs-virtual marks a VIRTUALIZED grid (Nodes page, node selector dialogs) and
   opts it out of density: Blazor's Virtualize positions rows by a fixed ItemSize
   (44px), so shrinking rows under it makes the scroll position drift — the same
   "buggy scrolling" class of bug as the translucent sticky header. */
body.hs-dense .fluent-data-grid:not(.hs-virtual) [role="gridcell"],
body.hs-dense .fluent-data-grid:not(.hs-virtual) th[cell-type="columnheader"],
body.hs-dense .fluent-table th,
body.hs-dense .fluent-table td {
	padding-top: 2px !important;
	padding-bottom: 2px !important;
	height: auto !important;
	min-height: 0 !important;
	line-height: 1.35 !important;
}

body.hs-dense .events-table th,
body.hs-dense .events-table td {
	padding: 2px .5rem !important;
	height: 1.4rem !important;
	min-height: 0 !important;
	max-height: none !important;
}

body.hs-dense .hs-proplist > dt,
body.hs-dense .hs-proplist > dd {
	padding-top: 4px;
	padding-bottom: 4px;
}

/* ============================================================
   10. Fixes carried in from the audit.
   ============================================================ */

/* app.css used to round every div by 11px (301 of 324 divs, 291 of which show
   no corner). That rule is gone; the surfaces that actually show a corner
   declare the token. */
.fluent-card,
.fluent-table,
.hs-panel-editing .grid-stack-item-content,
.hs-value-tile,
.hs-node-tile,
.hs-plot-tile,
.hs-picker-card,
.hs-panel-editor,
.hs-item {
	border-radius: var(--radius-lg);
}

.hs-palette,
.hs-shortcuts,
.hs-grp,
.hs-src {
	border-radius: var(--radius-xl);
}

/* .events-container hardcoded Segoe UI, overriding Montserrat. */
.events-container {
	font-family: var(--font-sans) !important;
}

/* The design system's focus ring was defined but never applied; Fluent's own
   focus behaviour is left alone, this only covers app-owned interactive bits. */
.hs-palette-item:focus-visible,
.hs-lift:focus-visible,
.hs-sev-row[role="row"]:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

/* ============================================================
   11. About page — hero, then the plant schematic.

   An About page first. The product, its version, its status and the three things you came here to
   do own the top of the page at full size; the schematic sits underneath as supporting material.

   That order is the correction of an earlier build where the brand was a thin strip floating over
   a full-bleed diagram. It looked striking on a laptop and failed on a large screen: the page's
   actual purpose was squeezed into a bar at the very top while the diagram filled the view.

   The schematic itself: a rail of source cards on the left, one card per signal group, and traces
   between the signals that feed each other (drawn by plant-viz.js, which only measures the DOM).
   Position carries the meaning — left is where a value comes from, right is what is derived from
   it — which is why it replaced an animated constellation that moved beautifully and told you
   nothing, because nothing was encoded by where it sat. It is deliberately sparse: a signal is a
   colour and a name, and the formula, register, range, unit and alarm condition behind it are in
   its hover title.

   ---- theming ----
   Every colour comes from the --pg-* / --k-* tokens below: light values on .hs-about, dark ones
   under body.theme-dark. An earlier revision pinned the page to navy in BOTH modes on the grounds
   that its glow language needs a dark sky, which made it a black slab dropped into an otherwise
   light application. The glow gives way instead: in light mode the lamps fade, shadows do the
   lifting, and the kind palette swaps to darker hues that hold their contrast on white.
   ============================================================ */
.hs-about {
	position: relative;
	height: 100%;
	width: 100%;
	overflow-y: auto;

	/* ---- surfaces ----
	   The page sits on the SAME surface as every other page: --neutral-layer-2 is Fluent's own,
	   written inline at runtime, so this tracks the app's luminance mode instead of inventing a
	   background. It used to be a blue gradient of its own, which made About the one page in the
	   app with a colour of its own — the thing it should least have been. */
	--pg-grid: rgba(23, 48, 92, .045);
	--pg-base: var(--neutral-layer-2);
	/* Washes, not panels. Every surface in the diagram is a soft translucent film with no border
	   at all: the section used to be built from bordered cards inside bordered cards, which read
	   as a dense settings screen rather than a drawing of the plant. Depth now comes from three
	   barely-separated tints and one soft shadow. */
	--pg-card: rgba(255, 255, 255, .72);
	--pg-wash: rgba(70, 130, 180, .06);
	/* Only a 4px and an 8px sliver of each ghost is ever visible, so it needs nearly the wash's
	   own strength to register as a stacked edge at all. */
	--pg-ghost: rgba(70, 130, 180, .055);
	--pg-hover: rgba(70, 130, 180, .1);
	--pg-shadow: 0 1px 2px rgba(12, 29, 56, .05), 0 8px 24px -12px rgba(12, 29, 56, .28);

	/* ---- ink ---- */
	--pg-ink-strong: #16233A;
	--pg-ink: #26364F;
	--pg-ink-2: #4A5B72;
	/* Carries the overlines and the section furniture, which are 11px bold and so still owe AA's
	   4.5:1 — measured 4.24 at the lighter #6B7B90 this replaces. */
	--pg-ink-3: #5A6A80;
	--pg-title: var(--navy-700);
	--pg-title-shadow: none;
	--pg-logo-glow: none;
	--pg-accent-ink: var(--navy-600);

	/* ---- controls ---- */
	--pg-btn-bg: rgba(255, 255, 255, .92);
	--pg-btn-border: rgba(46, 98, 166, .3);
	--pg-btn-ink: var(--navy-700);
	--pg-btn-hover: rgba(70, 130, 180, .16);
	--pg-port: var(--navy-500);
	--pg-port-glow: 0 0 0 3px rgba(46, 98, 166, .16);
	--pg-warn-bg: rgba(233, 144, 2, .14);
	--pg-warn-ink: #8A5600;
	--pg-ok-bg: rgba(27, 150, 83, .14);
	--pg-ok-ink: #146B39;
	--pg-danger-ink: #A62A33;

	/* ---- kind palette ----
	   The single source of truth for what each source kind looks like: the row terminal, the
	   legend swatch, the source card and the trace all resolve --k from these, so the four can
	   never drift apart. Light mode needs its own set — the dark palette's pastels vanish on
	   white — and both sets are chosen to stay apart from ONE ANOTHER, since with the kind labels
	   gone the colour is the only thing carrying the kind on the surface. */
	--k-measured: #2A67A8;
	--k-engine: #0E7B98;
	--k-random: #B0468A;
	--k-constant: #5D6B7E;
	--k-formula: #7440C9;
	--k-aggregate: #AE5D06;
	--k-modbus: #1A7A43;
	--k-opcua: #0A7A6E;
	--k-extra: #6B7280;
	--k-alarm: #BE3A34;

	/* The faint blueprint paper the schematic is drawn on. Backgrounds of the scroll container
	   itself rather than an absolutely positioned layer, so they stay put while the page scrolls
	   with no element to size or keep in sync.

	   The topmost layer is a veil, and it is what keeps the grid from becoming the page's
	   identity: transparent over the middle and fully --pg-base by ~70% of the way out, so the
	   ruling only ever shows behind the diagram and has dissolved completely by the edges. It
	   works because it ends on exactly the colour painted underneath it — hence a flat
	   background-color rather than a gradient base, which could not be matched seamlessly. */
	background-color: var(--pg-base);
	background-image:
		radial-gradient(ellipse 70% 55% at 50% 42%, transparent 0%, var(--pg-base) 72%),
		linear-gradient(var(--pg-grid) 1px, transparent 1px),
		linear-gradient(90deg, var(--pg-grid) 1px, transparent 1px);
	background-size: 100% 100%, 34px 34px, 34px 34px;
	color: var(--pg-ink-2);
}

body.theme-dark .hs-about {
	/* On the near-black scale: the page itself, not a step above it, so About
	   stops being lighter than the app around it. */
	--pg-grid: rgba(150, 190, 240, .035);
	--pg-base: var(--hs-n-050);
	/* Dark keeps the same three-film idea, lifting off the page rather than tinting away from it.
	   The card is an opaque step of the scale; the two films inside it stay translucent, so the
	   hierarchy inside a card is unchanged — only its edge against the page is firmer. */
	--pg-card: #121922;
	--pg-wash: rgba(126, 166, 220, .05);
	--pg-ghost: rgba(126, 166, 220, .045);
	--pg-hover: rgba(126, 166, 220, .09);
	--pg-shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 12px 30px -16px rgba(0, 0, 0, .85);

	--pg-ink-strong: #E8EEF6;
	--pg-ink: #DCE6F3;
	--pg-ink-2: #B6C6D8;
	--pg-ink-3: #8497AE;
	--pg-title: #FFFFFF;
	--pg-title-shadow: 0 0 22px rgba(107, 163, 206, .4);
	--pg-logo-glow: drop-shadow(0 0 16px rgba(107, 163, 206, .45));
	--pg-accent-ink: var(--steel-300);

	--pg-btn-bg: var(--hs-n-200);
	--pg-btn-border: var(--hs-n-500);
	--pg-btn-ink: var(--navy-100);
	--pg-btn-hover: rgba(76, 124, 191, .3);
	--pg-port: var(--hs-accent);
	--pg-port-glow: 0 0 10px rgba(107, 163, 206, .8);
	--pg-warn-bg: rgba(233, 144, 2, .18);
	--pg-warn-ink: #F7CE8B;
	--pg-ok-bg: rgba(67, 172, 106, .18);
	--pg-ok-ink: #9FE0B7;
	--pg-danger-ink: #F3B7BC;

	--k-measured: #6BA3CE;
	--k-engine: #5BC0DE;
	--k-random: #E88BC0;
	--k-constant: #94A3B2;
	--k-formula: #BE96FF;
	--k-aggregate: #FFA85C;
	--k-modbus: #57C785;
	--k-opcua: #3FBFB0;
	--k-extra: #9E9E9E;
	--k-alarm: #E65D67;
}

.k-Measured { --k: var(--k-measured); }
.k-Engine { --k: var(--k-engine); }
.k-Random { --k: var(--k-random); }
.k-Constant { --k: var(--k-constant); }
.k-Formula { --k: var(--k-formula); }
.k-Aggregate { --k: var(--k-aggregate); }
.k-Modbus { --k: var(--k-modbus); }
.k-Opcua { --k: var(--k-opcua); }
.k-Extra { --k: var(--k-extra); }
.k-Alarm { --k: var(--k-alarm); }

/* ============================================================
   The hero — what the page is actually for.

   Sized in vh as well as px so it grows with the screen instead of leaving the identity marooned
   at the top of a large monitor, which is exactly what the previous floating strip did.
   ============================================================ */
.hs-about-hero {
	display: flex;
	flex-direction: column;
	align-items: center;
	text-align: center;
	gap: var(--space-3);
	padding: clamp(28px, 6vh, 72px) var(--space-6) clamp(24px, 4vh, 48px);
}

.hs-about-hero-logo {
	width: clamp(76px, 8vw, 116px);
	filter: var(--pg-logo-glow);
	user-select: none;
}

.hs-about-hero-title {
	margin: 0;
	font-family: var(--font-display);
	font-size: clamp(1.6rem, 3.4vw, 2.75rem);
	font-weight: var(--fw-regular);
	letter-spacing: .02em;
	line-height: 1.1;
	color: var(--pg-title);
	text-shadow: var(--pg-title-shadow);
}

.hs-about-hero-meta {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: var(--space-2);
	font-size: var(--text-body2);
	color: var(--pg-ink-2);
}

.hs-about-hero-version {
	font-family: var(--font-mono);
}

.hs-about-hero-plant {
	font-weight: var(--fw-semibold);
	letter-spacing: var(--ls-overline);
	text-transform: uppercase;
	color: var(--pg-accent-ink);
}

.hs-about-sep {
	opacity: .45;
}

.hs-about-hero-tags {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: var(--space-2);
}

.hs-about-tag {
	padding: 4px 14px;
	border-radius: var(--radius-pill);
	font-size: var(--text-caption);
	font-weight: var(--fw-semibold);
	text-decoration: none;
}

.hs-about-tag.warn {
	background: var(--pg-warn-bg);
	border: 1px solid rgba(233, 144, 2, .6);
	color: var(--pg-warn-ink);
	animation: hs-pulse 2.6s var(--ease-standard) infinite;
}

.hs-about-tag.update {
	background: var(--pg-ok-bg);
	border: 1px solid rgba(27, 150, 83, .5);
	color: var(--pg-ok-ink);
}

.hs-about-tag:hover {
	filter: brightness(1.15);
}

.hs-about-hero-actions {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: var(--space-3);
	margin-top: var(--space-2);
}

/* Own buttons rather than FluentButton: Fluent picks its accent swatches for the app's own
   surfaces, and neither set sits correctly on this page's two backgrounds. */
.hs-about-btn {
	padding: 10px 22px;
	border-radius: var(--radius-lg);
	border: 1px solid var(--pg-btn-border);
	background: var(--pg-btn-bg);
	color: var(--pg-btn-ink);
	font-family: var(--font-sans);
	font-size: var(--text-body2);
	font-weight: var(--fw-semibold);
	cursor: pointer;
	white-space: nowrap;
	text-decoration: none;
	transition: background var(--dur-fast) var(--ease-standard),
				border-color var(--dur-fast) var(--ease-standard),
				box-shadow var(--dur-base) var(--ease-standard);
}

.hs-about-btn:hover {
	background: var(--pg-btn-hover);
	border-color: var(--color-accent);
}

.hs-about-btn:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

/* The brandmark gradient carries white text in both modes, so the primary action needs no
   light/dark split of its own. */
.hs-about-btn.primary {
	border-color: transparent;
	background: var(--gradient-brandmark);
	color: #FFFFFF;
	box-shadow: var(--elevation-2);
}

.hs-about-btn.primary:hover {
	box-shadow: var(--elevation-4);
}

.hs-about-btn.danger {
	border-color: rgba(214, 69, 80, .5);
	color: var(--pg-danger-ink);
}

.hs-about-btn.danger:hover {
	background: rgba(214, 69, 80, .14);
	border-color: var(--hs-error);
}

/* ============================================================
   The schematic section — supporting material, and dressed like it.

   Capped and centred rather than full-bleed: on a wide monitor a diagram that runs to both edges
   reads as the subject of the page no matter what sits above it.
   ============================================================ */
.hs-sys {
	width: 100%;
	max-width: 1240px;
	margin: 0 auto;
	padding: 0 var(--space-6) var(--space-10);
}

/* ---- the caption ----
   One centred block under the figure: the colour key, then the totals. They used to be a heading
   pinned left and a totals row pinned right, spanning a section that is deliberately wider than
   the diagram inside it — so both sat far from the thing they describe and read as loose
   furniture. Centred under the figure they are simply its caption, which is what they are.

   No rules anywhere, here or above: horizontal lines are what made this read as a document with
   sections rather than a drawing, and space separates the parts perfectly well. */
.hs-sys-caption {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 7px;
	margin-top: var(--space-8);
	font-size: var(--text-overline);
	color: var(--pg-ink-3);
}

.hs-sys-legend,
.hs-sys-stats {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: 5px var(--space-4);
}

.hs-about-stat {
	white-space: nowrap;
}

.hs-about-stat > b {
	font-family: var(--font-mono);
	font-weight: var(--fw-semibold);
	color: var(--pg-ink-2);
	margin-right: 2px;
}

.hs-about-key {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	white-space: nowrap;
}

.hs-about-swatch {
	width: 9px;
	height: 9px;
	border-radius: 50%;
	background: var(--k);
	flex: 0 0 auto;
}

/* The alarm marker is a rail on the row, so it is an outline here too. */
.hs-about-swatch.ring {
	background: transparent;
	border: 2px solid var(--k);
	width: 7px;
	height: 7px;
}

/* ---- the diagram surface ----
   The positioning context for both trace layers, so anchor coordinates are content-relative. */
.hs-plant {
	position: relative;
}

.hs-plant-wires {
	position: absolute;
	top: 0;
	left: 0;
	pointer-events: none;
	overflow: visible;
}

/* Source runs pass BEHIND the cards they cross — that is what keeps a wide grid of groups readable
   instead of a cat's cradle over the top of it. */
.hs-plant-wires.under {
	z-index: 0;
}

/* Flow links live in the gap between a card's two columns, so they paint above its surface. */
.hs-plant-wires.over {
	z-index: 3;
}

/* The top padding is a ROUTING LANE, not spacing. A run whose target sits behind another card
   travels over the top of the grid rather than through it, and it needs somewhere above the cards
   to do that which is inside this container — the SVG layers are sized to it. */
.hs-plant-scene {
	position: relative;
	z-index: 1;
	display: flex;
	align-items: flex-start;
	gap: var(--space-10);
	padding-top: 26px;
}

.hs-plant-empty {
	position: relative;
	z-index: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--space-3);
	padding: var(--space-10) var(--space-6);
	text-align: center;
	color: var(--pg-ink-2);
}

.hs-plant-empty h3 {
	margin: 0;
	color: var(--pg-ink-strong);
}

/* ---- the source rail ----
   A glyph and an address. The kind used to be spelled out beside them, which the legend already
   does and the glyph already shows. */
.hs-rail {
	position: relative;
	z-index: 2;
	flex: 0 0 auto;
	width: 208px;
	display: flex;
	flex-direction: column;
	gap: var(--space-2);
}

/* A tint of its own kind and nothing else — no border, no shadow. The glyph and the trace leaving
   it are what identify a source; the outline was only ever a box drawn around them.

   Two lines: what kind of provider this is, and the address it reads from under it. The glyph
   centres on the pair rather than on the first line, since it stands for the whole card. */
.hs-src {
	display: flex;
	align-items: center;
	gap: 9px;
	padding: 8px 12px;
	border-radius: var(--radius-xl);
	background: color-mix(in srgb, var(--k) 9%, transparent);
}

.hs-src-glyph {
	flex: 0 0 auto;
	width: 18px;
	height: 18px;
	color: var(--k);
}

.hs-src-glyph svg {
	width: 100%;
	height: 100%;
	display: block;
}

.hs-src-text {
	display: flex;
	flex-direction: column;
	min-width: 0;
}

.hs-src-name {
	font-size: var(--text-caption);
	font-weight: var(--fw-semibold);
	color: var(--pg-ink-strong);
}

/* Mono, and a step quieter than the name: it is a machine address, and it is the supporting half
   of the pair. */
.hs-src-addr {
	font-family: var(--font-mono);
	font-size: var(--text-overline);
	color: var(--pg-ink-2);
	overflow-wrap: anywhere;
	min-width: 0;
}

/* ---- group cards ---- */
.hs-groups {
	flex: 1 1 auto;
	min-width: 0;
	display: grid;
	/* The cap belongs on the CARD, not on the track: `auto-fit` counts repetitions against the
	   track's max sizing function when that is a definite length, so `minmax(a, 400px)` asks for
	   400px per column and collapses a narrow area to a single column. */
	grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
	justify-items: center;
	gap: var(--space-5);
	align-content: start;
}

.hs-grp {
	position: relative;
	z-index: 2;
	width: 100%;
	max-width: 400px;
	border-radius: var(--radius-xl);
	background: var(--pg-card);
	box-shadow: var(--pg-shadow);
	overflow: hidden;
}

/* The inbound terminal: every source run lands here, so a card's provenance is one glance. */
.hs-grp-port {
	position: absolute;
	left: -5px;
	top: 24px;
	width: 10px;
	height: 10px;
	border-radius: 50%;
	background: var(--pg-port);
	box-shadow: var(--pg-port-glow);
}

.hs-grp-head {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	padding: 12px var(--space-4) 2px;
}

.hs-grp-title {
	font-size: var(--text-subtitle1);
	font-weight: var(--fw-semibold);
	color: var(--pg-ink-strong);
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-grp-count {
	flex: 0 0 auto;
	font-family: var(--font-mono);
	font-size: var(--text-caption);
	font-weight: var(--fw-semibold);
	padding: 1px 8px;
	border-radius: var(--radius-pill);
	background: color-mix(in srgb, var(--color-accent) 13%, transparent);
	color: var(--pg-accent-ink);
}

/* What builds this group, as its glyph alone — the same mark as the source card the run arrives
   from, so the two read as one statement instead of two labels saying the same thing. */
.hs-grp-glyph {
	flex: 0 0 auto;
	width: 17px;
	height: 17px;
	margin-left: auto;
	color: var(--k);
	opacity: .8;
}

.hs-grp-glyph svg {
	width: 100%;
	height: 100%;
	display: block;
}

/* ---- the card body: one signal column, one wiring channel ----
   The left padding is empty ON PURPOSE, and it is the whole reason the traces look deliberate.
   Every dot in the card — per-item and group alike — sits in one vertical column at the channel's
   right edge, so plant-viz.js can route each link as a right-angled trace through that empty
   strip instead of curving across whatever happens to lie between its two ends.

   An earlier revision laid group values out as a wrapped row of chips. Their dots then sat at
   arbitrary x, every link became a free-form curve, and the curves crossed the chips they passed.
   A single column is what makes clean routing possible at all. */
.hs-grp-body {
	position: relative;
	padding: var(--space-3) var(--space-3) var(--space-4);
}

/* ---- inputs | flow gap | derived ----
   The column gap is the ONLY place a flow link travels, and the split is what gives every link a
   left-to-right direction before a single line is drawn. Both columns are emitted whenever the
   CARD has any derived signal — even where this block's right column is empty — because links
   cross between blocks, and they can only stay inside the gap while the gap is at the same x in
   all of them. */
.hs-cols {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
	column-gap: 42px;
	align-items: start;
}

.hs-cols.single {
	grid-template-columns: minmax(0, 1fr);
}

.hs-col {
	min-width: 0;
}

/* ---- the "× N" made visible ----
   Two offset washes behind the panel, no outlines. A stack says "this exists N times over"
   instantly, which a number beside a list never does. They lean down-RIGHT: leaning left would
   put them in the wiring channel. */
.hs-stack {
	position: relative;
	margin-right: 10px;
}

/* "Behind" takes saying twice. As later siblings of the panel in tree order these painted OVER
   it — a wash faint enough that the eye read the stack as intended anyway — and, worse, they HIT
   over it: every hover on a per-item row landed on the offset ghost instead, so the row never
   highlighted and its links never lifted. The negative z-index puts them truly behind the panel
   (still above the card, whose z-index makes it the stacking context), and pointer-events keeps
   decoration out of hit testing even on the fringe that really is exposed. */
.hs-stack.many::before,
.hs-stack.many::after {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: var(--radius-lg);
	background: var(--pg-ghost);
	z-index: -1;
	pointer-events: none;
}

.hs-stack.many::before {
	transform: translate(4px, 4px);
}

.hs-stack.many::after {
	transform: translate(8px, 8px);
}

.hs-item {
	position: relative;
	padding: 4px 0;
	border-radius: var(--radius-lg);
	background: var(--pg-wash);
}

/* ---- group values ----
   The same rows, in no panel at all: a value that exists once does not get the stack. Two things
   separate the levels — the wash-and-stack against bare card, and a round terminal (one per item)
   against a square one (one for the group) — and neither costs a word. Both had to be *styling*
   rather than a different arrangement: clean trace routing needs every dot in a single column. */
.hs-vals {
	margin-top: var(--space-3);
}

.hs-vals .hs-sig-dot {
	border-radius: 2px;
}

/* ---- signal rows ---- */
.hs-sig {
	position: relative;
	display: flex;
	align-items: center;
	gap: 7px;
	padding: 3px 8px 3px 17px;
	border-radius: var(--radius-sm);
	font-size: var(--text-caption);
	transition: background var(--dur-fast) var(--ease-standard);
}

.hs-sig:hover {
	background: var(--pg-hover);
}

.hs-sig-dot {
	position: absolute;
	left: 5px;
	width: 7px;
	height: 7px;
	border-radius: 50%;
	background: var(--k);
}

/* Hollow: a value the backing supplies for free, rather than one somebody declared. */
.hs-sig.implicit .hs-sig-dot {
	background: transparent;
	border: 1.5px solid var(--k);
}

.hs-sig-name {
	font-weight: var(--fw-semibold);
	color: var(--pg-ink-strong);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.hs-sig.implicit .hs-sig-name {
	font-weight: var(--fw-regular);
	color: var(--pg-ink-2);
}

/* ---- alarms ----
   A breathing dot, and nothing else on the row. There was a red rail here too — an inset shadow,
   which on a rounded row renders as a heavy bracket rather than a rail, and read as clunky against
   a diagram made of hairlines and washes. It was also the third thing saying "alarm" about the
   same row: the red dashed link arriving from the signal holding the threshold says it, and so
   does the dot. */
.hs-sig.alarmed .hs-sig-dot {
	animation: hs-pulse 2s var(--ease-standard) infinite;
}

/* ---- traces ---- */
.hs-wire {
	fill: none;
	stroke: var(--k, var(--color-accent));
	stroke-linecap: round;
}

/* Inbound runs carry the flow as a marching dash — a feed that never stops, which is what an
   endpoint being polled actually is. The links inside a card move differently on purpose: one
   discrete packet per trip, because a derived value is recomputed rather than streamed. Those two
   are the only things on the page that move, so movement still means something. */
.hs-wire-run {
	stroke-width: 1.4;
	opacity: .6;
	stroke-dasharray: 3 9;
	animation: hs-wire-flow 1.4s linear infinite;
}

@keyframes hs-wire-flow {
	to {
		stroke-dashoffset: -24;
	}
}

/* The glow belongs to the dark sky only — on white it is a smudge, not a light.

   It is declared HERE rather than as a --pg-* token, and that is not a style preference: a
   custom property containing var(--k) is substituted where it is DECLARED, not where it is used.
   Held on .hs-about — which has no --k of its own — the reference is unresolvable, the property
   computes to the guaranteed-invalid value, and every descendant inherits that emptiness. The
   glow silently vanished in dark mode until this was moved onto the rule that owns --k. */
body.theme-dark .hs-wire-run {
	filter: drop-shadow(0 0 4px color-mix(in srgb, var(--k) 55%, transparent));
}

/* ---- flow links ----
   Each link is two paths on one curve, plus two end marks:

     track  the connection itself. Faint and permanent, so the card still reads as a wiring
            diagram in a still frame — and under prefers-reduced-motion, where it is all there is.
     pulse  the same curve again, lit only where a travelling band of alpha crosses it. This is the
            difference between a printed schematic and a plant that is evidently running, and it
            carries direction more plainly than any arrowhead can.
     head   a stroked chevron on the target's terminal.

   The track takes a per-link gradient — set in plant-viz.js, source colour to target colour, in
   user space along the line — and that is the part worth keeping: a line is tied to its two ends
   by colour as well as by position, so colour is not decoration here, it is the sentence "this
   value becomes that one". That gradient also starts transparent, so a link fades up out of the
   word it leaves rather than being pinned to it by a terminal dot, which only ever read as a
   second bullet on the wrong side of the name.

   Everything measured lives on the elements as inline properties, written by the script: --len
   (path length), --x0/--y0/--x1/--y1 (the pulse's sweep), and the durations and stagger delays
   that follow from them. Everything about how it looks is here.

   --o is each mark's resting opacity, named once so the entrance keyframes below can land exactly
   on it instead of restating the number. */
.hs-flow {
	fill: none;
	stroke-linecap: round;
	transition: opacity var(--dur-fast) var(--ease-standard);
}

.hs-flow-track {
	--o: .32;
	stroke-width: 1.2;
	opacity: var(--o);
	/* One dash the length of the whole path: solid at rest, and the handle the entrance draws on. */
	stroke-dasharray: var(--len);
}

/* The pulse is a whole second copy of the line, at the line's OWN width, painted in a gradient
   that is transparent everywhere except a band that ramps up to full and back down. Nothing about
   the path moves; the GRADIENT is swept along it, so what you see is a stretch of wire brightening
   and dimming again.

   That is the difference that took three tries to find. A travelling dash — of any length, however
   softly blurred at its ends — has one alpha over its whole body, so it is a block sliding on the
   line, which is to say a caterpillar. There is no shortening or softening that fixes it; the
   envelope has to be a gradient.

   Blur (the filtered group plant-viz.js puts these in) is then only for the halo. */
.hs-flow-pulse {
	stroke-width: 1.6;
	stroke-linecap: round;
}

/* Blur spreads a stroke thin. The dark sky can carry the extra weight it takes to stay a light. */
body.theme-dark .hs-flow-pulse {
	stroke-width: 2;
}

/* The sweep, on the gradient rather than on any drawn element: CSS `transform` on a
   <linearGradient> IS its gradientTransform, and animations run on elements inside <defs>. One
   pass per cycle, from a pulse-width before the line to a beat past its end; the script writes the
   endpoints because only it knows which way the link points. `backwards` holds the band off the
   line through the delay, so nothing sits lit before its turn. */
.hs-flow-wave {
	animation: hs-flow-run 2.4s linear infinite backwards;
}

@keyframes hs-flow-run {
	from { transform: translate(var(--x0), var(--y0)); }
	to { transform: translate(var(--x1), var(--y1)); }
}

.hs-flow-head {
	--o: .8;
	fill: none;
	stroke-width: 1.4;
	stroke-linecap: round;
	stroke-linejoin: round;
	opacity: var(--o);
	transition: opacity var(--dur-fast) var(--ease-standard);
}

/* Alarm links are the one relationship that is not data being consumed, so they are dashed as
   well as red: this limit WATCHES that signal rather than feeding it. */
.hs-flow-track.alarm {
	stroke-dasharray: 3.5 3.5;
}

/* ---- the entrance ----
   The links draw themselves on, in order down the diagram, once per visit. It buys the reader a
   moment to see WHERE each line goes before anything starts moving along it, and it is the reason
   the card arrives rather than simply being there.

   `backwards`, never `both`: the animation holds its first frame through the stagger delay and
   then hands the property back to the rules above. With `both` the final frame would keep winning
   and the hover dimming below would have nothing to change. */
.hs-flow-track.enter {
	animation: hs-flow-draw 620ms var(--ease-decelerate) backwards;
}

/* A dashed track cannot draw itself on — the entrance would animate the very dasharray that
   carries "watches rather than feeds" — so an alarm link fades in where the others are drawn. */
.hs-flow-track.alarm.enter,
.hs-flow-head.enter {
	animation: hs-flow-fade 300ms var(--ease-decelerate) backwards;
}

@keyframes hs-flow-draw {
	from { stroke-dashoffset: var(--len); opacity: 0; }
	to { stroke-dashoffset: 0; opacity: var(--o); }
}

@keyframes hs-flow-fade {
	from { opacity: 0; }
	to { opacity: var(--o); }
}

/* ---- following one link ----
   Hovering either end lifts its links and drops everything else, which is what finally settles
   "which pair does this line join" — a group can hold two signals of the same name, one per
   level, and no amount of routing can disambiguate that on its own. */
.hs-plant-wires.focus .hs-flow,
.hs-plant-wires.focus .hs-flow-head {
	opacity: .07;
}

.hs-plant-wires.focus .hs-flow-track.hot {
	opacity: .7;
}

.hs-plant-wires.focus .hs-flow-pulse.hot {
	opacity: 1;
	stroke-width: 2.4;
}

.hs-plant-wires.focus .hs-flow-head.hot {
	opacity: 1;
}

.hs-sig.hot {
	background: var(--pg-hover);
}

@media (max-width: 1000px) {
	/* The rail stops being a column and becomes a header strip.

	   `align-items: stretch` is load-bearing, not tidiness. Once the scene is a COLUMN, its
	   children are sized on the cross axis by align-items, and a flex-start there let the group
	   grid size to its content — a grid of `1fr` tracks has no intrinsic content width, so the
	   whole card collapsed to 2px and the plant simply vanished below the sources. */
	.hs-plant-scene {
		flex-direction: column;
		align-items: stretch;
		gap: var(--space-5);
	}

	.hs-rail {
		width: 100%;
		flex-direction: row;
		flex-wrap: wrap;
	}

	.hs-src {
		flex: 1 1 190px;
	}
}

@media (max-width: 640px) {
	.hs-sys {
		padding-inline: var(--space-4);
	}

	.hs-groups {
		grid-template-columns: 1fr;
	}

	/* Decoration; the totals are the half of the caption worth keeping at this width. */
	.hs-sys-legend {
		display: none;
	}
}


/* ============================================================
   12. Dialogs — one chrome for all of them.

   44px header with the action's own icon, body on the card surface, footer
   right-aligned with the primary action last, on the bar surface with a
   hairline above it. Fluent pads the whole control by --dialog-padding and
   lets header, body and footer float inside that; the padding moves onto the
   three parts so the header and footer strips can run full-bleed.

   `#app` is there for specificity: Fluent's own rule is scoped with an
   attribute selector, which outranks a bare element selector.
   ============================================================ */
#app fluent-dialog::part(dialog) {
	padding: 0;
	border-radius: 10px;
	border: 1px solid var(--border-default);
	background: var(--hs-tile);
	color: var(--text-primary);
	box-shadow: var(--elevation-8);
	overflow: hidden;
}

/* Light: card white, the frame a step stronger, the shadow keyed to navy. */
body:not(.theme-dark) #app fluent-dialog::part(dialog) {
	border-color: var(--border-strong);
	box-shadow: 0 18px 40px -12px rgba(12, 29, 56, .35);
}

body:not(.theme-dark) .hs-overlay {
	background: rgba(12, 29, 56, .30);
}

/* Fluent UI 5 puts header, body and footer into one fluent-dialog-body (a grid padded by 24px with its
   title, content and actions as parts); the padding moves onto the parts, as it did onto 4's three divs. */
#app fluent-dialog-body {
	padding: 0;
	gap: 0;
	background: var(--hs-tile);
}

#app fluent-dialog-body::part(title) {
	display: flex;
	align-items: center;
	gap: 10px;
	min-height: 44px;
	margin: 0;
	padding: 0 16px;
	box-sizing: border-box;
	border-bottom: 1px solid var(--border-default);
	background: transparent;
	font-size: 14px;
	font-weight: var(--fw-semibold);
	line-height: 1.3;
}

/* The dismiss button Fluent adds: a quiet 28px square on the right. */
#app fluent-dialog-body > [slot='title-action'] fluent-button {
	height: 28px;
	min-height: 28px;
	min-width: 28px;
	padding: 0;
	border-radius: 7px;
	background: transparent;
	border-color: transparent;
	color: var(--text-secondary);
}

#app fluent-dialog-body > [slot='title-action'] fluent-button:hover {
	background: var(--hs-tile-bar);
	color: var(--text-primary);
}

#app fluent-dialog-body > [slot='title-action'] fluent-button > svg {
	fill: currentColor !important;
}

/* Whatever the header holds (a stack, a label, an h3), it reads as one 14px
   semibold line; the leading icon carries the accent. */
#app fluent-dialog-body > [slot='title'] {
	flex: 1 1 auto;
	min-width: 0;
}

#app fluent-dialog-body > [slot='title'] :is(h3, h4, fluent-text, fluent-label) {
	margin: 0 !important;
	font-size: 14px !important;
	font-weight: var(--fw-semibold) !important;
	line-height: 1.3 !important;
	color: var(--text-primary);
}

#app fluent-dialog-body > [slot='title'] svg {
	width: 17px;
	height: 17px;
}

#app fluent-dialog-body::part(content) {
	padding: 18px 16px 20px;
	min-height: 0;
	box-sizing: border-box;
}

/* A dialog whose body is full-bleed sections (node properties, the snapshot). */
#app fluent-dialog-body.hs-flush::part(content) {
	padding: 0;
}

#app fluent-dialog-body::part(actions) {
	margin: 0;
	padding: 12px 16px;
	background: var(--hs-tile-bar);
	border-top: 1px solid var(--border-default);
}

/* A footer with nothing in it is a strip of bar surface for no reason. */
#app fluent-dialog-body:not(:has(> [slot='action']))::part(actions) {
	display: none;
}

/* The footer's buttons sit in one div (the action slot): it is the row. */
#app fluent-dialog-body > [slot='action'] {
	display: flex;
	flex-direction: row !important;
	align-items: center;
	justify-content: flex-end;
	gap: 8px;
	flex: 1 1 auto;
}

/* Fluent's own message boxes (errors, the CSV split confirmation) render their
   buttons primary-first; the same footer, mirrored, puts the primary last. */
#app fluent-dialog-body:has(.fluent-message-box) > [slot='action'] {
	flex-direction: row-reverse !important;
	justify-content: flex-start;
}

#app fluent-dialog-body > [slot='action'] fluent-button {
	height: 32px;
	min-height: 32px;
	border-radius: 8px;
	padding: 0 14px;
	font-size: 12.5px;
}

/* Something that is not a button on the footer's left: a link to the node
   page, a note about what gets sent. */
.hs-dialog-aside {
	margin-right: auto;
	display: flex;
	align-items: center;
	gap: 7px;
	min-width: 0;
	font-size: 11px;
	line-height: 1.5;
	color: var(--text-secondary);
}

.hs-dialog-aside fluent-button {
	background: transparent !important;
	border-color: transparent !important;
	color: var(--accent-foreground-rest) !important;
	padding-left: 6px !important;
	padding-right: 6px !important;
}

/* A destructive primary: the verb in red, not "Yes". */
.hs-danger {
	background: var(--hs-error) !important;
	border-color: transparent !important;
	color: #210709 !important;
}

.hs-danger:hover {
	background: color-mix(in srgb, var(--hs-error) 85%, #FFFFFF) !important;
}

/* Labelled fields (sign in, feedback, export): overline label, hint under. */
.hs-form {
	display: flex;
	flex-direction: column;
	gap: 16px;
}

.hs-form fluent-field:has(> fluent-text-input),
.hs-form fluent-field:has(> fluent-textarea) {
	width: 100%;
}

/* Confirmation body: the name in the app's ink, the sentence around it quieter. */
.hs-confirm-body {
	font-size: 13px;
	line-height: 1.6;
	color: var(--text-secondary);
	max-width: 420px;
}

.hs-confirm-body b {
	color: var(--text-primary);
	font-weight: var(--fw-semibold);
}

/* Mode cards (Parquet export): a radio with room for a sentence. */
.hs-mode-cards {
	display: flex;
	gap: 10px;
}

.hs-mode-card {
	flex: 1 1 0;
	display: flex;
	flex-direction: column;
	gap: 5px;
	padding: 12px 13px;
	border-radius: var(--radius-lg);
	border: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
	color: var(--text-primary);
	font: inherit;
	text-align: left;
	cursor: pointer;
	min-width: 0;
	transition: border-color var(--dur-fast) var(--ease-standard), background var(--dur-fast) var(--ease-standard);
}

.hs-mode-card[aria-checked="true"] {
	background: var(--hs-accent-soft);
	border-color: var(--color-accent);
}

.hs-mode-card:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

.hs-mode-card-title {
	display: flex;
	align-items: center;
	gap: 8px;
	font-size: 12.5px;
	font-weight: var(--fw-semibold);
}

.hs-mode-card[aria-checked="true"] .hs-mode-card-title {
	color: var(--hs-accent-soft-ink);
}

.hs-mode-card-title::before {
	content: "";
	width: 12px;
	height: 12px;
	border-radius: 50%;
	border: 1px solid var(--border-strong);
	box-sizing: border-box;
	flex: 0 0 auto;
}

.hs-mode-card[aria-checked="true"] .hs-mode-card-title::before {
	border-color: var(--color-accent);
	background: radial-gradient(circle, var(--color-accent) 0 3px, transparent 3.5px);
}

.hs-mode-card-desc {
	font-size: 11.5px;
	line-height: 1.5;
	color: var(--text-secondary);
}

/* Attribution box ("Recorded as"), file rows, the drop zone. */
.hs-meta-box {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 11px 12px;
	border-radius: var(--radius-md);
	border: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
}

.hs-file-row {
	display: flex;
	align-items: center;
	gap: 9px;
	padding: 5px 5px 5px 11px;
	border-radius: 7px;
	border: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
	min-width: 0;
}

.hs-file-row > .hs-file-name {
	flex: 1 1 auto;
	min-width: 0;
	font-family: var(--font-mono);
	font-size: 11.5px;
	color: var(--text-primary);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-file-row > .hs-file-size {
	font-family: var(--font-mono);
	font-size: 10.5px;
	color: var(--text-secondary);
	flex: 0 0 auto;
}

.hs-dropzone {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 5px;
	padding: 16px;
	border: 1px dashed var(--border-strong);
	border-radius: var(--radius-lg);
	background: var(--hs-tile-bar);
	color: var(--text-primary);
	font-size: 12px;
	text-align: center;
	cursor: pointer;
	transition: border-color var(--dur-fast) var(--ease-standard);
}

.hs-dropzone:hover,
.hs-dropzone.hs-dropzone-over {
	border-color: var(--color-accent);
}

.hs-dropzone .hs-dropzone-link {
	color: var(--accent-foreground-rest);
}

.hs-dropzone .hs-dropzone-hint {
	font-family: var(--font-mono);
	font-size: 10.5px;
	color: var(--text-secondary);
}

/* Fluent's own drop zone (FluentInputFile) is styled the same way: it is the
   element that actually receives the files. */
.hs-dropzone-host .fluent-inputfile-container {
	border: 0 !important;
	background: transparent !important;
	padding: 0 !important;
}

/* Selected-node chips in the node selection dialog. */
/* The selection is a dense list — 26px rows with hairlines between them, the remove
   cross at the end of each — not a stack of boxed tiles: with a hundred-odd signals
   selected the tiles were three screens of scrolling for one glance. */
.hs-sel-list {
	display: flex;
	flex-direction: column;
	gap: 0;
	min-width: 0;
}

.hs-sel-chip {
	display: flex;
	align-items: center;
	gap: 8px;
	min-height: 26px;
	padding: 0 2px 0 8px;
	border-radius: 5px;
	border: 0;
	border-top: 1px solid var(--hs-hairline);
	background: transparent;
	min-width: 0;
}

.hs-sel-chip:first-child {
	border-top: 0;
}

.hs-sel-chip:hover {
	background: var(--hs-tile-bar);
}

.hs-sel-chip > .hs-sel-swatch {
	width: 8px;
	height: 8px;
	border-radius: 2px;
	background: var(--color-accent);
	flex: 0 0 auto;
}

.hs-sel-chip > .hs-sel-id {
	flex: 1 1 auto;
	min-width: 0;
	font-family: var(--font-mono);
	font-size: 11.5px;
	color: var(--text-primary);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-sel-chip fluent-button {
	background: transparent !important;
	border-color: transparent !important;
	color: var(--text-secondary) !important;
	height: 22px !important;
	min-width: 22px !important;
	padding: 0 !important;
}

.hs-sel-chip fluent-button:hover {
	color: var(--text-primary) !important;
}

/* The node selection dialog. The Tree tab is the container tree beside two panes,
   the picked container's nodes over the selection; the Manual tab is a filter bar
   over one flat list. Every row in the dialog is the selection's 26px with a hairline,
   so the three lists read as one control. */
.hs-node-editor {
	display: grid;
	grid-template-columns: minmax(180px, 2fr) minmax(0, 3fr);
	height: 55vh;
	min-height: 320px;
}

.hs-node-editor-tree {
	min-height: 0;
	overflow: auto;
	padding: 2px 10px 2px 0;
	border-right: 1px solid var(--border-default);
}

.hs-node-editor-side {
	display: flex;
	flex-direction: column;
	min-height: 0;
	min-width: 0;
	padding-left: 14px;
}

.hs-node-editor-pane {
	flex: 1 1 0;
	min-height: 0;
	display: flex;
	flex-direction: column;
}

.hs-node-editor-pane + .hs-node-editor-pane {
	border-top: 1px solid var(--border-default);
	margin-top: 8px;
	padding-top: 6px;
}

.hs-node-editor-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
	min-height: 28px;
	flex: 0 0 auto;
}

/* The count or the container id after a section label: plain, not shouted. */
.hs-node-editor-where {
	margin-left: 6px;
	letter-spacing: 0;
	text-transform: none;
	font-weight: var(--fw-regular);
	color: var(--text-secondary);
}

.hs-node-editor-list {
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
	padding-right: 4px;
}

.hs-node-row {
	display: flex;
	align-items: center;
	gap: 8px;
	height: 26px;
	padding: 0 8px;
	border-radius: 5px;
	border-top: 1px solid var(--hs-hairline);
	box-sizing: border-box;
	min-width: 0;
	cursor: pointer;
}

.hs-node-row:first-child {
	border-top: 0;
}

.hs-node-row:hover {
	background: var(--hs-tile-bar);
}

.hs-node-row code {
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-family: var(--font-mono);
	font-size: 11.5px;
	color: var(--text-primary);
	background: transparent;
	padding: 0;
}

/* 4's 16px box here, not its usual 20px (hydrasafe-design.css zooms 5's 16px box to that). */
.hs-node-row fluent-checkbox {
	zoom: 1;
	border-radius: 8px;
}

.hs-node-row-icon {
	display: inline-flex;
	flex: 0 0 auto;
	color: var(--hs-ink-4);
}

.hs-node-row-count {
	flex: 0 0 auto;
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 10.5px;
	color: var(--hs-ink-3);
}

.hs-node-manual {
	display: flex;
	flex-direction: column;
	height: 55vh;
	min-height: 320px;
}

.hs-node-manual-bar {
	display: flex;
	align-items: center;
	gap: 8px;
	flex: 0 0 auto;
	padding-bottom: 8px;
}

.hs-node-manual-bar > fluent-field:has(> fluent-text-input) {
	flex: 1 1 auto;
	min-width: 0;
}

.hs-node-manual-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
	min-height: 28px;
	padding: 0 8px 4px;
	flex: 0 0 auto;
	border-bottom: 1px solid var(--border-default);
}

.hs-node-manual-list {
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
}

.hs-node-selector fluent-tablist {
	margin-bottom: 4px;
}

.hs-sel-hint {
	font-size: 11.5px;
	line-height: 1.5;
	color: var(--text-secondary);
	padding-top: 10px;
}

/* ============================================================
   13. Plot cards on the Trend page.

   Each plot is a tile: the per-plot controls are the card's title row, the
   chart is its body. Before, fourteen loose controls sat over each chart.
   ============================================================ */
.hs-plot-card {
	display: flex;
	flex-direction: column;
	flex: 1 1 0;
	min-height: 0;
	width: 100%;
	box-sizing: border-box;
	background: var(--hs-tile);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-lg);
	overflow: hidden;
}

.hs-plot-card-head {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px;
	padding: 6px 10px;
	border-bottom: 1px solid var(--border-default);
	flex: 0 0 auto;
	min-height: 40px;
	box-sizing: border-box;
}

.hs-plot-card-head :is(fluent-button, fluent-menu-button),
.hs-plot-card-head fluent-text-input::part(root) {
	height: 28px !important;
	min-height: 28px !important;
	border-radius: 7px !important;
}

/* The plot's name field. A direct child only: the scale fields inside .hs-tool-field are text fields
   too since HsNumberInput and keep that rule's 104px. */
.hs-plot-card-head > fluent-field:has(> fluent-text-input) {
	width: 120px;
}

/* The scale fields are .hs-tool-field's, sized there. */
.hs-plot-card-head .hs-tool-field {
	height: 28px;
}

.hs-plot-card > .hs-plot-box {
	flex: 1 1 0;
	min-height: 0;
}

/* The trend page body is a column of plot cards with a gutter between. */
.hs-plot-body.hs-plot-cards {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding: 14px;
}

/* The live page: one card holding the bar chart, its name as the title. */
.hs-live-card {
	display: flex;
	flex-direction: column;
	height: 100%;
	min-height: 0;
	width: 100%;
	box-sizing: border-box;
	background: var(--hs-tile);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-lg);
	overflow: hidden;
}

.hs-live-card > .hs-live-card-body {
	flex: 1 1 auto;
	min-height: 0;
	padding: 4px 8px 8px;
}

/* ============================================================
   14. The app's own dropdown (HsSelect).

   A trigger that is one of the toolbar's chips, and a list that is placed by
   hs-shell.js as a fixed layer so it escapes overflow-hidden parents. The
   selected row takes the same accent-soft fill as the active nav item.
   ============================================================ */
.hs-select {
	position: relative;
	display: inline-flex;
	flex-direction: column;
	gap: 4px;
	min-width: 0;
	vertical-align: middle;
	flex: 0 0 auto;
}

.hs-select-label {
	font-size: 12px;
	line-height: 1.2;
	color: var(--text-secondary);
	cursor: default;
}

.hs-select-trigger {
	display: flex;
	align-items: center;
	gap: 8px;
	width: 100%;
	height: 30px;
	padding: 0 9px 0 10px;
	box-sizing: border-box;
	border: 1px solid var(--hs-control-stroke);
	border-radius: 8px;
	background: var(--hs-tile);
	color: var(--text-primary);
	font: inherit;
	font-size: 12.5px;
	line-height: 1;
	text-align: left;
	cursor: pointer;
	transition: border-color var(--dur-fast) var(--ease-standard);
}

.hs-select-trigger:hover {
	border-color: var(--border-strong);
}

body.theme-dark .hs-select-trigger:hover {
	border-color: var(--hs-n-500);
}

.hs-select-trigger:focus-visible {
	outline: none;
	border-color: var(--color-accent);
	box-shadow: var(--shadow-focus-ring);
}

.hs-select-trigger:disabled {
	opacity: .5;
	cursor: default;
}

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

.hs-select-value.placeholder {
	color: var(--text-disabled);
}

.hs-select-chevron {
	flex: 0 0 auto;
	color: var(--hs-ink-3);
}

.hs-select-backdrop {
	position: fixed;
	inset: 0;
	z-index: 2990;
}

.hs-select-list {
	position: fixed;
	z-index: 3000;
	margin: 0;
	padding: 4px;
	list-style: none;
	max-height: 320px;
	overflow-y: auto;
	border-radius: 8px;
	border: 1px solid var(--border-strong);
	background: var(--hs-tile);
	box-shadow: var(--elevation-8);
	outline: none;
	animation: hs-fade-in var(--dur-fast) var(--ease-decelerate);
}

.hs-select-option {
	display: block;
	padding: 6px 10px;
	border-radius: 6px;
	font-size: 12.5px;
	color: var(--text-primary);
	cursor: pointer;
	white-space: nowrap;
}

.hs-select-option.active {
	background: var(--hs-tile-bar);
}

body.theme-dark .hs-select-option.active {
	background: var(--hs-n-250);
}

.hs-select-option.selected {
	background: var(--hs-accent-soft);
	color: var(--hs-accent-soft-ink);
	font-weight: var(--fw-semibold);
}

.hs-select-empty {
	padding: 6px 10px;
	font-size: 12px;
	color: var(--text-disabled);
}

/* Sizes by context, matching the Fluent controls next to them. */
.hs-page-actions .hs-select-trigger {
	height: 32px;
}

.hs-plot-card-head .hs-select-trigger {
	height: 28px;
	border-radius: 7px;
}

.hs-form .hs-select,
.hs-panel-editor .hs-select,
.hs-settings-body .hs-select {
	width: 100%;
}

.hs-form .hs-select-trigger,
.hs-panel-editor .hs-select-trigger {
	height: 34px;
}

/* ============================================================
   15. The container tree (HsContainerTree) and the Nodes page.
   ============================================================ */
.hs-tree {
	display: flex;
	flex-direction: column;
	gap: 1px;
	min-width: 0;
}

.hs-tree-row {
	display: flex;
	align-items: center;
	gap: 6px;
	height: 30px;
	padding-right: 8px;
	border-radius: 6px;
	font-size: 12.5px;
	color: var(--hs-ink-2);
	cursor: pointer;
	white-space: nowrap;
	min-width: 0;
	user-select: none;
	transition: background var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}

.hs-tree-row:hover {
	background: var(--hs-tile-bar);
	color: var(--text-primary);
}

body.theme-dark .hs-tree-row:hover {
	background: var(--hs-n-250);
}

.hs-tree-row.selected {
	background: var(--hs-accent-soft);
	color: var(--hs-accent-soft-ink);
	font-weight: var(--fw-semibold);
}

.hs-tree-row:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

.hs-tree-twist {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 16px;
	height: 16px;
	flex: 0 0 auto;
	border: 0;
	border-radius: 4px;
	background: transparent;
	color: var(--hs-ink-4);
	cursor: pointer;
	padding: 0;
	transition: transform var(--dur-fast) var(--ease-standard);
}

.hs-tree-twist.open {
	transform: rotate(90deg);
}

button.hs-tree-twist:hover {
	background: color-mix(in srgb, var(--text-primary) 8%, transparent);
	color: var(--text-primary);
}

.hs-tree-icon {
	flex: 0 0 auto;
	color: var(--hs-ink-4);
}

.hs-tree-row.selected .hs-tree-icon {
	color: var(--color-accent);
}

.hs-tree-name {
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}

.hs-tree-count {
	flex: 0 0 auto;
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 10.5px;
	font-weight: var(--fw-regular);
	color: var(--hs-ink-3);
}

/* Nodes page: a 46px head with the title, the counts and the filter; then the
   tree column and the grid. */
/* A plain div, not a fluent-toolbar (which is a flex host of its own), so the
   row layout has to be said here: otherwise the title, the chips and the search
   sat on one text baseline while the search box floated above it. */
.hs-nodes-head {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	column-gap: 8px;
	row-gap: 6px;
	min-height: 46px;
}

.hs-nodes-title {
	font-size: 15px;
	font-weight: var(--fw-semibold);
	color: var(--text-primary);
	white-space: nowrap;
}

.hs-count-chip {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: 2px 9px;
	border-radius: var(--radius-pill);
	border: 1px solid var(--border-default);
	background: var(--hs-tile-bar);
	color: var(--hs-ink-2);
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 11px;
	white-space: nowrap;
}

.hs-count-chip.hs-count-chip-sev {
	background: var(--sev-bg);
	color: var(--sev-ink);
	border-color: color-mix(in srgb, var(--sev) 40%, transparent);
	font-family: var(--font-sans);
	font-size: 11.5px;
}

.hs-count-chip.hs-count-chip-sev::before {
	content: "";
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--sev);
	animation: hs-pulse 1.8s var(--ease-standard) infinite;
}

.hs-count-chip.hs-count-chip-sev b {
	font-family: var(--font-mono);
	font-weight: var(--fw-semibold);
}

/* The alarmed chip is a button too: a click narrows the grid to the alarmed nodes. */
button.hs-count-chip.hs-count-chip-sev {
	appearance: none;
	cursor: pointer;
	line-height: inherit;
	transition: background var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard);
}

button.hs-count-chip.hs-count-chip-sev:hover {
	border-color: color-mix(in srgb, var(--sev) 70%, transparent);
}

.hs-count-chip.hs-count-chip-sev[aria-pressed="true"] {
	background: color-mix(in srgb, var(--sev) 24%, var(--sev-bg));
	border-color: var(--sev);
}

.hs-count-chip.hs-count-chip-sev:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

/* The source chips: one per source with its node count, pressable — a click narrows
   the grid to that source. The square is the source's swatch colour (hs-src-*), the
   same one the Source column carries. */
.hs-count-chip.hs-count-chip-src {
	appearance: none;
	cursor: pointer;
	font-family: var(--font-sans);
	font-size: 11.5px;
	line-height: inherit;
	transition: background var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}

.hs-count-chip.hs-count-chip-src::before {
	content: "";
	flex: 0 0 auto;
	width: 8px;
	height: 8px;
	border-radius: 2px;
	background: var(--src, var(--grey-400));
	box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--src, var(--grey-400)) 55%, black);
}

.hs-count-chip.hs-count-chip-src b {
	font-family: var(--font-mono);
	font-weight: var(--fw-semibold);
}

.hs-count-chip.hs-count-chip-src:hover {
	border-color: var(--border-strong);
	color: var(--text-primary);
}

.hs-count-chip.hs-count-chip-src[aria-pressed="true"] {
	background: color-mix(in srgb, var(--src, var(--grey-400)) 18%, var(--hs-tile-bar));
	border-color: color-mix(in srgb, var(--src, var(--grey-400)) 55%, transparent);
	color: var(--text-primary);
}

.hs-count-chip.hs-count-chip-src:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

.hs-nodes-body {
	flex: 1 1 auto;
	min-height: 0;
	display: grid;
	grid-template-columns: 216px minmax(0, 1fr);
}

.hs-nodes-tree {
	min-height: 0;
	overflow: auto;
	padding: 12px 8px;
	border-right: 1px solid var(--border-default);
	box-sizing: border-box;
}

.hs-nodes-tree-label {
	display: block;
	padding: 0 10px 8px;
	color: var(--hs-ink-3);
	letter-spacing: .14em;
}

.hs-nodes-grid {
	min-height: 0;
	min-width: 0;
	overflow: auto;
	background: var(--hs-pane);
}

@media (max-width: 800px) {
	.hs-nodes-body {
		grid-template-columns: 1fr;
		grid-template-rows: auto minmax(0, 1fr);
	}

	.hs-nodes-tree {
		max-height: 30vh;
		border-right: 0;
		border-bottom: 1px solid var(--border-default);
	}
}

/* Every animation in this file is decorative. One switch turns the lot off —
   the token layer already does this globally, repeated here because this file
   may be loaded on its own in tests. */
@media (prefers-reduced-motion: reduce) {
	.hs-sev-row[role="row"].hs-needs-ack > :first-child,
	.hs-row-enter[role="row"],
	.hs-row-enter[role="row"] > *,
	.hs-pill.hs-needs-ack .hs-pill-dot,
	header.header,
	.hs-skeleton::after,
	.hs-flash,
	.hs-tick-up,
	.hs-tick-down,
	.hs-edit-hint,
	.hs-palette,
	.hs-shortcuts,
	.hs-overlay,
	.hs-about-tag.warn,
	.hs-sig.alarmed .hs-sig-dot,
	.hs-wire-run,
	.hs-flow-track,
	.hs-flow-head,
	.hs-flow-wave {
		animation: none !important;
	}

	/* The pulse is motion and nothing else. Stopping its sweep would park a lit band on the start
	   of every link, so it is not drawn at all — the chevron and the columns still carry the
	   direction it was there to show. */
	.hs-flow-pulse {
		display: none;
	}

	.hs-lift:hover {
		transform: none;
	}

	.hs-gauge-needle,
	.hs-gauge-fill,
	.hs-node-tile {
		transition: none !important;
	}
}

/* ============================================================
   The node page: a live column and a reference rail.
   The left column is everything that moves with time (the value strip, the
   chart with the window controls in its own header and the window's figures
   in its footer, the events of the window); the 420px rail on the right is
   everything that does not (limits, definition, source, the container).
   ============================================================ */
.hs-node-page {
	display: flex;
	flex-direction: column;
	height: 100%;
	min-height: 0;
}

.hs-node-path {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	font-family: var(--font-mono);
	font-size: 12px;
	color: var(--text-secondary);
	white-space: nowrap;
}

.hs-node-path-sep {
	color: var(--text-disabled);
}

/* No right padding: the rail reaches to the window's edge and keeps the 12px
   itself (.hs-node-rail), so its scrollbar sits at the edge. The rail's column is
   its 420px plus those 12. */
.hs-node-columns {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 432px;
	gap: 12px;
	padding: 12px 0 12px 12px;
	flex: 1 1 auto;
	min-height: 0;
}

.hs-node-live,
.hs-node-rail {
	display: flex;
	flex-direction: column;
	gap: 12px;
	min-width: 0;
	min-height: 0;
}

/* A card of the page: the tile's surface without the tile's child rules, which
   would give the last child of every card the scroll and the flex. */
.hs-node-card {
	display: flex;
	flex-direction: column;
	flex: 0 0 auto;
	min-height: 0;
	overflow: hidden;
	box-sizing: border-box;
	background: var(--hs-tile);
	border: 1px solid var(--border-default);
	border-radius: var(--radius-lg);
	box-shadow: var(--elevation-1);
}

.hs-node-live > .hs-node-tile,
.hs-node-rail > .hs-node-tile {
	height: auto;
	flex: 0 0 auto;
}

/* The chart and the events share what the strip leaves, three to two; both
   have a floor and both scroll inside rather than pushing the page. */
.hs-node-chart {
	flex: 3 1 0;
	min-height: 220px;
}

.hs-node-live > .hs-node-events {
	flex: 2 1 0;
	min-height: 200px;
}

/* The rail's cards are as tall as what they hold - all but the container's list,
   which gives first (.hs-node-siblings) - and a window too short for them scrolls
   the rail; the live column stays where it is. The cards used to shrink to fit the
   window and scroll inside, so below about 1000px of height every card was cut
   mid-line: the Source card's link, the Definition card's "Used in" row, the write
   control squeezed to nothing under the Alias field. The rail's padding holds the
   page's 12px on the right, between the cards and the scrollbar, and 4px of the
   card shadows on the left and at the bottom, which a scrolling box would cut. A
   long list inside a card (a limit used by 128 cells) still scrolls in its own
   foot (.hs-rail-foot). */
.hs-node-rail {
	overflow-y: auto;
	margin: 0 0 -4px -4px;
	padding: 0 12px 4px 4px;
}

.hs-node-rail > .hs-node-card {
	flex: 0 0 auto;
}

/* The write control in the Definition card (NodePropertyDisplay, Part Write) brings
   the node body's tile sizing along: 100% of the card and free to shrink to 0, so
   the card gave it the space it lacked and the box shrank under the Alias row - at
   1600x900 a click on the Write field typed into the Alias. Here it is as tall as
   it is. */
.hs-node-card > .hs-node-body {
	flex: 0 0 auto;
	height: auto;
	overflow: visible;
}

.hs-node-chart-head {
	display: flex;
	align-items: center;
	gap: 8px;
	min-height: 44px;
	flex: 0 0 auto;
	padding: 4px 12px 4px 16px;
	flex-wrap: wrap;
	background: var(--hs-tile-bar);
	border-bottom: 1px solid var(--border-default);
}

.hs-node-chart-label {
	font-size: 10.5px;
	letter-spacing: .11em;
	text-transform: uppercase;
	font-weight: var(--fw-bold);
	color: var(--text-secondary);
}

.hs-node-chart-window {
	font-size: 12.5px;
	color: var(--text-primary);
}

.hs-node-chart-body {
	flex: 1 1 auto;
	min-height: 0;
	position: relative;
}

.hs-node-chart-stats {
	display: flex;
	align-items: center;
	gap: 24px;
	min-height: 56px;
	flex: 0 0 auto;
	padding: 0 16px;
	background: var(--hs-tile-bar);
	border-top: 1px solid var(--border-default);
	overflow: hidden;
}

.hs-win-stat {
	display: flex;
	flex-direction: column;
	gap: 1px;
	min-width: 124px;
}

.hs-win-stat-k {
	font-size: 10px;
	letter-spacing: .11em;
	text-transform: uppercase;
	font-weight: var(--fw-bold);
	color: var(--text-secondary);
}

.hs-win-stat-v {
	display: flex;
	align-items: baseline;
	gap: 6px;
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 15px;
	font-weight: var(--fw-medium);
	color: var(--hs-value-ink);
	white-space: nowrap;
}

.hs-win-stat-t {
	font-family: var(--font-mono);
	font-size: 10.5px;
	font-weight: var(--fw-regular);
	color: var(--text-secondary);
}

/* The container's signals take what the other cards leave and scroll inside, down
   to their title, one row and the foot (min-content: the list's own floor below);
   only a window too short for that scrolls the rail. 1600x900 fits a cell's
   signal with its limit without a scrollbar on the rail. */
.hs-node-rail > .hs-node-siblings {
	flex: 1 1 0;
	min-height: min-content;
}

.hs-node-siblings-list {
	flex: 1 1 0;
	min-height: 34px;
	overflow-y: auto;
}

/* Narrower than the two columns: they stack and the page scrolls, the rail with
   it. The rows size to their content here (stacked, they would otherwise share the
   page height and the chart column would run on into the rail), and the plot takes
   a fixed height instead of the remaining one - the plot, not the card: the card's
   head wraps to two or three lines here, and the lines came off a card of fixed
   height, which left the plot 60px. The container's list is as long as it is.
   After the rules above, which it overrides. */
@media (max-width: 1100px) {
	.hs-node-columns {
		grid-template-columns: minmax(0, 1fr);
		grid-auto-rows: max-content;
		padding: 12px;
		overflow-y: auto;
	}

	.hs-node-live,
	.hs-node-rail {
		min-height: max-content;
	}

	.hs-node-rail {
		overflow: visible;
		margin: 0;
		padding: 0;
	}

	.hs-node-chart {
		flex: 0 0 auto;
	}

	.hs-node-chart-body {
		flex: 0 0 auto;
		height: 300px;
	}

	.hs-node-rail > .hs-node-siblings,
	.hs-node-siblings-list {
		flex: 0 0 auto;
	}
}

/* ---- the strip: value, movement and freshness in one row ---- */
.hs-node-strip {
	display: flex;
	align-items: center;
	gap: 24px;
	padding: 14px 20px;
	flex-wrap: wrap;
}

.hs-node-strip-main {
	display: flex;
	flex-direction: column;
	gap: 5px;
}

.hs-node-strip-number {
	font-size: 40px;
}

.hs-node-strip-pulse {
	display: flex;
	flex-direction: column;
	gap: 6px;
	padding-left: 24px;
	border-left: 1px solid var(--hs-hairline);
}

.hs-node-strip-fresh {
	display: flex;
	align-items: center;
	gap: 7px;
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 11.5px;
	color: var(--text-secondary);
	white-space: nowrap;
}

.hs-node-strip-sep {
	color: var(--text-disabled);
}

.hs-node-strip-dot {
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--hs-success);
	animation: hs-pulse 2.4s var(--ease-standard) infinite;
}

.hs-node-strip-dot.stale {
	background: var(--hs-warning);
	animation: none;
}

.hs-node-strip-dot.dead {
	background: var(--hs-error);
	animation: none;
}

.hs-node-strip-side {
	display: flex;
	align-items: center;
	gap: 14px;
	margin-left: auto;
}

/* ---- limits ---- */
.hs-limit-bar {
	display: flex;
	flex-direction: column;
	gap: 7px;
	padding: 14px 16px 12px;
}

.hs-limit-scale {
	display: flex;
	align-items: center;
	gap: 10px;
}

.hs-limit-end {
	font-size: 11px;
	color: var(--text-secondary);
	min-width: 42px;
}

.hs-limit-end:first-child {
	text-align: right;
}

.hs-limit-track {
	position: relative;
	flex: 1 1 auto;
	height: 8px;
	border-radius: 4px;
	background: var(--hs-hairline);
}

.hs-limit-fill {
	position: absolute;
	left: 0;
	top: 0;
	height: 8px;
	border-radius: 4px;
	background: linear-gradient(90deg, color-mix(in srgb, var(--color-accent) 25%, transparent), var(--color-accent));
}

.hs-limit-mark {
	position: absolute;
	top: -3px;
	width: 14px;
	height: 14px;
	border-radius: 50%;
	background: var(--hs-value-ink);
	box-shadow: 0 0 0 2px var(--hs-tile);
	transform: translateX(-7px);
}

.hs-limit-headroom {
	font-size: 11.5px;
	color: var(--text-secondary);
}

.hs-limit-headroom.past {
	color: var(--hs-error);
}

/* What the limit is on the first line - the priority and the alarm's name - and
   on the second its threshold, with what the alarm is doing beside it. The state
   used to stand beside the whole identity, and in the 420px rail "Gone
   Unacknowledged" with its acknowledge button took half the row: the name was
   left 50px ("Voltag…") and the threshold ran on under the pill. The name has its
   line to itself now, the names start at one x down the card, and the pills line
   up on the right. An alarm without a threshold (a cell flag) keeps its state on
   the first line. The priority column grows past its floor only for a badge wider
   than "Kritisch", so it can never clip one. */
.hs-limit-row {
	display: grid;
	grid-template-columns: minmax(76px, auto) minmax(0, 1fr) auto;
	align-items: center;
	gap: 4px 9px;
	min-height: 42px;
	padding: 7px 16px;
	border-top: 1px solid var(--hs-hairline);
	font-size: 12.5px;
}

/* Its parts are cells of the row. */
.hs-limit-id {
	display: contents;
}

.hs-limit-id > .hs-prio {
	grid-column: 1;
	grid-row: 1;
	text-align: center;
}

.hs-limit-state {
	grid-column: 3;
	grid-row: 2;
	justify-self: end;
	display: flex;
	align-items: center;
	gap: 9px;
}

.hs-limit-name {
	grid-column: 2 / -1;
	grid-row: 1;
	font-weight: var(--fw-semibold);
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-limit-row:not(:has(.hs-limit-trip)) .hs-limit-name {
	grid-column: 2;
}

.hs-limit-row:not(:has(.hs-limit-trip)) .hs-limit-state {
	grid-row: 1;
}

/* From under the badge: beside "Gone Unacknowledged" and its button the name's
   column is 50px, which cut the threshold to "above …". */
.hs-limit-trip {
	grid-column: 1 / 3;
	grid-row: 2;
	justify-self: start;
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: 12px;
	color: var(--text-secondary);
	white-space: nowrap;
}

.hs-rail-empty {
	padding: 14px 16px;
	font-size: 12.5px;
	color: var(--text-secondary);
}

/* ---- definition and source: the property list at the rail's density ---- */
.hs-rail-list {
	display: grid;
	grid-template-columns: 128px minmax(0, 1fr);
	margin: 0;
}

.hs-rail-list > dt,
.hs-rail-list > dd {
	display: flex;
	align-items: center;
	gap: 7px;
	min-height: 26px;
	padding: 2px 16px;
	margin: 0;
	box-sizing: border-box;
	border-top: 1px solid var(--hs-hairline);
	min-width: 0;
}

.hs-rail-list > dt {
	color: var(--text-secondary);
	font-size: 11.5px;
}

.hs-rail-list > dd {
	font-size: 12.5px;
	color: var(--text-primary);
	flex-wrap: wrap;
}

.hs-rail-list > dd .hs-mono {
	font-size: 12px;
}

.hs-rail-list .hs-note {
	color: var(--text-secondary);
	font-size: 11px;
}

.hs-rail-list > dt:first-of-type,
.hs-rail-list > dt:first-of-type + dd {
	border-top: 0;
}

.hs-rail-foot {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 7px;
	padding: 8px 16px;
	border-top: 1px solid var(--hs-hairline);
	font-size: 11.5px;
	color: var(--text-secondary);
	margin-top: auto;
	flex: 0 0 auto;
	/* A limit used by every cell lists every cell: the list scrolls, the card keeps
	   its share of the rail. */
	max-height: 120px;
	overflow-y: auto;
	align-content: flex-start;
}

.hs-rail-foot a {
	font-family: var(--font-mono);
	font-size: 11.5px;
	color: var(--text-link);
}

/* A provider's last line is a sentence, not a label: on one clipped line ("Injecting
   outlier: 0,102104…") the numbers it exists to carry were always the part cut off.
   Two wrapped lines fit the whole of most of them; longer ones still end in the
   ellipsis and keep the full text in the title. They come after the link, which
   shares the line of the time (.hs-rail-foot-end): a line less on the rail. */
.hs-rail-log {
	order: 1;
	min-width: 0;
	flex: 1 1 100%;
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	line-clamp: 2;
	overflow: hidden;
	overflow-wrap: anywhere;
}

.hs-rail-foot-end {
	margin-left: auto;
}

/* ---- the container's other signals ---- */
.hs-sib-row {
	display: grid;
	grid-template-columns: 14px minmax(0, 1fr) auto 14px;
	align-items: center;
	gap: 10px;
	min-height: 34px;
	padding: 0 14px;
	border-top: 1px solid var(--hs-hairline);
	font-size: 12.5px;
}

.hs-sib-row:first-child {
	border-top: 0;
}

.hs-sib-row.current {
	background: var(--hs-accent-soft);
	color: var(--hs-accent-soft-ink);
	font-weight: var(--fw-semibold);
}

.hs-sib-row a {
	color: var(--text-link);
}

/* ============================================================
   Node alias: the editor row, the hero's alias line and the palette's id detail.
   ============================================================ */

/* NodeAliasEditor: the field, apply and remove in one row, inside a property
   list's dd, so it takes the row's width. */
.hs-alias-row {
	display: flex;
	align-items: center;
	gap: var(--space-2);
	width: 100%;
	min-width: 0;
	padding: 4px 0;
}

.hs-alias-row .hs-alias-field {
	flex: 1 1 auto;
	min-width: 0;
}

/* The hero names an aliased node by its alias and keeps the id beneath, mono. */
.hs-node-hero-alias {
	font-size: var(--text-body2);
	font-weight: var(--fw-medium);
	color: var(--text-primary);
}

/* A palette node row found under an alias shows the id beside it, so a hit
   reads the same whichever name matched. */
.hs-palette-item .hs-palette-detail {
	font-family: var(--font-mono);
	font-size: var(--text-caption);
	color: var(--text-secondary);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	flex: 0 1 auto;
}

/* ============================================================
   The panel item dialog's settings column — one vocabulary for all five
   editors (live, trend, nodes, alarms, events).

   It started in PanelFilterEditor, which the alarm and event tiles share:
   labelled blocks, chips, the picked nodes as a list. The other three were raw
   Fluent stacks — a bare dropdown for live, a row of sliders and dividers for
   trend, a full data grid for nodes — so the same dialog spoke three languages
   depending on which card was selected. Everything below is that one language:
   a block is an overline label, its controls at full width, and an optional
   hint underneath.
   ============================================================ */
.hs-panel-form {
	display: flex;
	flex-direction: column;
	gap: 14px;
	min-width: 0;
}

.hs-panel-form-block {
	display: flex;
	flex-direction: column;
	gap: 6px;
	min-width: 0;
}

.hs-panel-form-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 8px;
}

.hs-panel-form-list {
	max-height: 140px;
	overflow-y: auto;
	border: 1px solid var(--hs-hairline);
	border-radius: var(--radius-md);
	background: var(--hs-tile);
}

/* The acknowledge switch with its own label: Fluent's label lives in the switch's
   shadow root and takes a token that is white on this surface. */
.hs-panel-form-switch {
	display: flex;
	align-items: center;
	gap: 10px;
	font-size: 12.5px;
	color: var(--text-primary);
	cursor: pointer;
}

/* A read-only box inside a block: what a picked configuration contains, or the
   axes of its plots. Same frame as the node list, with a header strip that names
   what is in it and states the one figure that matters. */
.hs-panel-form-box {
	border: 1px solid var(--hs-hairline);
	border-radius: var(--radius-md);
	background: var(--hs-tile);
	overflow: hidden;
	min-width: 0;
}

.hs-panel-form-box-head {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 7px 8px;
	background: var(--hs-tile-bar);
	border-bottom: 1px solid var(--border-default);
}

.hs-panel-form-box-head > .hs-section-label {
	flex: 1 1 auto;
	min-width: 0;
}

.hs-panel-form-box-figure {
	flex: 0 0 auto;
	font-size: 11px;
	font-weight: var(--fw-semibold);
	color: var(--hs-accent-ink);
}

/* The box scrolls rather than growing past the settings column. */
.hs-panel-form-box .hs-sel-list {
	max-height: 132px;
	overflow-y: auto;
}

/* A signal's unit, at the end of its row in the box. */
.hs-sel-chip > .hs-sel-unit {
	flex: 0 0 auto;
	font-family: var(--font-mono);
	font-size: 11px;
	color: var(--text-secondary);
}

/* ---- the trend tile's Y axes ----
   One row per plot: its name, the two bounds, and the box that clears them.
   Aligned in a grid, because a plot's min is only readable against the min of
   the plot above it. */
.hs-axis-grid {
	display: grid;
	grid-template-columns: minmax(0, 1fr) 74px 74px 34px;
	gap: 8px;
	align-items: center;
	padding: 6px 8px;
}

.hs-axis-grid + .hs-axis-grid {
	border-top: 1px solid var(--hs-hairline);
	padding-block: 7px;
}

.hs-axis-head {
	background: var(--hs-tile-bar);
	border-bottom: 1px solid var(--border-default);
}

.hs-axis-head > span {
	font-size: var(--text-overline);
	font-weight: var(--fw-bold);
	letter-spacing: var(--ls-overline);
	text-transform: uppercase;
	color: var(--hs-ink-3);
}

.hs-axis-head > span:last-child {
	text-align: center;
}

.hs-axis-name {
	font-size: var(--text-caption);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.hs-axis-grid fluent-text-input::part(root) {
	height: 26px;
	border-radius: var(--radius-md);
}

.hs-axis-grid fluent-text-input::part(control) {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
	font-size: var(--text-caption);
}

.hs-axis-auto {
	display: flex;
	justify-content: center;
}

/* ---- the nodes tile's renderer choice ----
   Two cards rather than a dropdown: the renderers differ in what they draw, and
   a name alone ("Gauge", "Bar") did not say it. Same card as the type picker,
   one step smaller. */
.hs-renderer-grid {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 8px;
}

.hs-renderer-card {
	display: flex;
	flex-direction: column;
	gap: 4px;
	min-width: 0;
	padding: 10px 11px;
	border: 1px solid var(--border-default);
	border-radius: var(--radius-lg);
	background: var(--hs-tile);
	color: var(--text-primary);
	font: inherit;
	text-align: left;
	cursor: pointer;
	transition: border-color var(--dur-fast) var(--ease-standard),
				background var(--dur-fast) var(--ease-standard);
}

.hs-renderer-card:hover {
	border-color: var(--color-accent);
}

.hs-renderer-card[aria-pressed="true"] {
	border-color: var(--color-accent);
	background: var(--hs-accent-soft);
}

.hs-renderer-card:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

.hs-renderer-name {
	font-size: 12.5px;
	font-weight: var(--fw-semibold);
}

.hs-renderer-card[aria-pressed="true"] .hs-renderer-name {
	color: var(--hs-accent-soft-ink);
}

.hs-renderer-desc {
	font-size: 11px;
	line-height: 1.45;
	color: var(--text-secondary);
}

/* The gauge's end stops: the unit the numbers are in, then the numbers. */
.hs-scale-grid {
	display: grid;
	grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr);
	gap: 8px;
	align-items: end;
}

/* A field's own small caption inside a grid cell — below the block's overline
   label, which names the group rather than the cell. */
.hs-panel-form .hs-subfield {
	display: flex;
	flex-direction: column;
	gap: 4px;
	min-width: 0;
}

.hs-subfield-label {
	font-size: 11px;
	color: var(--text-secondary);
}

/* Fluent's own label would sit above the control and break the block rhythm, so
   every field in here is labelled by the block. */
.hs-panel-form fluent-field:has(> fluent-text-input) {
	width: 100%;
}

/* ---- View scope rail: the Plant / Mine switch and the tabs of the chosen scope --------------
   Above the toolbar of the Live, Trend and Panel pages (ViewScopeRail). The switch is the
   segmented control the toolbars already use, with a glyph per scope: a filled square for the
   plant, a ring for the user. The tabs are quiet buttons of the switch's own height, the chosen
   one filled the way a pressed segment is; where a view comes from and how big it is sits in its
   tooltip, so the rail stays one line of chrome. */
.hs-viewrail {
	display: flex;
	align-items: center;
	gap: 14px;
	padding: 6px 16px;
	border-bottom: 1px solid var(--border-default);
	/* The toolbar's own surface, so rail and toolbar read as one block of chrome. */
	background: var(--hs-chrome);
	flex: 0 0 auto;
	min-width: 0;
}

body.theme-dark .hs-viewrail {
	border-bottom-color: var(--hs-n-300);
}

.hs-viewrail .hs-scope,
.hs-viewrail-add {
	flex: 0 0 auto;
}

.hs-scope > button {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	height: 24px;
	padding: 0 11px;
	font-family: inherit;
	font-size: 12px;
}

.hs-scope-glyph {
	width: 8px;
	height: 8px;
	border-radius: 2px;
	background: var(--color-accent);
	flex: 0 0 auto;
}

.hs-scope-glyph.mine {
	border-radius: 999px;
	background: transparent;
	border: 1.5px solid currentColor;
	box-sizing: border-box;
}

.hs-scope-count {
	font-family: var(--font-mono);
	font-size: 11px;
	font-variant-numeric: tabular-nums;
}

/* The strip scrolls sideways when the tabs outgrow it and never vertically. */
.hs-viewtabs {
	display: flex;
	align-items: center;
	gap: 2px;
	flex: 1 1 auto;
	min-width: 0;
	overflow-x: auto;
	overflow-y: hidden;
	scrollbar-width: thin;
}

.hs-viewtab {
	display: inline-flex;
	align-items: center;
	flex: 0 0 auto;
	height: 30px;
	padding: 0 11px;
	border: 0;
	border-radius: 6px;
	background: transparent;
	color: var(--text-secondary);
	font-family: inherit;
	font-size: 12.5px;
	font-weight: var(--fw-medium);
	line-height: 1;
	cursor: pointer;
	white-space: nowrap;
	transition: background var(--dur-fast) var(--ease-standard), color var(--dur-fast) var(--ease-standard);
}

.hs-viewtab:hover {
	color: var(--text-primary);
	background: var(--hs-tile-bar);
}

.hs-viewtab:focus-visible {
	outline: none;
	box-shadow: var(--shadow-focus-ring);
}

.hs-viewtab[aria-selected="true"] {
	background: var(--hs-accent-soft);
	color: var(--hs-accent-soft-ink);
	font-weight: var(--fw-semibold);
}

/* A setting on the Views tab: the switch, its label, and one line on what it does. */
.hs-setting-card {
	max-width: 76ch;
	padding: 14px 16px;
	border: 1px solid var(--border-default);
	border-radius: 9px;
	background: var(--hs-tile);
}

.hs-setting-row {
	display: flex;
	align-items: flex-start;
	gap: 14px;
}

.hs-setting-title {
	font-size: 13.5px;
	font-weight: var(--fw-semibold);
	color: var(--text-primary);
}

.hs-setting-hint {
	margin-top: 4px;
	font-size: 12.5px;
	line-height: 1.5;
	color: var(--text-secondary);
}
