/*
  The BSFT UI default theme. Built entirely on design tokens: applications re-skin by
  overriding the custom properties below (or by replacing this stylesheet), never by
  touching component behavior.

  Consumed via: <link rel="stylesheet" href="_content/Bsft.Ui.Theme/bsft-ui.css" />
*/

:root {
  --bsft-color-text: #1f2933;
  --bsft-color-muted: #6b7280;
  --bsft-color-surface: #ffffff;
  --bsft-color-border: #d2d6dc;
  --bsft-color-primary: #2563eb;
  --bsft-color-primary-contrast: #ffffff;
  --bsft-color-danger: #b91c1c;
  --bsft-color-danger-surface: #fef2f2;
  --bsft-color-warning: #92400e;
  --bsft-color-warning-surface: #fffbeb;
  --bsft-color-success: #166534;
  --bsft-color-success-surface: #f0fdf4;
  --bsft-color-info: #1e40af;
  --bsft-color-info-surface: #eff6ff;
  /* The resting colour of a placeholder standing in for content that has not arrived. Its own
     token rather than the border colour it happens to resemble: a skeleton is a surface, and a
     re-skin that darkens borders must not darken every loading page with them. */
  --bsft-color-skeleton: #e5e7eb;
  --bsft-spacing: 0.5rem;
  --bsft-radius: 0.375rem;
  --bsft-font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* The three floating layers, ordered as one contract: an anchored popup never outranks a
     modal, and nothing outranks the toasts. */
  --bsft-z-popup: 10;
  --bsft-z-dialog: 100;
  --bsft-z-toast: 1000;
  /* Durations are tokens for the same reason colours are: a host toning the theme's motion down
     without turning it off should not have to find every transition in the file. The reduced
     -motion guard below is the other half of that and is not a substitute for it — it answers a
     stated user preference, while these answer a designer's. */
  --bsft-motion-fast: 150ms;
  --bsft-motion-spin: 800ms;
  --bsft-motion-pulse: 1500ms;
}

/* --- dark theme ---------------------------------------------------------- */

/* The standard three-state model: without an attribute the system preference decides
   (prefers-color-scheme), data-bsft-theme="light"|"dark" on <html> overrides it in either
   direction. The dark palette lives once in the --bsft-dark-* tokens; the two activation
   rules below only map the public tokens onto it, so they cannot drift apart. Only the
   colors change — spacing, radius and typography are theme-independent. */

:root {
  --bsft-dark-color-text: #e5e7eb;
  --bsft-dark-color-muted: #9ca3af;
  --bsft-dark-color-surface: #111827;
  --bsft-dark-color-border: #374151;
  --bsft-dark-color-primary: #3b82f6;
  --bsft-dark-color-primary-contrast: #ffffff;
  --bsft-dark-color-danger: #f87171;
  --bsft-dark-color-danger-surface: #450a0a;
  --bsft-dark-color-warning: #fbbf24;
  --bsft-dark-color-warning-surface: #451a03;
  --bsft-dark-color-success: #4ade80;
  --bsft-dark-color-success-surface: #052e16;
  --bsft-dark-color-info: #93c5fd;
  --bsft-dark-color-info-surface: #172554;
  --bsft-dark-color-skeleton: #1f2937;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-bsft-theme="light"]) {
    --bsft-color-text: var(--bsft-dark-color-text);
    --bsft-color-muted: var(--bsft-dark-color-muted);
    --bsft-color-surface: var(--bsft-dark-color-surface);
    --bsft-color-border: var(--bsft-dark-color-border);
    --bsft-color-primary: var(--bsft-dark-color-primary);
    --bsft-color-primary-contrast: var(--bsft-dark-color-primary-contrast);
    --bsft-color-danger: var(--bsft-dark-color-danger);
    --bsft-color-danger-surface: var(--bsft-dark-color-danger-surface);
    --bsft-color-warning: var(--bsft-dark-color-warning);
    --bsft-color-warning-surface: var(--bsft-dark-color-warning-surface);
    --bsft-color-success: var(--bsft-dark-color-success);
    --bsft-color-success-surface: var(--bsft-dark-color-success-surface);
    --bsft-color-info: var(--bsft-dark-color-info);
    --bsft-color-info-surface: var(--bsft-dark-color-info-surface);
    --bsft-color-skeleton: var(--bsft-dark-color-skeleton);
  }
}

:root[data-bsft-theme="dark"] {
  --bsft-color-text: var(--bsft-dark-color-text);
  --bsft-color-muted: var(--bsft-dark-color-muted);
  --bsft-color-surface: var(--bsft-dark-color-surface);
  --bsft-color-border: var(--bsft-dark-color-border);
  --bsft-color-primary: var(--bsft-dark-color-primary);
  --bsft-color-primary-contrast: var(--bsft-dark-color-primary-contrast);
  --bsft-color-danger: var(--bsft-dark-color-danger);
  --bsft-color-danger-surface: var(--bsft-dark-color-danger-surface);
  --bsft-color-warning: var(--bsft-dark-color-warning);
  --bsft-color-warning-surface: var(--bsft-dark-color-warning-surface);
  --bsft-color-success: var(--bsft-dark-color-success);
  --bsft-color-success-surface: var(--bsft-dark-color-success-surface);
  --bsft-color-info: var(--bsft-dark-color-info);
  --bsft-color-info-surface: var(--bsft-dark-color-info-surface);
  --bsft-color-skeleton: var(--bsft-dark-color-skeleton);
}

/* --- shared ------------------------------------------------------------- */

.bsft-error {
  color: var(--bsft-color-danger);
  background: var(--bsft-color-danger-surface);
  border: 1px solid var(--bsft-color-danger);
  border-radius: var(--bsft-radius);
  padding: var(--bsft-spacing);
  font-family: var(--bsft-font);
}

.bsft-empty,
.bsft-busy {
  color: var(--bsft-color-muted);
  font-family: var(--bsft-font);
  padding: var(--bsft-spacing);
}

/* --- busy affordance ------------------------------------------------------ */

/* The busy region lays its affordance out beside the label rather than under it. The label is
   visually hidden and the affordance is aria-hidden, so what the viewer sees and what a screen
   reader hears are two disjoint halves of one region — which is why neither may be styled away:
   display:none on the label would take the announcement with it. */
.bsft-busy {
  display: flex;
  align-items: center;
  gap: var(--bsft-spacing);
}

/* The skeleton fills the width it was given, because it stands in for content that will. */
.bsft-busy-skeleton {
  display: block;
}

@keyframes bsft-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Opacity rather than a travelling highlight: a gradient sweep needs a background-position
   animation that composites on every frame, and the pulse says the same thing for less. */
@keyframes bsft-skeleton-pulse {
  50% {
    opacity: 0.5;
  }
}

/* A ring with one coloured quadrant, sized off the surrounding text so it matches whatever it
   sits beside. Frozen by the reduced-motion guard into exactly this shape, which is why the
   coloured quadrant is drawn at rest rather than only appearing mid-spin. */
.bsft-spinner {
  flex-shrink: 0;
  box-sizing: border-box;
  width: 1em;
  height: 1em;
  border: 2px solid var(--bsft-color-border);
  border-top-color: var(--bsft-color-primary);
  border-radius: 50%;
  animation: bsft-spin var(--bsft-motion-spin) linear infinite;
}

