/*
  bento-grid.css — THE PRIMER
  ===========================================================================
  Structural layer ONLY. Zero color. Zero decorative styling of any kind
  (D-TPL-SHIP-02, D-TPL-NOT-01, D-TPL-TOK-03). Typography here is SHAPE only
  (scale, weight, rhythm, family stack) — never color; color is Render-layer.

  This file's content is identical in shape and intent across every BU
  Ecosystem template that ships a visual surface. It is loaded first; pull
  every render file off and the application still works, in greyscale (the
  "is it skin-related?" diagnostic — see bu-framework skill).

  Construction metaphor vocabulary used throughout this system:
    slab, bay, panel, reveal, brick, rail, transom, mullion, soffit, gate.

  Accessibility (WCAG 2.1 AA, non-negotiable):
    - Interactive controls MUST have a minimum 44px touch target (both
      dimensions). This is noted at every interactive class below.
    - Interactive controls MUST use a maximum 5px border-radius. No pill
      buttons. Cards/panels are exempt from the radius cap.
    - Do not rely on color alone to convey state; that is a Render-layer
      concern in any case, since this file carries no color.

  A spun page must read as a structured product page in greyscale — a nav
  rail, a hero region, aligned columns, styled buttons and forms — not as
  unstyled text. See docs/srs BUP-SRS-001-S07 greyscale-proof definition.
  ===========================================================================
*/

:root {
  /* Structural scale only — no color tokens live here. Spacing/sizing scale
     is expressed in rem so it respects user font-size preferences. */
  --structure-gap-xs: 0.25rem;
  --structure-gap-sm: 0.5rem;
  --structure-gap-md: 1rem;
  --structure-gap-lg: 1.5rem;
  --structure-gap-xl: 2rem;
  --structure-gap-2xl: 3rem;

  --structure-radius-max: 5px; /* Law: interactive controls, max 5px. No pill buttons. */
  --structure-radius-panel: 8px; /* Panels/cards are exempt from the interactive cap. */
  --structure-touch-min: 44px; /* Law: 44px minimum touch target on every interactive control. */

  /* z-index stack: three layers only, kept intentionally sparse. */
  --structure-z-base: 1;
  --structure-z-rail: 10;
  --structure-z-overlay: 50;

  /* Typography SHAPE scale — sizes/weights/rhythm only, never color. System
     font stack: no Google Fonts / CDN font loads anywhere (D-TPL laws). */
  --structure-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --structure-font-family-mono: ui-monospace, SFMono-Regular, "Cascadia Mono",
    "Consolas", monospace;

  --structure-text-xs: 0.75rem;
  --structure-text-sm: 0.875rem;
  --structure-text-base: 1rem;
  --structure-text-md: 1.125rem;
  --structure-text-lg: 1.375rem;
  --structure-text-xl: 1.75rem;
  --structure-text-2xl: 2.25rem;
  --structure-text-3xl: 3rem;

  /* Form-label / helper-text step (13px). Sits deliberately BETWEEN -xs and
     -sm because a console form label is a caption, not body copy: it must read
     as subordinate to the 16px input it sits above, and must never approach a
     section heading's size (D-16 / owner 2026-08-29: "I don't know why it has
     header text in field box"). Console norm across Polaris/Carbon/Stripe. */
  --structure-text-label: 0.8125rem;

  --structure-leading-tight: 1.15;
  --structure-leading-base: 1.55;

  --structure-weight-normal: 400;
  --structure-weight-medium: 500;
  --structure-weight-semibold: 600;
  --structure-weight-bold: 700;

  /* R2 wide-layout law (owner ruling 2026-07-04): real product pages go
     edge-to-edge on desktop by default. This token now sizes the readable
     opt-in narrow column ONLY (see .bu-narrow below) — it is NOT applied
     as a default max-width anywhere in this file anymore. The previous
     default-narrow behavior is preserved solely via .adm-shell's sibling
     pattern generalized below. */
  --structure-container-max: 1200px;
  --structure-container-wide-pad: var(--structure-gap-xl);
}

*, *::before, *::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--structure-font-family);
  font-size: var(--structure-text-base);
  line-height: var(--structure-leading-base);
  font-weight: var(--structure-weight-normal);
}

/* -----------------------------------------------------------------------
   Typography rhythm — shape only (size, weight, spacing). Color is
   Render-layer (see render-*.css text-role tokens).
   ----------------------------------------------------------------------- */
/* ADMIN CONSOLE TYPE SCALE (D-16, owner 2026-08-29: "that commander's console,
   all those forms, the text is giant").
   This is the ADMIN copy of the Primer — every surface it dresses is a console
   surface (Command Console, embedded settings pages, onboarding wizard, sign-in),
   never a marketing page, so the marketing-scale headings this file shipped with
   (h1 48px / h2 36px / h3 28px) were wrong on every one of them. The scale below
   is the professional admin-console scale (Shopify Polaris / Stripe / Linear /
   Carbon): page title ~22px, section/panel title ~18px, sub-section 16px, and
   nothing below that ever exceeds body text. Hierarchy is preserved — four
   distinct steps — it is simply no longer shouted.
   NOTE: the public (BeltUp.Web.Public) and student (BeltUp.Components.Student)
   copies of this file keep the marketing scale; those surfaces DO carry hero
   type. The three copies have already diverged substantially (console shell,
   drawer, tab rails are admin-only additions) — do not "re-sync" this block. */
h1, h2, h3, h4, h5, h6 {
  margin: 0 0 var(--structure-gap-sm) 0;
  font-weight: var(--structure-weight-semibold);
  line-height: var(--structure-leading-tight);
  overflow-wrap: break-word;
}

h1 { font-size: var(--structure-text-lg); }   /* 22px — page title */
h2 { font-size: var(--structure-text-md); }   /* 18px — section / panel title */
h3 { font-size: var(--structure-text-base); } /* 16px — sub-section */
h4 { font-size: var(--structure-text-sm); }   /* 14px */
h5 { font-size: var(--structure-text-sm); }
h6 { font-size: var(--structure-text-sm); }

p {
  margin: 0 0 var(--structure-gap-md) 0;
}

p:last-child {
  margin-bottom: 0;
}

small, .bu-text-sm {
  font-size: var(--structure-text-sm);
}

ul, ol {
  margin: 0 0 var(--structure-gap-md) 0;
  padding-left: var(--structure-gap-lg);
}

a {
  font: inherit;
}

/* Form controls inherit the typeface (WT-0915-048). Browsers give <button>,
   <input>, <select> and <textarea> their own UA face unless told otherwise, so
   every admin button rendered in a different font from the labels and headings
   beside it (console Sign in measured Arial 16px next to 13px/500 and 22px/600
   system-font text). Inputs already opted in through the .bu-field / .bu-input
   rule further down; this is the element-level floor, so a bare <button>, an
   unclassed <select>, a date or file <input> and every .bu-control / .bu-btn /
   .bu-btn-secondary follow whatever face the skin sets on body. FAMILY ONLY —
   size and weight stay with each component class. Specificity (0,0,1) means any
   class rule or skin face override still wins (R-0823-01: the Primer owns
   inheritance, the skin owns the face). Zero colour (R-0829-02 / REQ-UNIFORM-006). */
button,
input,
select,
textarea,
optgroup {
  font-family: inherit;
}

/* The button inside <input type="file"> is a pseudo-element of the input, not a
   <button>, so the rule above cannot be relied on to reach it (WT-0915-048). */
input[type="file"]::file-selector-button {
  font-family: inherit;
}

/* -----------------------------------------------------------------------
   .bu-slab — the outermost structural container of a page or region.
   ----------------------------------------------------------------------- */
.bu-slab {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  width: 100%;
  position: relative;
  z-index: var(--structure-z-base);
}

/* -----------------------------------------------------------------------
   .bu-main — the page content region inside a layout's .bu-slab, sibling to
   .bu-rail. Plain block-level flow (NOT itself a grid) so that each page can
   supply its own .bu-bay-grid(s) without nesting a grid inside a grid — a
   grid-in-grid nesting silently collapses inner bays to a single 1-column
   width because the outer grid treats the inner root as one item.
   ----------------------------------------------------------------------- */
.bu-main {
  flex: 1;
  display: block;
  width: 100%;
}

/* -----------------------------------------------------------------------
   .bu-bay-1 .. .bu-bay-12 — a 12-column bento grid. Bays are the structural
   column-span units a .bu-panel occupies.
   ----------------------------------------------------------------------- */
.bu-bay-grid {
  display: grid;
  /* minmax(0, 1fr), not a bare 1fr: a bare 1fr's implicit minimum is
     auto (max-content of the child), which lets a single long unbroken
     word blow a column past its intended fractional width. minmax(0, 1fr)
     keeps every column genuinely equal-shareable. */
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--structure-gap-lg);
  padding: var(--structure-gap-lg) var(--structure-gap-xl);
  width: 100%;
  /* R2 wide-layout law (owner ruling 2026-07-04): full-width edge-to-edge
     by default, sane side padding only — no centered-column cap. Pure-text
     pages opt into a readable column via .bu-narrow (see below) instead of
     a global cap. */
  align-items: start;
}

.bu-bay-1  { grid-column: span 1; }
.bu-bay-2  { grid-column: span 2; }
.bu-bay-3  { grid-column: span 3; }
.bu-bay-4  { grid-column: span 4; }
.bu-bay-5  { grid-column: span 5; }
.bu-bay-6  { grid-column: span 6; }
.bu-bay-7  { grid-column: span 7; }
.bu-bay-8  { grid-column: span 8; }
.bu-bay-9  { grid-column: span 9; }
.bu-bay-10 { grid-column: span 10; }
.bu-bay-11 { grid-column: span 11; }
.bu-bay-12 { grid-column: span 12; }

/* Breakpoints: bays collapse to full width below tablet width so the grid
   never forces horizontal scroll on small viewports. */
@media (max-width: 768px) {
  .bu-bay-grid {
    padding: var(--structure-gap-md);
  }

  .bu-bay-1, .bu-bay-2, .bu-bay-3, .bu-bay-4, .bu-bay-5, .bu-bay-6,
  .bu-bay-7, .bu-bay-8, .bu-bay-9, .bu-bay-10, .bu-bay-11, .bu-bay-12 {
    grid-column: span 12;
  }
}

/* -----------------------------------------------------------------------
   .bu-narrow — R2 wide-layout law opt-in (owner ruling 2026-07-04). The
   Primer's default is edge-to-edge full-width (see .bu-bay-grid above);
   pure-text blocks (long-form copy, legal pages, single-column reading
   content) opt INTO a readable centered column with this class instead of
   the old global cap. Apply directly to a .bu-bay-grid, or to any block
   inside one, to constrain just that region's line length.
   ----------------------------------------------------------------------- */
