/* Simpli Management 2.0 — component layer.
   Consumes the semantic aliases declared by design/tokens.css; declares no colour,
   space or radius of its own. Every selector is class-based and namespaced `sm-`,
   so linking this file changes nothing until a class is applied to markup.

   NOT a cascade layer: Bootstrap 5.2.2 and DataTables 1.12.1 are unlayered, and
   unlayered rules always beat layered ones. @layer here would make this file lose
   every conflict.

   No @import of tokens.css: every layout that links this file (_UserLayout, _AuthLayout,
   _Layout, _ValuationToolLayout) links tokens.css itself, versioned, immediately before it.
   The old unversioned @import fetched tokens.css a second time and added a serial hop. */

.sm-template-preview > summary{
  cursor:pointer;
  list-style:none;
}
.sm-template-preview > summary::-webkit-details-marker{ display:none; }
/* The +/− glyph is decoration: the <details> already exposes its expanded state. Generated
   content is read as text, so Chrome named every preview summary "Basic Info Included +". The
   second `content` gives the glyph empty alternative text; the first is the fallback for an
   engine without the `/ ""` syntax, which drops the second declaration and keeps the glyph. */
.sm-template-preview > summary::after{
  content:"+";
  content:"+" / "";
  color:var(--brand-text);
  font-size:var(--text-lg);
  margin-left:var(--space-3);
}
.sm-template-preview[open] > summary::after{
  content:"−";
  content:"−" / "";
}
/* A disclosure toggle like .sm-disclosure__trigger, so it takes the token ring (section 0.2)
   rather than the bespoke 2px outline it used to draw. */
.sm-template-preview > summary:focus-visible{
  outline:var(--sm-focus-outline);
  border-radius:var(--radius-md);
  box-shadow:var(--focus-ring);
}
.sm-template-preview > summary .sm-badge{ margin-left:auto; }
.sm-template-editor .sm-card:has(> .sm-card__header input.sm-checkbox:not(:checked)) > .sm-card__body{
  display:none;
}
.sm-template-editor .sm-card:has(> .sm-card__header input.sm-checkbox:not(:checked)) > .sm-card__header,
.sm-template-preview:not([open]) > summary{
  border-bottom:0;
}

.sm-app .sm-map .leaflet-tooltip{
  background:var(--surface-overlay);
  border:1px solid var(--border);
  color:var(--text);
  font-family:var(--font-sans);
}
/* The zoom buttons are tools (1.1), so they take the neutral outline, painted from 0.5's tokens:
   a white-6% lift inside a full --sm-field-border outline under a --text label, and under the
   pointer a white-10% lift inside --sm-field-border-hover under --text-strong. Each link used to
   have a --surface-raised fill and only Leaflet's bottom divider, 1.12:1 against the map frame's
   --surface-sunken (1.20:1 hovered), so the pair was findable only by its glyphs. The lift is
   translucent, so the bar, which Leaflet clips to its padding box, lays an opaque
   --surface-overlay plate under the two links, the surface menus float on. They then read the
   same over Owner/Overview's dark state outlines and over Building/ViewAvailableUnit's light
   OpenStreetMap tiles, where a bare lift would show the tiles behind a near-white glyph; there
   the plated fill bounds the button, measured 8.4-13.2:1 against the sea, woodland and land
   tiles round the bar. On the plate the outline is 4.11:1 and the label 12.87:1, hovered 7.16:1
   and 12.41:1; against the frame's --surface-sunken round the bar the outline is 4.91:1. The
   links overlap by a hairline, the btn-group shape of the Buttons export (section 6), so the two
   outlines share one divider, and the hovered link rises over its neighbour so its lighter
   outline shows on all four sides (the focus rule below lifts a focused one the same way).
   Leaflet's 2px black-20% rim round the bar stays. Leaflet centres each glyph with a line-height
   equal to the link's box, 26px, or 30px under .leaflet-touch (every pointer-event browser); the
   box is border-box, so the two hairlines take 2px from it, and the line-height drops by the same
   2px, so each glyph stays centred in its own box, as Leaflet centres it. The hairline overlap
   moves the zoom-out link, glyph and all, 1px up from where Leaflet puts it (Owner/Overview, 9 Oct:
   the link at 31px instead of 32px, both glyphs 0px off their own box's centre). A link Leaflet
   disables at the minimum or maximum zoom (`leaflet-disabled`) looked live, because these rules
   outrank Leaflet's own disabled paint: it takes .sm-btn's disabled opacity and no hover, and
   keeps full opacity while it holds keyboard focus, where the opacity would fade the ring with it.
   Admin/ViewAdminDashboard's map turns its zoom control off. */
.sm-app .sm-map .leaflet-bar{
  background-color:var(--surface-overlay);
}
.sm-app .sm-map .leaflet-bar a{
  position:relative;
  background:var(--sm-btn-neutral-fill);
  color:var(--text);
  border:var(--border-hairline) solid var(--sm-field-border);
  line-height:calc(26px - var(--border-hairline) * 2);
}
.sm-app .sm-map.leaflet-touch .leaflet-bar a{
  line-height:calc(30px - var(--border-hairline) * 2);
}
.sm-app .sm-map .leaflet-bar a + a{
  margin-top:calc(var(--border-hairline) * -1);
}
.sm-app .sm-map .leaflet-bar a:hover:not(:where(.leaflet-disabled)){
  z-index:1;
  color:var(--text-strong);
  background:var(--sm-btn-neutral-fill-hover);
  border-color:var(--sm-field-border-hover);
}
.sm-app .sm-map .leaflet-bar a.leaflet-disabled:not(:focus-visible){
  opacity:.45;
}
/* The zoom buttons are real links. Leaflet's own `.leaflet-bar a:focus` only swaps the fill (and
   the rule above outranks it), so they take the token ring; z-index lifts the focused button over
   its stacked sibling, which would otherwise paint across the lower half of the ring.
   The three focus rules here, and their forced-colours twin in 0.2, also name
   .sm-public-units__map, the public unit page's map (Building/ViewAvailableUnit), so it keeps
   these rings with or without .sm-map beside it. */
.sm-app .sm-map .leaflet-bar a:focus-visible,
.sm-app .sm-public-units__map .leaflet-bar a:focus-visible{
  position:relative;
  z-index:1;
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
/* The map container is a tab stop too: Leaflet's keyboard handler gives it tabindex="0" (arrow keys
   pan, +/- zoom), and leaflet.css, <link>ed after this file, sets `.leaflet-container{outline:0}`, so
   arriving there showed nothing. (0,3,0) clears that (0,1,0). The ring is OUTSET: the container's
   overflow:hidden clips its panes, not its own box-shadow, and an inset shadow would be painted
   beneath the panes that fill the box; on the Admin map those carry opaque tiles. */
.sm-app .sm-map:focus-visible,
.sm-app .sm-public-units__map:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
/* The attribution's links (Leaflet's own "Leaflet" prefix on every map). They sit flush in the
   bottom corner the container clips, but the ring stays outset: an inset one would be --purple-300
   on Leaflet's 70%-white attribution box, about 1.2:1, while the outset ring's --surface-canvas gap
   band is 9.6:1 or better against that box. The clip can shave the outer band along the bottom edge
   and round off the ring's bottom-right corner; the rest of the ring shows. */
.sm-app .sm-map .leaflet-control-attribution a:focus-visible,
.sm-app .sm-public-units__map .leaflet-control-attribution a:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}

/* ============================================================================
   0. Aliases — motion (0.1), focus (0.2), field borders and status steps (0.3),
      icon sizes (0.4), button paint (0.5)
   Custom properties apply nothing, so every :root block in this section is
   inert on its own; the components below consume them.

   0.1 Motion. Components reference these instead of the raw motion tokens so that
   prefers-reduced-motion can neutralise every transform and transition from one
   place, without needing a duplicate selector list or !important.

   --sm-lift-undo is the lift's inverse, for a box that has to stay where a lifted
   card rests (12.3). A point reflection on either side turns a translation into its
   opposite, so it follows the token without restating its length. That holds for a
   translation, which is what --lift-card is.
   ========================================================================= */
:root{
  --sm-duration-instant:var(--duration-instant);
  --sm-duration-fast:var(--duration-fast);
  --sm-duration-base:var(--duration-base);
  --sm-press-transform:var(--press-scale);
  --sm-lift-transform:var(--lift-card);
  --sm-lift-undo:scale(-1) var(--sm-lift-transform) scale(-1);
}

@media (prefers-reduced-motion: reduce){
  :root{
    --sm-duration-instant:1ms;
    --sm-duration-fast:1ms;
    --sm-duration-base:1ms;
    --sm-press-transform:none;
    --sm-lift-transform:none;
    --sm-lift-undo:none;
  }
}

/* --- 0.2 Focus -----------------------------------------------------------
   One ring for the product, and one people can see. The vendored --focus-ring
   (tokens/colors.css) is a 3px wash of brand at 40%: 1.30:1 on --surface, 1.28:1
   on the rail and 1.00:1 against a brand-filled button, so it vanished exactly
   where focus lands most. It is redeclared here, not edited there: tokens.css is
   a vendored export and stays byte-identical.

   The replacement is a GAP ring: a band of --surface-canvas, then a band of
   --purple-300. The ring only ever touches the gap and the surface outside it,
   so it clears 7:1 everywhere in the dark UI — 8.28:1 against its own gap,
   7.88:1 on --surface, 7.53:1 on --surface-raised, 7.06:1 on --surface-overlay
   (the floor), 8.87:1 on today's #000 rail — and the gap keeps it apart from a
   brand fill it would otherwise meet at 3.37:1. --sm-focus-width is the ring's
   one length, the way tokens/colors.css writes its own 3px.

   Light scopes redeclare --focus-ring ITSELF, to --sm-focus-ring-light (a --page
   gap and a --purple-600 ring, 7.98:1 on white). A custom property is resolved
   where it is declared and inherited as a value, so rebinding --purple-300 or
   --surface-canvas further down would change nothing. .sm-auth__panel (10.1)
   does this; marketing.css does the same for .sm-site and puts the dark value
   back on its .sm-app islands. --sm-focus-ring-danger is the same geometry for
   a focused invalid field (the old danger wash was 1.14:1) and is redeclared
   alongside it.

   --sm-focus-ring-inset is for rows that are full-bleed inside a scrolling or
   clipping parent — rail items, scroll-list tabs, dropdown items, sortable
   table headers. An outset ring there is clipped on both sides by the parent's
   own overflow, so only its top and bottom ever showed.

   --sm-focus-outline is `none` until forced colours are active. Windows High
   Contrast drops box-shadow, so every ring above disappears there, and a
   (0,1,0) fallback cannot outrank the components' own outline declarations.
   So no focus rule in this file writes `outline:none`: each writes
   `outline:var(--sm-focus-outline)`, and the fallback reaches all of them
   through the variable. The :where() rule below is only the catch-all for
   focusable elements no component rule touches. (The exceptions: the skip
   link's <main tabindex="-1"> target in section 9, which is not a control; and
   two controls whose ring another box draws instead, the stretched card link
   (12.3, the card rings itself) and a sticky-first scroll body (section 6, an
   overlay on its frame rings it). Both replacements survive forced colours.)
   ------------------------------------------------------------------------ */
:root{
  --sm-focus-width:2px;
  --sm-focus-ring-dark:
    0 0 0 var(--sm-focus-width) var(--surface-canvas),
    0 0 0 calc(var(--sm-focus-width) * 2) var(--purple-300);
  --sm-focus-ring-light:
    0 0 0 var(--sm-focus-width) var(--page),
    0 0 0 calc(var(--sm-focus-width) * 2) var(--purple-600);
  --sm-focus-ring-danger-dark:
    0 0 0 var(--sm-focus-width) var(--surface-canvas),
    0 0 0 calc(var(--sm-focus-width) * 2) var(--danger);
  --sm-focus-ring-danger-light:
    0 0 0 var(--sm-focus-width) var(--page),
    0 0 0 calc(var(--sm-focus-width) * 2) var(--danger-600);
  --focus-ring:var(--sm-focus-ring-dark);
  --sm-focus-ring-danger:var(--sm-focus-ring-danger-dark);
  --sm-focus-ring-inset:inset 0 0 0 var(--sm-focus-width) var(--purple-300);
  --sm-focus-offset-inset:calc(var(--sm-focus-width) * -1);
  --sm-focus-outline:none;
}

@media (forced-colors: active){
  :root{
    --sm-focus-outline:var(--sm-focus-width) solid CanvasText;
  }
  :where(.sm-app, .sm-auth, .sm-site) :focus-visible{
    outline:var(--sm-focus-width) solid CanvasText;
    outline-offset:var(--sm-focus-width);
  }
  /* Leaflet 1.7.1 writes an inline `outline:none` onto the target (or its nearest focusable
     ancestor) of every mousedown, keydown, keypress and keyup inside a map: Map._handleDOMEvent calls
     DomUtil.preventOutline, and only the next keydown clears it. Tab's keyup lands on the element it
     has just focused, so the zoom links, the container and the attribution link each arrive with
     their outline struck out inline, and in forced colours, which drop the box-shadow ring, that
     outline is the only indicator left. Only !important beats an inline style: the one !important
     in this file's focus handling. */
  .sm-app .sm-map:focus-visible,
  .sm-app .sm-map .leaflet-bar a:focus-visible,
  .sm-app .sm-map .leaflet-control-attribution a:focus-visible,
  .sm-app .sm-public-units__map:focus-visible,
  .sm-app .sm-public-units__map .leaflet-bar a:focus-visible,
  .sm-app .sm-public-units__map .leaflet-control-attribution a:focus-visible{
    outline:var(--sm-focus-width, 2px) solid CanvasText !important;
  }
}

/* --- 0.3 Field borders and status steps ------------------------------------
   A field's boundary is UI-component contrast (WCAG 1.4.11, 3:1). --border
   (white 10%) is 1.29:1 on --surface-field and --border-strong 1.60:1 on
   --surface, so an empty input or an unchecked box was barely findable.
   --ink-400 is 4.69:1 on the field and 4.11:1 on --surface-overlay, the lightest
   surface a field sits on; hover steps up to --ink-300 (7.16:1 or better).
   Disabled and read-only fields deliberately keep the quiet --border.

   The status ramps stop at 600, and a filled status button's hover has to
   DARKEN: the 400 and 500 steps put its white label at 2.35-3.91:1. These two
   700 steps are NEW COLOURS — each 600 step mixed 80/20 with --ink-950 —
   recorded here and never in tokens/colors.css, pending sign-off as a palette
   addition. White text on them is 7.76:1 and 6.34:1.
   ------------------------------------------------------------------------ */
:root{
  --sm-field-border:var(--ink-400);
  --sm-field-border-hover:var(--ink-300);
  --sm-danger-700:#99292A;
  --sm-success-700:#146D4F;
}

/* --- 0.4 Icon sizes -------------------------------------------------------
   14, 16 and 20px at the default root, in rem so icons scale with the reader's
   font-size setting the way text does. Consumed by .sm-icon's size modifiers
   (section 9) and by .sm-rail__icon (8.3), which used to borrow --space-4 as a
   font size.
   ------------------------------------------------------------------------ */
:root{
  --sm-icon-sm:0.875rem;
  --sm-icon-md:1rem;
  --sm-icon-lg:1.25rem;
}

/* --- 0.5 Button paint -----------------------------------------------------
   The edges, washes and selected chip that section 1.1 paints buttons with, named once, so the
   hand-copied mirrors of the old secondary (the section 4 file-selector segment, the section 6
   Responsive control, pager and Buttons export group, the 17.2 drop zone) read the same values
   and cannot drift from the variants again. They are raw ramp steps on purpose: each has the
   same value on the light site and in its dark islands, so marketing.css needs no snapshot or
   restore line for them. The aliases that must differ on the light site (--text, --text-strong,
   --brand-text, --sm-field-border-hover) are read directly in the rules, never through an alias
   here: an alias declared on :root resolves on :root, so it would freeze the dark value.

   Edge contrast (WCAG 1.4.11 asks 3:1) on canvas / card / raised (table header, hovered row) /
   overlay (modal, menu):
     --sm-btn-rim-brand             --purple-400    5.43 / 5.16 / 4.93 / 4.63
     --sm-btn-rim-brand-hover       --purple-300    8.28 / 7.88 / 7.53 / 7.06
     --sm-field-border (0.3)        --ink-400       4.82 / 4.59 / 4.38 / 4.11   the neutral edge
     --sm-field-border-hover (0.3)  --ink-300       8.40 / 7.99 / 7.64 / 7.16
     --sm-btn-rim-danger            --danger-500    5.01 / 4.77 / 4.56 / 4.27
     --sm-btn-rim-danger-hover      --danger-400    6.81 / 6.48 / 6.20 / 5.81
     --sm-btn-rim-success           --success-500   6.56 / 6.24 / 5.96 / 5.59
     --sm-btn-rim-success-hover     --success-400   8.34 / 7.94 / 7.59 / 7.12
   The washes only lift a control off its surface (1.10-1.35:1 on their own), so the 1px edge
   carries the boundary everywhere. The tonal wash is --brand at 18% (40% on hover). The neutral
   lift is white at 6% (10% on hover): it keeps a tool lighter than the surface it sits on and
   lighter than any field beside it, so an outlined button no longer looks like an outlined
   input (a field is --surface-field), and it never reads as a hole the way the old secondary's
   --surface-raised fill did on a modal's --surface-overlay.
   --sm-btn-selected-fill / -on are the SELECTED chip (a pressed toggle, the pager's current
   page, the selected tab (15.1)): --purple-300 is 8.28 / 7.88 / 7.53 / 7.06:1 on the four
   surfaces, and --ink-950 on it is 8.28:1. Inert on its own.
   ------------------------------------------------------------------------ */
:root{
  --sm-btn-rim-brand:var(--purple-400);
  --sm-btn-rim-brand-hover:var(--purple-300);
  --sm-btn-tonal-fill:var(--brand-a18);
  --sm-btn-tonal-fill-hover:var(--brand-a40);
  --sm-btn-tonal-on-hover:var(--purple-100);
  --sm-btn-neutral-fill:var(--white-a06);
  --sm-btn-neutral-fill-hover:var(--white-a10);
  --sm-btn-rim-danger:var(--danger-500);
  --sm-btn-rim-danger-hover:var(--danger-400);
  --sm-btn-rim-success:var(--success-500);
  --sm-btn-rim-success-hover:var(--success-400);
  --sm-btn-selected-fill:var(--purple-300);
  --sm-btn-selected-on:var(--ink-950);
}

/* ============================================================================
   1. Buttons
   ========================================================================= */
.sm-btn{
  position:relative;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:var(--gap-control);
  height:var(--control-height);
  padding:0 var(--space-4);
  margin:0;
  font-family:var(--font-sans);
  font-size:var(--text-base);
  font-weight:var(--weight-semibold);
  letter-spacing:var(--tracking-tight);
  line-height:1;
  white-space:nowrap;
  text-align:center;
  text-decoration:none;
  vertical-align:middle;
  color:var(--text);
  background-color:transparent;
  background-image:none;
  border:var(--border-hairline) solid transparent;
  border-radius:var(--radius-md);
  cursor:pointer;
  user-select:none;
  -webkit-appearance:none;
  appearance:none;
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard),
    color var(--sm-duration-fast) var(--ease-standard),
    box-shadow var(--sm-duration-base) var(--ease-standard),
    transform var(--sm-duration-instant) var(--ease-standard);
}

.sm-btn:hover,
.sm-btn:focus-visible{ text-decoration:none; }

/* Sizes */
.sm-btn--sm{
  height:var(--control-height-sm);
  padding:0 var(--space-3);
  font-size:var(--text-sm);
  border-radius:var(--radius-sm);
  gap:var(--space-15);
}
.sm-btn--lg{
  height:var(--control-height-lg);
  padding:0 calc(var(--space-5) + var(--space-05));
  font-size:var(--text-md);
  border-radius:var(--radius-md);
  gap:calc(var(--space-2) + var(--space-05));
}

/* Variants. The :not(:where(...)) guard costs zero specificity and keeps every
   hover/active state off disabled controls without pointer-events:none, so
   title tooltips on disabled buttons still fire. */
.sm-btn--primary{
  background-color:var(--brand);
  color:var(--brand-on);
  border-color:transparent;
}
.sm-btn--primary:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  color:var(--brand-on);
  background-color:var(--brand-hover);
  box-shadow:var(--glow-brand);
}
.sm-btn--primary:active:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  background-color:var(--brand-active);
  transform:var(--sm-press-transform);
}

.sm-btn--secondary{
  background-color:var(--surface-raised);
  color:var(--text);
  border-color:var(--border);
}
.sm-btn--secondary:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  color:var(--text);
  background-color:var(--surface-overlay);
  border-color:var(--border-strong);
}
.sm-btn--secondary:active:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  transform:var(--sm-press-transform);
}

.sm-btn--soft{
  background-color:var(--brand-subtle);
  color:var(--brand-text);
  border-color:transparent;
}
.sm-btn--soft:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  background-color:var(--brand-subtle-hover);
}
.sm-btn--soft:active:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  transform:var(--sm-press-transform);
}

.sm-btn--ghost{
  background-color:transparent;
  color:var(--text-secondary);
  border-color:transparent;
}
.sm-btn--ghost:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  background-color:var(--surface-hover);
  color:var(--text);
}
.sm-btn--ghost:active:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  transform:var(--sm-press-transform);
}

/* Filled status buttons sit on the 600 steps: the white label is 5.80:1 on --danger-600 and 4.61:1
   on --success-600, where the 500 fills gave 3.91:1 and 2.99:1. Hover DARKENS to the 0.3 steps
   (7.76:1 and 6.34:1), the rule section 10 settled for the light variants — lightening a status
   fill costs the label its contrast (--danger-400 was 2.88:1, --success-400 2.35:1). */
.sm-btn--danger{
  background-color:var(--danger-600);
  color:var(--ink-0);
  border-color:transparent;
}
.sm-btn--danger:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  background-color:var(--sm-danger-700);
}
.sm-btn--danger:active:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  transform:var(--sm-press-transform);
}

.sm-btn--success{
  background-color:var(--success-600);
  color:var(--ink-0);
  border-color:transparent;
}
.sm-btn--success:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  background-color:var(--sm-success-700);
}
.sm-btn--success:active:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  transform:var(--sm-press-transform);
}

/* --- 1.1 Button roles --------------------------------------------------------
   Henry, 9 Oct 2026: action buttons in "the darker grey/black color" were hard to see, and he
   asked for "a unified UI where people can easily recognize what button does what". Measured on
   the live pages before this block: every secondary and soft button had a 1.06-1.46:1 boundary
   (secondary's --surface-raised fill and white-10% hairline; soft's 12% brand wash with no
   border), the pager 1.26:1, Copy/Excel/PDF 1.39:1, and even the primary's --brand fill only
   2.09-2.46:1. WCAG 1.4.11 asks 3:1 for a control's boundary. Every label already passed 4.5:1.

   PURPLE MEANS "THIS DOES SOMETHING"; GREY MEANS "THIS IS A TOOL". Four looks, one per role (the
   role table is in DESIGN-SYSTEM-BRIEF):
     filled purple    .sm-btn--primary     the one main action of a region: save, submit, create
                                           or add, send or invite, pay, sign, apply
     tonal purple     .sm-btn--secondary   every other action: open/view, manage, edit, go to a
                      .sm-btn--soft        related page, contact, start an upload ("Upload …"),
                                           add another row. Soft takes the same paint: the row
                                           "View" was soft in 23 places and secondary in 40, so
                                           the two names had become one role
     neutral outline  .sm-btn--neutral     tools that leave data alone: close, cancel, back,
                                           defer or skip ("Set up later"), copy, export,
                                           download, QR, choose a file ("Choose File"), sort and
                                           filter, field helpers, the unselected toggle pills,
                                           the pager, carousel previous/next (17.3)
     filled red/green --danger, --success  destroy; approve someone else's request
   A SELECTED state (a pressed toggle, the current page, the selected tab) is not an action, so it
   never takes the primary's fill: it is 0.5's light chip with a dark label (the selected pill
   below, the pager in section 6, the tab rail in 15.1). --ghost stays text-only, for chrome (the
   topbar Log out).

   Every edge is the real 1px border .sm-btn already has, so no geometry changes. Never a
   box-shadow or a background-image: the focus ring replaces a box-shadow, and forced colours drop
   both, while a real border becomes a system-colour outline on every variant there. Rest edges
   clear 4.11:1 on every dark surface, and hover moves each edge one step LIGHTER (0.5 has the
   table), so a control grows on hover instead of fading; the old danger and success hovers
   darkened to 2.15-2.96:1. Labels on canvas / card / raised / overlay:
     primary   white on --brand 7.98 (hover --brand-hover 5.51, press --brand-active 10.15)
     tonal     --brand-text on the 18% wash 7.56 / 7.15 / 6.82 / 6.41;
               hover --purple-100 on the 40% wash 11.93 / 11.34 / 10.89 / 10.34
     neutral   --text on the white-6% lift 15.83 / 14.74 / 13.90 / 12.87;
               hover --text-strong on white-10% 15.54 / 14.33 / 13.45 / 12.41
     danger    white 5.80 (hover 7.76); success white 4.61 (hover 6.31)
   The primary keeps the brand fill, #6D30A7, the colour of every marketing CTA and email; its
   --purple-400 rim (2.21:1 against that fill, so it still reads as one solid button) is what
   clears 3:1 now. The 0.2 gap ring is untouched: its --surface-canvas band meets every edge here
   at 4.82:1 or more, and its --purple-300 band is 7.06:1 or more on every surface. Disabled stays
   opacity .45 (below), which leaves about a 1.9:1 ghost of each edge: plainly unavailable.

   SCOPE. The repaint is `:where(.sm-app)`: the logged-in app (<body class="sm-app">) and the dark
   public islands (.sm-app.sm-public-dark: the apply flow, the error pages, the valuation tool).
   The public shell (.sm-site: the light marketing pages, their ink bands and the site nav) takes
   the same roles from marketing.css section 16, through aliases that hold light values on white and
   this file's 0.5 values on ink, and the .sm-auth entry flow's outline button takes the light tonal
   paint in 10.2 (Henry, 9 Oct, wave 2).

   NEUTRAL WITHOUT MARKUP. Two kinds of existing secondary are tools by definition, so inside the
   app they turn neutral with no markup change:
   - [data-bs-dismiss]: a button that closes its modal. Today exactly the 88 "Close" and 3
     "Cancel" modal buttons, and every future footer that follows the brief's "secondary Close,
     then the primary".
   - [aria-pressed]: a toggle button (ARIA defines it so). Today exactly ViewBuildingDetails'
     four range pills, whose script toggles aria-pressed together with .is-active.
   Every other tool takes .sm-btn--neutral in markup. That class is unscoped and reads only
   tokens that are right on a white page too, so it needs no light twin: --sm-field-border is
   --ink-400 on both (4.07:1 on white), marketing.css rebinds --sm-field-border-hover to --ink-500
   (6.61:1) and flips --text and --text-strong, and the white lift is invisible on white.

   WEIGHT. :where() costs nothing, so each rule here weighs what the rule it repaints weighs:
   (0,1,0) at rest and (0,2,0) on :hover, and each wins by source order; the neutral rules follow
   the tonal ones for the same reason. The selected pill (0,4,0) and the pager (0,4,1) and up still
   outrank all of it. No box-shadow is declared here, so `.sm-btn:focus-visible` (next) keeps the
   ring and the primary glow keeps its tie-break. Every :hover restates `color`, or Bootstrap's
   a:hover (0,1,1) would repaint an <a class="sm-btn"> label #0a58ca. Paint only, no `display`.
   The variant names are JS contracts and are not renamed: Tenant/ViewUnits swaps Accept/Reject to
   sm-btn--secondary while a post is in flight (the busy one shows tonal at full opacity through
   18.1, the other at .45), and User/ViewPaymentDispute's upload handler builds its file-row
   remove "X" as sm-btn--danger (sm-btn--ghost until the 9 Oct swap Henry approved).
   ------------------------------------------------------------------------ */

/* Filled purple: the main action. The fill, label, glow and press are the rules above. */
:where(.sm-app) .sm-btn--primary{
  border-color:var(--sm-btn-rim-brand);
}
:where(.sm-app) .sm-btn--primary:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  color:var(--brand-on);
  border-color:var(--sm-btn-rim-brand-hover);
}

/* Tonal purple: every other action. Secondary and soft share it. */
:where(.sm-app) :is(.sm-btn--secondary, .sm-btn--soft){
  background-color:var(--sm-btn-tonal-fill);
  color:var(--brand-text);
  border-color:var(--sm-btn-rim-brand);
}
:where(.sm-app) :is(.sm-btn--secondary, .sm-btn--soft):hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  color:var(--sm-btn-tonal-on-hover);
  background-color:var(--sm-btn-tonal-fill-hover);
  border-color:var(--sm-btn-rim-brand-hover);
}

/* Neutral outline: tools. The press transform of a dismiss or toggle secondary comes from
   `.sm-btn--secondary:active` above; the standalone class gets its own. */
.sm-btn--neutral,
:where(.sm-app) .sm-btn--secondary:where([data-bs-dismiss], [aria-pressed]){
  background-color:var(--sm-btn-neutral-fill);
  color:var(--text);
  border-color:var(--sm-field-border);
}
.sm-btn--neutral:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)),
:where(.sm-app) .sm-btn--secondary:where([data-bs-dismiss], [aria-pressed]):hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  color:var(--text-strong);
  background-color:var(--sm-btn-neutral-fill-hover);
  border-color:var(--sm-field-border-hover);
}
.sm-btn--neutral:active:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  transform:var(--sm-press-transform);
}

/* Filled red and green keep their fills and their darkening hover fills (above, for the white
   label's sake) and gain a rim: one step lighter than the fill at rest, two on hover. The fill
   alone fell to 2.88:1 (danger on a modal) and the hover fills to 2.15-2.96:1. */
:where(.sm-app) .sm-btn--danger{
  border-color:var(--sm-btn-rim-danger);
}
:where(.sm-app) .sm-btn--danger:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  color:var(--ink-0);
  border-color:var(--sm-btn-rim-danger-hover);
}
:where(.sm-app) .sm-btn--success{
  border-color:var(--sm-btn-rim-success);
}
:where(.sm-app) .sm-btn--success:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  color:var(--ink-0);
  border-color:var(--sm-btn-rim-success-hover);
}

/* Focus is the single ring, and it outranks the primary glow by source order. */
.sm-btn:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}

.sm-btn:disabled,
.sm-btn[disabled],
.sm-btn[aria-disabled="true"],
.sm-btn.sm-is-disabled{
  opacity:.45;
  cursor:not-allowed;
  box-shadow:none;
  transform:none;
}
/* A control that only LOOKS disabled can still take focus — an <a> with aria-disabled (the busy
   anchors of 17.1), or .sm-is-disabled — and the rule above would erase its ring. Keyboard users
   still need to see where they are, so the ring comes back; only hover and press stay withheld. */
.sm-btn:is([aria-disabled="true"], .sm-is-disabled):focus-visible{
  box-shadow:var(--focus-ring);
}

/* Layout modifiers */
.sm-btn--block{
  display:flex;
  width:100%;
}
.sm-btn--icon{
  width:var(--control-height);
  padding:0;
  gap:0;
}
.sm-btn--icon.sm-btn--sm{ width:var(--control-height-sm); }
.sm-btn--icon.sm-btn--lg{ width:var(--control-height-lg); }

.sm-btn__icon{
  flex:0 0 auto;
  display:inline-flex;
  line-height:1;
}

.sm-btn-group{
  display:inline-flex;
  align-items:center;
  gap:var(--gap-control);
  flex-wrap:wrap;
}

/* The selected pill of a single-select .sm-btn-group (Building/ViewBuildingDetails' reporting
   range). Its script writes the JS-owned `is-active`, bridged here under that name (the
   .is-invalid precedent), and the markup carries aria-pressed, which is what assistive
   technology reads; either one selects. Paint only, no `display`.
   The unselected pills are neutral outlines (1.1 reads their aria-pressed), so the chosen one is
   FILLED: 0.5's selected chip, a --purple-300 fill and border with an --ink-950 label (8.28:1).
   Against the white-6% lift of the pills beside it that fill is 7.34 / 6.83 / 6.44 / 5.96:1 on
   canvas / card / raised / overlay. The old state differed by 1.02:1 in fill and was carried by
   a --brand-text border alone, which would now read as one more outline beside the --ink-400
   ones. The chip is the product's "you are here" colour (the pager's current page and the selected
   tab in 15.1 share it, and the focus ring is drawn in it) and deliberately NOT the primary's --brand: "Apply"
   (#assetTypeRangeApply) is a primary right after this group, and a brand-filled pill read as a
   second Apply in the 9 Oct prototypes. The border takes the fill's colour, so the box keeps its
   size and forced colours still draw it. (0,4,0) beats every variant at rest (0,1,0) and on
   hover (0,2,0), so the pill stays marked under the pointer. The focus ring is a box-shadow and
   is untouched; its --surface-canvas gap keeps its --purple-300 band apart from this fill. */
.sm-app .sm-btn-group > .sm-btn:is(.is-active, [aria-pressed="true"]){
  background-color:var(--sm-btn-selected-fill);
  border-color:var(--sm-btn-selected-fill);
  color:var(--sm-btn-selected-on);
}
/* Forced colours repaint every pill's fill and border alike, so the selected one takes the
   system selection pair.
   FORCED-COLOR-ADJUST:NONE IS REQUIRED on every Highlight-filled marker that carries text (this
   pill, the 15.1 tab, the section 6 pager's current page, the onboarding step (after 18.5) and
   the introjs-modern.css bullets). Chromium draws a Canvas-coloured readability backplate behind the
   text of any element left at `auto`, and HighlightText is the colour of Canvas in Windows' High
   Contrast Black and White themes and in Chrome's emulated palettes (black/black, white/white), so
   the label vanished into a solid block inside the Highlight fill (verification wave 4:
   W4-app-fixes-1, W4-details-2, W4-units-1).
   Opting out removes the backplate. Everything the opted-out box paints is then restated as a
   system colour, because nothing is forced for it any more: Highlight fill and border,
   HighlightText label, and box-shadow:none so the dark/purple focus ring (which forced colours
   would otherwise have dropped) does not return; the CanvasText focus outline of 0.2 stays.
   Verify with forced colours on BEFORE the page loads: injected into a page already painted in
   forced colours, a change of forced-color-adjust alone did not repaint the <a> pager in the
   harness (it did repaint the <button> tabs), while the same rule present at the first forced
   paint made the pager's "1" readable. Here the rule is always there from the first paint, and a
   marker that moves also changes its fill, which repaints it. */
@media (forced-colors: active){
  .sm-app .sm-btn-group > .sm-btn:is(.is-active, [aria-pressed="true"]){
    forced-color-adjust:none;
    background-color:Highlight;
    border-color:Highlight;
    color:HighlightText;
    box-shadow:none;
  }
}

/* 44px minimum touch target on touch devices only, so desktop row heights are
   unchanged. The strip is centred on the control and never paints. */
@media (pointer: coarse){
  /* Grow the control itself rather than overlaying an invisible hit strip: a 44px ::after on a
     32px button overhangs its neighbours and silently swallows their clicks. */
  .sm-btn{
    min-height:var(--touch-target-min);
  }
}

/* --- Inline icon button ------------------------------------------------------
   A glyph-only control inside a line of text: the copy-to-clipboard buttons beside the QuickBooks
   code and the account tag (Building/ViewAccount, Building/ViewBuildingAccounts).
     <button type="button" class="sm-icon-btn" aria-label="…"><i class="fa-solid fa-copy" aria-hidden="true"></i></button>
   .sm-btn--icon is a 40px chip; this has no chrome at all. It is the .sm-field__info recipe
   (section 4), start margin included: the glyph keeps its 1em box so the line does not grow,
   and ::after carries the 24px hit area (WCAG 2.5.8) and the focus ring, centred on the glyph.
   The ring reaches (24px - 1em) / 2 + 2 x 2px past the glyph, 9.5px at the 13px of a section
   header meta item, and without the start margin it painted about 3.5px over the last character
   of the code in front of it (the 6px meta-item gap was all the clearance there was; design-a11y
   DA4-F4). The margin is that reach, so any host clears it; in the meta item the gap adds 6px of
   daylight (15.5px from the text to the glyph).
   The glyph is a CHILD <i>: Font Awesome's all.css loads after this file, and its
   `.fa-solid{display:inline-block}` on the button itself would undo the centring. `display` is
   safe here because no script shows or hides these buttons (the handlers swap the child's icon
   class). Muted at rest (4.59:1 on --surface, 4.11:1 on --surface-overlay; an icon needs 3:1),
   --brand-text on hover and focus (7.88:1). font-size:inherit sizes the glyph to its text. */
.sm-icon-btn{
  position:relative;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  margin:0;
  margin-inline-start:calc((var(--space-6) - 1em) / 2 + var(--sm-focus-width) * 2);
  padding:0;
  font-size:inherit;
  line-height:1;
  color:var(--text-muted);
  background:none;
  border:0;
  cursor:pointer;
  -webkit-appearance:none;
  appearance:none;
  transition:color var(--sm-duration-fast) var(--ease-standard);
}
.sm-icon-btn::after{
  content:"";
  position:absolute;
  top:50%;
  left:50%;
  width:var(--space-6);
  height:var(--space-6);
  border-radius:var(--radius-circle);
  transform:translate(-50%, -50%);
}
.sm-icon-btn:hover,
.sm-icon-btn:focus-visible{ color:var(--brand-text); }
.sm-icon-btn:focus-visible{ outline:var(--sm-focus-outline); }
.sm-icon-btn:focus-visible::after{ box-shadow:var(--focus-ring); }

/* ============================================================================
   2. Cards and panels
   ========================================================================= */
.sm-card{
  position:relative;
  background-color:var(--surface);
  color:var(--text);
  border:var(--border-hairline) solid var(--border-subtle);
  border-radius:var(--radius-lg);
  box-shadow:var(--elev-dark-2);
  padding:var(--pad-card);
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard),
    box-shadow var(--sm-duration-base) var(--ease-standard),
    transform var(--sm-duration-base) var(--ease-standard);
}

/* A card that supplies its own header/body/footer owns its padding there. */
.sm-card:has(> .sm-card__header, > .sm-card__body, > .sm-card__footer){
  padding:0;
}

.sm-card__header{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--space-3);
  flex-wrap:wrap;
  padding:var(--space-4) var(--pad-card);
  border-bottom:var(--border-hairline) solid var(--border-subtle);
}
.sm-card__title{
  margin:0;
  font-family:var(--font-display);
  font-size:var(--text-md);
  font-weight:var(--weight-semibold);
  letter-spacing:var(--tracking-tight);
  line-height:var(--leading-tight);
  color:var(--text-strong);
}
.sm-card__subtitle{
  margin:0;
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  color:var(--text-muted);
}
.sm-card__actions{
  display:flex;
  align-items:center;
  gap:var(--gap-control);
  margin-inline-start:auto;
}
.sm-card__body{ padding:var(--pad-card); }
.sm-card__footer{
  display:flex;
  align-items:center;
  gap:var(--gap-control);
  flex-wrap:wrap;
  padding:var(--space-4) var(--pad-card);
  border-top:var(--border-hairline) solid var(--border-subtle);
}

.sm-card--interactive{ cursor:pointer; }
.sm-card--interactive:hover{
  transform:var(--sm-lift-transform);
  border-color:var(--border);
}
.sm-card--interactive:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
.sm-card--flush{ padding:0; }

.sm-panel{
  background-color:var(--surface);
  color:var(--text);
  border:var(--border-hairline) solid var(--border-subtle);
  border-radius:var(--radius-xl);
  padding:var(--pad-panel);
}
/* CONTRAST — the 12.9 technique. --text-muted is 4.38:1 on --surface-raised (and 4.11:1 on the
   --surface-overlay fill ViewAddBulkUnits gives its tenant panels inline), below the 4.5:1 floor
   for the .sm-field__hint and subtitle text these panels hold. Rebinding the token on the panel
   fixes every muted descendant at once; --text-secondary is 7.64:1 / 7.16:1 here. */
.sm-panel--raised{
  --text-muted:var(--text-secondary);
  background-color:var(--surface-raised);
  box-shadow:var(--elev-dark-2);
}
/* --overlay: the lightest fill, for a panel nested inside another (ViewAddBulkUnits' tenant
   panels, which used to set it inline). Muted text is 4.11:1 there, so the token is rebound the
   same way (--text-secondary 7.16:1); a field's --ink-400 boundary still clears 3:1 (4.11:1). */
.sm-panel--overlay{
  --text-muted:var(--text-secondary);
  background-color:var(--surface-overlay);
}
/* Builder panels nest two deep (ViewAddBulkUnits unit > tenant; the AddUnitLease/ViewUnitLease
   option sets), and 32px of padding on every level left 145px fields at 375. Below 576px each
   level pads 16px (tenant fields measured 209px). (0,2,0), so no modifier order can undo it. */
@media (max-width:575.98px){
  .sm-app .sm-panel{ padding:var(--space-4); }
}
/* From 576px to 991.98px the open rail takes 240px, so a panel INSIDE another panel still sits in
   a phone-width column: ViewAddBulkUnits' tenant panel at 768 (480px of content, inside a 24px
   card body and a 32px unit panel) left its two-up fields 137px wide and cut the Email
   placeholder to "Example@ema" (W4-units-3). There the nested panel pads 16px as well: 153px
   fields at 768. Its host panel keeps 32px, so adding the first tenant never moves the unit's own
   fields. (0,3,0). */
@media (min-width:576px) and (max-width:991.98px){
  .sm-app .sm-panel .sm-panel{ padding:var(--space-4); }
}

/* min(…, 100%) caps the track minimum at the grid's own content box. Without it a minimum wider
   than the container — 34rem media grids at 375px (327px of content) and at 768-831px (480-543px
   beside the rail) — overflows instead of shrinking, and .sm-media-card's overflow:hidden clips
   the card with no scrollbar. With the cap auto-fit resolves one track that fills the box exactly
   (327px at 375, 480px at 768), and every width at or above the minimum is unchanged. */
.sm-card-grid{
  --sm-card-min:240px;
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(min(var(--sm-card-min), 100%), 1fr));
  gap:var(--gap-card);
}

/* A grid holding media cards needs a much wider minimum track, and 240px is not it.
   .sm-media-card (12.4) splits into a FIXED 192px figure column plus the body, and its
   stacking rule is keyed to the VIEWPORT (the 767.98px query at the end of 12.4) — not to
   the card's own width, which is set by how many tracks auto-fit chose. At a 1152px content
   width, four tracks are 270px each: the 192px figure eats the track, the body column is
   left with ~76px, and .sm-media-card's overflow:hidden silently CLIPS the detail list —
   every <dd> value and most of the title disappear, with no scrollbar to hint at it.
   Measured: 2 cards (564px tracks) clean, 3 cards (368px) clipped, 4 cards (270px) severe.
   34rem = 544px keeps every media grid at or above the clean boundary and folds to one
   column early, which is the honest layout for a card that is half illustration.
   :has() carries this to all five media-card views with no markup change. Below 544px of
   content the base rule's min(…, 100%) takes over, so the single column shrinks with the
   container instead of overflowing it. */
.sm-card-grid:has(.sm-media-card){
  --sm-card-min:34rem;
}

/* A compact media card (12.4) keeps its 96px figure beside the body at every width, so it needs
   far less track than a full one: 22rem, prototype-measured on Manager/Alerts with no clipped
   value at 375, 768, 1280 or 1440. Same weight as the rule above and after it, so it wins. */
.sm-card-grid:has(.sm-media-card--compact){
  --sm-card-min:22rem;
}

/* Explicit modifiers. Doubled to (0,2,0) and written after the two :has() minimums above, so a
   class written on the grid beats a minimum inferred from what the grid holds.

   --fill: auto-fill instead of auto-fit keeps the empty tracks, so a lone card holds its track
   width instead of stretching across the row (a single lease, document or policy card; a lone
   QuickBooks building field reached 1087px).
   --records: --fill on a 28rem (448px) minimum, for grids of building and record cards
   (Owner/MyBuildings and Owner/ProformaModels; Manager/ManagedBuildings may adopt it). Two tracks
   open at 920px of content, where a media card's body keeps 256px, enough for a term and
   "$140,260.00", and the 1152px maximum never fits a third. Measured on MyBuildings with 8 cards:
   no clipping or overflow at 375, 768, 1208, 1280 or 1440; 1280 goes from one 977px column to two
   476px tracks, and a lone card at 1440 keeps 564px instead of 1152px. Opt-in rather than the
   media-grid default, because Tenant/ViewUnit and Building/ViewUnit want their lone card full width.
   --metrics: a 13rem (208px) minimum for .sm-metric tiles, so beside the rail at 768 four tiles sit
   two to a row instead of stacking four deep; dates still fit (Tenant/ViewUnitLease). */
.sm-card-grid.sm-card-grid--fill,
.sm-card-grid.sm-card-grid--records{
  grid-template-columns:repeat(auto-fill, minmax(min(var(--sm-card-min), 100%), 1fr));
}
.sm-card-grid.sm-card-grid--records{ --sm-card-min:28rem; }
.sm-card-grid.sm-card-grid--metrics{ --sm-card-min:13rem; }
/* --metrics-4: the --metrics tiles, never more than four to a row. The minimum is the larger of
   13rem and a quarter of the row after its three gaps (less 1px, so rounding can never push the
   fourth track out), so auto-fit cannot open a fifth: 8 tiles sit 4 + 4 at 1280 and 1440, where
   --metrics gave 5 + 3, 7 sit 4 + 3, two to a row at 768 and one at 375, where the base rule's
   min(…, 100%) takes over. Building/ViewBuildingFinancing and BuildingDepreciationSummary; a
   tile set of any size may adopt it. After --metrics, so it wins if both are written. */
.sm-card-grid.sm-card-grid--metrics-4{ --sm-card-min:max(13rem, (100% - var(--gap-card) * 3 - 1px) / 4); }
/* --metrics-3: exactly three tiles, one per row or all three in a row, never 2 + 1. Below 3 x 13rem
   + 2 gaps (672px) of content the minimum is just over half the row, so a second track cannot open;
   at 672px the step term reaches 0, the minimum drops to 13rem and three open at once. The
   multiplier only has to push the step term past half the row one LayoutUnit (1/64px) below 672px;
   32768 does that and stays inside the layout range for any content width up to 1696px. After
   --metrics-4, so it wins if both are written. No display. */
.sm-card-grid.sm-card-grid--metrics-3{ --sm-card-min:max(13rem, min((100% - var(--gap-card)) / 2 + 1px, (13rem * 3 + var(--gap-card) * 2 - 100%) * 32768)); }
/* --charts: chart cards, never more than two to a row. Building/ViewBuildingDetails' three chart
   banks (pass 2, 8 Oct 2026) sat on the base 240px minimum, which opened three or four tracks of
   242-310px at 1280-1440: at 1440 the Rent Collection plot was 146x163 in a 266px track. The
   minimum is the larger of 20rem and half the row after its one gap (less 1px, so rounding can
   never push the second track out), so auto-fit opens one track below 664px of content
   (2 x 20rem + the gap) and two from there up, never three. An odd last card (the fifth chart,
   Work Requests) spans the row instead of leaving half of it empty; on one track that moves
   nothing. After --metrics-3, so it wins if both are written. No display: each bank's wrapper is
   jQuery .show()/.hide()n, so the class goes on the grid inside it, never on the wrapper. */
.sm-card-grid.sm-card-grid--charts{ --sm-card-min:max(20rem, (100% - var(--gap-card) - 1px) / 2); }
.sm-card-grid.sm-card-grid--charts > .sm-card:last-child:nth-child(odd){ grid-column:1 / -1; }

/* ============================================================================
   3. Metric
   ========================================================================= */
.sm-metric{
  display:flex;
  flex-direction:column;
  gap:var(--space-2);
  min-width:0;
}
/* margin:0 on both parts is load-bearing, not tidiness. .sm-metric is a flex column
   whose `gap` is the only intended spacing, and these two classes get put on <p> in some
   views and <span> in others — Bootstrap Reboot gives <p> a margin-bottom:1rem that would
   add to the gap and make otherwise-identical tiles taller in one view than another. */
.sm-metric__label{
  margin:0;
  font-size:var(--text-xs);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-caps);
  text-transform:uppercase;
  color:var(--text-muted);
}
/* overflow-wrap:anywhere: a value too long for its tile (an unrounded IRR, a long id) breaks inside
   the tile instead of running past it or being clipped by the card. It only breaks where nothing
   else fits, so a value that fits renders exactly as before. It is the LAST resort: a tile in a
   card grid first scales its value to its own width (the rules after --lg/--sm below), so a real
   figure never reaches it; the normal break opportunities (spaces, after a slash or dash) are
   always tried before it breaks inside a word. */
.sm-metric__value{
  margin:0;
  font-family:var(--font-display);
  font-size:var(--text-metric);
  font-weight:var(--weight-bold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-display);
  color:var(--text-strong);
  font-variant-numeric:var(--numeric-tabular);
  font-feature-settings:"tnum" 1;
  overflow-wrap:anywhere;
}
.sm-metric__hint{
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  color:var(--text-muted);
}
/* An empty hint still costs the tile's 8px gap. Tenant/ViewPayRent prints @Model.RentalPeriod,
   which that action never assigns, so three tiles carried a 0px hint. No script reads or writes
   this class, and text that arrives later ends :empty, so the hint shows again. The precedent is
   .sm-field__error:empty (section 10). */
.sm-metric__hint:empty{ display:none; }
.sm-metric__delta{
  display:inline-flex;
  align-items:center;
  align-self:flex-start;
  gap:var(--space-1);
  min-height:calc(var(--space-5) + var(--space-05));
  padding:0 var(--space-2);
  font-size:var(--text-xs);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  border-radius:var(--radius-pill);
  border:var(--border-hairline) solid var(--neutral-border);
  background-color:var(--neutral-subtle);
  color:var(--text-secondary);
  font-variant-numeric:var(--numeric-tabular);
  font-feature-settings:"tnum" 1;
  white-space:nowrap;
}
.sm-metric__delta--up{
  background-color:var(--success-subtle);
  border-color:var(--success-border);
  color:var(--success-text);
}
.sm-metric__delta--down{
  background-color:var(--danger-subtle);
  border-color:var(--danger-border);
  color:var(--danger-text);
}
.sm-metric--lg .sm-metric__value{ font-size:var(--text-metric-lg); }
.sm-metric--sm .sm-metric__value{ font-size:var(--text-xl); }
/* A metric tile in a card grid scales its value with its OWN width, never the window's (the auto-fit
   grid, not the viewport, decides how wide a tile is: DESIGN-SYSTEM-BRIEF, Stage 5). Fixed 32px
   money broke mid-number: --metrics-4 tiles hold a 178px value box at 768, 174px at 1024 and 176px
   at 1280, and Archivo 700 tabular "$15,481,750" is 181.7px at 32px, so BuildingDepreciationSummary
   printed "$15,481,75" over "0" (W4-fin-b-1). 15% of the tile is the size: 26.2-26.7px in those
   tiles (148-152px for that value), and the 32px step from a 213px value box up (1440's 216px tiles, every
   single-column phone tile), so wide tiles are unchanged. 15cqi fits 6.67em, e.g. "$9,999,999.99" or
   "$999,999,999"; anything longer still has A11. --sm and --lg keep their steps as the ceiling, and
   every step keeps a floor. The container is .sm-metric, a block child of a grid item whose width
   comes from its track, so inline-size containment changes no size; the tracks have fixed minimums,
   so the items' content never sized them either. All three are (0,1,0)/(0,2,0) and after the rules
   they refine; :where() keeps marketing.css's `.sm-valuation .sm-metric__value` (14cqi, (0,2,0),
   the valuation tool's 14-character money) in charge there. A browser without container units drops
   the declarations and keeps the fixed steps. */
:where(.sm-card-grid > .sm-card) > .sm-metric{ container-type:inline-size; }
:where(.sm-card-grid > .sm-card > .sm-metric) > .sm-metric__value{ font-size:clamp(var(--text-xl), 15cqi, var(--text-metric)); }
:where(.sm-card-grid > .sm-card) > .sm-metric--lg > .sm-metric__value{ font-size:clamp(var(--text-metric), 15cqi, var(--text-metric-lg)); }
:where(.sm-card-grid > .sm-card) > .sm-metric--sm > .sm-metric__value{ font-size:clamp(var(--text-lg), 15cqi, var(--text-xl)); }
/* --metrics-4 tiles are the narrow ones (a 174-178px value box from 768 to 1280, 216px at 1440, the
   13rem track minimum leaves 158px) and they carry money in cents: Building/ViewCapExTransaction's
   core asset prints "$14,550,000.00" and "$10,669,999.92", and Building/ViewFinancing's loans
   "$9,800,000.00". 15cqi fits 6.67em; Archivo 700 with tabular figures and the -0.025em tracking
   measures 7.13em for the 14-character pair (digits 0.598em, "$" 0.556em, "," and "." 0.307em,
   shaped with HarfBuzz from the served font), so both broke a digit onto a second line at 768,
   1024, 1280 and 1440 (F-bldg-b-1). In these grids the value takes 12.5cqi, which fits 8em: the
   widest 15-character figure, "$100,000,000.00", is 7.71em. Sizes: 22.25px at 768, 21.79px at
   1024, 22.03px at 1280, 27.03px at 1440, 32px in a phone's single column. The floor drops to
   --text-lg so the guarantee holds down to the 158px box (19.75px there); above a 256px box the
   32px step still caps it. The plain --metrics grids get the same scale (orchestrator, 6 Oct): their
   2-up tiles at 768 have the same 178px value box and carry 13-character balances today
   (ViewAccount "$1,168,980.00"), so a 14-character balance would break the same way. Every other
   grid keeps 15cqi. :where() keeps it at (0,1,0), after the rule it refines; --lg and --sm keep
   their own steps. No display. */
:where(.sm-card-grid--metrics-4 > .sm-card > .sm-metric, .sm-card-grid--metrics > .sm-card > .sm-metric) > .sm-metric__value{ font-size:clamp(var(--text-lg), 12.5cqi, var(--text-metric)); }

/* ============================================================================
   4. Fields and form controls
   ========================================================================= */
.sm-field{
  display:flex;
  flex-direction:column;
  gap:var(--space-15);
  min-width:0;
  margin-bottom:var(--space-4);
}
.sm-field--inline{
  flex-direction:row;
  align-items:center;
  gap:var(--space-2);
}

/* Labels sit at the inline start, aligned with the control they label. */
.sm-field__label{
  display:block;
  text-align:start;
  font-family:var(--font-sans);
  font-size:var(--text-sm);
  font-weight:var(--weight-medium);
  line-height:var(--leading-tight);
  color:var(--text-secondary);
}
.sm-field__label--required::after{
  content:"*";
  margin-inline-start:var(--space-1);
  color:var(--danger-text);
}
.sm-field__hint{
  font-size:var(--text-xs);
  line-height:var(--leading-snug);
  color:var(--text-muted);
}
.sm-field__error{
  font-size:var(--text-xs);
  line-height:var(--leading-snug);
  color:var(--danger-text);
}
/* A character counter written by script: textCounter() sets `countfield.value` ("40/45"), so it
   has to stay an <input>, and it cannot take .sm-input, whose :disabled state is a dimmed field.
     <input disabled id="…" class="sm-field__counter" aria-hidden="true" />
   It reads as a hint line under the field instead: no box, 12px --text-secondary (7.99:1 on
   --surface, 7.16:1 in a modal). aria-hidden, because it only echoes the field's maxlength and,
   unlabelled, it failed axe's critical `label` rule. -webkit-text-fill-color and opacity undo the
   engines' own dimming of disabled text (WebKit paints it through the fill colour; iOS also lowers
   the opacity). Building/ViewAccount, ViewBuildingAccounts, ViewBuildingAssumptions replace their
   inline transparent/white styles with it. (0,2,1). */
.sm-app input.sm-field__counter{
  width:auto;
  min-height:0;
  margin:0;
  padding:0;
  border:0;
  background:transparent;
  font-size:var(--text-xs);
  line-height:var(--leading-snug);
  color:var(--text-secondary);
  -webkit-text-fill-color:var(--text-secondary);
  opacity:1;
  cursor:default;
}

/* Info-tooltip trigger beside a label: `<i class="fa-solid fa-circle-info sm-field__info"
   tabindex="0" data-bs-toggle="tooltip" title="…">` or the same classes on a
   `<button type="button">` — the view supplies the focusability and the accessible name (see the
   settled convention on interactive non-native elements); this supplies the rest.
   The glyph keeps its own 1em box so the label's line does not grow. The 24px hit area (WCAG 2.5.8
   minimum) and the focus ring both live on ::after, centred on the glyph: Font Awesome draws the
   icon with ::before, so ::after is free, and a ring round the hit area reads better than one
   hugging a 13px glyph. No `display` here — Font Awesome's own inline-block stands. The button
   resets are inert on an <i>. Muted at rest (4.59:1 on --surface; an icon needs 3:1), brand
   text on hover and focus (7.88:1). */
.sm-field__info{
  position:relative;
  margin:0;
  /* The ::after hit area overhangs the 1em glyph by (24px - 1em) / 2 on each side, and the ring adds
     two focus widths beyond that: 9.5px at the 13px label size. Without this start margin the ring
     was drawn over the last letter of the label text in front of it. */
  margin-inline-start:calc((var(--space-6) - 1em) / 2 + var(--sm-focus-width) * 2);
  padding:0;
  border:0;
  background:none;
  color:var(--text-muted);
  line-height:1;
  cursor:help;
  transition:color var(--sm-duration-fast) var(--ease-standard);
}
.sm-field__info::after{
  content:"";
  position:absolute;
  top:50%;
  left:50%;
  width:var(--space-6);
  height:var(--space-6);
  border-radius:var(--radius-circle);
  transform:translate(-50%, -50%);
}
.sm-field__info:hover,
.sm-field__info:focus-visible{ color:var(--brand-text); }
.sm-field__info:focus-visible{ outline:var(--sm-focus-outline); }
.sm-field__info:focus-visible::after{ box-shadow:var(--focus-ring); }
/* A trigger written BESIDE its label (the settled form: a flex row holding the <label
   class="sm-field__label"> and then the <button class="sm-field__info">) no longer sits inside
   the label, so it inherited the field's 15px: the glyph drew larger than the 13px label, and its
   taller box pushed the control below 1px lower than its row neighbour (User/ViewCompany's
   Statement Descriptor beside Country). The label's size puts both back, and the start margin
   above resolves to the 9.5px it was written for. Headings keep their own size. */
.sm-field__label + .sm-field__info{ font-size:var(--text-sm); }

.sm-input,
.sm-select,
.sm-textarea{
  display:block;
  width:100%;
  min-width:0;
  height:var(--control-height);
  padding:0 var(--space-3);
  margin:0;
  font-family:var(--font-sans);
  font-size:var(--text-base);
  font-weight:var(--weight-regular);
  line-height:var(--leading-tight);
  color:var(--text);
  background-color:var(--surface-field);
  /* ≥3:1 at rest (0.3): --ink-400 is 4.69:1 on the field and 4.11:1 on --surface-overlay. */
  border:var(--border-hairline) solid var(--sm-field-border);
  border-radius:var(--radius-md);
  box-shadow:none;
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard),
    box-shadow var(--sm-duration-fast) var(--ease-standard);
}
.sm-input,
.sm-textarea{
  -webkit-appearance:none;
  appearance:none;
}
.sm-textarea{
  height:auto;
  min-height:calc(var(--control-height) * 2.5);
  padding:var(--space-3);
  line-height:var(--leading-normal);
  resize:vertical;
}
.sm-input::placeholder,
.sm-textarea::placeholder{
  color:var(--text-muted);
  opacity:1;
}
.sm-input:hover,
.sm-select:hover,
.sm-textarea:hover{ border-color:var(--sm-field-border-hover); }

/* The focused border is --brand-text, not --brand: --brand is 2.39:1 on the field, so focusing a
   control used to make its boundary DIMMER than at rest. --brand-text is 8.07:1 and reads as one
   piece with the ring outside it. The on-light variant (10.2) keeps --brand, 7.98:1 on white. */
.sm-input:focus-visible,
.sm-select:focus-visible,
.sm-textarea:focus-visible{
  outline:var(--sm-focus-outline);
  border-color:var(--brand-text);
  box-shadow:var(--focus-ring);
}
/* Disabled/readonly fields are dimmed with COLOUR, not opacity. `opacity:.45` composites --text
   down to roughly #76767A on --surface-field — 4.26:1, below the 4.5:1 AA floor — and several views
   in this product are permanently read-only review screens rendered as dead forms
   (Tenant/ViewTenantApplication alone has ~100 statically `disabled` controls, every one of them a
   value the reader has to be able to READ). --text-secondary is 8.24:1 on the same surface and still
   reads as inactive next to a live field, and the sunken fill plus not-allowed cursor carry the
   affordance that the opacity used to. */
.sm-input:disabled,
.sm-select:disabled,
.sm-textarea:disabled,
.sm-input[readonly],
.sm-textarea[readonly]{
  color:var(--text-secondary);
  background-color:var(--surface-sunken);
  border-color:var(--border);
  cursor:not-allowed;
}
/* A read-only note sizes to its text where the engine supports field-sizing (Chromium 123+), up to
   384px and then it scrolls: Building/ViewUnitApplication showed a 700-character applicant note in
   a 100px box, three lines at a time at 375. min-height still holds, so a short note keeps the
   field's size, and an engine without field-sizing keeps the fixed box. Read-only and disabled
   only: a field being typed into must not grow under the cursor. */
.sm-textarea:is(:disabled, [readonly]){
  field-sizing:content;
  max-height:calc(var(--space-32) * 3);
}
/* A file picker in .sm-input (Tenant/ViewTenantInsurance's policy upload, EditBuildingDetails'
   photo). The platform's "Choose File" button is a light-grey chip on the dark field; here it
   becomes the field's leading segment instead, in the neutral tool paint of 1.1: 0.5's white-6%
   lift (white-10% on hover) under a --text label (15.22:1), set off from the file name by an
   --sm-field-border hairline, the same line as the field's own outline (4.69:1 on the field). That
   outline (0.3, 4.11:1 or better) bounds the whole control. The start corners take the input's
   own radius, so the segment follows its curve. */
.sm-input[type="file"]{ padding-inline-start:0; }
.sm-input[type="file"]::file-selector-button{
  height:100%;
  margin:0;
  margin-inline-end:var(--space-3);
  padding:0 var(--space-4);
  font:inherit;
  font-weight:var(--weight-semibold);
  color:var(--text);
  background-color:var(--sm-btn-neutral-fill);
  border:0;
  border-inline-end:var(--border-hairline) solid var(--sm-field-border);
  border-start-start-radius:inherit;
  border-end-start-radius:inherit;
  cursor:pointer;
  transition:background-color var(--sm-duration-fast) var(--ease-standard);
}
.sm-input[type="file"]:hover:not(:disabled)::file-selector-button{ background-color:var(--sm-btn-neutral-fill-hover); }
.sm-input[type="file"]:disabled::file-selector-button{ cursor:not-allowed; }
/* A visually hidden file input opened by a <label> styled as a button (Building/AddBuilding's
   photo upload: <input type="file" class="sm-visually-hidden"> then <div class="sm-row"><label
   for="…" class="sm-btn …">). The input is the one that takes focus, so the label shows the ring
   for it: as the input's next sibling, or inside the .sm-row that follows it. */
.sm-app input[type="file"].sm-visually-hidden:focus-visible + label.sm-btn,
.sm-app input[type="file"].sm-visually-hidden:focus-visible + .sm-row > label.sm-btn{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
.sm-input--invalid,
.sm-select--invalid,
.sm-textarea--invalid,
/* `.is-invalid` is Bootstrap's class, and view scripts across this codebase toggle it directly —
   63 addClass("is-invalid")/removeClass("is-invalid") call sites over six views. It used to be
   painted by Bootstrap's own `.form-control.is-invalid`, which only matches while the control also
   carries `.form-control`. Restyling a view to `.sm-input` therefore silently removes the ONLY
   visible error state those scripts produce — in ViewUser that left six required fields rejecting a
   submit with no on-screen feedback at all (WCAG 3.3.1, Level A). Bridging the class here fixes
   every converted view at once and needs no JS edit, which matters because the three Building views
   that still use it are not converted yet. */
.sm-input.is-invalid,
.sm-select.is-invalid,
.sm-textarea.is-invalid,
/* `.input-validation-error` is the server-side twin: MVC's input tag helper writes it when a field
   comes back with a ModelState error, and jquery-validation-unobtrusive toggles it on the client.
   It was bridged only for the --on-light controls (10.2), so a dark `asp-for` field that failed
   server validation came back with no red border at all. */
.sm-input.input-validation-error,
.sm-select.input-validation-error,
.sm-textarea.input-validation-error,
/* `.unmapped-select` is the same kind of class: owned by CompanyQuickBooksMapping's own script,
   which toggles it at ten call sites. Doubled to (0,2,0) so it also beats `.sm-select:hover`. */
.sm-select.unmapped-select.unmapped-select{ border-color:var(--danger); }
/* `.mapping-warning` is built into an HTML string by that same script, so the class name cannot be
   swapped for .sm-field__error from markup. It deliberately declares NO `display`: jQuery .toggle()s
   these spans, and on restore jQuery writes the tag's default display, so any display here would
   degrade after one cycle. */
.sm-app .mapping-warning{
  font-size:var(--text-xs);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-snug);
  color:var(--danger-text);
}
/* The focus ring of an invalid field is the 0.2 gap ring in the full danger colour (5.01:1 against
   its gap, 4.56:1 on --surface-raised); the old 3px --danger-subtle wash was 1.14:1. Light scopes
   redeclare --sm-focus-ring-danger with a --page gap and --danger-600, so the on-light fields in
   the entry flow get the light version through this same rule. */
.sm-input--invalid:focus-visible,
.sm-select--invalid:focus-visible,
.sm-textarea--invalid:focus-visible,
.sm-input.is-invalid:focus-visible,
.sm-select.is-invalid:focus-visible,
.sm-textarea.is-invalid:focus-visible,
.sm-input.input-validation-error:focus-visible,
.sm-select.input-validation-error:focus-visible,
.sm-textarea.input-validation-error:focus-visible{
  border-color:var(--danger);
  box-shadow:var(--sm-focus-ring-danger);
}

.sm-input--sm,
.sm-select--sm{
  height:var(--control-height-sm);
  font-size:var(--text-sm);
  border-radius:var(--radius-sm);
}
.sm-input--lg,
.sm-select--lg{
  height:var(--control-height-lg);
  font-size:var(--text-md);
}

/* iOS Safari zooms the page when a focused field's text is under 16px, and does not zoom back out;
   --text-base (15px) and --sm (13px) both trigger it. --root-font-size is the system's 16px root,
   and max() keeps a larger reader setting instead of capping it. Touch-primary devices only, so
   desktop density is unchanged, and after the size modifiers so --sm is covered too. The
   --on-light controls always carry the base class, so they are included; the 18px code inputs
   (10.2) are later in the file and keep their size. */
@media (pointer: coarse){
  .sm-input,
  .sm-select,
  .sm-textarea{ font-size:max(var(--root-font-size), var(--text-md)); }
}

/* Opt-in wrapper that replaces the native select indicator with a token-coloured
   chevron. Without the wrapper .sm-select keeps the native indicator, which
   follows `color` — no hard-coded SVG fill anywhere in this file. */
.sm-select-wrap{
  position:relative;
  display:block;
  min-width:0;
}
.sm-select-wrap > .sm-select{
  -webkit-appearance:none;
  appearance:none;
  padding-inline-end:var(--space-8);
}
.sm-select-wrap::after{
  content:"";
  position:absolute;
  inset-inline-end:var(--space-4);
  top:calc(50% - var(--space-15));
  width:var(--space-2);
  height:var(--space-2);
  border-right:var(--border-emphasis) solid var(--text-muted);
  border-bottom:var(--border-emphasis) solid var(--text-muted);
  transform:rotate(45deg);
  pointer-events:none;
}

.sm-input-group{
  display:flex;
  align-items:center;
  gap:var(--gap-control);
  min-width:0;
}
.sm-input-group > .sm-input,
.sm-input-group > .sm-select{ flex:1 1 auto; }
/* The addon is a --surface-raised chip, where --text-muted is 4.38:1; rebinding the token on it
   (the 12.9 technique) puts its label at 7.64:1. */
.sm-input-addon{
  --text-muted:var(--text-secondary);
  flex:0 0 auto;
  display:inline-flex;
  align-items:center;
  height:var(--control-height);
  padding:0 var(--space-3);
  font-size:var(--text-sm);
  color:var(--text-muted);
  background-color:var(--surface-raised);
  border:var(--border-hairline) solid var(--border);
  border-radius:var(--radius-md);
}
/* A from–to pair that wraps instead of squeezing (Tenant/ViewTenantApplication's lease-term dates
   lost their year at 375, and its business-hours selects read "12:0" at 768). Each control keeps a
   152px floor, wide enough for mm/dd/yyyy and its picker at the 16px touch size, and below that the
   pair breaks onto two lines. Same weight as `.sm-input-group > .sm-input` and after it. */
.sm-input-group--range{ flex-wrap:wrap; }
.sm-input-group--range > .sm-input,
.sm-input-group--range > .sm-select{
  flex:1 1 calc(var(--space-32) + var(--space-6));
  min-width:0;
}
/* --wrap: a value with trailing actions (Building/EditBuildingDetails' available-units link with
   its Copy and QR Code buttons). The control keeps a 256px floor and the buttons wrap under it,
   where the value used to collapse to 26-52px at 375. The group's gap spaces both rows. Same
   weight as `.sm-input-group > .sm-input` and after it. */
.sm-input-group--wrap{ flex-wrap:wrap; }
.sm-input-group--wrap > .sm-input,
.sm-input-group--wrap > .sm-select{ flex:1 1 calc(var(--space-32) * 2); }

/* Checkbox / radio */
.sm-checkbox,
.sm-radio{
  -webkit-appearance:none;
  appearance:none;
  flex:0 0 auto;
  display:inline-grid;
  place-content:center;
  width:var(--space-5);
  height:var(--space-5);
  margin:0;
  background-color:var(--surface-field);
  border:var(--border-hairline) solid var(--sm-field-border);
  border-radius:var(--radius-xs);
  cursor:pointer;
  vertical-align:middle;
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard),
    box-shadow var(--sm-duration-fast) var(--ease-standard);
}
.sm-radio{ border-radius:var(--radius-circle); }

.sm-checkbox::before{
  content:"";
  width:var(--space-3);
  height:var(--space-3);
  transform:scale(0);
  transition:transform var(--sm-duration-fast) var(--ease-standard);
  background-color:var(--brand-on);
  clip-path:polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
.sm-radio::before{
  content:"";
  width:var(--space-2);
  height:var(--space-2);
  border-radius:var(--radius-circle);
  transform:scale(0);
  transition:transform var(--sm-duration-fast) var(--ease-standard);
  background-color:var(--brand-on);
}
.sm-checkbox:checked,
.sm-radio:checked{
  background-color:var(--brand);
  border-color:var(--brand);
}
.sm-checkbox:checked::before,
.sm-radio:checked::before{ transform:scale(1); }
.sm-checkbox:indeterminate{
  background-color:var(--brand);
  border-color:var(--brand);
}
.sm-checkbox:indeterminate::before{
  transform:scale(1);
  clip-path:polygon(12% 42%, 88% 42%, 88% 58%, 12% 58%);
}
/* Hover strengthens the boundary of an UNCHECKED box only. It used to go to --brand, which is
   2.39:1 on the field — weaker than the new resting --ink-400 — and a checked box is already a
   brand fill, so a lighter border there would only ring it. */
.sm-checkbox:hover:not(:checked, :indeterminate, :disabled),
.sm-radio:hover:not(:checked, :disabled){ border-color:var(--sm-field-border-hover); }
.sm-checkbox:focus-visible,
.sm-radio:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
.sm-checkbox:disabled,
.sm-radio:disabled{
  opacity:.45;
  cursor:not-allowed;
}
/* A disabled box that is CHECKED is a recorded answer on a read-only review screen
   (Building/ViewUnitApplication, Tenant/ViewTenantApplication, the ApplicationTemplate preview,
   User/ViewUser's roles, ViewPaymentDispute's evidence): at .45 the chosen radio composited to
   1.36:1 on the card and its dot to 3.3:1 on it. Checked, it returns to full strength (the white
   mark is 7.98:1 on --brand); unchecked options keep .45, so the chosen one stands out. The
   not-allowed cursor stays. (0,3,0) against the (0,2,0) above. */
.sm-checkbox:disabled:is(:checked, :indeterminate),
.sm-radio:disabled:checked{ opacity:1; }

/* Switch */
.sm-switch{
  -webkit-appearance:none;
  appearance:none;
  position:relative;
  flex:0 0 auto;
  width:calc(var(--space-10) + var(--space-1));
  height:calc(var(--space-6) - var(--space-05));
  margin:0;
  background-color:var(--surface-field);
  border:var(--border-hairline) solid var(--sm-field-border);
  border-radius:var(--radius-pill);
  cursor:pointer;
  vertical-align:middle;
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard),
    box-shadow var(--sm-duration-fast) var(--ease-standard);
}
.sm-switch::after{
  content:"";
  position:absolute;
  top:50%;
  inset-inline-start:var(--space-05);
  width:var(--space-4);
  height:var(--space-4);
  border-radius:var(--radius-circle);
  background-color:var(--text-secondary);
  transform:translateY(-50%);
  transition:
    transform var(--sm-duration-fast) var(--ease-standard),
    background-color var(--sm-duration-fast) var(--ease-standard);
}
.sm-switch:checked{
  background-color:var(--brand);
  border-color:var(--brand);
}
/* 22px = the 42px padding box, less the 16px thumb, less 2px of inset either side. */
.sm-switch:checked::after{
  transform:translateY(-50%) translateX(calc(var(--space-6) - var(--space-05)));
  background-color:var(--brand-on);
}
.sm-switch:hover:not(:checked, :disabled){ border-color:var(--sm-field-border-hover); }
.sm-switch:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
.sm-switch:disabled{
  opacity:.45;
  cursor:not-allowed;
}

/* Larger hit areas for the 20/22px controls on touch devices. */
@media (pointer: coarse){
  .sm-checkbox,
  .sm-radio{
    width:var(--space-6);
    height:var(--space-6);
  }
}

/* Forced colours. Every control above paints its state with background-color: the checked fill,
   the ::before tick and dot, the switch knob. Forced colours repaint all of them as Canvas, so a
   ticked box, a chosen radio and an on switch looked exactly like their empty twins
   (User/CreateUser's roles and terms, Signature/CreateSignature's consent, the apply flow's
   Yes/No answers, the valuation tool's Input/Analysis choice). System colours are not forced, so the
   states take the system selection pair and the knob the text colour, with no
   forced-color-adjust; disabled controls take GrayText. Same weights as the rules they repaint
   and after them, which also covers the entry flow's --on-light boxes (10.2 restyles only the
   unchecked box, at (0,1,0)) and the public-dark pages (section 19 repaints none of these). */
@media (forced-colors: active){
  .sm-checkbox:is(:checked, :indeterminate),
  .sm-radio:checked,
  .sm-switch:checked{
    background-color:Highlight;
    border-color:Highlight;
  }
  .sm-checkbox:is(:checked, :indeterminate)::before,
  .sm-radio:checked::before{ background-color:HighlightText; }
  .sm-switch::after{ background-color:CanvasText; }
  .sm-switch:checked::after{ background-color:HighlightText; }
  .sm-checkbox:disabled,
  .sm-radio:disabled,
  .sm-switch:disabled{ border-color:GrayText; }
  .sm-checkbox:disabled:is(:checked, :indeterminate),
  .sm-radio:disabled:checked,
  .sm-switch:disabled:checked{ background-color:GrayText; }
  .sm-checkbox:disabled:is(:checked, :indeterminate)::before,
  .sm-radio:disabled:checked::before,
  .sm-switch:disabled:checked::after{ background-color:Canvas; }
  .sm-switch:disabled:not(:checked)::after{ background-color:GrayText; }
}

/* --- Form sections and the action row -------------------------------------
   For later adoption; no view carries either yet. A long form is a stack of
   titled groups that ends in one row of actions, and today each view improvises
   both — a .sm-section-header--sm over its fields, a .sm-row round its buttons.
   No app view has a single <fieldset>.

   .sm-form-section goes on a <fieldset> with __title on its <legend> (the
   honest element for a group of controls), or on a <div> or <section> with an
   <h2>/<h3> title. The fieldset box is reset here as well as by Bootstrap's
   reboot, so the class is element-agnostic. The reboot also floats a <legend>
   (to take it out of the border notch) and clears whatever follows it; that
   stays, so __title sets type and spacing only. A second section is set off
   from the first by a hairline and 32px.

   .sm-form-actions is the closing row: right-aligned, primary action LAST in
   source so it lands on the trailing edge. Below 768px it becomes a column of
   full-width buttons in source order. Never column-reverse: that would put the
   visual order out of step with the tab order.
   ------------------------------------------------------------------------ */
.sm-form-section{
  min-width:0;
  margin:0;
  padding:0;
  border:0;
}
.sm-form-section + .sm-form-section{
  margin-top:var(--space-8);
  padding-top:var(--space-6);
  border-top:var(--border-hairline) solid var(--border-subtle);
}
.sm-form-section__title{
  margin:0 0 var(--space-4);
  padding:0;
  font-family:var(--font-display);
  font-size:var(--text-md);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-tight);
  color:var(--text-strong);
}
.sm-form-section__title:has(+ .sm-form-section__hint){ margin-bottom:var(--space-1); }
.sm-form-section__hint{
  margin:0 0 var(--space-4);
  max-width:var(--prose-max);
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  color:var(--text-muted);
}
.sm-form-actions{
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  justify-content:flex-end;
  gap:var(--gap-control);
  margin:var(--space-6) 0 0;
}
@media (max-width:767.98px){
  .sm-form-actions{
    flex-direction:column;
    align-items:stretch;
  }
}

/* ============================================================================
   5. Badges and avatar
   ========================================================================= */
.sm-badge{
  display:inline-flex;
  align-items:center;
  gap:var(--space-15);
  min-height:calc(var(--space-5) + var(--space-05));
  padding:0 var(--space-2);
  font-family:var(--font-sans);
  font-size:var(--text-xs);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-tight);
  white-space:nowrap;
  vertical-align:middle;
  border-radius:var(--radius-pill);
  border:var(--border-hairline) solid var(--neutral-border);
  background-color:var(--neutral-subtle);
  color:var(--text-secondary);
}
.sm-badge--success{
  background-color:var(--success-subtle);
  border-color:var(--success-border);
  color:var(--success-text);
}
.sm-badge--warning{
  background-color:var(--warning-subtle);
  border-color:var(--warning-border);
  color:var(--warning-text);
}
.sm-badge--danger{
  background-color:var(--danger-subtle);
  border-color:var(--danger-border);
  color:var(--danger-text);
}
.sm-badge--info{
  background-color:var(--info-subtle);
  border-color:var(--info-border);
  color:var(--info-text);
}
.sm-badge--neutral{
  background-color:var(--neutral-subtle);
  border-color:var(--neutral-border);
  color:var(--text-secondary);
}
/* Brand: a label that is the product's own rather than a status, such as a plan name or "New".
   The .sm-empty__medallion pairing: brand wash, brand text (7.4:1 on it over --surface). */
.sm-badge--brand{
  background-color:var(--brand-subtle);
  border-color:var(--brand-a40);
  color:var(--brand-text);
}
.sm-badge__dot{
  flex:0 0 auto;
  width:var(--space-15);
  height:var(--space-15);
  border-radius:var(--radius-circle);
  background-color:currentColor;
}

/* A wordless marker pinned to a control's corner, for a state the page already puts in words
   nearby (Tenant/ViewUnit's Payments button while rent is unpaid). Decorative: aria-hidden="true",
   and never the only carrier of the state. Solid --danger with a --surface-canvas ring, where a
   .sm-badge--danger is a 14% tint meant to hold text and read as a smudge at this size. The host
   must be positioned (.sm-btn is). font-size:0 swallows a stray &nbsp;. The translate is
   physical: this shell is LTR. No `display`: an absolute box needs none, so a script may show and
   hide it freely. */
.sm-dot{
  position:absolute;
  top:0;
  inset-inline-end:0;
  width:var(--space-3);
  height:var(--space-3);
  font-size:0;
  background-color:var(--danger);
  border-radius:var(--radius-circle);
  box-shadow:0 0 0 var(--space-05) var(--surface-canvas);
  transform:translate(40%, -40%);
  pointer-events:none;
}
/* Forced colours drop the ring and repaint the fill as Canvas, which left a Canvas disc cutting a
   notch out of the host button's border (Tenant/ViewUnit's Payments button). A CanvasText dot is
   visible, and it reads as one mark with the border it overlaps. No `display`, as above. */
@media (forced-colors: active){
  .sm-dot{ background-color:CanvasText; }
}

/* --- Avatar --------------------------------------------------------------
   For later adoption: the rail's user row and the contact lists draw
   `fa-circle-user`, and the profile headers `user-icon.png`. A circle holding
   initials or a photo:
     <span class="sm-avatar" aria-hidden="true">HD</span>       beside the visible name
     <span class="sm-avatar" role="img" aria-label="…">HD</span> standing alone
     <span class="sm-avatar"><img src="…" alt=""></span>          a photo
   role="img" is what lets a <span> take a name (the settled convention on
   accessible names: aria-label on a bare span is dropped). Two letters at most,
   supplied by the markup; this uppercases them. 40px, --sm 32px for dense rows;
   the size lives in --sm-avatar-size so a context can restate it. Brand wash
   and brand text, 7.4:1 over --surface, like .sm-badge--brand and the
   .sm-empty__medallion. */
.sm-avatar{
  --sm-avatar-size:var(--space-10);
  flex:0 0 auto;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:var(--sm-avatar-size);
  height:var(--sm-avatar-size);
  margin:0;
  overflow:hidden;
  font-family:var(--font-sans);
  font-size:var(--text-md);
  font-weight:var(--weight-semibold);
  line-height:1;
  letter-spacing:var(--tracking-normal);
  text-transform:uppercase;
  white-space:nowrap;
  vertical-align:middle;
  color:var(--brand-text);
  background-color:var(--brand-subtle);
  border-radius:var(--radius-circle);
  user-select:none;
}
.sm-avatar--sm{
  --sm-avatar-size:var(--space-8);
  font-size:var(--text-xs);
}
.sm-avatar > img{
  display:block;
  width:100%;
  height:100%;
  object-fit:cover;
}

/* ============================================================================
   6. Table
   Safe to ADD alongside `display responsive table table-dark`: nothing here
   removes or depends on those classes, and no selector reaches into structure
   DataTables rewrites (wrapper divs, .child rows, .dtr-* controls, sort glyphs).

   Specificity note — the class is doubled throughout. Bootstrap's `.table-dark`
   is (0,1,0) and its cell rule `.table > :not(caption) > * > *` is (0,1,3);
   doubling wins both outright, so the skin does not depend on link order.

   !important note — the four `background-color` declarations below are the only
   !important in this file. DataTables' stylesheets are <link>ed inside the view
   body (e.g. Views/Tenant/ViewUnits.cshtml:275-278), so they cascade AFTER any
   head-linked stylesheet, and their `.display`-scoped row/cell rules reach
   (0,4,3)-(0,5,3) — higher than anything reachable here without absurd
   class repetition. Backgrounds are the only property they contest.
   ========================================================================= */
.sm-table-wrap{
  background-color:var(--surface);
  border:var(--border-hairline) solid var(--border-subtle);
  border-radius:var(--radius-lg);
  box-shadow:var(--elev-dark-2);
  overflow:hidden;
  color:var(--text);
}
/* Horizontal overflow belongs to this inner element, never to .sm-table-wrap —
   the wrap clips to its radius and must not become the scroll container. */
.sm-table-scroll{
  overflow-x:auto;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:thin;
  scrollbar-color:var(--border-strong) transparent;
}
/* The platform scrollbar defaults to a light track, which reads as a white bar across a
   dark card. Only WebKit needs the explicit treatment; Firefox takes the two properties above. */
.sm-table-scroll::-webkit-scrollbar{height:var(--space-2)}
.sm-table-scroll::-webkit-scrollbar-track{background:transparent}
.sm-table-scroll::-webkit-scrollbar-thumb{
  background:var(--border-strong);
  border-radius:var(--radius-pill);
}
.sm-table-scroll::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}

/* DataTables emits its own Bootstrap `.row` control rows (length/search, table, info/pagination)
   inside this box. Bootstrap gives `.row` -12px side margins expecting a padded `.container` or
   `.col` to absorb them; .sm-table-scroll has no padding, so they overhang by 12px and raise a
   horizontal scrollbar on a table that already fits.
   The columns' 12px gutters go too, through --bs-gutter-x, the variable `.row > *` reads (the
   switch Bootstrap's own .gx-0 flips). This is the fix for the "4–7px Responsive overflow". The
   negative margins used to cancel the gutters exactly, so zeroing the margins alone left the
   table's column 24px narrower than the .dataTables_wrapper that Responsive budgets its columns
   against (dt.table().container()). It kept one column too many and the table overran its
   scroller: Tenant/ViewPayRent's deposits by 4px and ViewMakeOneTimePayment's transactions by 7px
   at 375, Tenant/ViewUnits 997 against 990 at 1280, User/ViewCustomerSupport 490 against 478 at
   768. With no gutters the budget is true and the table band runs edge to edge; the length,
   search, info and pager controls keep their own 16px padding (below), which now lines up with the
   cells and with .sm-table-toolbar. A table outside .sm-table-scroll keeps Bootstrap's gutters,
   where the negative margins still cancel them. */
.sm-table-scroll > .dataTables_wrapper > .row{
  --bs-gutter-x:0;
  margin-inline:0;
}
/* A control a table switches off (info:false, paging:false) leaves its bs5 column behind, EMPTY
   but still holding its share of the row: with info:false the pager was squeezed into 7/12 of it,
   and a three-page pager wrapped "Next" onto a second line at 768 (Tenant/ViewPayRent). An empty
   column goes, and when it led the row its neighbour takes the whole width. DataTables creates
   these columns and never shows or hides them, and one filled later (a Buttons container appended
   after init) stops matching :empty and comes back. */
.sm-table-scroll > .dataTables_wrapper > .row > [class*="col-"]:empty{
  display:none;
}
.sm-table-scroll > .dataTables_wrapper > .row > [class*="col-"]:empty + [class*="col-"]{
  flex:1 0 0%;
  width:100%;
  max-width:100%;
}
.sm-table-toolbar{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--space-3);
  flex-wrap:wrap;
  padding:calc(var(--space-3) + var(--space-05)) var(--space-4);
  border-bottom:var(--border-hairline) solid var(--border-subtle);
}
.sm-table-toolbar__title{
  margin:0;
  font-size:var(--text-md);
  font-weight:var(--weight-semibold);
  letter-spacing:var(--tracking-tight);
  color:var(--text-strong);
}
.sm-table-toolbar__actions{
  display:flex;
  align-items:center;
  gap:var(--gap-control);
  flex-wrap:wrap;
  margin-inline-start:auto;
}
.sm-table-footer{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--space-3);
  flex-wrap:wrap;
  padding:calc(var(--space-3) + var(--space-05)) var(--space-4);
  border-top:var(--border-hairline) solid var(--border-subtle);
  font-size:var(--text-sm);
  color:var(--text-muted);
}

.sm-table.sm-table{
  width:100%;
  margin:0;
  border-collapse:separate;
  border-spacing:0;
  font-family:var(--font-sans);
  font-size:var(--text-base);
  line-height:var(--leading-snug);
  color:var(--text);
  vertical-align:middle;
  border-color:var(--border-subtle);
  /* Bootstrap paints cells from these variables; redefining them is how the skin
     reaches every cell without restating .table-dark rule by rule. */
  --bs-table-color:var(--text);
  --bs-table-bg:var(--surface);
  --bs-table-border-color:var(--border-subtle);
  --bs-table-accent-bg:transparent;
  --bs-table-striped-color:var(--text);
  --bs-table-striped-bg:var(--surface-hover);
  --bs-table-active-color:var(--text-strong);
  --bs-table-active-bg:var(--surface-selected);
  --bs-table-hover-color:var(--text);
  --bs-table-hover-bg:var(--surface-hover);
}

.sm-table.sm-table > * > tr > th,
.sm-table.sm-table > * > tr > td{
  height:calc(var(--space-12) + var(--space-1));
  padding:0 var(--space-4);
  background-color:var(--surface) !important;
  border:0;
  border-bottom:var(--border-hairline) solid var(--border-subtle);
  box-shadow:none;
  vertical-align:middle;
  text-align:start;
  color:var(--text);
}

.sm-table.sm-table > thead > tr > th,
.sm-table.sm-table > thead > tr > td,
.sm-table.sm-table > tfoot > tr > th,
.sm-table.sm-table > tfoot > tr > td{
  height:calc(var(--control-height) - var(--space-05));
  font-size:var(--text-xs);
  font-weight:var(--weight-semibold);
  letter-spacing:var(--tracking-caps);
  text-transform:uppercase;
  white-space:nowrap;
  /* --text-secondary, not --text-muted: these cells are 12px/600, which is not WCAG
     "large text", so they need 4.5:1. --text-muted (#7C7C93) on --surface-raised
     (#17171E) is 4.38:1 and fails; --text-secondary (#A8A8BC) is 7.64:1 and clears AAA. */
  color:var(--text-secondary);
  background-color:var(--surface-raised) !important;
  border-bottom:var(--border-hairline) solid var(--border);
}
.sm-table.sm-table > tfoot > tr > th,
.sm-table.sm-table > tfoot > tr > td{
  border-top:var(--border-hairline) solid var(--border);
  border-bottom:0;
}

.sm-table.sm-table > tbody > tr:last-child > th,
.sm-table.sm-table > tbody > tr:last-child > td{ border-bottom:0; }

.sm-table.sm-table > tbody > tr:hover > th,
.sm-table.sm-table > tbody > tr:hover > td{
  background-color:var(--surface-raised) !important;
}

/* Header, footer and hovered rows all paint --surface-raised, where --text-muted is 4.38:1. The
   header cells already use --text-secondary; this covers any .sm-muted (or other muted) text
   INSIDE a cell, by rebinding the token on the row group — the 12.9 technique. */
.sm-table.sm-table > thead,
.sm-table.sm-table > tfoot,
.sm-table.sm-table > tbody > tr:hover{ --text-muted:var(--text-secondary); }

/* Keyboard focus on the cells DataTables itself makes focusable: sortable headers (tabindex="0")
   and Responsive's row-expand control, which lands on a row header (<th scope="row">) when the
   first column is one, as on Building/ViewBuildingDetails' NOI and Cash Flow rows, where it fell
   back to the UA's 1px outline. The ring is inset: an outset one is clipped by
   .sm-table-scroll's overflow and painted over by the neighbouring cell. (0,5,3) clears the
   (0,2,2) cell reset above and DataTables' own (0,3,3)-(0,4,3) striping shadows, which load later
   from the view body. */
.sm-app .sm-table.sm-table > thead > tr > th[tabindex]:focus-visible,
.sm-app .sm-table.sm-table > tbody > tr > th[tabindex]:focus-visible,
.sm-app .sm-table.sm-table > tbody > tr > td[tabindex]:focus-visible{
  outline:var(--sm-focus-outline);
  outline-offset:var(--sm-focus-offset-inset);
  box-shadow:var(--sm-focus-ring-inset);
}

.sm-table.sm-table > tbody > tr.sm-is-selected > th,
.sm-table.sm-table > tbody > tr.sm-is-selected > td{
  background-color:var(--surface-selected) !important;
  color:var(--text-strong);
}

/* Numeric columns. Scoped so the .sm-num utility stays alignment-neutral
   outside tables. */
.sm-table.sm-table > * > tr > .sm-num{
  text-align:right;
  font-variant-numeric:var(--numeric-tabular);
  font-feature-settings:"tnum" 1;
}

/* A photo in a table row: the building thumbnails on Owner/Overview, Manager/ManagerOverview,
   Manager/ViewManagerRelationshipPanel, Tenant/ViewUnits, User/ViewUser, User/ViewCompany and
   User/ViewCompanyRenting, which each sized theirs with an inline 50px pair. 48px on the space
   scale, the chip radius, cropped to fill. max-width:none so Bootstrap's percentage cap on
   .img-fluid / .img-thumbnail cannot let a squeezed Responsive column shrink it; flex:0 0 auto
   keeps it square beside a name in a flex row. The .sm-app scope clears both of those classes. */
.sm-app .sm-table-thumb{
  display:block;
  flex:0 0 auto;
  width:var(--space-12);
  height:var(--space-12);
  max-width:none;
  object-fit:cover;
  border-radius:var(--radius-sm);
}

/* Compact density */
.sm-table--compact.sm-table > tbody > tr > th,
.sm-table--compact.sm-table > tbody > tr > td{ height:var(--control-height); }

/* Opt-in sticky header. Not a default: .sm-table-wrap clips overflow, so the
   header only sticks when an ancestor actually scrolls vertically. */
.sm-table--sticky.sm-table > thead > tr > th,
.sm-table--sticky.sm-table > thead > tr > td{
  position:sticky;
  top:0;
  z-index:2;
}

/* Opt-in sticky first column, for a table that scrolls sideways (DataTables scrollX) instead of
   collapsing: Building/ViewBuildingDetails' statement (decision D7, option B), where every period
   and the Total stay on the row and the line-item labels stay in view.
     <table class="sm-table sm-table--sticky-first display table …" style="width:100%">
   with `scrollX:true, responsive:false` in its init. scrollX splits the table into a header copy
   in .dataTables_scrollHead (overflow:hidden, its scrollLeft kept in step with the body) and the
   body table in .dataTables_scrollBody (overflow:auto). The copy is cloneNode(false), so it keeps
   every class, and this one rule pins the column in both halves, each against its own scroller;
   the body's zero-height sizing row pins too, invisibly.
   BACKGROUND: nothing new. The cells above already paint an opaque fill in every state (--surface,
   and --surface-raised on the header and on a hovered row, all !important), and DataTables'
   stripe, hover and sort tints are inset box-shadows on every cell, the pinned one included, so
   the pinned cell matches its row with no rule here. That is also why the edge is a border and
   not a box-shadow, which would erase those tints on this one cell. (.sm-is-selected, whose fill
   is translucent, has no user and is not supported by this modifier.)
   STACKING: z-index 1 lifts the pinned cells over the cells that slide under them (sortable
   headers are position:relative); the header's first cell takes 3, so it also clears a
   .sm-table--sticky header row (2). All of it stays far below the sticky topbar (20, 8.4).
   WIDTH: labels keep one line from 992px up; below that they wrap between words. The breakpoint
   was 576px, but from 576px to 991px the open rail leaves a phone-width scroller (478px at 768),
   and one-line labels took 225-279px of it (47-58%), so only two periods showed (W4-details-7).
   The column is also held between a floor and a cap measured against the SCROLLER, which is why
   .dataTables_scroll (the box round the head and body scrollers; DataTables sizes it from its
   column, never from its content) becomes an inline-size container:
     cap    40% of the scroller. A cell's max-width limits only its max-content, never its
            min-content, so it bites where labels may wrap and the table has room to spare; a
            one-line label from 992px keeps its width (b65's longest is 279px, 40% of 697px).
     floor  140px or 40% of the scroller, the smaller. The label cells start with "&emsp; ", and a
            column squeezed to its longest word broke straight after that em space, leaving an
            indent alone on the first line (W4-details-15, "Application Fee" at 375). The floor
            gives the indent and a typical word one line: 768 gets 140px (29%: the periods get 338px
            instead of 199-253px), 375 gets 130px, and 320 keeps the longest word's 121px.
   DataTables' cells are content-box (section 6's BOX SIZING note), so both lengths subtract the
   cell's 32px of inline padding and 1px edge; the widths above are border-box. The empty-table
   cell is left out: it spans every column. No `display`. */
.sm-app .sm-table--sticky-first.sm-table > * > tr > :first-child{
  position:sticky;
  inset-inline-start:0;
  z-index:1;
  border-inline-end:var(--border-hairline) solid var(--border);
}
.sm-app .sm-table--sticky-first.sm-table > thead > tr > :first-child{ z-index:3; }
.sm-app .dataTables_scroll:has(.sm-table--sticky-first){ container-type:inline-size; }
.sm-app .sm-table--sticky-first.sm-table > * > tr > :first-child:not(.dataTables_empty){
  min-width:calc(min(calc(var(--space-32) + var(--space-3)), 40cqi) - var(--space-8) - var(--border-hairline));
  max-width:calc(40cqi - var(--space-8) - var(--border-hairline));
}
@media (min-width:992px){
  .sm-app .sm-table--sticky-first.sm-table > * > tr > :first-child{ white-space:nowrap; }
}
/* "No matching records found" (a search with no hits) is the pinned first cell too, but it spans
   every column, 1,082px of a 325px scroller at 375, so sticky cannot move it and the centred message
   sat outside the visible window (W4-details-5). In this table it starts at the pinned edge instead,
   icon included, where the scroller shows it: the icon (the cell's ::after, placed absolutely by
   the empty rule further down) moves from the centre to the cell's 16px start padding. (0,5,4) and
   (0,5,5) against the empty rule's (0,4,4) and (0,4,5). */
.sm-app table.dataTable.sm-table--sticky-first.sm-table > tbody > tr > td.dataTables_empty{ text-align:start; }
.sm-app table.dataTable.sm-table--sticky-first.sm-table > tbody > tr > td.dataTables_empty::after{
  inset-inline-start:var(--space-4);
  margin-inline-start:0;
}

/* Scroll frame. A scrollX/scrollY table is split into .dataTables_scroll > .dataTables_scrollHead +
   .dataTables_scrollBody (+ .dataTables_scrollFoot when the table has a <tfoot>); today that is two
   statements, both scrollX with .sm-table--sticky-first: Building/ViewBuildingDetails' (#example)
   and Building/ViewBuildingAssumptions' DCF statement (#DCFStatement, W4-fin-b-14), so the frame
   skin here and the pinned-column width, container and nowrap rules above serve both
   (F-contract-5). DataTables writes an inline `border:0` on the
   head and the foot, and jquery.dataTables.css, <link>ed mid-body after this file, closes the body
   with `.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid
   rgba(0,0,0,.3)}` (0,3,0): black at 30% is a 1.04:1 groove on --surface, not the hairline the rest
   of the table uses. The view-local block that carried a recolour for these containers went in wave
   2, so the recolour lives here, for every .sm-app table: --border-subtle, the table's own row
   divider (1.15:1, decorative), at (0,4,0) and colour only. A wrapper with a real footer draws no
   line, and a colour alone leaves it lineless.
   One more line is a vendor artefact. DataTables 1.12.1 appends an empty <tfoot> to a scrolling table
   that has a <caption> and no footer (both statements have one), builds a scroll foot for it from a clone of
   the table, `no-footer` class included, and `table.dataTable.no-footer{border-bottom:…}` (0,2,1)
   then drew a second groove directly under the first. The clone's line goes, at (0,5,1); the clone
   of a real footer never carries no-footer. No `display`. */
.sm-app .dataTables_wrapper .dataTables_scroll > .dataTables_scrollBody{
  border-bottom-color:var(--border-subtle);
}
/* site.js makes an overflowing scroll body a named, focusable region (keyboard users scroll the
   statement's hidden months with the arrow keys). Its ring is an INSET outline: the cells paint
   opaque fills above any inset box-shadow, and an outset ring would be clipped by the scroller's
   own overflow. Outlines survive forced colours. */
.sm-app .dataTables_wrapper .dataTables_scroll > .dataTables_scrollBody:focus-visible{
  outline:var(--sm-focus-width) solid var(--purple-300);
  outline-offset:calc(var(--sm-focus-width) * -1);
}
/* In a sticky-first frame that outline lost its left edge and the stretch of its top edge over the
   pinned column: the pinned cells are positioned at z-index 1, and Chrome paints a box's own
   outline before its positioned descendants, so the cells covered it (F-bldg-b-2 on
   ViewBuildingAssumptions' DCF statement, measured; F-contract-1 predicted the same for
   ViewBuildingDetails). An outset ring has no room (.sm-table-wrap and .sm-table-scroll clip the
   band edge to edge), so in these frames the ring moves to an ::after of .dataTables_scroll at
   z-index 4, above the pinned body cells (1) and the pinned header cell (3), and the body's own
   outline goes so the two do not double up under the header: one of the exceptions section 0.2
   lists to its "no outline:none", since a border on the overlay replaces it and borders, like
   outlines, survive forced colours (as CanvasText there). The overlay frames head and body
   together, the statement as the reader sees it, with all four edges drawn: 2px of --purple-300,
   7.88:1 on --surface and 7.53:1 on the --surface-raised header. pointer-events:none, so it never
   takes a click or the wheel. The frame becomes the overlay's containing block with
   position:relative and no z-index, which changes no layout and makes no stacking context, so the
   overlay and the pinned cells share one and z-index decides. Nothing here shows or hides an
   element, and no display is declared. */
.sm-app .dataTables_scroll:has(.sm-table--sticky-first){ position:relative; }
.sm-app .dataTables_wrapper .dataTables_scroll > .dataTables_scrollBody:has(> .sm-table--sticky-first):focus-visible{
  outline:none;
}
.sm-app .dataTables_scroll:has(> .dataTables_scrollBody:focus-visible > .sm-table--sticky-first)::after{
  content:"";
  position:absolute;
  inset:0;
  z-index:4;
  border:var(--sm-focus-width) solid var(--purple-300);
  pointer-events:none;
}
.sm-app .dataTables_scroll > .dataTables_scrollFoot table.dataTable.no-footer{
  border-bottom:0;
}

/* Opt-in wrapping headers. Header labels are nowrap caps, so a long one sets its column's minimum
   width and Responsive hides a column to make room, often the one holding the row action. Letting
   the label break between words gives that width back (User/ViewCompany keeps its row actions on
   screen with this and a class="all" priority on the action column). (0,3,3) against the nowrap's
   (0,2,3). */
.sm-app .sm-table--wrap-head.sm-table > thead > tr > :is(th, td){
  white-space:normal;
}

/* Readable floors for the cell that names a row in a Responsive table. Responsive 2.3.0 keeps every
   further column that still fits beside the others at their min-content widths, and auto table
   layout then hands the spare width to the columns with the most max-content, so a long name or
   title was left a word or two per line (Admin/ViewAdminDashboard's Support Tickets at 375, 768 and
   1440; ViewAdminCustomer's Buildings and Open Support Tickets at 375; Owner/Overview's Name at 768).
   The floor sits on BODY cells on purpose. Responsive's fit test (_resizeAuto) measures a clone of
   the table in which every HEADER cell is forced to an inline `width:auto; min-width:0` but the body
   rows are copied as they are, so only a <td> minimum is seen both by the fit test and by the real
   layout; Responsive never keeps a column that does not fit beside the floor, so nothing overflows.
   BOX SIZING: Bootstrap's reboot makes everything border-box, but jquery.dataTables.css puts every
   th and td of a table.dataTable back to content-box ((0,1,2)), so a plain min-width there is the
   CONTENT width and the cell's padding (and, on the first visible column, the expand control's
   30px inset) comes on top of it. Nothing in this file, site.css, Bootstrap 5.2.2 or the DataTables
   CSS gave a table cell a minimum before these two. No `display`.
     --lead          the table's first body cell. `min(12rem, 40vw)` keeps it phone-safe: 128px at
                     320, 150px at 375, 192px from 480 up, of CONTENT (the admin tables were
                     verified at those sizes: 183 + 87 = 270px at 320, no overflow). Child rows and
                     the empty-table cell are left out. (0,6,2).
                       <table class="sm-table sm-table--lead display responsive table table-dark" …>
     __cell--name    one named cell, for a name that is not the table's first column
                     (Owner/Overview's Name follows an Image column Responsive always hides). The
                     cell is made border-box, so the floor INCLUDES its padding and the 30px
                     control inset: as content-box it was 160 + 30 + 16 = 206px, and at 320
                     Overview's Name + Info came to 293px in a 270px scroller (W4-app-fixes-2,
                     W4-shell-perf-4). It is capped at 40vw on phones: 128px at 320, 144px at 360,
                     150px at 375, 160px from 400 up. (0,4,3) against DataTables' (0,1,2).
                       <td class="sm-table__cell--name">…</td> */
.sm-app .sm-table--lead.sm-table > tbody > tr:not(.child) > :first-child:not(.dataTables_empty){
  min-width:min(12rem, 40vw);
}
.sm-app .sm-table.sm-table > tbody > tr > td.sm-table__cell--name{
  box-sizing:border-box;
  min-width:min(calc(var(--space-32) + var(--space-8)), 40vw);
}
/* Opt-in floor for an input grid that scrolls sideways (responsive:false): a width:100% .sm-input
   gives an auto-layout table no min-content width, so thirteen year columns squeezed each input to
   30-56px ("$96," for $96,000.00). 15ch leaves ~12ch of text inside the border-box padding.
   ViewBuildingAssumptions' six JS-built grids carry the class. No display. */
.sm-app .sm-table--inputs.sm-table > tbody > tr > td > .sm-input{
  min-width:15ch;
}

/* Opt-in denser cells on phones, for a Responsive table that needs one more priority column below
   576px than 16px of inline cell padding leaves room for: Admin/ViewInvoice's transactions at 375,
   where Date, Status and Amount Received need 331.45px of column minimums with 16px and 317.11px
   with 12px, in a 325px band (modelled from Archivo metrics). Inline padding drops from 16px to 12px.
   The expand control's 30px inline-start (Responsive, (0,4,4)) and a sortable header's 26px
   inline-end (DataTables' glyph room, (0,2,4), which the :not() leaves alone) stay the vendor's.
   (0,3,2) and (0,4,2) against .sm-table's (0,2,2). A modifier of its own rather than a change to
   --wrap-head, which twelve tables in eight views carry, so nothing changes until a view opts in.
     <table class="sm-table sm-table--wrap-head sm-table--dense-sm display responsive table table-dark" …> */
@media (max-width:575.98px){
  .sm-app .sm-table--dense-sm.sm-table > * > tr > :is(th, td){
    padding-inline-start:var(--space-3);
  }
  .sm-app .sm-table--dense-sm.sm-table > * > tr > :is(th, td):not(.sorting, .sorting_asc, .sorting_desc, .sorting_asc_disabled, .sorting_desc_disabled){
    padding-inline-end:var(--space-3);
  }
}

/* DataTables Responsive injects `tr.child` detail rows and a `.dtr-control`
   cell. Neither should inherit the fixed row height. */
.sm-table.sm-table > tbody > tr.child > td,
.sm-table.sm-table > tbody > tr.child > th{
  height:auto;
  padding-top:var(--space-3);
  padding-bottom:var(--space-3);
  white-space:normal;
}

/* The Responsive row expander is a filled purple control like the primary (1.1): the --brand
   disc, now inside 0.5's --sm-btn-rim-brand ring in place of the white-10% one, 5.16:1 on the
   card and 4.93:1 on a hovered row (the disc alone is 2.34:1 and 2.23:1, the old ring
   2.83-2.96:1). The white +/- glyph stays 7.98:1 on the disc. */
.sm-app table.sm-table.dataTable.dtr-inline.collapsed > tbody > tr > .dtr-control::before{
  background:var(--brand);
  color:var(--text-on-brand);
  border-color:var(--sm-btn-rim-brand);
  box-shadow:none;
  font-family:var(--font-sans);
}

/* DataTables owns these controls, so skin its generated classes instead of
   renaming them. Both the plain and Bootstrap renderers are used by app views.
   The .sm-app scope clears site.css and the later view-level vendor styles. */
.sm-app .dataTables_wrapper{
  font-family:var(--font-sans);
  font-size:var(--text-sm);
  color:var(--text-secondary);
}
.sm-app .dataTables_wrapper .dataTables_length,
.sm-app .dataTables_wrapper .dataTables_filter,
.sm-app .dataTables_wrapper .dataTables_info,
.sm-app .dataTables_wrapper .dataTables_paginate{
  padding:var(--space-3) var(--space-4);
  color:var(--text-secondary);
}
.sm-app div.dataTables_wrapper div.dataTables_filter label,
.sm-app div.dataTables_wrapper div.dataTables_length label{
  display:inline-flex;
  align-items:center;
  flex-wrap:wrap;
  gap:var(--space-2);
  margin:0;
  font-size:var(--text-sm);
  font-weight:var(--weight-medium);
  line-height:var(--leading-snug);
  white-space:normal;
  text-align:start;
  color:var(--text-secondary);
}
.sm-app .dataTables_wrapper .dataTables_filter input,
.sm-app .dataTables_wrapper .dataTables_length select{
  min-height:var(--control-height);
  max-width:100%;
  margin:0;
  padding:var(--space-2) var(--space-3);
  font:inherit;
  line-height:var(--leading-tight);
  color:var(--text);
  background-color:var(--surface-field);
  border:var(--border-hairline) solid var(--sm-field-border);
  border-radius:var(--radius-md);
  box-shadow:none;
  color-scheme:dark;
  transition:border-color var(--sm-duration-fast) var(--ease-standard),
    box-shadow var(--sm-duration-fast) var(--ease-standard);
}
.sm-app .dataTables_wrapper .dataTables_filter input{
  width:calc(var(--space-32) + var(--space-24));
}
.sm-app .dataTables_wrapper .dataTables_length select{
  width:auto;
  min-width:var(--space-16);
  appearance:auto;
  background-image:none;
}
.sm-app .dataTables_wrapper .dataTables_filter input:hover,
.sm-app .dataTables_wrapper .dataTables_length select:hover{
  border-color:var(--sm-field-border-hover);
}
/* :focus-visible, like every other control in this file. A mouse focus no longer draws the ring,
   and it cannot pick up Bootstrap's white `.form-control:focus` / `.form-select:focus` fill or blue
   ring either: the base rule above is (0,3,1) against their (0,2,0), and it restates both. */
.sm-app .dataTables_wrapper .dataTables_filter input:focus-visible,
.sm-app .dataTables_wrapper .dataTables_length select:focus-visible{
  outline:var(--sm-focus-outline);
  color:var(--text);
  background-color:var(--surface-field);
  border-color:var(--brand-text);
  box-shadow:var(--focus-ring);
}
.sm-app .dataTables_wrapper .dataTables_paginate .pagination{
  flex-wrap:wrap;
  gap:var(--space-1);
  margin:0;
}
/* Bootstrap pagination adds a link inside .paginate_button. Its outer list
   item must not acquire the legacy hover gradient or a second border. */
.sm-app .dataTables_wrapper .dataTables_paginate li.paginate_button,
.sm-app .dataTables_wrapper .dataTables_paginate li.paginate_button:hover{
  margin:0;
  padding:0;
  border:0;
  background:none;
  box-shadow:none;
}
/* Page links are neutral tools (1.1): 0.5's white-6% lift inside an --sm-field-border outline,
   4.59:1 on the card the pager sits on (the --surface-field square and white-10% hairline it
   replaces was 1.26:1 and read as a hole), under a --text label (14.74:1) like every other tool.
   Hover moves the outline to --sm-field-border-hover (7.99:1) and the label to --text-strong.
   The current page is the SELECTED chip, the selected pill's paint (section 1, tokens in 0.5):
   --sm-btn-selected-fill, 7.88:1 on the card and 6.83:1 from the lifted links beside it, with an
   --ink-950 label (8.28:1); it was --brand at 2.34:1. Its label still reads --brand-on, rebound
   on the element itself, because the forced-colours rule just below rebinds that same token to
   HighlightText and has to win there (same weight, later). The current page is (0,6,0) and the
   hover (0,5,1), so it stays marked under the pointer. The labels stay !important to beat
   jquery.dataTables.css's own !important colours. Disabled Previous/Next/… keep their quiet
   --surface square and --border-subtle hairline below (exempt as inactive), so an unavailable
   link looks unlike every live one. */
.sm-app .dataTables_wrapper .dataTables_paginate :is(.page-link, a.paginate_button, button.paginate_button){
  display:inline-flex;
  align-items:center;
  justify-content:center;
  min-width:var(--control-height);
  min-height:var(--control-height);
  margin:0;
  padding:var(--space-2) var(--space-3);
  font:inherit;
  font-weight:var(--weight-medium);
  line-height:var(--leading-tight);
  text-decoration:none;
  color:var(--text) !important;
  background:var(--sm-btn-neutral-fill);
  border:var(--border-hairline) solid var(--sm-field-border);
  border-radius:var(--radius-md);
  box-shadow:none;
  transition:background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard),
    color var(--sm-duration-fast) var(--ease-standard);
}
.sm-app .dataTables_wrapper .dataTables_paginate :is(.page-link, a.paginate_button, button.paginate_button):hover{
  color:var(--text-strong) !important;
  background:var(--sm-btn-neutral-fill-hover);
  border-color:var(--sm-field-border-hover);
}
.sm-app .dataTables_wrapper .dataTables_paginate :is(.page-item.active > .page-link, a.paginate_button.current, button.paginate_button.current){
  --brand-on:var(--sm-btn-selected-on);
  color:var(--brand-on) !important;
  background:var(--sm-btn-selected-fill);
  border-color:var(--sm-btn-selected-fill);
}
/* Forced colours repaint every pager button's fill as Canvas and its border and label in one system
   colour, so the current page looked like the rest. It takes the system selection pair, as the
   selected pill does (section 1). Its label is the one declaration that cannot simply be restated:
   the rule above has to be !important to beat jquery.dataTables.css's own
   `.paginate_button.current{color:#333 !important}`, so the token that rule reads is rebound
   instead, and --brand-on resolves to HighlightText with no new !important. Left alone, the forced
   label would take the link colour, which has no contrast guarantee against Highlight. It opts out
   with forced-color-adjust:none, because Chromium's Canvas backplate behind the label turned "1"
   into a solid block (section 1's note on the selected pill); every paint it keeps is a system
   colour, and box-shadow:none keeps the dark/purple focus ring off it (the CanvasText outline of
   0.2, 2px outside the button, marks focus). Same weight as the rule above, after it. */
@media (forced-colors: active){
  .sm-app .dataTables_wrapper .dataTables_paginate :is(.page-item.active > .page-link, a.paginate_button.current, button.paginate_button.current){
    forced-color-adjust:none;
    --brand-on:HighlightText;
    background-color:Highlight;
    border-color:Highlight;
    box-shadow:none;
  }
}
.sm-app .dataTables_wrapper .dataTables_paginate :is(.page-item.disabled > .page-link, a.paginate_button.disabled, button.paginate_button.disabled){
  color:var(--text-muted) !important;
  background:var(--surface);
  border-color:var(--border-subtle);
  cursor:default;
}
/* Forced colours paint every page link LinkText, so a disabled Previous/Next looked live (F-bldg-c-7).
   GrayText is the system colour for an unavailable control. The !important matches the rule above,
   which has to beat jquery.dataTables.css's own !important colour. */
@media (forced-colors: active){
  .sm-app .dataTables_wrapper .dataTables_paginate :is(.page-item.disabled > .page-link, a.paginate_button.disabled, button.paginate_button.disabled){
    color:GrayText !important;
    border-color:GrayText;
  }
}
/* The bs5 renderer gives every pager link tabindex="0", disabled Previous/Next included, so they stay
   keyboard stops. The disabled rule above is (0,6,0) against the focus rule's (0,5,1): with its
   shadow reset unscoped, the ring vanished on exactly those two stops. */
.sm-app .dataTables_wrapper .dataTables_paginate :is(.page-item.disabled > .page-link, a.paginate_button.disabled, button.paginate_button.disabled):not(:focus-visible){
  box-shadow:none;
}
.sm-app .dataTables_wrapper .dataTables_paginate :is(.page-link, a.paginate_button, button.paginate_button):focus-visible{
  outline:var(--sm-focus-outline);
  border-color:var(--brand-text);
  box-shadow:var(--focus-ring);
}
.sm-app .dataTables_wrapper .dataTables_paginate .ellipsis{
  padding:0 var(--space-2);
  color:var(--text-muted);
}
.sm-app .dataTables_wrapper .dataTables_paginate :is(a.paginate_button, button.paginate_button){
  margin-inline-start:var(--space-1);
}
.sm-app .dataTables_wrapper .dataTables_processing{
  color:var(--text);
  background:var(--surface-overlay);
  border:var(--border-hairline) solid var(--border);
  border-radius:var(--radius-md);
  box-shadow:var(--elev-dark-2);
}
.sm-app table.dataTable > tbody > tr.child ul.dtr-details > li{
  border-bottom-color:var(--border-subtle);
}
@media (max-width:767.98px){
  .sm-app div.dataTables_wrapper div.dataTables_filter label{
    width:100%;
  }
  .sm-app .dataTables_wrapper .dataTables_filter input{
    flex:1 1 var(--space-32);
    min-width:0;
  }
}
/* A .sm-page--narrow column (68ch) splits the control row into two halves of roughly 235-260px
   from 768px, too narrow for "Search:" beside the 224px box, so the label stacked above it at
   1280 and 1440 (Tenant/ViewMakeOneTimePayment's transactions) where a full-width page keeps it
   inline. A 160px box fits beside the label there; below 768px the rule above already gives the
   box the whole row. Width only, no flex change. (0,4,1) against (0,3,1). */
@media (min-width:768px){
  .sm-app .sm-page--narrow .dataTables_wrapper .dataTables_filter input{
    width:calc(var(--space-32) + var(--space-8));
  }
}
/* The search box and page-length select inherit the wrapper's 13px, so iOS zooms on focus; the
   same 16px floor section 4 gives .sm-input on touch-primary devices. */
@media (pointer: coarse){
  .sm-app .dataTables_wrapper .dataTables_filter input,
  .sm-app .dataTables_wrapper .dataTables_length select{
    font-size:max(var(--root-font-size), var(--text-md));
  }
}

/* --- DataTables finishing states: sort, empty, processing, Responsive, Buttons --
   The states DataTables draws for itself. All five shipped light or all but
   invisible on this skin, and all five are painted by the vendor stylesheets
   the views <link> mid-body, AFTER this file. So each rule below outranks the
   vendor rule it answers (the weights are given beside each) instead of
   reaching for !important; none is added. The sort, empty and Responsive rules
   keep to .sm-table tables, like the rest of this section. The processing box
   and the Buttons toolbar sit in the wrapper, outside the <table>, and are
   scoped like the controls above.
   ------------------------------------------------------------------------ */

/* Sort state. 1.12.1 draws two glyphs on every sortable header, ::before "▴" and ::after "▾", at
   opacity .125 with the active one at .6, all in the header's own colour: a sorted column was
   hard to tell from an unsorted one. Now idle glyphs sit at .3; the glyph that names the current
   order is --brand-text at full opacity (7.53:1 on the header's --surface-raised); and hovering or
   keyboard-focusing a sortable header lifts its label to --text and its idle glyphs to .6
   (6.52:1), so the control announces itself. (0,4,5) and up against the vendor's (0,2,5). The
   keyboard ring is the inset one earlier in this section (DataTables gives every sortable header
   tabindex="0"). Glyph position, size and the 26px right padding stay the vendor's. */
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td):is(.sorting, .sorting_asc, .sorting_desc, .sorting_asc_disabled, .sorting_desc_disabled){
  transition:color var(--sm-duration-fast) var(--ease-standard);
}
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td):is(.sorting, .sorting_asc, .sorting_desc, .sorting_asc_disabled, .sorting_desc_disabled)::before,
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td):is(.sorting, .sorting_asc, .sorting_desc, .sorting_asc_disabled, .sorting_desc_disabled)::after{
  opacity:.3;
  transition:opacity var(--sm-duration-fast) var(--ease-standard);
}
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td):is(.sorting, .sorting_asc, .sorting_desc, .sorting_asc_disabled, .sorting_desc_disabled):is(:hover, :focus-visible){
  color:var(--text);
}
/* Only the idle glyphs: the active one keeps its full brand colour under the pointer. DataTables
   never takes `sorting` off a header it has sorted (1.12.1's header renderer removes only
   sorting_asc/_desc before adding the new one), so a sorted header is `.sorting.sorting_asc` and
   "unsorted" has to be spelled with :not(). A column limited to one direction carries
   sorting_asc_disabled or sorting_desc_disabled in place of `sorting`, and the vendor hides its
   other glyph. */
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td):is(.sorting, .sorting_asc_disabled, .sorting_desc_disabled):not(.sorting_asc, .sorting_desc):is(:hover, :focus-visible)::before,
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td):is(.sorting, .sorting_asc_disabled, .sorting_desc_disabled):not(.sorting_asc, .sorting_desc):is(:hover, :focus-visible)::after,
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td).sorting_asc:is(:hover, :focus-visible)::after,
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td).sorting_desc:is(:hover, :focus-visible)::before{
  opacity:.6;
}
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td).sorting_asc::before,
.sm-app table.dataTable.sm-table > thead > tr > :is(th, td).sorting_desc::after{
  color:var(--brand-text);
  opacity:1;
}

/* Empty. `td.dataTables_empty` spans the row with "No data available in table", or one of the nine
   `emptyTable` strings the views set; DataTables only centres it, so it sat in a 52px row like a
   data cell. Now it has the room and voice of .sm-empty--compact (section 7): 24px above and
   below, 13px muted text (4.59:1 on --surface; a hovered row rebinds the token, above) under a
   32px outline inbox in the same colour. The glyph is a CSS mask: the SVG's black is its alpha,
   not a colour, and the paint is `currentColor`.
   The inbox is the cell's ::after, not its ::before. Responsive 2.3.0's stylesheet hides
   `td.dataTables_empty:before` with `display:none !important` whenever a table is collapsed (it
   keeps ::before on first cells for its expand control), so the same empty table lost its icon
   at 375 and 768 and kept it at 1440 (F-bldg-a-6). Neither Responsive nor DataTables 1.12.1 (core
   and bs5 stylesheets, read from the served copies) has any ::after rule on a body cell. An
   ::after comes after the text, so it is placed rather than flowed: the cell is its containing
   block (position:relative; in a sticky-first table the pinned rule's sticky gives way to it,
   which moves nothing, since a cell as wide as the table cannot stick) and its top padding keeps
   the old band, 24px above the 32px icon and 8px between icon and text (64px in all). Same
   geometry as before at every width, collapsed or not. (0,4,4) and (0,4,5) against DataTables'
   (0,2,2) text-align rule on the cell. No display. */
.sm-app table.dataTable.sm-table > tbody > tr > td.dataTables_empty{
  position:relative;
  height:auto;
  padding:calc(var(--space-6) + var(--space-8) + var(--space-2)) var(--space-4) var(--space-6);
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  text-align:center;
  white-space:normal;
  color:var(--text-muted);
}
.sm-app table.dataTable.sm-table > tbody > tr > td.dataTables_empty::after{
  content:"";
  position:absolute;
  top:var(--space-6);
  inset-inline-start:50%;
  width:var(--space-8);
  height:var(--space-8);
  margin-inline-start:calc(var(--space-8) / -2);
  background-color:currentColor;
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 13h5l1.5 3h5l1.5-3h5'/%3E%3Cpath d='M5.5 5h13L21 13v5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18v-5z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 13h5l1.5 3h5l1.5-3h5'/%3E%3Cpath d='M5.5 5h13L21 13v5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18v-5z'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* Forced colours repaint the masked currentColor fill as Canvas on a Canvas cell, so the inbox
   vanished; it is decorative (the message text stays), but the empty state reads better with it. */
@media (forced-colors: active){
  .sm-app table.dataTable.sm-table > tbody > tr > td.dataTables_empty::after{ forced-color-adjust:none; background-color:CanvasText; }
}

/* The chrome round an empty table. A pager with nothing to page and a "Show 10 entries" menu with
   nothing to show outweighed the empty cell itself (Owner/ViewRentRoll drew the full chrome round 5
   of its 8 building tables). While the body holds the empty cell they go. The search box stays:
   a search that matched nothing is an empty table too, and the box is how the reader clears it.
   The length menu goes only from 768px, where it shares a row with the search box. Below that the
   two stack, and taking the menu out as a search narrowed to nothing would pull the box up under
   the reader's finger mid-word. DataTables never shows or hides these two itself, and no script in
   Views/ or wwwroot/js touches .dataTables_length, .dataTables_paginate or .dataTables_info, so the
   display trap does not apply. :has() is re-matched on every draw, so both return with a row. */
.sm-app .dataTables_wrapper:has(td.dataTables_empty) .dataTables_paginate{
  display:none;
}
@media (min-width:768px){
  .sm-app .dataTables_wrapper:has(td.dataTables_empty) .dataTables_length{
    display:none;
  }
}
/* The export buttons are hidden too (Copy, Excel and PDF on Building/ViewAccount,
   ViewBuildingAssumptions, ViewBuildingDetails and the public valuation tool): an empty table
   exports an empty file. That includes a search with no hits, which draws the same
   td.dataTables_empty ("No matching records found") while the reader is typing in the search box.
   Where the Buttons row stacks above the search box (phones, and 768 beside the rail) `display:none`
   took the row away and the focused box jumped up 44-51px, then back on the next keystroke
   (design-a11y DA4-F3, W4-details-4, W4-fin-b-3). visibility:hidden keeps the row's box, so nothing
   under the caret moves, and hidden buttons are neither focusable nor in the accessibility tree.
   No `display` any more. Re-matched on every draw, so the buttons return with a row. */
.sm-app .dataTables_wrapper:has(td.dataTables_empty) .dt-buttons{
  visibility:hidden;
}

/* Processing. 1.12.1's four animated dots are Bootstrap blue, rgba(13,110,253,.9). Here they are
   the brand's on-dark step, --brand-text: 7.06:1 on the box's --surface-overlay, where --brand
   itself is 2.09:1. The box's "Processing..." text carries the state; the dots are its motion,
   and DataTables stops them for nobody, so under reduced motion they rest as a row of three.
   The box is skinned with the controls above. No app table sets `processing:true` yet. (0,3,3)
   against the vendor's (0,2,3); :nth-child(n) lifts the animation reset to (0,4,3) over its
   per-dot (0,3,3). */
.sm-app div.dataTables_processing > div:last-child > div{
  background:var(--brand-text);
}
@media (prefers-reduced-motion: reduce){
  .sm-app div.dataTables_processing > div:last-child > div:nth-child(n){ animation:none; }
}

/* Responsive child rows. Each detail line's column title was vendor bold in the row's own --text,
   so label and value read at one level. Now it takes the header cells' voice in sentence case:
   semibold --text-secondary (7.99:1 on --surface), with a gap before its value. (0,5,4) against
   the vendor's (0,3,4). */
.sm-app table.dataTable.sm-table > tbody > tr.child span.dtr-title{
  margin-inline-end:var(--space-2);
  font-weight:var(--weight-semibold);
  color:var(--text-secondary);
}
/* A long unbroken value in a child row (an e-mail, a Stripe id, a file name) set the details
   list's minimum width, and the table widened to hold it: User/ViewCustomerSupport's grew to 685px
   inside a 325px scroller at 375 and scrolled sideways. The value may now break anywhere, so it
   wraps inside the row instead (301px). Ordinary text still breaks between words wherever a word
   fits. word-break:break-word beside overflow-wrap for the reason 13.4 gives. (0,5,4), like the
   title. */
.sm-app table.dataTable.sm-table > tbody > tr.child span.dtr-data{
  overflow-wrap:anywhere;
  word-break:break-word;
}
/* A .sm-table-thumb that Responsive moves into a child row lands in span.dtr-data, an inline box,
   where the thumb's display:block put it on a line of its own under the "Image" label while every
   other detail line reads label and value side by side (Owner/Overview; Manager/ManagerOverview,
   ViewManagerRelationshipPanel, Tenant/ViewUnits, User/ViewUser, ViewCompany and
   ViewCompanyRenting carry the same cell). inline-block puts it back on the label's line, centred
   on it. No script shows or hides the image. (0,5,4) against the thumb's (0,2,0). */
.sm-app table.dataTable > tbody > tr.child span.dtr-data > .sm-table-thumb{
  display:inline-block;
  vertical-align:middle;
}

/* Buttons extension: Building/ViewBuildingDetails' statement export (Copy, Excel, PDF), and the
   exports in ViewAccount and ViewBuildingAssumptions. The bs5 build renders
   `div.dt-buttons.btn-group > button.btn.btn-secondary`, Bootstrap's stock grey, so this is the
   12.9 technique: the --bs-btn-* variables are set at (0,3,0), over .btn-secondary's (0,1,0), and
   Bootstrap's own .btn rules paint every state from them. Copy and export leave data alone, so
   the variables carry 1.1's neutral tool paint, read from 0.5's tokens instead of a hand copy of a
   variant: the white-6% lift inside an --sm-field-border outline (4.59:1 on the card; the copy of
   the old secondary was 1.39:1) under a --text label (14.74:1), and on hover and press the
   white-10% lift inside --sm-field-border-hover (7.99:1) under --text-strong (14.33:1). The
   segmented btn-group shape stays: its -1px overlap turns two outlines into one divider, and
   `.btn-group > .btn:hover{z-index:1}` brings the hovered button's lighter outline to the front.
   The focus ring is also restated as a property at (0,4,0), which outranks Bootstrap's
   `.btn:focus-visible{outline:0}` (0,2,0), so forced colours keep the outline. It was first
   written to outlast ViewBuildingDetails' view-local `.statement-shell .dt-buttons
   .btn{box-shadow:none}` (0,3,0); wave 2 removed that block and the `statement-shell` class, so
   this section is now the only skin on those buttons.
   Two vendor defaults that break on a dark page are answered too. The `.processing` state an
   export shows is text at black 20% under a #282828 ring, invisible here. The "Copy to clipboard"
   confirmation is a white box with no colour of its own, so it inherited the body's light --text
   and printed its message white on white. (0,4,1)-(0,4,2) against (0,3,2)-(0,3,3), and (0,2,1)-(0,2,2)
   against (0,1,1)-(0,1,2). */
.sm-app .dataTables_wrapper .dt-buttons{
  padding:var(--space-3) var(--space-4) 0;
}
.sm-app .dt-buttons .btn{
  --bs-btn-padding-x:var(--space-3);
  --bs-btn-padding-y:var(--space-15);
  --bs-btn-font-family:var(--font-sans);
  --bs-btn-font-size:var(--text-sm);
  --bs-btn-font-weight:var(--weight-semibold);
  --bs-btn-line-height:var(--leading-tight);
  --bs-btn-color:var(--text);
  --bs-btn-bg:var(--sm-btn-neutral-fill);
  --bs-btn-border-color:var(--sm-field-border);
  --bs-btn-border-radius:var(--radius-md);
  --bs-btn-hover-color:var(--text-strong);
  --bs-btn-hover-bg:var(--sm-btn-neutral-fill-hover);
  --bs-btn-hover-border-color:var(--sm-field-border-hover);
  --bs-btn-active-color:var(--text-strong);
  --bs-btn-active-bg:var(--sm-btn-neutral-fill-hover);
  --bs-btn-active-border-color:var(--sm-field-border-hover);
  --bs-btn-disabled-color:var(--text-muted);
  --bs-btn-disabled-bg:transparent;
  --bs-btn-disabled-border-color:var(--border-subtle);
  --bs-btn-focus-box-shadow:var(--focus-ring);
  min-height:var(--control-height-sm);
}
.sm-app .dt-buttons .btn:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
/* --text-secondary, not --text-muted: the label sits on the button's white-6% lift (0.5), and on
   the white-10% lift while hovered, where muted is 3.97:1 and 3.52:1 on the card. These are
   6.92:1 and 6.14:1, and still visibly dimmer than the resting --text, so the busy state reads. */
.sm-app div.dt-buttons .btn.processing{ color:var(--text-secondary); }
.sm-app div.dt-buttons .btn.processing::after{
  border-top-color:var(--text);
  border-bottom-color:var(--text);
}
@media (prefers-reduced-motion: reduce){
  .sm-app div.dt-buttons .btn.processing::after{ animation:none; }
}
.sm-app div.dt-button-info{
  color:var(--text);
  background-color:var(--surface-overlay);
  border:var(--border-hairline) solid var(--border);
  border-radius:var(--radius-lg);
  box-shadow:var(--elev-dark-3);
}
.sm-app div.dt-button-info h2{
  font-size:var(--text-md);
  font-weight:var(--weight-semibold);
  color:var(--text-strong);
  background-color:transparent;
  border-bottom-color:var(--border-subtle);
}

/* ============================================================================
   7. Section header, empty state, alerts, modal surface
   ========================================================================= */
.sm-section-header{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--space-4);
  flex-wrap:wrap;
  padding-bottom:var(--space-3);
  margin-bottom:var(--space-5);
  border-bottom:var(--border-hairline) solid var(--border);
}
/* The page title is the 24px --text-title step, so it outranks every section heading below it
   (18px: .sm-section-header--sm, .sm-disclosure__title). It used to be 22px, the same as the
   disclosure title. The type tokens come through tokens.css's UNVERSIONED @import chain, which a
   browser may serve from a heuristic cache next to a fresh app.css, so each new token falls back
   to the one it replaced: a stale copy renders exactly as before. */
.sm-section-header__title{
  margin:0;
  font-family:var(--font-display);
  font-size:var(--text-title, var(--text-xl));
  font-weight:var(--weight-bold);
  line-height:var(--leading-title, var(--leading-tight));
  letter-spacing:var(--tracking-tight);
  color:var(--text-strong);
}
/* Eyebrow above the page title — a unit number over "Pay application fee". Write it first inside
   __main (13.1), whose column gap spaces it; width:100% keeps it on a line of its own if it is
   written straight into the header row instead, which wraps. Caps like .sm-caps, in brand text
   (7.53:1 even on --surface-raised). Same treatment as the .sm-eyebrow utility (section 9). */
.sm-section-header__eyebrow{
  width:100%;
  margin:0;
  font-size:var(--text-eyebrow, var(--text-xs));
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-caps);
  text-transform:uppercase;
  color:var(--brand-text);
}
.sm-section-header__sub{
  margin:0;
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  color:var(--text-muted);
}
.sm-section-header__actions{
  display:flex;
  align-items:center;
  gap:var(--gap-control);
  flex-wrap:wrap;
  margin-inline-start:auto;
}
.sm-section-header--sm{ margin-bottom:var(--space-4); }
.sm-section-header--sm .sm-section-header__title{ font-size:var(--text-lg); }
.sm-section-header--plain{
  padding-bottom:0;
  border-bottom:0;
}

.sm-empty{
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  text-align:center;
  gap:var(--space-3);
  padding:var(--space-12) var(--space-6);
  color:var(--text-secondary);
}
.sm-empty__medallion{
  flex:0 0 auto;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:var(--space-16);
  height:var(--space-16);
  border-radius:var(--radius-circle);
  background-color:var(--brand-subtle);
  color:var(--brand-text);
  font-size:var(--text-xl);
  line-height:1;
}
/* Tones for the medallion, for states that are a stop rather than an empty list: the public
   error and unavailable pages (stage 2D, section 19.1) first. The badge pairs of section 5; the
   glyph is non-text (3:1), and each pair clears that over --surface: danger 5.66:1, warning
   8.14:1, info 5.94:1, neutral 6.92:1 (measured on the dark skin). */
.sm-empty__medallion--danger{ background-color:var(--danger-subtle); color:var(--danger-text); }
.sm-empty__medallion--warning{ background-color:var(--warning-subtle); color:var(--warning-text); }
.sm-empty__medallion--info{ background-color:var(--info-subtle); color:var(--info-text); }
.sm-empty__medallion--neutral{ background-color:var(--neutral-subtle); color:var(--text-secondary); }
.sm-empty__title{
  margin:0;
  font-size:var(--text-lg);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-tight);
  color:var(--text-strong);
}
/* Body copy, so --text-secondary (checklist 12). --text-muted was 4.59:1 on --surface and failed
   on the --surface-raised an empty state also sits on (4.38:1). */
.sm-empty__body{
  margin:0;
  max-width:46ch;
  font-size:var(--text-base);
  line-height:var(--leading-normal);
  color:var(--text-secondary);
}
.sm-empty__actions{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:var(--gap-control);
  flex-wrap:wrap;
  margin-top:var(--space-2);
}
/* Compact: the in-card and in-table variant, for an empty list inside a .sm-card__body or a
   <td colspan>, where the full state's 48px padding, 64px medallion and 18px title outweigh the
   card around them (Tenant/ViewPayRent improvises one today with a bare <p class="sm-body-sm">).
   The same parts, one step down each; the DataTables empty cell (section 6) matches it. For later
   adoption. The art cap is .sm-app-scoped to outrank 12.7's `.sm-app .sm-empty__art`. */
.sm-empty--compact{
  gap:var(--space-2);
  padding:var(--space-6) var(--space-4);
}
.sm-empty--compact .sm-empty__medallion{
  width:var(--space-10);
  height:var(--space-10);
  font-size:var(--text-md);
}
.sm-empty--compact .sm-empty__title{ font-size:var(--text-md); }
.sm-empty--compact .sm-empty__body{
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
}
.sm-empty--compact .sm-empty__actions{ margin-top:var(--space-1); }
.sm-app .sm-empty--compact .sm-empty__art{ max-width:var(--space-24); }

/* --text-muted is rebound on the alert, the 12.9 technique: muted text on the status washes over
   the canvas measured 4.10:1 on --success-subtle and 4.27:1 on --neutral-subtle, under the 4.5:1
   floor, and --text-secondary is 7.14:1 there. The on-light variant (10.2) points it back at the
   light scale. */
.sm-alert{
  --sm-alert-accent:var(--text-secondary);
  --text-muted:var(--text-secondary);
  display:flex;
  align-items:flex-start;
  gap:var(--space-3);
  padding:var(--space-3) var(--space-4);
  font-size:var(--text-base);
  line-height:var(--leading-snug);
  color:var(--text);
  background-color:var(--neutral-subtle);
  border:var(--border-hairline) solid var(--neutral-border);
  border-radius:var(--radius-md);
}
.sm-alert--success{
  --sm-alert-accent:var(--success-text);
  background-color:var(--success-subtle);
  border-color:var(--success-border);
}
.sm-alert--warning{
  --sm-alert-accent:var(--warning-text);
  background-color:var(--warning-subtle);
  border-color:var(--warning-border);
}
.sm-alert--danger{
  --sm-alert-accent:var(--danger-text);
  background-color:var(--danger-subtle);
  border-color:var(--danger-border);
}
.sm-alert--info{
  --sm-alert-accent:var(--info-text);
  background-color:var(--info-subtle);
  border-color:var(--info-border);
}
.sm-alert__icon{
  flex:0 0 auto;
  line-height:var(--leading-snug);
  color:var(--sm-alert-accent);
}
.sm-alert__content{
  min-width:0;
  flex:1 1 auto;
}
.sm-alert__title{
  display:block;
  font-weight:var(--weight-semibold);
  color:var(--sm-alert-accent);
}
.sm-alert__body{
  color:var(--text-secondary);
}
.sm-alert__close{ margin-inline-start:auto; }

.sm-modal__surface{
  background-color:var(--surface-overlay);
  color:var(--text);
  border:var(--border-hairline) solid var(--border);
  border-radius:var(--radius-xl);
  box-shadow:var(--elev-dark-3);
}
.sm-modal__header{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--space-3);
  padding:var(--space-5) var(--pad-card);
  border-bottom:var(--border-hairline) solid var(--border-subtle);
}
.sm-modal__title{
  margin:0;
  font-family:var(--font-display);
  font-size:var(--text-lg);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-tight);
  color:var(--text-strong);
}
.sm-modal__body{
  padding:var(--pad-card);
}
.sm-modal__footer{
  display:flex;
  align-items:center;
  justify-content:flex-end;
  gap:var(--gap-control);
  flex-wrap:wrap;
  padding:var(--space-4) var(--pad-card);
  border-top:var(--border-hairline) solid var(--border-subtle);
}
/* A Bootstrap modal body ends at its own padding. Its last child's bottom margin (Reboot's 16px
   on a <p>, a .sm-field's 16px) added a blank band above the footer rule. A form that holds
   nothing but hidden inputs renders no box, but its stack margin still reached the body's end:
   User/ViewCompany's #ConfirmCompanyDelete, whose form keeps only the hidden id when the company
   has no QuickBooks link, showed 32px under its warning. That form's margin goes as well; the
   warning <p> before it keeps its Reboot margin until the view marks it mb-0 (requested). A
   hidden-only form that gains a control stops matching, so its spacing returns. */
.sm-app .modal-body > :last-child{ margin-bottom:0; }
.sm-app .modal-body > form:last-child:not(:has(> :not(input[type="hidden"]))){ margin-top:0; }
/* The same margin one level down. When the body's last child is a .sm-stack-* wrapper, the
   wrapper's own last child ends on the margin instead, and with no padding in the way it collapses
   through the stack to the body's end: Building/ViewWorkRequest's Mark Resolved (a .sm-field, 16px)
   and Building/ViewUnit's Delete Unit when the company uses QuickBooks (a hint <p>, 16px). A stack
   spaces its children with margin-top only, so nothing inside it moves, and a stack that is not the
   body's last child keeps its last child's margin. mb-* utilities still win. No `display`. */
.sm-app .modal-body > :is(.sm-stack-2, .sm-stack-3, .sm-stack-4, .sm-stack-6):last-child > :last-child{ margin-bottom:0; }
/* The footer. Bootstrap 5.2.2 spaces a footer's children with `.modal-footer > *{margin:
   calc(var(--bs-modal-footer-gap) * .5)}` and pads the footer half a gap less than the body
   (`calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5)`, 12px), so the children's own
   margins bring them to the body's 16px edge. .sm-btn's margin:0 (section 1, same weight, later
   than Bootstrap) took those margins off, so the settled [Close][primary] footer had the two
   buttons touching, Close flush against "Delete Building" and "Reject Request", and the primary
   4px past the body's content edge (W4-setup-1, W4-details-3, W4-fin-b-5, W4-ops-a-1). The gap is
   now the spacing token (--space-2, which is Bootstrap's own .5rem at the 16px root) and the
   .sm-btn children take their half of it back: 8px between buttons, 16px to both edges, and when a
   narrow footer wraps, 8px between the rows with every button still inside the padding (the widest
   footer, Close + "Save & Generate Schedule" with its icon, is 324px against 333px at 375; at 320
   it wraps and its 235px primary sits in 278px). A child that is a form (ViewPermissionProfile's
   Delete Profile) already had Bootstrap's margin and keeps it; margin utilities still win. The
   variable is declared on the footer, so both of Bootstrap's footer rules read it. (0,2,0) and
   (0,3,0). No `display`. */
.sm-app .modal-footer{ --bs-modal-footer-gap:var(--space-2); }
.sm-app .modal-footer > .sm-btn{ margin:calc(var(--bs-modal-footer-gap) * .5); }

/* ============================================================================
   8. Shell — app root, canvas, rail, topbar, breadcrumb

   Scope: every rule in this section sits under `.sm-app`, the root class the
   authenticated layout carries on <body>. That one ancestor does two jobs.

   1. Containment. The typography flip in 8.1 is inherited, so it would repaint
      the public marketing pages too — they share this stylesheet. The root
      class is what keeps it inside the product.
   2. Specificity. site.css is <link>ed AFTER this file, so a bare
      `.sm-rail__item` (0,1,0) loses every tie to site.css's `.list-group-item`,
      `.navfont`, `.drop-item-style` and `.border-bottom` sitting on the very
      same elements. `.sm-app .sm-rail__item` is (0,2,0) and beats all of them
      outright, which is how the shell restyles with site.css left alone.

   What this file still cannot outrank: site.css's ID rules on #wrapper,
   #sidebar-wrapper and #page-content-wrapper. Nothing class-only beats (1,0,0),
   so those keep the rail's background, its heading padding, and the whole
   open/close geometry. --sm-rail-width in 8.1 is chosen to agree with them.

   Motion: every transition below is expressed in the --sm-duration-* aliases
   from section 0, so the prefers-reduced-motion block there already neutralises
   all of it. Nothing here animates a property other than colour or transform.
   ========================================================================= */

/* --- 8.1 App root: page canvas and the typography flip -------------------
   The one deliberate global change of this stage: the product renders in
   Archivo at the 15px product reading size instead of the system stack at 16px.
   It reaches page content by inheritance, which is intended — every site.css
   rule that names its own font, size or colour still wins on its own elements.
   ------------------------------------------------------------------------ */
.sm-app{
  /* Rail width, deliberately NOT --sidebar-width (264px).
     site.css hides the rail with `#sidebar-wrapper{margin-left:-15rem}` and
     offsets the content with `#page-content-wrapper{margin-left:15rem}`. Both
     are ID rules no class here outranks, and both hard-code 15rem, so a 264px
     rail would leave a 24px sliver on screen when closed and sit 24px under the
     content when open. Raise this to var(--sidebar-width) only in the same
     change that fixes those two margins. */
  --sm-rail-width:15rem;

  background-color:var(--surface-canvas);
  color:var(--text);
  font-family:var(--font-sans);
  font-size:var(--text-base);
  line-height:var(--leading-normal);
}

/* --- 8.2 Shell frame ----------------------------------------------------
   Staged for later views. `.sm-shell` is safe on the existing flex wrapper;
   `.sm-shell__main` and `.sm-shell__content` impose a flex column and a content
   cap, which reflows page CONTENT — do not put them on #page-content-wrapper
   until the stage that owns those views.
   ------------------------------------------------------------------------ */
.sm-app .sm-shell{
  display:flex;
  align-items:stretch;
  min-height:100vh;
  background-color:var(--surface-canvas);
}
.sm-app .sm-shell__main{
  display:flex;
  flex-direction:column;
  flex:1 1 auto;
  min-width:0;
}
.sm-app .sm-shell__content{
  flex:1 1 auto;
  width:100%;
  max-width:var(--content-max);
  margin-inline:auto;
  padding:var(--space-8);
}
.sm-app .sm-shell__content--wide{ max-width:var(--content-max-wide); }

/* --- 8.3 Rail ----------------------------------------------------------- */
.sm-app .sm-rail{
  display:flex;
  flex-direction:column;
  flex:0 0 auto;
  width:var(--sm-rail-width, var(--sidebar-width));
  /* No inline padding, and the trailing divider is an inset shadow rather than a
     border. site.css pins the nav list to `#sidebar-wrapper .list-group{width:15rem}`
     at (1,1,0) — a width this file cannot shrink — and the rail is border-box, so
     ANY inline padding or border narrows the content box below that child and
     pushes it into horizontal overflow. Measured: a 1px border alone was enough.
     An inset shadow paints the same hairline and costs no layout. Rows are
     full-bleed and carry their own padding, which is also what puts the selected
     accent bar on the true leading edge.
     Physical -1px, not logical: this shell is LTR and site.css drives the whole
     rail off `margin-left`. */
  padding:0;
  background-color:var(--surface);
  box-shadow:inset calc(var(--border-hairline) * -1) 0 0 var(--border-subtle);
  color:var(--text-secondary);
  font-family:var(--font-sans);
  font-size:var(--text-base);
}

.sm-app .sm-rail__brand{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  flex:0 0 auto;
  min-height:var(--topbar-height);
  padding:var(--space-3) var(--space-4);
  border-bottom:var(--border-hairline) solid var(--border-subtle);
}
/* The logo link is the rail's first stop and carries no class. As a flex item it is already a
   block box round the logo, so the token ring follows the image; the brand's own padding leaves
   room for the ring inside the rail's overflow. */
.sm-app .sm-rail__brand > a:focus-visible{
  outline:var(--sm-focus-outline);
  border-radius:var(--radius-xs);
  box-shadow:var(--focus-ring);
}
/* The logo under forced colours. The only asset is white on transparent (simpli-white-tp.png and its
   -340.webp), drawn for the dark rail; forced colours repaint the rail and the topbar as Canvas and
   never adjust an image, so a LIGHT palette showed white on white and the rail's top corner was
   empty (W4-units-5). The same image is the topbar logo while the rail is closed. Each opts out and
   carries its own --surface-canvas plate, the backdrop it was drawn for, so it reads on any
   palette; on a dark one the plate all but disappears into Canvas. This is the plate marketing.css
   11.13 gives the public nav and footer logos, for the same reason; a CanvasText outline would only
   frame an empty box. The plate is on the <img>, so the rail link keeps its own forced-colours
   outline outside it. content-box keeps the logo at its 40px; the brand row gives back the 8px the
   plate adds (12px to 8px of vertical padding), so the rail header keeps its height, and the
   topbar's 48px of content box already fits the 48px plate. Normal mode is untouched. */
@media (forced-colors: active){
  .sm-app .sm-rail__brand img,
  .sm-app .sm-topbar__logo{
    forced-color-adjust:none;
    box-sizing:content-box;
    padding:var(--space-1) var(--space-2);
    background-color:var(--surface-canvas);
    border-radius:var(--radius-xs);
  }
  .sm-app .sm-rail__brand{ padding-block:var(--space-2); }
}

/* Goes on the existing nav container. `display` and `gap` are free here;
   padding, width and font-size on that element belong to site.css's
   `#sidebar-wrapper .list-group` and are not contested. */
/* overflow-y:auto is load-bearing, not cosmetic. A multi-role account (Owner +
   Manager + Tenant) renders 18 rail items; with every group expanded on a short
   viewport the nav outgrows the rail and, because the footer is pinned with
   margin-top:auto, "Customer Support" paints straight over the last nav items.
   Scrolling the nav itself keeps the footer pinned and every item reachable. */
.sm-app .sm-rail__nav{
  display:flex;
  flex-direction:column;
  flex:1 1 auto;
  min-height:0;
  overflow-y:auto;
  overscroll-behavior:contain;
  gap:var(--space-05);
  list-style:none;
}
.sm-app .sm-rail__group{
  display:flex;
  flex-direction:column;
  gap:var(--space-05);
}
.sm-app .sm-rail__heading{
  padding:var(--space-4) var(--space-4) var(--space-2);
  font-size:var(--text-xs);
  font-weight:var(--weight-semibold);
  letter-spacing:var(--tracking-caps);
  text-transform:uppercase;
  color:var(--text-muted);
}

.sm-app .sm-rail__item{
  position:relative;
  display:flex;
  align-items:center;
  gap:var(--space-2);
  min-height:var(--control-height);
  padding:var(--space-2) var(--space-4);
  margin:0;
  font-family:var(--font-sans);
  font-size:var(--text-base);
  font-weight:var(--weight-medium);
  line-height:var(--leading-snug);
  text-decoration:none;
  color:var(--text-secondary);
  background-color:transparent;
  border:0;
  border-radius:0;
  cursor:pointer;
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    color var(--sm-duration-fast) var(--ease-standard);
}
/* Interaction states, complete on their own. The class is doubled to (0,4,0) — the section 6 /
   15.1 technique — because the rail anchors can still carry legacy classes whose STATE rules
   outrank a (0,3,0) state here:
     site.css   .list-group-item-sidenav.list-group-item-action:hover/:focus (0,3,0), linked AFTER
                this file: text and background both rgba(35,35,34), which is what made
                Owner > Overview vanish on hover, on keyboard focus and after a click
     site.css   .list-group-item:active (0,2,0), after this file: a #202020 flash that also
                strips the selected wash while the pointer is down
     Bootstrap  .list-group-item-dark.list-group-item-action:focus (0,3,0): #bebebf with #141619
                text on Recent Building and My Buildings
   _UserLayout is dropping those classes, but nothing here depends on that: every state restates
   BOTH colours, so no legacy rule can leave text on a background of its own colour. Plain :focus
   (a click on a group toggle leaves focus there) keeps the resting look. Order matters inside the
   tie: focus, then hover, then active, then the selected variants after them. */
.sm-app .sm-rail__item.sm-rail__item:focus{
  background-color:transparent;
  color:var(--text-secondary);
}
.sm-app .sm-rail__item.sm-rail__item:hover{
  background-color:var(--surface-hover);
  color:var(--text);
  text-decoration:none;
}
.sm-app .sm-rail__item.sm-rail__item:active{
  background-color:var(--surface-selected);
  color:var(--text-strong);
}
.sm-app .sm-rail__item:hover .sm-rail__icon{ color:var(--text-secondary); }
/* Inset (0.2): rows are full-bleed inside .sm-rail__nav, whose overflow-y:auto clips both sides
   of an outset ring — only its top and bottom ever showed. 8.87:1 on today's #000 rail, 7.88:1 on
   --surface, 7.42:1 on the selected wash. Inside the row, so no z-index is needed any more. */
.sm-app .sm-rail__item.sm-rail__item:focus-visible{
  outline:var(--sm-focus-outline);
  outline-offset:var(--sm-focus-offset-inset);
  box-shadow:var(--sm-focus-ring-inset);
}

/* The glyph size is the 0.4 icon alias, not the --space-4 spacing token it used to borrow. */
.sm-app .sm-rail__icon{
  flex:0 0 auto;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:var(--sm-icon-md);
  font-size:var(--sm-icon-md);
  line-height:1;
  color:var(--text-muted);
  transition:color var(--sm-duration-fast) var(--ease-standard);
}
.sm-app .sm-rail__label{
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.sm-app .sm-rail__badge{ margin-inline-start:auto; }

/* Selected. Production ships no active state at all, so this is the headline
   change: brand wash, a 2px brand bar on the leading edge, purple icon. */
.sm-app .sm-rail__item--selected{
  background-color:var(--surface-selected);
  color:var(--text-strong);
  font-weight:var(--weight-semibold);
}
.sm-app .sm-rail__item--selected .sm-rail__icon,
.sm-app .sm-rail__item--selected:hover .sm-rail__icon{ color:var(--brand-text); }
/* The selected item's own states: the same (0,4,0) as the generic ones above and after them, so
   they win the tie. Focus keeps the wash instead of dropping to transparent. */
.sm-app .sm-rail__item.sm-rail__item--selected:focus{
  background-color:var(--surface-selected);
  color:var(--text-strong);
}
.sm-app .sm-rail__item.sm-rail__item--selected:hover,
.sm-app .sm-rail__item.sm-rail__item--selected:active{
  background-color:var(--brand-subtle-hover);
  color:var(--text-strong);
}
.sm-app .sm-rail__item--selected::before{
  content:"";
  position:absolute;
  inset-block:0;
  inset-inline-start:0;
  width:var(--space-05);
  background-color:var(--brand);
}
/* Forced colours repaint the selected wash and this bar as Canvas, which left the semibold label as
   the only mark of the current page. The bar takes Highlight (system colours are not forced, so no
   forced-color-adjust) and widens to 4px there, since no wash stands beside it. No `display`. */
@media (forced-colors: active){
  .sm-app .sm-rail__item--selected::before{
    width:var(--space-1);
    background-color:Highlight;
  }
}

/* Collapsible group row. The chevron is drawn here rather than left to
   Bootstrap's `.dropdown-toggle::after`, so it can sit at the trailing edge and
   turn with aria-expanded. Every border longhand is restated because that
   Bootstrap pseudo-element ships its own triangle borders on the same box. */
.sm-app .sm-rail__item--parent::after{
  content:"";
  flex:0 0 auto;
  width:var(--space-15);
  height:var(--space-15);
  margin:0 0 var(--space-05) auto;
  border:0;
  border-inline-end:var(--border-emphasis) solid currentColor;
  border-block-end:var(--border-emphasis) solid currentColor;
  transform:rotate(45deg);
  opacity:.7;
  transition:transform var(--sm-duration-fast) var(--ease-standard);
}
.sm-app .sm-rail__item--parent[aria-expanded="true"]::after{
  margin:var(--space-05) 0 0 auto;
  transform:rotate(-135deg);
}

/* Submenu panel. Never give this a `display`: Bootstrap hides a closed panel
   with `.collapse:not(.show){display:none}` at (0,2,0), and this file is linked
   after Bootstrap, so a same-weight `display` here would win the tie and pin
   every submenu permanently open. `flex` is safe and keeps the collapse height
   animation definite when the panel is a column flex item. */
.sm-app .sm-rail__submenu{
  flex:0 0 auto;
  margin:0;
  padding:0;
  list-style:none;
}
/* The row's 16px inline padding + the icon column + the 8px gap (40px at the
   default root), so a child label starts exactly under its parent's label. It
   follows --sm-icon-md, like the icon's own width, so the two cannot drift apart
   when the root size changes. */
.sm-app .sm-rail__item--sub{
  min-height:var(--control-height-sm);
  padding-inline-start:calc(var(--space-4) + var(--sm-icon-md) + var(--space-2));
  font-size:var(--text-sm);
  font-weight:var(--weight-regular);
}
/* The rule above has the same (0,2,0) weight as .sm-rail__item--selected and comes later, so a
   selected child row lost its semibold (400 where a selected top-level row is 600; coverage
   sweep, 6 Oct). A selected row is semibold at either level. */
.sm-app .sm-rail__item--sub.sm-rail__item--selected{ font-weight:var(--weight-semibold); }

.sm-app .sm-rail__footer{
  margin-top:auto;
  padding:var(--space-3) var(--space-4);
  border-top:var(--border-hairline) solid var(--border-subtle);
  font-size:var(--text-sm);
  color:var(--text-muted);
}

@media (pointer: coarse){
  .sm-app .sm-rail__item{ min-height:var(--touch-target-min); }
}

/* 72px icon rail. Opt-in and unused today: the hamburger slides the whole rail
   off-canvas through site.css rather than narrowing it. This is the spec's
   collapsed state, ready for when that geometry moves into this file. */
.sm-app .sm-rail--collapsed{
  width:var(--sidebar-width-collapsed);
  align-items:center;
}
.sm-app .sm-rail--collapsed .sm-rail__label,
.sm-app .sm-rail--collapsed .sm-rail__heading,
.sm-app .sm-rail--collapsed .sm-rail__badge,
.sm-app .sm-rail--collapsed .sm-rail__footer,
.sm-app .sm-rail--collapsed .sm-rail__item--parent::after{ display:none; }
.sm-app .sm-rail--collapsed .sm-rail__item{
  justify-content:center;
  padding-inline:0;
}
.sm-app .sm-rail--collapsed .sm-rail__item--sub{ padding-inline-start:0; }

/* The hamburger (#menu-toggle, wwwroot/js/site.js) toggles `toggled` on
   #wrapper, and site.css owns the slide itself, so these rules only add what a
   rail that overlays or leaves the viewport needs. `toggled` is never selected
   on its own: the vendored pdf.js viewer puts that same class on its toolbar
   buttons, and those render inside this shell. */
@media (min-width: 768px){
  /* Above this breakpoint site.css makes the rail position:fixed with
     min-height:100vh, so a nav taller than the viewport is unreachable — it has
     no scroll of its own and does not move with the page. Cap and scroll it. */
  .sm-app .sm-rail{
    max-height:100vh;
    overflow-y:auto;
    overscroll-behavior:contain;
  }
}
@media (max-width: 767.98px){
  /* Restates the inset divider: box-shadow is one property, so listing only the
     elevation here would drop the rail's trailing hairline while it is open. */
  .sm-app .toggled .sm-rail{
    box-shadow:
      inset calc(var(--border-hairline) * -1) 0 0 var(--border-subtle),
      var(--elev-dark-3);
  }
  .sm-app .sm-shell__content{ padding:var(--space-4); }
}

/* --- 8.4 Topbar --------------------------------------------------------- */
.sm-app .sm-topbar{
  display:flex;
  align-items:center;
  flex-wrap:wrap;
  gap:var(--space-3);
  /* Let content wrap at increased text sizes without clipping the controls. */
  min-height:var(--topbar-height);
  padding:var(--space-2) var(--space-6);
  background-color:var(--surface);
  border-bottom:var(--border-hairline) solid var(--border-subtle);
  color:var(--text);
  font-family:var(--font-sans);
  font-size:var(--text-base);
}
/* `justify-content` is deliberately left undeclared so Bootstrap's navbar
   distribution — which the existing topbar markup is arranged around — stands. */
.sm-app .sm-topbar--sticky{
  position:sticky;
  top:0;
  z-index:20;
}
.sm-app .sm-topbar__title{
  margin:0;
  font-family:var(--font-display);
  font-size:var(--text-md);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-tight);
  color:var(--text-strong);
}
.sm-app .sm-topbar__actions{
  display:flex;
  align-items:center;
  gap:var(--gap-control);
  margin-inline-start:auto;
}
/* The menu toggle is a tool (1.1) whose label is an icon, so it takes the neutral outline from
   0.5's tokens: a white-6% lift inside an --sm-field-border edge, 4.59:1 on the topbar's
   --surface, and under the pointer a white-10% lift inside --sm-field-border-hover, 7.99:1. The
   transparent fill and --border-subtle hairline it replaces were 1.15:1, so the button read as a
   bare glyph. The glyph keeps its own colours, --text-secondary at rest and --text on hover,
   6.92:1 and 13.06:1 on the lift. */
.sm-app .sm-topbar__toggle{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  flex:0 0 auto;
  width:var(--control-height);
  height:var(--control-height);
  padding:0;
  color:var(--text-secondary);
  background-color:var(--sm-btn-neutral-fill);
  border:var(--border-hairline) solid var(--sm-field-border);
  border-radius:var(--radius-md);
  cursor:pointer;
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard),
    color var(--sm-duration-fast) var(--ease-standard);
}
.sm-app .sm-topbar__toggle:hover{
  background-color:var(--sm-btn-neutral-fill-hover);
  border-color:var(--sm-field-border-hover);
  color:var(--text);
}
.sm-app .sm-topbar__toggle:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
@media (max-width:767.98px){
  .sm-app .sm-topbar__logout,
  .sm-app .sm-topbar__toggle{
    width:var(--touch-target-min);
    min-height:var(--touch-target-min);
    padding:0;
  }
  .sm-app .sm-topbar__logout-label{ display:none; }
}

/* --- 8.5 Breadcrumb ----------------------------------------------------- */
.sm-app .sm-breadcrumb{
  display:flex;
  align-items:center;
  flex-wrap:wrap;
  gap:var(--space-2);
  margin:0;
  padding:0;
  list-style:none;
  font-size:var(--text-sm);
  line-height:var(--leading-tight);
  color:var(--text-muted);
}
.sm-app .sm-breadcrumb__item{
  display:inline-flex;
  align-items:center;
  gap:var(--space-2);
}
.sm-app .sm-breadcrumb__item + .sm-breadcrumb__item::before{
  content:"/";
  color:var(--text-muted);
}
.sm-app .sm-breadcrumb__link{
  color:var(--text-link);
  text-decoration:none;
  border-radius:var(--radius-xs);
  transition:color var(--sm-duration-fast) var(--ease-standard);
}
.sm-app .sm-breadcrumb__link:hover{
  color:var(--text-link-hover);
  text-decoration:underline;
}
.sm-app .sm-breadcrumb__link:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
.sm-app .sm-breadcrumb__current{
  color:var(--text);
  font-weight:var(--weight-medium);
}
/* In the topbar a long page title truncates instead of pushing "Log out" onto a second row. The
   topbar is a wrapping flex row (8.4), and a wrapping row places each item at its max-content width
   first, so at 768 beside the rail (480px inside the bar) "Properties / Building Depreciation: QA
   Harborview Residences" needed 373px of the 323px the toggle and Log out leave, and Log out wrapped
   under it: a 112px bar (W4-fin-a-12). The breadcrumb's <nav> (the one topbar child holding
   .sm-breadcrumb; Bootstrap's d-md-flex shows it from 768px) now starts from zero and takes the space
   that is left, so the bar never wraps for it; min-width:0 on the nav and the list lets them shrink
   below the title's width; the section name keeps its own; and the current title ends in an
   ellipsis on one line (243px of text at 768). It becomes a block box for that, because
   text-overflow acts on a block container's own line and never on the anonymous item inside a flex
   one; the separator's 8px, which was the item's flex gap, moves onto the "/". The full title is
   still in the DOM, and it is the page's own heading. Below 768px the breadcrumb is not shown.
   Neither element is a .collapse or a script target, so `display` is safe here. */
.sm-app .sm-topbar > nav:has(> .sm-breadcrumb){
  flex:1 1 0%;
  min-width:0;
}
.sm-app .sm-topbar .sm-breadcrumb{
  flex-wrap:nowrap;
  min-width:0;
}
.sm-app .sm-topbar .sm-breadcrumb__item{ flex:0 0 auto; }
.sm-app .sm-topbar .sm-breadcrumb__current{
  display:block;
  flex:0 1 auto;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.sm-app .sm-topbar .sm-breadcrumb__item + .sm-breadcrumb__current::before{ margin-inline-end:var(--space-2); }
.sm-app .sm-shell-notice{
  width:calc(100% - var(--space-6) * 2);
  max-width:calc(var(--content-max) - var(--space-6) * 2);
  margin:var(--space-4) auto 0;
  padding-inline-end:calc(var(--space-4) + var(--control-height));
}

/* ============================================================================
   9. Utilities
   ========================================================================= */
.sm-num{
  font-variant-numeric:var(--numeric-tabular);
  font-feature-settings:"tnum" 1;
}
.sm-num--right{ text-align:right; }
.sm-muted{ color:var(--text-muted); }
/* For body copy that .sm-muted was carrying (checklist 12): --text-muted fails 4.5:1 on the raised
   and overlay surfaces, --text-secondary clears 7:1 on every dark one. Dark surfaces, and .sm-site,
   which rebinds the token; not the entry flow's white panel. */
.sm-text-secondary{ color:var(--text-secondary); }
.sm-mono{
  font-family:var(--font-mono);
  font-size:0.9375em;
  letter-spacing:var(--tracking-normal);
}
.sm-caps{
  font-size:var(--text-xs);
  font-weight:var(--weight-semibold);
  letter-spacing:var(--tracking-caps);
  text-transform:uppercase;
  color:var(--text-muted);
}
.sm-link{
  color:var(--text-link);
  text-decoration:none;
  border-radius:var(--radius-xs);
  transition:color var(--sm-duration-fast) var(--ease-standard);
}
.sm-link:hover{
  color:var(--text-link-hover);
  text-decoration:underline;
}
.sm-link:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
/* opacity:1 because the divider is usually an <hr>, and Bootstrap's reboot ships `hr{opacity:.25}`:
   a quarter of --border-subtle's 6% white is nothing, so every dark divider in the app (Tenant,
   User and Building payment and lease views) was invisible. */
.sm-divider{
  height:var(--border-hairline);
  margin:var(--space-5) 0;
  border:0;
  background-color:var(--border-subtle);
  opacity:1;
}
/* A background-painted rule is repainted as Canvas in forced colours and vanishes (F-entry-3).
   .sm-divider--on-light (10.2) sets its colour later at the same (0,1,0), so it is named here at
   (0,2,0) as well; without it the Login and User/CreateUser rule stayed #E7E7EE on a white Canvas
   (critic-2 defect 7). */
@media (forced-colors: active){
  .sm-divider,
  .sm-divider.sm-divider--on-light{ forced-color-adjust:none; background-color:CanvasText; }
}
.sm-stack-2 > * + *{ margin-top:var(--space-2); }
.sm-stack-3 > * + *{ margin-top:var(--space-3); }
.sm-stack-4 > * + *{ margin-top:var(--space-4); }
.sm-stack-6 > * + *{ margin-top:var(--space-6); }
.sm-row{
  display:flex;
  align-items:center;
  gap:var(--gap-control);
  flex-wrap:wrap;
}
/* A checkbox or switch whose label is the row's last child (the billing consents in
   Building/ViewUnitType's modals and ViewAddBulkUnits, the Offline-tenant rows, the payment
   confirmations' consent): the label takes the rest of the row and wraps beside the box, where
   .sm-row's wrapping used to drop a long label onto a line of its own at 375. flex-basis 0 keeps
   it on the box's line, the .sm-auth__choice technique (10.1). The label box now spans the row,
   so a click anywhere on that line toggles the box. Radio pairs (label, then radio) and rows
   with more after the label are untouched. */
.sm-row > :is(.sm-checkbox, .sm-switch) + .sm-field__label:last-child{
  flex:1 1 0;
  min-width:0;
}
.sm-grid{
  display:grid;
  gap:var(--gap-grid);
}
.sm-visually-hidden{
  position:absolute;
  width:var(--border-hairline);
  height:var(--border-hairline);
  padding:0;
  margin:calc(var(--border-hairline) * -1);
  overflow:hidden;
  clip-path:inset(50%);
  white-space:nowrap;
  border:0;
}

/* --- Type utilities ------------------------------------------------------
   Size and leading only; colour stays with the context (or .sm-muted). Each
   declares its own margin, per the settled convention: Bootstrap's reboot gives
   <p> a margin-bottom that would make the same class taller on a <p> than on a
   <span>. `sm-body-sm` in Tenant/ViewPayRent is the view-side name for
   .sm-text-sm and is mapped in that view's stage, not aliased here. */
.sm-text-xs{
  margin:0;
  font-size:var(--text-xs);
  line-height:var(--leading-snug);
}
.sm-text-sm{
  margin:0;
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
}
.sm-text-base{
  margin:0;
  font-size:var(--text-base);
  line-height:var(--leading-normal);
}
.sm-text-lg{
  margin:0;
  font-size:var(--text-lg);
  line-height:var(--leading-normal);
}

/* Lead: the one introductory paragraph under a page title. A step above the
   15px body in --text-secondary (7.64:1 or better on every dark surface), held
   to a readable measure. --text-lead falls back to the step it names, for the
   same stale-cache reason the page title gives (section 7). */
.sm-lead{
  margin:0;
  max-width:var(--prose-max);
  font-size:var(--text-lead, var(--text-lg));
  line-height:var(--leading-normal);
  color:var(--text-secondary);
}

/* Eyebrow: a short caps label above a heading. .sm-section-header__eyebrow (7)
   is the same treatment in the page-title slot. */
.sm-eyebrow{
  margin:0;
  font-size:var(--text-eyebrow, var(--text-xs));
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-caps);
  text-transform:uppercase;
  color:var(--brand-text);
}

/* --- Line and measure utilities -----------------------------------------------
   .sm-nowrap keeps an identifier whole where a title or a cell would otherwise break inside it
   ("QA-" / "101"): wrap the identifier alone in it, never a whole heading (checklist 11).
   .sm-measure holds a form or a run of prose to the reading measure .sm-lead already keeps, 68ch
   (User/ViewCustomerSupport's inputs reached 1087px at 1440).
   .sm-clamp cuts text to --sm-clamp-lines lines, 3 unless a context sets it, ending in an ellipsis.
   It declares `display`, so it goes on an inner <span>, never on a <td>, a .collapse or anything a
   script shows or hides; and the full text must stay readable elsewhere (the row's View page, for
   the Admin dashboard's error log). */
.sm-nowrap{ white-space:nowrap; }
.sm-measure{ max-width:var(--prose-max); }
.sm-clamp{
  display:-webkit-box;
  -webkit-box-orient:vertical;
  -webkit-line-clamp:var(--sm-clamp-lines, 3);
  line-clamp:var(--sm-clamp-lines, 3);
  overflow:hidden;
}

/* --- Icon -------------------------------------------------------------------
   For a Font Awesome <i> or an inline <svg>. A fixed box — 1em tall and 1.25em
   wide, fa-fw's width — so glyphs of different natural widths line up in a
   column, and it never squashes in a flex row. No colour of its own: a font
   glyph paints in the inherited colour, and an SVG with no fill attribute of its
   own is given currentColor (a stroke icon that declares fill="none" keeps it).
   An SVG has no text baseline, so it drops 0.125em to sit where Font Awesome's
   glyphs sit. Deliberately no `display`: Font Awesome makes the <i>
   inline-block, and a toggled icon (the disclosure carets) must never get one
   from here. */
.sm-icon{
  flex:0 0 auto;
  width:1.25em;
  height:1em;
  line-height:1;
  text-align:center;
}
svg.sm-icon{ vertical-align:-0.125em; }
svg.sm-icon:not([fill]){ fill:currentColor; }
.sm-icon--sm{ font-size:var(--sm-icon-sm); }
.sm-icon--md{ font-size:var(--sm-icon-md); }
.sm-icon--lg{ font-size:var(--sm-icon-lg); }

/* --- Skip link -----------------------------------------------------------
   The first focusable element in _UserLayout and _Layout:
     <a class="visually-hidden-focusable sm-skip-link" href="#main">
   Bootstrap's .visually-hidden-focusable owns the hidden state — it matches
   only :not(:focus):not(:focus-within), and every declaration is !important —
   and styles nothing once the link takes focus. So this supplies only what the
   revealed link needs and contests nothing: its padding simply loses to
   Bootstrap's `padding:0 !important` until focus arrives. A brand pill fixed to
   the top-left corner of the viewport, above the rail and the topbar; the
   z-index clears Bootstrap's whole scale (1000-1090) and is kept in a custom
   property so the number has a name. Global rather than .sm-app-scoped, because
   the marketing shell (.sm-site) uses it too. White on --brand is 7.98:1. */
.sm-skip-link{
  --sm-skip-link-z:1100;
  position:fixed;
  top:var(--space-3);
  inset-inline-start:var(--space-3);
  z-index:var(--sm-skip-link-z);
  padding:var(--space-2) var(--space-4);
  font-family:var(--font-sans);
  font-size:var(--text-base);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  text-decoration:none;
  white-space:nowrap;
  color:var(--brand-on);
  background-color:var(--brand);
  border-radius:var(--radius-pill);
}
/* Bootstrap's reboot `a:hover` (0,1,1) would otherwise recolour it. */
.sm-skip-link:hover{
  color:var(--brand-on);
  text-decoration:none;
  background-color:var(--brand-active);
}
.sm-skip-link:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
/* The link's target is #main with tabindex="-1" in every shell: a <main> in _UserLayout and
   _ValuationToolLayout, a <div role="main"> in _Layout until the apply-flow views drop their own
   <main>. tabindex="-1" exists only so the link can move focus there; the container is not a
   control, and the browser's own ring round the whole content area on arrival reads as a stray
   frame (the GOV.UK pattern drops it too). Unscoped because the valuation shell has no scope
   class. The one deliberate `outline:none` in this file, forced colours included: the next Tab
   lands on a real control, which shows its ring. */
:is(main, #main)[tabindex="-1"]:focus{ outline:none; }

/* A heading that receives focus programmatically (AddBuilding moves focus to the new step's heading
   after Next/Previous) shows the product ring for keyboard users instead of the browser default.
   The ring reaches 4px past the heading's box (a 2px gap and a 2px band; in forced colours a 2px
   outline at a 2px offset), and on AddBuilding's Address and Financial steps the building-name
   subtitle started right under the heading, so the band ran through the tops of its glyphs; the
   heading also spanned the subtitle's width, so the ring was a 183px box round "Address"
   (F-bldg-a-2). The heading now shrinks to its text, which moves nothing (it starts where it did,
   and its column is as wide as its widest line either way), and a subtitle that follows it sits
   4px lower, exactly the ring's reach, so the ring ends where the subtitle's line box begins: its
   tallest ink is 3.7px further down (13px Archivo 400, ascenders 0.723em, 1.7px half-leading). The other step
   headings already have 12px or more below them. No display. */
.sm-app :is(.sm-card__title, .sm-media-card__title)[tabindex="-1"]{ width:fit-content; }
.sm-app .sm-card__title[tabindex="-1"] + .sm-card__subtitle{ margin-top:calc(var(--sm-focus-width) * 2); }
.sm-app :is(.sm-card__title, .sm-media-card__title)[tabindex="-1"]:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
  border-radius:var(--radius-sm);
}

/* ============================================================================
   10. Entry flow — the split auth shell and its light-surface controls

   Scope: `.sm-auth` is the root class Views/Shared/_AuthLayout.cshtml carries on
   <body>. That layout is two columns — a brand panel on the product's dark
   canvas, and a form column on the marketing page white — so this section has
   two halves: the shell itself, and `--on-light` modifiers for the controls.

   The modifiers exist because sections 1-4 are tuned for dark surfaces: their
   text tokens are the 0-300 ink steps and their field fill is #0F0F15. Rather
   than override those rules under an ancestor (which would make the dark rule
   unreadable from its own declaration), each control that appears on white
   takes an explicit modifier class, and every modifier is declared after the
   rule it refines so a plain class tie resolves in the modifier's favour.

   site.css is deliberately NOT linked by _AuthLayout, so nothing here has to
   outrank it and the plain `.sm-` weights are enough.
   ========================================================================= */

/* The semantic layer has no on-light status aliases: --danger-text and its
   siblings are the 400 steps, picked for contrast against near-black, and they
   fail AA on white. These name the 600 steps for the same jobs. Declared here
   rather than in tokens.css because that file is a vendored export of the
   design project and stays byte-identical to it.
   Success and warning sit one step darker, because they also colour an
   on-light alert's title and icon ON THE ALERT'S OWN TINT, where the 600 steps
   miss 4.5:1: --success-600 is 4.00:1 on --success-subtle and --warning-600
   3.21:1 on --warning-subtle (axe, SuspendedAccount's "Important notice").
   Success takes the 0.3 700 step (5.46:1). Warning is --warning-600 mixed 75/25
   with --ink-950, #876213 (5.00:1 on its tint, 5.55:1 on white): the derivation
   marketing.css already makes for .sm-site, pending the same palette sign-off.
   A browser without color-mix() drops the value, and the title then inherits
   the alert's own --text-on-light.
   The field border is --ink-400, 4.07:1 on white: --border-light-strong was
   1.54:1, under the 3:1 a control's boundary needs. Hover DARKENS, to --ink-500
   (6.61:1); the old --ink-300 hover was 2.34:1, lighter than the new rest. The
   outline button no longer reads these field borders: it takes the light tonal
   paint of marketing.css section 16 (10.2). */
:root{
  --sm-on-light-danger:var(--danger-600);
  --sm-on-light-success:var(--sm-success-700);
  --sm-on-light-warning:color-mix(in srgb, var(--warning-600) 75%, var(--ink-950));
  --sm-on-light-info:var(--info-600);
  --sm-on-light-field:var(--ink-0);
  --sm-on-light-field-border:var(--ink-400);
  --sm-on-light-field-border-hover:var(--ink-500);
}

/* --- 10.1 Shell ---------------------------------------------------------- */
.sm-auth{
  margin:0;
  background-color:var(--surface-canvas);
  color:var(--text);
  font-family:var(--font-sans);
  font-size:var(--text-base);
  line-height:var(--leading-normal);
  -webkit-font-smoothing:antialiased;
}

/* auto-fit, not auto-fill: on a viewport wide enough for three 380px tracks the
   third has no item in it, so auto-fit collapses it to zero and the two real
   columns split the row 50/50. Below 760px only one track fits and the panel
   stacks above the form. */
.sm-auth__grid{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(380px,1fr));
  min-height:760px;
  min-height:max(760px,100vh);
}

.sm-auth__brand{
  position:relative;
  overflow:hidden;
  display:grid;
  align-content:space-between;
  gap:var(--space-10);
  padding:var(--space-16) var(--space-12);
  background-color:var(--surface-canvas);
}
/* The glow is absolutely positioned, so it paints above static siblings.
   Positioning the panel's own children puts them back on top of it without
   introducing a z-index scale. */
.sm-auth__brand > *{ position:relative; }

/* The one gradient the system sanctions. */
.sm-auth__glow{
  position:absolute;
  inset-inline-start:-15%;
  top:15%;
  width:80%;
  aspect-ratio:1;
  background:radial-gradient(closest-side,var(--brand-a24),transparent 70%);
  filter:blur(40px);
  pointer-events:none;
}

.sm-auth__logo-link{
  display:inline-flex;
  justify-self:start;
  text-decoration:none;
  border-radius:var(--radius-xs);
}
.sm-auth__logo-link:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
/* The lockup is img/simpli-white-tp-340.webp, 340x138 on a transparent ground, sized by height
   and left to scale, matching the 40px rail logo in _UserLayout. The mix-blend-mode:lighten that
   used to hide the opaque black ground of the 5762x2359 simpli-white.png is dropped: _AuthLayout
   no longer uses that PNG, which was the line's own stated condition for removal. */
.sm-auth__logo{
  display:block;
  width:auto;
  height:var(--space-10);
}
/* Forced colours paint the brand panel Canvas, so under a light palette this white-on-transparent
   lockup vanished exactly as the rail logo did (8.3). The same --surface-canvas plate, on the image,
   so the link keeps its plain forced-colours outline outside it; content-box keeps the 40px. */
@media (forced-colors: active){
  .sm-auth__logo{
    forced-color-adjust:none;
    box-sizing:content-box;
    padding:var(--space-1) var(--space-2);
    background-color:var(--surface-canvas);
    border-radius:var(--radius-xs);
  }
}

.sm-auth__headline{
  margin:0;
  max-width:22ch;
  font-family:var(--font-display);
  font-size:var(--text-2xl);
  font-weight:var(--weight-semibold);
  letter-spacing:var(--tracking-display);
  line-height:var(--leading-tight);
  color:var(--text-strong);
  text-wrap:balance;
}

.sm-auth__checks{
  display:grid;
  gap:var(--space-3);
  margin:var(--space-8) 0 0;
  padding:0;
  list-style:none;
}
.sm-auth__check{
  display:flex;
  align-items:flex-start;
  gap:var(--space-3);
  font-size:var(--text-base);
  line-height:var(--leading-snug);
  color:var(--text-secondary);
}
.sm-auth__check-icon{
  flex:0 0 auto;
  width:var(--space-5);
  height:var(--space-5);
  color:var(--brand-text);
}
/* The UA keeps the svg's explicit brand tint in forced colours (2.37:1 on a white Canvas, F-entry-2);
   the ticks follow the list text instead. */
@media (forced-colors: active){
  .sm-auth__check-icon{ color:CanvasText; }
}

.sm-auth__legal{
  margin:0;
  font-size:var(--text-sm);
  color:var(--text-muted);
}

/* The white column is the entry flow's one light scope, so it takes the light rings from 0.2: a
   --page gap with a --purple-600 ring (7.98:1 on white), and the danger variant on --danger-600
   (5.80:1). Declared on the scope root, where every control inside resolves them; the dark brand
   panel beside it keeps the root values. */
.sm-auth__panel{
  --focus-ring:var(--sm-focus-ring-light);
  --sm-focus-ring-danger:var(--sm-focus-ring-danger-light);
  display:flex;
  align-items:center;
  justify-content:center;
  padding:var(--space-16) var(--space-6);
  background-color:var(--page);
  color:var(--text-on-light);
}
.sm-auth__form{
  display:flex;
  flex-direction:column;
  gap:var(--space-6);
  width:100%;
  max-width:380px;
}
/* Opt-in via ViewData["AuthFormWidth"] for the three views whose content does
   not fit a sign-in column: the suspension notice, the SMS code screen and
   sign-up. */
.sm-auth__form--wide{ max-width:640px; }

/* Bootstrap's .alert — kept on the layout's two TempData blocks purely as the
   data-bs-dismiss hook — also carries margin-bottom:1rem, which would stack on
   top of the column's own gap. */
.sm-auth__form > .alert{ margin-bottom:0; }

/* Two fields on one line where they belong together, one line each as soon as
   the pair drops below 200px apiece. */
.sm-auth__row{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
  gap:var(--space-4);
}
.sm-auth__row .sm-field{ margin-bottom:0; }

/* Entry-flow forms are one column. .sm-field ships a 16px bottom margin for use
   outside a stack, which would double up against the rhythm below. */
.sm-auth__panel form{ margin:0; }
.sm-auth__panel form > * + *{ margin-top:var(--space-4); }
/* Login and CreateUser open their form with a script-owned alert that stays hidden (an inline
   display:none) until verification fails. Hidden, it still counted as the first `*` of `* + *`,
   so the first field sat 16px lower than on the other entry forms: 40px under the intro instead
   of 24px. This is keyed on the HIDDEN state, not on the alert alone, so a shown alert keeps the
   usual 16px under it with or without an mb-3 of its own: the two margins collapse in this block. */
.sm-auth__panel form > .sm-alert:first-child:is([hidden], .d-none, [style*="display"][style*="none"]) + *{
  margin-top:0;
}
.sm-auth__panel form > .sm-field{ margin-bottom:0; }
/* The submit sits further from the last field than the fields sit from each
   other — but only when it follows one. A form whose whole content is a button,
   like the resend-code forms, keeps its own box. */
.sm-auth__panel form > * + .sm-btn{ margin-top:var(--space-6); }

.sm-auth__head{
  display:grid;
  gap:var(--space-2);
}
.sm-auth__title{
  margin:0;
  font-family:var(--font-display);
  font-size:var(--text-2xl);
  font-weight:var(--weight-bold);
  letter-spacing:var(--tracking-display);
  line-height:var(--leading-tight);
  color:var(--text-on-light-strong);
}
.sm-auth__sub{
  margin:0;
  font-size:var(--text-base);
  line-height:var(--leading-snug);
  color:var(--text-on-light-muted);
}
.sm-auth__section-title{
  margin:0;
  font-family:var(--font-display);
  font-size:var(--text-lg);
  font-weight:var(--weight-semibold);
  letter-spacing:var(--tracking-tight);
  line-height:var(--leading-tight);
  color:var(--text-on-light-strong);
}
.sm-auth__body{
  margin:0;
  font-size:var(--text-base);
  line-height:var(--leading-normal);
  color:var(--text-on-light);
}
.sm-auth__meta{
  display:flex;
  align-items:center;
  justify-content:center;
  flex-wrap:wrap;
  gap:var(--space-2);
  margin:0;
  font-size:var(--text-sm);
  text-align:center;
  color:var(--text-on-light-muted);
}

/* Icon lists on the light column — the suspension notice's affected services and
   the SMS consent screen's reasons. The brand panel's .sm-auth__checks is the
   same shape in dark-surface colours. */
.sm-auth__items{
  display:grid;
  gap:var(--space-2);
  margin:0;
  padding:0;
  list-style:none;
}
.sm-auth__item{
  display:flex;
  align-items:flex-start;
  gap:var(--space-3);
  font-size:var(--text-base);
  line-height:var(--leading-snug);
  color:var(--text-on-light);
}
.sm-auth__item-icon{
  flex:0 0 auto;
  margin-top:var(--space-05);
  color:var(--brand-text-on-light);
}
.sm-auth__item-icon--danger{ color:var(--sm-on-light-danger); }

/* A checkbox beside its label. Not .sm-row: that wraps each item at its
   max-content width, so a label as long as the SMS consent copy drops onto its
   own line under the box. Here the label is flex:1 1 0 with min-width:0, so it
   counts as zero wide when the row breaks into lines and always stays on the
   box's line. That is what makes it safe to let this row wrap for the one item
   that should: a field error (CreateUser's terms), which now takes a line of its
   own under the label text instead of squeezing in as a third column — at 1440 it
   ran under the reCAPTCHA badge, at 375 it pushed the label onto two lines.
   The label's first line is centred on the box: padding-top is half the gap
   between the box and one line of label (a negative result floors at 0). */
.sm-auth__choice{
  display:flex;
  flex-wrap:wrap;
  align-items:flex-start;
  gap:var(--space-15) var(--space-3);
}
.sm-auth__choice > label{
  flex:1 1 0;
  min-width:0;
  padding-top:calc((var(--space-5) - var(--leading-snug) * 1em) / 2);
  line-height:var(--leading-snug);
}
.sm-auth__choice > .sm-field__error{
  flex:1 0 100%;
  padding-inline-start:calc(var(--space-5) + var(--space-3));
}
/* Section 4 grows the box to 24px on touch screens; the label and the error follow it. */
@media (pointer: coarse){
  .sm-auth__choice > label{ padding-top:calc((var(--space-6) - var(--leading-snug) * 1em) / 2); }
  .sm-auth__choice > .sm-field__error{ padding-inline-start:calc(var(--space-6) + var(--space-3)); }
}

.sm-auth__bullets{
  display:grid;
  gap:var(--space-2);
  margin:0;
  padding-inline-start:var(--space-5);
  font-size:var(--text-base);
  line-height:var(--leading-snug);
  color:var(--text-on-light);
}
.sm-auth__foot{
  margin:0;
  font-size:var(--text-xs);
  line-height:var(--leading-snug);
  text-align:center;
  color:var(--text-on-light-muted);
}

/* Bootstrap's reboot paints every unclassed <a> #0d6efd and underlines it, and
   the entry flow has no site.css to correct that. Buttons are excluded so an
   <a class="sm-btn"> keeps its own label colour. */
.sm-auth__panel a:not(.sm-btn){
  color:var(--brand-text-on-light);
  text-underline-offset:2px;
}
.sm-auth__panel a:not(.sm-btn):hover{ color:var(--purple-800); }
.sm-auth__panel a:not(.sm-btn):focus-visible{
  outline:var(--sm-focus-outline);
  border-radius:var(--radius-xs);
  box-shadow:var(--focus-ring);
}

@media (max-width: 759.98px){
  /* One column: the panel is sized by its content and the form takes the rest,
     so the white column still reaches the bottom of a tall viewport.
     grid-template-columns must be restated. The base rule is
     repeat(auto-fit,minmax(380px,1fr)), and a 380px floor is wider than a 360px
     or 375px phone, so leaving it inherited overflows the viewport horizontally
     on the most common handset widths. minmax(0,1fr) lets the column shrink. */
  .sm-auth__grid{
    grid-template-columns:minmax(0,1fr);
    grid-template-rows:auto 1fr;
  }
  .sm-auth__brand{
    gap:var(--space-6);
    padding:var(--space-8) var(--space-5);
  }
  /* The pitch and the entity line are brand, not task: on a phone they would
     push the form itself below the fold. */
  .sm-auth__pitch,
  .sm-auth__legal{ display:none; }
  .sm-auth__panel{ align-items:flex-start; padding:var(--space-10) var(--space-5); }
}

/* --- 10.2 Light-surface controls ---------------------------------------- */
.sm-field__label--on-light{ color:var(--text-on-light); }
.sm-field__hint--on-light{ color:var(--text-on-light-muted); }
.sm-field__error--on-light{ color:var(--sm-on-light-danger); }

.sm-input--on-light,
.sm-select--on-light,
.sm-textarea--on-light{
  color:var(--text-on-light-strong);
  background-color:var(--sm-on-light-field);
  border-color:var(--sm-on-light-field-border);
}
.sm-input--on-light::placeholder,
.sm-textarea--on-light::placeholder{ color:var(--text-on-light-muted); }
.sm-input--on-light:hover,
.sm-select--on-light:hover,
.sm-textarea--on-light:hover{ border-color:var(--sm-on-light-field-border-hover); }
.sm-input--on-light:focus-visible,
.sm-select--on-light:focus-visible,
.sm-textarea--on-light:focus-visible{
  border-color:var(--brand);
  box-shadow:var(--focus-ring);
}
/* `input-validation-error` is added by MVC's input tag helper when the field has
   a ModelState error, and again by jquery-validation-unobtrusive on the client,
   so one rule covers both round trips. A required checkbox gets it too
   (CreateUser's unticked terms box), and kept its grey boundary without this. */
.sm-input--on-light.input-validation-error,
.sm-select--on-light.input-validation-error,
.sm-textarea--on-light.input-validation-error,
.sm-checkbox--on-light.input-validation-error,
.sm-input--on-light.sm-input--invalid,
.sm-select--on-light.sm-select--invalid,
.sm-textarea--on-light.sm-textarea--invalid{ border-color:var(--danger-500); }

/* One-time codes: centred, tabular and without the number spinners, which are
   meaningless on a code and clip the last digit. */
.sm-input--code{
  font-family:var(--font-mono);
  font-size:var(--text-lg);
  letter-spacing:var(--tracking-caps);
  text-align:center;
}
.sm-input--code::-webkit-outer-spin-button,
.sm-input--code::-webkit-inner-spin-button{
  -webkit-appearance:none;
  margin:0;
}
.sm-input--code[type="number"]{
  -moz-appearance:textfield;
  appearance:textfield;
}

/* Six single-character boxes. The JS hook stays the shipped `.code-input`
   class; these two carry the styling only. The row never wraps: six 48px boxes
   and their gaps need 328px, more than the column on a 360px phone (320px), and
   the sixth box used to drop onto a row of its own. Each box starts at 48px and
   shrinks to share the one row instead: 47px at 360, 40px at 320. */
.sm-code-group{
  display:flex;
  flex-wrap:nowrap;
  gap:var(--space-2);
}
.sm-code-input{
  flex:0 1 var(--control-height-lg);
  width:auto;
  min-width:0;
  height:var(--control-height-lg);
  padding:0;
  font-family:var(--font-mono);
  font-size:var(--text-lg);
  text-align:center;
}

.sm-checkbox--on-light,
.sm-radio--on-light{
  background-color:var(--sm-on-light-field);
  border-color:var(--sm-on-light-field-border);
}
/* Section 4's unchecked hover is (0,3,0) and lightens; on white it has to darken instead. */
.sm-checkbox--on-light:hover:not(:checked, :indeterminate, :disabled),
.sm-radio--on-light:hover:not(:checked, :disabled){ border-color:var(--sm-on-light-field-border-hover); }

.sm-btn--on-light{
  background-color:var(--brand);
  color:var(--brand-on);
  border-color:transparent;
}
/* Hover darkens rather than lightens: --brand-hover is a step towards white,
   which loses contrast against the page instead of gaining it. No box-shadow
   here either — a shadow declared after `.sm-btn:focus-visible` would outrank
   the focus ring whenever a button is both focused and hovered. */
.sm-btn--on-light:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  background-color:var(--brand-active);
}
.sm-btn--on-light:active:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  background-color:var(--purple-800);
  transform:var(--sm-press-transform);
}

/* The light tonal role (9 Oct 2026): the paint marketing.css section 16 gives .sm-btn--secondary on
   white, so Login's "Create account" reads as the same role as the marketing pages' secondary
   actions on white (Read the Setup Guide, View All Posts, the pricing card's Book a Demo). A
   --purple-500 outline (5.51:1 on white; the old --ink-200 one was 1.54:1) on a --brand-a08 wash
   under a --brand-text-on-light label (8.92:1). Hover darkens each part one step: --brand
   (7.98:1), the --brand-subtle wash, --purple-800 (10.73:1), and restates `color`, because on
   Login's <a> Bootstrap's a:hover (0,1,1) used to repaint the label #0a58ca. The raw ramp steps
   mirror marketing.css section 1's light aliases, which this layout does not load: change both
   together, and section 11 with them. */
.sm-btn--outline-on-light{
  background-color:var(--brand-a08);
  color:var(--brand-text-on-light);
  border-color:var(--purple-500);
}
.sm-btn--outline-on-light:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  color:var(--purple-800);
  background-color:var(--brand-subtle);
  border-color:var(--brand);
}
.sm-btn--outline-on-light:active:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  transform:var(--sm-press-transform);
}

.sm-btn--link-on-light{
  height:auto;
  padding:0;
  background-color:transparent;
  color:var(--brand-text-on-light);
  border-color:transparent;
  font-size:inherit;
  font-weight:var(--weight-semibold);
}
/* Forced colours paint the transparent 1px border CanvasText, drawing a tight box that cuts the
   link-style label (F-entry-1); Canvas keeps the box's size and hides the line. */
@media (forced-colors: active){
  .sm-btn--link-on-light{ border-color:Canvas; }
}
.sm-btn--link-on-light:hover:not(:where([disabled],[aria-disabled="true"],.sm-is-disabled)){
  color:var(--purple-800);
  text-decoration:underline;
}

.sm-link--on-light{ color:var(--brand-text-on-light); }
.sm-link--on-light:hover{ color:var(--purple-800); }

/* The light line. The opacity:1 this rule used to restate against Bootstrap's
   `hr{opacity:.25}` now sits on .sm-divider itself (section 9), which both
   on-light dividers (Login, CreateUser) also carry. */
.sm-divider--on-light{
  background-color:var(--border-light);
}

/* --text-muted goes back to the light scale here: section 7 rebinds it to
   --text-secondary for the dark washes, which is about 2.3:1 on this fill. */
.sm-alert--on-light{
  --sm-alert-accent:var(--text-on-light);
  --text-muted:var(--text-on-light-muted);
  color:var(--text-on-light);
  background-color:var(--page-alt);
  border-color:var(--border-light);
}
.sm-alert--on-light .sm-alert__body{ color:var(--text-on-light-muted); }
/* Two-class selectors, and after the single-class block above, so the status
   colour survives whichever order the two modifiers are written in. */
.sm-alert--success.sm-alert--on-light{
  --sm-alert-accent:var(--sm-on-light-success);
  background-color:var(--success-subtle);
  border-color:var(--success-border);
}
.sm-alert--warning.sm-alert--on-light{
  --sm-alert-accent:var(--sm-on-light-warning);
  background-color:var(--warning-subtle);
  border-color:var(--warning-border);
}
.sm-alert--danger.sm-alert--on-light{
  --sm-alert-accent:var(--sm-on-light-danger);
  background-color:var(--danger-subtle);
  border-color:var(--danger-border);
}
.sm-alert--info.sm-alert--on-light{
  --sm-alert-accent:var(--sm-on-light-info);
  background-color:var(--info-subtle);
  border-color:var(--info-border);
}
/* Alert bodies in this flow are prose — Bootstrap's reboot gives every <p> a
   1rem bottom margin, which reads as a gap under the last line of an alert. */
.sm-alert p{ margin:0; }
.sm-alert p + p{ margin-top:var(--space-2); }
.sm-alert__title + p{ margin-top:var(--space-1); }

/* The validation summary tag helper renders its container on EVERY request —
   empty when the model is valid, and holding one `<li style="display:none">`
   when the only errors are property-level. Neither may paint a box, so the box
   is attached with :has() and keyed to a real error item, while the danger text
   colour is unconditional: a browser without :has() then still shows the
   messages in red exactly as the shipped `.text-danger` did, rather than
   hiding them. */
.sm-validation-summary{
  color:var(--sm-on-light-danger);
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
}
.sm-validation-summary ul{
  margin:0;
  padding:0;
  list-style:none;
}
.sm-validation-summary li + li{ margin-top:var(--space-1); }
.sm-validation-summary:has(li:not([style])){
  padding:var(--space-3) var(--space-4);
  background-color:var(--danger-subtle);
  border:var(--border-hairline) solid var(--danger-border);
  border-radius:var(--radius-md);
}
/* The same summary on the dark app shell (User/UpdatePassword, User/UpdateCard,
   Building/AddBuilding, and Signature/CreateSignature once it adopts the class):
   the on-light --danger-600 text is 2.81:1 on the danger wash there, --danger-text
   5.66:1. The entry flow's body is .sm-auth, not .sm-app, so it keeps the colour
   above. */
.sm-app .sm-validation-summary{ color:var(--danger-text); }

/* asp-validation-for spans: inline, per field, empty until something fails. */
.sm-field__error:empty{ display:none; }

/* ============================================================================
   11. Bootstrap `.btn` interop for the light entry-flow variants

   Section 10 gives the three light button variants their colours at (0,1,0),
   which is enough on a bare `.sm-btn`. It is NOT enough on the reCAPTCHA
   submit, and that button is how everyone signs in and signs up.

   Login.cshtml and CreateUser.cshtml ship
     <button class="btn btn-block btn-login g-recaptcha ...">
   and the constraint on this stage is that the element and its data-* attributes
   stay exactly as they are — so `btn` stays too, whatever sm- classes join it.

   Bootstrap 5.2.2 paints .btn's *states* from --bs-btn-* custom properties, and
   a bare `.btn` (no .btn-primary etc.) never defines the state ones. They resolve
   to invalid-at-computed-value-time, and `background-color` then falls back to
   `initial` — transparent. Verified against the shipped 5.2.2 stylesheet:

     .btn:hover                            (0,2,0)  bg: var(--bs-btn-hover-bg)
     .btn:focus-visible                    (0,2,0)  bg: var(--bs-btn-hover-bg)
     .btn:disabled                         (0,2,0)  bg: var(--bs-btn-disabled-bg)
     :not(.btn-check) + .btn:active        (0,3,0)  bg: var(--bs-btn-active-bg)
     .btn:first-child:active               (0,3,0)  bg: var(--bs-btn-active-bg)
     .btn:first-child:active:focus-visible (0,4,0)  box-shadow

   `.sm-btn--on-light` is (0,1,0), so .btn:disabled wins outright and a disabled
   submit paints transparent — CreateUser ships its submit disabled until the
   terms box is ticked, so that is the first thing a new user sees. The press and
   focus states fail the same way, and the (0,3,0)/(0,4,0) selectors are past
   what any sane class selector can outrank. The button is also the first child
   of its wrapper on Login, so :first-child:active genuinely matches.

   Defining the variables is the fix rather than a specificity war: Bootstrap's
   own rules then paint the section 10 colours. It is order-independent, it
   survives extra btn-* classes, and it costs nothing on buttons that do not
   carry `.btn`, since custom properties apply nothing by themselves.

   Values mirror section 10 exactly. If a variant's colour changes there, change
   it here too — these are a mirror, not a second source of truth.

   Deliberately scoped to the three light variants: the dark variants in
   section 1 have the same latent gap, but nothing in this stage renders one on
   a `.btn`, and fixing them belongs to the stage that does.
   ========================================================================= */
.sm-btn--on-light{
  --bs-btn-color:var(--brand-on);
  --bs-btn-bg:var(--brand);
  --bs-btn-border-color:transparent;
  --bs-btn-hover-color:var(--brand-on);
  --bs-btn-hover-bg:var(--brand-active);
  --bs-btn-hover-border-color:transparent;
  --bs-btn-active-color:var(--brand-on);
  --bs-btn-active-bg:var(--purple-800);
  --bs-btn-active-border-color:transparent;
  --bs-btn-disabled-color:var(--brand-on);
  --bs-btn-disabled-bg:var(--brand);
  --bs-btn-disabled-border-color:transparent;
  --bs-btn-disabled-opacity:.45;
  --bs-btn-focus-box-shadow:var(--focus-ring);
}
.sm-btn--outline-on-light{
  --bs-btn-color:var(--brand-text-on-light);
  --bs-btn-bg:var(--brand-a08);
  --bs-btn-border-color:var(--purple-500);
  --bs-btn-hover-color:var(--purple-800);
  --bs-btn-hover-bg:var(--brand-subtle);
  --bs-btn-hover-border-color:var(--brand);
  --bs-btn-active-color:var(--purple-800);
  --bs-btn-active-bg:var(--brand-subtle);
  --bs-btn-active-border-color:var(--brand);
  --bs-btn-disabled-color:var(--text-on-light-muted);
  --bs-btn-disabled-bg:transparent;
  --bs-btn-disabled-border-color:var(--border-light);
  --bs-btn-disabled-opacity:.45;
  --bs-btn-focus-box-shadow:var(--focus-ring);
}
.sm-btn--link-on-light{
  --bs-btn-color:var(--brand-text-on-light);
  --bs-btn-bg:transparent;
  --bs-btn-border-color:transparent;
  --bs-btn-hover-color:var(--purple-800);
  --bs-btn-hover-bg:transparent;
  --bs-btn-hover-border-color:transparent;
  --bs-btn-active-color:var(--purple-800);
  --bs-btn-active-bg:transparent;
  --bs-btn-active-border-color:transparent;
  --bs-btn-disabled-color:var(--text-on-light-muted);
  --bs-btn-disabled-bg:transparent;
  --bs-btn-disabled-border-color:transparent;
  --bs-btn-disabled-opacity:.45;
  --bs-btn-focus-box-shadow:var(--focus-ring);
}

/* Focus must not borrow the hover fill. Bootstrap's .btn:focus-visible paints
   --bs-btn-hover-bg, which the block above now resolves, so without this a
   merely-focused button would read as hovered. No box-shadow is declared here,
   so `.sm-btn:focus-visible` still supplies the ring — and section 10's reason
   for keeping shadows off these variants still holds. */
.sm-btn--on-light:focus-visible{
  background-color:var(--brand);
  color:var(--brand-on);
  border-color:transparent;
}
.sm-btn--outline-on-light:focus-visible{
  background-color:var(--brand-a08);
  color:var(--brand-text-on-light);
  border-color:var(--purple-500);
}
.sm-btn--link-on-light:focus-visible{
  background-color:transparent;
  color:var(--brand-text-on-light);
  border-radius:var(--radius-xs);
}

/* Bootstrap's .btn:disabled adds pointer-events:none, which suppresses both the
   not-allowed cursor and any title tooltip on the control. The `disabled`
   attribute already blocks activation, so restoring pointer events changes
   nothing a user can do — it only restores the affordance. */
.sm-btn--on-light:disabled,
.sm-btn--outline-on-light:disabled,
.sm-btn--link-on-light:disabled{
  pointer-events:auto;
  cursor:not-allowed;
}

/* ============================================================================
   12. Manager surfaces — page column, disclosure groups, media cards,
       detail lists, chart frames, the signature plate, and two Bootstrap
       variable mirrors

   Everything here came out of the eight Stage 3 views. Nothing was added that
   an existing component reaches: page titles use .sm-section-header, group
   counts use .sm-badge, "no data" blocks use .sm-empty, and the card chrome
   under .sm-media-card is still .sm-card — this section supplies only the
   internal grid. Phase 3 later added modifiers and states to 12.2-12.6 and 12.8,
   for the other roles' views; each names the views it came from.

   Scope: `.sm-app`, same as section 8 and for the same second reason. site.css
   is <link>ed AFTER this file, and it puts `.DarkThemeFormHeader`,
   `.DarkThemeTextColor`, `.signatureFont`, `.signatureContainer` and
   `.card-text` — all (0,1,0) — on the very elements these components land on.
   `.sm-app .sm-disclosure__title` is (0,2,0) and clears them outright, which is
   how these views restyle with site.css left alone.

   TWO DISPLAY TRAPS, both load-bearing for these views:

   1. Never declare `display` on .sm-disclosure__panel. Bootstrap's
      `.collapse:not(.show){display:none}` is (0,2,0), and Bootstrap is <link>ed
      BEFORE this file — so an equal-weight `display` here WINS and the group
      can never close again. Padding and margin on a display:none box are inert,
      so everything below is safe. The same reasoning bars `display` from
      .sm-media-card when it sits inside a collapse.
   2. Never declare `display` on .sm-disclosure__caret. All four views toggle the
      caret pair with jQuery `.toggle()`, which writes an inline `display`. On the
      way back jQuery restores the element's DEFAULT display for its tag, not the
      stylesheet's — so a caret styled `display:inline-flex` silently degrades to
      `display:inline` after one open/close cycle. Size and colour only.

   Motion uses the section 0 aliases, so prefers-reduced-motion already
   neutralises every transform below.
   ========================================================================= */

/* --- 12.1 Page column ----------------------------------------------------
   Replaces the eight hand-rolled `<section style="margin-top:100px">` and
   `.container.mt-5` openers with one content cap and one vertical rhythm.
   Safe on the same element as Bootstrap's `.container`: that is (0,1,0) and
   this is (0,2,0), so the cap and the gutters below are what apply.
   Deliberately NOT .sm-shell__content — section 8.2 reserves that for the
   stage that owns #page-content-wrapper.
   ------------------------------------------------------------------------ */
.sm-app .sm-page{
  width:100%;
  max-width:var(--content-max);
  margin-inline:auto;
  padding:var(--space-8) var(--space-6) var(--space-16);
}
.sm-app .sm-page--narrow{ max-width:var(--prose-max); }
/* In the narrow column a header's actions often wrap onto a row of their own, where the slot's
   auto start margin floated them to the right edge, ragged against the title above them
   (Building/ViewWorkRequest and Tenant/ViewWorkRequest at 1440, Admin/ViewEmailSubscriber, the
   two Tenant payment pages). Without it they start under the title once they wrap, and the
   header's space-between still puts them at the end while they share the title's row. (0,3,0)
   against the slot's (0,1,0). */
.sm-app .sm-page--narrow .sm-section-header__actions{ margin-inline-start:0; }

/* --- 12.2 Disclosure group ----------------------------------------------
   The single most repeated shape in this stage: a caret-toggled heading over a
   Bootstrap collapse. Contacts, ManagedBuildings, ManagerOverview use one
   level; Alerts nests a second (--sub) inside the first. It replaces both the
   `<h3 class="DarkThemeFormHeader">` and the 4px white `<hr>` that trailed it —
   the trigger's own bottom hairline is that rule.

   The trigger is the existing `<a data-bs-toggle="collapse" href="#…">`; the
   href/aria-controls pair and every caret id must survive untouched. Views
   converted in Phase 3 move it to a real <button> inside a heading; see
   .sm-disclosure__heading below.
   ------------------------------------------------------------------------ */
.sm-app .sm-disclosure{ margin-top:var(--space-8); }

.sm-app .sm-disclosure__trigger{
  display:flex;
  align-items:center;
  gap:var(--space-3);
  width:100%;
  padding:var(--space-3) var(--space-1);
  border-bottom:var(--border-hairline) solid var(--border);
  color:var(--text-strong);
  text-decoration:none;
  cursor:pointer;
  transition:
    border-color var(--sm-duration-fast) var(--ease-standard),
    color var(--sm-duration-fast) var(--ease-standard);
}
.sm-app .sm-disclosure__trigger:hover{
  color:var(--text-strong);
  text-decoration:none;
  border-bottom-color:var(--border-strong);
}
.sm-app .sm-disclosure__trigger:focus-visible{
  outline:var(--sm-focus-outline);
  border-radius:var(--radius-sm);
  box-shadow:var(--focus-ring);
}

/* The trigger as a real button inside a heading, the settled form for converted views:
     <h2 class="sm-disclosure__heading">
       <button type="button" class="sm-disclosure__trigger" data-bs-toggle="collapse"
               data-bs-target="#…" aria-expanded="…" aria-controls="…">
         …icon… <span class="sm-disclosure__title">…</span> …carets…
       </button>
     </h2>
   A native button answers Space as well as Enter (the <a role="button"> answered only Enter,
   and Space scrolled the page), and the title stays in the heading outline instead of vanishing
   inside role="button". The heading is only a wrapper: the type is set inside the button, so it
   hands on the font it inherits instead of Bootstrap's heading scale, and it zeroes its bottom
   and side margins only, leaving the top to a .sm-stack-* (reboot already zeroes an h2's), the
   rule 12.5 follows too. The button reset strips the platform chrome and restates what an <a>
   would inherit; the base trigger rule above still supplies display, width, padding, the bottom
   hairline, colour and cursor, and outranks the UA's button border on that side. Its :hover and
   :focus-visible rules (0,3,0) outrank this (0,2,1). No `display` on either element, so neither
   trap applies, and the existing <a> triggers match none of this. */
.sm-app .sm-disclosure__heading{
  margin-inline:0;
  margin-bottom:0;
  font:inherit;
  letter-spacing:inherit;
  color:inherit;
}
.sm-app button.sm-disclosure__trigger{
  -webkit-appearance:none;
  appearance:none;
  margin:0;
  font:inherit;
  letter-spacing:inherit;
  text-align:start;
  background:none;
  border-top:0;
  border-inline:0;
  border-radius:0;
}

.sm-app .sm-disclosure__icon{
  flex:0 0 auto;
  color:var(--brand-text);
  line-height:1;
}

/* A section heading, so 18px: the page title above it is the 24px --text-title (section 7), and at
   22px the two used to be identical. --sub steps down to 16px below so the nested level stays
   distinct from its parent. */
.sm-app .sm-disclosure__title{
  margin:0;
  min-width:0;
  font-family:var(--font-display);
  font-size:var(--text-lg);
  font-weight:var(--weight-bold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-tight);
  color:var(--text-strong);
}

/* Both carets carry this. Exactly one is visible at a time, so whichever it is
   takes the auto margin and lands on the trailing edge. See trap 2 above: no
   `display` here, ever. */
.sm-app .sm-disclosure__caret{
  flex:0 0 auto;
  margin-inline-start:auto;
  font-size:var(--text-base);
  color:var(--text-muted);
}

/* See trap 1: padding only. This is the element carrying `.collapse`. */
.sm-app .sm-disclosure__panel{
  padding-top:var(--space-5);
}

.sm-app .sm-disclosure--sub{
  margin-top:var(--space-6);
  padding-inline-start:var(--space-6);
}
.sm-app .sm-disclosure--sub .sm-disclosure__title{ font-size:var(--text-md); }
.sm-app .sm-disclosure--sub .sm-disclosure__trigger{
  border-bottom-color:var(--border-subtle);
}
/* The panel's own 20px top padding already sets its first group off the trigger; the sub-group's
   24px margin on top of it opened a 44px gap above the first sub-list (Manager/Alerts). */
.sm-app .sm-disclosure__panel > .sm-disclosure--sub:first-child{ margin-top:0; }

/* --- 12.3 Card link ------------------------------------------------------
   The stretched-link card (decision D11), on Owner/MyBuildings and ProformaModels,
   Manager/ManagedBuildings and Alerts, and Building/ViewBuildingNotifications. The card itself
   carries .sm-card--link, and its href-less `<a class="sm-card-link sm-card__link" role="button"
   tabindex="0">` sits inside the card's heading round the title text only; its onclick submits
   the card's own form. .sm-card-link is the hook the views' keydown handlers look up (the nearest
   .sm-card-link round the event target), so it never changes; ViewBuildingNotifications gives the
   link its own onkeydown instead. The heading stays a heading, and the control's name is the
   title rather than the whole card's 53-208 characters (V-owner-2). The link's two overlays cover
   the card, so a press anywhere on it still reaches the link and its onclick.
   ------------------------------------------------------------------------ */
/* Equal heights. The grid stretches its items, but in these views the item is the <form> round the
   card, so the card kept its content height and cards in one row ended at different heights
   (Owner/MyBuildings: a 256px form round a 233px card). height:100% hands the form's stretched
   height down to the card, so every card is its row's height. The form's hidden inputs take no
   space. */
.sm-app .sm-card-grid > form > .sm-card--link{
  height:100%;
}

/* The mechanics.
     - The card is the overlays' containing block (.sm-card is already relative; restated here so
       the modifier also works on another box) and isolates, so the overlays' z-index 1 orders them
       only inside the card and can never lift them over anything outside, a sticky header included.
     - The link takes the card's text colour and no underline: Bootstrap's reboot paints a classed
       href-less anchor as a blue underlined link.
     - Hover lifts the card by --sm-lift-transform. There is no pressed state. The lift also keys
       on the card's form, which does not move: a card that is its own hover target would drop out
       from under a pointer resting in its bottom 4px and flicker.
     - Two overlays, so every point of the card answers, lifted or not. ::after rides with the
       card. ::before holds the RESTING box: while the card is lifted it takes --sm-lift-undo
       (0.1), the lift's inverse, on the card's own transition, so the two move in step and
       ::before never leaves the resting box. It starts from translate(0) rather than none,
       because from none the reflections in the inverse would interpolate through scale(0).
       Without it the strip a lifted card uncovers at the foot of its resting box (the 4px lift
       plus the 1px border) belonged to the <form>: a press there did nothing, under the default
       arrow (critic-2 defect 6). Both overlays start one hairline outside the padding box and
       take the card's radius, so between them they cover the lifted and the resting border box
       exactly, rounded corners included, and nothing of the grid gap beyond.
     - The card no longer clips, or nothing could reach past its padding box: a media card's
       overflow:hidden (12.4) clipped every descendant to it, the overlays included. The clip
       moves down to the figure and the body (overflow:clip makes no scroll container, so their
       minimum sizes stay as they were), and the overlays pass both, since their containing
       block is the card. The figure rounds its own outer corners instead, to the curve the
       card's clip drew (the radius less the border): down its start side beside the body, and
       across its top where 12.4 stacks it, below 768px, below 992px for --stack-lg and in a
       --compact card narrower than 18rem. Measured pixel-identical to the clipped card, at rest
       and lifted, on all five views at 320-1440.
     - Focus: the link's own outline goes and the card draws the ring round itself, the product
       gap ring (2px --surface-canvas, then 2px --purple-300, 8.28:1 against the gap on one side
       and the page canvas on the other) plus --sm-focus-outline, a 2px CanvasText outline 2px
       off the card in forced colours, where box-shadow is dropped. The grid gap leaves it 24px.
     - Any other control inside such a card (a link with an href, a button, a field, a label or
       another tab stop) is raised to z-index 2 above the overlays so it keeps its own clicks.
       Hidden and visually-hidden fields are left as they are, since position would undo the
       latter. None of the five views has one today.
   No display anywhere here: nothing in these cards is shown or hidden by a script. */
.sm-app .sm-card--link{
  position:relative;
  isolation:isolate;
}
.sm-app .sm-card--link .sm-card__link,
.sm-app .sm-card--link .sm-card__link:hover{
  color:inherit;
  text-decoration:none;
  cursor:pointer;
}
.sm-app .sm-card--link .sm-card__link::before,
.sm-app .sm-card--link .sm-card__link::after{
  content:"";
  position:absolute;
  inset:calc(var(--border-hairline) * -1);
  z-index:1;
  border-radius:var(--radius-lg);
}
.sm-app .sm-card--link .sm-card__link::before{
  transform:translate(0);
  transition:transform var(--sm-duration-base) var(--ease-standard);
}
.sm-app .sm-card--link:hover,
.sm-app form:hover > .sm-card--link{
  transform:var(--sm-lift-transform);
  border-color:var(--border);
}
.sm-app .sm-card--link:hover .sm-card__link::before,
.sm-app form:hover > .sm-card--link .sm-card__link::before{
  transform:var(--sm-lift-undo);
}
.sm-app .sm-card--link.sm-media-card{
  --sm-card-inner-radius:calc(var(--radius-lg) - var(--border-hairline));
  overflow:visible;
}
.sm-app .sm-card--link.sm-media-card > :is(.sm-media-card__figure, .sm-media-card__body){
  overflow:clip;
}
.sm-app .sm-card--link.sm-media-card > .sm-media-card__figure{
  border-start-start-radius:var(--sm-card-inner-radius);
  border-end-start-radius:var(--sm-card-inner-radius);
}
@media (max-width: 767.98px){
  .sm-app .sm-card--link.sm-media-card:not(.sm-media-card--compact) > .sm-media-card__figure{
    border-start-end-radius:var(--sm-card-inner-radius);
    border-end-start-radius:0;
  }
}
@media (max-width: 991.98px){
  .sm-app .sm-card--link.sm-media-card--stack-lg > .sm-media-card__figure{
    border-start-end-radius:var(--sm-card-inner-radius);
    border-end-start-radius:0;
  }
}
@container (width < 18rem){
  .sm-app .sm-card--link.sm-media-card--compact > .sm-media-card__figure{
    border-start-end-radius:var(--sm-card-inner-radius);
    border-end-start-radius:0;
  }
}
.sm-app .sm-card--link .sm-card__link:focus-visible{
  outline:none;
}
.sm-app .sm-card--link:has(.sm-card__link:focus-visible){
  outline:var(--sm-focus-outline);
  outline-offset:var(--sm-focus-width);
  box-shadow:var(--focus-ring);
}
.sm-app .sm-card--link :where(a[href], button, input, select, textarea, label, summary, [tabindex]):not(.sm-card__link, [type="hidden"], .sm-visually-hidden, .visually-hidden){
  position:relative;
  z-index:2;
}

/* --- 12.4 Media card -----------------------------------------------------
   The internal split only: figure column, body column. Chrome stays .sm-card,
   which must also carry .sm-card--flush so this grid owns the padding.
   A link card (12.3) trades the overflow:hidden below for clips on the two
   columns and a figure that rounds its own outer corners, layout by layout,
   so a new figure layout here needs its corner pair there too.
   ------------------------------------------------------------------------ */
.sm-app .sm-media-card{
  /* 192px — the shipped cards ship a 240x220 raster and Alerts a wider one.
     Derived from the space scale rather than typed as a literal. */
  --sm-media-figure:calc(var(--space-32) + var(--space-16));
  display:grid;
  grid-template-columns:var(--sm-media-figure) minmax(0, 1fr);
  align-items:stretch;
  overflow:hidden;
}
.sm-app .sm-media-card--wide{
  --sm-media-figure:calc(var(--space-32) + var(--space-24));
}

/* CONTRAST — the 12.9 technique again. The figure is the one --surface-raised panel in the card,
   and --text-muted is 4.38:1 there, so .sm-media-card__caption (12px) failed; axe flags it on
   Owner/MyBuildings. Rebinding the token on the figure puts the caption, and anything else muted
   in it, at 7.64:1. Deliberately not on the whole card: the body sits on --surface, where muted
   is 4.59:1 and passes, and its detail-list terms keep their step below the values. */
.sm-app .sm-media-card__figure{
  --text-muted:var(--text-secondary);
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:var(--space-2);
  min-width:0;
  padding:var(--space-4);
  background-color:var(--surface-raised);
  border-inline-end:var(--border-hairline) solid var(--border-subtle);
}
/* Placeholder rasters (NoBuilding.png, NewBuilding1.png) bake light text and strokes onto a
   transparent ground; forced colours turn the figure white and the caption disappears (F-bldg-a-8).
   Like the rail logo (W4-units-5), the image keeps a dark plate. Opaque photos are unaffected. */
@media (forced-colors: active){
  .sm-app .sm-media-card__figure > img.sm-media-card__img{ forced-color-adjust:none; background-color:var(--surface-canvas); }
}
.sm-app .sm-media-card__img{
  display:block;
  width:100%;
  height:auto;
  border-radius:var(--radius-md);
  object-fit:cover;
}
.sm-app .sm-media-card__caption{
  margin:0;
  font-size:var(--text-xs);
  line-height:var(--leading-snug);
  text-align:center;
  color:var(--text-muted);
}

.sm-app .sm-media-card__body{
  display:flex;
  flex-direction:column;
  gap:var(--space-3);
  min-width:0;
  padding:var(--pad-card);
}
.sm-app .sm-media-card__title{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  flex-wrap:wrap;
  margin:0;
  font-family:var(--font-display);
  font-size:var(--text-lg);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-tight);
  letter-spacing:var(--tracking-tight);
  color:var(--text-strong);
}

/* Below Bootstrap's md the two columns stop fitting side by side. Matches the
   breakpoint the views' own col-md-* already switch on. */
@media (max-width: 767.98px){
  .sm-app .sm-media-card{ grid-template-columns:minmax(0, 1fr); }
  .sm-app .sm-media-card__figure{
    border-inline-end:0;
    border-block-end:var(--border-hairline) solid var(--border-subtle);
  }
  .sm-app .sm-media-card__img{ max-width:var(--space-32); }
}

/* --compact: a 96px figure holding a 64px image, beside the body at every width, for dense lists
   whose figure is a small illustration rather than a photo. Manager/Alerts ran 18 cards at 225px,
   each repeating a 224px raster, into a 4,932px page at 1440 and 6,816px at 375; compact, they
   measured 2,127px and 4,326px with no value clipped. Below 768 it keeps the two columns the rule
   above stacks, so it restates them after it. Its grid minimum is section 2's 22rem; written after
   --wide, it also wins when a card carries both. */
.sm-app .sm-media-card--compact{ --sm-media-figure:var(--space-24); }
.sm-app .sm-media-card--compact .sm-media-card__figure{ padding:var(--space-3); }
.sm-app .sm-media-card--compact .sm-media-card__img{ max-width:var(--space-16); }
@media (max-width: 767.98px){
  .sm-app .sm-media-card--compact{ grid-template-columns:var(--sm-media-figure) minmax(0, 1fr); }
  .sm-app .sm-media-card--compact .sm-media-card__figure{
    border-block-end:0;
    border-inline-end:var(--border-hairline) solid var(--border-subtle);
  }
}

/* --stack-lg: a full-width media card that stacks below 992px rather than 768. Beside the open
   rail, 768-991px of viewport leaves less content width than 767 without it, and User/ViewUser's
   profile card kept a 286px body there, with 107px inputs and its 2FA label on three lines.
   Viewport-keyed like the rule above, which is sound here only because the card is full width, so
   its width is the viewport less the rail; inside an auto-fit grid it would ask the wrong question
   (section 2's media-grid note). */
@media (max-width: 991.98px){
  .sm-app .sm-media-card--stack-lg{ grid-template-columns:minmax(0, 1fr); }
  .sm-app .sm-media-card--stack-lg > .sm-media-card__figure{
    border-inline-end:0;
    border-block-end:var(--border-hairline) solid var(--border-subtle);
  }
  .sm-app .sm-media-card--stack-lg .sm-media-card__img{ max-width:var(--space-32); }
}

/* A --compact card narrower than 18rem stacks its figure above the body. On Manager/Alerts at a
   320px viewport each card was 248px: a 150px body, a 102px detail list, and the card's
   overflow:hidden clipped both "Submitted" dates and a 76-character name. The card is its own
   inline-size container, so the question is asked of the CARD's width (section 2's media-grid
   note), and both children span the two columns, since a container query cannot restyle the
   container's own template. Under 288px, because at 288 (a 360px viewport) the list still fits
   a date beside its term, measured clean. Containment is safe here: the card's width comes from
   its track or column, never from its content, and it holds nothing fixed-position. */
.sm-app .sm-media-card--compact{ container-type:inline-size; }
@container (width < 18rem){
  .sm-app .sm-media-card--compact > :is(.sm-media-card__figure, .sm-media-card__body){ grid-column:1 / -1; }
  .sm-app .sm-media-card--compact > .sm-media-card__figure{
    border-inline-end:0;
    border-block-end:var(--border-hairline) solid var(--border-subtle);
  }
}

/* --- 12.4a Photo frame and cover card ---------------------------------------
   Building/ViewBuildingDetails' building photo (pass 2, 8 Oct 2026). Insert after 12.4's last
   rule (the --compact container query) and before 12.5, so .sm-photo-frame's background is
   written after .sm-media-card__figure's should a figure ever carry both.

   .sm-photo-frame is a fixed frame: the <img> is absolutely positioned inside it and cropped with
   object-fit:cover, so the frame's size never depends on the photo. The window.onload swap from
   the placeholder to the S3 photo (ViewBuildingDetails' localStorage script) therefore cannot move
   the page, whatever the photo's aspect ratio.

   Placeholder: while #output still holds the shipped placeholder src (NoBuilding.png, a 250px
   raster with "No Image Available" baked in as 9-11px white text), the raster is hidden and the
   frame shows an .sm-empty--compact medallion with the same words as live text, on a tinted
   --surface-raised plate. The state is read from the src attribute, which the script's
   `image.src = …` keeps in step, so no script changes. It keys on the placeholder's file name,
   not on `data:`, so a later move to a photo URL keeps working. --expected (Razor, when
   Model.BuildingHasImg) hides the words while a photo is on its way, so a building that has a
   photo never flashes "No Image Available" before window.onload.

   The card clips its own corners (overflow:clip, the 14px card radius). Unlike 12.3's link cards
   nothing here has to reach past the card, so the whole card may clip.

   No `display` on anything a script shows or hides; visibility only. */
.sm-app .sm-photo-frame{
  position:relative;
  overflow:clip;
  background:
    radial-gradient(120% 100% at 50% 0%, var(--brand-a12), transparent 70%),
    var(--surface-raised);
}
.sm-app .sm-photo-frame__img{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit:cover;
}
.sm-app .sm-photo-frame__img[src*="NoBuilding.png"]{ visibility:hidden; }
.sm-app .sm-photo-frame > .sm-empty{
  position:absolute;
  inset:0;
}
.sm-app .sm-photo-frame:not(:has(> .sm-photo-frame__img[src*="NoBuilding.png"])) > .sm-empty{ visibility:hidden; }
.sm-app .sm-photo-frame--expected > .sm-empty .sm-empty__body{ visibility:hidden; }

/* The cover card: the photo frame edge to edge on top, the identity line as a subtitle row under
   it. Stacked (one track) the frame is 16:9, capped at 20rem so a 721px track at 1024 is not
   405px tall. Beside the Quick Cash Flow card the row height is that card's (284px at 1280-1440)
   and the frame takes what the caption leaves, instead of imposing a 16:9 height of its own
   (313px at 1440, which would have stretched the summary card 80px past its content). */
.sm-app .sm-cover-card{
  display:flex;
  flex-direction:column;
  overflow:clip;
}
.sm-app .sm-cover-card__media{
  flex:1 1 auto;
  aspect-ratio:16 / 9;
  max-height:20rem;
  min-height:0;
}
.sm-app .sm-cover-card__caption{
  margin:0;
  padding:var(--space-4) var(--pad-card);
  border-top:var(--border-hairline) solid var(--border-subtle);
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  color:var(--text-secondary);
}
/* The grid is the query container (section 2's media-grid note: ask the container, not the
   viewport). --records opens its second track at 2 x 28rem + --gap-card. Only when the summary
   card shares the row does the frame give up its ratio; a manager without Quick Cash Flow read
   keeps the 16:9 card in its single track. Containment is safe: the grid's width comes from the
   page column, never from its content, and it holds nothing fixed-position. */
.sm-app .sm-card-grid:has(> .sm-cover-card){ container-type:inline-size; }
@container (min-width: calc(56rem + 24px)){
  .sm-app .sm-card-grid:has(> .sm-cover-card + #qcfSection) > .sm-cover-card > .sm-cover-card__media{
    aspect-ratio:auto;
    max-height:none;
    min-height:var(--space-32);
  }
}

/* --- 12.5 Detail list ----------------------------------------------------
   The `Label: value` runs inside every ManagedBuildings and Alerts card, and
   inside the ManagedBuildings owner-contact modal. A <dl>: <dt> takes __term,
   <dd> takes __value. Values are tabular because most of them are money,
   percentages or dates.

   The list zeroes its side and bottom margins only. A whole `margin:0` at (0,2,0)
   also beat `.sm-stack-N > * + *` at (0,1,0), so a list after a title in a stack
   sat flush against it: Admin/ViewErrorLog's Inner Exception list 0px under the
   trace, every card title on Tenant/ViewUnit. Bootstrap's reboot already gives a
   <dl> margin-top:0, so a list outside a stack does not move.
   ------------------------------------------------------------------------ */
.sm-app .sm-detail-list{
  display:grid;
  grid-template-columns:auto minmax(0, 1fr);
  gap:var(--space-1) var(--space-3);
  margin-inline:0;
  margin-bottom:0;
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
}
.sm-app .sm-detail-list__term{
  margin:0;
  font-weight:var(--weight-medium);
  color:var(--text-muted);
}
.sm-app .sm-detail-list__value{
  margin:0;
  min-width:0;
  color:var(--text);
  font-variant-numeric:var(--numeric-tabular);
  font-feature-settings:"tnum" 1;
}
/* An e-mail link in a value, written glyph first: `<a class="sm-link" href="mailto:…"><i
   class="fa-solid fa-envelope" aria-hidden="true"></i> address</a>` in a .sm-break <dd> (Building/
   ViewBuildingContact, ViewBuildingManager, ViewBuildingTenantInfo, ViewTenant, ViewUnitApplication,
   Tenant/ViewUnit). The only ordinary break opportunity in it is the space after the glyph, and
   .sm-break's overflow-wrap only breaks inside the address once that has failed, so an address
   longer than the value column left the envelope alone on the first line: envelope / 32
   characters / "t" at 375 (W4-ops-b-3). line-break:anywhere makes every character a normal
   opportunity, so each line is filled before it breaks and the glyph stays with the start of the
   address ("envelope qa.opscontact01.0930@example.t" / "est" in the same 218px). An address has no
   spaces to prefer, so nothing that fits changes, and a value that is already .sm-break was already
   allowed to break inside it. mailto: links in .sm-break values only: a phone number keeps its
   break at the space, and a value that never opted into breaking is left alone. (0,5,1). No
   `display`. */
.sm-app .sm-detail-list__value.sm-break > a.sm-link[href^="mailto:"]{
  line-break:anywhere;
}

/* --stack-sm: term above value below 576px. The auto term column is as wide as the longest term
   ("My Customer Stripe Account", about 180px at 375), which left about 100px for an e-mail or a
   Stripe id and broke them mid-token (Admin/ViewAdminCustomer, ViewBuilding, ViewErrorLog). */
@media (max-width: 575.98px){
  .sm-app .sm-detail-list--stack-sm{
    grid-template-columns:minmax(0, 1fr);
    row-gap:0;
  }
  .sm-app .sm-detail-list--stack-sm > .sm-detail-list__value + .sm-detail-list__term{
    margin-top:var(--space-2);
  }
}

/* --total: a breakdown whose last pair is its sum (Tenant/ViewPayRent's invoice breakdown and both
   payment confirmations), where the total read as one more line item. The last <dt> and <dd> take
   a hairline above them and the strong colour and weight. The column gap becomes padding on the
   values, so the hairline runs unbroken across both columns. */
.sm-app .sm-detail-list--total{ column-gap:0; }
.sm-app .sm-detail-list--total > .sm-detail-list__value{ padding-inline-start:var(--space-3); }
.sm-app .sm-detail-list--total > .sm-detail-list__term:last-of-type,
.sm-app .sm-detail-list--total > .sm-detail-list__value:last-of-type{
  margin-top:var(--space-1);
  padding-top:var(--space-2);
  font-weight:var(--weight-semibold);
  color:var(--text-strong);
  border-top:var(--border-hairline) solid var(--border-subtle);
}

/* --amounts: a list of money whose every amount must stay whole (Tenant/ConfirmOneTimePayment,
   ConfirmAutoPayments and ViewPayRent's invoice breakdown, all --total). On the base track a long term
   takes the whole width when it fits and the values get what is left, so a fee label ("ACH Processing
   Fee( 1%, capped at $5.00)") or a past-due line left the amounts 0-30px and ran them out of the card
   at 320-414px (QA of 14 Oct 2026; legal SW-02: the tenant must be able to read every amount
   authorized). Here the value column is never narrower than its widest word plus --total's padding,
   so an amount stays whole and a long term wraps beside it; where both fit, nothing moves. `1fr` alone
   would not do it: __value's min-width:0 is then its minimum. Opt-in rather than the base track: a
   long unbroken e-mail or Stripe id in the other lists would squeeze their terms and widen the grid
   instead. ConfirmAutoPayments still carries its own (0,3,0) copy in its head, with the same value.
   (0,2,0), after the base rule. No `display`. */
.sm-app .sm-detail-list--amounts{ grid-template-columns:auto minmax(min-content, 1fr); }

/* --- 12.6 Chart frame ----------------------------------------------------
   ManagerOverview renders five Chart.js canvases, every one of them with
   `responsive:true, maintainAspectRatio:false`. In that mode Chart.js sizes the
   canvas to its PARENT, so the parent must have a resolved height or the chart
   paints at zero and the page looks broken. __canvas is that parent, and it is
   the only reason this component exists — nothing in the vocabulary reserves a
   box of a given height.

   Heights are derived from the space scale (256px / 384px) rather than typed as
   literals. Do not put __canvas on the hidden fallback canvases: those carry
   inline `display:none`, which wins, and the box would just reserve dead space.
   ------------------------------------------------------------------------ */
.sm-app .sm-chart{
  display:flex;
  flex-direction:column;
  gap:var(--space-3);
  min-width:0;
}
.sm-app .sm-chart__title{
  margin:0;
  font-size:var(--text-xs);
  font-weight:var(--weight-semibold);
  letter-spacing:var(--tracking-caps);
  text-transform:uppercase;
  line-height:var(--leading-tight);
  color:var(--text-muted);
}
.sm-app .sm-chart__canvas{
  --sm-chart-height:calc(var(--space-32) * 2);
  position:relative;
  width:100%;
  height:var(--sm-chart-height);
  min-width:0;
}

/* Several views produce a chart's EMPTY state by hiding the <canvas> itself and revealing a sibling
   "No Current Data" block — e.g. ViewBuildingDetails does `$('#MonthlyExpense').hide()` alongside
   `$('#MonthlyExpenseNone').show()` for 15 charts. Hiding the canvas does not collapse this wrapper,
   so a 256px box was being reserved ABOVE every empty message. Matching on the hidden canvas is the
   actual condition, and tolerating both `display:none` and `display: none` covers however jQuery
   serialises it. */
.sm-app .sm-chart__canvas:has(> canvas[style*="display"][style*="none"]){
  display:none;
}
.sm-app .sm-chart__canvas--tall{ --sm-chart-height:calc(var(--space-32) * 3); }
.sm-app .sm-chart__canvas > canvas{
  display:block;
  width:100%;
  height:100%;
}

/* A chart card's empty state, centred. Chart cards sit in .sm-card-grid rows, stretched to the
   tallest chart in the row, and the empty block sat at the top of its card over a band of blank
   card (Owner/Overview's six chart hints in 330px cards; Manager/ManagerOverview's "No Current
   Data"). When the chart is the card's only child it takes the card's height, and an empty state
   inside it takes what the title leaves and centres its content there (.sm-empty is already a
   centred column; use .sm-empty--compact in a chart). A card that is not stretched has an auto
   height, so the percentage changes nothing, and a drawn chart's canvas box keeps its fixed
   height. No `display` is declared, so a script that shows and hides an empty block is safe. */
.sm-app .sm-card > .sm-chart:only-child{ height:100%; }
.sm-app .sm-chart > .sm-empty{ flex:1 1 auto; }
/* The same centring when the empty block sits in a script-toggled holder, the shape
   Building/ViewBuildingDetails keeps: <div id="…None" style="display:none"> round each
   .sm-empty, shown by jQuery .show(). The holder takes the height the title leaves and the block
   fills it. Still no `display`: jQuery owns the holder's, and flex only acts while it is shown. */
.sm-app .sm-chart > div:has(> .sm-empty){ flex:1 1 auto; }
.sm-app .sm-chart > div:has(> .sm-empty) > .sm-empty{ height:100%; }

/* --- 12.7 Empty-state illustration --------------------------------------
   An extension of section 7's .sm-empty, not a new block. Five of the eight
   views put a raster illustration where __medallion goes — CheckEmail.png,
   NewBuilding3.png, barchart.png — each with its own inline width/height pair.
   This is that slot, and it replaces the inline sizing.
   ------------------------------------------------------------------------ */
.sm-app .sm-empty__art{
  display:block;
  width:100%;
  max-width:calc(var(--space-32) + var(--space-24));
  height:auto;
  margin-inline:auto;
}
.sm-app .sm-empty__art--sm{ max-width:var(--space-32); }

/* --- 12.8 Signature plate ------------------------------------------------
   CreateSignature only, and nothing is within reach: the adopted signature has
   to read as ink on paper on an otherwise dark page, in the shipped
   --font-signature face. It is the one place in the product that deliberately
   inverts to a light surface outside the entry flow.

   Replaces site.css's `.signatureContainer` / `.signatureFont` (both (0,1,0),
   both after this file — hence the .sm-app scope). The plate must NOT carry
   those classes as well: .signatureFont hard-codes 30px and black.

   The mark is regular weight because the face is: --font-signature is WindSong
   Regular, the only file tokens/fonts.css registers, so the 600 this used to set
   (like the legacy .signatureFont) made the browser embolden it synthetically,
   32.5% more ink than the regular face that signed PDFs stamp.
   The plate is an inline-size container, and the mark scales with it: 10% of
   the plate's width, from 22px up to the 32px it was. At 375 a 14-character name
   ("Codex QA Owner") now fits one line at 23.5px, where it wrapped to two; 768
   and wider are unchanged. The plain 32px before the clamp is the fallback for a
   browser without container units. Containment is safe because the plate is a
   block in a form, a card or a card body in all three views that use it
   (Signature/CreateSignature, Tenant/ViewUnitLease, Tenant/ViewSupplementalDocument),
   so its width comes from the column and never from its content.
   ------------------------------------------------------------------------ */
.sm-app .sm-signature{
  container-type:inline-size;
  display:flex;
  align-items:center;
  justify-content:center;
  min-height:var(--space-24);
  padding:var(--space-6) var(--space-5);
  background-color:var(--page);
  border:var(--border-emphasis) solid var(--border-light-strong);
  border-radius:var(--radius-lg);
}
.sm-app .sm-signature__mark{
  margin:0;
  font-family:var(--font-signature);
  font-size:var(--text-metric);
  font-size:clamp(var(--text-xl), 10cqi, var(--text-metric));
  font-weight:var(--weight-regular);
  line-height:var(--leading-tight);
  text-align:center;
  color:var(--text-on-light-strong);
  overflow-wrap:anywhere;
}
.sm-app .sm-signature__caption{
  margin:0;
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  text-align:center;
  color:var(--text-muted);
}

/* --- 12.9 Bootstrap variable mirrors ------------------------------------
   The same technique section 11 uses on `.btn`, and for the same reason: these
   are live Bootstrap components whose behaviour is bound to their own classes
   (`data-bs-dismiss`, `data-bs-toggle="dropdown"`), so the classes stay and only
   the paint changes. Defining the variables beats a specificity war — it is
   order-independent, it survives extra bs classes, and it applies nothing on
   elements that do not carry the Bootstrap class.

   Geometry variables (widths, paddings, margins, z-index) are deliberately left
   alone; Bootstrap's own values are correct and the JS positions against them.

   .sm-modal-skin  -> ViewManagerRelationshipPanel #buildingActionModal,
                      ManagedBuildings #OwnerContact-*. Goes on the `.modal`
                      root, which is where Bootstrap declares --bs-modal-*.
   .sm-menu-skin   -> ManagedBuildings sort dropdown. Goes on `.dropdown-menu`,
                      and lets `dropdown-menu-dark` be dropped: that class ships
                      #343a40, which is not on this system's neutral ramp.
   .dropdown-menu  -> the same values, as the DEFAULT for every menu under
                      .sm-app, so a bare `.dropdown-menu` is no longer
                      Bootstrap's white card; .sm-menu-skin stays valid.
   .tooltip, .popover, .nav-tabs
                   -> the mirrors after the close-button block below, with the
                      --bs-font-sans-serif the first two read. The .toast
                      mirror sits with its component, in 18.5.

   Not needed: Bootstrap's `.list-group-item-dark.list-group-item-action:focus`
   (#bebebf behind #141619 text on Owner > Recent Building and My Buildings)
   no longer reaches the rail. The class is off the rail markup, and 8.3's
   (0,4,0) state rules outrank its (0,3,0) regardless.
   ------------------------------------------------------------------------ */
.sm-app .sm-modal-skin{
  --bs-modal-color:var(--text);
  --bs-modal-bg:var(--surface-overlay);
  /* CONTRAST. Everything inside a modal sits on --surface-overlay (#1D1D26), the lightest surface in
     the system, and --text-muted (#7C7C93) is only 4.11:1 there — below the 4.5:1 AA floor. Muted text
     is not rare in a dialog: .sm-field__hint, .sm-card__subtitle, .sm-empty__body, .sm-detail-list__term
     and .sm-muted all read it. Rebinding the TOKEN on the modal root fixes every one of them at once,
     because they all resolve var(--text-muted) at use time. --text-secondary is 7.16:1 here. */
  --text-muted:var(--text-secondary);
  --bs-modal-border-color:var(--border);
  --bs-modal-border-radius:var(--radius-xl);
  --bs-modal-inner-border-radius:var(--radius-lg);
  --bs-modal-box-shadow:var(--elev-dark-3);
  --bs-modal-header-border-color:var(--border-subtle);
  --bs-modal-footer-border-color:var(--border-subtle);
  --bs-modal-footer-bg:transparent;
}

.sm-app .dropdown-menu,
.sm-app .sm-menu-skin{
  --bs-dropdown-color:var(--text);
  --bs-dropdown-bg:var(--surface-overlay);
  --bs-dropdown-border-color:var(--border);
  --bs-dropdown-border-radius:var(--radius-md);
  --bs-dropdown-inner-border-radius:var(--radius-sm);
  --bs-dropdown-box-shadow:var(--elev-dark-3);
  --bs-dropdown-divider-bg:var(--border-subtle);
  --bs-dropdown-font-size:var(--text-base);
  --bs-dropdown-link-color:var(--text-secondary);
  --bs-dropdown-link-hover-color:var(--text-strong);
  --bs-dropdown-link-hover-bg:var(--surface-hover);
  --bs-dropdown-link-active-color:var(--text-strong);
  --bs-dropdown-link-active-bg:var(--surface-selected);
  --bs-dropdown-link-disabled-color:var(--text-muted);
  /* --text-secondary, not --text-muted: a header is text on --surface-overlay, where muted is
     4.11:1. The token is NOT rebound on the menu the way .sm-modal-skin rebinds it — links here
     are already --text-secondary, so a rebind would make disabled items (exempt from contrast)
     look exactly like live ones. */
  --bs-dropdown-header-color:var(--text-secondary);
}
/* Bootstrap 5.2.2 declares --bs-dropdown-box-shadow but paints it only in builds compiled with
   $enable-shadows, which the CDN build is not, so the --elev-dark-3 above was inert and a menu sat
   flat on whatever it covered. (0,2,0); a Buttons collection's own `box-shadow:none` (0,2,2)
   still wins inside its box. */
.sm-app .dropdown-menu{ box-shadow:var(--bs-dropdown-box-shadow); }
/* Bootstrap marks a focused item only with the hover fill, --surface-hover (white at 4%), which
   is not a visible focus indicator. Items are full-bleed in the menu, so the ring is inset. */
.sm-app .dropdown-menu .dropdown-item:focus-visible,
.sm-app .sm-menu-skin .dropdown-item:focus-visible{
  outline:var(--sm-focus-outline);
  outline-offset:var(--sm-focus-offset-inset);
  box-shadow:var(--sm-focus-ring-inset);
}

/* Bootstrap's close button, on every modal and dismissible notice in the app and on the entry
   flow's two TempData alerts. Its `.btn-close:focus` (0,2,0) paints a blue rgba(13,110,253,.25)
   ring on EVERY focus, mouse clicks included. A click now leaves no ring, and keyboard focus gets
   the token ring — whichever one the scope declares, so the entry flow's plain close buttons on
   the white panel get the light ring. */
.sm-app .btn-close:focus:not(:focus-visible),
.sm-auth .btn-close:focus:not(:focus-visible),
.sm-site .btn-close:focus:not(:focus-visible){ box-shadow:none; }
.sm-app .btn-close:focus-visible,
.sm-auth .btn-close:focus-visible,
.sm-site .btn-close:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
/* Every close button in the app is `.btn-close-white`, which whitens its black X with
   `filter:invert(1) grayscale(100%) brightness(200%)` — and a filter repaints the element's own
   box-shadow too, so the ring above comes out as a white band and a #B2B2B2 band (visible, 7.87:1
   on a modal, but not the brand ring). The public apply-flow notices (section 19.1) invert their
   close button the same way. Where relative colour syntax exists, the
   filter is cut to invert(1), which draws the X identically, and the ring is written PRE-INVERTED
   so the filter turns it back into exactly the dark ring of 0.2. Keep the two colours here in step
   with --sm-focus-ring-dark. Without the syntax the monochrome ring stands. */
@supports (color: rgb(from red calc(255 - r) calc(255 - g) calc(255 - b))){
  .sm-app .btn-close-white:focus-visible,
  body:has(.sm-public-dark) > .container > .alert .btn-close:focus-visible{
    filter:invert(1);
    box-shadow:
      0 0 0 var(--sm-focus-width) rgb(from var(--surface-canvas) calc(255 - r) calc(255 - g) calc(255 - b)),
      0 0 0 calc(var(--sm-focus-width) * 2) rgb(from var(--purple-300) calc(255 - r) calc(255 - g) calc(255 - b));
  }
}
/* The same filter inverts the forced-colours outline, so it is written as Canvas and comes back
   out as CanvasText. The whole shorthand, not just the colour: the public notices sit outside any
   scope class today, where only Bootstrap's `outline:0` would otherwise apply. */
@media (forced-colors: active){
  .sm-app .btn-close-white:focus-visible,
  body:has(.sm-public-dark) > .container > .alert .btn-close:focus-visible{
    outline:var(--sm-focus-width) solid Canvas;
  }
}
/* Forced colours with a LIGHT palette. The filter turns Bootstrap's black X white, which vanished
   on a light palette's Canvas until the button took focus; a dark palette keeps it. Forced colours
   set prefers-color-scheme from the palette, so a light palette drops the filter and the X draws
   in its own black (at Bootstrap's .5 opacity, 3.95:1 on white), and the focus outline, which no
   filter inverts any more, is written as CanvasText. The public notices get the same pair in
   19.1, after the invert rule there. */
@media (forced-colors: active) and (prefers-color-scheme: light){
  .sm-app .btn-close-white,
  .sm-app .btn-close-white:focus-visible{ filter:none; }
  .sm-app .btn-close-white:focus-visible{ outline-color:CanvasText; }
}

/* The remaining mirrors: Bootstrap chrome no view class reaches (Phase 0 audit 6.5), so the
   paint comes through Bootstrap's own variables, as above.

   Type first. .tooltip and .popover each set `font-family:var(--bs-font-sans-serif)`, Bootstrap's
   system-UI stack, on themselves, so a tooltip rendered in a different face from the page around
   it. Declared on .sm-app, the variable reaches both by inheritance: Bootstrap appends them to
   <body>, which is .sm-app in _UserLayout. (In the four public .sm-app islands the root is a
   <div>, so a popup appended to <body> falls outside it and keeps Bootstrap's look.) Nothing else
   moves: Bootstrap resolves --bs-body-font-family on :root, and 8.1 sets .sm-app's own family. */
.sm-app{
  --bs-font-sans-serif:var(--font-sans);
}

/* Tooltips: 70 triggers in 22 views. Bootstrap's is black at 90% opacity; this is the overlay
   surface every other popup here uses (menus, modals), fully opaque, with a hairline so it holds
   its edge over the canvas. --text on --surface-overlay is 15.2:1. The arrow needs no rule of its
   own: Bootstrap paints it with var(--bs-tooltip-bg) in all four placements, so it takes the box's
   fill, and it overlaps the box by 1px, which covers the hairline exactly where the two join. */
.sm-app .tooltip{
  --bs-tooltip-color:var(--text);
  --bs-tooltip-bg:var(--surface-overlay);
  --bs-tooltip-font-size:var(--text-sm);
  --bs-tooltip-border-radius:var(--radius-sm);
  --bs-tooltip-opacity:1;
}
.sm-app .tooltip-inner{
  border:var(--border-hairline) solid var(--border);
  box-shadow:var(--elev-dark-2);
}

/* Popovers: none in the product yet; this keeps the first one off Bootstrap's white card. The
   arrow follows without a rule, because Bootstrap derives --bs-popover-arrow-border from the
   border colour and fills with --bs-popover-bg. The header shares the body's surface and is parted
   by the hairline. --text-muted is rebound as .sm-modal-skin does, for the same 4.11:1. */
.sm-app .popover{
  --bs-popover-font-size:var(--text-sm);
  --bs-popover-bg:var(--surface-overlay);
  --bs-popover-border-color:var(--border);
  --bs-popover-border-radius:var(--radius-md);
  --bs-popover-inner-border-radius:calc(var(--radius-md) - var(--border-hairline));
  --bs-popover-header-color:var(--text-strong);
  --bs-popover-header-bg:var(--surface-overlay);
  --bs-popover-body-color:var(--text);
  --text-muted:var(--text-secondary);
}

/* .nav-tabs rules its list off in #dee2e6, a light line under the two tab rails in
   Building/ViewBuildingDetails, which keep `nav-tabs` beside .sm-tabs__list. The hover border is
   mirrored too, for a bare .nav-tabs: site.css paints a bare tab's resting and active borders
   directly, but not its hover. .sm-tabs__tab (15.1) outranks all of it. */
.sm-app .nav-tabs{
  --bs-nav-tabs-border-color:var(--border-subtle);
  --bs-nav-tabs-link-hover-border-color:var(--border-subtle) var(--border-subtle) var(--border);
}

/* ============================================================================
   13. Admin surfaces — page-opener meta, log blocks, the map frame and one
       wrapping utility

   Four small blocks, because the eight Stage 4 views turned out to be almost
   entirely already-covered shapes. Nothing below restates any of these:

     the 25 `.metric-box` tiles  -> .sm-card-grid > .sm-card > .sm-metric
     the 20 `DarkThemeFormHeader` + 4px-white-<hr> pairs
                                 -> .sm-section-header, or .sm-disclosure
                                    where the heading toggles a collapse
     the 9 DataTables            -> .sm-table-wrap / -scroll / -toolbar / .sm-table
     the ~50 `<p class="card-text">Label: value</p>` runs -> .sm-detail-list
     every `badge rounded-pill bg-*`                      -> .sm-badge
     the two Chart.js canvases   -> .sm-chart / .sm-chart__canvas
     #confirmActionModal         -> .sm-modal-skin
     `alert alert-@Model.DiscountCodeMessageType` -> .sm-alert; the controller
       only ever writes danger/warning/success/info, which are exactly the four
       .sm-alert modifier suffixes, so the interpolation survives verbatim.

   Scope: `.sm-app`, same as sections 8 and 12 and for the same second reason.
   site.css is <link>ed AFTER this file and puts `.DarkThemeTextColor`,
   `.DarkThemeFormHeader`, `.card-text` and `.card-subtitle` — all (0,1,0) — on
   the very elements these rules land on. Two classes clear one.

   THE MAP HEIGHT IS LOAD-BEARING. ViewAdminDashboard calls `L.map('map', …)`
   at parse time; Leaflet measures the container there and then, and a
   zero-height container paints nothing and never recovers without an
   invalidateSize() the view does not call. Today that height comes from the
   view-local `#map{height:500px}` at (1,0,0). Once that <style> block is
   deleted, 13.3's `.sm-app .sm-map{height:var(--sm-map-height)}` is the ONLY
   rule that sets it.
   ========================================================================= */

/* --- 13.1 Section-header page opener -------------------------------------
   An extension of section 7's .sm-section-header, not a new block — the same
   relationship 12.7's .sm-empty__art has to .sm-empty.

   Four views open with a title over two to four `<h6 class="card-subtitle">`
   meta lines (ViewErrorLog, ViewBuilding, ViewAdminCustomer and
   ViewAdminDashboard), and ViewAdminCustomer puts status badges inside the
   <h1> as well. .sm-section-header cannot carry that alone: it is a
   baseline-aligned flex row with justify-content:space-between, so a title and
   a __sub become two items that get pushed to opposite edges.

   __main is the column that fixes it, and it stays a flex ITEM of the header —
   so __actions still claims the trailing edge and the parent's
   align-items:baseline still aligns that button with the title's first line.
   The title row inside it is the existing .sm-row utility; a badge beside an
   <h1> needs no new rule.

   Meta labels are the existing .sm-caps utility. Only the pair wrapper is new,
   and it exists for one reason: without it `IP` and `72.21.0.4` are two flex
   items and wrap apart.
   ------------------------------------------------------------------------ */
.sm-app .sm-section-header__main{
  display:flex;
  flex-direction:column;
  gap:var(--space-2);
  min-width:0;
}
.sm-app .sm-section-header__meta{
  display:flex;
  flex-wrap:wrap;
  align-items:baseline;
  gap:var(--space-1) var(--space-5);
  margin:0;
  padding:0;
  list-style:none;
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  color:var(--text-secondary);
}
.sm-app .sm-section-header__meta-item{
  display:inline-flex;
  align-items:baseline;
  gap:var(--space-15);
  min-width:0;
  font-variant-numeric:var(--numeric-tabular);
  font-feature-settings:"tnum" 1;
}

/* --- 13.2 Log block ------------------------------------------------------
   ViewErrorLog prints a raw stack trace into a <pre>. Its view-local
   `.wrap-text` (white-space:pre-wrap; word-break:break-word) is the only thing
   stopping a 4KB single-line trace from blowing the page's horizontal scroll
   open, so that wrapping is behaviour rather than decoration and both
   declarations are reproduced verbatim below.

   Nothing in the vocabulary was within reach: .sm-mono is a text utility with
   no box, and a <pre> inherits neither the surface nor the margin reset it
   needs — Bootstrap's reboot ships `pre{margin-top:0;margin-bottom:1rem}` and
   an unstyled UA background.

   The restyling agent must also lift the <pre> OUT of the surrounding
   `<p class="card-text">`: <pre> is not phrasing content, so the parser closes
   the <p> in front of it and the trace escapes the card's text rhythm today.
   ------------------------------------------------------------------------ */
.sm-app .sm-log{
  display:block;
  margin:0;
  padding:var(--space-4);
  background-color:var(--surface-sunken);
  border:var(--border-hairline) solid var(--border-subtle);
  border-radius:var(--radius-md);
  font-family:var(--font-mono);
  font-size:var(--text-sm);
  line-height:var(--leading-relaxed);
  color:var(--text-secondary);
  white-space:pre-wrap;
  word-break:break-word;
  overflow-wrap:anywhere;
  tab-size:2;
}

/* Opt-in cap. A 200-frame trace otherwise buries the Resolve Issue button
   several screens below the fold. The scrollbar gets the same treatment as
   .sm-table-scroll's, and for the same reason: the platform default paints a
   light track, which reads as a white bar down a dark panel. */
.sm-app .sm-log--scroll{
  max-height:calc(var(--space-32) * 2);
  overflow-y:auto;
  scrollbar-width:thin;
  scrollbar-color:var(--border-strong) transparent;
}
.sm-app .sm-log--scroll::-webkit-scrollbar{width:var(--space-2)}
.sm-app .sm-log--scroll::-webkit-scrollbar-track{background:transparent}
.sm-app .sm-log--scroll::-webkit-scrollbar-thumb{
  background:var(--border-strong);
  border-radius:var(--radius-pill);
}
.sm-app .sm-log--scroll::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}

/* --- 13.3 Map frame ------------------------------------------------------
   ViewAdminDashboard's Leaflet choropleth of property locations. Two views
   outside this stage carry the identical shape (Views/Owner/Overview.cshtml and
   Views/Building/ViewAvailableUnit.cshtml), so this is written as the one map
   box for the product rather than as an Admin one-off.

   HEIGHT — the whole reason this component exists. `height:var(--sm-map-height)`
   resolves to calc(var(--space-32) * 4) = 512px, an absolute length that needs
   no ancestor to resolve against, so it holds identically inside .sm-page, a
   .sm-card__body or a Bootstrap .col. It must land on the SAME element that
   keeps `id="map"`: `L.map('map', …)` resolves its container by id, so the id
   stays and .sm-map joins it.

   Specificity: leaflet.css is <link>ed from the view, i.e. AFTER this file, and
   its `.leaflet-container` (0,1,0) ships `background:#ddd`. `.sm-app .sm-map`
   is (0,2,0) and clears it, which is also what keeps the pre-tile flash dark.
   Leaflet writes `position:relative` onto the container itself in _initLayout,
   so none is declared here.

   isolation — not overflow — is what contains Leaflet's z-index scale (panes
   200-700, controls 800-1000) underneath the sticky topbar's z-index:20 from
   8.4. overflow:hidden is here only to clip the tiles to the radius.
   ------------------------------------------------------------------------ */
.sm-app .sm-map{
  --sm-map-height:calc(var(--space-32) * 4);
  width:100%;
  height:var(--sm-map-height);
  isolation:isolate;
  overflow:hidden;
  background-color:var(--surface-sunken);
  border:var(--border-hairline) solid var(--border-subtle);
  border-radius:var(--radius-lg);
}
@media (max-width: 767.98px){
  .sm-app .sm-map{ --sm-map-height:calc(var(--space-32) * 2.5); }
}

/* The legend is injected at RUNTIME by the view's own script:
     const div = L.DomUtil.create('div', 'legend');
   Leaflet appends it into the control pane, which lives inside the map
   container — so the element is inside .sm-map and therefore inside .sm-app,
   and the scoped selector below matches it without a line of JS changing.
   That matters: the same call appears verbatim in Views/Owner/Overview.cshtml.

   The namespaced alias is there for whenever that string is renamed. Keeping
   the bare `.legend` half scoped under .sm-map is what stops this becoming a
   global land-grab on a six-letter class name — outside a map container the
   selector matches nothing.

   `float` is reproduced from the shipped rule because the injected markup is
   `<i style="background:…"></i> 0 &ndash; 5<br>`: the swatch floats beside its
   own text run and the <br>s do the rest. 0.9 matches the fillOpacity
   Admin/ViewAdminDashboard fills its shaded states with (it was .7, from the old
   polygons, and the swatches read greyer than the states they key); Owner/Overview's
   legend is a line of text with no swatch. There is no opacity token, and it is the
   only raw number in this section. */
.sm-app .sm-map .legend,
.sm-app .sm-map__legend{
  padding:var(--space-3);
  background-color:var(--surface-overlay);
  border:var(--border-hairline) solid var(--border);
  border-radius:var(--radius-sm);
  box-shadow:var(--elev-dark-3);
  font-family:var(--font-sans);
  font-size:var(--text-xs);
  line-height:var(--space-5);
  color:var(--text-secondary);
  font-variant-numeric:var(--numeric-tabular);
  font-feature-settings:"tnum" 1;
}
.sm-app .sm-map .legend i,
.sm-app .sm-map__legend i{
  float:left;
  width:var(--space-4);
  height:var(--space-4);
  margin-right:var(--space-2);
  border-radius:var(--radius-xs);
  opacity:.9;
}
/* Below 576px the frame is width-bound: fitBounds fills its width with the lower 48 and leaves a
   band of about 75px above and below it (at 375), and the five-row legend, about 136px with
   Leaflet's corner margin, covered the south-east (Admin/ViewAdminDashboard: Florida 64% hidden,
   South Carolina 70%). There the frame grows to 384px, which the view's fitBounds turns into
   bands of about 107px, and the swatch legend lies down into a strip of one or two lines along
   the bottom edge, inside that band (about 68px at 375). The swatches leave the float and the
   <br>s go: both are written once into the legend by the script, never shown or hidden.
   Owner/Overview's one-line text legend has neither, so it only gains the height. The public
   unit map sets its own height later (19.2) and keeps it.
   ENTRIES NEVER SPLIT. The script writes each entry as `<i style="background:…"></i> 13 &ndash; 17`
   with no element round it, so the strip broke wherever the line ran out: "13 –" / "17" at 320, and
   a swatch at the end of one line with its "+17" at the start of the next at 375 (W4-app-fixes-3).
   The legend is nowrap, which removes every break inside a label and between a swatch and its
   label, and each swatch brings its own break opportunity in front of it: the <i> becomes an
   inline box (still no `display` on anything a script shows or hides) holding a space (::before)
   and the swatch itself (::after, an inline-block that inherits the colour the script writes on
   the <i>). Only the ::before is white-space:normal. The <i> and its swatch inherit the legend's
   nowrap: when the <i> itself was normal, Chrome let the line break after the atomic swatch too,
   and the swatch ended one line with its label opening the next (F-admin-1, 320 and 375). A
   U+0020 rather than a zero-width space, because the box that holds a space decides whether the
   line may break after it (CSS Text 3, 5.1), so only the ::before offers a break, and it falls
   just before the swatch, between one entry and the next; an entry's leading margin stays on
   the line it ends. The <i> takes font-size:0, so the space is
   0px wide and its own inline box paints no square behind the rounded swatch, and line-height:0
   so it adds nothing to the 20px line; vertical-align on both keeps the swatch centred on the
   label's x-height, where the inline-block swatch sat before. Only a legend that holds swatches
   goes nowrap (Owner/Overview's sentence still wraps). Five entries measure 301px on one line
   (Archivo 12px tabular), so the strip stays one line where it fits (414 and up) and wraps whole
   entries where it does not: at 375 four entries and "+17" alone on line 2, at 320 three and two. */
@media (max-width:575.98px){
  .sm-app .sm-map{ --sm-map-height:calc(var(--space-32) * 3); }
  .sm-app .sm-map .legend,
  .sm-app .sm-map__legend{ padding:var(--space-2) var(--space-3); }
  .sm-app .sm-map .legend:has(> i),
  .sm-app .sm-map__legend:has(> i){ white-space:nowrap; }
  .sm-app .sm-map .legend br,
  .sm-app .sm-map__legend br{ display:none; }
  .sm-app .sm-map .legend i,
  .sm-app .sm-map__legend i{
    float:none;
    display:inline;
    vertical-align:middle;
    margin-right:var(--space-1);
    font-size:0;
    line-height:0;
  }
  .sm-app .sm-map .legend i::before,
  .sm-app .sm-map__legend i::before{
    content:" ";
    white-space:normal;
  }
  .sm-app .sm-map .legend i::after,
  .sm-app .sm-map__legend i::after{
    content:"";
    display:inline-block;
    width:var(--space-4);
    height:var(--space-4);
    vertical-align:middle;
    border-radius:var(--radius-xs);
    background:inherit;
  }
  .sm-app .sm-map .legend i:not(:first-child),
  .sm-app .sm-map__legend i:not(:first-child){ margin-left:var(--space-3); }
}

/* --- 13.4 Break utility --------------------------------------------------
   Stripe account, customer and payment-intent identifiers, ID-verification
   session ids and raw holding-company ids are rendered as values on
   ViewAdminCustomer, ViewBuilding and ViewInvoice. They are unbroken 20-30
   character strings, and every box they land in is narrower than that: a
   .sm-detail-list__value in its minmax(0, 1fr) track, a .sm-metric__value, a
   table cell. min-width:0 lets those boxes shrink; it does not make the string
   break, so without this the text simply runs out of the card.

   Box-neutral and alignment-neutral, so it composes with .sm-mono exactly the
   way .sm-num does. word-break:break-word is kept alongside overflow-wrap for
   the same reason 13.2 keeps it — it is the declaration the shipped `.wrap-text`
   used, and the two are not quite synonyms in older WebKit.
   ------------------------------------------------------------------------ */
.sm-app .sm-break{
  overflow-wrap:anywhere;
  word-break:break-word;
}

/* ============================================================================
   14. Owner surfaces — the onboarding progress rail and the blocking-modal blur

   Three small blocks for eight views, and the ratio is deliberate: the Owner
   area is almost entirely shapes Stages 3 and 4 already built for it. Nothing
   below restates any of these:

     the 7 `.metric-box` tiles   -> .sm-card-grid > .sm-card > .sm-metric, the
                                    same mapping 13's opener records for Admin —
                                    Owner/Overview carries the THIRD and last
                                    byte-identical copy of that view-local
                                    <style> block, and every rule in it is
                                    already covered (see 14.0 below)
     the 8 Chart.js canvases     -> .sm-chart / .sm-chart__canvas (12.6)
     the Leaflet choropleth      -> .sm-map (13.3), which was written for this
                                    view by name
     the 6 modals                -> .sm-modal-skin (12.9)
     the 5 DataTables            -> .sm-table-wrap / -scroll / .sm-table
     the 5 caret-toggled headings-> .sm-disclosure (12.2)
     the 2 building-card grids   -> .sm-card--flush .sm-media-card + .sm-card--link,
                                    the stretched link in its heading (D11), over a
                                    .sm-detail-list (12.3 / 12.4 / 12.5)
     the sort dropdown           -> .sm-menu-skin (12.9)
     the 4 "No Buildings Found" /
       "No Current Data" blocks  -> .sm-empty + .sm-empty__art (7 / 12.7)
     `badge rounded-pill bg-*`   -> .sm-badge

   Scope: `.sm-app`, same as sections 8, 12 and 13 and for the same second
   reason. site.css is <link>ed AFTER this file and puts `.DarkThemeTextColor`
   and `.card-text` — both (0,1,0) — on the elements these rules land on.

   14.0 — WHAT THE THREE VIEW-LOCAL <style> BLOCKS LEAVE BEHIND.

   Owner/Overview's block is byte-identical to the two Admin copies section 13
   retired, so it is already fully covered and can be deleted outright:
   `.metric-box`/`.metric-title`/`.metric-number` -> .sm-card + .sm-metric__label
   + .sm-metric__value; `#map{height:500px;width:100%}` -> .sm-map (13.3), which
   becomes the ONLY rule giving Leaflet a height; `.legend` / `.legend i` ->
   .sm-app .sm-map .legend / … i (13.3), matched at runtime without a line of JS
   changing; the @media(max-width:767px) margin patch on `.row-cols-md-3/-4` ->
   dropped, .sm-card-grid's gap supplies it in both axes with no breakpoint.

   OwnerOnboarding and EndOfOnboarding share a second byte-identical block, and
   only two of its six rules survive as anything: `body{background:#121212}` and
   `.dark-theme-text` are section 8.1's job, `.dark-card` is .sm-card, and
   `.progress-bar{background-color:#6200EE}` is dead on arrival — every instance
   overrides it with an inline `background-color:#6d30a7`. `.progress-steps` and
   `.progress-steps div` are 14.2.

   ProformaModels' block contributes exactly one rule. `.custom-modal-backdrop`
   is dead (the class appears in no markup, and its second declaration,
   `Semi-transparent: background`, is not valid CSS); its unscoped
   `.modal-content{background:transparent;color:white;border:none}` is a global
   leak that both modals already defeat with inline colours, and .sm-modal-skin
   replaces it. `.blur-container` is 14.3.
   ========================================================================= */

/* --- 14.1 Progress track skin --------------------------------------------
   A Bootstrap variable mirror, exactly the technique 12.9 uses on `.modal` and
   `.dropdown-menu`. Bootstrap 5.2.2 declares --bs-progress-* on `.progress`
   itself and the bar reads four of them by inheritance, so one class on the
   track repaints both halves; `progress-bar-striped` / `progress-bar-animated`
   keep working untouched, which matters because the striped gradient sizes
   itself off --bs-progress-height. That is also why `.progress` must STAY on
   the track: drop it and the stripe background-size loses its variable and
   resolves to one full-width band.

   Both onboarding views hard-code the two colours inline — `background-color:
   rgba(69,69,69,1)` on the track, `background-color:#6d30a7` on the bar. Inline
   beats any stylesheet, so those two declarations have to come off the elements
   for this to apply; the `width:N%` on the bar is state and stays.

   Nine views outside this stage carry the same `.progress` + `.progress-bar`
   pair, so this is written as the one progress rail for the product rather than
   as an Owner one-off — the same call 13.3 made for the map frame.
   ------------------------------------------------------------------------ */
/* CONTRAST — the filled/unfilled boundary is the state information, and WCAG 2.1 SC 1.4.11
   wants 3:1 for it. --brand is --purple-600 (#6D30A7), a DARK purple: against this track it
   is only 2.23:1, and lightening the track makes it worse, not better (against --ink-600 it
   falls to 1.25:1). The fix has to be a lighter BAR, so the fill is --purple-400 (#A26FD9)
   — 4.93:1 on the track and 5.16:1 on the card behind it. The track itself is near-invisible
   against --surface (1.05:1), so a hairline ring gives the component a findable extent
   rather than relying on the fill alone. */
.sm-app .sm-progress-skin{
  --bs-progress-height:var(--space-3);
  --bs-progress-font-size:var(--text-xs);
  --bs-progress-bg:var(--surface-raised);
  --bs-progress-border-radius:var(--radius-pill);
  --bs-progress-box-shadow:none;
  --bs-progress-bar-color:var(--brand-on);
  --bs-progress-bar-bg:var(--purple-400);
  --bs-progress-bar-transition:width var(--duration-slow) var(--ease-standard);
  border:var(--border-hairline) solid var(--border-strong);
}
/* Forced colours repaint the fill as Canvas, so the bar would read empty at every
   step; Highlight keeps the progress visible (the step labels carry it in text too). */
@media (forced-colors: active){
  .sm-app .sm-progress-skin > .progress-bar{ background-color:Highlight; }
}

/* --- 14.2 Step rail ------------------------------------------------------
   The four labelled steps under the onboarding progress bar, identical in
   OwnerOnboarding and EndOfOnboarding. Nothing in the vocabulary was within
   reach: .sm-row is a wrapping flex row with no equal-width distribution and no
   centring, .sm-grid declares only `display` and `gap`, and .sm-card-grid's
   240px track is far wider than a three-word step label needs.

   auto-fit over a 96px minimum is what replaces the shipped
   `justify-content:space-between` + `flex:1`: four steps sit on one row at any
   realistic page width and fold to two, then one, on a narrow screen — with no
   media query, and without the shipped block's `margin-top:-10px`, which was
   there only to claw back a Bootstrap row's gutter.

   Element-agnostic on purpose: the shipped markup is bare <div>s, but a <ul>/<li>
   is the honest structure, so the list reset is declared here rather than left
   to the view.
   ------------------------------------------------------------------------ */
.sm-app .sm-steps{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(var(--space-24), 1fr));
  gap:var(--space-3);
  margin:0;
  padding:0;
  list-style:none;
}
.sm-app .sm-steps__item{
  min-width:0;
  margin:0;
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  text-align:center;
  color:var(--text-secondary);
}
/* The icon stays an inline sibling of its label, as shipped. Colour only — no
   `display`, so the icon font's own box is left alone. */
.sm-app .sm-steps__icon{
  color:var(--brand-text);
}

/* --- 14.3 Inert blur -----------------------------------------------------
   ProformaModels blurs and deadens the whole page behind the "No Active
   Subscription" modal, which has no dismiss control — the blur is the signal
   that the page underneath is unavailable and `pointer-events:none` is what
   actually makes it so. That second declaration is behaviour, not decoration,
   so it is reproduced rather than reinterpreted, and nothing in the vocabulary
   comes close to either half.

   Three views carry this shape (Owner/ProformaModels, Tenant/ViewMakeOneTimePayment,
   Building/ViewBuildingDetails), all three as a view-local `.blur-container`
   toggled by `addClass`/`removeClass`. The class name is a JS hook: renaming the
   selector obliges the restyling agent to rename it in the two script calls too.

   THE BLURRED ELEMENT MUST NOT CONTAIN THE MODAL. A non-`none` `filter` makes
   the element a containing block for `position:fixed` descendants, so a modal
   inside it would be positioned and clipped against the blurred box instead of
   the viewport — and blurred. Today the modals are siblings after the container
   closes, and they have to stay that way.

   5px is reproduced from the shipped rule. There is no blur token, and it is the
   only raw length in this section; it is kept in a custom property so the value
   has one name rather than being typed into the filter.
   ------------------------------------------------------------------------ */
.sm-app .sm-blur{
  --sm-blur-radius:5px;
  filter:blur(var(--sm-blur-radius));
  pointer-events:none;
}

/* ============================================================================
   15. User surfaces — a tab rail, and one boundary marker for vendor embeds

   Two blocks for seventeen views, and the ratio is the point: like Owner, the
   User area is almost entirely shapes Stages 3-5 already built. Nothing below
   restates any of these:

     the 13 DataTables (17 init
       calls, 4 of them on ids
       that do not exist)        -> .sm-table-wrap / -scroll / -toolbar /
                                    .sm-table, with .sm-num on the <th> as well
                                    as the <td> on every money column
     the ~40 `<p class="card-text">Label: value</p>` runs -> .sm-detail-list
     the 20 `DarkThemeFormHeader` + 4px-white-<hr> pairs  -> .sm-section-header
     the ~120 `form-control DarkThemeFormInput` controls  -> .sm-input /
                                    .sm-select / .sm-textarea inside .sm-field
     the 27 `style="display:none"`
       validation spans          -> .sm-field__error, which declares NO display
                                    and therefore survives the jQuery
                                    .show()/.hide()/.toggle() driving all of them
     the 11 modals               -> .sm-modal-skin (12.9)
     `badge rounded-pill bg-*`   -> .sm-badge
     `alert alert-@Model.DiscountCodeMessageType` (ManageSubscription) and
       `alert alert-@companyDeleteMessageType` (ViewUser) -> .sm-alert; both
       controllers only ever write danger/warning/success/info, which are
       exactly the four .sm-alert modifier suffixes, so the interpolation
       survives verbatim — the same call section 13 recorded for Admin
     the 4 confirmation screens  -> .sm-empty + .sm-empty__art (7 / 12.7)
     the 6 copies of
       `.custom-checkbox-label`  -> .sm-checkbox on the <input> plus .sm-row on
                                    the <label>. Every declaration in all six
                                    copies is reproduced by those two, including
                                    the checked purple fill, and .sm-checkbox
                                    adds the focus ring the shipped rule removed
                                    with `outline:none` and never replaced
     the balance / total pairs   -> .sm-card-grid > .sm-card > .sm-metric
     the form-dense views (9 forms in ManageSubscription, 9 in ViewUser, 12 in
       ViewCompany)              -> .sm-field is enough; see 15.0

   Scope: `.sm-app`, same as sections 8, 12, 13 and 14 and for the same second
   reason. site.css is <link>ed AFTER this file and puts `.DarkThemeTextColor`,
   `.DarkThemeFormHeader`, `.card-text` and — the one that actually bites here —
   `.nav-pills .nav-link.active{background-color:#1b6ec2}` (site.css:709, the
   stock ASP.NET blue) on the very elements 15.1 lands on.

   15.0 — WHAT THE SIX VIEW-LOCAL <style> BLOCKS LEAVE BEHIND.

   ManageSubscription, ViewPaymentDispute, ViewPermissionProfile, ViewUser,
   CompanyQuickBooksMapping and UpdateCard each ship one. Five of the six are
   variations on the same diverged `.custom-checkbox-label` copy, and all six
   are fully covered once these six rules have a home:

     `.nav-link` / `.nav-link.active` (ViewPermissionProfile, both `!important`)
       -> 15.1. This is the only rule in any of the six whose deletion is a
       REGRESSION rather than a no-op: without it site.css:709 repaints the
       selected pill ASP.NET blue and Bootstrap's --bs-nav-link-color turns the
       other 21 link-blue on a near-black page.
     `.scrollable-nav{max-height:75vh;overflow-y:auto}` -> .sm-tabs__list--scroll.
     SUPERSEDED — what actually shipped differs from the plan below. Both
     `.mapping-warning` and `.unmapped-select` are owned by CompanyQuickBooksMapping's
     own <script> (one is built into an HTML string, the other toggled at ten call
     sites), so neither could be renamed from markup. Both are therefore BRIDGED in
     section 4 next to the `.is-invalid` bridge, keeping their names. `.is-invalid`
     is the product-wide answer for the 22 JS-toggled controls that lost `.form-control`
     when their views converted. The original plan text is kept below for history only.
     `.mapping-warning{display:inline-block}` (CompanyQuickBooksMapping)
       -> .sm-field__error. The shipped rule is ALREADY half-broken: the element
       is driven by jQuery `.toggle()`, which restores a <span>'s default
       `inline` on the way back, so the inline-block only survives until the
       first hide. Its replacement must keep declaring no display.
     `.unmapped-select` (same view) -> .sm-select--invalid. The string is a JS
       hook — `toggleClass("unmapped-select", …)` at four call sites — so either
       the class name stays on the element or all four are renamed together.
     `.cardBox` / `.achInput` (UpdateCard) -> .sm-card and .sm-input.
     `button{background:#444;border:1px solid #666;…}` (ViewPaymentDispute) is a
       BARE ELEMENT rule with no scope: it currently paints every <button> on
       that page, including the modal close buttons and the DataTables controls.
       It has no replacement and should simply go — but the one element that
       genuinely depends on it is created in JS with no class at all
       (`$('<button>').text('X')` in the upload handler), so removing the block
       obliges the restyling agent to add `.sm-btn .sm-btn--ghost .sm-btn--sm`
       in that call rather than in the markup (`sm-btn--danger` since the 9 Oct
       button-roles pass: a remove is red).

   15.2 — WHAT OUR CSS MAY NOT DO: THE FOUR VENDOR EMBEDS. See the block itself.

   15.3 and 15.4 came later, from the Phase 3 User survey: a row for one chosen
   file (ViewPaymentDispute's upload list) and a scrolling list of selected items
   (ViewPermissionProfile). The "two blocks" above are Stage 6's count.
   ========================================================================= */

/* --- 15.1 Tab rail --------------------------------------------------------
   ViewPermissionProfile's 22-item vertical pill rail over a `.tab-content`.
   Nothing in the vocabulary is within reach: .sm-rail__item is shell-scoped and
   full-bleed by design, .sm-btn--ghost has no selected state, and .sm-disclosure
   is a collapse trigger with a bottom hairline rather than a row in a group.

   Written as the one tab set for the product rather than as a User one-off —
   the same call 13.3 made for the map frame and 14.1 for the progress rail.
   Two views outside this stage carry the identical Bootstrap tab shape
   (Views/Building/ViewBuildingDetails.cshtml and Views/Building/ViewUnit.cshtml)
   and both render the same ASP.NET blue selected state today.

   SPECIFICITY — the tab class is doubled, the same technique section 6 uses on
   .sm-table and for the same reason. site.css's `.nav-pills .nav-link.active`
   is (0,3,0) and site.css is <link>ed AFTER this file, so a single
   `.sm-app .sm-tabs__tab` (0,2,0) loses that tie twice over. Doubling puts the
   base at (0,3,0) and the selected state at (0,4,0), which wins outright and
   means the skin does NOT depend on `.nav-pills` being dropped from the list.

   NO PANEL CLASS, deliberately. The panel is the element carrying `.tab-pane`,
   and Bootstrap hides it with `.tab-content > .tab-pane{display:none}` at
   (0,2,0) — the same trap 12.2 documents for `.collapse`. Spacing inside a
   panel is the existing .sm-stack-* utilities, which declare only child margins.

   `display:flex` on the TAB is safe: Bootstrap's Tab plugin toggles only
   `.active`, `.show` and aria attributes on a trigger, and never writes an
   inline display to it — unlike the carets in 12.2.
   ------------------------------------------------------------------------ */
.sm-app .sm-tabs__list{
  display:flex;
  flex-wrap:wrap;
  gap:var(--space-1);
  min-width:0;
  /* No `flex-direction`. Bootstrap's `.flex-column` is (0,1,0) and would lose to
     this selector, so declaring a row default here would silently straighten any
     list that keeps that utility. Direction is opt-in, below. */
}
.sm-app .sm-tabs__list--vertical{
  flex-direction:column;
  flex-wrap:nowrap;
}

/* Opt-in cap. 22 tabs at the row height run past 800px and push the panel they
   control off the bottom of the screen. Replaces the shipped `max-height:75vh`
   with a length off the space scale, for the reason 14's notes give: a
   viewport-relative rule on a component inside a Bootstrap column is asking the
   wrong question. Scrollbar treatment matches .sm-log--scroll's, and for the
   same reason — the platform default paints a light track, which reads as a
   white bar down a dark page. */
.sm-app .sm-tabs__list--scroll{
  max-height:calc(var(--space-32) * 4);
  overflow-y:auto;
  overscroll-behavior:contain;
  scrollbar-width:thin;
  scrollbar-color:var(--border-strong) transparent;
}
.sm-app .sm-tabs__list--scroll::-webkit-scrollbar{width:var(--space-2)}
.sm-app .sm-tabs__list--scroll::-webkit-scrollbar-track{background:transparent}
.sm-app .sm-tabs__list--scroll::-webkit-scrollbar-thumb{
  background:var(--border-strong);
  border-radius:var(--radius-pill);
}
.sm-app .sm-tabs__list--scroll::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}

/* No width: in a column list the default `align-items:stretch` already fills the
   track, and in a row list the tab should size to its label. */
.sm-app .sm-tabs__tab.sm-tabs__tab{
  display:flex;
  align-items:center;
  gap:var(--space-2);
  min-height:var(--control-height);
  padding:var(--space-2) var(--space-3);
  margin:0;
  font-family:var(--font-sans);
  font-size:var(--text-sm);
  font-weight:var(--weight-medium);
  line-height:var(--leading-snug);
  text-align:start;
  text-decoration:none;
  color:var(--text-secondary);
  background-color:transparent;
  /* Transparent rather than absent: the shipped rule outlined all 22 pills in
     #6d30a7 at once, which reads as 22 selected items. Reserving the border keeps
     the selected row from growing by 2px when it takes a visible one. */
  border:var(--border-hairline) solid transparent;
  border-radius:var(--radius-md);
  cursor:pointer;
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard),
    color var(--sm-duration-fast) var(--ease-standard);
}
/* Hover on a tab that is not selected is 1.1's neutral hover, the look of an unselected
   reporting-range pill under the pointer (section 1): 0.5's white-10% lift inside an
   --sm-field-border-hover edge under a --text-strong label. The edge is 7.99:1 on a card and
   8.40:1 on the canvas, the label 14.33 and 15.54:1; the white-4% wash it replaces was
   1.08-1.09:1, so the pointer used to change only the label. The rule above reserves the border,
   so the box does not move. The :not(:where()) guard is section 1's and weighs nothing: a tab
   that is [disabled], [aria-disabled="true"] or Bootstrap's .disabled takes no hover outline that
   would offer it as a target (Chromium matches :hover on a disabled <button>). The selected tab's
   rule below restates all three paints at the same weight, later, so a selected tab keeps its
   chip under the pointer. */
.sm-app .sm-tabs__tab.sm-tabs__tab:hover:not(:where([disabled], [aria-disabled="true"], .disabled)){
  color:var(--text-strong);
  background-color:var(--sm-btn-neutral-fill-hover);
  border-color:var(--sm-field-border-hover);
  text-decoration:none;
}
.sm-app .sm-tabs__tab.sm-tabs__tab:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
/* In a --scroll list the tabs are full-width and the list's own overflow clips an outset ring on
   both sides, so there the ring is inset (0.2) — the rail's case. */
.sm-app .sm-tabs__list--scroll .sm-tabs__tab.sm-tabs__tab:focus-visible{
  outline-offset:var(--sm-focus-offset-inset);
  box-shadow:var(--sm-focus-ring-inset);
}

/* A tab PANEL that its markup makes a Tab stop with tabindex="0" (Bootstrap's documented
   pattern, so a panel holding nothing focusable can still be reached) showed only the UA's 1px
   outline: Building/ViewBuildingDetails' Quick Cash Flow panels. It takes the product ring,
   outset, which the card body's padding has room for. Still no panel class and no `display`:
   Bootstrap owns .tab-pane's display. The radius only shapes the ring; a panel has no fill. */
.sm-app .tab-content > .tab-pane[tabindex="0"]:focus-visible{
  outline:var(--sm-focus-outline);
  border-radius:var(--radius-md);
  box-shadow:var(--focus-ring);
}

/* Selected. Both hooks are matched because Bootstrap's Tab plugin writes both,
   and `aria-selected` is also what the shipped markup ships the initial state as.
   Henry, 9 Oct 2026: "the selected tab gets a clearly visible marker, matching the lavender
   selected chip". It is that chip: 0.5's --sm-btn-selected-fill fill and border under an
   --sm-btn-selected-on label, the paint of the selected range pill (section 1) and of the pager's
   current page (section 6), so pills, pages and tabs share one "you are here". Measured on the
   live pages, the fill is 7.88:1 on a card (Building/ViewBuildingDetails' Quick Cash Flow,
   User/ViewPermissionProfile's list) and 8.28:1 on the canvas (the Units and People rails,
   Building/ViewUnit), against the rail and against the transparent unselected tabs alike, and the
   label is 8.28:1. The brand-12% wash in a white-10% border it replaces was 1.38-1.41:1.
   The weight no longer changes. The semibold this state used to add widened the selected tab by
   1.05-2.36px on the QA buildings (more on a longer label), which nudged every tab after it on
   each switch; the chip carries the state, so the label keeps the medium of the rule above and no
   box moves (0px measured on every rail at 1280 and 375).
   A quieter form, a 3px --purple-300 underline under a transparent tab, measured the same
   (7.88 / 8.28:1) in the same prototypes and lost on phones: rails of three or four tabs wrap at
   375px, and the bar under a first-row tab floated above the second row, away from the track line
   under the last one; in the vertical list it shrank to a 3px edge.
   (0,4,0), level with :hover and :focus-visible above and later than both, so the chip holds under
   the pointer. The focus ring is a box-shadow and is untouched; its --surface-canvas gap keeps its
   --purple-300 band apart from the chip, as on the selected pill. */
.sm-app .sm-tabs__tab.sm-tabs__tab.active,
.sm-app .sm-tabs__tab.sm-tabs__tab[aria-selected="true"]{
  color:var(--sm-btn-selected-on);
  background-color:var(--sm-btn-selected-fill);
  border-color:var(--sm-btn-selected-fill);
}

.sm-app .sm-tabs__icon{
  flex:0 0 auto;
  color:var(--text-muted);
  line-height:1;
}
/* On the chip the glyph takes the label's colour: --brand-text is --purple-300, the chip's own
   fill (1.00:1). No view carries .sm-tabs__icon today; the contract stays ready for one. */
.sm-app .sm-tabs__tab.sm-tabs__tab.active .sm-tabs__icon,
.sm-app .sm-tabs__tab.sm-tabs__tab[aria-selected="true"] .sm-tabs__icon{
  color:var(--sm-btn-selected-on);
}
/* A FOCUSED selected tab in a --scroll list (User/ViewPermissionProfile, whose roving tabindex
   keeps focus on the selected tab) takes the inset ring above, drawn in --purple-300, which is the
   chip's fill: the ring disappeared into it. On the chip the inset band is --sm-btn-selected-on
   instead, 8.28:1 against the fill inside it and against the chip's 1px lavender border outside
   it, the ring-and-gap shape the forced-colours rule below draws. (0,6,0) over the --scroll focus
   rule's (0,5,0); that forced-colours rule has the same weight and comes later, so it still wins
   there. */
.sm-app .sm-tabs__list--scroll .sm-tabs__tab.sm-tabs__tab:is(.active, [aria-selected="true"]):focus-visible{
  box-shadow:inset 0 0 0 var(--sm-focus-width) var(--sm-btn-selected-on);
}
/* Forced colours repaint the selected chip as Canvas and draw every tab's border, the transparent
   resting one included, in one system colour, so the selected tab would look like the rest (the
   weight that once told it apart is gone, above). It takes the system selection pair, as the
   selected pill does (section 1), and opts out
   with forced-color-adjust:none for the reason given there: Chromium's Canvas backplate turned the
   HighlightText label into a solid block ("Rent", "Current Month", "Tenants"). The icon is restated
   so it cannot keep another colour on the Highlight fill, and box-shadow:none keeps the dark/purple
   ring off the opted-out tab. Every tab list here is Bootstrap's nav tabs or pills carrying
   .sm-tabs__tab (Building/ViewBuildingDetails, Building/ViewUnit, User/ViewPermissionProfile), so
   one rule covers them. In a --scroll list the focus outline is drawn INSIDE the tab (the inset
   ring above), on the Highlight fill, where CanvasText is only about 1.2-1.4:1, so there it takes
   HighlightText, the colour made to read on Highlight. Same weights as the rules above and after
   them, so the state also holds under the pointer.
   A FOCUSED selected tab in a --scroll list (User/ViewPermissionProfile, whose roving tabindex keeps
   focus on the selected tab) cannot take an outset ring: the list scrolls and clips it on both
   sides. Its ring was HighlightText at the edge of the Highlight fill, and HighlightText is the
   Canvas colour in both palettes (black on black, white on white), so the ring merged with the
   page and the tab only looked 2px smaller (F-user-1). Now the outer 2px is a CanvasText outline
   (the inset offset above) and an inset Canvas box-shadow lays a 2px gap inside it, so the fill
   starts 4px in: CanvasText against Canvas on both sides of the ring (21:1 in the black and white
   palettes), the same ring-and-gap shape as the product ring. The box-shadow is painted because the
   tab opts out with forced-color-adjust:none, and the label keeps its 8px block and 12px inline
   padding clear of both bands. */
@media (forced-colors: active){
  .sm-app .sm-tabs__tab.sm-tabs__tab.active,
  .sm-app .sm-tabs__tab.sm-tabs__tab[aria-selected="true"]{
    forced-color-adjust:none;
    background-color:Highlight;
    border-color:Highlight;
    color:HighlightText;
    box-shadow:none;
  }
  .sm-app .sm-tabs__tab.sm-tabs__tab.active .sm-tabs__icon,
  .sm-app .sm-tabs__tab.sm-tabs__tab[aria-selected="true"] .sm-tabs__icon{
    color:HighlightText;
  }
  .sm-app .sm-tabs__list--scroll .sm-tabs__tab.sm-tabs__tab:is(.active, [aria-selected="true"]):focus-visible{
    outline-color:CanvasText;
    box-shadow:inset 0 0 0 calc(var(--sm-focus-width) + var(--border-hairline)) Canvas;
  }
}

@media (pointer: coarse){
  .sm-app .sm-tabs__tab.sm-tabs__tab{ min-height:var(--touch-target-min); }
}

/* --- 15.2 Vendor embed mount ---------------------------------------------
   Four mount points across three views, and this component exists to mark a
   BOUNDARY rather than to paint anything:

     #payment-element, #address-element   UpdateCard — Stripe Elements
     #account-management-container        ViewCompanyPaymentAccount — Connect
     #notification-banner-container       ViewCompany — Connect

   WHAT OUR CSS MAY DO: give the container a box. Width, margin, min-width and
   the min-height reserved below are the whole list, plus whatever the
   surrounding .sm-field / .sm-card supplies around it.

   WHAT OUR CSS MAY NOT DO: reach anything inside. Stripe Elements renders into
   a cross-origin IFRAME, and the Connect components are custom elements with a
   closed shadow root — no selector, inherited property or custom property of
   ours crosses either. Their entire styling channel is the `appearance` object
   passed in JS (UpdateCard ~line 509; ViewCompanyPaymentAccount ~line 179;
   ViewCompany ~line 738), which is JS configuration and out of scope for a CSS
   stage. Do NOT put a background, border, padding or colour on these
   containers: the two Connect appearance objects already declare their own
   `colorBackground:#14171D` and `colorBorder:#2B3039`, so anything painted here
   double-frames a surface it cannot match.

   The reserve is opt-in and defaults to nothing, which is the whole reason it is
   a modifier. The two Elements mounts always render content, so reserving their
   height stops the card jumping ~190px when the iframe resolves. The Connect
   notification banner renders NOTHING when there are no notifications — giving
   that one a floor would reserve permanent dead space on the company page.
   ------------------------------------------------------------------------ */
.sm-app .sm-embed{
  --sm-embed-reserve:0px;
  min-width:0;
  min-height:var(--sm-embed-reserve);
}
.sm-app .sm-embed--reserve{
  --sm-embed-reserve:calc(var(--space-32) + var(--space-16));
}
/* A smaller reserve, sized to the Stripe Address Element (117px rendered): the 192px one left 75px
   of dead space under it (User/UpdateCard). */
.sm-app .sm-embed--reserve-sm{
  --sm-embed-reserve:calc(var(--space-24) + var(--space-5));
}
/* Stripe Connect's embedded components enforce a 320px minimum width, and at 375 a card body is
   277px wide, so the account-management embed spilled about 22px past the card's edge
   (User/ViewCompanyPaymentAccount). Below 480px the mount bleeds through the card's padding to its
   edges, 325px at 375. For a mount inside a .sm-card or .sm-card__body padded by --pad-card;
   margin is on this block's allowed list. */
@media (max-width: 479.98px){
  .sm-app .sm-embed--bleed{ margin-inline:calc(var(--pad-card) * -1); }
}
/* A mount with nothing to show takes no stack margin: User/ViewCompany opened a 48px gap between
   "Rent Collection Account" and its table where the stack gives 24. Margin is on this block's
   allowed list, and neither state touches `display`, because the vendor script mounts into the
   container and must find it rendered.
   - Still :empty: before connect.js mounts, or if it never loads. Only mounts without a reserve:
     a reserved mount paints its box from the first frame and keeps its spacing.
   - Mounted but silent: the Connect banner's content sits behind a closed shadow root that no
     selector can read, but the same script shows its status line
     (#notification-banner-action-warning) exactly when the banner has notifications and sets it
     back to display:none when it has none. A hidden status line therefore means an empty banner.
   (0,5,0) and (2,3,0) against the stack's (0,1,0). */
.sm-app :is(.sm-stack-2, .sm-stack-3, .sm-stack-4, .sm-stack-6) > .sm-embed:not(.sm-embed--reserve, .sm-embed--reserve-sm):empty,
.sm-app #notification-banner-action-warning:is([hidden], [style*="display"][style*="none"]) + #notification-banner-container{
  margin-top:0;
}

/* --- 15.3 File row -------------------------------------------------------
   One chosen file as a row: its name, then a remove control.
     <div class="sm-file-row">
       <span class="sm-file-row__name">…</span>
       <button type="button" class="sm-btn sm-btn--danger sm-btn--sm">…</button>
     </div>
   User/ViewPaymentDispute's upload handler builds these rows in script and appends them to
   #uploadedDocuments, a .sm-stack-2 that spaces them, so the row zeroes only its side and bottom
   margins and leaves the top to the stack. Its view-local <style> retires once the handler's
   class strings become `sm-file-row` and `sm-file-row__name` and its class-less remove <button>
   takes .sm-btn .sm-btn--danger .sm-btn--sm, in the same calls (15.0; --ghost until the 9 Oct
   button-roles pass). `display:flex` is safe: the
   rows are created and .remove()d, never shown or hidden. --text-muted is rebound for anything
   muted on the raised fill, the 12.9 technique.
   ------------------------------------------------------------------------ */
.sm-app .sm-file-row{
  --text-muted:var(--text-secondary);
  display:flex;
  align-items:center;
  gap:var(--space-2);
  margin-inline:0;
  margin-bottom:0;
  padding:var(--space-2) var(--space-3);
  background-color:var(--surface-raised);
  border:var(--border-hairline) solid var(--border);
  border-radius:var(--radius-md);
}
.sm-app .sm-file-row__name{
  flex:1 1 auto;
  min-width:0;
  overflow-wrap:anywhere;
  font-weight:var(--weight-semibold);
  color:var(--text);
}

/* --- 15.4 Selected list --------------------------------------------------
   A bordered, scrolling list of chosen items: User/ViewPermissionProfile's selected permissions.
   With 53 selected, the list ran 2,158px, the stretched grid made all three cards 2,243px tall,
   and Save and Delete sat 2,700px down; the rows were painted by site.css .list-group-item in
   rgb(35,35,34), off the token ramp. Capped at 512px like .sm-tabs__list--scroll, whose scrollbar
   treatment it repeats. The items restate their own background and borders, so a leftover
   .list-group-item cannot paint through them.
     <ul class="sm-selected-list" tabindex="0" aria-labelledby="…">
       <li class="sm-selected-list__item">…</li>
     </ul>
   A box that scrolls has to be reachable by keyboard, hence tabindex="0" and a name from its
   heading; the ring below shows where focus is. An empty list holds one
   .sm-selected-list__item--empty row carrying the view's existing text.
   ------------------------------------------------------------------------ */
.sm-app .sm-selected-list{
  max-height:calc(var(--space-32) * 4);
  margin-inline:0;
  margin-bottom:0;
  padding:0;
  list-style:none;
  overflow-y:auto;
  overscroll-behavior:contain;
  scrollbar-width:thin;
  scrollbar-color:var(--border-strong) transparent;
  border:var(--border-hairline) solid var(--border-subtle);
  border-radius:var(--radius-md);
}
.sm-app .sm-selected-list::-webkit-scrollbar{width:var(--space-2)}
.sm-app .sm-selected-list::-webkit-scrollbar-track{background:transparent}
.sm-app .sm-selected-list::-webkit-scrollbar-thumb{
  background:var(--border-strong);
  border-radius:var(--radius-pill);
}
.sm-app .sm-selected-list::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
.sm-app .sm-selected-list:focus-visible{
  outline:var(--sm-focus-outline);
  box-shadow:var(--focus-ring);
}
.sm-app .sm-selected-list__item{
  padding:var(--space-2) var(--space-3);
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  color:var(--text);
  background-color:transparent;
  border:0;
  border-bottom:var(--border-hairline) solid var(--border-subtle);
}
.sm-app .sm-selected-list__item:last-child{ border-bottom:0; }
.sm-app .sm-selected-list__item--empty{
  padding-block:var(--space-6);
  text-align:center;
  color:var(--text-secondary);
}

/* ============================================================================
   16. Tenant surfaces — the native-control colour scheme, and one frame for the
       embedded PDF viewer

   Two blocks for fifteen views, and the ratio is the point: like Owner and
   User, the Tenant area is almost entirely shapes Stages 3-6 already built.
   Nothing below restates any of these:

     the 12 DataTable init calls -> .sm-table-wrap / -scroll / -toolbar /
                                    .sm-table, with .sm-num on the <th> as well
                                    as the <td> on every money column. ALL TWELVE
                                    target an id that exists in their own view:
                                    the copy-paste orphan section 15 recorded for
                                    User (4 of 17) does NOT recur here
     the 3 caret-toggled headings
       over a Bootstrap collapse -> .sm-disclosure (12.2), both display traps
                                    live and load-bearing — see ViewWorkRequests
     the 43 `DarkThemeFormHeader` +
       4px-white-<hr> pairs      -> .sm-section-header, or .sm-disclosure where
                                    the heading toggles a collapse
     the 9 `.circle` +
       `.statusColor*` rings     -> .sm-card > .sm-metric--lg + .sm-badge; 16.0
     the 5 copies of
       `.payment-list`/`.line-item` -> .sm-detail-list (12.5)
     the 5 copies of `.cardBox`  -> .sm-card, the mapping 15.0 already recorded
     `.blur-container`           -> .sm-blur (14.3), which names
                                    Tenant/ViewMakeOneTimePayment by file
     the 2 `signatureContainer` /
       `signatureFont` plates    -> .sm-signature / __mark / __caption (12.8).
                                    The markup in ViewUnitLease and
                                    ViewSupplementalDocument is byte-identical to
                                    the CreateSignature pair 12.8 was written
                                    for, down to the `text-center` on the <p>, so
                                    it is covered outright — nothing is missing.
                                    The same caveat carries: the plate must NOT
                                    keep `signatureFont`, which hard-codes 30px
                                    and black at (0,1,0) from site.css
     the 13 modal roots (two of
       them emitted inside a loop,
       so the runtime count is
       higher)                   -> .sm-modal-skin (12.9)
     the 102 `style="display:none"`
       validation spans          -> .sm-field__error, which declares NO display
                                    and therefore survives the jQuery
                                    .show()/.hide() driving all of them
     the 72 `form-control
       DarkThemeFormInput` controls -> .sm-input / .sm-select / .sm-textarea
                                    inside .sm-field
     the 50 `badge rounded-pill bg-*` -> .sm-badge
     the 4 confirmation screens  -> .sm-empty + .sm-empty__art (7 / 12.7)
     the 10 inline-coloured
       300x50 / 350x50 action tiles -> .sm-btn variants
     the itemised money figures  -> .sm-num on the cell AND its header

   Scope: `.sm-app`, same as sections 8 and 12-15 and for the same second reason.
   site.css is <link>ed AFTER this file and puts `.DarkThemeTextColor`,
   `.DarkThemeFormHeader`, `.card-text` and `.card-subtitle` — all (0,1,0) — on
   the elements these views' content lands on.

   16.0 — WHAT THE SIX VIEW-LOCAL <style> BLOCKS LEAVE BEHIND.

   ViewUnit and ViewPayRent share the `.circle` block. `.circle` is a 350px ring
   with a 10px border, `font-family: Arial, sans-serif`, and a colour supplied by
   one of `.statusColorPaid` / `.statusColorDue` / `.statusColorOverDue`
   (#4CAF50 / #FFC107 / #F44336 — none of them on this system's ramp). It maps to
   .sm-card > .sm-metric--lg, with the status carried by the .sm-badge BOTH views
   already render beside the <h1> two rows above the ring. The ring is not worth
   reproducing: 350px plus 20px of border is wider than a 360px handset, so it
   raises a horizontal scrollbar on the most common phone width, and the status
   colour it encodes is already stated in words. `.vh-100-minus-navbar` in the
   same two blocks is dead — the class appears in no markup in either view.

   ViewAutomaticPayments, ViewMakeOneTimePayment and ViewPayRent share
   `.payment-list` / `.line-item` (plus a @media (max-width:768px) half that
   turns each line into a space-between flex row). All of it is .sm-detail-list:
   a two-column grid needs no breakpoint to do the same job, and __value is
   already tabular. Note the leader dots are built in RAZOR, not CSS —
   `@Html.Raw(new string('.', Math.Max(0, 30 - item.Name.Length)))` — so they
   come out with the markup; they are a leader glyph, not copy, and the Math.Max
   already emits zero dots for any name over 30 characters, which is why the
   amounts do not line up today.

   ViewMakeOneTimePayment's remaining three rules are the ones 14.0 already
   retired for Owner/ProformaModels: `.blur-container` -> .sm-blur (14.3),
   `.custom-modal-backdrop` is dead (no markup carries it, and its second
   declaration `Semi-transparent: background` is not valid CSS), and the
   unscoped `.modal-content{background:transparent;color:white;border:none}` is
   a global leak that .sm-modal-skin replaces.

   ViewTenantInsurance's block is four rules and a bare element selector:
   `body{background-color:#121212;color:#fff}` and `.DarkThemeTextColor` are
   section 8.1's job; `.btn-dark-theme` (+ :hover) is the download button and
   becomes .sm-btn .sm-btn--secondary; `.form-control-dark` (+ :focus) is the
   five modal inputs and becomes .sm-input. None of the four is a JS hook — the
   view's script reaches its controls by id and by
   `input[data-type='currency']`.

   ViewTenantApplication's block is the one with load-bearing content, and every
   selector in it is an ID:
     `#fileInput` / `#fileInput2 {display:none}` — the hidden native file inputs
       that the drop-zone handlers click programmatically. Deleting the block
       without replacement puts two raw file pickers back on screen. The
       replacement is .sm-visually-hidden, which keeps them out of the layout and
       still clickable from script; `hidden` as an ATTRIBUTE would also work, but
       not on the 6 views that <link> pdf.js's viewer.css — it ships
       `.hidden,[hidden]{display:none !important}` globally.
     `#dropArea` / `#dropArea2` (2px dashed drop zones) and `.preview-image`
       (250px thumbnail cap) with `#imageList` / `#imageList2` (a nowrap,
       overflow-x:auto thumbnail strip) are deliberately NOT given a component
       here. Only ONE view in this area carries them; the other three are
       Building/ApplicationTemplate, Building/ApplyToAvailableUnit and
       Building/AddUnitPhotos, so the shape belongs to whichever stage owns
       Building. Until then those five rules stay in the view.

   16.0b — TWO THINGS THE RESTYLING AGENT MUST NOT ASSUME.

   The 158 `disabled` attributes across the area are ALL static — no script in
   any Tenant view adds or removes `disabled` on a field. ViewTenantApplication
   alone ships ~100 of them, i.e. it is a permanently read-only review screen
   rendered as a dead form. Do not blanket-convert those to .sm-input:
   `.sm-input:disabled` is opacity .45, which drops --text on --surface-field to
   about 4.1:1 and fails WCAG 1.4.3 on a page that is nothing BUT read-back
   values. Read-only values belong in .sm-detail-list; keep .sm-input only where
   the control is genuinely writable.

   `#commercialModel`, `#residentialModel`, `#isEmployedSection`,
   `#otherIncomeSection` and `#CompanyHadPreviousLocationSection` are driven by
   jQuery `.show()` / `.hide()`. That is trap 2 from section 12 in its purest
   form: nothing landing on those five elements may declare `display`.
   ========================================================================= */

/* --- 16.1 Native control colour scheme -----------------------------------
   One declaration, and it is the replacement for an idiom that appears 82 times
   in ViewTenantApplication alone and in 37 views product-wide:
   `style="color-scheme:dark"`, pasted onto every input, select and textarea.

   Nothing in the system expressed it until now, and until this stage nothing
   needed it — no view converted in Stages 3-6 contains a single date, time or
   month input. Two views here do: ViewTenantInsurance's `#expirationDate` (being
   restyled now) and ViewTenantApplication's business-hours selects. Drop the
   inline declaration without this rule and the native date picker and the
   native <select> popup render in the LIGHT scheme on a near-black page.

   On `.sm-app` rather than on `.sm-input`, because the controls that need it
   most are the ones our classes never reach: DataTables' generated search box,
   Bootstrap's own controls, and every native scrollbar on an overflow:auto
   descendant — which is the platform default the WebKit scrollbar treatments in
   6, 13.2 and 15.1 were each written to work around.

   Two known limits, both accepted. `.sm-app` is on <body>, not <html>, so the
   document scrollbar is unaffected; and `.sm-signature` (12.8) is a deliberately
   light plate, which is fine because it contains no native control.
   ------------------------------------------------------------------------ */
.sm-app{
  color-scheme:dark;
}

/* pdf.js ships its own LIGHT chrome and is mounted in SIX views, and all six now wrap
   #outerContainer in .sm-doc-frame (16.2), which hands the vendor `color-scheme:light` back:
   Tenant/ViewSupplementalDocument and Tenant/ViewUnitLease first, then the four Building views
   (ViewUnitLease, ViewSupplementalDocument, AddUnitLease, AddSupplementalDocuments). So this rule is
   redundant on every page today, harmlessly. It stays as the fallback for a viewer mounted without
   the frame, which would otherwise inherit the dark scheme above and paint dark native scrollbars
   and form controls inside a light viewer. Keying off pdf.js's own #outerContainer needs no markup. */
.sm-app #outerContainer{
  color-scheme:light;
}

/* --- 16.2 Embedded document frame ----------------------------------------
   Mozilla pdf.js, mounted inline in six views: Tenant/ViewUnitLease,
   Tenant/ViewSupplementalDocument, Building/ViewUnitLease,
   Building/ViewSupplementalDocument, Building/AddUnitLease and
   Building/AddSupplementalDocuments. All six ship the identical
   `<div id="outerContainer" style="height:750px">`. Written as the one document
   frame for the product rather than as a Tenant one-off — the same call 13.3
   made for the map frame and 14.1 for the progress rail.

   HEIGHT is the whole reason this component exists, and it is the same failure
   mode 13.3 documents for Leaflet. viewer.css ships
   `#outerContainer{width:100%;height:100%;position:relative}`; with no definite
   height on the parent that percentage resolves to auto, the viewer collapses to
   zero and paints nothing. Today the only thing preventing that is the inline
   750px.

   THIS CLASS IS A WRAPPER, NOT A CLASS FOR #outerContainer. viewer.css is
   <link>ed from inside the view body, i.e. AFTER this file, and `#outerContainer`
   is (1,0,0) — no class selector can outrank it, so a `.sm-doc-frame` ON that
   element could not set its height at all. It has to be a new parent, and the
   inline `height:750px` comes off #outerContainer in the same edit.

   overflow:auto rather than 13.3's overflow:hidden. Both clip to the radius, but
   viewer.css also ships `#mainContainer{position:absolute;inset:0;min-width:350px}`,
   so below ~350px of frame width the toolbar overflows; `hidden` would clip the
   right-hand controls with no way to reach them, `auto` lets the frame scroll to
   them. isolation — not overflow — is what keeps pdf.js's z-index scale (which
   reaches 99999 on its overlay container) underneath the sticky topbar's
   z-index:20 from 8.4.

   color-scheme:light hands the vendor subtree back its own environment: 16.1
   flips the whole product to dark, and viewer.css is a LIGHT UI
   (--body-bg-color rgba(212,212,215,1), --main-color rgba(12,12,13,1)). Without
   this line pdf.js's page-number input and scale <select> would render dark
   inside its light toolbar. Nothing else here reaches into the subtree, for the
   reason 15.2 states at length: the vendor owns its interior.

   A COLLISION TO KNOW ABOUT, not fixed here. viewer.css:1091 declares
   `:root{--sidebar-width:200px}`. tokens/layout.css declares
   `:root{--sidebar-width:264px}`. Same specificity, and viewer.css loads later,
   so on these six views the token resolves to pdf.js's value. It is inert today
   only because 8.1 sets `--sm-rail-width:15rem` and `.sm-rail` reads
   `var(--sm-rail-width, var(--sidebar-width))` — the fallback never fires.
   Whoever carries out the change 8.1's comment describes ("Raise this to
   var(--sidebar-width) only in the same change that fixes those two margins")
   must rename the token or scope it, or the rail will silently narrow to 200px
   on exactly these six pages.
   ------------------------------------------------------------------------ */
.sm-app .sm-doc-frame{
  /* 768px, the nearest step on the space scale to the shipped 750px. */
  --sm-doc-height:calc(var(--space-32) * 6);
  width:100%;
  height:var(--sm-doc-height);
  overflow:auto;
  isolation:isolate;
  color-scheme:light;
  background-color:var(--surface-sunken);
  border:var(--border-hairline) solid var(--border-subtle);
  border-radius:var(--radius-lg);
}
@media (max-width: 767.98px){
  .sm-app .sm-doc-frame{ --sm-doc-height:calc(var(--space-32) * 4); }
}

/* --- 16.3 pdf.js page leaks, toolbar focus and the narrow frame ------------
   viewer.css is linked from the view body, after this file, and some of its rules reach the whole
   page (measured on Tenant/ViewUnitLease, Building/AddUnitLease, AddSupplementalDocuments). It is
   never copied or edited (decision D7): this answers the three measured leaks on pages that link
   it, and gives the vendor toolbar a focus ring.
   KEY. `html:is(:has(#outerContainer), :has(link[href*="/css/viewer.css"]))`: a page that embeds
   the viewer, or links the stylesheet without one (a lease with no stored PDF still links it).
   (1,0,1) and up, so it outranks viewer.css's element selectors (0,0,1) whatever the order.
   LEFT ALONE, deliberately: viewer.css's :root custom properties, because pdf.js writes
   --sidebar-width and --scale-select-width on <html> at run time and restating them anywhere
   would freeze them (the --sidebar-width collision is 16.2's note); and `.hidden,[hidden]
   {display:none!important}`, which viewer.js depends on and which only ever hides.
   1. `html,body{height:100%}` pinned both boxes to the viewport (900px round 1,422-2,141px of
      page). Back to auto, which is what every other page has, so no page chrome can depend on
      the pinned value: the rail is position:fixed at min-height:100vh and the topbar is not
      sticky. SCREEN ONLY: pdf.js's print path sizes #printContainer and each printed page at
      height:100% of that body, so print keeps viewer.css's values.
   2. `body{scrollbar-color}` is inherited, so pdf.js's grey pair (under the OS dark scheme)
      reached every scroller on the page. body goes back to auto and #outerContainer takes the
      same declaration, so the viewer's own scrollers are exactly as pdf.js draws them (in the
      light scheme its `auto auto` pair is invalid and falls back to the inherited auto).
   3. `*{padding:0;margin:0}` took the UA padding off every <option> outside the viewer (the
      page's own selects); `revert` hands them the browser's value back. It is the only property
      that reset changed outside the viewer (3F3, stylesheet toggled off and diffed at 1440). The
      viewer's own options keep the reset. */
@media screen{
  html:is(:has(#outerContainer), :has(link[href*="/css/viewer.css"])),
  html:is(:has(#outerContainer), :has(link[href*="/css/viewer.css"])) > body{
    height:auto;
  }
  html:is(:has(#outerContainer), :has(link[href*="/css/viewer.css"])) > body{ scrollbar-color:auto; }
  .sm-app #outerContainer{ scrollbar-color:var(--scrollbar-color) var(--scrollbar-bg-color); }
  html:is(:has(#outerContainer), :has(link[href*="/css/viewer.css"])) option:not(#outerContainer option){
    padding:revert;
  }
}

/* Toolbar focus. viewer.css marks a focused toolbar control only with its hover fill, and it
   strips the outline (`.toolbar button{outline:none}`, (0,1,1)): #666667 on the dark #38383D
   toolbar is 2.0:1, and the tab walk on these pages starts in the toolbar. A 2px outline at
   offset 0 sits in the space pdf.js leaves round every control (2px above and below a button,
   3px round a field, 1px of margin plus 1px of group padding at the toolbar's two ends), so the
   frame's edge never clips it. pdf.js follows the OS colour scheme, not the frame's
   color-scheme, so the ring follows the same media query viewer.css does: --purple-600 on the
   light toolbar (7.59:1; 7.98:1 on the white menus), --purple-300 on the dark one (4.93:1; 3.72:1
   on the #4A4A4F menus). An outline, so forced colours keep it. The find bar's option boxes are
   invisible (opacity 0), so their visible label takes the ring. (1,3,0). */
.sm-app #outerContainer :is(.toolbarButton, .secondaryToolbarButton, .dialogButton, .toolbarField, select):focus-visible,
.sm-app #outerContainer .toolbarField[type="checkbox"]:focus-visible + .toolbarLabel{
  outline:var(--sm-focus-width) solid var(--purple-600);
  outline-offset:0;
}
@media (prefers-color-scheme: dark){
  .sm-app #outerContainer :is(.toolbarButton, .secondaryToolbarButton, .dialogButton, .toolbarField, select):focus-visible,
  .sm-app #outerContainer .toolbarField[type="checkbox"]:focus-visible + .toolbarLabel{
    outline-color:var(--purple-300);
  }
}
/* The ">>" tools menu is the exception to "the space pdf.js leaves": its 19 rows are
   `min-width:100%; margin:0` inside #secondaryToolbarButtonContainer, whose overflow-y:auto makes
   overflow-x clip at the rows' own edges, so the offset-0 ring lost both sides, the first row its
   top band and the last its bottom (design-a11y DA4-F1, W4-pdfjs-3). There the ring is drawn
   inside the row (0.2's inset offset), so all four sides show; the colour follows the two rules
   above, and forced colours keep the outline. (1,3,0), tied with the ring rule and after it. */
.sm-app #outerContainer .secondaryToolbarButton:focus-visible{
  outline-offset:var(--sm-focus-offset-inset);
}

/* The frame is an inline-size container, so the toolbar can answer to the FRAME's width.
   viewer.css drops the zoom select only below a 560px viewport, but beside the rail at 768 the
   frame is 430-480px, and the select and zoom buttons wrapped out of the 32px toolbar and floated
   over the page (Tenant/ViewUnitLease). At 600px of frame or less the select goes, as viewer.css
   already does on a phone; the zoom buttons stay. viewer.js never names #scaleSelectContainer
   (it reads #scaleSelect), so nothing scripts its display.
   What containment changes, checked against viewer.css and viewer.js 3.3.122:
   - Layout containment makes the frame the containing block for FIXED descendants. viewer.css
     has one fixed rule, the hand tool's `.grab-to-pan-grab:active`, which pins #viewerContainer
     while the pointer is held: that now covers the frame instead of the whole viewport. The
     hand tool's drag overlay is appended to <body>, outside the frame. Dialogs open with
     showModal() into the top layer, which containment does not reach, and #printContainer sits
     outside the frame in all six views.
   - pdf.js's absolutely positioned parts (#mainContainer, the sidebar, #viewerContainer, the
     loading bar) are still placed against #outerContainer, which is position:relative.
   - The frame already took its width from its column (width:100%) and was already a stacking
     context (isolation), so its size and stacking do not change. Style containment scopes CSS
     counters and quotes, which viewer.css does not use. */
.sm-app .sm-doc-frame{ container-type:inline-size; }
@container (max-width:37.5rem){
  .sm-app .sm-doc-frame #scaleSelectContainer{ display:none; }
}

/* ============================================================================
   17. Building surfaces — the busy-anchor guard, the shared upload drop zone, the carousel
       figure, and two bridges for class names a view's Razor or script owns

   Two blocks for the largest area in the product, and the ratio is the point for
   the fourth stage running: Building is almost entirely shapes Stages 3-7 already
   built. Nothing below restates any of these:

     the DataTables            -> .sm-table-wrap / -scroll / -toolbar / .sm-table,
                                  with .sm-num on the <th> as well as the <td> on
                                  every money column. All 65 init calls in the area
                                  target an id that exists in their own view — the
                                  copy-paste orphan section 15 recorded for User
                                  (4 of 17) does NOT recur here
     the 7 `user-icon.png`
       profile headers         -> .sm-card--flush .sm-media-card + __figure /
                                  __img / __body (12.4) over a .sm-detail-list
                                  (12.5). This is the mapping User/ViewUser
                                  settled, down to the `style="width:18%"` the
                                  192px figure column replaces
     the caret-toggled headings
       over a Bootstrap collapse-> .sm-disclosure (12.2). BOTH display traps are
                                  live in this area: ViewBuildingNotifications and
                                  ViewBuildingUnits toggle their caret pair with
                                  jQuery .toggle() inside `shown.bs.collapse` /
                                  `hidden.bs.collapse` handlers
     the `<a onclick="submitXForm(i)">`
       card links              -> the stretched-link card (12.3): .sm-card--link
                                  on the card, the <a> (.sm-card-link plus
                                  .sm-card__link) round the title inside its
                                  heading. ViewBuildingNotifications carries it,
                                  as Owner/MyBuildings and ProformaModels and
                                  Manager/ManagedBuildings and Alerts do (D11)
     the Chart.js canvases     -> .sm-chart / .sm-chart__canvas (12.6). Every one
                                  is `responsive:true, maintainAspectRatio:false`
                                  inside a bare <div> with no height, so __canvas
                                  is not decoration — it is the parent box the
                                  chart measures
     the `<iframe id="pdfViewer" height="800px">`
       in ViewTenantInsurance  -> .sm-doc-frame (16.2) as a WRAPPER, exactly as
                                  for pdf.js. It is the only <iframe> in the
                                  authenticated product
     the `DarkThemeFormHeader` +
       4px-white-<hr> pairs    -> .sm-section-header, or .sm-disclosure where the
                                  heading toggles a collapse
     the `form-control DarkThemeFormInput`
       controls                -> .sm-input / .sm-select / .sm-textarea in .sm-field
     the `style="color-scheme:dark"`
       inline pairs            -> 16.1, which sets it once on .sm-app
     the `style="display:none"`
       validation spans        -> .sm-field__error, which declares NO display
     the `badge rounded-pill bg-*`
       and the Razor-built
       `badge-ontrack`/`badge-missing`
       pair                    -> .sm-badge. The latter two are built in RAZOR
                                  (`var statusClass = asset.HasSchedule ? … : …`),
                                  not in script, so they CAN be renamed from markup
     the modal roots           -> .sm-modal-skin (12.9)
     the `.metric-box` / `.summary-card`
       tiles                   -> .sm-card-grid > .sm-card > .sm-metric
     the `.payment-list` / `.line-item`
       copies                  -> .sm-detail-list (12.5), the mapping 16.0 recorded
     the `.cardBox` copies     -> .sm-card, the mapping 15.0 recorded
     the `.custom-checkbox-label`
       copies                  -> .sm-checkbox + .sm-row, the mapping 15.0 recorded
     the inline-coloured 300x50
       action tiles            -> .sm-btn variants
     the confirmation screens  -> .sm-empty + .sm-empty__art (7 / 12.7)

   Scope: `.sm-app`, same as sections 8 and 12-16 and for the same second reason.
   site.css is <link>ed AFTER this file and puts `.DarkThemeTextColor`,
   `.DarkThemeFormHeader`, `.card-text` and `.card-subtitle` — all (0,1,0) — on the
   elements these views' content lands on.

   17.0 — WHAT THIS SECTION USED TO DEFER, BOTH NOW SETTLED.

   (The carousel figure this note used to defer, whose three copies disagreed on a 400px fixed
   height against a 650px max-height, is settled in wave 2 as 17.3: one contained figure for
   AddUnitPhotos and ViewUnitApplication. ViewAvailableUnit's photo slides keep 19.2's `.c-item`
   and `.c-img` until that view adopts it.)

   The DataTables BUTTONS toolbar skin that ViewBuildingDetails carried in its own <style>
   (`.statement-shell .dt-buttons .dt-button`, ~60 lines of `!important`) was left alone here
   while that one view was its only user. Wave 2 removed the block and the `statement-shell`
   class; section 6 now skins those buttons (its Buttons extension rules) and the statement's
   scroll frame.

   17.1 — SIX VIEW-LOCAL <style> BLOCKS IN THE THIRTEEN RESTYLED HERE. Their
   dispositions are recorded at the blocks below and in the stage report; the two
   that leave anything behind are BuildingDepreciationSummary's (17.1) and the four
   drop-zone copies (17.2).
   ========================================================================= */

/* --- 17.1 Busy anchor guard ----------------------------------------------
   One declaration, and it is behaviour rather than paint.

   Three views run the same double-submit guard on a control that is an ANCHOR,
   not a button — BuildingDepreciationSummary (two of them), ViewBuildingFinancing
   (#deprBtn) and the already-converted User/ViewCompanyPaymentAccount:

     if ($btn.hasClass('disabled')) return;          // NOTE: returns BEFORE
     e.preventDefault();                             //       preventDefault
     $btn.addClass('disabled').attr('aria-disabled','true').prop('disabled', true)
         .html('Processing… <i class="fa-solid fa-spinner fa-spin"></i>');
     setTimeout(function(){ window.location.href = href; }, 50);

   `prop('disabled', true)` does nothing on an <a> — there is no disabled attribute
   on anchors — and the early `return` happens before preventDefault, so on a second
   click the browser simply follows the href and the navigation fires twice. The only
   thing stopping that today is `pointer-events:none`, which comes from Bootstrap's
   `.btn:disabled, .btn.disabled` (bootstrap 5.2.2) plus a hand-written
   `a.btn.disabled, a.btn[aria-disabled="true"]` block those two Building views each
   ship locally. BOTH depend on `btn` staying on the element — and `btn` is exactly
   what the settled convention drops when .sm-btn is added, because a classless
   Bootstrap `.btn` resolves --bs-btn-disabled-bg to nothing and paints the control
   transparent the moment it goes disabled. That is not a hypothetical here: these
   controls are disabled by their own handler.

   So the guard has to move into the system, and this is it. Section 1 already
   dims `[aria-disabled="true"]`, so only the pointer suppression is new.

   `[href]` is what keeps it off <button>: section 1 deliberately avoids
   pointer-events so a `title` tooltip still fires on a disabled button, and a real
   <button> needs none of this — its `disabled` attribute suppresses activation by
   itself. `.disabled` is matched alongside `[aria-disabled="true"]` because it is
   the class every one of these handlers sets first, and a handler that set only
   that one would otherwise slip through.
   ------------------------------------------------------------------------ */
.sm-app .sm-btn[href][aria-disabled="true"],
.sm-app .sm-btn[href].disabled{
  pointer-events:none;
}

/* --- 17.2 Upload drop zone -----------------------------------------------
   The shape app.css 16.0 deferred to this stage by name. Four views share it —
   Building/AddUnitPhotos, Building/ApplicationTemplate, Building/ApplyToAvailableUnit
   and Tenant/ViewTenantApplication, which is already converted and is still carrying
   its view-local block waiting for this. Nothing in the vocabulary was within reach:
   no component reserves a dashed drop target, and no component is a single-row
   horizontal thumbnail strip.

   WHAT THE FOUR SHIPPED BLOCKS ACTUALLY DO, because most of it is already dead:

     `#dropArea{border:2px dashed #999}` never renders. The element also carries
       `class="border border-secondary rounded p-2"`, and Bootstrap's border and
       spacing UTILITIES are `!important` — so the live border today is Bootstrap's
       1px solid secondary, and the live padding is p-2's 0.5rem, not 1rem. The
       dashed affordance the author wrote has never been on screen.
       CONSEQUENCE FOR THE RESTYLING AGENT: `border border-secondary rounded p-2`
       must come OFF the element in the same edit that adds .sm-dropzone, or the
       same !important will beat this rule too.
     `#dropArea{text-align:center;cursor:pointer}` are the two declarations that do
       apply, and both are reproduced below.
     `#fileInput{display:none}` / `#fileInput2{display:none}` are pure duplication:
       every one of the eight inputs already carries an inline `style="display: none;"`,
       which outranks any stylesheet. Stage 7 established this and did NOT swap them
       for .sm-visually-hidden — that would put an unlabelled file input back in the
       tab order for no gain. Keep the inline declaration; it is state.
     `#imageList` / `#imageList2` (+ their `li`) are the thumbnail strip, and they
       are the reason __list exists.
     `.preview-image` is the 250px thumbnail cap, and it is JS-OWNED — the script
       writes `img.classList.add('img-thumbnail','mx-2','my-2','preview-image')` — so
       the name cannot be swapped from markup. It is bridged below under its own name,
       the same call section 4 makes for `.is-invalid`, `.unmapped-select` and
       `.mapping-warning`.

   `border-primary` STAYS ON THE JS SIDE. The drop handlers add and remove it on
   dragover/drop, and Bootstrap paints it `!important`, so nothing here can or should
   contest it — it lands as a colour change on the border width and style .sm-dropzone
   supplies, which is more visible than it is today, not less.

   NO `display` ANYWHERE ON .sm-dropzone, on purpose. In ApplyToAvailableUnit and
   ApplicationTemplate the zone sits inside `#commercialModel` / `#residentialModel`,
   which are driven by jQuery .show()/.hide() — trap 2 from section 12. The zone is a
   <div> and needs no display of its own. __list is a <ul> that no script ever toggles
   (verified across all four views), so its `display:flex` is safe.
   ------------------------------------------------------------------------ */
/* The zone is an upload tool (1.1), so its dashed edge is the field's --sm-field-border, 4.59:1
   on the card (the white-16% dash was 1.54:1), and steps up to --sm-field-border-hover (7.99:1)
   under the pointer, where it used to turn --brand at 2.34:1. The fill stays --surface-field: a
   drop target reads as a place, like a field. Bootstrap's `.border-primary !important`, the
   drag-over colour the drop handlers toggle, still lands uncontested. */
.sm-app .sm-dropzone{
  padding:var(--space-4);
  text-align:center;
  color:var(--text-secondary);
  background-color:var(--surface-field);
  border:var(--border-emphasis) dashed var(--sm-field-border);
  border-radius:var(--radius-md);
  cursor:pointer;
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard),
    color var(--sm-duration-fast) var(--ease-standard);
}
.sm-app .sm-dropzone:hover{
  border-color:var(--sm-field-border-hover);
  color:var(--text);
}
/* The zone is click-to-open as well as drop-to-upload, so it takes a focus ring, and its edge
   turns --brand-text with it, as a focused pager link's does. The CSS supplies the ring; it cannot
   supply the focusability — see the settled convention on interactive non-native elements. */
.sm-app .sm-dropzone:focus-visible{
  outline:var(--sm-focus-outline);
  border-color:var(--brand-text);
  box-shadow:var(--focus-ring);
}

/* The thumbnail strip. `gap` replaces the shipped `#imageList li{margin-right:1rem}`,
   which put a trailing margin after the last item; the <li> carries no class (the
   script creates it with only a data-image-id), so the item rule has to reach it as
   an element, the way 13.3 reaches the Leaflet legend's injected <i>. The item no
   longer shows a pointer: it promised a click-to-enlarge that the views' own
   `listItem.innerHTML +=` destroys (the image's listener does not survive it).

   Scrollbar treatment matches .sm-table-scroll's, and for the same reason: the
   platform default paints a light track, which reads as a white bar under a dark card. */
.sm-app .sm-dropzone__list{
  display:flex;
  flex-wrap:nowrap;
  gap:var(--space-4);
  margin:0;
  padding:0;
  list-style:none;
  overflow-x:auto;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:thin;
  scrollbar-color:var(--border-strong) transparent;
}
.sm-app .sm-dropzone__list::-webkit-scrollbar{height:var(--space-2)}
.sm-app .sm-dropzone__list::-webkit-scrollbar-track{background:transparent}
.sm-app .sm-dropzone__list::-webkit-scrollbar-thumb{
  background:var(--border-strong);
  border-radius:var(--radius-pill);
}
.sm-app .sm-dropzone__list::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
.sm-app .sm-dropzone__list > li{
  flex:0 0 auto;
}
/* The strip's margin:0 above (0,2,0) also cancels the stack rhythm it sits in (`.sm-stack-3 > * + *`,
   (0,1,0)), so once AddUnitPhotos' previews stopped carrying my-2 the thumbnails touched the drop
   zone's dashed border (W4-ops-b-2). A strip that holds thumbnails takes the 12px rhythm back; an
   empty one (the markup ships `<ul …></ul>`, and the script only appends and removes <li>s) keeps 0,
   so no blank band shows before the first upload. ApplyToAvailableUnit's and
   Tenant/ViewTenantApplication's scripts still give each preview Bootstrap's my-2 (8px, !important),
   so there the strip adds only the other 4px: 12px in all four views. (0,3,0) and (0,4,1). */
.sm-app .sm-dropzone__list:not(:empty){ margin-top:var(--space-3); }
.sm-app .sm-dropzone__list:not(:empty):has(> li > .my-2){ margin-top:calc(var(--space-3) - var(--space-2)); }

/* JS-owned name, bridged rather than renamed. 256px is the nearest step on the space
   scale to the shipped 250px; both axes are capped, so the thumbnail keeps its aspect
   ratio whichever way the source image is oriented. The corner is the input radius:
   AddUnitPhotos' script no longer adds `.img-thumbnail`, whose frame was the preview's
   only rounding. ApplyToAvailableUnit's and Tenant/ViewTenantApplication's scripts still
   add it, and there (0,2,0) replaces its 6px (.375rem) radius and clears its
   `max-width:100%`. */
.sm-app .preview-image{
  max-width:calc(var(--space-32) * 2);
  max-height:calc(var(--space-32) * 2);
  border-radius:var(--radius-md);
}

/* --- 17.3 Carousel figure ------------------------------------------------
   One figure for every carousel slide, the shape 17.0 used to defer. Building/AddUnitPhotos'
   400px .c-item with object-fit:cover cropped floor plans (a 16:9 plan showed about 46% of its
   width at 375), and Building/ViewUnitApplication's base64 documents set each slide's height by
   whatever the applicant uploaded.
     <div class="carousel-item [active]">
       <div class="sm-carousel-figure"><img src="…" alt="…"></div>
     </div>
   The figure is a NEW wrapper inside the slide, never a class on .carousel-item: Bootstrap shows
   and hides slides by toggling .active and .carousel-item-next/-prev against its own
   `.carousel-item{display:none}`, and nothing here declares `display`. A 16:10 box, capped at 60vh
   and 512px, with the sunken fill round a CONTAINED image, so a whole plan or page is always on
   screen, letterboxed rather than cropped. The img may keep d-block and w-100; they agree.

   CONTROLS, on every .sm-app carousel (these two, Tenant/ViewTenantApplication, and the page and
   modal carousels of the public unit page, Building/ViewAvailableUnit). Bootstrap's chevrons are
   white SVGs at .5 opacity that all but vanished on a light photo or plan, and the dark halo they
   wore after that still gave the control no edge. They are tools (1.1), so each chevron now sits
   on the neutral-rimmed disc of the public screenshots' arrows (marketing.css 11.4), drawn with
   0.5's neutral edge: a 40px --ink-a64 disc inside a --sm-field-border rim, the chevron white at
   16px. The disc parts the glyph from light slide pixels and the rim from dark ones (4.82:1 on
   the canvas, 4.91:1 on the figure's sunken letterbox, 4.07:1 on white); under the pointer the
   disc goes opaque (--ink-950) and the rim takes --sm-field-border-hover. The white glyph is
   5.99:1 on the disc over white and 19.61:1 on the opaque one. Over real slides at 1280, 375 and
   320px, the share of pixels round a disc that meet 3:1 against its rim or its disc depends on
   the photo: 93.1% at rest on one sample set (screenshots, photos, a letterboxed portrait) and
   79.1% on another, with single discs as low as 30-40% over mid-tone photos (a brick facade,
   foliage); 91-100% under the pointer. Round the halo it replaces, sampled the same way, 4.8-14.6%
   did. Where the ring is weak, the white glyph on its own disc, 5.97:1 or more, is what marks
   the control, so a carousel control is judged by its glyph on the disc (an exception listed in
   DESIGN-SYSTEM-BRIEF), not by the disc's edge against the photo.
   The disc is the ICON, not the control: the control keeps Bootstrap's full-height strip, 15% of
   the carousel wide, as its hit area, now at full opacity. When that strip is narrower than the
   disc and its ring (48px, a carousel under 320px wide: a card's carousel on a phone, the gallery
   on a 320px screen) the disc shrinks to fit, so it never overhangs the slide and the ring stays
   clear of the gallery 19.2 clips. Keyboard focus only raised Bootstrap's opacity to .9: its
   `:focus{outline:0}` takes the outline away. The focused control shows the product ring round
   the disc, a --surface-canvas band and then --purple-300, so one band stands out on any photo
   (the dark band 19.61:1 on white, the purple one 8.87:1 on black). Forced colours get the
   outline, inset on the control; the disc opts out of them, as the public arrows do (marketing.css
   DA-D4), because a forced Canvas disc at 64% would put the white glyph on near-white in a light
   palette, and opting out keeps its ring too. (0,2,0) at rest, (0,3,0) and (0,4,0) on focus and
   hover, against Bootstrap's (0,1,0) and (0,2,0): the control's full opacity outranks Bootstrap's
   .5, and ties its .9 on :hover and :focus and wins by order (every layout links this file after
   Bootstrap). The rules match the controls whether they are <button>s or <a role="button">s;
   only a <button> answers Space natively.

   WIDE CAROUSELS. The figure's width is auto, so its max-height passes through the 16:10 ratio into
   a max-width of 1.6 x min(60vh, 512px): 819px at a 900px-tall window. From a carousel of about that
   width (an app viewport near 1180px) the figure stopped filling it and sat at the start, 108-318px
   short of the end: Next and its focus ring floated over bare card while Prev sat on the image, and
   the indicators centred on the carousel, not on the image (W4-ops-b-1, W4-tenants-2). The figure
   now centres itself (margin-inline:auto), and a carousel whose slides are figures is capped at
   that same width and centred, so the figure fills it again and both controls, their focus rings
   and the indicators land on the image at every width, in the same place on both sides. Below the
   cap nothing changes. The cap uses the figure's own expression, so the two cannot drift apart. No
   `display`: AddUnitPhotos' script hides an emptied carousel with jQuery, and Bootstrap owns every
   slide's display. (0,2,0) and (0,5,0). */
.sm-app .sm-carousel-figure{
  aspect-ratio:16 / 10;
  max-height:min(60vh, 32rem);
  margin-inline:auto;
  background-color:var(--surface-sunken);
}
.sm-app .carousel:has(> .carousel-inner > .carousel-item > .sm-carousel-figure){
  max-width:calc(min(60vh, 32rem) * 16 / 10);
  margin-inline:auto;
}
.sm-app .sm-carousel-figure > img{
  width:100%;
  height:100%;
  object-fit:contain;
}
.sm-app :is(.carousel-control-prev, .carousel-control-next){
  opacity:1;
}
.sm-app :is(.carousel-control-prev-icon, .carousel-control-next-icon){
  width:min(var(--space-10), 100% - var(--sm-focus-width) * 4);
  height:auto;
  aspect-ratio:1;
  background-color:var(--ink-a64);
  background-size:var(--space-4) var(--space-4);
  border:var(--border-hairline) solid var(--sm-field-border);
  border-radius:var(--radius-circle);
  transition:
    background-color var(--sm-duration-fast) var(--ease-standard),
    border-color var(--sm-duration-fast) var(--ease-standard);
}
.sm-app :is(.carousel-control-prev, .carousel-control-next):hover > :is(.carousel-control-prev-icon, .carousel-control-next-icon){
  background-color:var(--ink-950);
  border-color:var(--sm-field-border-hover);
}
.sm-app :is(.carousel-control-prev, .carousel-control-next):focus-visible{
  outline:var(--sm-focus-outline);
  outline-offset:var(--sm-focus-offset-inset);
}
.sm-app :is(.carousel-control-prev, .carousel-control-next):focus-visible > :is(.carousel-control-prev-icon, .carousel-control-next-icon){
  box-shadow:var(--focus-ring);
}
@media (forced-colors: active){
  .sm-app :is(.carousel-control-prev-icon, .carousel-control-next-icon){
    forced-color-adjust:none;
  }
}

/* --- 17.4 Flagged answer -------------------------------------------------
   Building/ViewUnitApplication marks a Yes to a screening question ("Has this business ever been
   sued for rent?") with `concerning-question`, which its Razor helper ConcerningClass() writes onto
   the question's .sm-field__label. The name is the helper's, so it is bridged here under that name
   rather than renamed (section 4's .is-invalid precedent) and the view-local rule goes. Bold as
   well as --danger-text, so the flag is not carried by colour alone: 6.48:1 on --surface, 6.20:1
   on --surface-raised, 5.81:1 on --surface-overlay. (0,3,0) against the label's (0,1,0). */
.sm-app .sm-field__label.concerning-question{
  color:var(--danger-text);
  font-weight:var(--weight-bold);
}

/* --- 17.5 bootstrap-multiselect bridge -----------------------------------
   Building/AddUnitLease and AddSupplementalDocuments turn #inputTenant into bootstrap-multiselect's
   widget, which the plugin builds in script, so its names are bridged, not renamed:
     div.btn-group-multi > button.multiselect.dropdown-toggle.btn.btn-default
                         + ul.multiselect-container.dropdown-menu > li > a > label > input
   MultiSelectCSS.css, linked in those views AFTER this file, paints the toggle black with a white
   border, `border-radius:5px 5%`, 52px tall and calc(100% - 150px) wide from 1025px; the menu
   black and 100px wide; its items #000 (invisible on the dark menu), "Select all" in gray (4.23:1)
   and each box in a 20% white border (1.91:1). Every declaration below outranks the rule it
   answers on specificity, so the load order cannot matter:
     toggle   (0,4,0), and (0,5,0) for its states, against `.multiselect` (0,1,0),
              `.multiselect:hover` (0,2,0), Bootstrap's `.btn:hover` and `.btn:focus-visible`
              (0,2,0) and `.btn:first-child:active` (0,3,0)
     menu     (0,3,0) against `.multiselect-container` (0,1,0) and `.dropdown-menu` (0,1,0)
     items    (0,3,2) to (0,4,3) against (0,1,2) to (0,2,4)
   The menu keeps NO `display`: the Bootstrap 4 dropdown plugin these views load for the widget
   toggles .show, and 5.2.2's `.dropdown-menu.show{display:block}` does the rest. The toggle's flex
   is safe, since no script shows or hides it. The wrapper becomes the menu's positioning box, so
   the menu can take the field's width. The menu's colours are 12.9's --bs-dropdown-* mirror, which
   it carries as a .dropdown-menu: --text on --surface-overlay 15.24:1, the --ink-400 box border
   4.11:1 there, and the toggle's --ink-400 border 4.69:1 on its field. */
.sm-app .btn-group-multi{ position:relative; }
.sm-app .btn-group-multi > .multiselect.btn{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:var(--space-2);
  width:100%;
  height:var(--control-height);
  padding:0 var(--space-3);
  font:inherit;
  font-size:var(--text-base);
  line-height:var(--leading-tight);
  color:var(--text);
  text-align:start;
  background-color:var(--surface-field);
  border:var(--border-hairline) solid var(--sm-field-border);
  border-radius:var(--radius-md);
  box-shadow:none;
}
.sm-app .btn-group-multi > .multiselect.btn:hover{
  color:var(--text);
  background-color:var(--surface-field);
  border-color:var(--sm-field-border-hover);
}
.sm-app .btn-group-multi > .multiselect.btn:focus-visible{
  outline:var(--sm-focus-outline);
  border-color:var(--brand-text);
  box-shadow:var(--focus-ring);
}
/* The plugin's summary ("Tenant A, Tenant B…", or "N selected"): one line, ellipsised inside the
   40px toggle. The plugin puts the full list in the button's title, and the menu lists every item. */
.sm-app .btn-group-multi > .multiselect.btn > .multiselect-selected-text{
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.sm-app .btn-group-multi > .multiselect-container{
  width:100%;
  margin-top:var(--space-1);
  padding:var(--space-2) 0;
  color:var(--bs-dropdown-color);
  background-color:var(--bs-dropdown-bg);
  border:var(--border-hairline) solid var(--bs-dropdown-border-color);
  border-radius:var(--bs-dropdown-border-radius);
}
.sm-app .btn-group-multi > .multiselect-container > li > a{
  color:var(--text);
  text-decoration:none;
}
.sm-app .btn-group-multi > .multiselect-container > li > a:hover{ background-color:var(--surface-hover); }
/* Items are full-bleed rows in the menu, so the ring is inset, as for 12.9's .dropdown-item. */
.sm-app .btn-group-multi > .multiselect-container > li > a:focus-visible{
  outline:var(--sm-focus-outline);
  outline-offset:var(--sm-focus-offset-inset);
  box-shadow:var(--sm-focus-ring-inset);
}
.sm-app .btn-group-multi > .multiselect-container > li > a.multiselect-all label{ color:var(--text); }
.sm-app .btn-group-multi > .multiselect-container > li > a > label::before{ border-color:var(--sm-field-border); }

/* ============================================================================
   18. Feedback — busy controls, spinners, skeletons, busy regions and toasts

   What the product shows while it waits, and its one transient notice. None of
   it was in the vocabulary: the design CSS had no @keyframes and no busy,
   spinner, skeleton or toast selector (Phase 0 audit, section 5).

     18.1 Busy button   the "Processing…" state 34 app views already produce
     18.2 Spinner       .sm-spinner, and Bootstrap's .spinner-border made to match
     18.3 Skeleton      .sm-skeleton placeholders and a table-row helper
     18.4 Busy region   .sm-table-wrap / .sm-card while aria-busy="true", and
                        the .sm-map frame while its data loads
     18.5 Toast         .sm-toast in a .sm-toast-region, driven by
                        bootstrap.Toast, and a .toast mirror

   Scope: global, like sections 1-7. These are components, and a busy button
   means the same in the entry flow as in the app. The Bootstrap mirrors are
   .sm-app-scoped, like 12.9's.

   MOTION. Two keyframes, both transform-only, which is all motion.css allows,
   and they differ on purpose. A spinner turns unless the reader has asked for
   reduced motion, and then it stands still: a ring with a gap still reads as
   "working", and the label beside it says so in words. The skeleton shimmer is
   the other way round, declared ONLY under `prefers-reduced-motion:
   no-preference`, so a static tint is the default and the sweep is opt-in.
   Font Awesome's .fa-spin and Bootstrap's .fade honour reduced motion on their
   own; Bootstrap's .spinner-border only slows down, so 18.2 stops it.

   DISPLAY. .sm-spinner and .sm-skeleton declare `display` (a ring and a bar
   need a box), so section 12's second trap applies to them in reverse: never
   jQuery .toggle()/.show()/.hide() one. Remove it, or toggle the `hidden`
   attribute (Bootstrap's reboot makes it display:none !important) or .d-none.
   .sm-toast keeps its `display` at (0,1,0) on purpose, so that Bootstrap can
   hide it; see 18.5.
   ========================================================================= */
:root{
  /* One turn of a spinner: Bootstrap's own .spinner-border speed, so the two
     kinds of ring turn together. One pass of the skeleton highlight. Named here,
     as 14.3 names its blur radius, because motion.css has no step for a loop. */
  --sm-spinner-duration:0.75s;
  --sm-shimmer-duration:1.6s;
}
@keyframes sm-spin{
  to{ transform:rotate(360deg); }
}
@keyframes sm-shimmer{
  from{ transform:translateX(-100%); }
  to{ transform:translateX(100%); }
}

/* --- 18.1 Busy button -----------------------------------------------------
   The pattern in 34 app views: on submit, the handler disables the control and
   swaps its label,
     $btn.prop('disabled', true)
         .html('Processing... <i class="fa-solid fa-spinner fa-spin"></i>');
   (or a Bootstrap .spinner-border in place of the icon, or, on the anchors of
   17.1, aria-disabled and .disabled in place of `disabled`). Section 1 then dims
   it to .45 like any unavailable control, which put the "Processing…" the user
   has to read at 3.34:1 on a primary button. A disabled control holding a
   spinner is not unavailable, it is WORKING, so it gets its full colour back
   and a progress cursor. No script changes: the spinner each handler already
   injects is the signal. aria-busy="true" counts too, spinner or not
   (ViewWorkRequests in Tenant and Building, EditBuildingDetails and
   ApplyToAvailableUnit set it on their submits).

   Weight: :is() takes its heaviest branch, (0,3,0) with the :has(), so the rule
   is (0,4,0), above every section 1 disabled selector (0,2,0) and after them in
   source. A spinner still carrying .d-none or [hidden] does not count
   (Building/ViewUnitPaymentForPeriod renders one in advance and reveals it). A
   browser without :has() drops that branch, since :is() forgives it, and keeps
   the aria-busy one. A disabled busy button keeps section 1's hover and press
   guard, which keys off `disabled` and aria-disabled.

   The spinner is sized to the label: 1em square, whichever kind it is, so it
   turns about its own centre and centres on the text in the button's flex row.
   ------------------------------------------------------------------------ */
.sm-btn:is([aria-busy="true"], :is(:disabled, [aria-disabled="true"], .sm-is-disabled, .disabled):has(:is(.fa-spin, .spinner-border, .sm-spinner):not(.d-none, [hidden]))){
  opacity:1;
  cursor:progress;
}
.sm-btn .fa-spin{
  flex:0 0 auto;
  width:1em;
  height:1em;
  line-height:1;
  text-align:center;
}
.sm-btn .spinner-border{
  --bs-spinner-width:1em;
  --bs-spinner-height:1em;
  flex:0 0 auto;
}
.sm-btn .sm-spinner{ --sm-spinner-size:1em; }

/* A busy button whose script swaps the label but injects no spinner (User/UpdateCard's Stripe
   step, User/ViewPermissionProfile's "Processing...") draws the ring itself, 1em after the label,
   from 18.2's shared ring rule; only `content` switches it on, so the ring rule alone draws
   nothing. The visible-spinner test is the one above. A browser without :has() drops this rule and
   shows the label alone, as before. (Login's "Signing in…" is the entry-form case below: its
   script marks the form busy, not the button.) */
.sm-btn[aria-busy="true"]:not(:has(:is(.fa-spin, .spinner-border, .sm-spinner):not(.d-none, [hidden])))::after{
  --sm-spinner-size:1em;
  content:"";
  flex:0 0 auto;
}

/* The entry flow's two reCAPTCHA submits, Login's "Signing in…" and CreateUser's "Creating
   account…". Their frozen handlers disable the button and swap its label but set aria-busy on the
   FORM (#loginForm, #createUserForm), so the rules above never reached them and the label read at
   .45, about 2.25:1. A disabled .sm-btn in a busy entry form is working, not unavailable: full
   colour (white on --brand, 7.98:1), a progress cursor, and the same 1em ring after the label (18.2
   lists it). The handlers' reset paths already take the form's aria-busy off (resetSignInButton,
   resetRegistrationButton, pageshow). (0,4,1), above section 1's .sm-btn:disabled (0,2,0) and
   section 11's --on-light disabled cursor (0,2,0). */
.sm-auth__panel form[aria-busy="true"] .sm-btn:disabled{
  opacity:1;
  cursor:progress;
}
.sm-auth__panel form[aria-busy="true"] .sm-btn:disabled:not(:has(:is(.fa-spin, .spinner-border, .sm-spinner):not(.d-none, [hidden])))::after{
  --sm-spinner-size:1em;
  content:"";
  flex:0 0 auto;
}

/* --- 18.2 Spinner --------------------------------------------------------
   A ring in the current text colour with a gap in it, turning. 1em by default,
   so it sits in a line of text or a button at the text's own size; set
   --sm-spinner-size on it or on an ancestor for another size, or use --lg
   (32px) in a panel. The stroke is an eighth of the size, the ratio of
   Bootstrap's default .spinner-border, and never under 2px: browsers snap a
   border width down to whole device pixels, so a 1em ring's 1.9px would draw
   at 1px on a standard display.

   Beside words that already say what is happening ("Processing…"), the ring is
   decoration: aria-hidden="true". Standing alone it needs a name: role="status"
   on the ring or its wrapper, with a .sm-visually-hidden "Loading…" inside.

   18.4's busy regions and map, and 18.1's spinner-less busy button and busy entry-form
   submit, draw the same ring on their ::after, so they share the first rule.

   The mirror: .spinner-border (24 elements in 5 views) keeps Bootstrap's size
   classes but takes the same stroke ratio and speed, and stands still under
   reduced motion instead of Bootstrap's 1.5s crawl. Bootstrap declares its
   --bs-spinner-* on the element itself, at (0,1,0); (0,2,0) wins.
   ------------------------------------------------------------------------ */
.sm-spinner,
.sm-table-wrap[aria-busy="true"]::after,
.sm-card[aria-busy="true"]::after,
.sm-app .sm-map[aria-busy="true"]::after,
.sm-btn[aria-busy="true"]::after,
.sm-auth__panel form[aria-busy="true"] .sm-btn:disabled::after{
  box-sizing:border-box;
  width:var(--sm-spinner-size, 1em);
  height:var(--sm-spinner-size, 1em);
  border:max(2px, calc(var(--sm-spinner-size, 1em) / 8)) solid currentColor;
  border-inline-end-color:transparent;
  border-radius:var(--radius-circle);
  animation:sm-spin var(--sm-spinner-duration) linear infinite;
}
.sm-spinner{
  display:inline-block;
  flex:0 0 auto;
  vertical-align:-0.125em;
}
.sm-spinner--lg{ --sm-spinner-size:var(--space-8); }

.sm-app .spinner-border{
  --bs-spinner-border-width:max(2px, calc(var(--bs-spinner-width) / 8));
  --bs-spinner-animation-speed:var(--sm-spinner-duration);
}

@media (prefers-reduced-motion: reduce){
  .sm-spinner,
  .sm-table-wrap[aria-busy="true"]::after,
  .sm-card[aria-busy="true"]::after,
  .sm-app .sm-map[aria-busy="true"]::after,
  .sm-btn[aria-busy="true"]::after,
  .sm-auth__panel form[aria-busy="true"] .sm-btn:disabled::after{ animation:none; }
  .sm-app .spinner-border{ --bs-spinner-animation-name:none; }
}

/* --- 18.3 Skeleton -------------------------------------------------------
   Placeholders for content on its way: the first load of a table, a card or a
   chart fed by a request, drawn where the content will be and at its size, so
   nothing jumps when it lands. For later adoption; no view has one yet.

   The fill is translucent white (--neutral-subtle, 6%), so one skeleton reads
   on --surface, --surface-raised and --surface-overlay alike. A light scope
   restates --sm-skeleton-fill and --sm-skeleton-shine on an ancestor; both are
   read with a fallback, so the ancestor's value inherits in.

     --text    one line of text, 0.75em tall inside a line box as tall as a
               --leading-normal line, so a stack of them fills exactly the lines
               it stands in for. The last of several stops at 60%.
     --title   a heading line: 1.25em tall, 40% wide.
     --block   a chart, an image or a tile: 96px unless --sm-skeleton-height
               says otherwise, and it fills a .sm-chart__canvas box outright.
     --circle  an avatar or medallion: --sm-skeleton-size, 40px like .sm-avatar.
     .sm-skeleton-row
               on a <tr> in a .sm-table body, with one .sm-skeleton--text per
               cell. The widths vary by column so the rows do not read as a grid
               of bars, a .sm-num cell right-aligns its bar, and the row takes no
               pointer events, so it never picks up the row hover.
   Any of them takes --sm-skeleton-width / --sm-skeleton-height from a context
   rule.

   ARIA. A skeleton is decoration: aria-hidden="true" on each one, or on the
   wrapper of a group (the whole <tr> for a row). The WAIT is told on the region
   being filled instead: aria-busy="true" on the table or list, removed when the
   content lands, plus a role="status" element holding a .sm-visually-hidden
   "Loading…" where the wait is long enough to matter. Put that aria-busy on the
   <table> or its <tbody>, not on a .sm-table-wrap or .sm-card: on those it is
   18.4's switch, and would lay a spinner over the skeletons.
   ------------------------------------------------------------------------ */
.sm-skeleton{
  position:relative;
  display:block;
  width:var(--sm-skeleton-width, 100%);
  max-width:100%;
  height:var(--sm-skeleton-height, 1em);
  margin:0;
  overflow:hidden;
  background-color:var(--sm-skeleton-fill, var(--neutral-subtle));
  border-radius:var(--radius-xs);
  pointer-events:none;
  user-select:none;
}
.sm-skeleton--text{
  --sm-skeleton-height:0.75em;
  margin-block:calc((var(--leading-normal) * 1em - 0.75em) / 2);
}
.sm-skeleton--text:last-child:not(:first-child){ --sm-skeleton-width:60%; }
.sm-skeleton--title{
  --sm-skeleton-width:40%;
  --sm-skeleton-height:1.25em;
  border-radius:var(--radius-sm);
}
.sm-skeleton--block{
  --sm-skeleton-height:var(--space-24);
  border-radius:var(--radius-md);
}
.sm-skeleton--circle{
  --sm-skeleton-width:var(--sm-skeleton-size, var(--space-10));
  --sm-skeleton-height:var(--sm-skeleton-size, var(--space-10));
  flex:0 0 auto;
  border-radius:var(--radius-circle);
}
.sm-app .sm-chart__canvas > .sm-skeleton--block{ --sm-skeleton-height:100%; }

.sm-skeleton-row{ pointer-events:none; }
.sm-skeleton-row > :is(td, th) > .sm-skeleton{ --sm-skeleton-width:70%; }
.sm-skeleton-row > :is(td, th):nth-child(3n+2) > .sm-skeleton{ --sm-skeleton-width:45%; }
.sm-skeleton-row > :is(td, th):nth-child(3n) > .sm-skeleton{ --sm-skeleton-width:85%; }
.sm-skeleton-row > .sm-num > .sm-skeleton{ margin-inline-start:auto; }

@media (prefers-reduced-motion: no-preference){
  .sm-skeleton::after{
    content:"";
    position:absolute;
    inset:0;
    background-image:linear-gradient(90deg, transparent, var(--sm-skeleton-shine, var(--surface-hover)), transparent);
    transform:translateX(-100%);
    animation:sm-shimmer var(--sm-shimmer-duration) var(--ease-standard) infinite;
  }
}

/* --- 18.4 Busy region ----------------------------------------------------
   A table or card whose content is being replaced (a refresh, a filter, the
   next page) while what is on screen stays put:
     <div class="sm-table-wrap" aria-busy="true">…</div>
   aria-busy is both the switch and the semantics: assistive technology may hold
   a busy region's changes back until it clears, so the script sets it before
   the request and removes it after. While it is set the content dims to 40%
   (stale, not gone) and takes no pointer events, and a 32px ring in
   --brand-text (7.88:1 on --surface) turns over it: centred in a short box, and
   never more than 128px down in a tall one, so it stays above the fold of a
   long table.

   The content steps back through its own opacity rather than under a scrim, so
   the surface keeps its colour and a light scope has nothing to restate.
   pointer-events stops the mouse only; a keyboard can still reach a control
   inside, so a script that must lock the region sets `inert` on it as well.
   isolation keeps the ring's z-index inside the box.

   A first load, with nothing on screen to dim, wants 18.3's skeletons instead.
   DataTables' own processing indicator (section 6) is the third option, for a
   table that loads through DataTables; do not stack them.
   ------------------------------------------------------------------------ */
.sm-table-wrap[aria-busy="true"],
.sm-card[aria-busy="true"]{
  position:relative;
  isolation:isolate;
  cursor:progress;
}
.sm-table-wrap[aria-busy="true"] > *,
.sm-card[aria-busy="true"] > *{
  opacity:.4;
  pointer-events:none;
}
.sm-table-wrap[aria-busy="true"]::after,
.sm-card[aria-busy="true"]::after{
  --sm-spinner-size:var(--space-8);
  content:"";
  position:absolute;
  top:min(50%, var(--space-32));
  left:50%;
  z-index:1;
  margin:calc(var(--sm-spinner-size) / -2) 0 0 calc(var(--sm-spinner-size) / -2);
  color:var(--brand-text);
}

/* The map frame (13.3) while its data loads: <div id="map" class="sm-map" aria-busy="true">,
   cleared by the script once the GeoJSON is drawn (Admin/ViewAdminDashboard; Owner/Overview can
   adopt it). The same ring, centred, over a map that is not dimmed: until then the frame is empty
   or holds bare tiles. z-index 1000 clears Leaflet's panes (up to 700) and ties with its controls,
   which the ring follows in tree order, so it paints over them; .sm-map's isolation keeps that
   inside the frame. The ring takes no pointer events, so the map still drags through it.
   NO `position` here, deliberately. Leaflet writes an inline position:relative on the container
   only when the computed position is static, so a stylesheet `position` on the busy state would
   make it skip that write, and the map would lose its positioning when aria-busy cleared. */
.sm-app .sm-map[aria-busy="true"]{ cursor:progress; }
.sm-app .sm-map[aria-busy="true"]::after{
  --sm-spinner-size:var(--space-8);
  content:"";
  position:absolute;
  top:50%;
  left:50%;
  z-index:1000;
  margin:calc(var(--sm-spinner-size) / -2) 0 0 calc(var(--sm-spinner-size) / -2);
  color:var(--brand-text);
  pointer-events:none;
}

/* --- 18.5 Toast ----------------------------------------------------------
   A transient notice for the outcome of an action taken without leaving the
   page. TempData notices after a redirect stay inline, as the
   .sm-alert.sm-shell-notice of 8.5. For later adoption: the first users are the
   12 app views that answer with a native alert() (27 calls), and replacing
   those is script work in each view.

     <div class="sm-toast-region" aria-live="polite">   in the page from load, empty
       <div class="toast sm-toast sm-toast--success" role="status" aria-atomic="true">
         <i class="fa-solid fa-circle-check sm-toast__icon" aria-hidden="true"></i>
         <div class="sm-toast__content">
           <p class="sm-toast__title">…</p>
           <p class="sm-toast__body">…</p>
         </div>
         <button type="button" class="btn-close btn-close-white sm-toast__close"
                 data-bs-dismiss="toast" aria-label="Close"></button>
       </div>
     </div>
     bootstrap.Toast.getOrCreateInstance(toastElement).show();

   ARIA, after Bootstrap's own guidance. The region is the live region, and it
   has to be in the page before a toast is added to it: a live region created
   together with its content is not announced. Routine news is role="status"
   (polite). A failure the user must act on is role="alert" (assertive) with
   data-bs-autohide="false", so it stays until dismissed; an error that times
   out may never have been read. An auto-hiding toast holds no control but its
   close button, and its delay should outlast its text (Bootstrap's 5s is the
   floor). The title is optional when the body says it all.

   BOOTSTRAP. `.toast` is the plugin's hook, and Bootstrap hides a toast that is
   not showing with `.toast:not(.show){display:none}` (0,2,0). .sm-toast's own
   display:flex is therefore declared at (0,1,0) and has to stay there:
   restated at (0,2,0), after Bootstrap, it would win that tie and a dismissed
   toast could never go (the .collapse trap of section 12, again). The paint is
   doubled to (0,2,0) instead, so it beats `.toast` whatever the load order.
   Bootstrap's .fade drives the opacity; the doubled rule retimes it from 150ms
   to --sm-duration-base (220ms) and adds an 8px rise over the same span, both
   on section 0's reduced-motion aliases. The plugin reads that duration back
   to time its callbacks, so the two stay in step.

   The region is fixed to the bottom-right corner, in Bootstrap's toast layer
   (1090: above modals and tooltips, under the skip link's 1100). Below 768px it
   spans the screen between 16px gutters, clear of the home indicator. It takes
   no pointer events itself, so it never blocks the page beneath; its toasts do.

   The accent is a 4px bar on the leading edge in the status's text step, the
   .sm-alert palette (5.81-9.2:1 on --surface-overlay), drawn as an inset shadow
   so it follows the corner radius. Physical left, like the rail's divider:
   this shell is LTR. The close button is `btn-close btn-close-white`, the
   product's one close idiom, so 12.9's focus-ring handling applies; here it is
   sized to the 24px minimum target and centred on the title's line.

   A plain Bootstrap toast (.toast-header / .toast-body markup) under .sm-app
   takes the same surface from the variable mirror at the end, instead of
   Bootstrap's white card at 85%.
   ------------------------------------------------------------------------ */
.sm-toast-region{
  --sm-toast-width:24rem;
  --sm-toast-z:1090;
  position:fixed;
  inset-block-end:var(--space-6);
  inset-inline-end:var(--space-6);
  z-index:var(--sm-toast-z);
  display:flex;
  flex-direction:column;
  gap:var(--space-3);
  width:min(var(--sm-toast-width), 100% - var(--space-6) * 2);
  pointer-events:none;
}
.sm-toast-region > *{ pointer-events:auto; }
@media (max-width:767.98px){
  .sm-toast-region{
    inset-inline:var(--space-4);
    inset-block-end:calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
    width:auto;
  }
}

.sm-toast{
  --sm-toast-accent:var(--text-secondary);
  display:flex;
}
.sm-toast--success{ --sm-toast-accent:var(--success-text); }
.sm-toast--warning{ --sm-toast-accent:var(--warning-text); }
.sm-toast--danger{ --sm-toast-accent:var(--danger-text); }
.sm-toast--info{ --sm-toast-accent:var(--info-text); }
.sm-toast.sm-toast{
  --text-muted:var(--text-secondary);
  position:relative;
  align-items:flex-start;
  gap:var(--space-3);
  width:100%;
  max-width:100%;
  margin:0;
  padding:var(--space-3) var(--space-4) var(--space-3) calc(var(--space-4) + var(--space-1));
  font-family:var(--font-sans);
  font-size:var(--text-sm);
  line-height:var(--leading-snug);
  color:var(--text);
  background-color:var(--surface-overlay);
  background-clip:padding-box;
  border:var(--border-hairline) solid var(--border);
  border-radius:var(--radius-md);
  box-shadow:inset var(--space-1) 0 0 var(--sm-toast-accent), var(--elev-dark-3);
  pointer-events:auto;
  transition:
    opacity var(--sm-duration-base) var(--ease-standard),
    transform var(--sm-duration-base) var(--ease-standard);
}
.sm-toast.showing{ transform:translateY(var(--space-2)); }
.sm-toast__icon{
  flex:0 0 auto;
  font-size:var(--text-base);
  line-height:var(--leading-snug);
  color:var(--sm-toast-accent);
}
.sm-toast__content{
  flex:1 1 auto;
  min-width:0;
}
.sm-toast__title{
  display:block;
  margin:0;
  font-size:var(--text-base);
  font-weight:var(--weight-semibold);
  line-height:var(--leading-snug);
  color:var(--text-strong);
}
.sm-toast__body{
  margin:0;
  color:var(--text-secondary);
}
.sm-toast__title + .sm-toast__body{ margin-top:var(--space-05); }
.sm-toast .sm-toast__close{
  flex:0 0 auto;
  margin:calc((var(--leading-snug) * var(--text-base) - var(--space-6)) / 2) 0 0 auto;
  padding:calc((var(--space-6) - 1em) / 2);
}

.sm-app .toast{
  --bs-toast-color:var(--text);
  --bs-toast-bg:var(--surface-overlay);
  --bs-toast-border-color:var(--border);
  --bs-toast-border-radius:var(--radius-md);
  --bs-toast-box-shadow:var(--elev-dark-3);
  --bs-toast-font-size:var(--text-sm);
  --bs-toast-header-color:var(--text-strong);
  --bs-toast-header-bg:transparent;
  --bs-toast-header-border-color:var(--border-subtle);
  --text-muted:var(--text-secondary);
}

/* Property setup: the same progress state drives the bar and step list. */
.sm-onboarding__progress-label{ display:flex; justify-content:space-between; gap:var(--space-3); margin-bottom:var(--space-3); color:var(--text-secondary); font-size:var(--text-sm); }
.sm-onboarding__steps{ display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:var(--space-3); margin:var(--space-4) 0 0; padding:0; list-style:none; }
.sm-onboarding__step{ display:flex; align-items:center; gap:var(--space-2); padding:var(--space-3); color:var(--text-muted); border:1px solid var(--border-subtle); border-radius:var(--radius-md); font-size:var(--text-sm); }
.sm-onboarding__step--current{ color:var(--brand-text); background:var(--brand-subtle); border-color:var(--brand-text); }
.sm-onboarding__step--complete{ color:var(--success-text); }
/* Forced colours repaint the current step's wash as Canvas and draw every step's border and label in
   one system colour, so the step the owner is on looked like the others (a completed step still has
   its check icon). It takes the system selection pair, as the selected pill does (section 1), and
   opts out with forced-color-adjust:none for the reason given there: Chromium's Canvas backplate
   turned the HighlightText label and icon into solid blocks (Owner/OwnerOnboarding, W4-app-fixes-1).
   The icon and the label are restated as HighlightText rather than left to inherit, so no later
   colour rule can put a non-system colour on the Highlight fill. The step is not focusable, so it
   has no ring to answer for. After the rule it repaints. */
@media (forced-colors: active){
  .sm-onboarding__step--current{ forced-color-adjust:none; background-color:Highlight; border-color:Highlight; color:HighlightText; }
  .sm-onboarding__step--current > *{ color:HighlightText; }
}
/* Side and bottom margins only: a whole margin:0 cancelled the card body's .sm-stack-4, so badge,
   title and text sat flush. The stack spaces them now, and a title and its paragraph pair at 8px. */
.sm-onboarding__title{ margin-inline:0; margin-bottom:0; font-size:var(--text-xl); font-weight:var(--weight-bold); }
.sm-onboarding .sm-card__body > p{ color:var(--text-secondary); margin-inline:0; margin-bottom:0; line-height:1.6; }
.sm-onboarding .sm-card__body > .sm-onboarding__title + p{ margin-top:var(--space-2); }
.sm-onboarding__actions{ display:flex; flex-wrap:wrap; gap:var(--space-3); }
.sm-onboarding__action .sm-btn{ max-width:100%; }
@media (max-width:767.98px){
  .sm-onboarding__steps{ grid-template-columns:repeat(2, minmax(0, 1fr)); }
  .sm-onboarding__action, .sm-onboarding__action .sm-btn{ width:100%; }
  .sm-onboarding__actions{ flex-direction:column; }
}

/* ============================================================================
   19. Public-dark pages: the apply flow, the unavailable page and the error pages (stage 2D)

   ONE scope, `.sm-public-dark`, set beside `.sm-app` on the view root of:
     Building/ViewAvailableUnits, ViewAvailableUnit, ApplyToAvailableUnit, ViewPayApplicationFee,
     ViewAvailableUnitsDisabled, Error/UnexpectedError, AuthorizationError, SessionTimeOut,
     NoApplicationTemplate, and Shared/Error.
   Shape, inside _Layout's <main id="main"> on <body class="sm-site">:
     <div class="sm-app sm-public-dark [namespace]"><div class="sm-page […]">…</div></div>

   Who does what:
   - marketing.css section 2 gives the root, and <body> on any page that holds one, the dark
     aliases back, plus color-scheme:dark. `.sm-site` paints <body> from its own --surface-canvas,
     so the canvas comes out ink with no rule here. The shell (nav, footer, notices) is stage 2A's.
   - app.css 8.1 and 16.1: `.sm-app` sets the product type, text colour and the dark control scheme.
   - Section 7: the error-state medallion tones (.sm-empty__medallion--danger|warning|info|neutral).
   - This section: the phone gutter, the TempData-notice bridge, the error-state text, and the
     three view namespaces. `.sm-public-units` (listing, unit detail), `.sm-application`
     (application form) and `.sm-application-payment` (fee checkout) are component roots only:
     nothing dark keys on them any more. `.sm-application-payment` also stays because the fee
     page's script reads its Stripe theme from that element's computed tokens.

   It replaces available-units.css and application-payment.css, which no view links any more,
   and the "Public application form" block that used to open this file. _Layout loads this file
   after Bootstrap and site.css, and this section comes after every generic section above, so
   each rule here wins its ties by source order. That includes the detail-list split that
   marketing.css section 10 had to bridge, so section 10 is deleted in the same change.

   Display traps (DESIGN-SYSTEM-BRIEF): nothing here declares `display` on a Bootstrap .collapse
   (the listing's #UnitType-N panels), on .sm-disclosure__caret (#Down-N/#Right-N, jQuery
   .toggle()), or on anything the application or checkout scripts show and hide (#commercialModel,
   #residentialModel, the optional sections, #selectExistingPaymentMethod, #billingInfoSection,
   #enterNewPaymentInfo, the *Error spans). The two `display` rules on the validation summary and
   on empty checkout errors are the pre-2D ones, moved unchanged.
   ========================================================================= */

/* --- 19.1 Page column, notices, error and unavailable states ---------------- */
/* The public shell's 16px phone gutter; the app's .sm-page keeps 24px elsewhere. */
@media (max-width:767.98px){
  .sm-public-dark > .sm-page{ padding-inline:var(--space-4); }
}

/* _Layout's TempData notices sit outside the island (body > .container > .alert), on the dark
   <body> the page gets from marketing.css section 2. Stage 2A gave them .sm-alert, which paints
   them from those aliases; the first three rules repeat the same values from the pre-2A bridge and
   can be deleted once 2A's notices are verified on every public-dark page. The close button is
   Bootstrap's black X, inverted here; its focus ring is written pre-inverted in 12.9's @supports
   relative-colour block (and its forced-colours twin). Keep those two selectors and this one in
   step. */
body:has(.sm-public-dark) > .container > .alert{
  color:var(--text);
  border-radius:var(--radius-md);
  font-family:var(--font-sans);
}
body:has(.sm-public-dark) > .container > .alert-danger{
  background:var(--danger-subtle);
  border-color:var(--danger-border);
}
body:has(.sm-public-dark) > .container > .alert-success{
  background:var(--success-subtle);
  border-color:var(--success-border);
}
body:has(.sm-public-dark) > .container > .alert .btn-close{ filter:invert(1); }
/* Forced colours with a LIGHT palette: the inverted X turned white on the white Canvas notice and
   vanished until it took focus (12.9 explains; a dark palette keeps the invert). The filter goes,
   the X draws in its own black, and the focus outline, which no filter inverts any more, is
   written as CanvasText. After the invert rule above and 12.9's pair, at their weights. */
@media (forced-colors: active) and (prefers-color-scheme: light){
  body:has(.sm-public-dark) > .container > .alert .btn-close,
  body:has(.sm-public-dark) > .container > .alert .btn-close:focus-visible{ filter:none; }
  body:has(.sm-public-dark) > .container > .alert .btn-close:focus-visible{ outline-color:CanvasText; }
}
/* Below 576px Bootstrap's .container is fluid with a 12px gutter, so a notice box started 4px
   outside the page column's 16px phone gutter (the rule above; Building/ViewAvailableUnitsDisabled
   at 375). The notice's container takes the same 16px. From 576px .container is the 2A shell's
   centred fixed width and is left alone. (0,3,1) against Bootstrap's (0,1,0). */
@media (max-width:575.98px){
  body:has(.sm-public-dark) > .container:has(> .alert){ padding-inline:var(--space-4); }
}

/* Error and unavailable states: a .sm-card.sm-empty under the page's .sm-section-header, with a
   toned medallion (section 7), the message, and one action row (_PublicErrorActions).
   On these pages the empty state's sentence is the message itself (the error, the reason the
   listing is closed), so it reads in --text-secondary (7.64:1 on --surface) rather than the
   muted hint colour (4.59:1). */
.sm-public-dark .sm-empty__body{ color:var(--text-secondary); }
/* Shared/Error's request id: a long unbroken token in the mono face, not Bootstrap's pink code,
   on a line of its own under its label so it does not break mid-line. */
.sm-public-dark .sm-empty__body code{
  display:block;
  margin-top:var(--space-1);
  font-family:var(--font-mono);
  font-size:0.9375em;
  color:var(--text);
  overflow-wrap:anywhere;
}
/* Shared/Error's second card (the stock "Development Mode" note, verbatim). margin-bottom only,
   so the card's .sm-stack-3 spacing stands. */
.sm-public-dark .sm-error-note{ margin-top:var(--space-6); }
.sm-public-dark .sm-error-note > p{
  margin-bottom:0;
  font-size:var(--text-sm);
  line-height:var(--leading-normal);
  color:var(--text-secondary);
}

/* --- 19.2 Listing and unit detail (.sm-public-units), was available-units.css --- */
.sm-public-units .sm-section-header__actions{ margin-inline-start:0; }
.sm-public-units .sm-section-header__sub{ overflow-wrap:anywhere; }
.sm-public-units .sm-public-units__section-title{ margin:0; }
.sm-public-units .sm-disclosure__trigger{ flex-wrap:wrap; }
.sm-public-units .sm-disclosure__caret{ margin-inline-start:auto; }
.sm-public-units .sm-public-units__grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
  gap:var(--space-5);
  padding-top:var(--space-5);
}
/* An empty category shows a card where the grid would be; it gets the grid's 20px below the section
   hairline instead of sitting on it (F-apply-errors-2). */
.sm-public-units .sm-card.sm-empty{ margin-top:var(--space-5); }
.sm-public-units .sm-public-units__card{
  display:flex;
  flex-direction:column;
  min-width:0;
  overflow:hidden;
}
.sm-public-units .sm-public-units__card > .sm-card__body{ flex:1; }
.sm-public-units .sm-public-units__card-heading{
  display:flex;
  align-items:center;
  justify-content:space-between;
  flex-wrap:wrap;
  gap:var(--space-2);
}
.sm-public-units .sm-public-units__photo{
  display:block;
  width:100%;
  aspect-ratio:16 / 9;
  object-fit:cover;
}
.sm-public-units .sm-public-units__no-photo{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:var(--space-3);
  min-height:var(--space-24);
  padding:var(--space-5);
  background:var(--surface-raised);
  color:var(--text-secondary);
  font-size:var(--text-sm);
}
.sm-public-units .sm-public-units__no-photo > i{ font-size:var(--text-xl); }
.sm-public-units .sm-public-units__card .sm-card__footer > form,
.sm-public-units .sm-public-units__card .sm-card__footer .sm-btn{ width:100%; }
/* Term left, value right, in two equal tracks. Before 2D this lost its tie to 12.5's
   `auto minmax(0,1fr)` and needed marketing.css section 10; here, after 12.5, it wins by order. */
.sm-public-units .sm-detail-list{ grid-template-columns:minmax(0, 1fr) minmax(0, 1fr); }
.sm-public-units .sm-detail-list__value{ text-align:right; }
.sm-public-units .sm-public-units__gallery{ overflow:hidden; }
.sm-public-units .c-item{
  height:clamp(14rem, 40vw, 26rem);
  cursor:zoom-in;
}
/* An inset outline, not the token box-shadow: the gallery clips (overflow:hidden) and the photo
   paints over an inset shadow. */
.sm-public-units .c-item:focus-visible{
  outline:var(--sm-focus-width, 2px) solid var(--brand-text);
  outline-offset:calc(var(--sm-focus-width, 2px) * -2);
}
.sm-public-units .c-img{
  height:100%;
  object-fit:cover;
}
.sm-public-units .modal .c-img{
  max-height:75vh;
  object-fit:contain;
}
.sm-public-units .sm-public-units__metrics{
  display:grid;
  grid-template-columns:repeat(4, minmax(0, 1fr));
  gap:var(--space-4);
}
/* margin-bottom only: a `margin:0` here used to cancel the card's .sm-stack-* gap above the text
   (and the list below), which left the description touching its title and the contact list
   touching the "Call us…" line. */
.sm-public-units .sm-public-units__description{ margin-bottom:0; overflow-wrap:anywhere; }
.sm-public-units .sm-public-units__contacts{
  display:grid;
  grid-template-columns:repeat(3, minmax(0, 1fr));
  gap:var(--space-5);
  margin-bottom:0;
}
.sm-public-units .sm-public-units__contacts dt{
  color:var(--text-secondary);
  font-size:var(--text-sm);
  font-weight:var(--weight-medium);
  margin-bottom:var(--space-2);
}
.sm-public-units .sm-public-units__contacts dd{ margin:0; overflow-wrap:anywhere; }
/* The map is the shared .sm-map (13.3: dark zoom buttons with the token ring, a sunken fill before
   the tiles arrive, isolation for Leaflet's z-index scale); the listing only sets its height. */
.sm-public-units .sm-public-units__map{ --sm-map-height:calc(var(--space-32) * 2.5); }
.sm-public-units .modal-body .sm-btn{ width:100%; }
.sm-public-units .sm-btn{ min-height:var(--touch-target-min); }
@media (max-width:767.98px){
  .sm-public-units .sm-public-units__metrics{ grid-template-columns:repeat(2, minmax(0, 1fr)); }
  .sm-public-units .sm-public-units__metrics .sm-card{ padding:var(--space-4); }
  .sm-public-units .sm-public-units__contacts{ grid-template-columns:minmax(0, 1fr); }
  .sm-public-units .sm-public-units__apply > .sm-btn{ width:100%; }
  .sm-public-units .sm-public-units__map{ --sm-map-height:calc(var(--space-32) * 2); }
}

/* --- 19.3 Application form (.sm-application), was the block at the top of this file --- */
.sm-app.sm-application .sm-application__page{ max-width:calc(var(--space-32) * 7); }
.sm-application .sm-application__section{
  padding:var(--pad-card);
  background:var(--surface);
  border:var(--border-hairline) solid var(--border-subtle);
  border-radius:var(--radius-lg);
}
.sm-application .sm-application__section .row{
  --bs-gutter-x:var(--space-6);
  row-gap:var(--space-4);
}
.sm-application .sm-application__section .row + .row,
.sm-application form > .row,
.sm-application form > .sm-application__section,
.sm-application .sm-application__validation{
  margin-top:var(--space-6);
}
.sm-application .sm-application__validation{ margin-bottom:var(--space-6); }
.sm-application .sm-application__validation[hidden]{ display:none; }
.sm-application .sm-application__validation ul{ padding-inline-start:var(--space-5); }
.sm-application .sm-application__validation a{ color:var(--danger-text); }
.sm-application .sm-field__label{ margin-bottom:var(--space-2); }
.sm-application .form-check{
  display:inline-flex;
  align-items:center;
  gap:var(--space-2);
  min-height:var(--touch-target-min);
  padding:0;
  margin-block:0;
  margin-inline-end:var(--space-4);
}
.sm-application .form-check-label{ cursor:pointer; }
.sm-application .sm-radio{ flex:0 0 auto; }
.sm-application .sm-select{ padding-inline-end:var(--space-6); }
.sm-application .sm-application__upload{ box-shadow:none; }
.sm-application .sm-dropzone{ margin-top:var(--space-3); }
.sm-application .sm-application__range{
  display:grid;
  grid-template-columns:minmax(0, 1fr) auto minmax(0, 1fr);
  gap:var(--space-2);
}
.sm-application .sm-application__range-divider{
  padding-top:var(--space-2);
  color:var(--text-secondary);
}
.sm-application .sm-application__section :is(.col-3, .col-md-3){ width:50%; }
.sm-application .sm-card__title{ margin:0; }
.sm-application .is-invalid{ scroll-margin-block:var(--space-8); }
/* New in 2D. The auto-fill prompt, a card of its own above the form. */
.sm-application .sm-application__autofill{ margin-bottom:var(--space-6); }
/* The fee: one total row, the checkout's pattern (19.4). The label keeps its copy; the amount is
   the one figure on the page, in the mono face. margin-bottom only, so the section's .sm-stack-4
   gap above it stands. */
.sm-application .sm-application__total{
  display:flex;
  flex-wrap:wrap;
  align-items:baseline;
  justify-content:space-between;
  gap:var(--space-2) var(--space-4);
  margin-bottom:0;
  color:var(--text-secondary);
}
.sm-application .sm-application__total-amount{
  font-family:var(--font-mono);
  font-size:var(--text-2xl);
  font-weight:var(--weight-medium);
  line-height:var(--leading-tight);
  color:var(--text-strong);
  font-variant-numeric:var(--numeric-tabular);
}
.sm-application .sm-application__actions{
  display:flex;
  justify-content:flex-end;
  margin-top:var(--space-6);
}
@media (max-width:767.98px){
  .sm-application .sm-application__section{ padding:var(--space-4); }
  .sm-application .sm-application__section .row > [class*="col-"]{
    flex:0 0 100%;
    width:100%;
    max-width:100%;
  }
  .sm-application .sm-application__range{
    grid-template-columns:minmax(0, 1fr);
  }
  .sm-application .sm-input,
  .sm-application .sm-select,
  .sm-application .sm-btn{ min-height:var(--touch-target-min); }
  .sm-application .sm-application__actions > .sm-btn{ width:100%; }
}

/* --- 19.4 Fee checkout (.sm-application-payment), was application-payment.css --- */
.sm-app.sm-application-payment .sm-application-payment__page{ max-width:42rem; }
/* (application-payment.css's .sm-application-payment__eyebrow is not carried over: the eyebrow
   takes 7's .sm-section-header__eyebrow in the same change.) */
.sm-application-payment .sm-section-header__sub{ line-height:1.6; }
.sm-application-payment__choices{ display:flex; flex-wrap:wrap; gap:var(--space-3); margin:0 0 var(--space-5); padding:0; border:0; }
/* A payment choice is a <label> round its .sm-radio, and the radio is the control: its --ink-400
   ring (4.69:1 on its field) identifies each option and its fill carries the choice. The card's
   resting border stays the quiet --border-subtle (1.15:1) on purpose: the chosen card's
   --brand-text border then stands 6.83:1 apart from it, where a 3:1 resting border (--ink-400)
   would leave the two borders 1.72:1 apart and blunt the state the card exists to show. */
.sm-application-payment__choice{ flex:1 1 12rem; display:flex; align-items:center; gap:var(--space-3); min-height:var(--touch-target-min); padding:var(--space-3) var(--space-4); border:1px solid var(--border-subtle); border-radius:var(--radius-md); cursor:pointer; }
.sm-application-payment__choice:has(input:checked){ background:var(--brand-subtle); border-color:var(--brand-text); }
/* Forced colours repaint both borders alike; the chosen card takes Highlight, beside its radio's
   own Highlight fill (section 4). */
@media (forced-colors: active){
  .sm-application-payment__choice:has(input:checked){ border-color:Highlight; }
}
.sm-application-payment__choice .sm-radio{ margin:0; flex-shrink:0; }
.sm-application-payment__subtitle{ font-size:var(--text-base); margin:var(--space-5) 0 var(--space-3); }
.sm-application-payment .sm-select{ width:100%; }
.sm-application-payment .sm-application-payment__checkout{ display:flex; flex-direction:column; align-items:stretch; gap:var(--space-4); }
.sm-application-payment__total{ display:flex; align-items:center; justify-content:space-between; gap:var(--space-4); }
.sm-application-payment__total strong{ font-family:var(--font-mono); font-size:var(--text-2xl); }
.sm-application-payment__terms{ display:flex; align-items:flex-start; gap:var(--space-3); line-height:1.6; cursor:pointer; }
.sm-application-payment__terms input{ margin-top:var(--space-1); flex-shrink:0; }
.sm-application-payment .sm-field__error:empty{ display:none; }
.sm-application-payment__secure{ display:flex; align-items:center; justify-content:center; gap:var(--space-2); margin:0; font-size:var(--text-xs); color:var(--text-secondary); }
.sm-application-payment .sm-application-payment__terms a{ color:var(--text-link); text-underline-offset:3px; }
.sm-application-payment .sm-application-payment__terms a:hover{ color:var(--text-link-hover); }
