Files
member-console/internal/embeds/static/app.css
T
cgalo5758 408fa6f5a6 Add page anatomy parts and UI quality gate
- Add shared ui_*.html parts (pageHeader, sectionHeader, statusBadge,
  emptyState) parsed into every template set
- Add anatomy lint rules with a shrinking allowlist and screen-coverage
  check
- Add make screens capture harness with contact sheets and baseline diff
- Compose member and FedWiki regions server-side so pages arrive
  complete
- Rebuild Domains and Integrations on the parts as pilots
2026-08-30 04:05:31 -05:00

319 lines
11 KiB
CSS

/* ============================================================
* 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).
* - 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);
}
/* 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 dashboard"), sits last 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 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). */
@media (max-width: 991.98px) {
.app-topbar .navbar-toggler {
display: inline-block;
}
}
/* The account menu's trigger is a <button> so hx-boost never treats it as
* a link; strip the user-agent button chrome so it reads as a nav link. */
.app-topbar .nav-link[type="button"] {
background: none;
border: 0;
}
/* 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 "Operator" label never wraps. */
.app-topbar .container-fluid {
flex-wrap: nowrap;
min-width: 0;
}
.app-topbar .navbar-brand {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.app-topbar .app-surface-label {
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 .app-surface-label {
font-size: 0.875rem;
}
.app-topbar .navbar-toggler {
margin-right: 0.25rem !important;
padding: 0.25rem 0.5rem;
}
/* The trigger sits at the bar's right edge; its right padding is dead
* space at 390px, where the bar's five pieces fit with 9px to spare. */
.app-topbar .navbar-nav .nav-link {
padding-right: 0;
}
}
/* The operator lookup box is large on desktop, where it is the landing
* surface's first region; on phones the large size left room for half its
* placeholder, so it drops to the regular control size below md. */
@media (max-width: 767.98px) {
.operator-lookup.input-group-lg > .form-control,
.operator-lookup.input-group-lg > .btn {
padding: 0.375rem 0.75rem;
font-size: 1rem;
border-radius: var(--bs-border-radius);
}
}
/* 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;
}
/* Disclosure buttons show their open state (maintainer 2026-08-23: an
* opened panel's trigger must not look unpressed). Bootstrap's collapse
* plumbing maintains aria-expanded on the trigger; painting the button's
* own active palette (the .btn variant's CSS variables) flips an outline
* button to its filled look while the panel is open, for any variant. */
.btn[data-bs-toggle="collapse"][aria-expanded="true"] {
color: var(--bs-btn-active-color);
background-color: var(--bs-btn-active-bg);
border-color: var(--bs-btn-active-border-color);
}
/* 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;
}
/* 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;
}
/* 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 {
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;
}