.bu-narrow {
  max-width: var(--structure-container-max);
  margin-left: auto;
  margin-right: auto;
}

/* -----------------------------------------------------------------------
   .bu-panel — a structural card/region. Every .bu-panel MUST have a
   .bu-reveal as its first child (the panel's structural header/intro slot).
   Panels are exempt from the interactive border-radius cap.
   ----------------------------------------------------------------------- */
.bu-panel {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-sm);
  padding: var(--structure-gap-lg);
  border-radius: var(--structure-radius-panel);
  position: relative;
  z-index: var(--structure-z-base);
  min-width: 0; /* grid item safety: don't let content force the column wider */
  /* Appearance (background, border color, elevation) is a Render-layer
     concern — see render-*.css. This file assigns no color. */
}

.bu-panel > .bu-reveal:first-child {
  display: block;
  width: 100%;
  margin-bottom: var(--structure-gap-xs);
}

/* A long form panel carries several .bu-reveal section headers (Identity /
   Address / Contact / Description ...). Only the FIRST one had any rhythm rule,
   so every later section header sat 8px off the field above it and read as part
   of that field rather than as the start of a new group. 24px above / 8px below
   is the standard form-section step on the 8px grid, and it is what makes a
   section title legible AS a section title instead of as text stacked on the
   field box under it (D-16). */
.bu-panel > .bu-reveal:not(:first-child) {
  display: block;
  width: 100%;
  margin-top: var(--structure-gap-lg);
  margin-bottom: var(--structure-gap-xs);
}

/* -----------------------------------------------------------------------
   .bu-brick — the smallest structural building block (a list item, a
   stat tile, a single content atom within a panel).
   ----------------------------------------------------------------------- */
.bu-brick {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-xs);
  padding: var(--structure-gap-sm) 0;
}

/* -----------------------------------------------------------------------
   .bu-nag — a small, persistent, non-blocking inline warning banner
   (R-S7-15 paperwork-compliance nag; PaperworkNagBanner.razor). Structural
   shape only — small, low-profile, single line where possible; appearance
   (warning color) is a Render-layer concern, see render-*.css .bu-nag-warning.
   ----------------------------------------------------------------------- */
.bu-nag {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-xs);
  padding: var(--structure-gap-xs) var(--structure-gap-sm);
  border-radius: var(--structure-radius-panel);
  font-size: var(--structure-text-sm, 0.875rem);
}

.bu-nag-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25em;
  height: 1.25em;
  border-radius: 50%;
  font-weight: bold;
  flex-shrink: 0;
}

/* -----------------------------------------------------------------------
   .bu-metric — a compact stat/number tile (dashboards, value props).
   ----------------------------------------------------------------------- */
.bu-metric {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-xs);
  padding: var(--structure-gap-md);
  border-radius: var(--structure-radius-panel);
}

/* --- Tile rows share a bottom edge (D-0910-01) -----------------------------
   THE DEFECT, measured on the live preproduction console (Payments &
   Subscription, viewport 1718x864, 2026-09-10): the two status tiles rendered
   the same WIDTH (658px each — the R-0904-03 equal-width rule further down
   works) but different HEIGHT. Entity status ended at y=247 (77px tall),
   Stripe at y=272 (102px tall): 25px of misalignment, because the Stripe tile
   carries a third line (its subscription sub-line) and Entity status does not.

   CAUSE: .bu-bay-grid sets `align-items: start` (see the grid definition
   above). That is the correct default for BAYS — a short panel must not be
   stretched to a tall neighbour's height merely because they share a row — but
   it is exactly wrong for TILES, whose whole job is to read as one instrument
   row. Under `start` each tile sizes to its own line count, so a row of tiles
   with unequal copy loses its bottom edge.

   THE RULE: a bay grid that DIRECTLY holds tiles stretches its items to the
   row's tallest, so siblings share a bottom edge no matter how many lines of
   text each carries. This is a property of the tile row itself, not of one
   page, so every panel that lays tiles side by side gets it: the Payments
   status pair, the Dashboard KPI pair, Retention's 3-up, Sessions' counters,
   BillingOverviewPanel's 4-wide grid and the Kitchen Sink specimen.

   COMPOSES WITH, DOES NOT FIGHT, R-0904-03 (equal WIDTH, console section
   below): that rule owns `grid-template-columns` on two-child half rows only,
   this one owns `align-items` and touches no track sizing at all. Sessions'
   .bu-bay-3 counters and BillingOverviewPanel's .bu-bay-4 tiles keep the
   12-track ruler they were designed against and simply gain a common bottom
   edge within each row. A tile that is alone on its row is unaffected —
   an auto row's height is its own content — so BillingOverviewPanel's
   trailing full-width note and the Payments tab's .bu-bay-12 Stripe-account
   tile do not grow.

   `:has(> .bu-metric)` is the scope guard, and it is load-bearing: the rule
   fires ONLY on a grid whose own children are tiles. Bay grids of panels,
   School Profile's compact .bu-kv key-value rows, and plain content bays all
   keep `align-items: start` exactly as before. The Student surfaces' stat
   cells live in .bu-metric-grid, a different shape entirely, and are never
   reached by this selector. (:has() is already load-bearing in this file —
   see the empty-rail collapse and R-0904-03 below.)

   GEOMETRY ONLY — this block declares no colour; R-0829-02 still gates the
   reskin.
   ------------------------------------------------------------------------- */
.bu-bay-grid:has(> .bu-metric) {
  align-items: stretch;
}

/* D-16: this was --structure-text-2xl (36px) bold — hero scale. On a dashboard
   KPI that is merely loud; on a read-only settings value ("Not set", a school
   name, an address) it is the defect the owner reported, and on School Profile's
   overview seven of them blew the within-fold budget (R-0709-15 / R-0828-04) all
   on their own. Console law: a read-only VALUE never renders larger than the
   section title above it, so this now matches h2 (18px) exactly and no more.
   Long-form read-only values belong in .bu-kv rows (below), not in a tile. */
.bu-metric-value {
  font-size: var(--structure-text-md);
  font-weight: var(--structure-weight-semibold);
  line-height: var(--structure-leading-tight);
}

.bu-metric-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* -----------------------------------------------------------------------
   .bu-kv — compact read-only key/value rows (D-16 / R-0828-04 item 1).
   The overview-first settings surface the ruling asks for is a SUMMARY, not a
   dashboard: a dozen committed values a school owner scans in one pass and
   then clicks Edit. A tile per value (.bu-metric) spends ~90px of vertical
   budget on a 14px string, which is what forced School Profile's overview to
   scroll. These rows spend ~40px, flow into as many readable columns as the
   pane is wide, and let a long value (an address, a description) claim a full
   row via .bu-kv-wide.
   Structure only, no colour — like .bu-toolbar/.bu-empty-state this needs no
   render-file rule and so cannot cause partial-skin fallthrough (D-S07-SKIN-02).
   Muted label colour comes from composing .bu-text-muted in the markup, which
   every render file already defines.
   ----------------------------------------------------------------------- */
.bu-kv {
  display: grid;
  /* auto-fit + a readable minimum: two columns on a wide console pane, one on a
     narrow one — no media query and no bay class needed. */
  grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr));
  column-gap: var(--structure-gap-xl);
  row-gap: 0; /* rows carry their own 8px padding; see .bu-kv-row */
  margin: 0;
}

/* Label left, value right, one line each — the settings-summary row shape used
   by every console this was measured against. Baseline alignment (not centre)
   keeps the 13px label sitting on the same text baseline as the 16px value. */
.bu-kv-row {
  display: flex;
  align-items: baseline;
  gap: var(--structure-gap-md);
  padding: var(--structure-gap-sm) 0;
  min-width: 0;
}

/* A value with no natural short form (address, free-text description) takes the
   whole row instead of being squeezed into one column and wrapping four deep. */
.bu-kv-row.bu-kv-wide {
  grid-column: 1 / -1;
}

/* Scoped as `.bu-kv .bu-kv-label` (0,2,0) on purpose: the markup composes
   `.bu-kv-label .bu-text-muted` to inherit each skin's muted colour, and
   .bu-text-muted also carries a font-size and is declared later in this file —
   a bare .bu-kv-label would lose the size to it. */
.bu-kv .bu-kv-label {
  flex: 0 0 40%;
  max-width: 12rem;
  font-size: var(--structure-text-label);
  font-weight: var(--structure-weight-medium);
  line-height: 1.3;
}

.bu-kv-value {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--structure-text-base);
  font-weight: var(--structure-weight-semibold);
  line-height: 1.35;
  overflow-wrap: break-word;
}

@media (max-width: 640px) {
  /* Below a phone-ish width there is no room for two columns inside one row —
     stack, which is the same shape a form field already uses. */
  .bu-kv-row {
    flex-direction: column;
    align-items: stretch;
    gap: var(--structure-gap-xs);
  }

  .bu-kv .bu-kv-label {
    flex: 0 0 auto;
    max-width: none;
  }
}

/* -----------------------------------------------------------------------
   .bu-rail — a persistent structural strip (navigation rail, header bar,
   footer bar). Sits above base content, below overlays, in the z-stack.
   Top nav shape: brand/logo left, links right, single horizontal row.
   ----------------------------------------------------------------------- */
.bu-rail {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-md);
  padding: var(--structure-gap-sm) var(--structure-gap-xl);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--structure-z-rail);
  min-height: 70px;
  width: 100%;
}

/* .bu-slab that carries a fixed .bu-rail as its first child needs top
   padding so fixed-rail content never overlaps the page's own content —
   the rail is removed from document flow by position:fixed. Applied via
   .bu-has-fixed-rail on the .bu-slab wrapper (see MainLayout). Harvested
   from the reference layout authority. */
.bu-slab.bu-has-fixed-rail {
  padding-top: 70px;
}

/* Narrow viewports: .bu-nav wraps to a second/third line below 768px, so the
   fixed rail grows well past the 70px reserved above (measured 123px at 735px
   wide, 2026-08-21) and silently overlaps the top of page content. Reserving a
   taller fixed strip cannot work — the wrapped height depends on content — so
   below the wrap breakpoint the rail returns to normal flow and occupies its
   own real height. Structure only. */
@media (max-width: 768px) {
  .bu-rail {
    position: static;
  }

  .bu-slab.bu-has-fixed-rail {
    padding-top: 0;
  }
}

/* -----------------------------------------------------------------------
   .bu-gate — an overlay/modal structural surface. Sits at the top of the
   z-stack, above rails and base content.
   ----------------------------------------------------------------------- */
.bu-gate {
  position: fixed;
  inset: 0;
  z-index: var(--structure-z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--structure-gap-md);
}

