/* FLARE portal — component layer.
 *
 * Built on tokens.css. No framework, no build step, no npm.
 *
 * Rules from docs/design.md that govern everything here:
 *   1. Square corners, everywhere. Never write a non-zero border-radius.
 *   2. Hairline chrome. Every border is 1px low-contrast; hierarchy comes from
 *      type, not boxes.
 *   3. Theme-aware. Light, dark and system.
 */

/* ─── Theming ────────────────────────────────────────────────────────────────
 *
 * tokens.css is vendored and generated, so it is never edited. Instead the
 * component layer consumes app-level aliases, and only those flip per theme.
 * Dark is the default. Light is a genuine theme, not an inversion: the cyan
 * glow mutes to a neutral shadow on white, and the action colour steps one
 * shade darker because #3ABCF8 on white is ~1.9:1 and fails contrast for text.
 */

:root,
:root[data-theme='dark'] {
  --app-bg:          var(--bf-bg-page);            /* #070D1D */
  --app-surface:     var(--bf-bg-surface);         /* #0B1020 */
  --app-surface-alt: var(--bf-bg-surface-alt);     /* #192858 */
  --app-text:        var(--bf-text-on-dark);
  --app-text-muted:  var(--bf-text-on-dark-muted);
  --app-border:      var(--bf-border-default);
  --app-link:        var(--bf-color-cyan-400);
  --app-elevation:   var(--bf-shadow-flare-soft);  /* the signature flare */
  --app-input-bg:    var(--bf-bg-page);
  --app-btn2-hover:  var(--bf-action-secondary);
  /* A whisper of the signature cyan under a hovered row — enough to say
     "live", never enough to fight the text on it. */
  --app-row-hover:   rgba(58, 188, 248, 0.07);
  /* The brand red is 3.22:1 on the dark surface. Lightened for dark only, so
     destructive actions clear AA in both themes rather than one. */
  --app-danger:      #FF6B6B;
  --app-danger-hover-tint: rgba(255, 107, 107, 0.12);
  /* Same step-down as --app-link: --bf-border-accent is flat cyan-400 in
     every theme, which is ~2.16:1 on light backgrounds and fails the 3:1
     WCAG 1.4.11 floor for a focus indicator. */
  --app-focus-ring:  var(--bf-color-cyan-400);
  color-scheme: dark;
}

:root[data-theme='light'] {
  --app-bg:          var(--bf-bg-page-light);      /* #FFFFFF */
  --app-surface:     var(--bf-color-neutral-100);  /* #F6F6F6 */
  --app-surface-alt: var(--bf-color-neutral-100);
  --app-text:        var(--bf-text-on-light);
  --app-text-muted:  var(--bf-text-on-light-muted);
  --app-border:      var(--bf-border-subtle);      /* #D9D9D9 hairline */
  /* One shade darker so link text clears AA on white. */
  --app-link:        var(--bf-color-cyan-600);
  --app-danger:      #C7162B;
  --app-danger-hover-tint: rgba(199, 22, 43, 0.12);
  /* The flare mutes on white — never a cyan glow on a light surface. */
  --app-elevation:   var(--bf-shadow-elevation-1);
  --app-input-bg:    var(--bf-bg-page-light);
  --app-btn2-hover:  var(--bf-color-neutral-100);
  --app-row-hover:   rgba(55, 118, 190, 0.06);
  /* One shade darker, same reasoning as --app-link above. */
  --app-focus-ring:  var(--bf-color-cyan-600);
  color-scheme: light;
}

/* System preference, used when the user has not chosen explicitly.
   `data-theme` is absent in that case, so this must not out-specify the
   explicit selectors above — hence :root:not([data-theme]). */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --app-bg:          var(--bf-bg-page-light);
    --app-surface:     var(--bf-color-neutral-100);
    --app-surface-alt: var(--bf-color-neutral-100);
    --app-text:        var(--bf-text-on-light);
    --app-text-muted:  var(--bf-text-on-light-muted);
    --app-border:      var(--bf-border-subtle);
    --app-link:        var(--bf-color-cyan-600);
  --app-danger:      #C7162B;
    --app-danger-hover-tint: rgba(199, 22, 43, 0.12);
    --app-elevation:   var(--bf-shadow-elevation-1);
    --app-input-bg:    var(--bf-bg-page-light);
    --app-btn2-hover:  var(--bf-color-neutral-100);
    --app-row-hover:   rgba(55, 118, 190, 0.06);
    --app-focus-ring:  var(--bf-color-cyan-600);
    color-scheme: light;
  }
}

/* ─── Reset ─────────────────────────────────────────────────────────────── */

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

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

body {
  margin: 0;
  background: var(--app-bg);
  color: var(--app-text);
  font-family: 'Segoe UI', Selawik, system-ui, -apple-system, sans-serif;
  font-size: var(--bf-text-base);
  line-height: 20px;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  font-family: Sora, 'Segoe UI', Selawik, system-ui, sans-serif;
  margin: 0;
}

img { max-width: 100%; height: auto; display: block; }

a { color: var(--app-link); text-decoration: none; }
a:hover { text-decoration: underline; }

:focus-visible {
  outline: 2px solid var(--app-focus-ring);
  outline-offset: 2px;
}

/* ─── Type scale ────────────────────────────────────────────────────────── */