.bsft-skeleton {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
  width: 100%;
}

.bsft-skeleton-line {
  height: 0.75rem;
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-skeleton);
  animation: bsft-skeleton-pulse var(--bsft-motion-pulse) ease-in-out infinite;
}

/* The last line is short, the way a paragraph's last line is: a block of equal bars reads as a
   table that failed to load rather than as text on its way. */
.bsft-skeleton-line:last-child {
  width: 60%;
}

.bsft-notice {
  color: var(--bsft-color-warning);
  background: var(--bsft-color-warning-surface);
  border: 1px solid var(--bsft-color-warning);
  border-radius: var(--bsft-radius);
  padding: var(--bsft-spacing);
  font-family: var(--bsft-font);
}

/* --- form --------------------------------------------------------------- */

.bsft-form {
  display: flex;
  flex-direction: column;
  gap: var(--bsft-spacing);
  font-family: var(--bsft-font);
  color: var(--bsft-color-text);
}

.bsft-form-field {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
}

.bsft-form-field label {
  font-size: 0.875em;
  font-weight: 500;
  color: var(--bsft-color-text);
}

/* The required marker is presentational (aria-hidden); requiredness reaches assistive
   technology through the host's input. */
.bsft-form-field-required {
  color: var(--bsft-color-danger);
  margin-left: 0.25em;
}

/* Text inputs, selects and textareas share the control chrome. Checkboxes and radios keep
   their native rendering — boxing them in the same border/padding would break their layout
   next to a label. */
.bsft-form-field input:not([type="checkbox"]):not([type="radio"]),
.bsft-form-field select,
.bsft-form-field textarea {
  font-family: var(--bsft-font);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  padding: calc(var(--bsft-spacing) / 2) var(--bsft-spacing);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
}

.bsft-form-field input:not([type="checkbox"]):not([type="radio"]):focus-visible,
.bsft-form-field select:focus-visible,
.bsft-form-field textarea:focus-visible {
  border-color: var(--bsft-color-primary);
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 1px;
}

.bsft-form-field-hint {
  color: var(--bsft-color-muted);
  font-size: 0.875em;
  margin: 0;
}

.bsft-form-field-errors {
  color: var(--bsft-color-danger);
  font-size: 0.875em;
  margin: 0;
  padding-left: 1.25em;
}

.bsft-form-actions {
  display: flex;
  gap: var(--bsft-spacing);
}

.bsft-button {
  font-family: var(--bsft-font);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
  padding: calc(var(--bsft-spacing) / 2) var(--bsft-spacing);
  cursor: pointer;
}

/* One definition of the primary surface. The selected theme and locale options are that same
   surface, so they join the rule instead of restating its three declarations. */
.bsft-button-primary,
.bsft-theme-option[aria-pressed="true"],
.bsft-locale-option[aria-pressed="true"],
.bsft-scope-option[aria-pressed="true"] {
  background: var(--bsft-color-primary);
  border-color: var(--bsft-color-primary);
  color: var(--bsft-color-primary-contrast);
}

.bsft-button:disabled {
  opacity: 0.6;
  cursor: default;
}

/* --- copy affordance ----------------------------------------------------- */

/* The affordance is a button plus the region that announces the verdict, laid out inline so it
   sits beside the value it copies rather than under it. It joins .bsft-button rather than
   restating a surface of its own: a copy button is an ordinary button that happens to have two
   more states to show. */
.bsft-copy {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--bsft-spacing) / 2);
}

.bsft-copy-button {
  font-size: 0.875rem;
  padding: calc(var(--bsft-spacing) / 4) calc(var(--bsft-spacing) / 2);
}

.bsft-copy-button:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* The two outcomes are tokenized like every other themed surface, and — the point of colouring
   them at all — never the only carrier of the verdict: the text beside the button says it too,
   for anyone who cannot tell the two tokens apart. */
.bsft-copy-button-copied {
  border-color: var(--bsft-color-success);
  color: var(--bsft-color-success);
}

.bsft-copy-button-failed {
  border-color: var(--bsft-color-danger);
  color: var(--bsft-color-danger);
}

.bsft-copy-status {
  font-size: 0.875rem;
  color: var(--bsft-color-muted);
}

/* --- list --------------------------------------------------------------- */

.bsft-list {
  font-family: var(--bsft-font);
  color: var(--bsft-color-text);
}

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

.bsft-list th,
.bsft-list td {
  text-align: left;
  padding: var(--bsft-spacing);
  border-bottom: 1px solid var(--bsft-color-border);
}

.bsft-list th button {
  all: unset;
  cursor: pointer;
  font: inherit;
}

/* The sort glyphs live here, not in markup, so a theme override (or an RTL layout)
   re-skins them without touching the component. */
.bsft-list-sort-asc::after {
  content: " ▲";
}

.bsft-list-sort-desc::after {
  content: " ▼";
}

.bsft-list-loaded {
  color: var(--bsft-color-muted);
}

/* The scrolling container a windowed list needs: something with a height for the browser to
   scroll, and spacer rows inside it standing in for the rows that are not drawn. Both numbers
   arrive as custom properties written by the rendering itself — the same row height it placed
   the spacers by — so the stylesheet and the arithmetic cannot disagree about how tall a row is.
   The fallbacks are the rendering's own defaults and exist for a host that styles a window of
   its own rather than as a second opinion. */
.bsft-list-window {
  overflow-y: auto;
  max-height: var(--bsft-list-window-height, 480px);
}

/* Uniform rows are the premise of the whole arrangement, so the window states the height rather
   than hoping the content agrees on one. The spacers carry their own inline height and override
   this, which is exactly what they are for. */
.bsft-list-window tbody tr {
  height: var(--bsft-list-row-height, 40px);
}

/* The header stays put while the rows scroll under it — a windowed list is the one place a user
   can lose sight of what the columns mean without leaving the screen. It needs its own background
   because the rows would otherwise show through it. */
.bsft-list-window thead th {
  position: sticky;
  top: 0;
  background: var(--bsft-color-surface);
}

/* Where the keyboard is. Painted only inside a window, because that is where the grid itself
   holds the focus and the cursor is the only thing saying which row the arrow keys are on; in a
   paged list every row's own controls are still in the tab order and a second cursor beside the
   focus ring would be two answers to one question. */
.bsft-list-window .bsft-list-row-active > td {
  background: var(--bsft-color-info-surface);
}

.bsft-list-pager {
  display: flex;
  align-items: center;
  gap: var(--bsft-spacing);
  padding-top: var(--bsft-spacing);
}

/* The page-size chooser sits at the far end of the pager, away from the previous/next
   pair, so a mis-aimed click changes the page rather than the whole page size. */
.bsft-list-pagesize {
  display: flex;
  align-items: center;
  gap: calc(var(--bsft-spacing) / 2);
  margin-left: auto;
}

.bsft-list-select {
  width: 1px;
  white-space: nowrap;
}

/* The filter bar wraps rather than scrolls: a filter the user cannot see is a filter they
   cannot take off again, and a narrow viewport is exactly where that bites. */
