/* =============================================================================
   YKSA UI kit.

   Requires Bootstrap >= 5.3: the state pills below are built on the
   --bs-*-bg-subtle / --bs-*-text-emphasis / --bs-*-border-subtle families, and
   text-body-secondary is a 5.3 utility. On 5.2.3 every rule here still parses
   and the page renders wrong -- muted text is not muted, tinted backgrounds are
   transparent. The bundled bootstrap.min.css is the version this is written for;
   do not point a service at its own older copy.

   Two rules govern the whole kit:
   1. Typography carries meaning through weight, colour and size only. No
      small-caps, no thin/semibold, no letter-spacing, no .display-*. Weights
      are 400 and 700.
   2. Colour is never the only signal. Every state renders an icon and a word
      alongside its hue.

   Anything Bootstrap already provides is used as-is. The classes below exist
   only where it has no equivalent.
   ============================================================================= */

:root {
	--navbar-height: 44px;

	/* One layer scale for the whole estate, because the alternative is each
	   surface picking a number and a sticky bar eventually landing on top of an
	   open menu -- which is exactly what ops's console header did to the navbar's
	   timezone dropdown. Bootstrap's own scale starts at modal-backdrop (1040),
	   so everything here stays below that.

	     1020  in-page sticky bars (ops's console header)
	     1030  page chrome: navbar, action bar, cookie banner
	     1035  anything transient the user just opened -- above all of it  */
	--yksa-z-sticky: 1020;
	--yksa-z-chrome: 1030;
	--yksa-z-floating: 1035;

	/* One hover treatment for the whole estate: the surface's ground goes one
	   step darker and nothing else moves. Every hoverable thing -- table rows,
	   list rows, step rows, catalog cards, disclosures -- uses this token, so a
	   page cannot end up with three ways of saying "you are pointing at this".
	   Borders, shadows and colour shifts are all reserved for *state*, which is
	   a property of the record; hover is a property of the pointer. */
	--yksa-hover-bg: var(--bs-tertiary-bg);
}

/* -----------------------------------------------------------------------------
   Density scope

   The estate has one type scale and one alternative to it, not one per service.
   A console -- a page read at arm's length, all day, with more rows on screen
   than prose -- declares the dense scope on <main>:

     <main class="yksa-dense flex-grow-1">

   Never on <body>: the navbar and the footer keep the default scale so they
   stay pixel-identical across services, which is the whole reason they are
   shared. `.ops` is the same scope under its historical name -- ops's <main>
   carries `ops`, and this rule is what makes it dense; ops-kit.css keeps only
   what is genuinely ops's (its step rows, its console header, its kiosk board).

   It changes size and geometry, nothing else. Colour, weight and spacing
   utilities behave exactly as they do outside it, so a partial written for one
   scale renders correctly in the other -- which is what lets tdas, odms and ops
   share `yksa/ui/*` without a single conditional.
   -------------------------------------------------------------------------- */
.yksa-dense,
.ops {
	--bs-body-font-size: .875rem;
	--bs-body-line-height: 1.4;
	--bs-border-radius: 0;
	--bs-border-radius-sm: 0;
	--bs-border-radius-lg: 0;
	--bs-border-radius-xl: 0;
	--bs-border-radius-xxl: 0;
	--bs-border-radius-2xl: 0;
	--bs-border-radius-pill: 0;
}

/* Card padding is the scope's decision, not the page's: a header re-padded per
   page is a card a few pixels shorter than the one beside it. Enforced by test
   -- no spacing utility is allowed on a `.card-header` or a `.card-body`.

   Square, at Bootstrap's `p-3`. It was `.75rem 1rem`, which read as a bottom
   gap narrower than the sides, and pages "fixed" that by adding `py-2` to some
   bodies and `mt-2` to the first child of others -- so no two cards on a page
   had the same internal spacing, and the top and bottom of a single card did
   not match each other. One value, no utilities, nothing to rediscover. */
.yksa-dense .card-body,
.ops .card-body { padding: 1rem; }

/* The one card body that is not a card body: a KPI tile is a caption and a
   figure, and at `p-3` a strip of them is a band of mostly empty box. It was
   `py-2 px-3` spelled in the template, which the no-utilities-on-a-card-body
   rule above now forbids -- and rightly, because the tile's padding is the
   kit's decision and not the page's. Same value, named, so every tile in the
   estate (including the hand-built ones whose figure is markup) gets it by
   asking for the class instead of by remembering two utilities.

   Qualified by the scope so it out-specifies the rule it overrides; a bare
   `.yksa-kpi` loses to `.yksa-dense .card-body` and would do nothing. */
.yksa-dense .card-body.yksa-kpi,
.ops .card-body.yksa-kpi { padding: .5rem .75rem; }

/* A card body holding a plot. The canvas brings its own axis margins, so the
   square `p-3` read as a frame around a frame. */
.yksa-dense .card-body.yksa-plot-body,
.ops .card-body.yksa-plot-body { padding: .5rem 1rem; }

.yksa-dense .card-header,
.ops .card-header { padding: .4rem 1rem; }

/* A label belongs to the control under it. Bootstrap's `.375rem` is set against
   the default 16px body; at the scope's 14px it reads as a gap rather than as
   attachment, and a column of fields comes out taller than the card it is in. */
.yksa-dense .form-label,
.ops .form-label { margin-bottom: .25rem; }

/* Reboot's `1rem` is against a 16px body too, and a rule drawn wider apart than
   the card's own padding reads as a gap in the card rather than a division of
   it. Pages were writing `my-2` over it by hand. */
.yksa-dense hr,
.ops hr { margin-block: .75rem; }

/* A header this tight fits a small control and nothing taller, which is what
   `.btn-badge` below is for. */
.yksa-dense .form-control-sm,
.ops .form-control-sm { padding: .2rem .5rem; }

