/* ============================================================================= YKSA UI kit. Ships with yksa-web-kit; see ecosystem/ui-kit.md. 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; } /* 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); } /* ----------------------------------------------------------------------------- Navbar and footer -- layout-identical across every service -------------------------------------------------------------------------- */ .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; } .nav-full-height.nav-link:hover, .nav-full-height.nav-link.active { color: #fff !important; background-color: var(--bs-primary); } /* ----------------------------------------------------------------------------- State -- the sanctioned rendering of status (ui/_state.html) -------------------------------------------------------------------------- */ .yksa-state { display: inline-flex; align-items: center; gap: .25rem; font-size: .75rem; font-weight: 700; padding: .05rem .4rem; white-space: nowrap; border: 1px solid transparent; /* An inline-flex box baseline-aligns on its own last line box, which in a table cell or a run of text leaves it sitting a few pixels low. It is a self-contained box; align it on the middle instead. */ vertical-align: middle; } /* health */ .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. */ .yksa-chip { display: inline-flex; align-items: center; gap: .25rem; font-size: .75rem; padding: 0 .35rem; white-space: nowrap; color: var(--bs-secondary-color); background: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); vertical-align: middle; } /* ----------------------------------------------------------------------------- Typography helpers -------------------------------------------------------------------------- */ .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; } /* 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; } /* ----------------------------------------------------------------------------- Containers -------------------------------------------------------------------------- */ /* A container that supplies its own padding must not also inherit the trailing margin of whatever it ends with. Bootstrap does this for `p` in a few places and not at all for `dl`, `ul` or `table`, so the fix was being applied by hand -- an `mb-0` on the last element of every card body -- which every element added later then has to remember. It is one rule and it belongs here. Scoped to direct children: anything deeper is inside something else's box, and that box's spacing is its own business. */ .card-body > :last-child, .alert > :last-child, .list-group-item > :last-child, .modal-body > :last-child, .offcanvas-body > :last-child { margin-bottom: 0; } /* ----------------------------------------------------------------------------- Alerts The estate theme paints alerts in solid colour, not the tinted panel Bootstrap ships by default. That changes what belongs inside one: a utility text colour is computed against the page ground and is unreadable on a saturated one, and an outline button drawn in `--bs-secondary` all but vanishes. -------------------------------------------------------------------------- */ /* 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. */ .alert .btn-outline-primary, .alert .btn-outline-secondary, .alert .btn-outline-success, .alert .btn-outline-danger, .alert .btn-outline-warning { --bs-btn-color: inherit; --bs-btn-border-color: currentColor; --bs-btn-hover-color: var(--bs-alert-bg); --bs-btn-hover-bg: currentColor; --bs-btn-hover-border-color: currentColor; --bs-btn-active-color: var(--bs-alert-bg); --bs-btn-active-bg: currentColor; --bs-btn-active-border-color: currentColor; --bs-btn-focus-shadow-rgb: 255, 255, 255; } /* ----------------------------------------------------------------------------- Layout -------------------------------------------------------------------------- */ /* A record in a list is a row with a left status marker, not a card. */ .yksa-row { display: flex; align-items: center; gap: .75rem; padding: .5rem; border-left: 4px solid var(--bs-border-color); } .yksa-row:hover { background: var(--bs-tertiary-bg); } .yksa-row.is-in-progress { border-left-color: var(--bs-primary); } .yksa-row.is-complete { border-left-color: var(--bs-success); } .yksa-row.is-pending { border-left-color: var(--bs-warning); } .yksa-row.is-failed { border-left-color: var(--bs-danger); } /* The one saturated element allowed on a surface: the active shift, the pass in contact, the source currently polling. Two on a page means one is wrong. */ .yksa-row.is-active { border-left-color: var(--bs-success); background: var(--bs-success-bg-subtle); } /* Catalog card: a card summarising one record, where the whole card is the link to it. The hover treatment is defined once, here, because three services had a grid of these and none of them said what hovering meant -- the cursor changed over the title only, and the padding around it was dead. The card carries this class and the title anchor carries .stretched-link. */ .yksa-card-link { position: relative; } .yksa-card-link:hover { border-color: var(--bs-primary); background: var(--bs-tertiary-bg); } /* The title is already the link; underlining it on hover would be the only moving part in a grid of forty. The border carries the affordance instead. */ .yksa-card-link:hover .stretched-link { color: var(--bs-primary); } /* 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); } /* 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 -------------------------------------------------------------------------- */ .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; } /* ----------------------------------------------------------------------------- Responsive -------------------------------------------------------------------------- */ @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; } .custom-navbar { height: auto; padding-top: 0.5rem; padding-bottom: 0.5rem; } } /* ----------------------------------------------------------------------------- Chip filters (yksa/includes/chip_filters.html + src/chip-filters.js) -------------------------------------------------------------------------- */ .filter-chip { cursor: pointer; user-select: none; transition: background-color 0.12s ease; } .filter-chip:hover { filter: brightness(110%); } .filter-chip.active { background-color: var(--bs-primary) !important; color: #fff; } /* ----------------------------------------------------------------------------- Prose and tables Long-form text rendered from the database (policies, descriptions) and the table cells that hold it. Everything else that used to live in main.css was qsl-only and did not come across. -------------------------------------------------------------------------- */ .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; } .btn-outline-bordered { border-color: rgba(0, 0, 0, 0.066); }