.bf-display-2 { font-size: var(--bf-text-3xl); line-height: 48px; font-weight: 700; }
.bf-h2        { font-size: var(--bf-text-2xl); line-height: 36px; font-weight: 600; }
.bf-h3        { font-size: var(--bf-text-xl);  line-height: 28px; font-weight: 700; }
.bf-lead      { font-size: var(--bf-text-md);  line-height: 28px; color: var(--app-text-muted); }
.bf-body-sm   { font-size: var(--bf-text-sm);  line-height: 20px; }
.bf-caption   { font-size: var(--bf-text-xs);  line-height: 16px; color: var(--app-text-muted); }
.bf-eyebrow {
  font-size: var(--bf-text-xs);
  line-height: 12px;
  font-weight: 700;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--app-link);
}
.bf-muted { color: var(--app-text-muted); }

/* ─── Layout ─────────────────────────────────────────────────────────────
 *
 * 80px section padding is a *marketing page* rhythm. This is a dense
 * application UI where a screenful of chrome before any content is wasted
 * space, so app pages use 40px (32 on mobile). A deliberate choice, not an
 * oversight — do not "fix" it back.
 */

.bf-container {
  max-width: var(--bf-container-max);
  margin: 0 auto;
  padding: 0 var(--bf-gutter);
}

.bf-section { padding: var(--bf-space-10) 0; }

.bf-stack   { display: flex; flex-direction: column; gap: var(--bf-space-3); }
.bf-stack-6 { display: flex; flex-direction: column; gap: var(--bf-space-4); }
.bf-row     { display: flex; align-items: center; gap: var(--bf-space-4); }
.bf-row-between { display: flex; align-items: center; justify-content: space-between;
                  gap: var(--bf-space-4); }

@media (max-width: 640px) {
  .bf-container { padding: 0 var(--bf-gutter-mobile); }
  .bf-section   { padding: var(--bf-space-8) 0; }
}

/* ─── Theme control — one button, cycling system / light / dark ─────────── */
/*
 * A single control rather than three segments: the theme is a preference set
 * rarely, and three permanently-visible buttons spent appbar width on it every
 * page. The cycle keeps all three states -- including "match system", which is
 * required -- and the icon reports which one is active.
 */

.bf-theme {
  display: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  background: transparent;
  color: var(--app-text-muted);
  border: 1px solid var(--app-border);
  cursor: pointer;
}
.bf-theme:hover { color: var(--app-text); }
.bf-theme__icon { width: 16px; height: 16px; }
/* Without JS the control cannot work, so it does not pretend to exist. */
.js .bf-theme { display: inline-flex; }

/* ─── Surfaces ──────────────────────────────────────────────────────────── */

.bf-panel {
  background: var(--app-surface);
  border: 1px solid var(--app-border);
  box-shadow: var(--app-elevation);
  padding: var(--bf-space-4);
}

.bf-panel__title {
  font-size: var(--bf-text-md);
  line-height: 22px;
  font-weight: 700;
  margin-bottom: var(--bf-space-3);
}

.bf-auth {
  max-width: 380px;
  margin: 0 auto;
  padding: var(--bf-space-10) var(--bf-gutter);
}

/* ─── Tiles: the dashboard's at-a-glance grid ────────────────────────────────
 *
 * A KPI strip. auto-fill rather than auto-fit so an isolated tile keeps its
 * measure instead of stretching across the row, and each tile is bordered like
 * a bf-panel so a lone tile and a stack of them read as the same product. The
 * gap matches bf-stack-6, so a tilegrid drops into a work zone at the panels'
 * own rhythm. */
.bf-tilegrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--bf-space-4);
}

.bf-tile {
  background: var(--app-surface);
  border: 1px solid var(--app-border);
  box-shadow: var(--app-elevation);
  padding: var(--bf-space-4);
  display: flex;
  flex-direction: column;
  gap: var(--bf-space-1);
}

/* The eyebrow grammar (see .bf-eyebrow) at the tile scale: muted here, because
   a wall of link-cyan labels would fight the numerals they caption. */
.bf-tile__label {
  font-size: 11px;
  line-height: 12px;
  font-weight: 700;
  letter-spacing: var(--bf-tracking-wider);
  text-transform: uppercase;
  color: var(--app-text-muted);
}
/* Tabular figures so a column of values keeps its decimal alignment as the
   numbers change under it. */
.bf-tile__value {
  font-family: var(--bf-font-display);
  font-size: var(--bf-text-2xl);
  line-height: var(--bf-leading-display);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.bf-tile__sub  { font-size: var(--bf-text-xs); line-height: 16px; color: var(--app-text-muted); }
.bf-tile__link { font-size: var(--bf-text-sm); color: var(--app-link); }

/* One tile carrying two tiles' worth of value spans the pair. */
.bf-tile--span2 { grid-column: span 2; }

/* A left marker states the tile's condition without recolouring its numerals —
   the value stays readable, the border carries the alarm. Warning is flat in
   both themes; danger flips with the theme like the rest of the destructive UI. */
.bf-tile.is-caution  { border-left: 3px solid var(--bf-color-warning); }
.bf-tile.is-negative { border-left: 3px solid var(--app-danger); }

/* Two-up where 220px columns would each be too narrow to breathe. */
@media (max-width: 640px) {
  .bf-tilegrid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
}

/* ─── Buttons ───────────────────────────────────────────────────────────── */

.bf-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--bf-space-2);
  padding: var(--bf-space-3) var(--bf-space-6);
  font-family: inherit;
  font-size: var(--bf-text-base);
  font-weight: 600;
  line-height: 20px;
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  /* no border-radius — square corners, everywhere */
}
.bf-btn:hover { text-decoration: none; }
.bf-btn:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; }

.bf-btn--primary {
  background: var(--bf-action-primary);
  color: var(--bf-action-primary-text);
}
.bf-btn--primary:hover:not(:disabled) {
  background: var(--bf-action-primary-hover);
  box-shadow: var(--bf-shadow-flare-hover);
}