.yksa-dense .form-select-sm,
.ops .form-select-sm { padding: .2rem 1.75rem .2rem .5rem; }

/* Must match Bootstrap's own .form-control-sm min-height calc, or selects
   misalign with inputs. */
.yksa-dense .form-select-sm,
.ops .form-select-sm { min-height: calc(1.5em + .5rem + calc(var(--bs-border-width) * 2)); }

/* -----------------------------------------------------------------------------
   Controls that live in a card header

   A card header is a title strip, and a control put in one must not make it
   taller than the header beside it that holds only a word. `.btn-sm` still
   carries `.25rem` of vertical padding, which is enough to grow the strip; the
   fix ops's procedure steps found -- `py-0` on a `.btn-sm` -- is named here so
   it stops being a spacing utility that every page rediscovers.

     <button class="btn btn-sm btn-outline-secondary btn-badge">Hex</button>

   Use it for anything sitting beside a card title: a window switcher, a format
   toggle, a reset. A control in a card *body* is an ordinary `.btn-sm`.

   The second selector is not decoration. Bootstrap sets the small padding as
   `.btn-group-sm > .btn, .btn-sm { --bs-btn-padding-y: .25rem }`, and the first
   half of that is (0,2,0) -- so a bare `.btn-badge` at (0,1,0) loses inside a
   `.btn-group-sm`, which is where every one of these switchers actually lives.
   The class did nothing on any of them. Matching Bootstrap's own shape ties the
   specificity and the kit's later position in the cascade settles it; `ops`
   reached the same result with the `py-0` utility, which wins by `!important`
   instead.
   -------------------------------------------------------------------------- */
.btn-badge,
.btn-group-sm > .btn-badge {
	--bs-btn-padding-y: 0;
}

/* A card header is one line. It used to wrap, which made a card a row taller
   than the one beside it as soon as its title or its controls grew; the title
   is truncated instead and the controls beside it keep their size. */
.card-header.d-flex > :first-child {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.card-header.d-flex > :first-child ~ * {
	flex-shrink: 0;
}

/* A wrapping control group such as the filter bar would overflow a phone-width
   card at full width, so it shrinks and wraps instead. */
.card-header.d-flex > :first-child ~ .flex-wrap {
	flex-shrink: 1;
	min-width: 0;
	justify-content: flex-end;
}

/* Bootstrap's own hoverable components, pointed at the same token.

   `border-collapse` is here rather than beside the row-separator rules below
   because a second `.table` block is a second place to look. The reasoning is
   with those rules; the short version is that collapsed borders belong to the
   table and are painted before positioned descendants, so a positioned row --
   `.yksa-row-link` -- painted over its own separator. */
.table {
	--bs-table-hover-bg: var(--yksa-hover-bg);
	border-collapse: separate;
	border-spacing: 0;
}

.list-group {
	--bs-list-group-action-hover-bg: var(--yksa-hover-bg);
}

/* Bootstrap declares --bs-dropdown-zindex on .dropdown-menu, not on :root, so it
   has to be overridden here -- a value set on :root is shadowed and does nothing.
   At Bootstrap's default of 1000 an open menu loses to every bar above. */
.dropdown-menu {
	--bs-dropdown-zindex: var(--yksa-z-floating);
}

.custom-navbar {
	height: var(--navbar-height);
	padding-top: 0;
	padding-bottom: 0;
	z-index: var(--yksa-z-chrome);
	border: none;
	background-color: var(--bs-body-bg) !important;
}

.navbar {
	z-index: var(--yksa-z-chrome);
}

.navbar-brand {
	margin-right: 1em;
}

/* Nav links fill the bar's height and butt against one another, so the active
   item reads as a selected tab rather than a highlighted word. */
.nav-full-height {
	display: flex;
	align-items: center;
	height: 100%;
}

/* The top padding is deliberately larger than the bottom: it is what sits the link
   text on the same optical line as the wordmark in the brand block. Do not
   symmetrise it -- the alignment is the reason it is here. */
.nav-full-height.nav-link {
	color: inherit;
	padding-left: 1rem !important;
	padding-right: 1rem !important;
	padding-top: 12px;
	background-color: var(--bs-body-bg);
	margin-right: -1px;
}

/* The locale item's only content is a flag image. Because the link is a flex
   container, an image child gives it no text line box to take its height from, so
   the item came out ~9px shorter than every neighbour. Reserve the height one line
   of body text would have occupied and letterbox the flag inside it: same box as
   its neighbours, flag unscaled and undistorted.

   Scoped to .nav-link so the brand block's wordmark is untouched. */
.nav-full-height.nav-link > img {
	min-height: calc(1em * var(--bs-body-line-height));
	object-fit: contain;
}

/* An open dropdown is as much "the item you are on" as an active one. Without
   this the toggle dropped back to the resting colours the moment its own menu
   appeared, which reads as the click having missed. */
.nav-full-height.nav-link:hover,
.nav-full-height.nav-link.active,
.nav-full-height.nav-link.show {
	color: #fff !important;
	background-color: var(--bs-primary);
}

/* Muted text inside a highlighted nav item takes the item's colour.

   Bootstrap's text utilities set `color` with `!important`, so a muted span --
   a countdown, a hint -- kept its resting grey when the item turned solid primary
   underneath it: grey on blue, the one combination the theme has no contrast for.
   `inherit` needs `!important` here for the same reason the utility does.

   Deliberately only the muted utility, not every descendant. A badge and a state
   pill are separate surfaces carrying their own background, and flattening them
   to the parent's colour would erase what they are for. */
.nav-full-height.nav-link:hover .text-body-secondary,
.nav-full-height.nav-link.active .text-body-secondary,
.nav-full-height.nav-link.show .text-body-secondary,
/* Same problem, same fix, one row down: a dropdown item with a muted hint or
   sub-line under its label -- the outcome menu on the pass console, the duty
   menu in the header -- highlights on hover, focus and keyboard selection, and
   the muted line stayed grey against it. Any menu that explains its options has
   this shape, so the rule belongs on `.dropdown-item` rather than on each one. */
.dropdown-item:hover .text-body-secondary,
.dropdown-item:focus .text-body-secondary,
.dropdown-item:active .text-body-secondary,
.dropdown-item.active .text-body-secondary {
	color: inherit !important;
}

/* `inline-block`, not `inline-flex`, and this is the whole of the table-alignment
   fix. An inline-flex box's baseline is its last flex item's baseline, and its
   padding and border hang *below* that -- so in a table cell the pill sits low,
   and no amount of `vertical-align` corrects it without also fighting the line
   height it inherits. An inline-block shares the row's baseline with the text
   beside it and straddles it symmetrically, which is why Bootstrap's own `.badge`
   has always looked right in a table. The icon is centred by bootstrap-icons'
   `vertical-align: -.125em`, the same way it is in running text.

   Nothing here sets `line-height`: the pill takes the surface's, so it matches
   the row it sits in rather than being a second type size. */
.yksa-state {
	display: inline-block;
	font-size: .75rem;
	font-weight: 700;
	padding: .05rem .4rem;
	white-space: nowrap;
	border: 1px solid transparent;
}

.yksa-state-ok,
.yksa-state-complete {
	color: var(--bs-success-text-emphasis);
	background: var(--bs-success-bg-subtle);
	border-color: var(--bs-success-border-subtle);
}

.yksa-state-warning,
.yksa-state-forced {
	color: var(--bs-warning-text-emphasis);
	background: var(--bs-warning-bg-subtle);
	border-color: var(--bs-warning-border-subtle);
}

.yksa-state-failed {
	color: var(--bs-danger-text-emphasis);
	background: var(--bs-danger-bg-subtle);
	border-color: var(--bs-danger-border-subtle);
}

.yksa-state-running,
.yksa-state-in-progress {
	color: var(--bs-primary-text-emphasis);
	background: var(--bs-primary-bg-subtle);
	border-color: var(--bs-primary-border-subtle);
}

.yksa-state-skipped,
.yksa-state-unknown,
.yksa-state-not-started {
	color: var(--bs-secondary-color);
	background: var(--bs-secondary-bg-subtle);
	border-color: var(--bs-border-color);
}

/* Neutral metadata chip. Not a coloured badge, because it carries no status.
   Same box model as .yksa-state, for the same reason -- a slot that renders a
   chip when it has a value and a state pill when it does not (the catalogue's
   "last seen" cell) must not change height between the two. */
.yksa-chip {
	display: inline-block;
	font-size: .75rem;
	padding: .05rem .35rem;
	white-space: nowrap;
	color: var(--bs-secondary-color);
	background: var(--bs-tertiary-bg);
	border: 1px solid var(--bs-border-color);
}

/* A flex run inside a chip wraps on its own, whatever the chip's white-space
   says, and a two-line chip is twice the height of the one beside it. */
.yksa-chip > .yksa-meta {
	flex-wrap: nowrap;
}

/* A borderless icon button for dense rows: a table row's remove control, an
   overflow `...` toggle, a field's clear. Bordered buttons in those places draw a
   box around every row and the boxes become the pattern the eye follows.

   This exists because the alternative was being spelled out by hand, differently,
   in five templates -- `btn btn-sm btn-link p-0 text-body-secondary`, sometimes
   with `border-0`, sometimes with `px-1`, once with an `fs-5` on the glyph. It is
   a button, not a link: `.btn-link` is reserved for navigation, and a control
   styled as a link but wired to a POST invites a middle-click that silently
   does nothing. */
.yksa-btn-icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 1.75rem;
	min-height: 1.75rem;
	padding: 0 .25rem;
	color: var(--bs-secondary-color);
	background: none;
	border: none;
	line-height: 1;
}

