The Rules section is one record table grouped by kind, Limit then Boolean, on fixed columns, edited in place: Edit opens a row's controls in their columns, Add rule opens a dense row above the table, and every change is staged into a tray that lists the deltas with Undo and applies them as one rule-change act. The reduction policy is a column of the rule beside its limit. History shows counts only. Group rows are a quiet heading rather than a divider, the maintainer's pick from four rounds of outside-model ideation. Dense rows align to the top and render each error under its control in every form family (design D16), replacing the below-row error block; the forms library gains the batch form (rows plus one tray) and the RowField dense and label-hidden options. Migration 00019 records the governing reduction policy on effect rows. Archive staged-rule-changes with its spec updates (entitlement-set- management, entitlement-set-history, entitlements, form-library, form-conventions, ui-quality-gate). Screens accepted 2026-09-19.
696 lines
27 KiB
CSS
696 lines
27 KiB
CSS
/* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial */
|
|
/* SPDX-FileCopyrightText: 2025-2026 Christian Galo */
|
|
|
|
/* ============================================================
|
|
* app.css — supplemental styles only
|
|
* ------------------------------------------------------------
|
|
* The member-console design system is Bootstrap-first. This file
|
|
* does NOT define a parallel token layer or utility system: it only
|
|
* covers the small set of things Bootstrap 5 does not.
|
|
*
|
|
* Contract:
|
|
* - Use Bootstrap utilities/components directly in templates.
|
|
* - Add a rule here only when Bootstrap genuinely cannot express it
|
|
* (e.g. the HTMX loading-indicator hook, fixed toast positioning,
|
|
* cross-browser <meter> styling, the linked card's hover shade).
|
|
* - Reference Bootstrap CSS variables (var(--bs-*)) instead of
|
|
* hardcoded color values so this file stays in sync if Bootstrap's
|
|
* defaults change.
|
|
*
|
|
* See docs/design-system.md for the full Bootstrap usage guide and the
|
|
* custom-primitives reference.
|
|
* ============================================================ */
|
|
|
|
/* HTMX loading indicators */
|
|
.htmx-indicator {
|
|
display: none;
|
|
}
|
|
|
|
.htmx-request .htmx-indicator {
|
|
display: inline-block;
|
|
}
|
|
|
|
.htmx-request.htmx-indicator {
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Dashboard quota bar */
|
|
.quota-bar {
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Global toast positioning */
|
|
.toast-container {
|
|
position: fixed;
|
|
top: 1rem;
|
|
right: 1rem;
|
|
z-index: 1100;
|
|
}
|
|
|
|
/* Entitlement quota meter — Bootstrap doesn't style <meter> consistently
|
|
* across engines, so we normalize it to the Bootstrap primary color. */
|
|
meter {
|
|
height: 0.75rem;
|
|
border-radius: 0.25rem;
|
|
overflow: hidden;
|
|
}
|
|
meter::-webkit-meter-bar {
|
|
background: var(--bs-border-color);
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
}
|
|
meter::-webkit-meter-optimum-value,
|
|
meter::-webkit-meter-suboptimum-value,
|
|
meter::-webkit-meter-even-less-good-value {
|
|
background: var(--bs-primary);
|
|
border-radius: 0.25rem;
|
|
}
|
|
meter::-moz-meter-bar {
|
|
background: var(--bs-primary);
|
|
}
|
|
|
|
/* Inline code chip (design D16): renders in the body colour on a faint
|
|
* background, keeping the mono font, instead of Bootstrap's default pink
|
|
* <code> colour — an identifier (a key, a route) is not an error, and the
|
|
* unstyled pink read as one. */
|
|
code {
|
|
color: var(--bs-body-color);
|
|
background-color: var(--bs-tertiary-bg);
|
|
padding: 0.1em 0.3em;
|
|
border-radius: 0.25rem;
|
|
}
|
|
pre code {
|
|
color: inherit;
|
|
background-color: transparent;
|
|
padding: 0;
|
|
border-radius: 0;
|
|
}
|
|
|
|
/* White card headers (design D16): links and outline-primary buttons
|
|
* inside a card header use --bs-link-hover-color (6.1:1 on white) rather
|
|
* than Bootstrap's default tinted card-header background, which clears the
|
|
* organization composite's residual contrast failure (4.24:1 on the tinted
|
|
* header) without a one-off override there. */
|
|
.card-header {
|
|
background-color: var(--bs-body-bg);
|
|
}
|
|
.card-header .nav-link,
|
|
.card-header .btn-outline-primary {
|
|
color: var(--bs-link-hover-color);
|
|
}
|
|
.card-header .btn-outline-primary {
|
|
border-color: var(--bs-link-hover-color);
|
|
}
|
|
|
|
/* Application shell rail (docs/design-system.md "Application shell";
|
|
* chrome-conventions "One shell for both surfaces"). Present on every
|
|
* top-level layout: the member rail (partials/shell_rail_member.html) and
|
|
* the operator rail (operator.html). A fixed 15rem so labels never reflow
|
|
* and no entry's content can ever widen it (maintainer 2026-08-23: the
|
|
* sidebar never gets wide; long labels wrap instead). Below lg the same
|
|
* element is a Bootstrap offcanvas drawer, where the fixed width also caps
|
|
* the drawer. */
|
|
.app-sidebar {
|
|
min-width: 15rem;
|
|
max-width: 15rem;
|
|
}
|
|
|
|
/* At lg and up Bootstrap's responsive offcanvas (.offcanvas-lg) becomes a
|
|
* static block, resets its background to transparent, and lays out
|
|
* .offcanvas-body as a flex row. Restore the rail's surface and let the
|
|
* entry list fill the rail's width so the active highlight spans it. */
|
|
@media (min-width: 992px) {
|
|
.app-sidebar {
|
|
background-color: var(--bs-light) !important;
|
|
}
|
|
.app-sidebar .offcanvas-body {
|
|
flex-direction: column;
|
|
}
|
|
.app-sidebar .offcanvas-body > .nav {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
/* The surface switch, the rail's one non-section entry (member rail:
|
|
* "Operator panel"; operator rail: "Member panel", design D25), sits last
|
|
* among the entries and apart from the task entries, using the
|
|
* border-top-plus-spacing grouping idiom the rails have always used. */
|
|
.app-rail-switch {
|
|
margin-top: 0.5rem;
|
|
padding-top: 0.5rem;
|
|
border-top: var(--bs-border-width) solid var(--bs-border-color);
|
|
}
|
|
|
|
/* The account group, the drawer's second list below lg (mobile-shell-menu
|
|
* D8). Two groups in one drawer are told apart by distance first and by ink
|
|
* second (NN/g, the proximity principle: varying whitespace is what
|
|
* communicates a grouping, and rules of equal weight say "peers"). So the
|
|
* group is pushed to the drawer's foot by the auto margin, which also lands
|
|
* it in the same place on both surfaces however many sections the rail has,
|
|
* and it carries exactly one hairline — the boundary that survives when a
|
|
* full drawer squeezes the whitespace out. The rail's own entries keep the
|
|
* one they have for the switch; nothing else in the drawer draws a line.
|
|
*
|
|
* The drawer's padding moved from the entry list to .offcanvas-body when
|
|
* this became two lists, so both sit at one inset; the flex column is what
|
|
* gives margin-top:auto something to push against (Bootstrap already lays
|
|
* the body out this way at lg and up, where the group is display:none). */
|
|
.app-sidebar .offcanvas-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.app-rail-account {
|
|
margin-top: auto;
|
|
padding-top: 0.75rem;
|
|
border-top: var(--bs-border-width) solid var(--bs-border-color);
|
|
}
|
|
|
|
/* The identity block names the group (aria-labelledby), so it is set as a
|
|
* heading and hugs the entries it heads rather than floating between them
|
|
* and the rule. It carries the entries' horizontal padding itself, being no
|
|
* link; the email wraps rather than widening the rail (chrome-conventions
|
|
* "The sidebar has a fixed width"). */
|
|
.app-rail-account-identity {
|
|
margin-bottom: 0.25rem;
|
|
padding: 0 1rem;
|
|
}
|
|
.app-rail-account-name {
|
|
display: block;
|
|
font-weight: 600;
|
|
}
|
|
.app-rail-account-email {
|
|
display: block;
|
|
font-size: 0.875em;
|
|
color: var(--bs-secondary-color);
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
/* The rail toggler lives in an always-expanded navbar, and Bootstrap's
|
|
* .navbar-expand hides .navbar-toggler at every width; show it below lg,
|
|
* where the rail is a drawer (d-lg-none hides it again at lg and up). It is
|
|
* the bar's only control there, at the right end, and it never shrinks. The
|
|
* brand's 55vw cap keeps it off the account trigger, which does not render
|
|
* below lg, so the deployment name takes the rest of the line; min-width:0
|
|
* and the ellipsis still stop it widening the bar (mobile-shell-menu D2). */
|
|
@media (max-width: 991.98px) {
|
|
.app-topbar .navbar-toggler {
|
|
display: inline-block;
|
|
flex-shrink: 0;
|
|
}
|
|
.app-topbar .navbar-brand {
|
|
max-width: none;
|
|
}
|
|
}
|
|
|
|
/* Sign out is a <button> in both of the account menu's homes, because it
|
|
* posts (logout-ends-session D6); the account trigger is a <button> so
|
|
* hx-boost never treats it as a link. Strip the user-agent button chrome in
|
|
* both places so they read as the links beside them, and let the rail's
|
|
* entry fill its width like the anchors above it. */
|
|
.app-topbar .nav-link[type="button"],
|
|
.app-sidebar .nav-link[type="button"] {
|
|
background: none;
|
|
border: 0;
|
|
}
|
|
.app-sidebar .nav-link[type="button"] {
|
|
width: 100%;
|
|
text-align: left;
|
|
}
|
|
|
|
/* The top bar must fit the narrowest phone without widening the layout
|
|
* viewport (a 390px phone laid the page out at 438px and cut the person's
|
|
* name in half). Nothing in the bar wraps; the brand yields first, with an
|
|
* ellipsis; the person's name is capped at 40vw and truncates with an
|
|
* ellipsis while the caret stays visible. The bar holds the brand and
|
|
* exactly one control — the account menu at lg and up, the menu toggler
|
|
* below lg — and nothing else, ever (design D16; the surface label that
|
|
* used to sit here left the top bar for the trail's root crumb, D18; the
|
|
* account trigger left it below lg for the drawer, mobile-shell-menu D2). */
|
|
.app-topbar .container-fluid {
|
|
flex-wrap: nowrap;
|
|
min-width: 0;
|
|
}
|
|
/* design D16: the brand truncates with an ellipsis instead of wrapping
|
|
* when a deployment name is long, capped so it never crowds out the
|
|
* account menu even on a wide viewport. */
|
|
.app-topbar .navbar-brand {
|
|
flex: 0 1 auto;
|
|
min-width: 0;
|
|
max-width: 55vw;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.app-topbar .navbar-nav {
|
|
flex-shrink: 0;
|
|
}
|
|
.app-topbar .app-account-label {
|
|
display: inline-block;
|
|
max-width: 40vw;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
vertical-align: bottom;
|
|
}
|
|
@media (max-width: 575.98px) {
|
|
.app-topbar .navbar-brand {
|
|
margin-right: 0.5rem;
|
|
font-size: 1rem;
|
|
}
|
|
.app-topbar .navbar-toggler {
|
|
padding: 0.25rem 0.5rem;
|
|
}
|
|
}
|
|
|
|
/* External-destination cue (chrome-conventions: "the identity-provider
|
|
* handoff is marked subtly") — a small monochrome inline SVG that inherits
|
|
* the link color. An SVG, not a character: arrow/box glyphs like U+2197
|
|
* take emoji presentation in common font stacks, which reads as unserious.
|
|
* Inline SVG is an atomic inline, so browsers allow a soft wrap before it
|
|
* even with no whitespace — the icon must share a text-nowrap span with the
|
|
* label's last word so it can never land on a line by itself. */
|
|
.external-link-icon {
|
|
width: 0.75em;
|
|
height: 0.75em;
|
|
margin-left: 0.35em;
|
|
vertical-align: -0.05em;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* A panel opener states its open/closed state in its glyph, not its weight
|
|
* (maintainer 2026-09-03: the 2026-08-23 pressed rule that painted an open
|
|
* trigger with its active palette "looks pressable as opposed to
|
|
* untoggleable", so it is retired). The leading plus (the openerIcon part
|
|
* in ui_section_header.html) rotates 45 degrees into a cross while
|
|
* Bootstrap's collapse plumbing reports aria-expanded="true" on the
|
|
* trigger. Sources: Material Design's speed-dial FAB rotates its plus into
|
|
* a close glyph when the dial opens; Carbon's accordion flips its icon
|
|
* while a section is expanded. Sized in rem, not em, so the glyph is the
|
|
* same 14px on a btn-sm opener and a full-size one. */
|
|
.app-opener-icon {
|
|
width: 0.875rem;
|
|
height: 0.875rem;
|
|
margin-right: 0.35em;
|
|
vertical-align: -0.1em;
|
|
transition: transform 0.15s ease-in-out;
|
|
}
|
|
|
|
[aria-expanded="true"] > .app-opener-icon {
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.app-opener-icon {
|
|
transition: none;
|
|
}
|
|
}
|
|
|
|
/* A linked card's chevron glyph (page-anatomy "A linked card is marked by
|
|
* a glyph and the row's hover"; overview-consistency D3, round 2): marks
|
|
* the at-a-glance tiles' trailing edge with a "go into" cue, unlike
|
|
* .external-link-icon's "leaves the console" arrow. Sized larger than the
|
|
* opener glyph because it sits alone at a card's edge rather than beside a
|
|
* button's own label. */
|
|
.app-chevron-icon {
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
flex-shrink: 0;
|
|
color: var(--bs-secondary-color);
|
|
}
|
|
|
|
/* A radio or checkbox group's <legend> is a form label, not a heading:
|
|
* Bootstrap's reboot sizes every legend at 1.5rem, which made the
|
|
* Visibility group's title dwarf the field labels beside it (round-2 page
|
|
* pass, 2026-09-03). Inherit the form's size and weight; float:none is
|
|
* already on the element via .float-none. */
|
|
legend.form-label {
|
|
font-size: inherit;
|
|
font-weight: inherit;
|
|
width: auto;
|
|
}
|
|
|
|
/* Field-help cue (form-conventions: "Dense form rows keep explanations
|
|
* behind a help icon") — a small circled question mark following a label,
|
|
* carrying its explanation in the wrapping span's title tooltip, so a
|
|
* compact form row never holds a paragraph. Same rationale as
|
|
* .external-link-icon for being an SVG rather than a character. */
|
|
.form-help-icon {
|
|
width: 0.85em;
|
|
height: 0.85em;
|
|
margin-left: 0.3em;
|
|
vertical-align: -0.1em;
|
|
opacity: 0.6;
|
|
cursor: help;
|
|
}
|
|
|
|
/* Help icon button (design D23 "Help is disclosed by click-opened
|
|
* popovers") — the helpIcon part wraps .form-help-icon in a plain <button>
|
|
* (data-bs-toggle="popover") instead of a `<span title>`, so it needs a
|
|
* pointer cursor rather than .form-help-icon's own "help" cursor, and no
|
|
* underline/outline from the .btn-link base it resets. */
|
|
.app-help-icon {
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
}
|
|
.app-help-icon .form-help-icon {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* The form library's four layout families (spec form-conventions "Forms use
|
|
* the layout family their kind names"; design D10, D20, D21, D22).
|
|
* Everything else a form needs is a Bootstrap class the form part already
|
|
* writes; the rules below are the parts Bootstrap has no utility for.
|
|
*
|
|
* Stacked caps the form's own section box at a readable measure so a name
|
|
* input does not stretch across a 1600px window, which is what NN/g,
|
|
* Baymard, Polaris and USWDS agree on for a single-column form. The cap
|
|
* sits on the box the form part renders, not on the field column inside
|
|
* it, so the box ends where the fields end and hugs them (maintainer,
|
|
* 2026-09-04: "Hugging definitely"). It is a max-width on a block element
|
|
* with no auto margins, so the box stays left-aligned under its section
|
|
* header; the sections below it keep the full content width.
|
|
*
|
|
* The commit never stretches to its grid column (finding FA-18): a dense
|
|
* row's commit sits in a col-auto and takes its natural width. The
|
|
* form-level error slot renders on every form and collapses when unused,
|
|
* so an empty slot adds no space above the first field. */
|
|
.app-form-box {
|
|
max-width: 40rem;
|
|
}
|
|
|
|
/* The Rows family's tray is a box at its body's width, not at the readable
|
|
* measure: it is the table's own footing, and a tray narrower than the
|
|
* table it belongs to reads as a separate panel (design D10 of
|
|
* staged-rule-changes; the shipped preview box read that way). */
|
|
.app-form-box-wide {
|
|
max-width: none;
|
|
}
|
|
|
|
.app-form-error:empty {
|
|
display: none;
|
|
}
|
|
|
|
/* The Confirm kind's message region (shell.confirm) always renders, even
|
|
* with nothing in it at page load: confirm-action-modal.js writes each
|
|
* trigger's body text into it after the page has already loaded, so the
|
|
* element must already exist for the script to find (forms.FormView.
|
|
* MessageID's doc). Collapse it while empty so it adds no space until the
|
|
* script fills it in. */
|
|
.app-form-message:empty {
|
|
display: none;
|
|
}
|
|
|
|
.app-form-commit .btn {
|
|
width: auto;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.app-form-dense .app-form-commit {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
}
|
|
|
|
/* A dense row aligns its columns to the top under one label line (design
|
|
* D16 of staged-rule-changes): every labelled column is a one-line label
|
|
* over its control, so the controls share a line, and an error under one
|
|
* control grows that column alone. A column with no label of its own, the
|
|
* commit and a checkbox, carries the label line's height as padding from
|
|
* the breakpoint where the row lays out side by side, so it sits on the
|
|
* control line at rest and stays there under a refusal. The height is the
|
|
* label's: one line at the body size plus .form-label's margin. Below xl
|
|
* the row stacks and nothing needs to line up. This replaced D22's error
|
|
* block below the row, whose line sat at the row's left edge far from its
|
|
* control (maintainer, 2026-09-19). */
|
|
@media (min-width: 1200px) {
|
|
.app-form-dense-row > .app-form-unlabeled {
|
|
padding-top: calc(1.5rem + 0.5rem);
|
|
}
|
|
}
|
|
|
|
/* Bar (design D20): a search is one input group, capped at a measure that
|
|
* suits a search box rather than running the content column's width. The
|
|
* cap sits on the form, a block element with no auto margins, so the bar
|
|
* stays left-aligned where the list's own header puts it. Wide drops the
|
|
* cap for a search that is its page's primary control (the landing
|
|
* surface's lookup), so its width says so. */
|
|
.app-form-bar {
|
|
/* The bar is a flex item inside the list controls row (beside the
|
|
* facet pills), so a bare max-width would let it shrink to its
|
|
* content: a basis of 20rem grows to the cap where there is room and
|
|
* wraps the pills below it where there is not. */
|
|
flex: 1 1 20rem;
|
|
max-width: 30rem;
|
|
}
|
|
|
|
/* Table (design D21): the label column carries the label and its hint on
|
|
* two lines, so the label's own bottom margin would open a gap between
|
|
* them; the row is the spacing. */
|
|
.app-form-table .form-label {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* A bool key's control is a checkbox alone in its cell: the table's label
|
|
* column already names it, so no .form-check wrapper and no second label.
|
|
* Bootstrap's standalone .form-check-input carries the 0.25em top margin
|
|
* that lines a checkbox up with the text beside it inside a .form-check;
|
|
* in a cell the table itself centres, that margin drops it off centre. */
|
|
.app-form-table .form-check-input {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* Identity-provider glyph (design D13, round 5): a field label the
|
|
* identity provider owns carries this after its text, wrapped in a
|
|
* .text-body-secondary span (managedGlyph, ui_managed_glyph.html). Sized
|
|
* to the 16px body text it follows; colour comes from the wrapping span's
|
|
* class, not the SVG. */
|
|
.app-managed-icon {
|
|
width: 1em;
|
|
height: 1em;
|
|
vertical-align: -0.15em;
|
|
}
|
|
|
|
/* The main content region is a flex item beside the sidebar, and flex
|
|
* items default to min-width:auto — they refuse to shrink below their
|
|
* content's min-content width, so one wide table (e.g. the plan-topology
|
|
* grid) silently widens the whole page past the viewport instead of
|
|
* scrolling inside its own .table-responsive wrapper (maintainer
|
|
* 2026-08-23: the body extended beyond the top navbar). min-width:0 lets
|
|
* the region shrink to the available space; wide content then scrolls in
|
|
* its own overflow container. Applies to the operator <main> and the
|
|
* member layouts' .flex-fill content divs alike. */
|
|
.app-sidebar ~ main,
|
|
.app-sidebar ~ .flex-fill {
|
|
min-width: 0;
|
|
}
|
|
|
|
/* Record tables (ACC-5, design D9): min-width:0 above only lets the wide
|
|
* case scroll instead of widening the page — it does nothing for the
|
|
* narrow case, where a table with no minimum width keeps shrinking until
|
|
* cells compress to one letter per line. .table-record is the floor: a
|
|
* record table declares it so its .table-responsive wrapper scrolls
|
|
* horizontally below 44rem instead of squeezing. Pair it with
|
|
* `text-nowrap` on identifier (<code>), money, and date cells so those
|
|
* tokens never wrap either. 44rem suits a typical multi-column record
|
|
* table; a table that is narrow by nature (few columns, short content)
|
|
* should look artificially wide stretched to 44rem — give it
|
|
* .table-record-sm instead of tuning .table-record itself, so every
|
|
* lane's tables keep declaring a floor through a shared, named class
|
|
* rather than a one-off inline number. */
|
|
.table-record {
|
|
min-width: 44rem;
|
|
}
|
|
.table-record-sm {
|
|
min-width: 28rem;
|
|
}
|
|
/* The scroll wrapper is the containing block for anything positioned
|
|
* inside a record table. Bootstrap leaves .table-responsive static, so an
|
|
* absolutely positioned descendant (the disabled-control part's
|
|
* visually-hidden reason, a tooltip anchor) resolves against the document
|
|
* instead: at a narrow width its static position lands where the scrolled
|
|
* cell would be, past the viewport, and the document grows to reach it
|
|
* (Plan ladders at mobile width, 2026-09-02 capture). Relative positioning
|
|
* keeps such descendants inside the wrapper's overflow. */
|
|
.table-responsive {
|
|
position: relative;
|
|
}
|
|
|
|
/* A control inside an alert takes the alert's own text colour. Bootstrap's
|
|
* secondary outline (#6c757d) on the warning alert's background (#fff3cd)
|
|
* measures 4.23:1, under AA; the alert's text colour (#664d03 on warning)
|
|
* clears it on every alert tone (2026-09-02 Lighthouse re-run: the
|
|
* not-configured state's Settings link on the FedWiki and Discourse pages). */
|
|
.alert .btn-outline-secondary {
|
|
--bs-btn-color: var(--bs-alert-color);
|
|
--bs-btn-border-color: var(--bs-alert-color);
|
|
--bs-btn-hover-bg: var(--bs-alert-color);
|
|
--bs-btn-hover-border-color: var(--bs-alert-color);
|
|
--bs-btn-active-bg: var(--bs-alert-color);
|
|
--bs-btn-active-border-color: var(--bs-alert-color);
|
|
}
|
|
|
|
/* Rail link colour (ACC-40, change acceptance-fixes 2026-09-02; chrome-
|
|
* conventions "The shell meets AA contrast"). Bootstrap's default
|
|
* --bs-link-color (#0d6efd) on the rail's bg-light (#f8f9fa) measures
|
|
* 4.26:1 at rest and 3.84:1 active (the active state's primary-tinted
|
|
* background pulls it lower still) — both fail AA's 4.5:1 for normal
|
|
* text. --bs-primary-text-emphasis (#052c65) is Bootstrap's own darker
|
|
* shade of primary, meant for text on a light/subtle background (the
|
|
* `.alert-primary`/`.link-primary` pairing); it measures 12.8:1 at rest
|
|
* and 11.5:1 active here — comfortably past the floor. */
|
|
.app-sidebar .nav-link {
|
|
color: var(--bs-primary-text-emphasis);
|
|
}
|
|
|
|
/* Active page indicator — Bootstrap's default .nav-link.active only changes
|
|
* color, which is faint against bg-light; add weight + a tinted background
|
|
* so the current page is unambiguous at a glance. */
|
|
.app-sidebar .nav-link.active {
|
|
color: var(--bs-primary-text-emphasis);
|
|
font-weight: 600;
|
|
background-color: rgba(var(--bs-primary-rgb), 0.08);
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
/* Plan topology overview (operator-topology-legibility). The first column
|
|
* (rank) stays pinned while the cross-ladder grid scrolls horizontally inside
|
|
* its .table-responsive wrapper, so ranks stay readable when many ladders push
|
|
* the grid wider than the viewport. Scoped to .topology-grid so no other
|
|
* operator table is affected. */
|
|
.topology-grid th:first-child,
|
|
.topology-grid td:first-child {
|
|
position: sticky;
|
|
left: 0;
|
|
z-index: 1;
|
|
background-color: var(--bs-body-bg);
|
|
}
|
|
|
|
/* Drag-to-reorder (operator) — shared by the topology ladder columns
|
|
* (horizontal) and the ladder tier rows (vertical), both driven by SortableJS
|
|
* (self-hosted for CSP) via sortable-reorder.js. Bootstrap-variable drag
|
|
* feedback + grab affordance, no inline styles. Pinned items (e.g. the topology
|
|
* rank column) carry no handle and are excluded via data-sortable-filter. */
|
|
.sortable-handle {
|
|
cursor: grab;
|
|
user-select: none;
|
|
}
|
|
.sortable-handle:active {
|
|
cursor: grabbing;
|
|
}
|
|
.sortable-ghost {
|
|
opacity: 0.5;
|
|
}
|
|
.sortable-ghost,
|
|
.sortable-chosen {
|
|
background-color: var(--bs-primary-bg-subtle) !important;
|
|
}
|
|
|
|
/* A table set flush against a card's edges (.card > .table-responsive — the
|
|
* operator landing's Recent activity timeline) is a scroll container: it
|
|
* clips its rows to its own square box, painting straight through the curve
|
|
* the card border draws at each corner. Give the wrapper Bootstrap's own
|
|
* inner-radius token so the clip follows the border; scoped to first/last
|
|
* child (mirroring Bootstrap's .card > .list-group treatment) so a wrapper
|
|
* sitting below a card header keeps its square top corners. */
|
|
.card > .table-responsive:first-child {
|
|
border-top-left-radius: var(--bs-card-inner-border-radius);
|
|
border-top-right-radius: var(--bs-card-inner-border-radius);
|
|
}
|
|
.card > .table-responsive:last-child {
|
|
border-bottom-left-radius: var(--bs-card-inner-border-radius);
|
|
border-bottom-right-radius: var(--bs-card-inner-border-radius);
|
|
}
|
|
|
|
/* Affected-population disclosure in preview panels (org-type default change,
|
|
* tier reorder/removal): the bucket line carries only the count; org names
|
|
* live behind a <details> and render one per row, scrolling past ~8 rows so
|
|
* hundred-org previews stay compact. */
|
|
.org-disclosure-list {
|
|
max-height: 10rem;
|
|
overflow-y: auto;
|
|
margin-top: .25rem;
|
|
padding-left: .75rem;
|
|
}
|
|
|
|
/* Page anatomy (docs/design-system.md §6): the header's text column may
|
|
* shrink so a long title wraps instead of pushing the slot off the page;
|
|
* a section summary keeps the disclosure marker beside its heading. */
|
|
.app-page-header-text {
|
|
min-width: 0;
|
|
}
|
|
.app-section-header > summary,
|
|
summary.app-section-header {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Disabled controls (design-system.md §3, design D6; spec form-conventions
|
|
* "Disabled controls carry their reason in the DOM"): the disabledControl
|
|
* part wraps its button in this span because a disabled button receives no
|
|
* pointer events of its own and so cannot show a cursor by itself. */
|
|
.disabled-control {
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
|
|
/* Row primaries (chrome-conventions "Row navigation is the primary
|
|
column's link"): the identifier cell is a th[scope=row]. Semibold sits
|
|
between body text (400) and the column headers (700) so the primary
|
|
stands out without repeating the header weight (maintainer,
|
|
2026-08-30). */
|
|
.table > :not(caption) > * > th[scope="row"] {
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Linked cards (page-anatomy "A linked card is marked like a row";
|
|
overview-consistency D3): a card that is itself a link (the overview's
|
|
tiles) is marked the way a record table's row is marked. The readout
|
|
part gives the label the link style; this gives the box the hover shade,
|
|
the same --bs-tertiary-bg token .table-hover uses on a row. No elevation
|
|
and no motion: operator-overview-polish removed those on purpose, and a
|
|
background change is neither. */
|
|
a.card:hover {
|
|
background-color: var(--bs-tertiary-bg);
|
|
}
|
|
|
|
/* The rules table (staged-rule-changes design D10) lays out on fixed
|
|
* column widths: its editing rows put controls in the Limit, Per unit and
|
|
* Reduction policy columns, and with the automatic algorithm a row that
|
|
* opens for editing or carries a refusal would move every column. The
|
|
* Resource column takes the remainder.
|
|
*
|
|
* The row that opens each kind's group is a quiet heading: the template
|
|
* sets the word small, semibold and secondary, and this rule gives it air
|
|
* above and no line below, so the word hugs the rows it heads and the
|
|
* hairline the row above already draws is the group's whole boundary,
|
|
* the rail's own grouping idiom (distance, one hairline, a heading).
|
|
* Bootstrap's table-group-divider, a 2px line in the text colour, was the
|
|
* first build's boundary and read as the heaviest ink on the page
|
|
* (maintainer, 2026-09-19: "too harsh"; round 4 of the exploration picked
|
|
* this over a tinted band, which would have put a second grey beside the
|
|
* staged row's and nearly matched the hover wash). The group row is not
|
|
* a record, so it takes no hover tint. */
|
|
.app-rules-table {
|
|
table-layout: fixed;
|
|
}
|
|
.app-rules-table .app-col-limit {
|
|
width: 14%;
|
|
}
|
|
.app-rules-table .app-col-per-unit {
|
|
width: 10%;
|
|
}
|
|
.app-rules-table .app-col-policy {
|
|
width: 16%;
|
|
}
|
|
.app-rules-table .app-col-actions {
|
|
width: 20%;
|
|
}
|
|
.app-rules-table .app-rules-group > th {
|
|
padding-top: 0.75rem;
|
|
padding-bottom: 0.25rem;
|
|
border-bottom-width: 0;
|
|
}
|
|
.app-rules-table .app-rules-group:hover > * {
|
|
--bs-table-bg-state: transparent;
|
|
}
|