/* GATE SURFACES — sign-in, step-up verify, and onboarding's invalid-link and
   identity-mismatch states are ONE pattern. These three rules were authored
   verbatim in three scoped .razor.css files (a gate surface cannot share scoped
   CSS with its siblings); they live here once so the pattern cannot drift apart
   again, which is how the three surfaces diverged in the first place
   (WT-0915-029, WT-0915-046). Structure only, no colour (R-0829-02 /
   REQ-UNIFORM-006). */

/* WT-0915-046 — gate pages scrolled ~114px with nothing below. A page root is a
   nested .bu-slab (min-height:100vh) inside MainLayout's own .bu-slab (also
   100vh, plus the 70px rail padding and the 44px footer), so 100vh + 70 + 44 =
   1014px in a 900px viewport. A gate's only child is position:fixed and out of
   flow, so the nested slab has nothing to size to. Opting it out lets the
   layout's slab own exactly one viewport. The console shell is untouched: the
   R-0709-15 cap is scoped to :has(.bu-console-body) on MainLayout's root. */
.bu-fit-slab {
  min-height: 0;
}

/* WCAG 2.1 SC 1.4.10 (reflow): .bu-gate is position:fixed, so a card taller than
   the viewport — an error banner on a small phone, or 400% zoom — was clipped
   with no way to reach it. Scroll inside the overlay instead. */
.bu-gate {
  overflow-y: auto;
}

/* WT-0915-046 — the card was a fixed 265px at EVERY width (measured 265x305 at
   1440 and the same 265px at 375, inputs 215px). It is a flex item of .bu-gate,
   and .bu-bay-4 is `grid-column: span 4`, which means nothing in a flex row, so
   the card shrink-wrapped its inputs' intrinsic width. Full width inside the
   gate's gutters, capped for readability: fields land near 366px on desktop and
   fill the 16px gutters on a 375px phone. margin:auto centres it AND keeps it
   reachable when it overflows — auto margins collapse to zero rather than
   pushing the top out of the scroll area, which align-items:center alone does.
   A modal surface, not a page column: the wide-layout law governs page bays. */
.bu-gate-card {
  width: 100%;
  max-width: 26rem;
  margin: auto;
  overflow-wrap: break-word;
}

/* -----------------------------------------------------------------------
   .bu-transom / .bu-mullion / .bu-soffit — structural dividers.
   Transom: a horizontal structural divider (above a section).
   Mullion: a vertical structural divider (between columns).
   Soffit: a structural footer/underside region of a panel or slab.
   ----------------------------------------------------------------------- */
.bu-transom {
  width: 100%;
  height: 1px;
  border: none;
  margin: var(--structure-gap-md) 0;
}

.bu-mullion {
  width: 1px;
  align-self: stretch;
  margin: 0 var(--structure-gap-md);
}

.bu-soffit {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  min-height: var(--structure-touch-min);
}

/* -----------------------------------------------------------------------
   Navigation — brand left, link row right, horizontal single line.
   Structural only; render supplies color/hover/active appearance.
   ----------------------------------------------------------------------- */
.bu-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  gap: var(--structure-gap-md);
}

.bu-nav-brand {
  display: inline-flex;
  align-items: center;
  font-size: var(--structure-text-md);
  font-weight: var(--structure-weight-bold);
  text-decoration: none;
  min-height: var(--structure-touch-min);
  letter-spacing: 0.02em;
}

/* Product-name suffix in the brand lockup: BELT|UP is the mark, "ONLINE" names
   the product (the company is BeltUp LLC; the app is BeltUp Online — owner
   ruling 2026-08-21). Deliberately a <small>, NOT a <span>, so the render
   layer's `.bu-nav-brand span` accent rule does not capture it — the suffix
   inherits .bu-nav-brand's own text colour in every skin, which is why it needs
   no per-skin colour rule and cannot cause partial-skin fallthrough
   (D-S07-SKIN-02). Structure only — no colour here. */
.bu-nav-brand-suffix {
  font-size: 0.62em;
  letter-spacing: 0.22em;
  margin-left: 0.5em;
  align-self: center;
}

/* .bu-nav-links sits in the CENTER of the rail (brand left, actions right) —
   flex:1 plus centered content lets it claim the middle third without a
   third explicit grid column. Harvested from the reference layout authority
   (fixed header: logo left / nav center / CTA right). */
.bu-nav-links {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: center;
  gap: var(--structure-gap-lg);
  list-style: none;
  margin: 0;
  padding: 0;
}

.bu-nav-links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--structure-touch-min);
  text-decoration: none;
  font-size: var(--structure-text-sm);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.bu-nav-actions {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-sm);
}

/* #533: below 480px the header actions (invite, mat, door screen, sign out) take their own full row and wrap, so the page never scrolls sideways at 390. */
@media (max-width: 480px) {
  .bu-nav-actions {
    flex: 1 1 100%;
    flex-wrap: wrap;
    min-width: 0;
  }
}

@media (max-width: 768px) {
  .bu-nav {
    flex-wrap: wrap;
  }

  .bu-nav-links {
    order: 3;
    flex-basis: 100%;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: var(--structure-gap-md);
  }
}

/* -----------------------------------------------------------------------
   Hero region — eyebrow label, headline, lede, action row. Structural
   shape harvested from the reference layout authority's hero slab.
   ----------------------------------------------------------------------- */
.bu-hero {
  text-align: center;
  padding: var(--structure-gap-2xl) var(--structure-gap-xl);
}

.bu-hero-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.3em;
  margin-bottom: var(--structure-gap-lg);
}

.bu-hero-headline {
  font-size: clamp(2rem, 5vw, 3.75rem);
  font-weight: var(--structure-weight-bold);
  line-height: 1.05;
  letter-spacing: 0.01em;
  margin: 0 auto var(--structure-gap-lg);
  max-width: 18ch;
}

.bu-hero-lede {
  max-width: 60ch;
  margin: 0 auto var(--structure-gap-xl);
  font-size: var(--structure-text-md);
  line-height: var(--structure-leading-base);
}

.bu-hero-actions {
  display: flex;
  gap: var(--structure-gap-md);
  justify-content: center;
  flex-wrap: wrap;
}

/* -----------------------------------------------------------------------
   Interactive controls — structural rules only. Colors are Render-layer.
   Law: max 5px border-radius (no pill buttons); 44px minimum touch target.
   ----------------------------------------------------------------------- */
.bu-control {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--structure-touch-min);
  min-width: var(--structure-touch-min);
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  font-size: var(--structure-text-base);
  font-weight: var(--structure-weight-medium);
  text-decoration: none;
  line-height: 1;
  cursor: pointer;
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease;
}

.bu-control:disabled,
.bu-control[disabled] {
  cursor: not-allowed;
}

/* EB-04 (2026-10-10): the native "Choose File" button inside a file input is a
   control too, but it kept the browser's own shape — 23px tall, square corners,
   a 2px outset border — inside the report panel on every admin page. Same shape
   as .bu-control (44px, 5px radius, 1px solid border, medium weight); the skin
   supplies its face, so this rule carries no colour. */
input[type="file"]::file-selector-button {
  min-height: var(--structure-touch-min);
  margin-right: var(--structure-gap-sm);
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  font-family: inherit;
  font-size: var(--structure-text-sm);
  font-weight: var(--structure-weight-medium);
  cursor: pointer;
}

/* -----------------------------------------------------------------------
   .bu-btn / .bu-btn-secondary — inline-block button shape (BENTO-CORE
   inventory: primary solid, secondary bordered-transparent). Built on top
   of .bu-control's touch-target/radius law. Structural only.
   ----------------------------------------------------------------------- */
.bu-btn,
.bu-btn-secondary {
  /* WT-0915-047 — these rendered 46px beside 44px .bu-control siblings in the same
     row. The line-height below used to be computed as (touch-min - padding - 2px),
     which silently assumes a 1px border; cobalt-dark draws .bu-btn with 2px top and
     bottom rails (render-cobalt-dark.css), so the box came out 26 + 16 + 4 = 46px.
     Centring the label in a flex box instead of computing a line box makes the
     height skin-independent: 20 + 16 + 4 = 40px, under the 44px floor below, so
     every variant lands on exactly 44 whatever rails a skin draws. Structure only;
     the rails themselves are the skin's business (R-0829-02 / REQ-UNIFORM-006). */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto; /* inside a flex CTA row: hug content width, never stretch */
  width: fit-content;
  min-height: var(--structure-touch-min);
  /* SRSV-06-17: the 44px law is BOTH dimensions; .bu-control declares both, the
     button family declared height only. Cannot reflow any CTA row — the 2 x 24px
     padding plus the 1px borders already make every labelled button wider than
     44px; this only makes the floor a declared guarantee instead of an accident
     of padding. */
  min-width: var(--structure-touch-min);
  padding: var(--structure-gap-sm) var(--structure-gap-lg);
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  font-size: var(--structure-text-base);
  font-weight: var(--structure-weight-medium);
  text-align: center;
  text-decoration: none;
  line-height: 1.25;
  cursor: pointer;
  transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease;
}

.bu-btn:disabled,
.bu-btn-secondary:disabled {
  cursor: not-allowed;
}

/* -----------------------------------------------------------------------
   Form controls — full-width fields, stacked vertical rhythm (BENTO-CORE
   inventory: full-width fields, 0.5rem stacked gap).
   ----------------------------------------------------------------------- */
.bu-field {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-xs);
  margin-bottom: var(--structure-gap-md);
}

/* A form label is a caption above its input: 13px, medium, muted by the render
   layer — never a heading, never larger than the value it labels (D-16, owner
   2026-08-29: "I don't know why it has header text in field box"). */
.bu-field label {
  font-size: var(--structure-text-label);
  font-weight: var(--structure-weight-medium);
  line-height: 1.3;
}

/* Guard, not decoration: if any field group ever gets a heading ELEMENT used as
   its label, it still renders at label scale rather than as a section title
   sitting on top of the field box. The correct repair is to demote that element
   to a real <label for=...> in the markup (WCAG 2.1 SC 1.3.1/2.4.6, and a
   heading in the rotor that is really a field name is a screen-reader defect) —
   this rule only stops it LOOKING like a heading in the meantime. */
.bu-field h1,
.bu-field h2,
.bu-field h3,
.bu-field h4,
.bu-field h5,
.bu-field h6 {
  font-size: var(--structure-text-label);
  font-weight: var(--structure-weight-medium);
  line-height: 1.3;
  margin: 0;
}