.yksa-btn-icon:hover,
.yksa-btn-icon:focus-visible {
	color: var(--bs-body-color);
	background: var(--yksa-hover-bg);
}

.yksa-btn-icon.is-danger:hover,
.yksa-btn-icon.is-danger:focus-visible {
	color: var(--bs-danger);
}

.yksa-label {
	font-size: .75rem;
	font-weight: 700;
	color: var(--bs-secondary-color);
}

.yksa-hint {
	font-size: .75rem;
	font-weight: 400;
	color: var(--bs-secondary-color);
}

/* Separator between items in a run of inline metadata: a hairline rule, not a
   character. A middot at .75rem is hard to tell from a decimal point or a full
   stop, it is announced by screen readers, and it lands in the clipboard when the
   line is copied. This is the inline-text counterpart to Bootstrap's `.vr`, which
   is built for flex rows and has no margins of its own. */
.yksa-sep {
	display: inline-block;
	width: 1px;
	height: 0.9em;
	margin-inline: 0.5rem;
	vertical-align: -0.1em;
	background-color: var(--bs-border-color);
}

/* Inside an already-tight run (a card header, a table cell), the default margin
   is too generous. */
.yksa-sep-tight {
	margin-inline: 0.3rem;
}

/* A run of metadata separated by rules. Use this rather than dropping bare
   `.yksa-sep`s between text nodes.

   The margins above are symmetric, but the *source* around them is not: a
   template that puts the separator at the start of a line contributes a collapsed
   space on its left and none on its right, so the rule sits visibly off-centre
   between the two items it divides. Nobody sees that while writing the template
   and everybody sees it on the page.

   A flex container fixes it at the root: whitespace-only text nodes never become
   flex items, so indentation stops existing, and the gap is the only spacing.
   Each item goes in its own element -- an unwrapped text node *would* become an
   anonymous flex item, and then the whole run is one item and the gap does
   nothing. That is the reason for the spans. */