.bsft-list-filterbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--bsft-spacing);
  padding-bottom: var(--bsft-spacing);
}

.bsft-list-filter {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
}

.bsft-list-filter-label {
  color: var(--bsft-color-muted);
  font-size: 0.875rem;
}

.bsft-list-bulk {
  display: flex;
  align-items: center;
  gap: var(--bsft-spacing);
  /* Reserves its height even while nothing is selected, so the table does not jump down the
     page the moment the first row is ticked. */
  min-height: 2rem;
}

.bsft-list-bulk-count {
  color: var(--bsft-color-muted);
}

/* Hidden from sight, kept for assistive technology — never display:none, which takes the text
   out of the accessibility tree along with the pixels. */
.bsft-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- detail ------------------------------------------------------------- */

.bsft-detail {
  font-family: var(--bsft-font);
  color: var(--bsft-color-text);
}

/* --- dialog -------------------------------------------------------------- */

.bsft-dialog {
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  padding: var(--bsft-spacing);
  font-family: var(--bsft-font);
  display: flex;
  flex-direction: column;
  gap: var(--bsft-spacing);
}

.bsft-button-danger {
  background: var(--bsft-color-danger);
  border-color: var(--bsft-color-danger);
  color: var(--bsft-color-primary-contrast);
}

/* --- theme and locale switchers ------------------------------------------- */

/* Both groups are the same segmented control: buttons joined into one control, the active one
   marked by aria-pressed. Styling selects on that attribute rather than on an extra class,
   so a theme cannot style an option as active that assistive technology reads as inactive —
   the active surface itself is the shared .bsft-button-primary rule above. One set of rules
   for both switchers, because the two ask the same kind of question and must not drift. */
.bsft-locale-switcher,
.bsft-theme-switcher {
  display: inline-flex;
  font-family: var(--bsft-font);
}

.bsft-locale-option,
.bsft-theme-option {
  border-radius: 0;
}

.bsft-locale-option:first-child,
.bsft-theme-option:first-child {
  border-start-start-radius: var(--bsft-radius);
  border-end-start-radius: var(--bsft-radius);
}

.bsft-locale-option:last-child,
.bsft-theme-option:last-child {
  border-start-end-radius: var(--bsft-radius);
  border-end-end-radius: var(--bsft-radius);
}

.bsft-locale-option + .bsft-locale-option,
.bsft-theme-option + .bsft-theme-option {
  border-inline-start: none;
}

.bsft-locale-option:focus-visible,
.bsft-theme-option:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* --- popover, menu and user menu -------------------------------------------- */

/* Anchored popup: the trigger stays in flow, the panel floats off the corner the placement
   attribute names. One block for the plain popover, the dropdown menu and the user menu, since
   all three render the same markup. */
.bsft-popover {
  position: relative;
  display: inline-flex;
  font-family: var(--bsft-font);
}

.bsft-popover-trigger:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* The outside-click dismissal: an invisible layer under the popup and over the page, so a
   click anywhere else closes it without a document-level script. */
.bsft-popover-backdrop {
  position: fixed;
  inset: 0;
  z-index: calc(var(--bsft-z-popup) - 1);
}

.bsft-popover-panel {
  position: absolute;
  min-width: 12rem;
  display: flex;
  flex-direction: column;
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  padding: calc(var(--bsft-spacing) / 2);
  gap: calc(var(--bsft-spacing) / 2);
  z-index: var(--bsft-z-popup);
}

/* The four placements, written as the tokens BsftPopover emits rather than as CSS-side names,
   so the two cannot drift. Logical properties throughout: "start" is the left edge in a
   left-to-right locale and the right edge in a right-to-left one. */
.bsft-popover[data-bsft-placement="bottom-start"] > .bsft-popover-panel {
  top: 100%;
  inset-inline-start: 0;
}

.bsft-popover[data-bsft-placement="bottom-end"] > .bsft-popover-panel {
  top: 100%;
  inset-inline-end: 0;
}

.bsft-popover[data-bsft-placement="top-start"] > .bsft-popover-panel {
  bottom: 100%;
  inset-inline-start: 0;
}

.bsft-popover[data-bsft-placement="top-end"] > .bsft-popover-panel {
  bottom: 100%;
  inset-inline-end: 0;
}

.bsft-menu-items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.bsft-menu-item {
  appearance: none;
  background: none;
  border: 0;
  text-align: start;
  color: var(--bsft-color-text);
  font: inherit;
  padding: calc(var(--bsft-spacing) / 2) var(--bsft-spacing);
  border-radius: var(--bsft-radius);
  cursor: pointer;
}

.bsft-menu-item:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* Refused rather than removed, so the menu keeps its shape: the pointer says so and the
   contrast drops, while aria-disabled says the same thing to assistive technology. */
.bsft-menu-item[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.6;
}

/* --- disclosure ------------------------------------------------------------- */

.bsft-disclosure {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
  font-family: var(--bsft-font);
}

.bsft-disclosure-heading {
  margin: 0;
  font-size: inherit;
}

.bsft-disclosure-trigger {
  appearance: none;
  width: 100%;
  text-align: start;
  background: none;
  border: 0;
  color: var(--bsft-color-text);
  font: inherit;
  font-weight: 600;
  padding: calc(var(--bsft-spacing) / 2) 0;
  cursor: pointer;
}

.bsft-disclosure-trigger:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

.bsft-disclosure-trigger:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

.bsft-disclosure-panel {
  padding-inline-start: var(--bsft-spacing);
}

/* --- breadcrumbs ------------------------------------------------------------ */

.bsft-breadcrumbs {
  font-family: var(--bsft-font);
}

.bsft-breadcrumbs-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--bsft-spacing) / 2);
}

/* The separator is drawn rather than written into the markup: a literal slash between two list
   items is read aloud once per step and adds nothing the ordered list has not already said. */
.bsft-breadcrumbs-step + .bsft-breadcrumbs-step::before {
  content: "/";
  margin-inline-end: calc(var(--bsft-spacing) / 2);
  color: var(--bsft-color-muted);
}

.bsft-breadcrumbs-label {
  color: var(--bsft-color-primary);
  text-decoration: none;
}

.bsft-breadcrumbs-label[aria-current="page"] {
  color: var(--bsft-color-text);
  font-weight: 600;
}

.bsft-breadcrumbs-label:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* --- shell ------------------------------------------------------------------- */

.bsft-shell {
  display: flex;
  flex-direction: column;
  /* The viewport height gives the main region's flex: 1 free space to absorb. The svh line
     wins where the small viewport unit exists: the height with the mobile URL bar expanded,
     never changing — so the bar neither forces a permanent scrollbar nor reflows the shell
     while it animates, at the stated price that a collapsed bar leaves a short page ending
     that bar's height above the bottom edge. */
  min-height: 100vh;
  min-height: 100svh;
  gap: var(--bsft-spacing);
  font-family: var(--bsft-font);
  color: var(--bsft-color-text);
}

.bsft-shell-header {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--bsft-spacing);
}

.bsft-shell-main {
  flex: 1;
}

/* The drawer toggle. It sits first in the header and is pushed to the start edge, because the
   control that opens the navigation belongs where the navigation is. Above the shell breakpoint
   the responsive block takes it out of the document entirely — display:none rather than a
   visual hide, so a keyboard user is not handed a control for a drawer that is not there. */