/* Form help / hint line — the muted sentence under a field's input or checkbox
   (SchoolProfile tax-id / public-address / handle help, ThemeSettings motion
   help). The admin has no dedicated help class: every page composes the existing
   `.bu-text-sm .bu-text-muted` pair as a DIRECT child of .bu-field, so this binds
   to that class by position instead of inventing a new one. It had no structural
   rule, so its size and weight were whatever each span inherited (the 2026-09-02
   deployed run recorded console form help at 11.16px / weight 700: smaller than
   the label above it and bolder than the value). Now one fixed step BELOW the
   13px / 500 label (D-16): 12px, regular weight, base leading so a wrapped
   sentence stays readable — never larger or bolder than the label. Specificity
   (0,2,1) beats .bu-text-sm and .bu-text-muted (each (0,1,0)) whatever their
   order, so no page needs an override. :not(label) keeps a muted LABEL placed
   directly in a field at label scale (13px, rule above) instead of dropping it to
   help scale. Colour stays with .bu-text-muted in the render layer
   (R-0829-02 / REQ-UNIFORM-006). */
.bu-field > .bu-text-muted:not(label) {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-normal);
  line-height: var(--structure-leading-base);
}

.bu-field input[type="text"],
.bu-field input[type="email"],
.bu-field input[type="tel"],
.bu-field input[type="password"],
.bu-field input[type="number"],
.bu-field select,
.bu-field textarea,
input.bu-input,
select.bu-input,
textarea.bu-input {
  width: 100%;
  min-height: var(--structure-touch-min);
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  font-family: inherit;
  font-size: var(--structure-text-base);
}

.bu-field-row {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-sm);
  min-height: var(--structure-touch-min);
}

.bu-field-row input[type="checkbox"],
.bu-field-row input[type="radio"] {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

/* The row above already guarantees a 44px band; its LABEL did not, so the
   clickable sentence was only as tall as its own text (~25px) while the sibling
   idiom `label.bu-check-label` (further down) gives a full 44px clickable unit.
   Two checkbox idioms with different hit areas met on the same consent step
   (Onboarding.razor step 1, Sessions.razor). This fills the band the label is
   already centred in: same look, same target as its sibling. A long consent
   sentence still wraps and grows past the floor. Structural only, no colour
   (R-0829-02 / REQ-UNIFORM-006). */
.bu-field-row > label {
  display: flex;
  align-items: center;
  min-height: var(--structure-touch-min);
}

.bu-field-error {
  font-size: var(--structure-text-sm);
  margin: 0;
}

/* -----------------------------------------------------------------------
   .bu-form-actions — a form's submit/confirm row.
   A .bu-panel is display:flex/column, so a button placed directly inside one
   becomes a full-width flex item unless it hugs its own content. .bu-btn does
   (width: fit-content), .bu-control does NOT — which is why a confirm step
   rendered a hugging "Yes" stacked over a stretched full-width "Cancel". A
   submit row is a row: one line, left-aligned, 16px between controls, on the
   8px grid, both controls hugging. Structure only; no colour, so no render
   rule is needed and no partial-skin fallthrough is possible (D-S07-SKIN-02).
   ----------------------------------------------------------------------- */
.bu-form-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--structure-gap-md);
  margin-top: var(--structure-gap-lg);
}

.bu-form-actions > .bu-control,
.bu-form-actions > .bu-btn,
.bu-form-actions > .bu-btn-secondary {
  flex: 0 0 auto;
  width: auto;
}

/* =========================================================================
   HARVESTED COMPONENT SHAPES — ported from the owner's approved reference
   skeleton (BeltUp Online legacy layout authority, bu-skin-skeleton.css),
   structure only, every class/token renamed to current law on the way in.
   Colors for every class below live exclusively in render-*.css.
   ========================================================================= */

/* --- Muted/small text role (color assigned in render) -------------------- */
.bu-text-muted {
  font-size: 0.85rem;
}

/* --- Section / panel heading shapes -------------------------------------- */
.bu-section-heading {
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin-bottom: var(--structure-gap-lg);
}

.bu-panel-title {
  font-size: 0.9rem;
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin-bottom: 1.25rem;
}

.bu-panel-sublabel {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 4px;
  text-transform: uppercase;
  margin-bottom: var(--structure-gap-sm);
}

.bu-panel-headline {
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin-bottom: var(--structure-gap-xl);
}

.bu-panel-footnote {
  margin-top: var(--structure-gap-md);
  font-size: 0.85rem;
  letter-spacing: 1px;
  text-transform: uppercase;
}

.bu-command-header {
  text-align: center;
  margin-bottom: var(--structure-gap-sm);
}

.bu-command-label {
  font-size: 0.8rem;
  font-weight: var(--structure-weight-bold);
  letter-spacing: 8px;
  text-transform: uppercase;
  margin-bottom: 0.75rem;
}

.bu-command-title {
  font-size: clamp(2rem, 5vw, 3.5rem);
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin: 0;
}

/* --- Circular gauge (SVG stroke-dash percentage indicator) ---------------- */
.bu-gauge {
  position: relative;
  width: 100px;
  height: 100px;
}

.bu-gauge svg {
  width: 100%;
  height: 100%;
  display: block;
}

.bu-gauge-value {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: var(--structure-weight-bold);
  font-size: var(--structure-text-base);
}

.bu-gauge-row {
  display: flex;
  gap: var(--structure-gap-lg);
  justify-content: center;
  flex-wrap: wrap;
}

.bu-gauge-node {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--structure-gap-sm);
}

.bu-gauge-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 2px;
  text-transform: uppercase;
}

/* --- 75/25 progression bar (relative track, absolute fill) ---------------- */
.bu-skill-row {
  margin-bottom: var(--structure-gap-md);
}

.bu-skill-row:last-child {
  margin-bottom: 0;
}

.bu-status-labels {
  display: flex;
  justify-content: space-between;
  width: 100%;
  font-size: 0.85rem;
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  margin-bottom: 0.35rem;
}

.bu-progress-track {
  position: relative;
  height: 12px;
  border-radius: 4px;
  width: 100%;
  overflow: hidden;
}

.bu-track-hard {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
}

/* --- Media panel (thumbnail left, content right) --------------------------- */
.bu-media-panel {
  flex-direction: row;
  gap: var(--structure-gap-xl);
  align-items: center;
  flex-wrap: wrap;
}

.bu-media-frame {
  width: 180px;
  height: 110px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.bu-play-circle {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.bu-play-icon {
  width: 22px;
  height: 22px;
}

.bu-media-content {
  flex: 1;
  min-width: 240px;
}

.bu-media-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 4px;
  text-transform: uppercase;
  margin-bottom: var(--structure-gap-sm);
}

.bu-media-title {
  font-size: 1.25rem;
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 1px;
  margin-bottom: var(--structure-gap-sm);
}

.bu-media-lede {
  margin-bottom: var(--structure-gap-md);
  line-height: 1.5;
}

.bu-badge-tag {
  display: inline-block;
  padding: 0.4rem 0.8rem;
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 1px;
  text-transform: uppercase;
}

/* --- Metric grid (three-up stat cells with center dividers) ---------------- */
.bu-metric-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  align-items: center;
  justify-items: center;
  text-align: center;
}

.bu-metric-cell {
  padding: var(--structure-gap-lg) var(--structure-gap-md);
  text-align: center;
}

.bu-metric-cell .bu-metric-value {
  font-size: 3.5rem;
  line-height: 1;
}

@media (max-width: 720px) {
  .bu-metric-grid {
    grid-template-columns: 1fr;
  }
}

/* --- Pillar panels (label / title / body) ---------------------------------- */
.bu-pillar-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 4px;
  text-transform: uppercase;
  margin-bottom: 0.75rem;
}

.bu-pillar-title {
  font-size: clamp(1.25rem, 2vw, 1.75rem);
  font-weight: var(--structure-weight-bold);
  line-height: 1.15;
  margin-bottom: var(--structure-gap-md);
}

.bu-pillar-body {
  line-height: 1.6;
}

/* --- Corner panels (label / title / body / bottom-anchored actions) --------- */
.bu-corner-panel {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.bu-corner-label {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 4px;
  text-transform: uppercase;
}

.bu-corner-title {
  font-size: 1.75rem;
  font-weight: var(--structure-weight-bold);
  text-transform: uppercase;
  letter-spacing: 2px;
  margin: 0;
}

.bu-corner-body {
  line-height: 1.6;
  margin: 0;
}

.bu-corner-actions {
  margin-top: auto;
  padding-top: var(--structure-gap-md);
}

/* --- Slat (thin horizontal work-item strip: level | info | action) --------- */
.bu-slat {
  display: grid;
  grid-template-columns: 80px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--structure-gap-md);
  padding: 0.75rem var(--structure-gap-md);
}

.bu-slat-level {
  font-weight: var(--structure-weight-bold);
  font-size: 0.95rem;
  letter-spacing: 1px;
  text-align: center;
}

.bu-slat-info h4 {
  text-transform: uppercase;
  font-size: 0.9rem;
  letter-spacing: 1px;
  margin: 0 0 0.35rem 0;
}

.bu-slat-info p {
  margin: 0 0 0.4rem 0;
  font-size: var(--structure-text-xs);
  letter-spacing: 0.5px;
}

.bu-slat .bu-progress-track {
  height: 4px;
}

.bu-slat-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

/* --- Status dot (pulse indicator) ------------------------------------------- */
.bu-status-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  display: inline-block;
}

@keyframes bu-alert-pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.35); opacity: 0.6; }
}

.bu-status-dot.is-alert {
  animation: bu-alert-pulse 1.2s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .bu-status-dot.is-alert {
    animation: none;
  }
}

/* --- Retention heat map (GROUP 8 Command Center panel) ----------------------
   Structural shape only — glow-ring color per state (healthy/warning/error/
   neutral) is a Render-layer concern, see render-*.css .bu-status-dot.is-*.
   .is-error re-uses the same pulse animation as .is-alert above (critical/
   high churn risk deserves the same attention-grabbing treatment). ------- */
.bu-status-dot.is-error {
  animation: bu-alert-pulse 1.2s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .bu-status-dot.is-error {
    animation: none;
  }
}

.bu-heatmap-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--structure-gap-sm);
  padding: 0;
  list-style: none;
}

.bu-heatmap-cell {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-xs);
  padding: var(--structure-gap-xs) var(--structure-gap-sm);
  border-radius: var(--structure-radius-panel);
  font-size: var(--structure-text-sm, 0.875rem);
}

/* --- Stealth link (uppercase, quiet) ----------------------------------------- */
.bu-link-stealth {
  text-decoration: none;
  font-weight: var(--structure-weight-bold);
  font-size: 0.8rem;
  text-transform: uppercase;
}

/* --- Global footer (brand column + link columns + copyright bar) ------------- */
.bu-footer {
  margin-top: var(--structure-gap-2xl);
  padding: var(--structure-gap-2xl) var(--structure-gap-xl) 0;
  position: relative;
  z-index: var(--structure-z-rail);
}

.bu-footer-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: var(--structure-gap-lg);
  max-width: var(--structure-container-max);
  margin: 0 auto;
}