.bf-btn--secondary {
  background: transparent;
  color: var(--app-text);
  border-color: var(--app-border);
}
.bf-btn--secondary:hover:not(:disabled) { background: var(--app-btn2-hover); }

.bf-btn--danger {
  background: transparent;
  color: var(--app-danger);
  border-color: var(--app-danger);
}
.bf-btn--danger:hover:not(:disabled) { background: var(--app-danger-hover-tint); }

/* Sibling forms in a panel are separate actions -- on the connector panels one
   saves config and the next spends money sending a real SMS. bf-stack only
   spaces items *within* a form, so without this their edges touch. */
.bf-panel > form + form,
.bf-panel > .bf-table-wrap + form,
.bf-panel > form + .bf-table-wrap {
  margin-block-start: var(--bf-space-5);
  padding-block-start: var(--bf-space-5);
  border-block-start: 1px solid var(--app-border);
}

.bf-btn--block { width: 100%; }
.bf-btn--sm { padding: var(--bf-space-2) var(--bf-space-4); font-size: var(--bf-text-sm); }

/* ─── Forms ─────────────────────────────────────────────────────────────── */

.bf-field { display: flex; flex-direction: column; gap: var(--bf-space-2); }

.bf-label { font-size: var(--bf-text-sm); line-height: 18.6px; font-weight: 600; }

.bf-input,
.bf-select {
  font-family: inherit;
  font-size: var(--bf-text-base);
  line-height: 20px;
  padding: var(--bf-space-3) var(--bf-space-4);
  background: var(--app-input-bg);
  color: var(--app-text);
  border: 1px solid var(--app-border);
  width: 100%;
}
.bf-input:focus,
.bf-select:focus {
  border-color: var(--bf-border-accent);
  /* The glow is decoration; the ring is the actual keyboard indicator, so it
     is no longer suppressed. :focus-visible keeps it off for mouse clicks. */
  outline: revert;
  box-shadow: var(--bf-shadow-flare-hover);
}

.bf-help { font-size: var(--bf-text-xs); line-height: 16px; color: var(--app-text-muted); }

/* ─── Status: a small coloured dot plus a text label, never colour alone ─── */

.bf-status { display: inline-flex; align-items: center; gap: var(--bf-space-2);
             font-size: var(--bf-text-sm); }
.bf-status::before {
  content: '';
  width: 8px; height: 8px;
  border-radius: 50%;   /* semantically round — the documented exception */
  background: currentColor;
  flex: none;
}
/* Status colours are theme-dependent. The single set previously used failed
   WCAG AA in both directions, measured in-browser: caution 3.08:1 on the light
   surface, and on dark, negative 3.22:1 and positive 3.91:1 — all under 4.5:1
   for normal text. Darker on light, lighter on dark. */