.bsft-shell-nav-toggle {
  appearance: none;
  margin-inline-end: auto;
  padding: calc(var(--bsft-spacing) / 2) var(--bsft-spacing);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
  font: inherit;
  line-height: 1;
  cursor: pointer;
}

/* The glyph is drawn here rather than written into the markup, exactly as the list's sort
   markers are: the button carries an aria-label instead, so a re-skin swapping this for an icon
   font changes nothing about what the control is announced as. */
.bsft-shell-nav-toggle::before {
  content: "\2261";
}

.bsft-shell-nav-toggle:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* --- navigation ------------------------------------------------------------ */

.bsft-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--bsft-spacing);
  font-family: var(--bsft-font);
}

.bsft-nav-item {
  color: var(--bsft-color-primary);
  text-decoration: none;
  padding: calc(var(--bsft-spacing) / 2) var(--bsft-spacing);
  border-radius: var(--bsft-radius);
}

.bsft-nav-item:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* The current location, marked by the same attribute that announces it. */
.bsft-nav-item[aria-current="page"] {
  background: var(--bsft-color-surface);
  font-weight: 600;
}

.bsft-nav-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--bsft-spacing) / 2);
}

/* A nested level stacks and indents instead of wrapping: a group's children are a list under a
   heading, not more entries of the bar above them. */
.bsft-nav-group > .bsft-nav-list {
  flex-direction: column;
  align-items: stretch;
  flex-wrap: nowrap;
  padding-inline-start: var(--bsft-spacing);
}

.bsft-nav-entry {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
}

.bsft-nav-group-trigger {
  appearance: none;
  background: none;
  border: 0;
  color: var(--bsft-color-text);
  font: inherit;
  padding: calc(var(--bsft-spacing) / 2);
  cursor: pointer;
}

.bsft-nav-group-trigger:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

.bsft-nav-group {
  flex-basis: 100%;
}

/* --- org scope -------------------------------------------------------------- */

.bsft-scope-switcher {
  display: inline-flex;
  align-items: center;
  gap: var(--bsft-spacing);
  font-family: var(--bsft-font);
}

.bsft-scope-option:focus-visible,
.bsft-scope-leave:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* --- toasts -------------------------------------------------------------- */

/* The stack is fixed to a corner so the host can live anywhere in the shell markup.
   Only the toasts themselves take pointer events — an empty stack never covers the
   screen it floats over. */
.bsft-toasts {
  position: fixed;
  inset-block-end: var(--bsft-spacing);
  inset-inline-end: var(--bsft-spacing);
  z-index: var(--bsft-z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--bsft-spacing);
  pointer-events: none;
  font-family: var(--bsft-font);
}

.bsft-toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--bsft-spacing);
  max-width: 24rem;
  padding: var(--bsft-spacing);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
}

.bsft-toast-message {
  flex: 1;
}

.bsft-toast-dismiss {
  all: unset;
  cursor: pointer;
  line-height: 1;
  font-size: 1.25em;
  color: inherit;
}

/* `all: unset` drops the user agent's focus ring with everything else, so it is put back
   here: the dismiss button is reachable by keyboard and must show where the focus is. */
.bsft-toast-dismiss:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

.bsft-toast-info {
  color: var(--bsft-color-info);
  background: var(--bsft-color-info-surface);
  border-color: var(--bsft-color-info);
}

.bsft-toast-success {
  color: var(--bsft-color-success);
  background: var(--bsft-color-success-surface);
  border-color: var(--bsft-color-success);
}

.bsft-toast-warning {
  color: var(--bsft-color-warning);
  background: var(--bsft-color-warning-surface);
  border-color: var(--bsft-color-warning);
}

.bsft-toast-error {
  color: var(--bsft-color-danger);
  background: var(--bsft-color-danger-surface);
  border-color: var(--bsft-color-danger);
}

/* --- tabs ----------------------------------------------------------------- */

.bsft-tabs-list {
  display: flex;
  gap: var(--bsft-spacing);
  border-bottom: 1px solid var(--bsft-color-border);
}

.bsft-tab {
  all: unset;
  cursor: pointer;
  padding: var(--bsft-spacing) calc(var(--bsft-spacing) * 2);
  color: var(--bsft-color-muted);
  border-bottom: 2px solid transparent;
  font-family: var(--bsft-font);
}

.bsft-tab:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

.bsft-tab-active {
  color: var(--bsft-color-text);
  border-bottom-color: var(--bsft-color-primary);
}

.bsft-tabs-panel {
  padding: calc(var(--bsft-spacing) * 2) 0;
}

/* --- dialog ----------------------------------------------------------------- */

.bsft-dialog-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--bsft-z-dialog);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--bsft-spacing);
  background: rgb(0 0 0 / 50%);
  font-family: var(--bsft-font);
}

.bsft-dialog-panel {
  max-width: 32rem;
  width: 100%;
  max-height: 100%;
  overflow: auto;
  padding: calc(var(--bsft-spacing) * 2);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
}

/* The stacked host's own content. Every overlay of a stack carries the same --bsft-z-dialog
   layer deliberately: the newest is painted last because it is rendered last, which is what
   puts it on top among equally-layered siblings. A z-index computed per stack entry would be
   the one way a dialog could eventually climb over a toast, and the three layers above are a
   contract, not a suggestion. */
.bsft-dialog-title {
  margin: 0 0 var(--bsft-spacing);
  font-size: 1.125em;
}

.bsft-dialog-message {
  margin: 0 0 calc(var(--bsft-spacing) * 2);
}

/* --- toggle ----------------------------------------------------------------- */

.bsft-toggle {
  all: unset;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  width: 2.5rem;
  height: 1.5rem;
  padding: 2px;
  border-radius: 999px;
  background: var(--bsft-color-border);
  transition: background-color var(--bsft-motion-fast) ease;
}

.bsft-toggle:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

.bsft-toggle[disabled] {
  cursor: not-allowed;
  opacity: 0.5;
}

.bsft-toggle-on {
  background: var(--bsft-color-primary);
}

.bsft-toggle-thumb {
  width: 1.125rem;
  height: 1.125rem;
  border-radius: 50%;
  background: var(--bsft-color-surface);
  transition: transform var(--bsft-motion-fast) ease;
}

.bsft-toggle-on .bsft-toggle-thumb {
  transform: translateX(1rem);
}

/* --- wizard ----------------------------------------------------------------- */

.bsft-wizard-steps {
  display: flex;
  gap: calc(var(--bsft-spacing) * 2);
  list-style: none;
  padding: 0;
  margin: 0 0 calc(var(--bsft-spacing) * 2);
  font-family: var(--bsft-font);
}

.bsft-wizard-step {
  color: var(--bsft-color-muted);
}

.bsft-wizard-step-current {
  color: var(--bsft-color-text);
  font-weight: 600;
}

.bsft-wizard-step-completed {
  color: var(--bsft-color-text);
}

.bsft-wizard-step-invalid {
  color: var(--bsft-color-danger);
}

/* The reachable markers are buttons; they are styled back down to reading as part of the
   step list rather than as a control, because what they say is where the user is in a
   sequence and only secondarily that it can be jumped to. */