.bu-footer-brand {
  grid-column: span 6;
}

.bu-footer-links {
  grid-column: span 2;
}

.bu-footer-tagline {
  margin-top: 0.75rem;
  max-width: 32ch;
  line-height: 1.5;
}

.bu-footer-heading {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 2px;
  text-transform: uppercase;
  margin-bottom: 0.75rem;
}

.bu-footer-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-sm);
}

.bu-footer-list a {
  font-size: 0.85rem;
}

.bu-footer-copyright {
  max-width: var(--structure-container-max);
  margin: var(--structure-gap-xl) auto 0;
  padding: 1.25rem 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--structure-text-xs);
}

.bu-footer-lab-link {
  opacity: 0.5;
  text-decoration: none;
  font-size: var(--structure-text-xs);
}

@media (max-width: 900px) {
  .bu-footer-brand {
    grid-column: span 12;
  }

  .bu-footer-links {
    grid-column: span 4;
  }
}

@media (max-width: 600px) {
  .bu-footer-links {
    grid-column: span 12;
  }
}

/* =========================================================================
   KITCHEN SINK COMPONENT SHAPES — restored per owner order 2026-07-03
   (gallery realignment to the owner's approved reference gallery). Structure
   only, per D-S07-FW-01; colors for every class below live exclusively in
   render-*.css. These are genuine Primer components (badges, alerts,
   roster tables, a tactical SVG node) with no prior equivalent in this
   file — none of D-S07's directives forbid them; S07 governs appearance/
   structure only, not vocabulary (Language_Map's domain, out of scope here).
   ========================================================================= */

/* --- Badge / tag (inline status chip). Border-radius stays under the
   5px interactive-control cap even though a badge is not itself
   interactive, for shape consistency with the rest of the Primer. -------- */
.bu-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.6rem;
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 0.5px;
  text-transform: uppercase;
  line-height: 1.2;
}

/* --- System alert (notice / warning banners). Reuses the .bu-nag shape's
   flex/padding rhythm but sized for block-level page content. ------------ */
.bu-alert {
  display: block;
  padding: var(--structure-gap-sm) var(--structure-gap-md);
  border-radius: var(--structure-radius-panel);
  border-width: 1px;
  border-style: solid;
  border-left-width: 4px;
  font-size: var(--structure-text-sm, 0.875rem);
  line-height: 1.5;
  margin-bottom: var(--structure-gap-md);
}

.bu-alert:last-child {
  margin-bottom: 0;
}

/* --- Checkbox/radio labelled rows. Builds on .bu-field-row's touch-target
   law; a bu-check-label wraps the input + its own text as one clickable
   unit (larger hit target, still >= 44px min via the row's min-height). -- */
/* Selector is `label.bu-check-label` (type + class), not `.bu-check-label`, so
   this size survives inside a .bu-field: `.bu-field label` is (0,1,1) and would
   otherwise out-specify a bare class and drag a checkbox's own sentence down to
   caption size. A checkbox label is the control's TEXT, not a field caption —
   it stays at body-adjacent 14px, which is what this rule always intended. */
label.bu-check-label,
.bu-check-label {
  display: inline-flex;
  align-items: center;
  gap: var(--structure-gap-sm);
  min-height: var(--structure-touch-min);
  cursor: pointer;
  font-size: var(--structure-text-sm, 0.875rem);
}

.bu-check-label input[type="checkbox"],
.bu-check-label input[type="radio"] {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

.bu-input-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--structure-gap-lg);
  margin-bottom: var(--structure-gap-sm);
}

.bu-input-row:last-child {
  margin-bottom: 0;
}

/* --- Roster-style data table (Primer shape; column widths content-driven,
   colors/borders assigned in render-*.css). ------------------------------ */
.bu-table-wrapper {
  overflow-x: auto;
}

.bu-table {
  width: 100%;
  border-collapse: collapse;
}

.bu-table th,
.bu-table td {
  text-align: left;
  padding: 0.75rem;
  border-bottom-width: 1px;
  border-bottom-style: solid;
}

.bu-table th {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 1px;
  text-transform: uppercase;
}

/* --- SVG tactical node (octagon frame; a structural shape sample, not a
   domain icon — the same construction-metaphor "frame" idea as .bu-hero,
   expressed as an outlined polygon). -------------------------------------- */
.bu-node-svg {
  width: 100px;
  height: 100px;
}

.bu-node-svg polygon {
  fill: none;
  stroke-width: 3px;
}

/* --- Gauge lab layout (three-up variant row; gauges themselves reuse the
   existing .bu-gauge/.bu-gauge-node Primer shapes already defined above). - */
.bu-gauge-lab-row {
  display: flex;
  gap: var(--structure-gap-lg);
  flex-wrap: wrap;
  justify-content: center;
}

.bu-gauge-lab-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--structure-gap-sm);
  flex: 1 1 160px;
}

/* -----------------------------------------------------------------------
   Greyscale fallback canvas — structural surface reference only. These
   two values referenced here by name only (canvas, surface) are the grounded baseline
   documented for the greyscale-skeleton skin. They are declared as tokens
   in render-greyscale-skeleton.css, NOT as literals here — this comment
   exists only to document intent; this file contains zero color literals.
   ----------------------------------------------------------------------- */

/* =========================================================================
   COMMAND CONSOLE BENTO PASS (BUP-UI-CONSOLE-VISION-001) — universal header
   row, bridge bento row, tab rails + slide-out well. Structure only; color
   lives in render-*.css. Every interactive control below reuses .bu-control's
   touch-target/radius law (D-S07-UX-04: no pill buttons on tabs/close btns).
   ========================================================================= */

/* --- Universal header row (NOT a giant heading) --------------------------
   Left: entity name + status dot. Right: two-tone wordmark. Single row,
   sits above the bridge bento row. Reuses .bu-rail's fixed-strip shape but
   is its own class so the header can live in normal flow (top of .bu-main)
   without double-reserving fixed-rail space already claimed by MainLayout's
   nav bu-rail. */
.bu-console-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-md);
  width: 100%;
  /* R2 wide-layout law: full-width by default, no centered-column cap. */
  padding: var(--structure-gap-md) var(--structure-gap-xl);
  min-height: var(--structure-touch-min);
}

.bu-console-header-entity {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-sm);
  min-width: 0;
  font-weight: var(--structure-weight-bold);
  font-size: var(--structure-text-md);
}

.bu-console-header-entity span.bu-text-sm {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 768px) {
  .bu-console-header {
    flex-wrap: wrap;
    padding: var(--structure-gap-sm) var(--structure-gap-md);
  }
}

/* --- Bridge bento row (always-visible, glanceable) ------------------------
   Small stat chips: status / payments / roster / retention headline chip.
   Reuses .bu-bay-grid + .bu-metric; no new grid shape needed. The retention
   entry renders as a CHIP only (status dot + label) — never a numeric score,
   see .bu-bridge-chip below, consumed by Home.razor's RetentionPanelLogic
   .DecideChipVariant output exclusively. */
.bu-bridge-chip {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-xs);
  min-height: var(--structure-touch-min);
}

/* --- Tab rails (left + right verticals flanking the slide-out well) ------- */
.bu-console-body {
  display: grid;
  /* Rails are BOUNDED, never `auto`. With `auto` side tracks the free space is
     handed to the rails whenever the centre pane is empty or the rails grow,
     and the centre's minmax(0, 1fr) is free to collapse to zero — measured on
     QA 2026-08-21. A capped rail keeps the centre pane dominant in every
     state, which is the whole point of a wide-short console.

     NOTE: this 3-track shape is the shared default for a rail | centre | rail
     consumer. Home.razor's console actually has FIVE children (a .bu-mullion
     divider sits between each rail and the centre) and overrides this with its
     own 5-track version in Home.razor.css — keep the two in step.

     GUTTER (R-0904-03): the shared shape here already yields ONE gutter between
     rail and centre — this `gap`. The 5-track version cannot use `gap` for that
     (a gap would land on BOTH sides of its mullion track, which is how the 81px
     dead band the owner photographed on 2026-09-04 was built), so it sets
     column-gap:0 and carries the same --structure-gap-lg as explicit gutter
     TRACKS instead. Both shapes therefore put exactly 24px between the rail and
     the pane; change one value and change the other. */
  grid-template-columns: minmax(0, 14rem) minmax(0, 1fr) minmax(0, 14rem);
  gap: var(--structure-gap-lg);
  width: 100%;
  /* R2 wide-layout law: full-width by default, no centered-column cap. */
  padding: 0 var(--structure-gap-xl) var(--structure-gap-xl);
  align-items: start;
}

.bu-tab-rail {
  display: flex;
  flex-direction: column;
  gap: var(--structure-gap-sm);
  position: sticky;
  top: var(--structure-gap-lg); /* Home owns its header in normal flow (@layout ConsoleLayout, no MainLayout fixed rail) */
  z-index: var(--structure-z-rail);
}

/* --- Collapsed "Coming soon" stub group (R-0709-14b) -----------------------
   Native <details>/<summary> disclosure, not a new class family: the
   <summary> reuses .bu-control/.bu-tab-btn for the same button shape as the
   LIVE tabs above it, and the stub buttons inside stack the same way the
   live tabs do (see .bu-tab-btn width:100% below). Kept as a plain block so
   it participates in .bu-tab-rail's flex-column gap like any other child. */
/* NOTE: do NOT put `display: flex` (or grid) on the <details> itself. Replacing a
   details' default `display: block` overrides the UA's closed-state slot handling,
   so the collapsed group's buttons keep rendering and pile up on top of the
   summary. Spacing is applied to the children instead, which is safe in both
   states. (Trap found 2026-08-22 while chasing overlapping rail buttons.) */
.bu-tab-rail-stubs {
  display: block;
}

.bu-tab-rail-stubs > .bu-tab-btn {
  margin-top: var(--structure-gap-sm);
}

/* A closed <details> hides its non-summary children through the UA stylesheet —
   but ANY author `display` on those children overrides that and they render
   anyway. Every stub button carries .bu-control, which is `display: inline-flex`,
   so this group never actually collapsed: on desktop the rail's own
   `overflow-y: auto` scrolled the escapees out of sight and hid the problem,
   while on a phone (overflow visible) the left rail's buttons ran 400px past its
   own box and landed on top of the right rail. Re-assert the hiding explicitly.
   Found 2026-08-22 by measuring rail vs button geometry: rail ended at y=1625,
   its buttons continued to y=2041. */
.bu-tab-rail-stubs:not([open]) > *:not(summary) {
  display: none;
}


.bu-tab-rail-stubs > summary {
  cursor: pointer;
  list-style: none;
}

.bu-tab-rail-stubs > summary::-webkit-details-marker {
  display: none;
}