.yksa-meta {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	/* A row gap, small: the run is one line most of the time, and when it wraps
	   -- a catalogue row at phone width, a long list of programs -- a zero gap
	   put the two lines' glyphs against each other. */
	gap: .15rem .5rem;
}

.yksa-meta-tight {
	gap: 0 .3rem;
}

/* Inside the run the gap does the spacing; the rule's own margins would double it. */
.yksa-meta > .yksa-sep {
	margin-inline: 0;
}

/* Tabular numerals in the body face. Monospace is reserved for verbatim machine
   text -- hex dumps, TLE lines, command mnemonics. */
.yksa-num {
	font-variant-numeric: tabular-nums;
}

.yksa-readout {
	font-variant-numeric: tabular-nums;
	font-weight: 700;
	line-height: 1;
}

.yksa-utc {
	font-variant-numeric: tabular-nums;
}

.yksa-utc .yksa-local {
	color: var(--bs-secondary-color);
	font-size: .8em;
}

/* A container that supplies its own padding must not also inherit the trailing
   margin of whatever it ends with. `!important` because the common last child is
   a Bootstrap field group, `<div class="mb-3">`, whose utility is itself
   `!important` -- without it the card shows padding plus a 1rem margin and reads
   as bottom-heavy. Direct children only: deeper boxes own their own spacing. */
.card-body > :last-child,
.card-body > .tab-content > .tab-pane > :last-child,
.alert > :last-child,
.list-group-item > :last-child,
.modal-body > :last-child,
.offcanvas-body > :last-child {
	margin-bottom: 0 !important;
}

/* The same at the top, for a first child a utility pushed down. `.row` is
   exempt: its negative top margin is what cancels the gutter padding on its own
   columns, and zeroing it indents the grid instead of the reverse. */
.card-body > :first-child:not(.row),
.card-body > .tab-content > .tab-pane > :first-child:not(.row),
.alert > :first-child:not(.row),
.list-group-item > :first-child:not(.row),
.modal-body > :first-child:not(.row),
.offcanvas-body > :first-child:not(.row) {
	margin-top: 0 !important;
}

/* A definition list ends with a `dd`, which carries a bottom margin of its own.
   `mb-0` on the `dl` does not reach it, so the gap survives the very utility
   applied to remove it -- which is how several fact lists ended up looking
   bottom-heavy inside an otherwise even card. */
dl:last-child > dd:last-child {
	margin-bottom: 0;
}

/* Alerts here paint in solid colour, not Bootstrap's default tinted panel: a
   utility text colour computed against the page ground is unreadable on a
   saturated one, and an outline button drawn in `--bs-secondary` all but
   vanishes -- hence the overrides below.

   Muted text inside an alert is the alert's own colour, softened. All three of
   these resolve to --bs-secondary-color, which is computed against the page. */
.alert .text-body-secondary,
.alert .yksa-label,
.alert .yksa-hint {
	color: currentColor !important;
	opacity: .8;
}

/* Outline buttons take their colour from the alert rather than from the palette,
   and invert to the alert's ground on hover. The semantic distinction between
   `-secondary` and `-danger` is lost here, which is correct: the alert already
   says which of those it is, and a red outline on a red ground says nothing.

   Every value comes from `--bs-alert-color` / `--bs-alert-bg`, which the alert
   variant declares and the button inherits, so one rule covers all six variants
   in both text directions. Not `currentColor`: inside `:hover` that resolves to
   the *hover* colour, so a hover background of `currentColor` paints the button
   the colour it is about to be and the hover state disappears.

   The theme fills outline buttons with the subtle tint, so the fill is cleared
   here too, or alert-coloured text lands on a pale chip. */
.alert .btn-outline-primary,
.alert .btn-outline-secondary,
.alert .btn-outline-success,
.alert .btn-outline-info,
.alert .btn-outline-danger,
.alert .btn-outline-warning {
	--bs-btn-color: var(--bs-alert-color);
	--bs-btn-bg: transparent;
	--bs-btn-disabled-bg: transparent;
	--bs-btn-border-color: var(--bs-alert-color);
	--bs-btn-hover-color: var(--bs-alert-bg);
	--bs-btn-hover-bg: var(--bs-alert-color);
	--bs-btn-hover-border-color: var(--bs-alert-color);
	--bs-btn-active-color: var(--bs-alert-bg);
	--bs-btn-active-bg: var(--bs-alert-color);
	--bs-btn-active-border-color: var(--bs-alert-color);
	--bs-btn-disabled-color: var(--bs-alert-color);
	--bs-btn-disabled-border-color: var(--bs-alert-color);
	--bs-btn-focus-shadow-rgb: 255, 255, 255;
}

/* A filled button on a solid alert is the alert's own colour inverted -- the same
   pair, the other way round. `btn-light` was doing this by hand and only happened
   to be right on the four dark variants. */
.alert .btn-contrast {
	--bs-btn-color: var(--bs-alert-bg);
	--bs-btn-bg: var(--bs-alert-color);
	--bs-btn-border-color: var(--bs-alert-color);
	--bs-btn-hover-color: var(--bs-alert-bg);
	--bs-btn-hover-bg: var(--bs-alert-color);
	--bs-btn-hover-border-color: var(--bs-alert-color);
	--bs-btn-active-color: var(--bs-alert-bg);
	--bs-btn-active-bg: var(--bs-alert-color);
	--bs-btn-active-border-color: var(--bs-alert-color);
	--bs-btn-focus-shadow-rgb: 255, 255, 255;
	opacity: 1;
}

.alert .btn-contrast:hover {
	opacity: .9;
}

/* A record in a list is a row with a left status marker, not a card. */
.yksa-row {
	display: flex;
	/* Stated, not left to the flex default: `.card` sets `flex-direction:
	   column`, so a row that is also a card -- the catalogue entry, which needs
	   the card's border and the row's accent on one element -- silently became a
	   vertical stack, and with `flex-wrap` on it the rail then took a line of its
	   own. Nothing in the class's own declaration said which way it ran. */
	flex-direction: row;
	align-items: center;
	gap: .75rem;
	padding: .5rem;
	border-left: 4px solid var(--bs-border-color);
}