.bsft-wizard-step-link {
  border: 0;
  padding: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: underline;
}

.bsft-wizard-content {
  margin-bottom: calc(var(--bsft-spacing) * 2);
}

/* --- badge ----------------------------------------------------------------- */

.bsft-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0 0.375rem;
  border-radius: 999px;
  font-family: var(--bsft-font);
  font-size: 0.75rem;
  line-height: 1;
  color: var(--bsft-color-text);
  background: var(--bsft-color-border);
}

.bsft-badge:empty {
  min-width: 0.5rem;
  width: 0.5rem;
  height: 0.5rem;
  padding: 0;
}

.bsft-badge-info {
  color: var(--bsft-color-info);
  background: var(--bsft-color-info-surface);
}

.bsft-badge-success {
  color: var(--bsft-color-success);
  background: var(--bsft-color-success-surface);
}

.bsft-badge-warning {
  color: var(--bsft-color-warning);
  background: var(--bsft-color-warning-surface);
}

.bsft-badge-danger {
  color: var(--bsft-color-danger);
  background: var(--bsft-color-danger-surface);
}

/* --- choice list ----------------------------------------------------------------- */

.bsft-choice-list {
  display: flex;
  flex-direction: column;
  gap: var(--bsft-spacing);
  font-family: var(--bsft-font);
}

.bsft-choice {
  all: unset;
  box-sizing: border-box;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--bsft-spacing);
  width: 100%;
  /* A full-width card at comfortable touch-target height: the option list is the mobile
     replacement for a native select whose options cannot carry a badge. */
  min-height: 2.75rem;
  padding: var(--bsft-spacing) calc(var(--bsft-spacing) * 2);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
}

.bsft-choice:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

.bsft-choice[disabled] {
  cursor: not-allowed;
  opacity: 0.5;
}

.bsft-choice-selected {
  border-color: var(--bsft-color-primary);
  /* An inset ring rather than a wider border, so selecting an option does not change its
     size and shift the cards below it. */
  box-shadow: inset 0 0 0 1px var(--bsft-color-primary);
}

.bsft-choice-text {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
  text-align: left;
}

.bsft-choice-description {
  color: var(--bsft-color-muted);
  font-size: 0.875rem;
}

.bsft-choice-badge {
  flex-shrink: 0;
}

/* --- combobox -------------------------------------------------------------------- */

.bsft-combobox {
  /* The popup is positioned against this wrapper rather than the input, so the chip row above
     the input never shifts it out from under the control it belongs to. */
  position: relative;
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
  font-family: var(--bsft-font);
}

.bsft-combobox-input {
  box-sizing: border-box;
  width: 100%;
  min-height: 2.75rem;
  padding: var(--bsft-spacing) calc(var(--bsft-spacing) * 2);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
  font: inherit;
}

.bsft-combobox-input:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

.bsft-combobox-chips {
  display: flex;
  flex-wrap: wrap;
  gap: calc(var(--bsft-spacing) / 2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.bsft-combobox-chip {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--bsft-spacing) / 2);
  padding: calc(var(--bsft-spacing) / 2) var(--bsft-spacing);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
}

.bsft-combobox-chip-remove {
  all: unset;
  cursor: pointer;
  color: var(--bsft-color-muted);
  line-height: 1;
}

.bsft-combobox-chip-remove:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

.bsft-combobox-listbox {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  /* The popup floats over the content under it and must stay below a modal and the toasts —
     the one ordered contract the three layer tokens carry. */
  z-index: var(--bsft-z-popup);
  max-height: 16rem;
  overflow-y: auto;
  margin: calc(var(--bsft-spacing) / 2) 0 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
}

.bsft-combobox-option {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
  min-height: 2.75rem;
  padding: var(--bsft-spacing) calc(var(--bsft-spacing) * 2);
  cursor: pointer;
  color: var(--bsft-color-text);
}

.bsft-combobox-option-active {
  /* Where the keyboard is, not what is chosen: the two states are styled apart because they
     mean different things and can be true of two different rows at once. */
  background: var(--bsft-color-info-surface);
}

.bsft-combobox-option-selected {
  border-left: 2px solid var(--bsft-color-primary);
}

.bsft-combobox-option[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.5;
}

.bsft-combobox-option-description {
  color: var(--bsft-color-muted);
  font-size: 0.875rem;
}

.bsft-combobox-create {
  color: var(--bsft-color-primary);
}

/* --- empty state ----------------------------------------------------------------- */

.bsft-empty-state {
  padding: calc(var(--bsft-spacing) * 4) var(--bsft-spacing);
  text-align: center;
  color: var(--bsft-color-muted);
  font-family: var(--bsft-font);
}

.bsft-empty-state-title {
  margin: 0;
  color: var(--bsft-color-text);
  font-weight: 600;
}

.bsft-empty-state-description {
  margin: calc(var(--bsft-spacing) / 2) 0 0;
}

.bsft-empty-state-actions {
  margin-top: calc(var(--bsft-spacing) * 2);
}

/* --- scroll region ----------------------------------------------------------------- */

/* The transcript container. Its height is the surrounding layout's to give — a scroll region
   only scrolls once something bounds it — and overscroll containment keeps a reader flicking
   through the transcript from dragging the page behind it along. */
.bsft-scroll-region {
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* --- file transfer ----------------------------------------------------------------- */

.bsft-file-upload {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
}

/* The track under the moving bar; the width of the bar is written inline per render,
   because it is state (the bytes sent), not style. */
.bsft-file-upload-progress {
  height: 0.5rem;
  border-radius: 999px;
  background: var(--bsft-color-border);
  overflow: hidden;
}

.bsft-file-upload-progress-bar {
  height: 100%;
  border-radius: inherit;
  background: var(--bsft-color-primary);
  transition: width var(--bsft-motion-fast) ease;
}

.bsft-file-upload-cancel,
.bsft-file-download-button {
  align-self: flex-start;
  padding: calc(var(--bsft-spacing) / 2) var(--bsft-spacing);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
  cursor: pointer;
}

.bsft-file-upload-cancel:hover,
.bsft-file-download-button:hover {
  border-color: var(--bsft-color-primary);
}

.bsft-file-download-button[disabled] {
  cursor: not-allowed;
  opacity: 0.5;
}

.bsft-file-download {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
}

/* --- dashboard --------------------------------------------------------------- */

.bsft-dashboard {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: calc(var(--bsft-spacing) * 2);
  font-family: var(--bsft-font);
}

.bsft-dashboard-widget {
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  padding: calc(var(--bsft-spacing) * 2);
}

.bsft-dashboard-widget-title {
  margin: 0 0 var(--bsft-spacing);
  font-size: 1rem;
  color: var(--bsft-color-text);
}

/* --- date and time pickers ------------------------------------------------------- */

/* The three pickers share a shape: a row holding the text input (or two of them) and the popup
   trigger, with the popup itself positioned by the popover rules above. Only the panel's
   contents differ — a month grid for the two date pickers, a listbox for the time picker — which
   is why the grid is styled once here and rendered by one component. */

.bsft-datepicker,
.bsft-daterangepicker,
.bsft-timepicker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--bsft-spacing) / 2);
  font-family: var(--bsft-font);
}