.bf-status--positive    { color: #0A6B1A; }
.bf-status--caution     { color: #8A5200; }
.bf-status--negative    { color: #A5101F; }
/* Neutral is grey on purpose: an invitation is a person expected, not a state
   worth a colour yet. */
.bf-status--neutral     { color: #59636E; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .bf-status--positive { color: #56D364; }
  :root:not([data-theme='light']) .bf-status--caution  { color: #E3A008; }
  :root:not([data-theme='light']) .bf-status--negative { color: #FF7B72; }
  :root:not([data-theme='light']) .bf-status--neutral  { color: #9198A1; }
}
:root[data-theme='dark'] .bf-status--positive { color: #56D364; }
:root[data-theme='dark'] .bf-status--caution  { color: #E3A008; }
:root[data-theme='dark'] .bf-status--negative { color: #FF7B72; }
:root[data-theme='dark'] .bf-status--neutral  { color: #9198A1; }
.bf-status--information { color: #3776BE; }

/* ─── Add-person disclosure ─────────────────────────────────────────────── */

/* The personnel screen's add flow lives behind a plain <details>, so reading
   the list — the common case — pays no form, and the toggle needs no script.
   Open, the fields take the button's place in its row rather than stacking
   under it: the row was the affordance, the row becomes the form. */
.bf-add > summary { list-style: none; }
.bf-add > summary::-webkit-details-marker { display: none; }
.bf-add[open] > summary { display: none; }
.bf-btn svg { width: 1em; height: 1em; }

/* Permission rows on the role editor: the whole row is the checkbox's label,
   so the click target is the row, not a 16px square. */
.bf-perm { cursor: pointer; align-items: center; }
.bf-perm input[type='checkbox'] { flex: none; margin: 0; }
.bf-cap { text-transform: capitalize; }

/* Inputs are 100%-wide by default for forms; inside an inline add-row they
   share the line instead, or every field stacks. */
.bf-add__fields .bf-input,
.bf-add__fields .bf-select { width: auto; flex: 1 1 10rem; }

/* ─── Folds: collapsible panels and subsections ─────────────────────────── */

/* A category of permissions is a drawer, closed until wanted: the chevron is
   the same right-pointing promise the nav rows make, rotating down when the
   drawer is open. Native <details>, so it works before any script loads —
   and the search script holds folds open while a query is live, because a
   hit hidden behind a closed fold is a hit that does not exist. */
.bf-fold > summary { list-style: none; display: flex; align-items: center;
                     gap: var(--bf-space-3); cursor: pointer;
                     padding-block: var(--bf-space-3); }
.bf-fold > summary::-webkit-details-marker { display: none; }
.bf-fold__title { font-weight: 600; }
/* Drawers in one card read as table rows: hairline-separated, no card each. */
.bf-panel > .bf-fold + .bf-fold { border-block-start: 1px solid var(--app-border); }
.bf-fold__chev { flex: none; width: 8px; height: 16px;
                 color: var(--app-text-muted); transition: transform 120ms ease; }
.bf-fold[open] > summary > .bf-fold__chev { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .bf-fold__chev { transition: none; } }
.bf-subfold { margin-inline-start: var(--bf-space-6); }
.bf-subfold > summary { padding-block: var(--bf-space-2); }
.bf-subfold__title { font-weight: 600; }
.bf-add__fields { display: flex; flex-wrap: wrap; align-items: center;
                  gap: var(--bf-space-3); }

/* ─── Alerts ────────────────────────────────────────────────────────────── */

.bf-alert {
  border: 1px solid var(--app-border);
  padding: var(--bf-space-3) var(--bf-space-4);
  font-size: var(--bf-text-sm);
  line-height: 20px;
  background: var(--app-surface);
}
.bf-alert--error   { border-color: #C7162B; }
.bf-alert--success { border-color: #0E8420; }
/* Both of these were used in markup with no rule to match, so the element
   rendered as a plain neutral box — including on the panel showing a freshly
   minted credential, which is the one alert on that page that must not be
   missed. `--info` is neutral prose; `--notice` marks something shown once. */
.bf-alert--info    { border-color: var(--app-border); }
.bf-alert--notice  { border-color: var(--bf-color-cyan-400); }

/* ─── Tables: open, not filled ──────────────────────────────────────────── */

.bf-table { width: 100%; border-collapse: collapse; font-size: var(--bf-text-sm); }
.bf-table th {
  text-align: left;
  font-weight: 600;
  padding: var(--bf-space-2) var(--bf-space-3);
  border-bottom: 1px solid var(--app-border);
  white-space: nowrap;
}
.bf-table td {
  padding: var(--bf-space-2) var(--bf-space-3);
  border-bottom: 1px solid var(--app-border);
  vertical-align: middle;
}
.bf-table tr:last-child td { border-bottom: none; }
.bf-table-wrap { overflow-x: auto; }

/* ─── Tags — permission keys and role names ─────────────────────────────── */

.bf-tag {
  display: inline-block;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--bf-text-xs);
  line-height: 16px;
  padding: 2px var(--bf-space-2);
  border: 1px solid var(--app-border);
  color: var(--app-text-muted);
  margin: 2px 2px 2px 0;
}
.bf-tag--role { color: var(--app-link); border-color: var(--app-link); }

/* ─── Footer ────────────────────────────────────────────────────────────── */

.bf-footer {
  border-top: 1px solid var(--app-border);
  padding: var(--bf-space-5) 0;
  margin-top: var(--bf-space-10);
  color: var(--app-text-muted);
  font-size: var(--bf-text-xs);
}

/* ─── Utilities ─────────────────────────────────────────────────────────── */

.bf-visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Every page re-walks the full rail before reaching <main>; this lets a
   keyboard user jump past it. Undoes bf-visually-hidden on focus rather than
   toggling the class, so it stays keyboard-reachable at all times. */
.bf-skip-link:focus {
  position: fixed; top: var(--bf-space-2); left: var(--bf-space-2);
  width: auto; height: auto; margin: 0; padding: var(--bf-space-2) var(--bf-space-4);
  overflow: visible; clip: auto; white-space: normal;
  z-index: 1000;
  background: var(--app-surface);
  color: var(--app-text);
  border: 1px solid var(--bf-border-accent);
}

/* ─── Application shell — navy rail + content ────────────────────────────────
 *
 * Follows the Landscape pattern already established in
 * the technical-portal pattern: a dark navigation rail beside light or
 * dark content.
 *
 * The rail stays navy in BOTH themes. That is the signature of the look, and
 * it is why the rail uses its own fixed colours rather than the --app-* theme
 * aliases — those flip, and the rail must not.
 */

.bf-shell { display: flex; min-height: 100vh; }

.bf-rail {
  --rail-bg:     var(--bf-color-navy-900);
  --rail-text:   var(--bf-color-neutral-0);
  --rail-muted:  var(--bf-color-neutral-300);
  --rail-border: var(--bf-color-navy-700);

  flex: none;
  width: 232px;
  background: var(--rail-bg);
  color: var(--rail-text);
  border-right: 1px solid var(--rail-border);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh; /* fallback chain: dvh tracks the visible viewport where supported */
  overflow-y: auto;
  overscroll-behavior: contain;
  transition: width 120ms ease;
}

/* Collapsed: icons only. Labels are hidden rather than removed so the DOM —
   and therefore the accessible name of each link — is unchanged. */
.bf-shell.is-collapsed .bf-rail { width: 56px; }
.bf-shell.is-collapsed .bf-rail__label,
.bf-shell.is-collapsed .bf-rail__brand-text,
.bf-shell.is-collapsed .bf-rail__section,
.bf-shell.is-collapsed .bf-rail__expand,
.bf-shell.is-collapsed .bf-rail__sub { display: none; }
.bf-shell.is-collapsed .bf-rail__link { justify-content: center; padding-left: 0; padding-right: 0; }

.bf-rail__head {
  display: flex;
  align-items: center;
  gap: var(--bf-space-2);
  padding: var(--bf-space-3) var(--bf-space-3);
  border-bottom: 1px solid var(--rail-border);
  min-height: 56px;
}
.bf-rail__brand { display: flex; align-items: center; gap: var(--bf-space-2); color: inherit; overflow: hidden; }
.bf-rail__brand img { height: 20px; width: auto; }

.bf-rail__toggle {
  margin-left: auto;
  background: transparent;
  border: 0;
  color: var(--rail-muted);
  cursor: pointer;
  padding: var(--bf-space-1);
  display: inline-flex;
}
.bf-rail__toggle:hover { color: var(--rail-text); }
.bf-shell.is-collapsed .bf-rail__toggle { margin: 0 auto; }

.bf-rail__nav { padding: var(--bf-space-2) 0; }
.bf-rail__nav--foot { margin-top: auto; border-top: 1px solid var(--rail-border); }

.bf-rail__section {
  font-size: var(--bf-text-xs);
  line-height: 16px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--rail-muted);
  padding: var(--bf-space-3) var(--bf-space-4) var(--bf-space-1);
}

.bf-rail__item { position: relative; display: flex; align-items: center; }

.bf-rail__link {
  flex: 1;
  display: flex;
  align-items: center;
  gap: var(--bf-space-3);
  padding: var(--bf-space-2) var(--bf-space-4);
  color: var(--rail-text);
  font-size: var(--bf-text-sm);
  font-weight: 600;
  text-decoration: none;
  min-height: 36px;
  /* A left marker rather than a fill: hairline chrome, and it survives
     collapse where a background would look like a stray block. */
  border-left: 2px solid transparent;
}
.bf-rail__link:hover { text-decoration: none; background: var(--bf-color-navy-800); }
.bf-rail__link[aria-current='page'] {
  border-left-color: var(--bf-color-cyan-400);
  background: var(--bf-color-navy-800);
}

.bf-rail__icon { width: 18px; height: 18px; flex: none; }
.bf-rail__label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Expander for a group. A real <button> so it is keyboard-operable and its
   state is announced; aria-expanded is the single source of truth. */
.bf-rail__expand {
  background: transparent;
  border: 0;
  color: var(--rail-muted);
  cursor: pointer;
  padding: var(--bf-space-2);
  display: inline-flex;
  align-items: center;
}
.bf-rail__expand:hover { color: var(--rail-text); }
.bf-rail__expand svg { width: 12px; height: 12px; transition: transform 120ms ease; }
.bf-rail__expand[aria-expanded='true'] svg { transform: rotate(180deg); }

/* Children are hidden by default — "Sites" starts collapsed. */
.bf-rail__sub { display: none; }
.bf-rail__sub.is-open { display: block; }
.bf-rail__sub .bf-rail__link {
  /* Reserves an icon-sized gap so a label-only row still lines up under the
     group's own label. The hub row has a real icon instead of reserving the
     gap, so it resets to the plain base padding just below. */
  padding-left: calc(var(--bf-space-4) + 18px + var(--bf-space-3));
  font-weight: 400;
  color: var(--rail-muted);
  min-height: 32px;
}
.bf-rail__sub .bf-rail__link:has(.bf-rail__icon) { padding-left: var(--bf-space-4); }
.bf-rail__sub .bf-rail__link:hover,
.bf-rail__sub .bf-rail__link[aria-current='page'] { color: var(--rail-text); }
.bf-rail__empty {
  padding: var(--bf-space-2) var(--bf-space-4) var(--bf-space-2)
           calc(var(--bf-space-4) + 18px + var(--bf-space-3));
  font-size: var(--bf-text-xs);
  color: var(--rail-muted);
}

/* ─── Content column ────────────────────────────────────────────────────── */

.bf-content { flex: 1; min-width: 0; display: flex; flex-direction: column; }

.bf-appbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--bf-space-4);
  padding: var(--bf-space-3) var(--bf-gutter);
  border-bottom: 1px solid var(--app-border);
  min-height: 56px;
}
.bf-appbar__right { display: flex; align-items: center; gap: var(--bf-space-4); }

/* The signed-in username, which links to account settings.
 *
 * Muted like the text it replaced, so the appbar does not grow a second piece
 * of prominent chrome next to Sign out — but it underlines on hover and focus
 * so it reads as the control it now is.
 *
 * Truncated rather than allowed to push: the appbar is the one row the
 * stylesheet's no-sideways-scroll rule does not otherwise protect, and a long
 * username at 390px would widen the page. */
.bf-appbar__user {
  color: var(--app-text-muted);
  text-decoration: none;
  max-width: 16ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bf-appbar__user:hover,
.bf-appbar__user:focus-visible { color: var(--app-text); text-decoration: underline; }
.bf-appbar__user[aria-current='page'] { color: var(--app-text); }

.bf-main { padding: var(--bf-space-5) var(--bf-gutter); flex: 1; min-width: 0; }
.bf-main > * { max-width: var(--bf-container-max); }

/* Off-canvas below the medium breakpoint: a 232px rail on a phone leaves no
   room for content. */
@media (max-width: 900px) {
  .bf-rail {
    position: fixed;
    /* Pin to both edges rather than sizing with vh: fixed-position edge
       pinning tracks the visible viewport on iOS where vh does not. */
    top: 0;
    bottom: 0;
    height: auto;
    z-index: 20;
    transform: translateX(-100%);
    transition: transform 140ms ease;
  }
  .bf-shell.is-open .bf-rail { transform: translateX(0); }
  .bf-shell.is-collapsed .bf-rail { width: 232px; }
  .bf-shell.is-collapsed .bf-rail__label,
  .bf-shell.is-collapsed .bf-rail__brand-text,
  .bf-shell.is-collapsed .bf-rail__section,
  .bf-shell.is-collapsed .bf-rail__expand,
  .bf-shell.is-collapsed .bf-rail__sub { display: revert; }
  .bf-shell.is-collapsed .bf-rail__link { justify-content: flex-start; padding: var(--bf-space-2) var(--bf-space-4); }
  .bf-main { padding: var(--bf-space-5) var(--bf-gutter-mobile); }
}
/* The rail toggle only makes sense with scripting. */
.bf-rail__toggle, .bf-appbar__menu { display: none; }
.js .bf-rail__toggle { display: inline-flex; }
@media (max-width: 900px) { .js .bf-appbar__menu { display: inline-flex; } }

/* ─── Breadcrumbs ───────────────────────────────────────────────────────── */

/* In the main column, between the tab strip and the content: the crumb
   describes where you are inside the section the tabs chose. */
.bf-main .bf-crumbs { margin-block-end: var(--bf-space-4); }

.bf-crumbs ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bf-space-2);
  font-size: var(--bf-text-sm);
  line-height: 20px;
}
/* Separator is generated, not markup: it is decoration and should never be
   announced or selectable as content. */
.bf-crumbs li + li::before {
  content: '/';
  margin-right: var(--bf-space-2);
  color: var(--app-text-muted);
}
.bf-crumbs a { color: var(--app-text-muted); }
.bf-crumbs a:hover { color: var(--app-link); }
.bf-crumbs [aria-current='page'] { font-weight: 600; color: var(--app-text); }

/* Group rows in the rail are <button>, so they need the anchor's look. */
.bf-rail__link--group {
  background: transparent;
  border: 0;
  border-left: 2px solid transparent;
  font-family: inherit;
  cursor: pointer;
  width: 100%;
  text-align: left;
}
.bf-rail__chevron { width: 12px; height: 12px; margin-left: auto; flex: none;
                    transition: transform 120ms ease; color: var(--rail-muted); }
.bf-rail__link--group[aria-expanded='true'] .bf-rail__chevron { transform: rotate(180deg); }
.bf-shell.is-collapsed .bf-rail__chevron { display: none; }

/* Covers the rail's own motion: the desktop collapse/expand width transition
   (:bf-rail, above), the mobile off-canvas slide (:bf-rail under the
   max-width:900px query), and this chevron's rotation. Same specificity as
   those rules and later in source order, so it wins under both viewports. */
@media (prefers-reduced-motion: reduce) {
  .bf-rail, .bf-rail__chevron { transition: none; }
}

/* ─── Tabs ───────────────────────────────────────────────────────────────────
 *
 * A sidebar item with subcategories expresses them as horizontal tabs on the
 * page, never as nested rail entries. The rail answers
 * "where am I in the system"; tabs answer "which part of this thing".
 *
 * Real links, not JS panels — each tab is its own URL, so it is bookmarkable,
 * survives reload, and works without scripting.
 */

.bf-tabs {
  display: flex;
  gap: var(--bf-space-5);
  margin-bottom: var(--bf-space-6);
  /* The rule under the tabs is painted as a background rather than as a
     border-bottom, so the selected marker can sit *on* it without any element
     having to overhang the box.

     That overhang is what put a vertical scrollbar beside a horizontal tab
     strip. `overflow-x` here is not optional -- the tabs must scroll sideways
     on a narrow screen -- and setting it silently promotes a `visible`
     overflow-y to `auto`. The old `margin-bottom: -1px` on the links then
     stuck 1px past the content box, and Chromium spent a 15px scrollbar
     gutter on that 1px. */
  background-image: linear-gradient(to top, var(--app-border) 1px, transparent 1px);
  overflow-x: auto;
  /* Load-bearing, and not the same as omitting it: left unset this computes to
     `auto`, not `visible`, and any future 1px of overhang brings the scrollbar
     straight back. */
  overflow-y: hidden;
  /* When a keyboard user Tabs to an overflowed tab, the browser scrolls it into
     view. Reserve the fade's 24px as scroll padding so it lands *before* the
     masked zone rather than flush against the dimmed right edge. */
  scroll-padding-right: 24px;
}

.bf-tabs a {
  display: inline-block;
  padding: var(--bf-space-3) 0;
  font-size: var(--bf-text-sm);
  font-weight: 600;
  color: var(--app-text-muted);
  text-decoration: none;
  white-space: nowrap;
  /* The flex container stretches every link to the strip's full height, so
     this 2px edge lands exactly on the hairline the background paints and
     reads as part of the same rule -- what the old negative margin bought,
     without the overhang. */
  border-bottom: 2px solid transparent;
}
.bf-tabs a:hover { color: var(--app-text); text-decoration: none; }
.bf-tabs a[aria-current='page'] {
  color: var(--app-text);
  border-bottom-color: var(--bf-color-cyan-400);
}
/* The container clips vertically (see overflow-y above), which would clip the
   global focus ring's 2px offset top and bottom. Draw it inward instead --
   still 2px, just never past the link's own box. */
.bf-tabs a:focus-visible { outline-offset: -2px; }

/* A fade at the right edge when the strip overflows, so a half-clipped tab
   reads as "more this way" rather than a hard cut. Pure CSS: a horizontal
   mask on the scroll container, which lives in the border box and does NOT
   travel with the scrolled content, so it stays pinned to the visible right
   edge with no script. The gradient is horizontal only, so it never touches
   the underline along the bottom or the inward focus ring — those keep full
   alpha at every x outside the final 24px. And because the mask only dims
   whatever paints under that last 24px, it is self-gating: with few tabs the
   region is empty and the fade is invisible; it appears exactly when a tab's
   content runs off the edge. `scroll-padding-right` (above) keeps a
   keyboard-focused tab from scrolling under the fade; as a belt-and-suspenders
   guarantee for the case where the strip cannot scroll any further (the focused
   tab is already the last one, hard against the edge), the mask is dropped
   entirely while any tab holds focus, so a focus ring or label is never
   dimmed. */
.bf-tabs {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
          mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
}
.bf-tabs:focus-within {
  -webkit-mask-image: none;
          mask-image: none;
}

/* Rename affordance in the breadcrumb. Sits with the current page's label
   rather than in the page body, because it edits the thing the crumb names. */
.bf-crumbs__edit {
  display: inline-flex;
  align-items: center;
  margin-inline-start: .4rem;
  color: var(--app-text-muted);
}
.bf-crumbs__edit svg { width: 1em; height: 1em; }
.bf-crumbs__edit:hover { color: var(--app-link); }

/* "Add site" is an action, not a destination, so it is set apart from the
   sites above it without being a different control. */
.bf-rail__link--add { color: var(--app-text-muted); }
.bf-rail__link--add:hover { color: var(--app-link); }

/* Settings search. Filters in place — no results is a state of the list, not a
   separate page. */
.bf-search { position: relative; display: block; margin-block-end: 1rem; }
.bf-search svg {
  position: absolute; inset-inline-start: .625rem; inset-block-start: 50%;
  translate: 0 -50%; width: 1em; height: 1em; color: var(--app-text-muted);
  pointer-events: none;
}
.bf-search .bf-input { padding-inline-start: 2.1rem; }

/* One device in a section list. */
/* ─── Nav list: rows that open subsections ──────────────────────────────── */

/* The first templated list component (`ui::nav_row` in templates/ui.html):
   screens hand it records and it renders every one identically. The whole
   row is the link — a row that *is* a destination should not make the
   pointer hunt for a button — and the trailing chevron is the standing
   promise that a screen lies behind it. Same hairline grammar as
   .bf-device, so record lists and nav lists read as one product. */
.bf-nav__row {
  display: flex;
  align-items: center;
  gap: var(--bf-space-4);
  min-height: 44px;                    /* a full touch target, not a line of text */
  padding: var(--bf-space-3) var(--bf-space-2);
  color: inherit;
  text-decoration: none;
}
.bf-nav__row:hover { text-decoration: none; background: var(--app-row-hover); }
.bf-nav__row + .bf-nav__row { border-block-start: 1px solid var(--app-border); }
/* Same escape hatch as .bf-device's: the row's own display:flex would
   otherwise beat the hidden attribute the settings search sets. */
.bf-nav__row[hidden] { display: none; }
.bf-nav__title { font-weight: 500; flex: none; }
.bf-nav__meta {
  margin-inline-start: auto;
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--app-text-muted);
  font-size: .875rem;
}
.bf-nav__chevron {
  flex: none;
  width: 8px; height: 16px;
  margin-inline-start: auto;
  color: var(--app-text-muted);
  transition: transform 120ms ease;
}
/* When a meta is present it takes the free space; the chevron then sits
   snug against it instead of both auto-margins splitting the gap. */
.bf-nav__meta + .bf-nav__chevron { margin-inline-start: 0; }
.bf-nav__row:hover .bf-nav__chevron { transform: translateX(2px); }
@media (prefers-reduced-motion: reduce) {
  .bf-nav__chevron { transition: none; }
  .bf-nav__row:hover .bf-nav__chevron { transform: none; }
}

.bf-device { display: flex; align-items: baseline; gap: .75rem; padding: .625rem 0; }
.bf-device + .bf-device { border-block-start: 1px solid var(--app-border); }
.bf-device__name { font-weight: 500; }
.bf-device__meta { color: var(--app-text-muted); font-size: .875rem; }
.bf-device__actions { margin-inline-start: auto; display: flex; gap: .5rem; }
.bf-device[hidden] { display: none; }

/* Empty state: a single action, no explanation. */
.bf-empty { padding: 1.25rem 0; }

/* Awaiting/degraded plate: the formalized "the Warden has not published this
   yet" block. A dashed hairline says provisional where bf-panel's solid one
   says settled, and the centred, generously-padded frame reads as a held space
   rather than bf-empty's bare padding. The icon slot is optional — an <svg>
   dropped in leads the message. */
.bf-plate {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--bf-space-3);
  text-align: center;
  padding: var(--bf-space-8) var(--bf-space-5);
  border: 1px dashed var(--app-border);
  color: var(--app-text-muted);
  font-size: var(--bf-text-sm);
  line-height: 20px;
}
.bf-plate__icon { flex: none; width: 24px; height: 24px; color: var(--app-text-muted); }

/* Enrolment QR. Fixed light background regardless of theme: a dark-mode QR with
   inverted modules will not scan on many readers. */
.bf-qr {
  display: inline-block;
  padding: var(--bf-space-3);
  background: #fff;
  border: 1px solid var(--bf-border-default);
  margin-block-end: var(--bf-space-4);
}
.bf-qr svg { display: block; width: 240px; height: 240px; }

/* Narrow numeric input, so a 0-255 field does not span the panel. */
.bf-input--num { width: 6rem; }

/* ─── Mobile: overflow, breadcrumbs, full-screen rail ────────────────────── */

/* Nothing may scroll the page sideways. A single over-wide table or a long
   unbroken key has otherwise been enough to shift the whole layout, which on a
   phone reads as the app being broken rather than one element being wide. */
/* Deliberately NOT overflow-x: hidden. That hides a too-wide element instead of
   containing it, and still leaves the page programmatically scrollable — so it
   makes the fault invisible to the eye and to most tests, without fixing it.
   Containment below is what actually keeps the page in its viewport. */
html, body { max-width: 100%; }
.bf-shell, .bf-content, .bf-main { min-width: 0; }
.bf-main > * { max-width: min(var(--bf-container-max), 100%); }

/* Wide content scrolls inside its own box rather than pushing the page. */
/* `position: relative` is load-bearing, not decoration. An absolutely
   positioned descendant — a visually-hidden form label, say — is laid out
   against its nearest *positioned* ancestor, so without this it escapes the
   scroll clipping entirely and pins itself to the page at the table's full
   width. That is how a 1px-wide invisible label ends up making a phone scroll
   sideways by 39px. */
.bf-table-wrap { position: relative; overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Long opaque strings -- keys, base64, device paths -- are the usual culprits. */
code, .bf-device__meta { overflow-wrap: anywhere; }

@media (max-width: 640px) {
  /* Only the current page, not the trail. A wrapped three-line breadcrumb costs
     more vertical space than the heading it describes, and the parent links are
     reachable from the rail anyway. */
  .bf-crumbs li { display: none; }
  .bf-crumbs li:last-child { display: flex; align-items: center; }
  .bf-crumbs li:last-child::before { content: none; }
  .bf-crumbs ol { flex-wrap: nowrap; }

  /* Denser still where space is scarcest. */
  .bf-panel { padding: var(--bf-space-3); }
  .bf-main  { padding: var(--bf-space-4) var(--bf-gutter-mobile); }

  /* Rows become two lines rather than squeezing four columns onto one. */
  .bf-device { flex-wrap: wrap; gap: .25rem .75rem; }
  .bf-device__actions { margin-inline-start: auto; }
}

@media (max-width: 900px) {
  /* Full screen when opened. A 232px drawer with the page showing behind it
     invites taps that land on the page underneath, and leaves site names
     truncated for no benefit. */
  .bf-rail { width: 100%; max-width: 100%; }
  .bf-shell.is-collapsed .bf-rail { width: 100%; }
  .bf-rail__nav { font-size: var(--bf-text-base); }
}

/* Close control for the full-screen mobile rail. Hidden on desktop, where the
   rail is permanent and tapping outside is meaningful. */
.bf-rail__close {
  display: none;
  align-items: center;
  justify-content: center;
  width: 32px; height: 32px;
  padding: 0;
  background: transparent;
  color: var(--rail-muted);
  border: 0;
  cursor: pointer;
}
.bf-rail__close:hover { color: var(--rail-text); }
@media (max-width: 900px) {
  .js .bf-rail__close { display: inline-flex; }
  .bf-rail__toggle { display: none !important; }
}

/* Safety net: a table that escaped its scroll wrapper must still not widen the
   page. Templates should use .bf-table-wrap — this is what stops forgetting
   once from breaking every narrow viewport. */
.bf-table { width: 100%; }
@media (max-width: 640px) {
  .bf-panel > .bf-table { display: block; overflow-x: auto; }
}


/* Minimum tap targets. Measured at 14x14 (breadcrumb pen) and 13x13
   (checkboxes), both under the 24x24 floor; the theme button was also being
   flex-squeezed from 32px to 23px on pages with a long breadcrumb. */
.bf-crumbs__edit { min-width: 24px; min-height: 24px; justify-content: center; }
.bf-theme { flex: none; }
.bf-field input[type='checkbox'] { min-width: 18px; min-height: 18px; margin-inline-end: .35rem; }

/* Setup command block. The command is long and must be copied exactly, so it
   wraps rather than scrolls — a horizontally scrolled command hides the half
   someone forgot to select. */
.bf-code {
  display: flex;
  align-items: flex-start;
  gap: var(--bf-space-3);
  background: var(--app-surface-alt);
  border: 1px solid var(--app-border);
  padding: var(--bf-space-3);
  margin-block-end: var(--bf-space-3);
}
.bf-code pre {
  flex: 1;
  margin: 0;
  min-width: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: var(--bf-text-sm);
  line-height: 20px;
}
.bf-code button { flex: none; }

.bf-steps { margin: 0 0 var(--bf-space-3) 1.1rem; padding: 0; }
.bf-steps li { margin-block-end: .2rem; }

/* Pairing code. Large, spaced and monospaced because it is read off a screen
   and typed on a keypad — the failure mode this guards against is transcription,
   not comprehension. */
.bf-paircode {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 2rem;
  line-height: 1.2;
  letter-spacing: .18em;
  font-weight: 600;
  margin: 0 0 var(--bf-space-3);
  user-select: all;
}

/* Tooltips, not captions (docs/design.md). One reusable inline marker:
   <button type="button" class="bf-tip" aria-label="…">?</button>. A real
   button, not a span: natively focusable (so a touch tap focuses it), and a
   valid aria-label host (ARIA prohibits naming a role-less span). The text
   is revealed as ::after content drawn from aria-label — CSS only, so it
   works under the strict CSP, without JavaScript. Placed beside its label,
   never inside a <label for>, or the tap would forward to the control.
   Square corners and hairline border, per the stylesheet's rules. */
.bf-tip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 16px;
  height: 16px;
  margin-inline-start: .35rem;
  padding: 0;
  background: none;
  border: 1px solid var(--app-border);
  color: var(--app-text-muted);
  font-family: inherit;
  font-size: var(--bf-text-xs);
  line-height: 1;
  cursor: help;
  user-select: none;
}
/* :focus, deliberately not :focus-visible: a pointer/touch tap focuses the
   button without matching :focus-visible, and that tap is the only reveal
   path a touch user has. */
.bf-tip:hover::after,
.bf-tip:focus::after {
  content: attr(aria-label);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  width: max-content;
  max-width: 240px;
  padding: .25rem .5rem;
  background: var(--app-surface-alt);
  color: var(--app-text);
  border: 1px solid var(--app-border);
  font-size: var(--bf-text-xs);
  line-height: 16px;
  white-space: normal;
  z-index: 100;
}

/* Inline value inside a step — the server address the operator types into the
   device. Selectable as a unit so a double-click grabs the whole host. */
.bf-inline-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  background: var(--app-surface-alt);
  border: 1px solid var(--app-border);
  padding: .05rem .35rem;
  user-select: all;
}