.bu-tab-btn {
  width: 100%;
  min-width: 120px;
  justify-content: flex-start;
  text-align: left;
  white-space: nowrap;
}

@media (max-width: 900px) {
  .bu-console-body {
    grid-template-columns: 1fr;
    padding: 0 var(--structure-gap-md) var(--structure-gap-lg);
  }

  .bu-tab-rail {
    flex-direction: row;
    flex-wrap: wrap;
    position: static;
    order: 2;
  }

  .bu-tab-rail-left { order: 2; }
  .bu-tab-rail-right { order: 3; }

  .bu-tab-btn {
    width: auto;
    min-width: var(--structure-touch-min);
  }
}

/* --- Empty-rail collapse + embedded-page inset (D-16) -----------------------
   THE DEFECT (2026-08-29 live walkthrough, D-16): "content panel is offset-left
   with a large empty right gutter (right rail only sometimes populated)". Two
   structural causes, both repaired here.

   1) DEAD GUTTER. Home.razor's RightTabs returns an empty list for most left-rail
      tabs, but TabRail still renders its <nav class="bu-tab-rail"> shell either
      way, so the right rail track kept its full 14rem whether or not it held a
      single button — 224px of reserved nothing plus its divider, pushing the
      content pane visibly left of centre on a wide desktop (wide-desktop law).
      Collapse the empty rail AND the divider that introduces it, and let the
      centre span the freed tracks. The test is "this rail holds no .bu-tab-btn"
      (`:not(:has(...))`), NOT `:empty` — :empty's handling of whitespace text
      nodes is not something to bet a layout on, and .bu-tab-btn is carried by
      every rail child that counts, live tabs and the "Coming soon" <summary>
      alike. Note :has() may not be nested INSIDE another :has() (the whole
      selector would be dropped as invalid); :has() inside :not() as used here
      is valid, and :has() is already load-bearing in this codebase
      (.bu-drawer-host below; MainLayout.razor.css's console shell cap).

      SPECIFICITY NOTE: Home.razor.css legitimately overrides .bu-console-body
      with its own FIVE-track version for the console page, and a Blazor scoped
      rule carries an extra [b-xxxxx] attribute (0,2,0). The selectors below
      reach (0,3,0), so the collapse still lands there while that five-track rule
      keeps owning the populated case it was written for. Whoever next edits
      Home.razor.css should fold this collapse into that rule and delete these
      three — until then the two must stay in step, exactly as that file's own
      "keep the two in step" note already requires.

   2) TRIPLE INSET. A routed page embedded in the console well already sits
      inside .bu-console-body's 32px padding and .bu-slideout-content's 24px
      panel padding before its own .bu-bay-grid adds a third 32px — 88px of
      gutter per side before a single character, on top of the dead rail. An
      embedded page keeps its vertical rhythm and drops the horizontal padding
      it no longer owns; standalone visits to /school-profile, /sessions,
      /curriculum, /theme-settings, /regulatory are untouched (no wrapper).
   ------------------------------------------------------------------------- */
.bu-console-body:not(:has(> .bu-tab-rail-right .bu-tab-btn)) > .bu-tab-rail-right {
  display: none;
}

.bu-console-body:not(:has(> .bu-tab-rail-right .bu-tab-btn)) > .bu-console-center ~ .bu-mullion {
  display: none;
}

/* MEDIA GUARD IS LOAD-BEARING: both this file (900px) and Home.razor.css (768px)
   collapse .bu-console-body to a SINGLE column on narrow viewports, where a
   `grid-column: 3 / -1` would place the pane on a line that does not exist and
   the browser would invent implicit columns for it — a broken phone layout, not
   a wider pane. These two rules only ever run where the multi-track shell is
   genuinely in force, which is also the only place the dead gutter exists. */
@media (min-width: 901px) {
  .bu-console-body:not(:has(> .bu-tab-rail-right .bu-tab-btn)) > .bu-console-center {
    grid-column: 3 / -1;
  }

  /* Same repair for a plain three-track consumer of the shared shape above
     (rail | well | rail, no mullions and no .bu-console-center wrapper). */
  .bu-console-body:not(:has(> .bu-tab-rail-right .bu-tab-btn)):not(:has(> .bu-console-center)) > .bu-slideout-well {
    grid-column: 2 / -1;
  }
}

.bu-embedded-page > .bu-slab > .bu-bay-grid {
  padding: 0 0 var(--structure-gap-lg) 0;
}

.bu-embedded-page > .bu-slab > .bu-bay-grid:last-child {
  padding-bottom: 0;
}

/* --- Slide-out well --------------------------------------------------------
   The central content area a tab's content transitions INTO. Never a route
   change (BUP-UI-CONSOLE-VISION-001) — .is-open toggles a transform/opacity
   pair. Direction (.bu-slideout-left / .bu-slideout-right) sets which edge
   the content conceptually slides in from, matching the rail that owns the
   active tab.

   Animation-off (two paths, both collapse to the same instant-show rule):
     1) prefers-reduced-motion: reduce (OS-level).
     2) [data-motion="off"] (explicit in-app toggle, Home.razor "Reduce
        motion" control) — sets the same attribute this component already
        reads, so no separate code path is needed for the two triggers. */
.bu-slideout-well {
  position: relative;
  min-height: 320px;
  min-width: 0;
}

.bu-slideout-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 320px;
  text-align: center;
}

.bu-slideout-content {
  min-height: 320px;
  opacity: 0;
  transform: translateX(24px);
  transition: transform 240ms ease, opacity 240ms ease;
}

.bu-slideout-well.bu-slideout-left .bu-slideout-content {
  transform: translateX(-24px);
}

.bu-slideout-well.is-open .bu-slideout-content {
  opacity: 1;
  /* `none`, not translateX(0): any transform (even an identity one) makes the panel the containing block of a position:fixed descendant, so a
     dialog opened from inside the well (homework Review) was laid out against the panel - a 24px sliver at 800px wide - instead of the window, and the
     notice strip could land on its controls (REQ-UX-TOAST-001, AdultWalk 800px check). The slide-in still animates (none <-> translateX interpolates). */
  transform: none;
}

.bu-slideout-well[data-motion="off"] .bu-slideout-content {
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .bu-slideout-content {
    transition: none;
  }
}

.bu-slideout-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-md);
  margin-bottom: var(--structure-gap-sm);
}

.bu-slideout-title {
  font-size: var(--structure-text-lg);
  font-weight: var(--structure-weight-bold);
  /* ADM-EB-17c: the two drawer titles are now real <h2>s like the well's own
     title (Home.razor). Zero the shared h1-h6 bottom margin so the title stays
     centred against the close button in the flex header. */
  margin: 0;
}

.bu-slideout-close {
  min-width: var(--structure-touch-min);
  min-height: var(--structure-touch-min);
  padding: 0;
  line-height: 1;
}

.bu-slideout-body {
  min-width: 0;
}

/* =========================================================================
   CONSOLE PANEL GEOMETRY — padding + vertical rhythm (R-0904-03)
   docs/rulings/BUP-RULING-MULTISTYLE-EVENTS-CONSOLE_2026-09-04.md

   THE DEFECTS (owner screenshots of admin.qa.beltup.online, 2026-09-04):
   section headings ("Warnings", "Upcoming Classes") crowded the panel edge and
   the content above them, and "Upcoming Classes" plus its empty-state line sat
   flush against the KPI tiles with no separation at all.

   CAUSE: a console tab's sections are consecutive .bu-bay-grid siblings, each
   carrying an inline `style="padding:0"` to cancel the Primer's default
   24px/32px grid padding. Zeroing that padding also zeroed the ONLY thing
   separating one section from the next — adjacent grids have no margin of their
   own — so three distinct sections rendered as one continuous run.

   THE RULE, on the 8px grid and inside the ruling's stated budget: the panel
   owns the side inset, a section grid owns none, sections are separated by
   32px, and a section heading stands 16px clear of its content. Everything here
   is scoped to .bu-slideout-body (SlideOutPanel.razor), so standalone visits to
   /school-profile, /sessions, /curriculum, /theme-settings and /regulatory are
   untouched. GEOMETRY ONLY — this block assigns no colour, and R-0829-02 still
   gates the reskin.
   ========================================================================= */

/* Panel inner padding. Horizontal goes 24px -> 32px so a heading is no longer
   flush against the panel edge. VERTICAL DELIBERATELY STAYS 24px: the
   2026-09-02 type-scale pass measured School Profile's overview fitting inside
   the fold at that value, and spending another 16px of vertical budget here is
   exactly the change that would push it back out. */
.bu-slideout-content.bu-panel,
.bu-slideout-placeholder.bu-panel {
  padding: var(--structure-gap-lg) var(--structure-gap-xl);
}

/* A section grid inside a panel is a rhythm device, not a padded surface — the
   panel already supplies the inset. This is the same triple-inset repair
   .bu-embedded-page got for routed pages (above), generalised to hand-written
   console sections so they no longer need an inline style each. */
.bu-slideout-body .bu-bay-grid {
  padding: 0;
}

/* Section -> section: 32px. */
.bu-slideout-body .bu-bay-grid + .bu-bay-grid,
.bu-slideout-body .bu-bay-grid + .bu-reveal {
  margin-top: var(--structure-gap-xl);
}

/* ...but an EMBEDDED routed page already spends 24px of bottom padding between
   its own sections (.bu-embedded-page rule above). Without this reset it would
   get that 24px AND the 32px — 56px between every section of School Profile,
   i.e. a regression to a surface the 2026-09-02 pass measured as correct. The
   (0,3,0) specificity beating the (0,2,0) rule above it is deliberate. */
.bu-embedded-page > .bu-slab > .bu-bay-grid + .bu-bay-grid {
  margin-top: 0;
}

/* Section heading -> its content: 16px (the h3's own 8px was all these had).
   A .bu-reveal that IS a panel's own header keeps the already-tuned rhythm from
   the .bu-panel > .bu-reveal:first-child / :not(:first-child) rules above —
   those are (0,3,0) and still win — so every form panel on School Profile,
   Curriculum and Sessions is untouched. This reaches only the console's own
   section headings, which sit in a plain .bu-bay-N with no panel around them. */
.bu-slideout-body .bu-reveal {
  margin-bottom: var(--structure-gap-md);
}