.bsft-datepicker-input,
.bsft-daterangepicker-input,
.bsft-timepicker-input {
  box-sizing: border-box;
  min-height: 2.75rem;
  padding: var(--bsft-spacing) calc(var(--bsft-spacing) * 2);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
  font: inherit;
}

.bsft-datepicker-input:focus-visible,
.bsft-daterangepicker-input:focus-visible,
.bsft-timepicker-input:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* The refusal is the input's own border as well as the message under it: a colour alone would
   be the only thing distinguishing a rejected date, and the message carries the rest. */
.bsft-datepicker-input[aria-invalid="true"],
.bsft-daterangepicker-input[aria-invalid="true"],
.bsft-timepicker-input[aria-invalid="true"] {
  border-color: var(--bsft-color-danger);
}

.bsft-datepicker-error,
.bsft-daterangepicker-error,
.bsft-timepicker-error {
  flex-basis: 100%;
  color: var(--bsft-color-danger);
  font-size: 0.875rem;
}

/* Each end of a range is a captioned field of its own, stacked so the caption sits above the
   input it names rather than beside it. */
.bsft-daterangepicker-field {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
}

.bsft-daterangepicker-field label {
  color: var(--bsft-color-muted);
  font-size: 0.875rem;
}

/* --- the month grid --------------------------------------------------------------- */

.bsft-calendar {
  display: flex;
  flex-direction: column;
  gap: var(--bsft-spacing);
  min-width: 18rem;
  font-family: var(--bsft-font);
  color: var(--bsft-color-text);
}

.bsft-calendar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--bsft-spacing);
}

.bsft-calendar-heading {
  font-weight: 600;
}

.bsft-calendar-page {
  all: unset;
  min-width: 2rem;
  padding: calc(var(--bsft-spacing) / 2);
  border-radius: var(--bsft-radius);
  cursor: pointer;
  text-align: center;
  color: var(--bsft-color-text);
}

.bsft-calendar-page:focus-visible {
  /* `all: unset` drops the user agent's focus ring, so the visible focus is stated here. */
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

.bsft-calendar-grid {
  border-collapse: collapse;
  width: 100%;
}

.bsft-calendar-weekday {
  padding: calc(var(--bsft-spacing) / 2);
  color: var(--bsft-color-muted);
  font-size: 0.875rem;
  font-weight: 500;
}

.bsft-calendar-day {
  padding: calc(var(--bsft-spacing) / 2);
  border-radius: var(--bsft-radius);
  cursor: pointer;
  text-align: center;
}

.bsft-calendar-day:focus-visible {
  /* The cell itself is the focusable element of the grid pattern, so the ring goes on the cell
     and not on a control inside it. */
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: -2px;
}

/* The leading and trailing days of the grid are real, selectable dates rather than padding, so
   they are dimmed rather than hidden. */
.bsft-calendar-day-outside {
  color: var(--bsft-color-muted);
}

/* Today is marked with a ring rather than a fill, so it stays legible under the selection fill
   when the two land on the same cell. */
.bsft-calendar-day-today {
  box-shadow: inset 0 0 0 1px var(--bsft-color-primary);
}

.bsft-calendar-day-selected {
  background: var(--bsft-color-primary);
  color: var(--bsft-color-primary-contrast);
}

/* Everything between a range's ends, the ends themselves excluded — they carry the selection
   fill above, and this is the weaker surface that joins them up. It is also what the
   provisional span is painted with while the pointer is choosing the second end, so the user
   sees what the next click produces before making it. */
.bsft-calendar-day-inrange {
  background: var(--bsft-color-info-surface);
}

/* A cell outside the window is still announced and still a date; what it is not is choosable,
   and a td has no native disabled attribute to lean on. */
.bsft-calendar-day[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.5;
}

/* --- the time list ---------------------------------------------------------------- */

.bsft-timepicker-listbox {
  max-height: 16rem;
  overflow-y: auto;
  margin: 0;
  padding: 0;
  list-style: none;
}

.bsft-timepicker-option {
  min-height: 2.75rem;
  display: flex;
  align-items: center;
  padding: calc(var(--bsft-spacing) / 2) var(--bsft-spacing);
  cursor: pointer;
  color: var(--bsft-color-text);
}

.bsft-timepicker-option-active {
  /* Where the keyboard is, not what is chosen: the two states are styled apart because they
     mean different things and can be true of two different rows at once. */
  background: var(--bsft-color-info-surface);
}

.bsft-timepicker-option-selected {
  border-left: 2px solid var(--bsft-color-primary);
}

/* --- tree --------------------------------------------------------------------- */

.bsft-tree {
  /* How far one level indents, as a token of the tree's own. BsftTree writes only the level
     number onto a row (--bsft-tree-level); the length that number is multiplied by belongs to
     the theme, so a host restyles the whole hierarchy by overriding this one value instead of
     the markup carrying a computed padding no stylesheet can reach. */
  --bsft-tree-indent: calc(var(--bsft-spacing) * 1.5);
  display: flex;
  flex-direction: column;
  font-family: var(--bsft-font);
}

.bsft-tree-item {
  display: flex;
  align-items: center;
  gap: calc(var(--bsft-spacing) / 2);
  padding: calc(var(--bsft-spacing) / 2) var(--bsft-spacing);
  /* Logical rather than left, so a right-to-left document indents from the side it reads from.
     It follows the shorthand above deliberately: the shorthand sets the block padding and the
     inline end, and this overrides the one side that carries the depth. */
  padding-inline-start: calc(var(--bsft-tree-level, 1) * var(--bsft-tree-indent));
  border-radius: var(--bsft-radius);
  color: var(--bsft-color-text);
  cursor: pointer;
}

.bsft-tree-item:focus-visible {
  /* Inset, because a row spans the tree's full width and an outward ring would be clipped by
     the container that scrolls it. */
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: -2px;
}

.bsft-tree-selected {
  background: var(--bsft-color-primary);
  color: var(--bsft-color-primary-contrast);
}

.bsft-tree-twisty {
  display: inline-block;
  flex: none;
  width: 1em;
  text-align: center;
  transition: transform var(--bsft-motion-fast) ease-in-out;
}

.bsft-tree-twisty::before {
  content: "\25B8";
}

.bsft-tree-item[aria-expanded="true"] > .bsft-tree-twisty {
  transform: rotate(90deg);
}

.bsft-tree-item[aria-busy="true"] > .bsft-tree-twisty {
  opacity: 0.5;
}

.bsft-tree-label {
  /* A flex item refuses to shrink below its content by default, which is what makes a long node
     name push the row wider than the tree instead of wrapping inside it. */
  min-width: 0;
  overflow-wrap: anywhere;
}

/* --- split panes ------------------------------------------------------------------ */

/* Positioned, because the surface a pointer drag runs on is an absolutely placed child of this
   element and has to cover exactly the split and nothing else. */
.bsft-split {
  position: relative;
  display: flex;
  align-items: stretch;
  min-height: 0;
}

/* A drag that crosses two panes would otherwise paint a text selection behind the divider, which
   reads as the page breaking rather than as a pane resizing. */
.bsft-split[data-bsft-split-drag="true"] {
  user-select: none;
}

/* The basis is the separator's announced value applied verbatim: no max-width, no percentage
   ceiling and no shrinking, because a divider whose aria-valuenow says something other than what
   is on screen is worse than a pane somebody made too narrow. The range is the caller's
   MinPosition/MaxPosition and is enforced there, once. */
.bsft-split-primary {
  flex: 0 0 var(--bsft-split-basis, 20rem);
  min-width: 0;
  overflow: auto;
}

.bsft-split-secondary {
  flex: 1 1 0;
  min-width: 0;
  overflow: auto;
}

/* touch-action is load-bearing rather than cosmetic: without it a touch pointer on the divider is
   claimed by the browser's own panning and the component never sees a pointermove at all. */
.bsft-split-divider {
  flex: 0 0 auto;
  inline-size: 0.375rem;
  cursor: col-resize;
  background: var(--bsft-color-border);
  touch-action: none;
}

.bsft-split-divider:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 1px;
}