.yksa-row:hover {
	background: var(--yksa-hover-bg);
}

/* Both state vocabularies reach a row, because both kinds of record get listed
   as one: progress for a work item, health for a thing in a condition. The
   colours are the state pill's, so a row and the pill inside it cannot disagree. */
.yksa-row.is-in-progress,
.yksa-row.is-running     { border-left-color: var(--bs-primary); }
.yksa-row.is-complete,
.yksa-row.is-ok          { border-left-color: var(--bs-success); }
.yksa-row.is-pending,
.yksa-row.is-warning,
.yksa-row.is-forced      { border-left-color: var(--bs-warning); }
.yksa-row.is-failed      { border-left-color: var(--bs-danger); }
/* Withdrawn/skipped: still a record, but nothing here is work. The border
   stays the resting grey and the row's text steps back, which is the same
   thing `skipped` says in the state vocabulary. */
.yksa-row.is-skipped     { color: var(--bs-secondary-color); }

/* A sized first column in a `.yksa-row`: the fact the list is ordered by, so it
   starts at the same x on every row and the eye runs straight down it. Sized
   and not `min-width`, or a row with a longer value pushes everything after it
   out of line with the row above.

   It wraps under the body below `sm` -- at phone width a fixed rail and a
   flexible body next to it leave the body about eight characters wide. */
.yksa-row-rail {
	flex: 0 0 auto;
	width: 100%;
}

@media (min-width: 576px) {
	.yksa-row-rail {
		width: 13rem;
	}
}

/* A table row that *is* a record: the row is the link to it. Sanctioned where a
   card's equivalent is not -- a row is one target, while a card is a container
   of several. `.stretched-link` goes on the cell that names the record; this
   class is only here to give that anchor something to stretch against.

   Sibling links in the row are lifted above it, the same lift `.card-footer`
   needed and for the same reason: the stretched anchor covers the whole row, so
   without this a press on the station's own link silently opens the record
   instead. A row with no sibling links needs no lift and pays nothing for it. */
.yksa-row-link {
	position: relative;
}

.yksa-row-link a:not(.stretched-link) {
	position: relative;
	z-index: 2;
}

/* A spoiler, as <details class="yksa-disclosure"><summary>..</summary>..</details>.
   Native disclosure rather than a button pointed at a collapse target: it needs
   no JS, no id that can collide with another card's, and it opens for find-in-page
   and for print. The marker is the bootstrap-icons chevron of `.yksa-chevron`
   (stratoflights HIG 5), drawn here because a summary has no aria-expanded. */
.yksa-disclosure > summary {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	padding: 0.5rem var(--bs-card-spacer-x);
	font-size: 0.875rem;
	cursor: pointer;
	list-style: none;
}

.yksa-disclosure > summary::-webkit-details-marker {
	display: none;
}