/* --- Console stat-tile row: equal siblings, one gutter (R-0904-03) ---------
   THE DEFECT: the two dashboard KPI tiles measured UNEQUAL (~735px against
   ~730px) with an odd gap between them.

   CAUSE: they are .bu-bay-6 spans of the 12-track `repeat(12, minmax(0, 1fr))`
   bento grid. Twelve fractional tracks plus eleven 24px gaps almost never
   divide a real panel width into whole pixels, and the sub-pixel remainder the
   browser distributes across those tracks does not land symmetrically on a span
   starting at track 1 versus one starting at track 7 — so two nominally
   identical 6-spans render at different widths.

   THE RULE: a row built as "two halves" is an EQUAL-FRACTION grid — two
   minmax(0, 1fr) tracks with one grid gap between them — so the tiles are
   identical by construction at any width, instead of being hand-counted spans
   of a 12-track ruler. `grid-column: auto` is load-bearing: without it
   .bu-bay-6's `span 6` would ask for six of two tracks and the browser would
   invent four implicit columns.

   SCOPE GUARDS, both load-bearing:
     :has(> .bu-bay-6.bu-metric)  — only rows built as two halves. Sessions'
                                    .bu-bay-3 counters and BillingOverviewPanel's
                                    .bu-bay-4 tiles keep the 12-track ruler they
                                    were designed against.
     :not(:has(> :nth-child(3)))  — only rows of EXACTLY two children. The
                                    Payments row grows a third, full-width
                                    .bu-bay-12 tile when a Stripe account id
                                    exists; a two-track grid cannot host that,
                                    so that state stays on the 12-track shape
                                    (where its first two tiles are already the
                                    two halves this rule would otherwise draw).
   (:has() inside :not() is valid and already load-bearing in this file — see
   the empty-rail collapse above. What is invalid, and is NOT done here, is
   nesting :has() inside another :has().)

   HEIGHT IS NOT THIS RULE'S JOB (D-0910-01, 2026-09-10): equal WIDTH alone
   still left the Payments pair with bottom edges 25px apart, because the
   Primer's `align-items: start` let each tile size to its own line count. The
   equal-HEIGHT companion lives with the .bu-metric definition near the top of
   this file (`.bu-bay-grid:has(> .bu-metric) { align-items: stretch }`) —
   deliberately NOT here, because it is not console-specific and it must also
   reach 3-wide and 4-wide tile rows this rule intentionally excludes. The two
   compose cleanly: that rule sets align-items only, this one sets track
   sizing only, so neither can clobber the other.
   ------------------------------------------------------------------------- */
.bu-slideout-body .bu-bay-grid:has(> .bu-bay-6.bu-metric):not(:has(> :nth-child(3))) {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.bu-slideout-body .bu-bay-grid:has(> .bu-bay-6.bu-metric):not(:has(> :nth-child(3))) > * {
  grid-column: auto;
}

@media (max-width: 768px) {
  /* One tile per line on a phone — this mirrors the Primer's own bay collapse
     (.bu-bay-N { grid-column: span 12 }), which the rules above would otherwise
     outrank and leave as two ~170px tiles on a 390px screen. */
  .bu-slideout-body .bu-bay-grid:has(> .bu-bay-6.bu-metric):not(:has(> :nth-child(3))) {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* --- Diagnostic drawer (R-0709-15 point 3) ---------------------------------
   Overlay that slides in from the right OVER the center bento — not a new
   center-swap tab (that's .bu-slideout-well above), a second layer that
   floats above whichever center content is currently showing. Lives inside
   .bu-console-center (Home.razor.css), which is the positioning context.
   Structural shape only; color/elevation via --theme-* tokens in render-*.css. */
.bu-drawer-host {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: var(--structure-z-overlay);
}

.bu-drawer {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(360px, 100%);
  transform: translateX(100%);
  transition: transform 240ms ease;
  pointer-events: auto;
  overflow-y: auto;
  border-radius: 0;
}

.bu-drawer.is-open {
  /* `none`, not translateX(0): any transform (even an identity one) makes the drawer the containing block of a position:fixed descendant. The student record
     holds dialogs (Unlock further, Adjust for this student), which were laid out against the 360px drawer instead of the window - the same defect and the same
     fix as the slide-out panel (commit e0256cc, REQ-UX-TOAST-001). The slide-in still animates (none <-> translateX interpolates). #513 */
  transform: none;
}

.bu-drawer[data-motion="off"] {
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .bu-drawer {
    transition: none;
  }
}

.bu-drawer-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.bu-drawer-host:has(.bu-drawer.is-open) .bu-drawer-scrim {
  pointer-events: auto;
}

/* #439: on a phone the drawer is a viewport-fixed sheet with its own scroll. The host used to be absolute inside the (very tall)
   roster well, so a student tapped far down opened a 7000px panel at the top of the page and the screen showed empty space.
   Body scroll is locked while it is open (html.bu-scroll-locked, set by buDrawer.open). Same z-index as before, so the
   promote/sign dialogs still sit above it. */
@media (max-width: 768px) {
  .bu-drawer-host {
    position: fixed;
    inset: 0;
  }

  .bu-drawer {
    width: 100%;
    height: 100%;
    max-height: 100dvh;
    overscroll-behavior: contain;
  }
}

html.bu-scroll-locked,
html.bu-scroll-locked body {
  overflow: hidden;
}

.bu-drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--structure-gap-md);
  margin-bottom: var(--structure-gap-sm);
}

.bu-drawer-field {
  display: flex;
  justify-content: space-between;
  gap: var(--structure-gap-sm);
  padding: var(--structure-gap-xs) 0;
}

/* --- Motion toggle control (explicit "animation-off" affordance) ---------- */
.bu-motion-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--structure-gap-xs);
  min-height: var(--structure-touch-min);
}

/* --- Gate overlay (added 2026-08-21, owner dictation, KEY-COMMAND §7a) -----
   A locked (tier-gated) or not-yet-built tab opens this instead of doing
   nothing. Same stacking law as .bu-drawer above: position:absolute +
   z-index, layered OVER .bu-console-body, never a route change and never
   adds height to the shell. Structure only; color lives in render-*.css. */
.bu-tab-btn.is-locked {
  opacity: 0.72;
}

.cc-lockglyph {
  margin-left: auto;
  font-size: 0.7em;
}

/* -----------------------------------------------------------------------
   Gated tab presentation (owner ruling 2026-08-22). A tab the school cannot
   use yet is NOT blocked at the rail and NOT a modal: the tab opens like any
   other, its content renders greyed and inert, and a notice sits over it
   INSIDE the panel. For a tier-locked tab this deliberately leaves the shape
   of the feature visible behind the notice — that is the upsell; a modal hid
   the very thing being sold.

   .cc-gated-content pairs with the `inert` attribute in Home.razor: the
   filter handles appearance, `inert` handles focus and pointer events, so the
   greyed content cannot be tabbed into. Never rely on the filter alone.
   ----------------------------------------------------------------------- */
.cc-gated {
  position: relative;
  /* A not-yet-built tab has no content to grey out, so without a floor the
     absolutely-positioned notice would have nothing to sit in. */
  min-height: 16rem;
}

.cc-gated-content {
  filter: grayscale(1);
  opacity: 0.4;
  user-select: none;
}

/* The notice only exists in the DOM while a tab is gated, so its resting
   state is VISIBLE. Never gate visibility on a transition finishing — a
   window that is not compositing never advances one, which strands an
   overlay at opacity 0 while still covering content. The fade is a one-shot
   entry animation: if animations do not run the element simply shows
   immediately, which is the correct degradation. */
.cc-gate-notice {
  position: absolute;
  inset: 0;
  z-index: var(--structure-z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--structure-gap-lg);
  animation: cc-gate-in 200ms ease both;
}

@keyframes cc-gate-in {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

.cc-upgrade-card {
  width: min(360px, 86%);
  border-radius: var(--structure-radius-panel);
  padding: var(--structure-gap-xl) var(--structure-gap-lg);
  text-align: center;
}

.cc-upgrade-card h3 {
  font-size: 0.9rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  margin: 0 0 var(--structure-gap-sm) 0;
}

.cc-upgrade-card p {
  font-size: var(--structure-text-sm);
  line-height: 1.5;
  margin: 0 0 var(--structure-gap-lg) 0;
}

.cc-upgrade-btn {
  width: 100%;
}

.cc-upgrade-dismiss {
  display: block;
  margin: var(--structure-gap-sm) auto 0;
  background: none;
  border: none;
  font-size: 0.75rem;
  cursor: pointer;
  text-decoration: underline;
  min-height: var(--structure-touch-min);
}

@media (prefers-reduced-motion: reduce) {
  /* Drop the fade, keep the overlay shown — `both` would otherwise hold the
     from-state. */
  .cc-gate-notice {
    animation: none;
  }
}

/* -----------------------------------------------------------------------
   Reduced motion, app preference (VISION-001 §18 "animation-off toggle is
   required"). Set from localStorage on the document root in App.razor's
   <head>; the control lives on the settings page, never in the console
   header (owner ruling 2026-08-21). This is the app-level switch — the OS
   `prefers-reduced-motion` query is handled separately, per-component, and
   is not overridden by the absence of this attribute.

   Deliberately near-zero rather than `none`: a 0s duration still fires
   transitionend/animationend, so any component that waits on those events to
   finalise state keeps working instead of hanging.
   ----------------------------------------------------------------------- */
:root[data-reduced-motion="true"] *,
:root[data-reduced-motion="true"] *::before,
:root[data-reduced-motion="true"] *::after {
  animation-duration: 0.01ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.01ms !important;
  scroll-behavior: auto !important;
}

/* =========================================================================
   SITE MAP — DENSE TABULAR RENDER (.smp-*) — owner order 2026-07-04.
   Compact monospace rows replacing the old spread-out bu-panel-per-node
   layout, matching ViralDecay's InternalSiteMap.razor.css structure
   (legend / filter chips / stat row / dense zone rows / MVP gap cluster).
   Structure only; color assigned via --state-*/--theme-* tokens in the
   render layer or inline below where the token already exists in this
   Primer (§3.7 state baselines).
   ========================================================================= */
.smp-legend {
  display: flex;
  gap: var(--structure-gap-md);
  flex-wrap: wrap;
  padding: 0.45rem 0.75rem;
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  margin: 0 var(--structure-gap-xl) var(--structure-gap-sm);
  font-size: var(--structure-text-xs);
}

.smp-legend-item {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.smp-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  flex-shrink: 0;
}

.smp-filters {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin: 0 var(--structure-gap-xl) var(--structure-gap-sm);
}

.smp-filter-btn {
  font-family: var(--structure-font-family-mono);
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 0.5px;
  padding: 0.25rem 0.6rem;
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
  cursor: pointer;
  text-transform: uppercase;
}

.smp-stats {
  display: flex;
  align-items: center;
  gap: var(--structure-gap-md);
  flex-wrap: wrap;
  margin: 0 var(--structure-gap-xl) var(--structure-gap-md);
  padding: 0.6rem 0.9rem;
  border-radius: var(--structure-radius-max);
  border-width: 1px;
  border-style: solid;
}

.smp-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  min-width: 60px;
}

.smp-stat-n {
  font-size: var(--structure-text-lg);
  font-weight: var(--structure-weight-bold);
  line-height: 1;
}