/* Only on screen while a gesture is in flight. It sits above the panes so every move lands on one
   element, and below the floating layers so a dialog opened over a split still covers it. */
.bsft-split-drag-surface {
  position: absolute;
  inset: 0;
  z-index: calc(var(--bsft-z-popup) - 1);
  cursor: col-resize;
}

.bsft-split-swap {
  align-self: flex-start;
  padding: var(--bsft-spacing);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
  cursor: pointer;
}

.bsft-split-swap:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* --- reduced motion --------------------------------------------------------------- */

/* The guard is deliberately universal rather than a list of the rules that animate today. A
   guard that enumerates what it knows about is the guard that misses the next transition
   somebody adds, and a half-covered reduced-motion story is worse than none: it reads as
   handled. Durations are collapsed rather than set to zero so that transitionend and
   animationend still fire — a rendering that waits for one would otherwise hang forever — and
   an infinite animation is cut to a single iteration, which is what leaves the spinner standing
   as the static ring it draws at rest.

   It applies to the host's own rules too, on purpose: the preference is the user's statement
   about the page, not about one stylesheet on it. A host that wants motion back for its own
   surfaces states that in its own reduced-motion block. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- responsive ------------------------------------------------------------------- */

/* One breakpoint, at 48rem — the width below which a sidebar and a content column stop both
   being readable. It is written as a literal in both queries because a media query cannot read a
   custom property, which is a limitation of the cascade and not a token this file forgot to
   declare; the two halves are stated as a max/min pair around the same number so they can never
   both apply and never both fail to. */

@media (max-width: 47.999rem) {
  /* Off-canvas: the navigation leaves the flow and becomes a drawer over the content, disclosed
     by the header's toggle. It is hidden with visibility rather than display, for two reasons
     that both matter — visibility takes it out of the accessibility tree exactly as display
     would, and unlike display it is a transitionable property, so the drawer slides instead of
     appearing. The transition is one of the ones the reduced-motion guard above collapses. */
  .bsft-shell-nav {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    width: min(18rem, 80vw);
    padding: var(--bsft-spacing);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--bsft-color-surface);
    border-inline-end: 1px solid var(--bsft-color-border);
    /* Above the modal layer's backdrop and below the modal itself: a drawer is dismissed by the
       next thing the user does, so a dialog opened from it still has to cover it. */
    z-index: calc(var(--bsft-z-dialog) - 1);
    visibility: hidden;
    transform: translateX(-100%);
    transition: transform var(--bsft-motion-fast) ease, visibility var(--bsft-motion-fast);
  }

  /* The state lives on the shell rather than on the drawer, because the backdrop is a sibling
     and has to react to the same fact. */
  .bsft-shell[data-bsft-nav="open"] .bsft-shell-nav {
    visibility: visible;
    transform: translateX(0);
  }

  .bsft-shell-nav-backdrop {
    position: fixed;
    inset: 0;
    z-index: calc(var(--bsft-z-dialog) - 2);
    background: rgb(0 0 0 / 50%);
  }

  /* A drawer is a column, never the wrapping band the wide layout renders. */
  .bsft-shell-nav .bsft-nav,
  .bsft-shell-nav .bsft-nav-list {
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
  }

  /* The table scrolls sideways inside the list rather than squeezing every column to
     illegibility; the pager wraps onto a second line and gives up its far-end alignment, which
     only ever made sense on a row wide enough to have two ends. */
  .bsft-list {
    overflow-x: auto;
  }

  .bsft-list-pagesize {
    margin-left: 0;
  }

  .bsft-list-pager {
    flex-wrap: wrap;
  }

  /* A split pane on a phone is not a split pane. There is room for one column, so the column is
     the pane the caller nominated and the other is reached through the swap control — which is
     the same trade the navigation makes one breakpoint above: the second thing on screen becomes
     the second thing you ask for. */
  .bsft-split {
    flex-direction: column;
  }

  /* The pane that is not showing and the control that would resize it are both gone, which takes
     them out of the accessibility tree along with the pixels. */
  .bsft-split-divider,
  .bsft-split[data-bsft-split-pane="primary"] .bsft-split-secondary,
  .bsft-split[data-bsft-split-pane="secondary"] .bsft-split-primary {
    display: none;
  }

  /* Whatever basis the wide shape persisted means nothing in one column: the pane that is showing
     takes the height it is given. */
  .bsft-split-primary,
  .bsft-split-secondary {
    flex: 1 1 auto;
  }
}

@media (min-width: 48rem) {
  /* Wide: the navigation is a sidebar beside the content instead of a band above it. Grid
     rather than flex, because the shell's markup order — header, navigation, main — is the
     reading order and must not change to get a two-column layout. */
  .bsft-shell {
    display: grid;
    grid-template-columns: minmax(12rem, 16rem) 1fr;
    grid-template-rows: auto 1fr;
    grid-template-areas:
      "header header"
      "nav main";
  }

  .bsft-shell-header {
    grid-area: header;
  }

  .bsft-shell-nav {
    grid-area: nav;
  }

  .bsft-shell-main {
    grid-area: main;
    min-width: 0;
  }

  /* There is no drawer up here, so there is no control for one and no layer over the page. */
  .bsft-shell-nav-toggle,
  .bsft-shell-nav-backdrop {
    display: none;
  }

  /* Both panes are on screen up here, so there is nothing to swap to and no control for it. */
  .bsft-split-swap {
    display: none;
  }
}

/* --- drag and drop ------------------------------------------------------- */

/* A reorderable list is a list, not a table: the rows carry their own border and the container
   carries none, so a list dropped into a panel does not draw a second frame around it. */
.bsft-reorder {
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--bsft-font);
  color: var(--bsft-color-text);
}

.bsft-reorder-item {
  display: flex;
  align-items: center;
  gap: var(--bsft-spacing);
  padding: var(--bsft-spacing);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  cursor: grab;
}

.bsft-reorder-item + .bsft-reorder-item {
  margin-top: calc(var(--bsft-spacing) / 2);
}

/* Every row is focusable, so the focus ring is the keyboard path's only affordance and is never
   removed — the visible half of "pick up, move, drop". */
.bsft-reorder-item:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

/* The held row, whether it was picked up by pointer or by key: one state, because the two
   gestures are one gesture with two inputs. */