.yksa-disclosure > summary::before {
	content: "\f285";
	font-family: bootstrap-icons;
	font-style: normal;
	font-weight: 400;
	line-height: 1;
	color: var(--bs-primary);
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.yksa-disclosure[open] > summary::before {
	content: "\f282";
}

.yksa-disclosure > summary:hover {
	background-color: var(--yksa-hover-bg);
}

/* Collapse toggle: <i class="bi bi-chevron-down yksa-chevron"> inside the element
   that carries aria-expanded. The closed state swaps in bi-chevron-right's glyph,
   so the icon follows Bootstrap's collapse state without a script. */
.yksa-chevron {
	color: var(--bs-primary);
}

[aria-expanded="false"] > .yksa-chevron::before {
	content: "\f285";
}

/* A card header that is itself the collapse toggle:
   <button class="yksa-header-toggle card-header" aria-expanded=..> with the
   chevron first. A button brings a border on every side; only the header's
   own rule is kept, and it drops out while the body is closed. */
.yksa-header-toggle {
	display: flex;
	align-items: center;
	gap: .5rem;
	width: 100%;
	text-align: left;
	color: var(--bs-body-color);
	border-width: 0 0 var(--bs-card-border-width) 0;
	border-style: solid;
}

.yksa-header-toggle[aria-expanded="false"] {
	border-bottom-color: transparent;
}

/* A table that is the card's own content, rather than content inside a card
   body, has only its cell padding between it and the card edge -- half the
   card's gutter, so the first column sits closer to the border than the header
   above it does. Only the outer columns move: the vertical rhythm is
   Bootstrap's, and a .card-body wrapper instead would stop the row borders
   short of the card edge. */
.card > .table-responsive > .table > :not(caption) > * > *:first-child,
.card > .table > :not(caption) > * > *:first-child {
	padding-left: var(--bs-card-spacer-x);
}

.card > .table-responsive > .table > :not(caption) > * > *:last-child,
.card > .table > :not(caption) > * > *:last-child {
	padding-right: var(--bs-card-spacer-x);
}

/* -----------------------------------------------------------------------------
   Row separators

   Bootstrap already draws them: `.table > :not(caption) > * > *` carries a
   `border-bottom-width`, and that is the whole mechanism. What it needs from the
   kit is one line, and it is not a workaround.

   Reboot leaves tables at `border-collapse: collapse`, where borders belong to
   the *table* and are painted before positioned descendants. `.yksa-row-link` is
   positioned -- `.stretched-link` has to have something to stretch against -- so
   the row painted its opaque cell backgrounds over the collapsed borders around
   it: its own separator, and, when it was the first body row, the rule under the
   header. Every table built out of record rows lost its lines that way, while the
   plain-row tables beside them kept theirs.

   In the separate model a border belongs to its own cell and is painted with it,
   so a positioned row carries its separator along. `border-spacing: 0` keeps the
   geometry identical to collapsed, and with borders only on the bottom edge there
   is nothing for the collapsing rules to have merged anyway.

   `border-collapse: separate; border-spacing: 0` is set on `.table` at the top of
   this file, next to the hover token, so there is one `.table` rule to find.

   This replaced a set of inset-`box-shadow` separators that reimplemented the
   border on the cell. They worked, but every rule that touched one had to
   re-declare Bootstrap's accent fill in the same `box-shadow` or the row lost
   hover and striping with the line, and each had to be checked by hand against
   the specificity of Bootstrap's own cell rule. One property does it instead.

   What is left below is only where a separator is *not* wanted. */
/* A row that continues the record above it: a run's error message under the run.
   The pair is one record, so the rule between them goes and the one under the
   *pair* stays.

   It was spelled `border-0` on the continuation row, which removes the lower line
   -- the error stayed divided from its own run and ran into the next one. The
   class goes on the continuation row and the rule suppresses the separator of the
   row before it. `:has()` rather than a second class on the run row, which would
   mean writing the same `{% if run.error %}` twice in seven templates; where it is
   unsupported the rule drops and the extra line is the harmless direction to fail
   in. */
.table > :not(caption) > tr:has(+ tr.yksa-row-cont) > * {
	border-bottom-width: 0;
}

/* The last row of a table that is the card's own content draws no rule: the card
   edge already closes the table, and a `.card-footer` -- the pager under every
   list -- draws its own border on top of that. All three landed at the foot of
   the same table, three hairlines within two pixels of each other.

   Scoped to a table the card holds directly, in the three shapes that put one
   flush against the card's bottom edge -- bare, wrapped for horizontal scroll, or
   inside a disclosure that ends the card. A table inside a `.card-body` is content
   within a surface rather than the surface itself: the body's padding stands
   between it and the edge, so its last row is a row like any other. */
.card > .table > tbody > tr:last-child > *,
.card > .table-responsive > .table > tbody > tr:last-child > *,
.card > .yksa-disclosure > .table-responsive > .table > tbody > tr:last-child > * {
	border-bottom-width: 0;
}

/* And it draws no trailing margin either. `.table` carries 1rem of it, and the
   `.table-responsive` wrapper hides the table from the `.card-body > :last-child`
   rule above, so this was being cancelled by hand on every list in the estate --
   an `mb-0` each in tdas, ops, pda and directory. Same three shapes. */
.card > .table,
.card > .table-responsive > .table,
.card > .yksa-disclosure > .table-responsive > .table {
	margin-bottom: 0;
}

/* The one emphasised element allowed on a surface: the active shift, the pass in
   contact, the source currently polling. Two on a page means one is wrong.

   It is a heavier border in the accent colour, not a tint. A row already carries
   a coloured left border and usually a state pill; adding a filled ground makes
   three ways of saying one thing, and the strongest of the three drowns the row's
   own content. Emphasis is the *weight* of the existing signal, not a new one. */
.yksa-row.is-active {
	border-left-color: var(--bs-success);
	border-left-width: 6px;
	padding-left: calc(.5rem - 2px);
}

/* Catalog card: a card summarising one record. The card is a container, not a
   control: its title is the link and nothing else in it moves under the pointer.
   Two earlier versions made the whole card hot -- one recoloured the border and
   title, one painted the hover ground -- and both put a card-sized reaction
   behind a card-sized target that also held its own footer buttons, so the page
   read as a grid of giant buttons and a press near a footer control was a coin
   toss. Hover is reserved for rows, which are one target each.

   `position: relative` stays so a card can still host absolutely-positioned
   corner chrome; it no longer backs a .stretched-link. */
.yksa-card-link {
	position: relative;
}

/* The title is a link and says so in the link colour, the way odms has always
   drawn it. `.text-decoration-none` on the anchor suppresses the underline at
   rest so a grid of forty titles is not forty underlines; hover brings it back,
   which is this card's one hover affordance. Both rules have to out-specify that
   utility, hence the element-qualified selectors. */
.yksa-card-link .card-title a,
.yksa-card-link h2 a,
.yksa-card-link h3 a {
	color: var(--bs-link-color);
}

.yksa-card-link .card-title a:hover,
.yksa-card-link h2 a:hover,
.yksa-card-link h3 a:hover,
.yksa-card-link .card-title a:focus-visible,
.yksa-card-link h2 a:focus-visible,
.yksa-card-link h3 a:focus-visible {
	color: var(--bs-link-hover-color);
	text-decoration: underline;
}

/* Row chrome that appears under the pointer: the retract on one entry of a long
   append-only stream, where forty always-visible trash icons would be the loudest
   thing on the page. It keeps its box at rest, so revealing it never reflows the
   row. Focus counts as pointing -- without that clause the control exists only
   for mouse users. */
.yksa-reveal {
	opacity: 0;
}

.yksa-reveal-host:hover .yksa-reveal,
.yksa-reveal:focus-visible,
.yksa-reveal-host:focus-within .yksa-reveal {
	opacity: 1;
}

/* The fact grid inside a catalog card: two columns of `.yksa-label` + figure.
   It was `.meta-col` in odms.css, defined against `.sat-row-card`, which is why
   ops built its own with a `dl.row` instead and got a different rhythm and a
   stray bottom margin. One card, one grid. */
.yksa-card-meta {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: .25rem .75rem;
}

/* Empty state: a sentence plus, where there is one, the action that resolves
   it -- beside the sentence, not under it. Never a blank card. */
.yksa-empty {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: .75rem;
	padding: 1.25rem;
	color: var(--bs-secondary-color);
}

/* A card stack spaces itself with `mb-3` on each card, but the empty state is
   also used as a `td`/`li` filler and so cannot carry one. Space the next card
   off it instead, or the two touch on exactly the branch nobody previews. */
.yksa-empty + .card {
	margin-top: 1rem;
}

/* Reading column for forms and reference pages that are worse for being wide. */
.yksa-narrow {
	max-width: 920px;
}

/* Save bar pinned to the viewport bottom, plus a spacer so it never covers the
   last row of the form it belongs to. */
.yksa-actionbar {
	position: fixed;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: var(--yksa-z-chrome);
}

.yksa-actionbar-spacer {
	height: 5rem;
}

.form-error-bar ul {
	margin-bottom: 0;
}

.cookie-banner {
	position: sticky;
	bottom: 0;
	left: 0;
	right: 0;
	z-index: var(--yksa-z-chrome);
	background-color: #1f2937;
	color: #f8fafc;
}

.cookie-banner a {
	color: #bfdbfe;
}

@media (max-width: 991px) {
	/* Collapsed, the nav is a stacked list; the full-height tab treatment would
	   put a border around every row. */
	.nav-full-height.nav-link {
		padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);
		margin-right: 0;
		border: none !important;
	}

	/* The bar grows here (the brand block keeps its height and the padding is
	   added around it), and --navbar-height has to grow with it: it is what
	   every in-page sticky bar offsets itself by, and a stale 44px slid ops's
	   pass console header up under the navbar on tablet widths.

	   61px is the same arithmetic that produces 44px above -- 34px logo + the
	   brand block's .3125rem padding either side -- plus .5rem of bar padding
	   either side and the 1px bottom border. min-height, not height, so a taller
	   brand overflows the offset rather than being clipped by it. */
	:root {
		--navbar-height: 61px;
	}

	.custom-navbar {
		height: auto;
		min-height: var(--navbar-height);
		padding-top: 0.5rem;
		padding-bottom: 0.5rem;
	}
}