.smp-stat-l {
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.smp-stat-sep {
  width: 1px;
  height: 28px;
}

/* --- Dense node row: status dot, route/name, tags, all on one line where
   possible — this is the "compact tabular" shape the owner asked for,
   replacing one bu-panel-per-node. */
.smp-row {
  display: flex;
  align-items: flex-start;
  gap: 0.45rem;
  padding: 0.3rem 0.4rem;
  border-radius: 3px;
  font-size: var(--structure-text-xs);
}

.smp-row-dot {
  margin-top: 0.2rem;
  flex-shrink: 0;
}

.smp-row-body {
  flex: 1;
  min-width: 0;
}

.smp-row-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem;
}

.smp-route {
  font-family: var(--structure-font-family-mono);
  font-size: var(--structure-text-xs);
  text-decoration: none;
  white-space: nowrap;
}

.smp-tag {
  display: inline-block;
  font-size: 0.6rem;
  font-weight: var(--structure-weight-bold);
  letter-spacing: 0.5px;
  padding: 0.05rem 0.3rem;
  border-radius: 2px;
  border-width: 1px;
  border-style: solid;
  text-transform: uppercase;
}

.smp-zone-card {
  border-radius: var(--structure-radius-panel);
  border-width: 1px;
  border-style: solid;
  padding: 0.7rem 0.85rem;
}

/* --- MVP Gap cluster — a distinct, alarmed panel at the bottom of the
   filtered list (VD's smp-gap-cluster). */
.smp-gap-cluster {
  margin: 0 var(--structure-gap-xl) var(--structure-gap-md);
  padding: var(--structure-gap-md);
  border-radius: var(--structure-radius-panel);
  border-width: 1px;
  border-style: solid;
}

.smp-gap-title {
  font-size: var(--structure-text-xs);
  font-weight: var(--structure-weight-bold);
  letter-spacing: 1px;
  text-transform: uppercase;
  margin-bottom: var(--structure-gap-sm);
}

/* --- Status-color utility classes — the ONLY color rules in this Primer
   file, permitted because they consume the §3.7 functional --state-*
   tokens directly (not brand hex) per the owner's explicit instruction
   to keep green/orange/red state colors even inside the grayscale
   diagnostic shell. Every other rule in this file stays colorless. */
.smp-dot-live,
.is-live-dot { background: var(--state-success); }
.smp-dot-scaffold,
.is-scaffold-dot { background: var(--state-warning); }
.smp-dot-inprogress,
.is-inprogress-dot { background: var(--state-warning); }
.smp-dot-gap,
.is-gap-dot { background: var(--state-error); }
.smp-dot-dev,
.is-dev-dot { background: var(--theme-text-muted, #888888); }

.smp-tag-mvp,
.smp-tag-gap { color: var(--state-error); border-color: var(--state-error); }
.smp-tag-auth { color: var(--theme-text-muted, #888888); border-color: var(--theme-text-muted, #888888); }
.smp-tag-orphan { color: var(--state-warning); border-color: var(--state-warning); }

.smp-live { color: var(--state-success); }
.smp-inprogress,
.smp-scaffold { color: var(--state-warning); }
.smp-gap,
.smp-mvp { color: var(--state-error); }

/* -----------------------------------------------------------------------
   Phone ergonomics (2026-08-22, from the layout walkthrough).

   A page may be DESIGNED for desktop, but it must be USABLE on a phone —
   these rules only fix reachability and fit, never appearance, so they are
   structure and belong in the Primer.
   ----------------------------------------------------------------------- */
@media (max-width: 768px) {
  /* .bu-link-stealth is a deliberately quiet 0.8rem footer link. Quiet is fine;
     un-tappable is not — it accounted for 107 of the 120 undersized touch
     targets found across the site, because it appears in the footer of every
     page. Given a real target height it stays visually identical and becomes
     hittable. */
  .bu-link-stealth {
    display: inline-flex;
    align-items: center;
    min-height: var(--structure-touch-min);
  }

  /* Tables cannot be made to fit a 390px screen without lying about the data,
     so let them scroll horizontally INSIDE their own box rather than pushing
     the whole page sideways. Applied to the element itself because most tables
     in this codebase are not wrapped in .bu-table-wrapper; `max-width: 100%`
     keeps the scroll contained to the table. */
  .bu-table-wrapper,
  table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* Tighter cell padding so fewer columns need scrolling in the first place. */
  table th,
  table td {
    padding: 0.5rem;
    white-space: nowrap;
  }

  /* Native checkboxes and radios render ~13px, well under any reachable target.
     Enlarging the control itself keeps the hit area on the control (not just its
     label), which is what a thumb actually aims at. */
  input[type="checkbox"],
  input[type="radio"] {
    min-width: 24px;
    min-height: 24px;
  }

  /* A label wrapping a control is the real target — give it the full height so
     tapping the words works, not just the 24px box. */
  .bu-check-label,
  label:has(> input[type="checkbox"]),
  label:has(> input[type="radio"]) {
    display: inline-flex;
    align-items: center;
    gap: var(--structure-gap-sm);
    min-height: var(--structure-touch-min);
  }

  /* Text inputs, selects and file pickers: the Primer's 44px law applies on a
     phone above all, where these were rendering short. */
  .bu-input,
  input[type="text"],
  input[type="email"],
  input[type="tel"],
  input[type="number"],
  input[type="date"],
  input[type="password"],
  input[type="url"],
  input[type="file"],
  select,
  textarea {
    min-height: var(--structure-touch-min);
  }

  /* A <summary> styled as a control is a control. */
  summary {
    min-height: var(--structure-touch-min);
    display: flex;
    align-items: center;
  }
}

/* -----------------------------------------------------------------------
   .bu-toolbar — a panel's own action row: summary figures on the left, the
   primary action on the right, one line. Exists so a create action can sit
   BESIDE the thing it creates instead of being stacked underneath it, which
   buried the list it belonged to (owner ruling 2026-08-23, Roster tab).

   .bu-empty-state — an empty list is a place to act, not a dead sentence, so
   it centres its message and its call to action.

   Structure only, no colour, so neither needs a rule in any render file and
   neither can cause partial-skin fallthrough (D-S07-SKIN-02).
   ----------------------------------------------------------------------- */
.bu-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--structure-gap-md);
  margin-bottom: var(--structure-gap-lg);
}

/* A panel/page heading inside a .bu-toolbar kept its 8px bottom margin while the
   row centres its items, so the title sat ~4px above the button beside it
   (SchoolProfile Overview and Identity headers). The toolbar owns the spacing. */
.bu-toolbar > h1,
.bu-toolbar > h2,
.bu-toolbar > h3,
.bu-toolbar > h4 {
  margin: 0;
}

.bu-toolbar-metrics {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--structure-gap-xs) var(--structure-gap-md);
}

.bu-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--structure-gap-md);
  padding: var(--structure-gap-2xl) var(--structure-gap-lg);
  text-align: center;
}

/* -----------------------------------------------------------------------
   .bu-panel.bu-brick — padding collision repair (2026-08-23).
   Markup legitimately combines the two (`class="bu-panel bu-brick"`), but
   .bu-panel sets padding: var(--structure-gap-lg) and .bu-brick sets
   `var(--structure-gap-sm) 0`. Same specificity, .bu-brick declared later, so
   it won and zeroed the HORIZONTAL padding — content then sat flush against
   the panel edge. Under skins that draw a corner ornament (Fight Night's 35px
   cobalt wedge on .bu-panel::before) that clipped the first letter of every
   panel heading. A panel is a padded surface first; being a brick as well must
   not remove that.
   ----------------------------------------------------------------------- */
.bu-panel.bu-brick {
  padding: var(--structure-gap-lg);
}

/* -----------------------------------------------------------------------
   US-182 (#227) / R-1016-02: preproduction dev/test overlay + banner.
   Rendered only when Environment:Preproduction is true. Uses a DEDICATED high-contrast
   pair (not skin tokens) so text is >= 4.5:1 in every skin (greyscale, executive-blue,
   fight-night): body #f5f7fa on #0b0e13 (~17:1), heading/banner/button amber #fbbf24
   against #0b0e13 (~11:1) and #1a1300 on #fbbf24 (~11:1).
   No-JS: the overlay blocks via CSS; the "I understand" anchor targets #bu-preprod-ack,
   which hides it (:target). With JS the script handles sessionStorage + focus trap.
   ----------------------------------------------------------------------- */
.bu-preprod-overlay {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  align-items: flex-start; /* top-aligned + scrollable: the top is never clipped on short viewports */
  justify-content: center;
  padding: var(--structure-gap-lg);
  overflow-y: auto;
  background-color: #05070a;
  color: #f5f7fa;
}
.bu-preprod-overlay[hidden],
#bu-preprod-ack:target + .bu-preprod-overlay { display: none; }
html.bu-preprod-acked .bu-preprod-overlay { display: none; }
.bu-preprod-dialog {
  max-width: 40rem;
  width: 100%;
  margin: auto; /* centres vertically when it fits, scrolls from the top when it does not */
  padding: var(--structure-gap-xl, var(--structure-gap-lg));
  border: 3px solid #fbbf24;
  background-color: #0b0e13;
  color: #f5f7fa;
}
.bu-preprod-dialog h1 { color: #fbbf24; margin-top: 0; }
.bu-preprod-dialog .bu-btn {
  display: inline-block;
  text-decoration: none;
  margin-top: var(--structure-gap-md);
  background-color: #fbbf24;
  color: #1a1300;
  border: 2px solid #fbbf24;
  font-weight: 700;
}
.bu-preprod-dialog .bu-btn:hover { background-color: #fcd34d; color: #1a1300; }
.bu-preprod-dialog .bu-btn:focus-visible { outline: 3px solid #ffffff; outline-offset: 2px; }
.bu-preprod-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 9000;
  padding: 2px var(--structure-gap-md);
  text-align: center;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  background-color: #fbbf24;
  color: #1a1300;
}
.bu-preprod-checkout-notice {
  border-left: 4px solid #fbbf24;
  padding-left: var(--structure-gap-sm);
}

/* #552: cause found by reading the markup and rules, not by a browser run. In Payments & Subscription the paperwork nag (a bordered,
   filled box) is the previous sibling of the first tile grid, inside .bu-slideout-body. That panel rule zeroes the grid's padding
   (".bu-slideout-body .bu-bay-grid { padding: 0 }") and only gives grid-after-grid / grid-after-reveal a gap, so nothing separated a
   nag from the grid after it: the tiles sat flush against the banner's border and read as overlapping. No rule positions or
   negatively offsets the nag. The fix is the missing nag-to-grid step. */
.bu-slideout-body .bu-nag + .bu-bay-grid {
  margin-top: var(--structure-gap-md);
}
