Files
T
cgalo5758 f8a3478f2a Rebuild the entitlement set Rules surface as a staged batch
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.
2026-09-19 19:46:09 -05:00

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;
}