/* A filter chip is a toggle, so it looks like one at both ends of its state: an
   outlined chip at rest, a filled one when it is on. It used to be `badge
   bg-secondary` at rest, which made an unselected filter a solid grey block --
   as loud as the selected one and easy to read as already applied. The class is
   self-contained now; callers need no `badge bg-*` alongside it. */
.filter-chip {
	display: inline-block;
	font-size: .75rem;
	line-height: 1.5;
	padding: .05rem .5rem;
	color: var(--bs-body-color);
	background-color: var(--bs-body-bg);
	border: 1px solid var(--bs-border-color);
	cursor: pointer;
	user-select: none;
	transition: background-color 0.12s ease;
}

.filter-chip:hover {
	background-color: var(--yksa-hover-bg);
}

.filter-chip.active {
	color: #fff;
	background-color: var(--bs-primary);
	border-color: var(--bs-primary);
}

.filter-chip.active:hover {
	background-color: var(--bs-primary);
}

/* Long-form text rendered from the database (policies, descriptions) and the
   table cells that hold it. */
.text-content {
	overflow: auto;
	hyphens: auto;
}

.text-content img {
	max-width: 100%;
	height: auto !important;
}

.text-content p:last-child,
.text-content ul:last-child,
.text-content ol:last-child,
td p:last-child,
td ul:last-child {
	margin-bottom: 0 !important;
}

/* Django's messages framework emits `error`; Bootstrap only ships `danger`. */
.alert-error {
	--bs-alert-color: #fff;
	--bs-alert-bg: #c42526;
	--bs-alert-border-color: #c42526;
}

/* A quiet border for grouped buttons that sit inside another bordered surface --
   a catalog card's footer -- where the button group's own outline would read as a
   second box around the first. `--bs-border-color` rather than a black alpha: the
   alpha approximated exactly this colour on white and disappeared on anything else. */
/* -----------------------------------------------------------------------------
   Verbatim machine text
   -------------------------------------------------------------------------- */

/* TLE lines, an OMM body, a hex dump, a stored run log. Scrolls rather than
   wraps: a wrapped TLE line is no longer a TLE line. */
.yksa-verbatim {
	font-family: var(--bs-font-monospace);
	white-space: pre;
	overflow: auto;
	max-height: 30rem;
}

.yksa-verbatim-short {
	max-height: 12rem;
}

/* Opt-in, for text with no meaningful line structure: a hex dump reads as well
   reflowed and a sideways scrollbar hides most of it. */
.yksa-verbatim-wrap {
	white-space: pre-wrap;
	overflow-wrap: anywhere;
}

/* -----------------------------------------------------------------------------
   Plot panels
   -------------------------------------------------------------------------- */

/* Lives here rather than in a service because ops and odms ship the same
   plot-panel.js and both address these classes. */
.plot-main-wrap {
	position: relative;
	height: 320px;
}

/* The strip of controls above a plot: a reset, a log-scale switch, a window
   toggle. Fixed height, because its contents vary per plot and a `.form-switch`
   is taller than a `.btn-badge` -- so the elements tab's strip was two pixels
   shorter than the altitude tab's, and the button and the whole canvas below it
   moved when the reader switched tabs. Declared once so no plot can opt out. */
.yksa-plot-controls {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: .5rem;
	min-height: 1.75rem;
	margin-bottom: .5rem;
}

/* Pointer feedback for the drag gestures plot-panel.js binds on the main canvas:
   plain drag pans, ctrl+drag draws a zoom rectangle, and a click on an axis band
   fixes that axis. Classes rather than inline styles so the states are greppable
   and a service can restyle them. */
.plot-main-wrap.is-pannable {
	cursor: grab;
}

.plot-main-wrap.is-panning {
	cursor: grabbing;
}

.plot-main-wrap.is-over-axis {
	cursor: pointer;
}

.plot-main-wrap.is-zooming {
	cursor: crosshair;
}

/* A chart with no brush strip under it: dashboard cards, the BC scatter. */
.plot-panel-wrap {
	position: relative;
	height: 260px;
}

.plot-inline-wrap {
	position: relative;
	height: 160px;
}