.bsft-reorder-item.is-held {
  border-color: var(--bsft-color-primary);
  cursor: grabbing;
  opacity: 0.7;
}

/* The position the held row would take. Only ever set where the target actually accepts it, so
   the marker is a promise the drop keeps rather than a highlight under every pointer. */
.bsft-reorder-item.is-over {
  border-color: var(--bsft-color-primary);
  box-shadow: inset 0 2px 0 0 var(--bsft-color-primary);
}

.bsft-file-drop {
  display: flex;
  flex-direction: column;
  gap: var(--bsft-spacing);
  padding: calc(var(--bsft-spacing) * 2);
  border: 1px dashed var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  font-family: var(--bsft-font);
  color: var(--bsft-color-text);
}

/* A drag that will land and one that will not are told apart by colour *and* by the refusal the
   surface states after the drop, never by colour alone. */
.bsft-file-drop.is-over {
  border-style: solid;
  border-color: var(--bsft-color-primary);
  background: var(--bsft-color-info-surface);
}

.bsft-file-drop.is-over.is-refusing {
  border-color: var(--bsft-color-danger);
  background: var(--bsft-color-danger-surface);
}

.bsft-file-drop-statement {
  margin: 0;
  color: var(--bsft-color-muted);
}

.bsft-file-drop-rejections {
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--bsft-color-danger);
}

.bsft-file-drop-progress {
  height: 0.5rem;
  overflow: hidden;
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-skeleton);
}

.bsft-file-drop-progress-bar {
  height: 100%;
  background: var(--bsft-color-primary);
  transition: width var(--bsft-motion-fast) linear;
}

/* --- file preview ----------------------------------------------------------------- */

/* A figure, because that is what it is: a caption naming the file and one rendering of its
   contents beneath. Every payload below is bounded in height rather than left to grow — a preview
   that pushes the rest of the screen off it has stopped being a preview. */
.bsft-file-preview {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
  margin: 0;
  padding: var(--bsft-spacing);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
}

.bsft-file-preview-caption {
  display: flex;
  gap: var(--bsft-spacing);
  align-items: baseline;
  justify-content: space-between;
}

.bsft-file-preview-name {
  overflow-wrap: anywhere;
  font-weight: 600;
}

.bsft-file-preview-size {
  flex: none;
  color: var(--bsft-color-muted);
}

/* Contained rather than stretched: an image is shown at whatever size fits, and a portrait scan
   must not become a column of empty page. */
.bsft-file-preview-image {
  max-width: 100%;
  max-height: 32rem;
  object-fit: contain;
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-skeleton);
}

.bsft-file-preview-pdf {
  width: 100%;
  height: 32rem;
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
}

/* pre-wrap rather than pre: a text file's own line breaks are kept, and a line longer than the
   surface wraps instead of scrolling the whole page sideways. */
.bsft-file-preview-text {
  max-height: 24rem;
  margin: 0;
  overflow: auto;
  padding: var(--bsft-spacing);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-skeleton);
  font-family: ui-monospace, "Cascadia Mono", "Consolas", monospace;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The table scrolls inside its own box. A CSV with sixty columns is ordinary, and letting it set
   the width of the screen would break every layout it appears in. */
.bsft-file-preview-csv {
  display: block;
  max-height: 24rem;
  overflow: auto;
  border-collapse: collapse;
}

.bsft-file-preview-csv th,
.bsft-file-preview-csv td {
  padding: calc(var(--bsft-spacing) / 2);
  border: 1px solid var(--bsft-color-border);
  text-align: left;
  white-space: nowrap;
}

.bsft-file-preview-csv th {
  position: sticky;
  top: 0;
  background: var(--bsft-color-surface);
}

.bsft-file-preview-fallback {
  display: flex;
  flex-direction: column;
  gap: calc(var(--bsft-spacing) / 2);
}

/* --- command palette -------------------------------------------------------------- */

/* The armed wrapper is layout-neutral on purpose: it contains the whole application, so anything
   it drew — a box, a padding, a stacking context — would be a frame around every screen. It is
   here only to carry tabindex and the focus ring the ordinary rules would otherwise give a
   focusable element covering the viewport. */
.bsft-command-palette-root {
  display: contents;
}

.bsft-command-palette-root:focus,
.bsft-command-palette-root:focus-visible {
  outline: none;
}

.bsft-command-palette {
  display: flex;
  flex-direction: column;
  gap: var(--bsft-spacing);
  /* Wide enough to read a whole label, capped so the panel does not become the page on a large
     screen — the dialog centres whatever it is given. */
  width: min(36rem, 90vw);
  font-family: var(--bsft-font);
}

.bsft-command-palette-input {
  box-sizing: border-box;
  width: 100%;
  min-height: 2.75rem;
  padding: var(--bsft-spacing) calc(var(--bsft-spacing) * 2);
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
  color: var(--bsft-color-text);
  font: inherit;
}

.bsft-command-palette-input:focus-visible {
  outline: 2px solid var(--bsft-color-primary);
  outline-offset: 2px;
}

.bsft-command-palette-list {
  /* Bounded and scrolled rather than paged: the ranking has already put the answer at the top,
     so what is below the fold is the long tail and never the thing being looked for. */
  max-height: 20rem;
  overflow-y: auto;
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--bsft-color-border);
  border-radius: var(--bsft-radius);
  background: var(--bsft-color-surface);
}

.bsft-command-palette-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--bsft-spacing);
  min-height: 2.75rem;
  padding: var(--bsft-spacing) calc(var(--bsft-spacing) * 2);
  cursor: pointer;
  color: var(--bsft-color-text);
}

.bsft-command-palette-option-active {
  /* Where the keyboard is — which in this control is also what Enter would run, since a palette
     binds no value of its own. */
  background: var(--bsft-color-info-surface);
}

.bsft-command-palette-option-kind {
  color: var(--bsft-color-muted);
  font-size: 0.875em;
  white-space: nowrap;
}

/* --- print ------------------------------------------------------------------------ */

/* What a printed page of a BSFT screen is: the record the detail or list layout holds, without
   any of the chrome that only means something to somebody who can click it. Nothing here sets a
   colour — the printer's own rendering of the theme's tokens is closer to right than a guess at
   what paper wants, and a literal here would be the one place in this file that is not a
   token. */
@media print {
  .bsft-shell-header,
  .bsft-shell-nav,
  .bsft-shell-nav-toggle,
  .bsft-shell-nav-backdrop,
  .bsft-toasts,
  .bsft-list-filterbar,
  .bsft-list-bulk,
  .bsft-list-pager,
  .bsft-file-drop {
    display: none;
  }

  /* The shell stops reserving a viewport's height: on paper that is a blank first sheet. */
  .bsft-shell {
    display: block;
    min-height: 0;
  }

  /* A scroll container prints only what was scrolled into view, so both layouts give up their
     overflow and let the content paginate instead. */
  .bsft-list,
  .bsft-detail,
  .bsft-scroll-region {
    overflow: visible;
  }

  /* A row split across a page break is a row the reader has to reassemble; the header repeats
     on every sheet so the columns stay named. */
  .bsft-list tr {
    break-inside: avoid;
  }

  .bsft-list thead {
    display: table-header-group;
  }
}