/* Brush / overview panel. The brush, its body and the two handles are flat
   siblings of the canvas so their absolute coordinates are measured against this
   element rather than compounding offsets. */
.plot-overview-wrap {
	position: relative;
	height: 35px;
	margin-top: 4px;
	cursor: crosshair;
	user-select: none;
	touch-action: none;
}

.plot-overview-wrap canvas {
	display: block;
}

.plot-overview-wrap .brush {
	position: absolute;
	top: 0;
	bottom: 0;
	background: rgba(var(--bs-primary-rgb), 0.18);
	border: 0;
	pointer-events: none;
}

.plot-overview-wrap .brush-body {
	position: absolute;
	top: 0;
	bottom: 0;
	cursor: grab;
	pointer-events: auto;
}

.plot-overview-wrap .brush-body:active {
	cursor: grabbing;
}

.plot-overview-wrap .handle {
	position: absolute;
	top: 0;
	bottom: 0;
	width: 16px;
	pointer-events: auto;
	cursor: ew-resize;
	transform: translateX(-50%);
	display: flex;
	align-items: center;
	justify-content: center;
}

.plot-overview-wrap .handle::before {
	content: "";
	display: block;
	width: 4px;
	height: 60%;
	background: var(--bs-primary);
	box-shadow: 0 0 0 1px var(--bs-body-bg);
}

.btn-outline-bordered {
	border-color: var(--bs-border-color);
}

/* -----------------------------------------------------------------------------
   Outcome swatches

   A run outcome, or a month of coverage, drawn as a square. It is not a state
   pill: a pill names one object, and these appear twenty or a hundred at a time
   in a strip or a grid, where a word per cell is unreadable. The legend beside
   them carries the words instead, which is what keeps colour from being the
   only signal.

   Colours read the same health vocabulary as yksa/ui/_state.html, so a run that
   is `warning` in the pill is the same amber in the strip. `odms` and `tdas`
   both draw a source board from this; `odms`'s decay grid draws its months from
   the same squares.
   -------------------------------------------------------------------------- */

.yksa-swatch {
	display: inline-block;
	width: 14px;
	height: 14px;
	vertical-align: -2px;
}

/* One run outcome in a strip. Narrower than a legend square because a table row
   carries twenty of them beside eight other columns. */
.yksa-run-cell {
	display: inline-block;
	width: 8px;
	height: 16px;
	margin-right: 2px;
	vertical-align: -3px;
}

.yksa-swatch-ok { background: var(--bs-success); }
.yksa-swatch-warning { background: var(--bs-warning); }
.yksa-swatch-failed { background: var(--bs-danger); }
.yksa-swatch-running { background: var(--bs-primary); }
.yksa-swatch-skipped { background: var(--bs-secondary); opacity: 0.45; }
.yksa-swatch-unknown { background: var(--bs-secondary); opacity: 0.25; }

/* Missing is deliberately not a colour: an absent run is an absent cell, and the
   eye should read it as a hole in the grid rather than as another state. */
.yksa-swatch-missing {
	background: transparent;
	border: 1px dashed var(--bs-border-color);
}

/* Due but not yet late -- the current month on a coverage grid. Hatched, so it
   reads as "in flight" rather than as either done or missed. */
.yksa-swatch-pending {
	background: repeating-linear-gradient(45deg,
		var(--bs-border-color) 0 3px, transparent 3px 6px);
}

/* Outside the period the record could exist for. Not a gap in the pipeline, so
   it must not look like one. */
.yksa-swatch-not-applicable {
	background: transparent;
}

/* -----------------------------------------------------------------------------
   Notation switcher
   -------------------------------------------------------------------------- */

/* yksa/ui/_variants.html. Inline: it sits in a sentence, a table cell or a fact
   grid, so it must not introduce a line box taller than the text around it --
   which is why the caret is sized down from .yksa-btn-icon's row height. */
.yksa-variants {
	display: inline-flex;
	align-items: baseline;
	gap: .35rem;
	max-width: 100%;
}

.yksa-variants-value {
	overflow-wrap: anywhere;
	font-variant-numeric: tabular-nums;
}

.yksa-variants-toggle {
	min-width: 1rem;
	min-height: 1rem;
	padding: 0 .1rem;
	font-size: .7rem;
	vertical-align: baseline;
}

/* An alert inside a card body or a tab pane, where the full-size one is a slab.
   Named rather than spelled `py-2 px-3 small` per site, which is how the same
   warning ended up three heights on three pages. */
.yksa-alert-sm {
	padding: .4rem .6rem;
	font-size: .875rem;
}

/* The alert is a flex row (icon, then body). A flex item will not shrink below
   its content, so a `verbatim` body of long lines pushed the alert wider than
   its card instead of scrolling inside it. */
.alert > div {
	min-width: 0;
}

/* The same escape hatch, as a utility, because a flex item refusing to shrink
   below its content is the single most common layout bug in this estate: a
   `<pre>` of TLE lines, a long satellite name, a monospace URL. Bootstrap has
   `mw-100` and `min-vw-100` but nothing for this, so seven templates across
   three services were already spelling it `min-w-0` -- and it did nothing on
   any of them, because nothing defined it. */
.min-w-0 {
	min-width: 0;
}

/* An unattended full-screen board: <body class="yksa-wall"> with no navbar,
   footer or controls, and the board in `.yksa-wall-board`. It is sized to one
   screen; whatever does not fit scrolls inside the board, not the page. */
.yksa-wall {
	max-height: 100vh;
	display: flex;
	flex-direction: column;
	overflow: hidden;
	font-size: .9375rem;
}

.yksa-wall-board {
	flex: 1 1 auto;
	min-height: 0;
	display: flex;
	flex-direction: column;
	gap: .5rem;
	overflow-y: auto;
}

.yksa-wall .plot-overview-wrap {
	display: none;
}
