/* ============================================================
   ella — the estate stylesheet
   ============================================================

   BOOTSTRAP, RECONCILED. Byte-identical in all six consoles.

   Bootstrap is the base layer of every console in the estate. Its components
   are the estate's components: the modal is `.modal`, the card is `.card`, the
   floating field is `.form-floating`, the table is `.table`, the dropdown is a
   `<select>` enhanced by Tom Select.

   This file is everything that exists BECAUSE Bootstrap is underneath — the
   places where its defaults are wrong for a console, or wrong on a light-grey
   page, or where it has no opinion and the estate does. If Bootstrap were
   removed, none of this would have a reason to exist.

   ── It carries no colour of its own ──────────────────────────────────────

   Every value here is a token: `--ink`, `--surface`, `--border-strong`,
   `--primary`, `--radius`. That is what lets one file be byte-identical in six
   consoles that do not look alike — ella Hub is purple and ella Draw is not,
   and neither of them says so here. Each console's own token file supplies the
   values and the `--bs-*` bridge that hands them to Bootstrap.

   ── What is NOT here ─────────────────────────────────────────────────────

   The admin shell — sidebar, topbar, flyouts. The estate's own vocabulary that
   Bootstrap has no equivalent for: the chip, the split-button row action, the
   tab strip's overflow menu. Anything belonging to one product's screens. All
   of that stays in the console's own stylesheet.

   ── It is byte-identical across the estate ───────────────────────────────

   `tools/estate-sync-check.sh` holds it to that. There is no package and no
   build step in this estate, so the copies ARE the distribution mechanism —
   which works exactly as long as something notices when they drift.

   A fix here is a fix in one repository. Copy it to all six.
   ============================================================ */

/* ── The scrollbar never changes how wide the page is ───────────────────────

   `scrollbar-gutter: stable` reserves the track whether or not the page
   scrolls, so a short screen and a long one are the same width.

   Without it, whether the content is 15px narrower depends on how much of it
   there is. ella Hub and ella Platform have the longest dashboards in the
   estate, so those two always scrolled and were always narrower — and the four
   that fit were not. The report was "Platform and Hub seem to have a permanent
   scrollbar, so their page contents are slightly narrower than the others",
   which is exactly right and is not something either console asked for: it is
   a consequence of having more to show.

   It also stops a page shifting sideways as it loads, which is the same effect
   on a shorter timescale — a table that gains rows after a fetch takes the
   whole layout 15px left under the reader's cursor.

   On macOS with overlay scrollbars this reserves nothing and costs nothing;
   it does its work on Windows, Linux, and a Mac set to always show them. */
html { scrollbar-gutter: stable; }

/* ── One gap under the page title, whatever the markup calls it ─────────────

   Six consoles, three answers: ella Hub and ella Platform set
   `.pagehead { margin-bottom: 22px }`, ella Converse and ella Draw set
   `.page-header { margin: 0 0 18px }`, and ella Secure v2 and ella Jobs use the
   `.page-header` class with no rule for it at all — so their spacing was
   whatever the last element happened to leave behind.

   Both class names are matched rather than one renamed. Renaming would mean
   editing every view in six repositories to fix four pixels, and the two names
   are the same thing: the row carrying the page's title and its actions.

   Stated here because this file is the estate's shared component sheet and is
   byte-identical everywhere — which is the only way six copies stay agreed. A
   console that genuinely needs a different gap on one screen still overrides it
   in its own stylesheet. */
.pagehead,
.page-header { margin: 0 0 18px; }

/* ---- the dropdown chevron, defined once ----
   Every dropdown in the app draws this same mark: native <select> (which can
   only take a background-image) and the enhanced picker's caret (which masks
   it so the colour can follow state). The viewBox is the chevron's own bounds,
   so the drawn size IS the box — no invisible padding to guess at when
   positioning it. */
:root {
  --chevron: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%238A90A6' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m1 1.75 5 4.5 5-4.5'/%3e%3c/svg%3e");
  /* Same path, no stroke colour — used as a mask, so currentColor paints it. */
  --chevron-mask: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m1 1.75 5 4.5 5-4.5'/%3e%3c/svg%3e");
  --chevron-w: 15px;
  --chevron-h: 10px;
  --chevron-inset: 14px;
}

/* ── A card is a surface, not the page ────────────────────────────────────
 *
 * Bootstrap defaults `--bs-card-bg` to `--bs-body-bg`, on the assumption that a
 * card sits on a white page and needs no fill of its own. This estate's page is
 * light grey, so every card that did not state a background of its own became
 * the same grey as the page behind it — a panel beside it stayed white, and the
 * screen had two surfaces where it should have one.
 *
 * ella Hub never showed it, because its `brand.css` has always given `.card` a
 * background. The other four consoles do not, and that is the whole difference.
 *
 * The same applies to a list group and a modal, for the same reason. */
.card {
  --bs-card-bg: var(--surface);
  --bs-card-border-color: var(--border);
  --bs-card-border-radius: var(--radius);
}
.list-group { --bs-list-group-bg: var(--surface); }

/* ---- modal — Bootstrap's, said in the estate's tokens ----
 *
 * There is no CSS here for how a modal opens, closes, stacks, traps focus or
 * locks the page behind it. That is Bootstrap's, and it is better than what was
 * here: the previous version was `display: none` and a class called `open`.
 *
 * What IS here is the look, and all of it goes through Bootstrap's own
 * variables — so the rules below are the whole difference between this estate's
 * modal and a stock one, and there is nothing to keep in step by hand.
 *
 * The two vocabularies name these elements in opposite directions, which is
 * worth having written down somewhere:
 *
 *     was  .modal-overlay   ->  now  .modal            (the backdrop layer)
 *     was  .modal           ->  now  .modal-dialog     (+ .modal-content)
 *     was  .modal-close     ->  now  .btn-close
 *     was  data-modal-close ->  now  data-bs-dismiss="modal"
 */
.modal {
  --bs-modal-zindex: 1500;
  --bs-modal-width: 640px;
  --bs-modal-margin: 16px;
  --bs-modal-bg: var(--surface);
  --bs-modal-border-width: 0;
  --bs-modal-border-radius: var(--radius);
  --bs-modal-inner-border-radius: var(--radius);
  --bs-modal-box-shadow: var(--shadow-lg);
  --bs-modal-padding: 20px;
  --bs-modal-header-padding: 16px 20px;
  --bs-modal-header-border-color: var(--border);
}
/* 16px of air on a phone, 48px once there is room — which is what the overlay's
   `padding: 48px 16px` used to say. It cannot be one two-value custom property:
   at ≥576px Bootstrap writes `margin: var(--bs-modal-margin) auto`, and a
   two-value margin there becomes a three-value one, which means something else. */
@media (min-width: 576px) { .modal { --bs-modal-margin: 48px; } }
.modal-sm { --bs-modal-width: 460px; }
.modal-lg { --bs-modal-width: 760px; }
/* Bootstrap declares `--bs-modal-box-shadow` and then never reads it. */
.modal-content { box-shadow: var(--bs-modal-box-shadow); }
.modal-header { justify-content: space-between; gap: 12px; }
.modal-header h3 { margin: 0; font-size: 1.15rem; font-weight: 700; }
.modal-backdrop {
  --bs-backdrop-zindex: 1490;
  --bs-backdrop-bg: var(--ink);
  --bs-backdrop-opacity: .45;
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}

/* ---- alerts — Bootstrap's, in the estate's colours ----
 *
 * `.alert danger` is `.alert alert-danger` now, and the tone is expressed in
 * Bootstrap's own `--bs-alert-*` variables rather than by restating padding,
 * border and background per variant.
 *
 * Three things about this are not Bootstrap's defaults and are deliberate.
 *
 * THE ICON COMES FROM A `::before`, NOT FROM MARKUP, so every one of the 117
 * call sites got one without being touched — which is why it was done that way
 * when alerts were first styled, and it still is.
 *
 * IT IS A BLOCK, NOT A FLEX ROW. As a flex container every text node and every
 * inline `<a>` inside became its own flex item, so a sentence with two links in
 * it laid out as four columns: "…Add the domain under | Domains | , then pick it
 * in | settings". It only showed on longer messages, because a short one fits on
 * the single line where the two layouts agree. The content of an alert is a
 * sentence, so it flows like one.
 *
 * AMBER IS NEVER THE TEXT. `--warning` is 1.46:1 on its own background, so
 * `--bs-alert-color` for the warning variant is `--warning-ink`. Bootstrap's own
 * `.alert-warning` would put its amber in the text. `AmberContrastTest` fails
 * the build if that comes back.
 */
.alert {
  --bs-alert-padding-x: 14px;
  --bs-alert-padding-y: 12px;
  --bs-alert-border-radius: var(--radius-sm);
  --bs-alert-border: 1px solid var(--bs-alert-border-color);
  position: relative; display: block;
  padding-left: 36px;
  border-left-width: 3px;
  font-size: 14px; line-height: 1.45;
}
.alert::before {
  position: absolute; left: 14px; top: 12px;
  font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: 14px;
  line-height: 1.45; color: var(--bs-alert-color);
}
/* ── Two vocabularies for one thing, and both are styled ────────────────────
 *
 * Bootstrap's `.alert-warning` is what a view writes when it knows it is writing
 * a Bootstrap class. The estate's own word is `warn`, and it is what a
 * CONTROLLER emits: `['tone' => 'warn', 'title' => …]` describes a fact about
 * the message rather than a stylesheet, which is right — a controller should not
 * know what a warning looks like.
 *
 * Only the Bootstrap half was styled. `<div class="alert <?= $alert['tone'] ?>">`
 * therefore produced `class="alert warn"`, which got the padding and none of the
 * colour: no background, no border, and a blank `::before` where the icon goes.
 * The activation screen's four banners — entries paused, in test, not opened
 * yet, started while still in test — are the most load-bearing warnings in the
 * console and every one of them rendered as body text. The reports screen does
 * the same thing.
 *
 * Aliased rather than renamed. Renaming means changing every controller to talk
 * about Bootstrap, and the next person writing a `tone` — the natural word, and
 * the one `data-confirm-tone` already uses — walks into it again. */
.alert-success, .alert.success {
  --bs-alert-color: #08543E; --bs-alert-bg: var(--success-bg); --bs-alert-border-color: var(--success);
}
.alert-success::before, .alert.success::before { content: "\f058"; color: var(--success-ink); }
.alert-danger, .alert.danger {
  --bs-alert-color: #7F1D1D; --bs-alert-bg: var(--danger-bg); --bs-alert-border-color: var(--danger);
}
.alert-danger::before, .alert.danger::before { content: "\f06a"; color: var(--danger-ink); }
.alert-warning, .alert.warn, .alert.warning {
  --bs-alert-color: var(--warning-ink); --bs-alert-bg: var(--warning-bg); --bs-alert-border-color: var(--warning);
}
.alert-warning::before, .alert.warn::before, .alert.warning::before { content: "\f071"; }
.alert-info, .alert.info {
  --bs-alert-color: #1E3A8A; --bs-alert-bg: var(--info-bg); --bs-alert-border-color: var(--info);
}
.alert-info::before, .alert.info::before { content: "\f05a"; color: var(--info-ink); }
.alert a { color: inherit; font-weight: 600; }

/* ---- Tabs (detail pages: user admin, project admin) ---- */
/* Positioning context for the "More" menu, which lives here rather than inside
   the strip — see .tab-more-menu. */
.tabs { width: 100%; position: relative; }
/* One line, always. It used to wrap, and a second row of tabs under the first
   reads as a broken layout rather than as more tabs — the underline of the
   active tab lands in the middle of the strip and the eye stops trusting it.
   Anything that does not fit moves into the "More" menu, which is built by
   layout.php and needs no markup in the view. */
/* Bootstrap's `.nav-tabs`, drawn as an underline rather than as folders.
   Its own look is a bordered tab with rounded top corners sitting on a rule;
   this estate's is a single rule with the active tab underlined through it. The
   `--bs-nav-*` variables cover the spacing, but not the borders, so those are
   turned off here rather than fought at each state. */
.tabs-nav {
  --bs-nav-tabs-border-width: 0;
  --bs-nav-tabs-border-radius: 0;
  --bs-nav-tabs-link-active-border-color: transparent;
  --bs-nav-link-padding-x: 16px;
  --bs-nav-link-padding-y: 11px;
  display: flex; gap: 2px; flex-wrap: nowrap;
  /* The rule under the strip is an inset shadow, not a border. A border sits
     OUTSIDE the padding box that `overflow: hidden` clips to, so a tab could
     only draw its underline across the rule by hanging 1px below the strip —
     where it was clipped, and a 3px underline showed as 2px. A shadow is
     painted beneath the tabs, so the underline simply covers it. */
  border-bottom: 0;
  box-shadow: inset 0 -1px 0 var(--border);
  margin: 0 0 20px; overflow: hidden;
  align-items: stretch;
}

/* ---- Tab overflow ("More") ---- */
/* Sits immediately after the last visible tab, NOT pushed to the far right.
   Two reasons: a "More" floating alone at the edge reads as unrelated to the
   tabs, and an `auto` margin eats the free space, which makes the overflow
   measurement in layout.php depend on a value that is zero exactly when it
   matters.

   Deliberately NOT `position: relative` — the menu must not be positioned
   against this button, because this button is inside the clipped strip. */
.tab-more { flex: none; }
.tab-more-btn {
  background: transparent; border: 0; padding: 11px 14px;
  font-family: inherit; font-size: 15px; font-weight: 600;
  color: var(--slate-light); cursor: pointer; white-space: nowrap;
  border-bottom: var(--tab-underline) solid transparent;
  display: inline-flex; align-items: center; gap: 6px;
}
.tab-more-btn:hover { color: var(--ink); }
/* The button carries the active underline when the open tab is one it is
   hiding, so the strip never looks like nothing is selected. */
.tab-more-btn:focus-visible { outline: none; box-shadow: inset 0 0 0 2px rgba(var(--accent-rgb), .45); border-radius: 6px 6px 0 0; }
.tab-more.has-active .tab-more-btn { color: var(--primary-ink); border-bottom-color: var(--primary); }
.tab-more-btn .caret { font-size: 10px; transition: transform .15s ease; }
.tabs[data-more-open] .tab-more-btn .caret { transform: rotate(180deg); }

/* A CHILD OF `.tabs`, not of the strip.

   `.tabs-nav` has to clip — that is what keeps the row to one line before the
   script has run, and what makes `scrollWidth > clientWidth` the honest test of
   whether a tab fits. But clipping applies to descendants regardless of
   positioning, so a menu built inside the strip is invisible: it opens, and it
   is cut off at the strip's own bottom edge. The first version of this did
   exactly that, and looked like the click had done nothing.

   So the menu is a sibling of the strip, anchored to `.tabs`, and layout.php
   sets `top` and `left` from the button's measured position. The moved tab
   buttons are still inside `.tabs`, so `qsa('.tab-btn', tabs)` finds them and
   the tab machinery is untouched. */
.tab-more-menu {
  position: absolute; z-index: 60;
  min-width: 220px; max-width: 280px; padding: 6px; display: none;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow);
}
.tabs[data-more-open] .tab-more-menu { display: block; }
/* A tab inside the menu is a menu row, not a tab: full width, left aligned, no
   underline. Same element, so the tab JS keeps working on it unchanged. */
.tab-more-menu .tab-btn {
  display: flex; width: 100%; justify-content: flex-start;
  padding: 9px 10px; border-radius: 9px; border-bottom: 0; margin-bottom: 0;
  font-size: 13.5px;
}
.tab-more-menu .tab-btn:hover { background: var(--bg); }
.tab-more-menu .tab-btn.active { color: var(--primary-ink); background: rgba(var(--accent-rgb), .08); }
.tab-more-menu .tab-btn .tab-count { margin-left: auto; }
/* ── The tab, and which one is open ─────────────────────────────────────────
 *
 * The open tab is underlined in the console's accent, 3px, and its label is in
 * the accent's INK — the fill is what a line is painted in, the ink is what a
 * word is read in (`ReadableTextTest`).
 *
 * WRITTEN AS `.tabs-nav .tab-btn`, NOT `.tab-btn`, and that is the whole fix.
 * Every tab strip is also Bootstrap's `.nav-tabs` with `.nav-link` buttons, and
 * Bootstrap draws them with `.nav-tabs .nav-link { border: var(--bs-nav-tabs-
 * border-width) solid transparent }` and `.nav-tabs .nav-link.active { color;
 * border-color }` — two and three classes. The estate's own rules were one and
 * two, so they lost on every declaration they shared: the border width came
 * from the `0` this file sets on `.tabs-nav`, the colour from Bootstrap's
 * body-ink, and the open tab was told apart from the rest by weight alone. The
 * underline had been written all along and had never once been drawn.
 *
 * Equal specificity is enough, because this file loads after Bootstrap. The
 * strip's `--bs-nav-tabs-*` variables stay as they are: they keep Bootstrap's
 * folder-tab look switched off for anything that reads them.
 *
 * HOVER is ink and a grey underline — visibly "you could open this", never
 * mistakable for "this is open". FOCUS from the keyboard is an inset ring,
 * because the strip clips (see `.tab-more-menu`) and an outer ring would be cut
 * off at the strip's edge. */
:root { --tab-underline: 3px; }
.tab-btn {
  background: transparent; border: 0; padding: 11px 16px;
  text-decoration: none;
  font-family: inherit; font-size: 15px; font-weight: 600;
  color: var(--slate-light); cursor: pointer;
  white-space: nowrap; display: inline-flex; align-items: center; gap: 7px;
  transition: color .15s ease, border-color .15s ease;
}
.tabs-nav .tab-btn {
  color: var(--slate-light); background: transparent;
  border: 0; border-bottom: var(--tab-underline) solid transparent; border-radius: 0;
  margin-bottom: 0;
}
.tab-btn i { font-size: 14px; opacity: .85; }
.tabs-nav .tab-btn:hover { color: var(--ink); border-bottom-color: var(--border-strong); }
.tabs-nav .tab-btn:focus-visible {
  outline: none; color: var(--ink);
  box-shadow: inset 0 0 0 2px rgba(var(--accent-rgb), .45); border-radius: 6px 6px 0 0;
}
.tabs-nav .tab-btn.active,
.tabs-nav .tab-btn.active:hover,
.tabs-nav .tab-btn.active:focus-visible {
  color: var(--primary-ink); background: transparent; border-bottom-color: var(--primary);
}
.tab-btn.active { color: var(--primary-ink); }
.tab-btn .tab-count { margin-left: auto; }
.tab-btn {
  background: transparent; border: 0; padding: 11px 16px;
  text-decoration: none;
  font-family: inherit; font-size: 15px; font-weight: 600;
  color: var(--slate-light); cursor: pointer;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  white-space: nowrap; display: inline-flex; align-items: center; gap: 7px;
  transition: color .15s ease, border-color .15s ease;
}
.tab-btn i { font-size: 14px; opacity: .85; }
.tab-btn:hover { color: var(--ink); }
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); background: transparent; }
.tab-btn .tab-count {
  font-size: 12.5px; font-weight: 700; line-height: 1; padding: 3px 7px;
  border-radius: var(--radius-pill); background: var(--bg); color: var(--slate);
}
.tab-btn.active .tab-count { background: rgba(var(--accent-rgb), .14); color: var(--primary-dark); }
/* Bootstrap scopes its own pane rule as `.tab-content > .tab-pane`, because it
   expects a wrapper around them. Here the panes are direct children of `.tabs` —
   the strip and the panes ARE the component — so a `.tab-content` div per tab
   group would exist only to satisfy a descendant selector. The rule is written
   unscoped instead.

   `!important` because several panes carry an inline `display: flex`, and an
   inline declaration beats any selector without it. */
.tab-pane:not(.active) { display: none !important; }
.tab-pane.active { display: block; }

/* ---- toggle switch — Bootstrap's, at the estate's size ----
   A checkbox that reads as a switch. Used wherever a single boolean saves on its
   own: a tick-plus-Save reads as "compose a change, then commit it", which is the
   wrong promise when the only thing to change is on-or-off.

   Bootstrap draws it as a background image on the checkbox itself, so there is no
   track element here any more. What is left is the size — 38×22 rather than
   Bootstrap's 2em×1em — and the colours. */
/* `flex-wrap` is here for the hint below: without a hint there is nothing to
   wrap, so it changes no existing control. */
.switch { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; user-select: none;
  flex-wrap: wrap; padding-left: 0; min-height: 0; margin: 0; }
.switch .form-check-input {
  flex: 0 0 auto; width: 38px; height: 22px; margin: 0; float: none;
  background-color: var(--border-strong); border-color: var(--border-strong);
  /* Bootstrap positions the thumb with `background-position`, so its travel is
     the difference between the control's width and the thumb's — no transform,
     and nothing here to keep in step with it. */
  background-size: 16px 16px;
  transition: background-color .16s ease, background-position .16s ease, box-shadow .16s ease;
}
.switch .form-check-input:checked {
  background-color: var(--success); border-color: var(--success);
}
.switch .form-check-input:focus { border-color: var(--border-strong); box-shadow: none; }
.switch .form-check-input:focus-visible { box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .24); }
.switch-label { font-size: 14px; color: var(--slate); white-space: nowrap; margin: 0; }
.switch .form-check-input:checked ~ .switch-label { color: var(--ink); font-weight: 600; }
/* A line of explanation, under the control rather than beside it: these run to a
   full sentence, and inline they would stretch the row the switch sits in. The
   caption keeps `nowrap`; the hint must not. */
.switch-hint { flex-basis: 100%; margin: 2px 0 0; line-height: 1.35; white-space: normal; }
/* Mid-flight: the control is authoritative only once the server has agreed. */
.switch.is-saving { opacity: .6; pointer-events: none; }
.switch.is-saving .form-check-input { background-color: var(--primary-light); border-color: var(--primary-light); }

/* ---- buttons ----------------------------------------------------------
 *
 * A BUTTON IS A CONTROL AND A CARD IS A SURFACE, and they must not read alike.
 * The first attempt at this made both of them a white rectangle with a hairline
 * border and a 3px accent rule along the top, which is the same object twice —
 * a row of buttons looked like a row of very small cards.
 *
 * Two things separate them here, and both are structural rather than decorative:
 *
 *   A BUTTON HAS A FACE. A faint top-to-bottom shading (white at the top,
 *   4.5% ink at the bottom), a firmer border (`--border-strong` against a
 *   card's `--border`) and 1px of shadow. It is the oldest trick there is for
 *   saying "press this", it costs no colour, and it inverts on `:active` so the
 *   button reads as going in. A card stays flat, because a card is not
 *   pressable and should not pretend to be.
 *
 *   THE TYPE LINE IS DOWN THE LEFT EDGE, not along the top. Cards own the top
 *   rule. Same 3px, same tokens, different edge — so at a glance the horizontal
 *   marks are surfaces and the vertical ones are actions.
 *
 * What the line means:
 *
 *   accent   the action this screen is FOR — Save, Publish, Send, Move
 *   green    it creates a new record            (`.btn-add`)
 *   red      it destroys something              (`.btn-danger` / `.btn-delete`)
 *   none     an ordinary action, and a cancel
 *
 * They were gradient-filled pills with a coloured glow, and the fills were
 * fixed hues unrelated to anything: PRIMARY WAS GREEN and edit was blue, in all
 * six consoles. ella Draw — a cyan console — offered a green Save beside a blue
 * Edit. Green was also already `--success`, so the main action on every screen
 * wore the colour that elsewhere means "this worked".
 *
 * NOT A PILL. `--radius` is the 8px a card, an input and a select already use;
 * the pill is a CHIP's shape here — a status you read — and giving it to a
 * control you press made the two read alike.
 *
 * Colours come from tokens ONLY. This file is byte-identical in all six
 * consoles and is supposed to carry no colour; the block it replaces held
 * eleven literal hexes. The shading below is neutral ink and white at low
 * alpha, which is the same thing every shadow in this file is made of.
 */
.btn {
  --bs-btn-padding-x: 16px;
  --bs-btn-padding-y: 9px;
  --bs-btn-font-size: 15px;
  --bs-btn-font-weight: 600;
  --bs-btn-line-height: 1.5;
  --bs-btn-border-radius: var(--radius);
  --bs-btn-border-width: 1px;
  --bs-btn-disabled-opacity: .5;
  --bs-btn-focus-shadow-rgb: var(--accent-rgb);

  /* Every button is this. The type only adds a line. */
  --bs-btn-color: var(--ink);           --bs-btn-bg: var(--surface);  --bs-btn-border-color: var(--border-strong);
  --bs-btn-hover-color: var(--ink);     --bs-btn-hover-bg: var(--bg);  --bs-btn-hover-border-color: var(--slate-light);
  --bs-btn-active-color: var(--ink);    --bs-btn-active-bg: var(--bg); --bs-btn-active-border-color: var(--slate-light);
  --bs-btn-disabled-color: var(--ink);  --bs-btn-disabled-bg: var(--surface); --bs-btn-disabled-border-color: var(--border-strong);

  position: relative;
  /* `hidden`, so the type line is clipped by the button's OWN corner radius.
     The line used to carry its own `border-radius` and sit at -1px on three
     edges; a 3px-tall box cannot honour an 8px radius, so the browser scaled it
     into a capsule whose square ends then stuck out past the button's curve.
     Clipping is exact and needs no second radius to keep in step. */
  overflow: hidden;
  background-image: linear-gradient(180deg, rgba(255,255,255,.55) 0%, rgba(17,17,26,.045) 100%);
  box-shadow: 0 1px 1.5px rgba(17,17,26,.06);

  /* An icon and a label, spaced, on one line. Bootstrap's button is
     `inline-block`, which leaves the two to be spaced by a text node. */
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  letter-spacing: .01em; white-space: nowrap; text-decoration: none;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
/* Pressed: the shading inverts, so the face goes concave. */
.btn:active {
  background-image: linear-gradient(180deg, rgba(17,17,26,.06) 0%, rgba(255,255,255,.35) 100%);
  box-shadow: none;
  transform: translateY(1px);
}
/* A BUTTON IS THE WIDTH OF ITS LABEL, in every container it can land in.
 *
 * `.pagehead > a { flex: 1 1 100% }` gives a back-link its own line, and it
 * matched any `<a>` — so the delivery board's "New job" came out as a 1,840px
 * pill across the top of the page, which is the shape of a banner and not of a
 * control. That rule is narrowed to `:not(.btn)` where it lives; this is the
 * belt to its braces, so no flex or grid parent can stretch a button. */
.btn { flex: 0 0 auto; align-self: center; justify-self: start; }
/* `.is-disabled` is a link dressed as a button; a real `:disabled` needs none of
   this, and Bootstrap already handles it. */
.btn.is-disabled { opacity: var(--bs-btn-disabled-opacity); cursor: not-allowed; pointer-events: none; }

/* The type line: down the LEFT edge, full height, clipped to the radius.
 *
 * The last selector is the one that matters most in practice. A `<button
 * type="submit">` with no variant IS the form's main action — "Save", "Move",
 * "Send" — and hundreds of them are written with a bare `.btn`. Left untyped
 * they came out identical to Cancel beside them, which is worse than the old
 * green pill it replaced. A submit is primary unless it says otherwise. */
.btn-add::before,
.btn-primary::before,
.btn-edit::before,
.btn-delete::before,
.btn-danger::before,
.btn[type="submit"]:not(.btn-add):not(.btn-primary):not(.btn-edit):not(.btn-delete):not(.btn-danger):not(.btn-outline):not(.btn-ghost):not(.btn-dark)::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 3px;
}
/* The action this screen is for. */
.btn-primary::before,
.btn-edit::before,
.btn[type="submit"]:not(.btn-add):not(.btn-primary):not(.btn-edit):not(.btn-delete):not(.btn-danger):not(.btn-outline):not(.btn-ghost):not(.btn-dark)::before {
  background: var(--accent);
}
/* It creates a new record. */
.btn-add::before                         { background: var(--success); }
/* It destroys something. */
.btn-delete::before, .btn-danger::before { background: var(--danger); }

/* A destructive button also says so in its text, which a 3px line alone cannot
   carry to somebody who does not see colour. `--danger-ink`, never `--danger`:
   the fill is what a status is painted in, the ink is what it is read in. */
.btn-delete, .btn-danger {
  --bs-btn-color: var(--danger-ink);
  --bs-btn-hover-color: var(--danger-ink);  --bs-btn-hover-bg: var(--danger-bg);
  --bs-btn-active-color: var(--danger-ink); --bs-btn-active-bg: var(--danger-bg);
}
/* Cancel and secondary: no face and no line, so it sits behind whatever it is
   next to. It was a frosted panel (`backdrop-filter: blur(10px) saturate(140%)`)
   over a flat page — an effect with nothing behind it to refract. */
.btn-outline {
  --bs-btn-bg: transparent;
  --bs-btn-border-color: var(--border);
  background-image: none;
  box-shadow: none;
}
.btn-dark {
  --bs-btn-color: #fff; --bs-btn-bg: var(--ink); --bs-btn-border-color: var(--ink);
  --bs-btn-hover-color: #fff; --bs-btn-hover-bg: var(--slate); --bs-btn-hover-border-color: var(--slate);
}
.btn-ghost {
  --bs-btn-color: var(--slate); --bs-btn-bg: transparent; --bs-btn-border-color: transparent;
  --bs-btn-hover-color: var(--ink); --bs-btn-hover-bg: var(--bg); --bs-btn-hover-border-color: transparent;
  background-image: none; box-shadow: none;
}
/* Full width INSIDE A NARROW COLUMN — a sign-in card, a modal footer — and
   capped so it can never become the page-wide banner described above. Every
   call site is an auth screen whose card is about 380px, so the cap changes
   none of them; it stops the next one. */
.btn-block { width: 100%; max-width: 380px; justify-content: center; }
.btn-sm { --bs-btn-padding-x: 12px; --bs-btn-padding-y: 6px; --bs-btn-font-size: 14px; }

/* ---- The estate's table is Bootstrap's `.table` ----
   `.tablewrap` was `overflow-x: auto`, which is what `.table-responsive` is; both
   names are Bootstrap's now. What is below is the difference between a stock
   table and this one: a quieter, uppercase header, roomier cells, and a hover
   that is on by default rather than behind `.table-hover`.

   The cell rules are written `> :not(caption) > * > *` because that is the
   selector Bootstrap sets padding with, and a bare `.table td` loses to it. */
.table{
  --bs-table-border-color: var(--border);
  --bs-table-color: var(--ink);
  --bs-table-bg: transparent;
  width:100%; margin-bottom:0; font-size:13.5px; vertical-align:middle;
}
.table > :not(caption) > * > *{ padding:13px 14px; background-color:transparent; box-shadow:none; }
.table > thead > tr > th{
  text-align:left; font-size: 12px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
  color:var(--slate-light); padding:12px 14px; white-space:nowrap;
}
/* Every row highlights on hover, without each table having to ask for it.
   Bootstrap does this with an inset box-shadow the width of the cell, which
   paints over a cell's own background — a status tint, a "just added" row — so
   the background is set directly instead. */
.table > tbody > tr:hover > *{ background-color:var(--bg); }

/* ══ Form controls — Bootstrap's, sized and coloured for this estate ═══════
 *
 * `.field` is `.form-floating` now, and the two hundred lines that used to be
 * here are Bootstrap's. What is left is the difference: the estate's 54px
 * control, its 10px corner, its purple focus ring, and the two places where
 * Bootstrap's floating label is genuinely wrong on a light-grey page.
 *
 * The markup did not have to change shape. Both vocabularies put the CONTROL
 * FIRST and the LABEL SECOND and float on `:not(:placeholder-shown)`, which is
 * why every text control in this console already carries `placeholder=" "`.
 */

/* The estate's control is 54px, not Bootstrap's 58px, and its padding splits
   22/6 rather than 16/16 — which is what makes room for the floated label. */
.form-floating > .form-control,
.form-floating > .form-select {
  height: 54px;
  min-height: 54px;
}
.form-floating > .form-control { padding: 22px 13px 6px; }
.form-floating > .form-control:focus,
.form-floating > .form-control:not(:placeholder-shown),
.form-floating > .form-control:-webkit-autofill { padding: 22px 13px 6px; }
.form-floating > .form-select { padding: 22px 13px 6px; }

/* ── The floated label, as arithmetic rather than as a transform ───────────
 *
 * THE BUG THIS REPLACES, because it is the one everybody saw and nobody could
 * name. Bootstrap floats a label by `transform: scale(.85) translateY(-.5rem)`
 * about `transform-origin: 0 0`, and centres it in the meantime with
 * `height: 100%` — where the 100% is of `.form-floating`, NOT of the control.
 *
 * A `.form-floating` is taller than its control the moment it contains
 * anything else, and the estate puts the hint INSIDE the field (see
 * `.field > .form-text` further down, which is where `fld()` has always put
 * it). So on ella Platform's activation setup screen the "Type" field was
 * 77px tall — 54px of control and 23px of "Changing this changes which fields
 * below apply." — its label centred on 38.5px instead of 27px, and the floated
 * result landed at 17.2–33.4px. The value underneath begins at 22px. The label
 * was printed ON the word "eVoucher", and every field carrying a hint had it.
 *
 * Fields without a hint were not right either, only quieter: the descender of
 * a floated label finished 1.6px inside the value's box, which is the faint
 * crowding that made every form look tight without looking broken.
 *
 * So the transform is gone and the two states are stated as measurements, both
 * relative to the CONTROL. There is no scaling, so nothing has to be corrected
 * for afterwards — Bootstrap's `translateX(.15rem)` exists only to undo the
 * 15% that `scale()` takes off the left padding, and its `label::after` plate
 * is scaled and mispositioned for the same reason.
 *
 *   RESTING   the label stands in for the value, so it sits exactly where the
 *             value will: 16px of padding above a 38px line box centres it on
 *             35px, and 22px of control padding above a 26px content box puts
 *             the value's centre on 35px too. It rises out of the text rather
 *             than jumping to a new place.
 *   FLOATED   a 22px band at the top of the control — which is the control's
 *             top padding, exactly — with a 23px line box inside it. The label
 *             finishes at ~19px and the value starts at 22px. Three pixels of
 *             daylight, by construction, at every size this estate uses.
 *
 * It is a BLOCK, not a flex row. `text-overflow: ellipsis` does nothing to a
 * flex container's items, so the label was set to ellipsis and could not: a
 * long one was cut mid-glyph. "Entry method" in an 84px control read "Entry
 * meth" — which is not a truncation a reader can recognise as one. */
.form-floating > label {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 54px;
  padding: 16px 13px 0;
  line-height: 38px;
  display: block;
  font-size: 15px;
  color: var(--slate);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transform: none;
  transition: height .12s ease, padding .12s ease, line-height .12s ease,
              font-size .12s ease, color .12s ease;
}
.form-floating > .form-control:focus ~ label,
.form-floating > .form-control:not(:placeholder-shown) ~ label,
.form-floating > .form-control-plaintext ~ label,
.form-floating > .form-control:-webkit-autofill ~ label,
.form-floating > .form-select ~ label,
.form-floating > .ts-wrapper ~ label,
/* A date, time or datetime control is never "empty" to the browser — it draws
   `dd/mm/yyyy` itself — so `:placeholder-shown` never matches and its label
   stayed resting, printed across the very placeholder it was describing. That
   is the doubled `dd/mm/yyyy` under a label in the new-activation dialog. */
.form-floating > input[type="date"] ~ label,
.form-floating > input[type="time"] ~ label,
.form-floating > input[type="month"] ~ label,
.form-floating > input[type="week"] ~ label,
.form-floating > input[type="datetime-local"] ~ label {
  height: 22px;
  padding: 0 13px;
  line-height: 23px;
  font-size: 12px;
  color: var(--slate-light);
  /* Bootstrap's own float rule is more specific than the base one above, so
     clearing the transform there is not enough — it has to be cleared in the
     state that sets it, or the label is scaled to 85% AND resized to 12px and
     lands 2.5px above the top of the control. */
  transform: none;
}
/* Bootstrap's plate. It is scaled with the label it hangs off, so it covers
   the top eleven pixels of a 22px band and stops 85% of the way across — see
   the textarea note below, which is where the estate draws its own. */
.form-floating > label::after { display: none; }

/* A floated label must not run under the chevron and come out as an ellipsis
   sitting on the arrow. */
.form-floating > .form-select ~ label,
.form-floating > .ts-wrapper ~ label {
  padding-right: calc(var(--chevron-inset) + var(--chevron-w) + 8px);
}

.form-control, .form-select {
  border-radius: 10px;
  border-color: var(--border-strong);
  color: var(--ink);
  background-color: var(--surface);
}
/* Bootstrap hard-codes its focus ring blue — `#86b7fe` and a blue shadow — in
   the `.form-control:focus` rule rather than reading a variable, so this is the
   one part of a control that cannot be re-branded through `--bs-*`. */
.form-control:focus, .form-select:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .16);
}
/* A focused field says so in the label as well as the border, which is the
   one cue that survives being colour-blind to the ring.
   
   This was a selector list with no declaration block: two selectors, a blank
   line, a comment, and then `.form-floating > label .req`'s block — so CSS
   read all three as one rule and painted the FOCUSED LABEL `--slate-light`,
   which is the opposite of drawing attention to it. Whatever was written here
   went missing; this is what it was for. */
.form-floating > .form-control:focus ~ label,
.form-floating > .form-select:focus ~ label,
.form-floating > .ts-wrapper.focus ~ label { color: var(--primary-ink); }

/* "(optional)" and the required mark sit next to the label text. They used to
   need a margin because the label was a flex row and the whitespace between
   the two elements collapsed — "Converse base URL(e.g. https://…)", welded
   together, on the Comms settings form. The label is a block again, so the
   space in the markup is a space; the margin stays because a mark reads better
   with a little more room than a word space. */
.form-floating > label .req { color: var(--slate-light); font-weight: 400; }
.form-floating > label .req,
.form-floating > label .muted { margin-left: 4px; }

/* ── The select ────────────────────────────────────────────────────────────
 *
 * Bootstrap floats a select's label permanently, which is right: a select
 * always has a value, so a label that only floats when it is "filled" would sit
 * on top of the first option forever. What Bootstrap does not have is this
 * estate's chevron — it draws its own, so a page with both a `<select>` and a
 * an enhanced one on it showed two different arrows for the same gesture.
 * `--chevron` is defined once at the top of this file and both read it. */
.form-select:not([multiple]):not([size]) {
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right var(--chevron-inset) center;
  background-size: var(--chevron-w) var(--chevron-h);
  padding-right: calc(var(--chevron-inset) + var(--chevron-w) + 12px);
  cursor: pointer;
}
.form-floating > .form-select { padding-right: calc(var(--chevron-inset) + var(--chevron-w) + 12px); }

/* ── The textarea, and why Bootstrap's version of this is wrong here ───────
 *
 * A floating label sits INSIDE the control, so a textarea long enough to scroll
 * runs its text up behind the label. Bootstrap knows this and lays a small
 * plate behind the label to hide what passes under it:
 *
 *     .form-floating > textarea:not(:placeholder-shown) ~ label::after {
 *         inset: 1rem 0.375rem; height: 1.5em;
 *         background-color: var(--bs-body-bg);
 *     }
 *
 * Two things about that do not survive contact with this estate.
 *
 * IT IS PAINTED IN THE PAGE COLOUR, NOT THE CONTROL COLOUR. `--bs-body-bg` is
 * this console's light grey; a textarea is white. So the plate was a grey band
 * sitting inside a white box — which is the "textareas look terrible" everybody
 * notices immediately, and it is worse than the problem it fixes.
 *
 * IT IS INSET 0.375rem FROM THE SIDES AND STARTS 1rem DOWN. Text scrolling past
 * therefore appeared in the 6px gutters either side of the plate and above it,
 * so a line did not slide under the label so much as fray around it.
 *
 * The plate below is the control's own colour, spans the full width inside the
 * border, and is exactly as tall as the control's top padding — so the first
 * line of text begins where the plate ends and nothing is ever half-hidden. */
.form-floating > textarea.form-control {
  height: auto;
  min-height: 92px;
  padding: 22px 12px 8px;
  line-height: 1.4;
  resize: vertical;
}
/* A textarea is 92px and growing, so "centred on the value" means centred on
   its FIRST LINE — 22px of padding plus half of a 21px line, which is 32.5px,
   the same place the first character lands. Centring it in the box put the
   resting label a third of the way down an empty panel. */
.form-floating > textarea.form-control ~ label { height: 43px; padding: 22px 13px 0; line-height: 21px; }

/* THE PLATE CANNOT LIVE ON THE LABEL, which is the part Bootstrap gets wrong
   and the reason its own version only half-works.
   
   A floated label is `transform: scale(.85) translateY(-.5rem)` with
   `transform-origin: 0 0`, and a `::after` is transformed with the element it
   belongs to. So Bootstrap's plate is scaled to 85% and pulled 8px upwards: it
   ends up covering roughly the top ELEVEN pixels of a 22px band, and stopping
   85% of the way across. Text scrolling past appeared in the strip underneath
   it and in the gutter to the right of it — which reads as the label smearing
   rather than as anything anybody would call a plate.
   
   Put on the wrapper, which is not transformed, it covers what it is supposed
   to. `z-index: 1` puts it over the textarea and under the label, which is
   `z-index: 2`. */
.form-floating > textarea.form-control ~ label::after { display: none; }
.form-floating:has(> textarea.form-control:not(:placeholder-shown))::before,
.form-floating:has(> textarea.form-control:focus)::before {
  content: "";
  position: absolute;
  top: 1px; left: 1px; right: 1px;
  height: 21px;
  z-index: 1;
  border-radius: 9px 9px 0 0;
  background: var(--surface);
  pointer-events: none;
}
.form-floating:has(> textarea.form-control:disabled)::before { background: var(--bg); }


/* ── A file input keeps a normal label, above it ───────────────────────────
 *
 * Bootstrap has no floating variant for `input[type=file]`, and there is no
 * sensible one: the control draws its own "Choose file" button, so a label
 * floating over it is a caption on top of a caption. `.form-stack` is the same
 * arrangement wherever the caption belongs above the control. */
.form-field-file, .form-stack { display: block; }
.form-stack > .form-label,
.form-field-file > .form-label { display: block; font-size: 12px; color: var(--slate); margin-bottom: 5px; }

/* ══ `.form-grid` — where a field goes, decided rather than fallen out ═════
 *
 * THE PROBLEM IT REPLACES. Fields were laid out by `.field-row`, a flex row
 * whose children were `flex: 1 1 auto` — or, in ella Platform, whose children
 * matched nothing at all, because the rule there still names the pre-Bootstrap
 * `.field` and the markup is `.form-floating`. A flex item with no basis is
 * sized by its CONTENTS, so the activation setup screen came out
 *
 *     Name 213px · Description 213px · Type 273px · Entry method 84px
 *     Launch date 169px · Launch time 140px · Currency 140px
 *
 * — seven widths with no common factor, an 84px box whose own label needs
 * 79px, and nothing on the page lining up with anything else. That is the "no
 * logic to how inputs are positioned" that this estate was fairly accused of.
 * It is not a styling fault; there was no layout system, only a row that let
 * whatever was in it decide.
 *
 * A TWELVE-COLUMN GRID, and the width is a decision somebody makes. `.fg-6` is
 * half, `.fg-4` a third, `.fg-3` a quarter. Every column is `minmax(0, 1fr)`,
 * so a long value cannot push its own column wider and squeeze the one beside
 * it — which is the other half of how the widths above happened.
 *
 * ONE ROW GAP AND ONE COLUMN GAP, for the whole estate. Vertical rhythm was
 * whatever margin each screen had reached for; 20px between rows and 16px
 * between columns is stated once here and nowhere else.
 *
 * IT COLLAPSES TO ONE COLUMN ON A NARROW SCREEN. Below 760px every field is
 * full width, because two 84px controls side by side on a phone is the same
 * defect at a smaller size.
 *
 * `align-items: start` matters: a field with a hint under it is taller than
 * one without, and stretching them to match would put a 54px control in the
 * middle of a 77px cell with the label floating over nothing. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 20px 16px;
  align-items: start;
}
.form-grid > * { grid-column: 1 / -1; min-width: 0; }
@media (min-width: 760px) {
  .form-grid > .fg-1  { grid-column: span 1; }
  .form-grid > .fg-2  { grid-column: span 2; }
  .form-grid > .fg-3  { grid-column: span 3; }
  .form-grid > .fg-4  { grid-column: span 4; }
  .form-grid > .fg-5  { grid-column: span 5; }
  .form-grid > .fg-6  { grid-column: span 6; }
  .form-grid > .fg-7  { grid-column: span 7; }
  .form-grid > .fg-8  { grid-column: span 8; }
  .form-grid > .fg-9  { grid-column: span 9; }
  .form-grid > .fg-10 { grid-column: span 10; }
  .form-grid > .fg-11 { grid-column: span 11; }
  .form-grid > .fg-12 { grid-column: 1 / -1; }
  /* Start a fresh row without an empty filler element. */
  .form-grid > .fg-break { grid-column: 1 / -1; height: 0; margin: 0; padding: 0; }
  /* The "+ New …" button beside a picker is not a field and does not get a
     field's share of the row — it is 54px, the height of the control it sits
     next to, and the picker takes the rest. One column rather than `auto`, so
     it lands in the grid instead of opening a thirteenth track. */
  .form-grid > .qc-add-wrap { grid-column: span 1; justify-self: start; align-self: start; }
  .form-grid > .qc-add-wrap .qc-add { width: 54px; height: 54px; flex-basis: 54px; }
}
/* A grid of fields is a section, and sections need air between them. Stated as
   an adjacency rather than a margin on the grid itself, so a grid that is the
   only thing in a card does not push the card open. */
.form-grid + .form-grid,
.form-grid + .field-row,
.field-row + .form-grid { margin-top: 20px; }

/* ── A grid inside a grid ──────────────────────────────────────────────────
 *
 * A whole form is one `.form-grid` and each row of fields is another, so the
 * rows line up with each other as well as within themselves — two sibling
 * grids of the same width with the same spans agree column for column, which
 * is the alignment a single flat grid would give and the nesting is what lets
 * a row keep its own ratios.
 *
 * The outer grid's 20px row gap has already spaced them, so the adjacency
 * above would double it to 40px. Same for the button row: `.form-actions`
 * carries a 20px top margin for the forms that are not on a grid at all. */
/* The button row under a form. This file's rules assumed it for a year while it
   lived in ella Hub's own stylesheet alone, so four consoles each grew a copy of
   it (or none, and buttons sat flush against the last field). One rule. */
.form-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 20px; }

.form-grid > .form-grid + .form-grid,
.form-grid > .form-grid + .field-row,
.form-grid > .field-row + .form-grid,
.form-grid > .form-actions { margin-top: 0; }

/* A button on the grid takes a button's width, not the column's, and sits on
   the line of the field beside it rather than at the top of a 54px cell. */
.form-grid > .btn { justify-self: start; align-self: center; }

/* ── A form's measure ──────────────────────────────────────────────────────
 *
 * The narrow-panel problem, named rather than solved one inline style at a
 * time. `max-width` was written by hand on 136 views in the estate, from 200px
 * to 1100px, and neither end of that range was a decision anybody would defend:
 * the Estimate tab was six fields and four paragraphs in 480px on a 1440px
 * screen, and some prose ran the full width of the window.
 *
 * Three measures, because there are three things being measured:
 *
 *   `.panel-form`   960px — a two- or three-column form. Wide enough for
 *                   `.form-grid` to actually have columns, and no wider.
 *   `.panel-narrow` 560px — one column of controls, and only where that IS the
 *                   screen: a sign-in box, a single-field dialog.
 *   `.prose`        68ch  — running text. Characters, not pixels: the limit is
 *                   how far the eye travels back to find the next line, and
 *                   that is a count of letters.
 */
.panel-form   { max-width: 960px; }
.panel-narrow { max-width: 560px; }
.prose        { max-width: 68ch; }

/* A field that must stay narrow whatever column it lands in — a currency, a
   two-digit count. It is a MINIMUM everywhere else: 210px is what the longest
   label this estate uses needs before it starts eating its own tail. */
.form-grid > .form-floating,
.field-row > .form-floating { min-width: 0; }
.fg-tight { max-width: 220px; }

/* ══ The (i) — one explanation, everywhere ════════════════════════════════
 *
 * It lived in ella Hub's own stylesheet, so ella Hub was the only console that
 * could explain anything in place. The audience for all six is the same and it
 * is not a technical one, so the mark that means "this has a plain-English
 * explanation behind it" has to be the same mark in all six.
 *
 * It is a BUTTON, not a span with a `title`. A native tooltip does not appear
 * on a touch screen, cannot be reached from the keyboard, waits a second and a
 * half, and is drawn by the operating system in whatever size it likes. This
 * one opens on hover, on focus and on tap, and reads at 13px.
 *
 * `--info` is a fill and `--info-ink` is a word, per the status-ink rule — the
 * mark is a filled disc with a white glyph, which is the one place the fill is
 * correct and the ink would be invisible. */
.ihelp {
  display: inline-grid; place-items: center;
  width: 17px; height: 17px; padding: 0;
  border: 0; border-radius: 50%;
  background: var(--info-ink); color: #fff;
  font-size: 12px; font-weight: 800; font-style: normal; line-height: 1;
  font-family: inherit;
  cursor: help; vertical-align: middle; margin-left: 6px;
  position: relative; user-select: none;
  flex: none;
}
.ihelp:hover, .ihelp:focus-visible { background: var(--ink); outline: none; }
.ihelp:focus-visible { box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .28); }
.ihelp::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + 9px); left: 50%;
  transform: translateX(-50%) translateY(4px);
  background: var(--ink); color: #fff;
  padding: 10px 13px; border-radius: 10px;
  font-size: 13px; font-weight: 500; line-height: 1.5; letter-spacing: 0;
  width: max-content; max-width: min(300px, calc(100vw - 32px)); text-align: left; white-space: normal;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .22);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .14s ease, transform .14s ease, visibility .14s;
  z-index: 1600;
}
/* A HIDDEN TIP TAKES NO ROOM. `visibility: hidden` still lays the bubble out, so
   every "i" near the right edge of a phone pushed the page 4–8px sideways with
   nothing visible doing it — the board, the guide and the org screen all
   scrolled for a tooltip nobody had opened. Collapsed until it is shown; the
   fade still runs on the way in. */
.ihelp:not(:hover):not(:focus):not(.is-open)::after { width: 0; max-width: 0; padding: 0; overflow: hidden; }
.ihelp::before {
  content: ""; position: absolute; bottom: calc(100% + 3px); left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent; border-top-color: var(--ink);
  opacity: 0; visibility: hidden; transition: .14s ease; z-index: 1601;
}
.ihelp:hover::after, .ihelp:focus::after, .ihelp.is-open::after {
  opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0);
}
.ihelp:hover::before, .ihelp:focus::before, .ihelp.is-open::before { opacity: 1; visibility: visible; }
/* Near the top of a scroll container the bubble would be cut off, so it drops
   below the mark instead. `estate.js` sets this when there is no room above. */
.ihelp.tip-below::after { bottom: auto; top: calc(100% + 9px); transform: translateX(-50%) translateY(-4px); }
.ihelp.tip-below::before { bottom: auto; top: calc(100% + 3px); border-top-color: transparent; border-bottom-color: var(--ink); }
.ihelp.tip-below:hover::after, .ihelp.tip-below:focus::after, .ihelp.tip-below.is-open::after { transform: translateX(-50%) translateY(0); }
/* Against a window edge the bubble is pinned to the mark's side instead of
   centred on it, so it stays on the page. */
.ihelp.tip-start::after  { left: 0; transform: translateX(0) translateY(4px); }
.ihelp.tip-start:hover::after, .ihelp.tip-start:focus::after, .ihelp.tip-start.is-open::after { transform: translateX(0) translateY(0); }
.ihelp.tip-end::after    { left: auto; right: 0; transform: translateX(0) translateY(4px); }
.ihelp.tip-end:hover::after, .ihelp.tip-end:focus::after, .ihelp.tip-end.is-open::after { transform: translateX(0) translateY(0); }
/* Neither edge fits (a phone): centred, then slid by what the script measured. */
.ihelp.tip-shift::after { transform: translateX(calc(-50% + var(--tip-dx, 0px))) translateY(4px); }
.ihelp.tip-shift:hover::after, .ihelp.tip-shift:focus::after, .ihelp.tip-shift.is-open::after { transform: translateX(calc(-50% + var(--tip-dx, 0px))) translateY(0); }

/* ── The (i) inside a floating label ───────────────────────────────────────
 *
 * Bootstrap sets `pointer-events: none` on a floating label, and it is right
 * to: the label is a box laid OVER the control, so without it every click
 * aimed at the field lands on the caption instead and the field never focuses.
 *
 * It also means a button inside that label is not a button. The (i) could not
 * be hovered, could not be tapped, and could not be clicked — the pointer went
 * straight through it to the input underneath — so on the first screen built
 * with the component, all five explanations were unreachable and nothing said
 * so. The mark was drawn, the tooltip existed, and the feature was decoration.
 *
 * Handed back for the mark alone. Clicks on the LABEL still fall through to the
 * control, which is what makes a floating label feel like part of the field. */
.form-floating > label { pointer-events: none; }
.form-floating > label .ihelp { pointer-events: auto; }
/* THE LABEL CANNOT CLIP ITS OWN TOOLTIP.
 *
 * The label is `overflow: hidden` so a long one ellipsises rather than being
 * cut mid-glyph. A tooltip bubble is drawn by the mark INSIDE that label, so
 * it was clipped to a 22px band and never appeared — the second way the same
 * feature was invisible, found the same way as the first: by opening it.
 *
 * A label carrying an (i) becomes a flex row and the TEXT does the clipping
 * instead, in a span of its own. The mark does not shrink and the bubble is
 * not inside anything that hides it. Labels without an (i) — which is every
 * hand-written one in the estate — are unchanged. */
.form-floating > label:has(> .ihelp) {
  display: flex; align-items: center; gap: 0;
  line-height: 1.25; overflow: visible;
}
.form-floating > label > .fld-text {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
/* Optically centred on the label's own line rather than on the text baseline,
   at both sizes. A 17px disc hanging off a 12px cap-height sits low without
   it. */
.form-floating > label > .ihelp { position: relative; top: -1px; vertical-align: middle; }

/* ══ Tom Select — every dropdown, searchable ═══════════════════════════════
 *
 * It enhances a real `<select>`, so the estate's job is to make the control it
 * draws look like the control it replaced.
 *
 * ── Which element is the control ──────────────────────────────────────────
 *
 * This is the thing to know before changing anything here, and it is not
 * obvious from the markup. Tom Select COPIES THE SELECT'S CLASSES ONTO ITS
 * WRAPPER, so the wrapper is `.form-select`. Its Bootstrap theme then forces
 * the inner `.ts-control` to `border: none !important` and
 * `background: transparent !important` and takes its padding away entirely.
 *
 * That is deliberate: the WRAPPER is meant to be the visual control, drawn by
 * Bootstrap's own `.form-select`, and `.ts-control` is only the content area
 * inside it. Which means the estate's border, radius, background and chevron
 * all arrive here already — they are the `.form-select` rules further up this
 * file, and nothing needs restating.
 *
 * What is left is the two things Bootstrap cannot know: the inner padding,
 * because the theme removed it, and the focus ring, because a `<div>` has no
 * `:focus`.
 */
.ts-wrapper.form-select { --ts-pr-caret: calc(var(--chevron-inset) + var(--chevron-w) + 12px); }
.ts-control {
  min-height: 40px;
  padding: 9px 12px;
  padding-right: var(--ts-pr-caret, 2.25rem);
  font-size: 14px;
  gap: 4px;
}
.ts-wrapper.single .ts-control, .ts-wrapper.single .ts-control input { cursor: pointer; }
/* The ring goes on the wrapper: it is the bordered box, and it is a `<div>`,
   so `:focus` never matches it. Tom Select puts `.focus` there instead. */
.ts-wrapper.focus.form-select, .ts-wrapper.focus.form-control {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .16);
}
.ts-wrapper.disabled.form-select { background-color: var(--bg); opacity: .6; }

/* ── Inside a floating field ───────────────────────────────────────────────
   The wrapper is the 54px box; the padding that makes room for the floated
   label has to be on `.ts-control`, because the theme zeroes the wrapper's
   with `!important`. The label floats permanently: a select always has a
   value, so one that floated only when "filled" would sit on the first option
   for ever. */
.form-floating > .ts-wrapper.form-select { height: 54px; min-height: 54px; padding: 0; }
.form-floating > .ts-wrapper .ts-control {
  height: 100%; min-height: 0;
  padding: 22px 13px 6px;
  padding-right: var(--ts-pr-caret, 2.25rem);
  align-items: center;
}
/* A SINGLE SELECT'S VALUE IS ONE LINE, AND ELLIPSISES.
 *
 * `.ts-control` is a flex box and its `.item` is a flex child, so a value wider
 * than a narrow control WRAPS — and the floating-label box is a fixed 54px, so
 * the second line is simply cut off. "30 minutes" came out as "30" with the top
 * two pixels of "minutes" showing underneath it, which reads as a rendering
 * fault rather than as a long value.
 *
 * `min-width: 0` is the half that is easy to miss: a flex child will not shrink
 * below its content's intrinsic width without it, so `text-overflow` never gets
 * the chance to apply.
 *
 * Single only. A multi-select's items are separate chips and are meant to wrap
 * onto as many lines as they need. */
.ts-wrapper.single .ts-control { flex-wrap: nowrap; }
.ts-wrapper.single .ts-control > .item {
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The label floats permanently over an enhanced select, for the same reason it
   does over a plain one: a select always has a value. Where and how it floats
   is the shared rule above — this only lifts it over the wrapper Tom Select
   draws, which is a positioned element of its own. */
.form-floating > .ts-wrapper ~ label { z-index: 3; }
/* The original select stays in the DOM so the form posts what it always did,
   moved out of sight. Without this, Bootstrap still lays a 54px box over it. */
.form-floating > select.ts-hidden-accessible { height: 0; min-height: 0; padding: 0; }

/* ── The menu ──────────────────────────────────────────────────────────────
 *
 * The theme paints the dropdown in `--bs-body-bg`, which is the PAGE colour —
 * light grey here. A panel floating over the page in the colour of the page
 * reads as a hole rather than as a panel. It is the same mistake Bootstrap's
 * floating label makes with its textarea plate, for the same reason: both
 * assume the page and the controls on it are the same colour.
 */
.ts-dropdown, .ts-dropdown.form-control, .ts-dropdown.form-select {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  margin-top: 4px;
  font-size: 14px;
  color: var(--ink);
  /* Over a modal, because a picker inside one opens into it. */
  z-index: 1700;
}
.ts-dropdown .ts-dropdown-content { padding: 4px 0 6px; max-height: 320px; }
.ts-dropdown .option { padding: 8px 14px; }
.ts-dropdown .active { background: rgba(var(--accent-rgb), .10); color: var(--ink); }
.ts-dropdown .selected { font-weight: 600; }
.ts-dropdown .optgroup-header {
  background: var(--bg); color: var(--slate-light);
  font-size: 12px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  padding: 8px 14px 4px;
}
.ts-dropdown .no-results { padding: 18px 14px; text-align: center; color: var(--slate-light); font-size: 13px; }
/* "+ New …" — sticky at the foot, so it does not scroll away from somebody who
   has just finished failing to find what they wanted. */
.ts-dropdown .create {
  position: sticky; bottom: 0;
  padding: 10px 14px; font-weight: 600; color: var(--primary);
  background: var(--surface); border-top: 1px solid var(--border);
}
.ts-dropdown .create i { font-size: 11px; margin-right: 4px; }
/* Tom Select highlights the matched text in a yellow from nobody's palette. */
.ts-dropdown .highlight { background: rgba(var(--accent-rgb), .18); border-radius: 2px; }

/* A record created from inside the picker a moment ago. Shown on its row and on
   the closed control, so the value you are looking at is visibly the one you
   just made rather than one that was always there. Matches the `.dt-new` pill
   the tables use. Lasts until the page is reloaded — it means "just added", not
   a stored property of the record. */
.ts-new {
  display: inline-block; margin-left: 8px; padding: 1px 7px;
  background: #F3E9FF; color: var(--purple-deep);
  font-size: 12px; font-weight: 700; line-height: 1.5;
  border-radius: var(--radius-pill); vertical-align: middle; letter-spacing: .01em;
}

/* ---- A unit, in the field rather than in the label ----
 *
 * "£" or "%" or "± pts", in a shaded cell joined to the left of the input.
 *
 * It replaces the habit of writing the unit into the label — "Default cost / hr
 * (£)" — which had two costs. The label floats and shrinks the moment somebody
 * types, so the unit shrank away with it exactly when a number was being entered
 * and the unit mattered most. And on a narrow field the label is the part that
 * gets the ellipsis, so "Second approver above (£)" lost its unit before it lost
 * anything else.
 *
 * Here the unit is not part of the label at all. It does not move, it cannot be
 * truncated, and the label is free to say only what the field is.
 *
 * Markup order is input, unit, label — the floating-label rules are sibling
 * selectors and need the label last.
 *
 * ONE VARIABLE DRIVES ALL THREE MEASUREMENTS. The cell's width, the input's left
 * padding and the label's inset have to agree, and a wide unit with a fixed
 * padding put "± pts" straight through the number beside it. Changing the width
 * now moves the other two with it.
 */
.form-floating.has-unit { --unit-w: 38px; }
.form-floating.has-unit.unit-wide { --unit-w: 62px; }

.form-floating.has-unit > .unit {
  position: absolute; left: 1px; top: 1px; bottom: 1px; width: var(--unit-w);
  z-index: 3;
  display: grid; place-items: center;
  font-size: 14px; font-weight: 600; color: var(--slate); line-height: 1;
  background: var(--bg); border-right: 1px solid var(--border-strong);
  border-radius: 9px 0 0 9px;
  /* Clicks fall through to the input underneath, so the shaded cell behaves as
     part of the control rather than as a dead spot beside it. */
  pointer-events: none;
}
.form-floating.has-unit > .form-control { padding-left: calc(var(--unit-w) + 12px); }
/* The label starts after the unit cell rather than under it. Only the LEFT
   inset moves: the vertical padding is what puts the label on its line, and a
   shorthand here would flatten it back to the top of the box. */
.form-floating.has-unit > label { left: calc(var(--unit-w) + 13px); padding-left: 0; }
/* The divider follows the border it is attached to, or a focused field has a
   purple outline with a grey line dropped through it. */
.form-floating.has-unit > .form-control:focus ~ .unit { border-right-color: var(--primary); }

/* ── The unit AFTER the number ─────────────────────────────────────────────
 *
 * "£ 900" and "900 secs". A currency is read before the quantity and a measure
 * after it, and the component only had the first — so a schedule's timeout
 * rendered as "secs 900", with the unit reached before there was anything for
 * it to be a unit of.
 *
 * Same cell, same border, mirrored. The label keeps the control's normal left
 * inset, because the cell is not in front of it any more. */
.form-floating.has-unit-after { --unit-w: 38px; }
.form-floating.has-unit-after.unit-wide { --unit-w: 62px; }
.form-floating.has-unit-after > .unit {
  position: absolute; top: 1px; right: 1px; bottom: 1px;
  width: var(--unit-w);
  display: grid; place-items: center;
  font-size: 14px; font-weight: 600; color: var(--slate); line-height: 1;
  background: var(--bg); border-left: 1px solid var(--border-strong);
  border-radius: 0 9px 9px 0;
  pointer-events: none;
  z-index: 1;
}
.form-floating.has-unit-after > .form-control { padding-right: calc(var(--unit-w) + 12px); }
.form-floating.has-unit-after > label { padding-right: calc(var(--unit-w) + 12px); }
.form-floating.has-unit-after > .form-control:focus ~ .unit { border-left-color: var(--primary); }

/* ── When the unit is a CHOICE ─────────────────────────────────────────────
 *
 * A prize can be valued in a currency other than the job's, so the £ in front
 * of the number has to be pickable. It was a second box beside the amount,
 * labelled "Currency", which is one control too many for one figure — and the
 * figure and its currency then read as two unrelated answers.
 *
 * Same cell, same border, same width arithmetic; the cell simply holds a real
 * `<select>` instead of a word. `pointer-events` come back on (the plain unit
 * turns them off so a click lands in the number), and the select is drawn with
 * no chrome of its own so the cell still reads as part of the control.
 *
 * `--unit-w` is wider here because a select needs room for its caret.
 */
.form-floating.has-unit.unit-pick { --unit-w: 58px; }
.form-floating.has-unit.unit-pick > .unit { pointer-events: auto; padding: 0; position: absolute; }
/* A caret, so the cell reads as something to press. Without it the symbol looks
   like the plain unit next to it and nobody finds the currency. */
.form-floating.has-unit.unit-pick > .unit::after {
  content: ""; position: absolute; right: 7px; top: 50%;
  width: 0; height: 0; margin-top: -1px;
  border: 4px solid transparent; border-top-color: var(--slate-light);
  pointer-events: none;
}
.form-floating.has-unit.unit-pick > .unit > select {
  width: 100%; height: 100%;
  border: 0; border-radius: 9px 0 0 9px;
  background: transparent;
  font: inherit; font-size: 14px; font-weight: 600; color: var(--slate);
  text-align: center;
  padding: 0 2px 0 6px;
  cursor: pointer;
  appearance: none; -webkit-appearance: none;
}
.form-floating.has-unit.unit-pick > .unit > select:focus-visible {
  outline: 2px solid var(--primary); outline-offset: -2px; border-radius: 9px 0 0 9px;
}

/* Bootstrap's `.form-text` is this estate's field hint. Bootstrap's own values
   are `.875em` and the secondary colour; these are the sizes already in use. */
.form-text { font-size: 13px; color: var(--slate-light); margin: 5px 2px 0; }
/* Figures line up on the decimal point, not on however wide each glyph is.
 *
 * Qualified with `.table`, because `.table > thead > tr > th { text-align: left }`
 * in app.css is more specific than a bare class and would otherwise win — a utility
 * class that loses to the thing it is meant to override is worse than no class,
 * since it looks applied in the markup. */
.text-right { text-align: right; }
.table th.text-right, .table td.text-right { text-align: right; }
.tabular, .table td.tabular { font-variant-numeric: tabular-nums; }
/* A hint under a form control needs to clear the control's box.
 *
 * `.form-floating` has no bottom margin, so the base 5px is measured from the
 * bottom BORDER — and a 12px paragraph has leading above its glyphs, which put
 * the first line of text on that border. Views papered over it with an inline
 * `margin:-4px 0 0`, which made it worse: the text sat a clear few pixels inside
 * the box. Those are gone; this is the spacing, in one place, and it applies to
 * a hint after a single field and after a row of them alike. */
/* A hint INSIDE a `.field`, which is where `fld()` puts it: the label is
   absolutely positioned over the control, so the hint is the next thing in flow
   and needs the same top margin a hint after the field would get. */
.field > .form-text { margin: 5px 2px 0; }
.form-floating + .form-text,
.field-row + .form-text,
.field-inline + .form-text,
.form-floating + .muted.small,
.field-row + .muted.small { margin-top: 9px; }
/* The same clearance, the other way up. A hint used as an INTRO — above the
   control it describes — got none, because the rule above only sets margin-top,
   and it sat on the box beneath it just as the old ones sat on the box above. */
.form-text.above { margin-bottom: 9px; }

/* ---- DataTable cells — general styling like ella-secure ---- */
table.dataTable { font-size: 15px; }
table.dataTable th, table.dataTable td { padding: 11px 8px; border-top: 1px solid var(--border); text-align: left; vertical-align: middle; line-height: 1.45; }
table.dataTable thead th { color: var(--slate-light); font-weight: 600; font-size: 13px; letter-spacing: .04em; text-transform: uppercase; border-top: 0; }
.dt-sev { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 8px; vertical-align: middle; }
.dt-new { background: #F3E9FF; color: var(--purple-deep); font-size: 12px; font-weight: 700; padding: 1px 7px; border-radius: 999px; margin-left: 6px; }
.dt-title { font-weight: 600; }
/* Two lines, then an ellipsis. A Deliver reminder lists every task on one job,
   which is right in the notification itself and wrong as a table row two screens
   tall — View opens the full list. */
.dt-sub { color: var(--slate); font-size: 12.5px; margin-top: 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.dt-svc { display: inline-flex; align-items: center; gap: 7px; font-size: 13px;
  color: var(--slate); white-space: nowrap; }
/* Only the ICON wears the platform's colour, set inline; the name beside it stays
   in ink. Text wears text colours — six differently-coloured service names is a
   legibility problem pretending to be a legend. */
.dt-svc i { flex: none; }
/* A numeric column's HEADER has to move with its numbers.
   This was scoped to `td`, so every right-aligned figure sat under a header
   still hard left of it — the eye reads the header as belonging to the column
   to its left, and a table of money becomes genuinely hard to scan.

   Qualified with the table class for the same reason `.text-right` below is:
   `.table > thead > tr > th { text-align:left }` in app.css is more specific than a bare
   class and silently wins. A utility that loses to the thing it is meant to
   override is worse than no utility, because the markup looks right. */
/* Numbers line up on the right, header and cells together.
   DataTables 2 ships `table.dataTable th.dt-right` itself, at two classes — more
   than the `.table > thead > tr > th` alignment in app.css — so a DataTable needs
   nothing here. A plain table still does: an unqualified `.dt-right` is one class
   and loses, and every numeric header silently stays left. */
.table th.dt-right, .table td.dt-right,
.data-table th.dt-right, .data-table td.dt-right { text-align: right; }
/* DataTables controls + pagination — styled EXACTLY like ella-secure (hub palette) */
.dt-container { font-size: 15px; color: var(--ink); }
/* DataTables 2's Bootstrap layout wraps its toolbar, its table and its footer in
   three `.row.mt-2` elements. That is three 8px top margins and a grid gutter on
   every child, on top of the 12px/14px inset below — so a list gained about 25px
   of air it did not ask for, and its controls stopped lining up with its columns.
   The rows are used as rows; the spacing is this file's.

   `!important` because `mt-2` is a Bootstrap UTILITY, and every Bootstrap utility
   carries `!important` by design — a plain rule, however specific, does not
   reach it. */
.dt-container > .row { --bs-gutter-x: 0; margin: 0 !important; }
.dt-container .dt-length,
.dt-container .dt-search,
.dt-container .dt-info,
.dt-container .dt-paging { color: var(--slate-light); font-size: 14px; padding: 12px 14px; }
/* 14px horizontally because that is the inset on a `.table` cell (app.css) and
   `.card` itself has none — so with `padding: 8px 0` the "N per page" select and
   the paginator sat hard against the card edge while every column of the table
   they belong to started 14px in. The toolbar has to line up with the columns,
   or the table looks like it is wearing somebody else's furniture. */
.dt-container .dt-length select,
.dt-container .dt-search input {
  font-family: inherit; font-size: 15px; border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 10px; background: #fff; color: var(--ink); margin: 0 6px; outline: none;
  transition: border-color .15s ease, box-shadow .15s ease;
}
/* The "N per page" select draws the app chevron too, scaled down and on a
   tighter inset — the control is only a couple of digits wide, so the full-size
   mark would dominate it. */
.dt-container .dt-length select {
  --chevron-w: 12px; --chevron-h: 8px; --chevron-inset: 9px;
  appearance: none; -webkit-appearance: none; cursor: pointer;
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right var(--chevron-inset) center;
  background-size: var(--chevron-w) var(--chevron-h);
  padding-right: calc(var(--chevron-inset) + var(--chevron-w) + 9px);
}
.dt-container .dt-length select:focus,
.dt-container .dt-search input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .18); }
.dt-container .dt-search input { min-width: 220px; }

/* Column filters, sitting in the toolbar beside "N per page".
   They used to be a separate band above the table: a row of full-height form
   fields with floating labels, above a row with the per-page select and the
   search box, above the table. Three stacked bands of furniture for one list,
   each aligned to a different grid, and the filters looked bolted on because
   they were.

   In the toolbar they read as what they are — another way to narrow the same
   list — and they borrow the length select's styling so the controls beside
   each other are the same size and shape. */
.dt-filters { display: inline-flex; gap: 8px; align-items: center; vertical-align: middle; }
.dt-filters select {
  font-family: inherit; font-size: 14px; border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 10px; background: #fff; color: var(--ink); outline: none; cursor: pointer;
  --chevron-w: 12px; --chevron-h: 8px; --chevron-inset: 9px;
  appearance: none; -webkit-appearance: none;
  background-image: var(--chevron); background-repeat: no-repeat;
  background-position: right var(--chevron-inset) center; background-size: var(--chevron-w) var(--chevron-h);
  padding-right: calc(var(--chevron-inset) + var(--chevron-w) + 9px);
  transition: border-color .15s ease, box-shadow .15s ease;
}
/* Capped, because a select sizes itself to its longest option and one lane
   called "Account / Requester" would otherwise set the width of the toolbar. */
.dt-filters select { max-width: 170px; text-overflow: ellipsis; }
.dt-filters select:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .18); }
/* A filter that is actually filtering says so, because a narrowed list with no
   visible reason is a list somebody reports as missing rows. */
.dt-filters select.is-on { border-color: var(--primary); color: var(--primary); font-weight: 600; }
/* ── A filter that is not a select ────────────────────────────────────────
   A date range is a text box and "Clear" is a button, and both land in the same
   toolbar as the selects. They borrow the same height, radius and border so the
   row reads as one set of controls rather than as three widgets that happened
   to end up beside each other. Without this they inherit the page's defaults
   and sit a few pixels taller than everything next to them, which is the kind
   of misalignment nobody can name and everybody notices. */
.dt-filters .dt-filter-date, .dt-filters .dt-filter-clear {
  font-family: inherit; font-size: 14px; border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 10px; background: var(--surface); color: var(--ink); outline: none;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.dt-filters .dt-filter-date { width: 150px; }
.dt-filters .dt-filter-date:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .18); }
.dt-filters .dt-filter-clear { cursor: pointer; color: var(--slate); }
.dt-filters .dt-filter-clear:hover { border-color: var(--border-strong); color: var(--ink); }

/* ── The plumbing, folded away ────────────────────────────────────────────
   A screen that reports on an integration ends up carrying both halves: what a
   person came to find out, and the raw material the answer was worked out from
   — ids, timestamps, a service key, a capability matrix. Put side by side the
   second half wins on sheer volume, and the screen reads as a diagnostic dump
   with a sentence hidden in it.

   `<details class="tech-details">` is where the raw material goes. It is a
   disclosure rather than a separate page because the person chasing a mismatch
   needs it on the same screen as the summary that sent them looking. */
.tech-details > summary {
  display: inline-flex; align-items: center; gap: 9px; cursor: pointer; list-style: none;
  font-weight: 700; font-size: 15px; color: var(--slate); padding: 2px 0;
}
.tech-details > summary::-webkit-details-marker { display: none; }
.tech-details > summary::after {
  content: "\203A"; font-size: 18px; line-height: 1; transition: transform .15s ease;
}
.tech-details[open] > summary::after { transform: rotate(90deg); }
.tech-details > summary:hover { color: var(--ink); }
.tech-details > summary:focus-visible {
  outline: none; box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .18); border-radius: 6px;
}

/* ── The toolbar's two popups: Filters, and which columns to show ─────────
   A `<details>` rather than a scripted dropdown: it opens on click and on
   Enter, closes on Escape, and is reachable by keyboard without a line of
   JavaScript deciding any of that. The summary borrows the length select's
   box so the controls beside each other are one set.

   The panel is ABSOLUTE. In flow it pushed the table down by its own height
   every time somebody opened it, which moves the rows they were about to
   read — the same complaint that keeps an edit in a modal instead of letting
   a row expand. */
.dt-pop { display: inline-flex; align-items: center; vertical-align: middle; }
.dt-pop details { position: relative; }
.dt-pop summary {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer; list-style: none;
  font-family: inherit; font-size: 14px; border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 10px; background: #fff; color: var(--ink); white-space: nowrap;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.dt-pop summary::-webkit-details-marker { display: none; }
.dt-pop summary:hover { border-color: var(--border-strong); }
.dt-pop summary:focus-visible { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .18); outline: none; }
/* Says it is doing something, for the same reason a filter says it is
   filtering: a table narrowed or missing a column somebody expects is a table
   they report as broken, and the toolbar is where they will look for why. */
.dt-pop summary.is-on { border-color: var(--primary); color: var(--primary); font-weight: 600; }
/* How many filters are on, because a shut panel hides its own state, */
.dt-pop-count {
  display: inline-grid; place-items: center; min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 999px; background: var(--primary); color: #fff; font-size: 12px; font-weight: 700;
}
.dt-pop-panel {
  /* Left, explicitly: DataTables' Bootstrap layout centres its toolbar cells
     at narrow widths, and the panel hangs inside one — so the labels came out
     centred on a phone and ragged on a desktop, from a rule about something
     else entirely. */
  text-align: left;
  position: absolute; z-index: 30; top: calc(100% + 6px); left: 0; min-width: 200px;
  background: #fff; border: 1px solid var(--border); border-radius: 10px; box-shadow: var(--shadow-lg);
  padding: 8px; display: flex; flex-direction: column; gap: 2px; max-height: 340px; overflow-y: auto;
}
/* A CLOSED panel must not lay out at all.
   `display: flex` above beats the UA rule that hides a `<details>`'s content
   while it is shut, so the panel kept a box — absolutely positioned, hanging
   off the right of a phone, invisible and 80px past the viewport. It cost
   nothing visually and `console-audit.mjs` failed /admin/audit at 390px for
   it, which is exactly what that tool is for: the page looked right. */
.dt-pop details:not([open]) > .dt-pop-panel { display: none; }
.dt-pop-reset {
  margin-top: 6px; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none;
  font-family: inherit; font-size: 13px; color: var(--slate); cursor: pointer; padding: 9px 8px 4px; text-align: left;
}
.dt-pop-reset:hover { color: var(--primary); }

/* ── Filters, stacked and named ───────────────────────────────────────────
   Every filter on the list in one panel, each under its own heading. Loose in
   the toolbar they were a row of selects reading "Any client  Any account
   Any status" — which is legible at two and stops being legible at five, and
   pushes the search box off the end of the row on the way. Named, stacked and
   behind one button, what can be narrowed is readable at a glance. */
.dt-filter-panel { min-width: 236px; gap: 10px; padding: 12px; }
.dt-filter-row { display: flex; flex-direction: column; gap: 5px; }
.dt-filter-name {
  /* 12px is the estate's floor, and `ReadableTextTest` holds it — uppercase
     and letter-spaced is what makes this read as a heading, not smallness. */
  font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--slate-light);
}
/* Full width inside the panel — the cap that stops one long option setting
   the width of the TOOLBAR is not wanted once they are stacked in a column. */
.dt-filter-panel select { width: 100%; max-width: none; }

/* ── Which columns to show ────────────────────────────────────────────── */
.dt-cols { margin-left: 8px; }
.dt-cols-panel label {
  display: flex; align-items: center; gap: 9px; margin: 0; padding: 6px 8px; border-radius: 7px;
  font-size: 14px; color: var(--ink); cursor: pointer; white-space: nowrap;
}
.dt-cols-panel label:hover { background: var(--surface); }
.dt-cols-panel label.is-locked { cursor: default; color: var(--slate-light); }
.dt-cols-panel label.is-locked:hover { background: none; }
.dt-cols-panel input { accent-color: var(--primary); width: 15px; height: 15px; flex: none; }

/* A filter whose column has been hidden goes with it. */
.dt-filter-row[hidden] { display: none; }
/* The toolbar is now three controls wide — per page, Filters, Columns. On a
   narrow screen they wrap instead of pushing the card sideways, because
   nothing scrolls sideways on a phone. */
.dt-container .dt-length { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; max-width: 100%; }
@media (max-width: 767.98px) {
  .dt-pop summary { font-size: 13px; }
  /* Capped to the viewport. WHICH edge it hangs from is measured on open —
     `ellaPlacePopup` in estate.js — because on a phone the toolbar wraps and
     the button can end up at either side. */
  .dt-pop-panel { max-width: calc(100vw - 16px); }
}
/* DataTables 2 paginates with Bootstrap's `.pagination`, so these are Bootstrap's
   elements now — `li.page-item > a.page-link` — rather than DataTables' own
   `.paginate_button`. Same look; the `!important`s are gone with the fight. */
/* ── A list that fits on one page ─────────────────────────────────────────
   "10 per page" and a pager whose only page is the current one are controls
   describing a list you can already see all of. They are DIMMED rather than
   removed: hiding them left an empty band of toolbar with the search box
   marooned at one end, which reads as a rendering fault rather than as a short
   list.

   The search box is never touched — it is what somebody opens the page for, and
   a table that grows a search box at row eleven reads as broken until it does. */
.dt-container .dt-idle { opacity: .45; pointer-events: none; }
.dt-container .dt-idle select, .dt-container .dt-idle .page-link { cursor: default; }

.dt-container .dt-paging .pagination { gap: 4px; margin: 0; }
.dt-container .dt-paging .page-link {
  font-size: 14px; padding: 6px 12px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface); color: var(--ink); cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease;
}
.dt-container .dt-paging .page-link:hover { background: var(--bg); border-color: var(--border-strong); color: var(--ink); }
.dt-container .dt-paging .page-link:focus { box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .18); }
.dt-container .dt-paging .page-item.active .page-link {
  background: var(--primary); border-color: var(--primary); color: #fff;
}
.dt-container .dt-paging .page-item.disabled .page-link {
  background: transparent; border-color: var(--border); color: var(--slate-light); cursor: not-allowed;
}
.dt-container .dt-processing {
  background: rgba(255,255,255,.92); border: 1px solid var(--border); border-radius: 8px; color: var(--slate-light);
  font-size: 14px; padding: 8px 14px; box-shadow: var(--shadow-sm);
}
table.dataTable { border-collapse: collapse !important; margin: 4px 0 !important; }
/* DataTables 1 drew its own bottom rule on a footerless table, in near-black,
   landing a pixel under the row border and reading as two rules with a gap.
   Version 2 does not, and `no-footer` is not a class it sets any more. */
/* And where a table is the LAST thing in a card, its final row need not repeat
   the card's own edge — it is the same line drawn twice, four pixels apart.
   Three shapes, because a card can BE the scroller (`class="card table-responsive"`)
   as well as contain one, and DataTables inserts a wrapper between them. The
   `.dt-onepage` cases are the ones where the info bar and paginator are hidden,
   so the table really is the last visible thing; with them showing, that rule
   separates the rows from the bar and is wanted. */
.card > .table-responsive:last-child > .table > tbody > tr:last-child > td,
.card.table-responsive > .table > tbody > tr:last-child > td,
.card.table-responsive > .dt-onepage > .table > tbody > tr:last-child > td,
.card > .table-responsive:last-child > .dt-onepage > .table > tbody > tr:last-child > td { border-bottom: 0; }
/* The search cell of the toolbar. A flex row, so a filter docked in beside the
   search box sits ON the same line — DataTables' own markup wraps its label
   onto a second row instead, which puts two controls where one row was. */
.dt-container .dt-search { display: flex; align-items: center; justify-content: flex-end; gap: 14px; flex-wrap: wrap; }
.dt-container .dt-search > label { display: inline-flex; align-items: center; gap: 7px; margin: 0; }
.dt-inline-filter { white-space: nowrap; }
.dt-inline-filter select { padding: 6px 26px 6px 10px; font-size: 13px; }
table.dataTable thead th { border-bottom: 1px solid var(--border) !important; cursor: pointer; user-select: none; }
/* DataTables 2 draws its sort arrows in a `.dt-column-order` ELEMENT rather than
   as a background image on the header, and lays the header out as
   `display: flex; justify-content: space-between`. So a title and its arrow are
   pushed to opposite ends of the cell — on a wide column the heading floated
   away from the words underneath it and sat next to the wrong one.

   The arrow belongs beside its title. `flex-start` does that, and it keeps
   working for a right-aligned column, where DataTables sets `row-reverse` and
   the pair lands at the right-hand end together. */
table.dataTable thead > tr > th div.dt-column-header { justify-content: flex-start; }
/* …and it did not, because DataTables also gives the TITLE `flex-grow: 1`, so
   the title filled the cell and there was no free space for `flex-start` to
   act on: the arrow still sat at the far end. On a right-aligned column that
   end is the LEFT edge, hard against the previous column's arrow, and Delivery
   › Stages read as though it had an empty column between Kind and By. */
table.dataTable thead > tr > th div.dt-column-header .dt-column-title { flex-grow: 0; }

/* ── DataTables 2 guesses which columns are numbers. It guesses wrong. ───────
 *
 * Version 2 added automatic column-type detection and aligns on it: a column it
 * reads as `dt-type-numeric` is right-aligned, header and cells. On the people
 * list it decided "Platforms" — a row of chips — and "Last seen" — the word
 * "Never" — were numeric, and pushed both to the right against columns of text.
 *
 * More to the point, this estate has already decided that alignment is a
 * DECISION, written in the markup as `dt-right` and carried on the `<th>` as
 * well as the `<td>`, with `NumericColumnAlignmentTest` failing the build if a
 * cell moves without its header. A library inferring it from content is the
 * opposite of that arrangement, and it cannot be tested: the answer depends on
 * whatever rows happen to be on the page.
 *
 * So the detection stays — DataTables sorts by it, and sorting by type is
 * right — and only its opinion about ALIGNMENT is dropped. A column the markup
 * DECLARES `dt-right` keeps it: at equal specificity this rule loaded later and
 * took the alignment off the very columns that had asked for it. */
table.dataTable th.dt-type-numeric:not(.dt-right), table.dataTable td.dt-type-numeric:not(.dt-right),
table.dataTable th.dt-type-date:not(.dt-right), table.dataTable td.dt-type-date:not(.dt-right) { text-align: inherit; }
table.dataTable thead > tr > th.dt-type-numeric:not(.dt-right) div.dt-column-header,
table.dataTable thead > tr > th.dt-type-date:not(.dt-right) div.dt-column-header { flex-direction: row; }

/* ── Where Bootstrap's reboot and the estate disagree ─────────────────────────
 *
 * Bootstrap restyles bare elements — a heading with no class, a paragraph, a
 * table cell. Most of the console never notices, because its components are
 * specific enough to win. These are the places it does.
 */

/* Headings.
 *
 * Bootstrap's scale is fluid — `h1 { font-size: calc(1.375rem + 1.5vw) }` — and
 * 500-weight. Neither suits a console. A fixed-width admin screen does not want
 * a heading that changes size with the window, and this estate's headings are
 * 700 everywhere somebody has set one deliberately.
 *
 * What this replaces is not a design. Before Bootstrap, a heading with no class
 * got the BROWSER's default, so `h5` and `h6` rendered smaller than body text
 * on a 15px base. The scale below is close enough to what the console was
 * already drawing that nothing moves, and it says so out loud.
 *
 * Real heading sizes still come from the component — `.pagehead h1`,
 * `.modal-header h3`. This is only what a heading gets when nobody said. */
h1, h2, h3, h4, h5, h6 {
  font-weight: 700;
  color: var(--ink);
  line-height: 1.15;
  letter-spacing: -.025em;
}
h1 { font-size: 2rem; }        /* 32px */
h2 { font-size: 1.5rem; }      /* 24px */
h3 { font-size: 1.125rem; }    /* 18px */
h4 { font-size: 1rem; }        /* 16px */
h5 { font-size: .9375rem; }    /* 15px */
h6 { font-size: .875rem; }     /* 14px */

/* `brand.css` zeroes the margin on h1–h4 and always has. Bootstrap gives every
   heading `margin-bottom: .5rem`, which caught h5 and h6 — the two nothing had
   an opinion about — and moved them alone. Whatever a heading's margin should
   be, it should not depend on which level it is. */
h5, h6 { margin: 0; }

/* Inline code keeps the size of the text it sits in. Bootstrap makes it
   `.875em`, which inside a 12.5px caption is 10.9px — small enough that a
   database column name or a token in a hint became the hardest thing on the
   screen to read, in the places those most often appear. */
code, kbd, samp { font-size: 1em; }

/* ── ONE COMPONENT IS STILL ON THE OLD MARKUP ───────────────────────────────
 *
 * `app/Views/admin/_confirm_dialog.php` is the dialog behind every destructive
 * action, and it is ESTATE-SYNCED — `tools/estate-sync-check.sh` requires the
 * six consoles to hold byte-identical copies. So it cannot move to Bootstrap's
 * modal markup until all six have Bootstrap, and today only this one does.
 *
 * Every other modal in this console has moved. These rules put Bootstrap's
 * `.modal` back where the old vocabulary needs it, scoped to the one structure
 * that still produces it — so they stop applying by themselves the moment the
 * dialog is converted, rather than waiting for somebody to notice.
 *
 * DELETE THIS BLOCK when `_confirm_dialog.php` moves. Nothing else needs it.
 *
 * The two vocabularies invert these names: here `.modal-overlay` is the
 * backdrop and `.modal` is the panel; in Bootstrap `.modal` IS the backdrop.
 * Left alone, Bootstrap's rule makes the confirm panel a fixed, full-height
 * layer nailed to the top-left of the window.
 */
.modal-overlay {
  position: fixed; inset: 0; z-index: 1500;
  display: none; align-items: flex-start; justify-content: center;
  padding: 48px 16px; overflow-y: auto;
  background: rgba(11, 11, 18, .45);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
.modal-overlay.open { display: flex; }
.modal-overlay > .modal {
  position: static; inset: auto; z-index: auto;
  width: 100%; height: auto; overflow: visible; outline: revert;
  margin: auto; display: flex; flex-direction: column;
  background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow-lg);
}
.modal-overlay > .modal { max-width: 640px; }
.modal-overlay > .modal.small { max-width: 460px; }

/* The confirm dialog is centred rather than top-aligned like the working
   modals: it interrupts, so it sits where the eye already is instead of making
   you look up. These lived in `hub.css` and lost to the rules above, which are
   both later and more specific — the dialog rendered 20px wider than designed
   and with the wrong padding. Keeping one vocabulary in one place is what stops
   that; it also means the whole block goes at once. */
/* Above a modal stacked over another (1500, then 1520, 1540 … — `estate.js`),
   so a confirmation asked for from inside one never opens behind it. */
.modal-overlay.confirm-overlay { align-items: center; padding: 24px 16px; z-index: 1590; }
.modal-overlay.confirm-overlay > .modal { max-width: 440px; }

/* ── The confirmation dialog ──────────────────────────────────────────────────
 *
 * `_confirm_dialog.php` is one file, byte-identical in all six consoles, and
 * its styling was not: six copies in six stylesheets, and two of them still
 * carried an older layout — `.confirm-body { display: flex }`, from when the
 * icon sat beside the text — that the newer centred rules never cancelled. The
 * result in ella Converse and ella Draw was a dialog laying its icon, heading,
 * message and buttons out in a ROW, with the message squeezed into a column one
 * word wide. It looked like a rendering fault, because it was one.
 *
 * The markup is shared, so the styling is shared. Here, once. */
.confirm-body { padding: 26px 26px 22px; text-align: center; }
.confirm-icon {
  width: 52px; height: 52px; margin: 0 auto 14px; border-radius: 999px;
  display: flex; align-items: center; justify-content: center; font-size: 22px;
  background: var(--danger-bg, #fff1f2); color: var(--danger-ink);
}
.modal-overlay.confirm-overlay.is-safe .confirm-icon { background: var(--info-bg, #DBEAFE); color: var(--info-ink); }
.confirm-body h3 { margin: 0 0 8px; font-size: 1.1rem; font-weight: 700; color: var(--ink); }
.confirm-body p { margin: 0; font-size: 14px; line-height: 1.55; color: var(--slate); }
.confirm-body p .confirm-warn { display: block; margin-top: 10px; font-weight: 600; color: var(--danger-ink); }
.confirm-actions { display: flex; gap: 10px; justify-content: center; margin-top: 20px; }
.confirm-actions .btn { min-width: 116px; }

/* The dialog's two buttons are drawn HERE, not left to Bootstrap's `.btn`.
 *
 * Everything else about a `.btn` in this file is a set of `--bs-btn-*` values,
 * and it is Bootstrap's own `.btn` rule that turns them into a padding, a
 * border and a face. The confirm dialog is the one component every destructive
 * action in six consoles goes through, so it must not need that rule to look
 * like a button: on any page that reaches it without Bootstrap's stylesheet —
 * a layout that forgets the vendor sheet, an asset base that does not serve
 * it — it fell back to the browser's grey bevelled control beside a red line,
 * which reads as a broken page at the one moment somebody is deciding whether
 * to destroy something. These declarations say what Bootstrap's would, from
 * the same variables, so with Bootstrap present nothing changes. */
.confirm-actions .btn {
  -webkit-appearance: none; appearance: none;
  padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
  font-family: inherit; font-size: var(--bs-btn-font-size);
  font-weight: var(--bs-btn-font-weight); line-height: var(--bs-btn-line-height);
  color: var(--bs-btn-color); background-color: var(--bs-btn-bg);
  border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
  border-radius: var(--bs-btn-border-radius);
  cursor: pointer; user-select: none;
}
/* Hover and keyboard focus share a face, as Bootstrap's do: Cancel is focused
   when the dialog opens, and it has to look like the button the Enter key is on. */
.confirm-actions .btn:hover,
.confirm-actions .btn:focus-visible {
  color: var(--bs-btn-hover-color); background-color: var(--bs-btn-hover-bg); border-color: var(--bs-btn-hover-border-color);
}
.confirm-actions .btn:active {
  color: var(--bs-btn-active-color); background-color: var(--bs-btn-active-bg); border-color: var(--bs-btn-active-border-color);
}
.confirm-actions .btn:disabled {
  color: var(--bs-btn-disabled-color); background-color: var(--bs-btn-disabled-bg);
  border-color: var(--bs-btn-disabled-border-color); opacity: var(--bs-btn-disabled-opacity);
  cursor: not-allowed; pointer-events: none;
}

/* Typed confirmation. Only on the handful of actions whose consequences reach
   past the record in front of you — clicking through a dialog is muscle memory
   after the twentieth one, and typing a word is not. */
.confirm-type { display: block; margin: 16px auto 0; max-width: 260px; text-align: left; }
/* A plain confirmation asks nothing to be typed, and the script says so with
   the `hidden` attribute. `display: block` above outranks the browser's own
   `[hidden]` rule, so without this line the box is hidden only on pages where
   some other stylesheet happens to restate `[hidden]` with `!important` — and
   every plain confirm elsewhere showed an empty text box under its message,
   asking for nothing. The dialog states it for itself. */
.confirm-type[hidden] { display: none !important; }
.confirm-type span { display: block; font-size: 12.5px; font-weight: 600; color: var(--slate); margin-bottom: 6px; }
.confirm-type input {
  width: 100%; font: inherit; font-size: 15px; padding: 8px 10px;
  border: 1px solid var(--border-strong); border-radius: 8px; background: var(--surface); color: var(--ink);
}
.confirm-type input:focus { outline: none; border-color: var(--danger, #dc3545); box-shadow: 0 0 0 3px rgba(220,53,69,.18); }

/* `table.grid` is `.table` everywhere in this console except one place.
 *
 * `_editor/canvas.php` builds a revision table in a JavaScript string, and it is
 * estate-synced with ella Platform, ella Converse and ella Draw — so its class
 * name can only change when all four have Bootstrap. It is one selector, and it
 * is why the editor's history panel still has borders and padding.
 *
 * DELETE with the rest of this block. */
table.grid { width: 100%; border-collapse: collapse; font-size: 13.5px; }
table.grid td { padding: 13px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }

/* `.field` is `.form-floating` everywhere in this console except one place.
 *
 * `_editor/canvas.php` is estate-synced with ella Platform, ella Converse and
 * ella Draw, so its one floating field can only convert when all four have
 * Bootstrap. Without this it would render as a bare browser input with a label
 * lying across it — the editor's "What changed" box, on every save.
 *
 * DELETE THIS with the modal block above, when the editor converts. */
.field { position: relative; display: block; margin: 0; }

/* ── All three controls, not just `input` ────────────────────────────────────
 *
 * This block covered `input` alone, because it was written for the editor's one
 * floating field — "What changed" — and nothing else used it. The block editor's
 * inspector is built entirely from `.field` now, and half of it is selects and
 * textareas: they rendered as bare browser controls, grey-bordered and 23px
 * tall, with the label sitting UNDERNEATH rather than floating in them, because
 * an unstyled select is not 54px and there was nothing for the label to lie
 * across.
 *
 * Shaped after Bootstrap's `.form-floating`, which is what ella Hub's own forms
 * use and which has covered input, textarea and select from the start. `.field`
 * is the estate's own version of the same idea — it exists because the estate
 * styles a control the way it wants rather than overriding Bootstrap's — and
 * "input only" was simply how far it had been taken, not a decision. */
.field > input,
.field > textarea,
.field > select {
  width: 100%; height: 54px; padding: 22px 12px 6px;
  font: inherit; font-size: 14px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: 10px; outline: none;
  appearance: none; -webkit-appearance: none;
}
.field > textarea {
  height: auto; min-height: 92px; resize: vertical; line-height: 1.45;
  padding-top: 24px;
}
.field > select {
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: var(--chevron-w) var(--chevron-h);
  padding-right: calc(var(--chevron-w) + 24px);
  cursor: pointer;
}
.field > label {
  position: absolute; top: 0; left: 12px; right: 12px; height: 54px;
  display: flex; align-items: center; font-size: 14px; color: var(--slate-light);
  margin: 0; pointer-events: none; transform-origin: 0 0;
  transition: transform .15s ease, color .15s ease;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* An inline span inside a floating label loses the space before it — the label
   is a flex container, so the whitespace in the markup collapses and it reads
   "Help text(optional)". */
.field > label .muted,
.field > label .req { margin-left: 4px; }

.field > input:focus ~ label,
.field > input:not(:placeholder-shown) ~ label,
.field > input:-webkit-autofill ~ label,
.field > textarea:focus ~ label,
.field > textarea:not(:placeholder-shown) ~ label,
/* A select is ALWAYS floated: it always has a value showing, so a label lying
   across it would sit on top of the chosen option. */
.field > select ~ label {
  transform: translateY(-11px) scale(.78);
  right: auto; width: calc((100% - 24px) / .78);
}
.field > input:focus,
.field > textarea:focus,
.field > select:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .16); }
.field > input:focus ~ label,
.field > textarea:focus ~ label,
.field > select:focus ~ label { color: var(--primary-ink); }

/* A textarea's label sits at the top of the box rather than centred down a
   92px-tall control. */
.field > textarea ~ label { height: 46px; align-items: flex-start; padding-top: 15px; }
.field > textarea:focus ~ label,
.field > textarea:not(:placeholder-shown) ~ label { padding-top: 0; height: 54px; align-items: center; }

/* ── A card that asks for flex means a ROW ──────────────────────────────────
 *
 * Bootstrap's `.card` is `display:flex; flex-direction:column`. A call site that
 * writes `style="display:flex"` on one is asking for a row — that is what plain
 * `display:flex` means everywhere else in CSS — and the inline rule matches
 * Bootstrap's `display` exactly, so nothing looks overridden. The DIRECTION is
 * not in the inline style, so Bootstrap's column silently wins.
 *
 * The symptom is not subtle and is easy to blame on the markup: ella Hub's job
 * header put its stage rail on one line and its stage picker on the next, both
 * CENTRED, in a card with a third of the page empty either side — because
 * `align-items:center` is a horizontal instruction once the direction flips.
 * Fifteen call sites across the estate write this shape.
 *
 * Scoped to cards that asked for flex and did NOT name a direction, so a card
 * that deliberately says `flex-direction:column` inline is untouched, and a
 * card that never asked for flex stays a real Bootstrap card — which is what
 * makes `.card-header` and `.card-footer` work if anything ever wants them.
 * Both spacings, because both are written. */
.card[style*="display:flex"]:not([style*="flex-direction"]),
.card[style*="display: flex"]:not([style*="flex-direction"]) { flex-direction: row; }

/* ── `.panel` is this estate's `.card-body`, and says so ────────────────────
 *
 * Bootstrap's card is a flex column with a `.card-body` inside it holding the
 * padding. Every console's card is `class="card panel"` — one padded region, no
 * wrapper — which is the same arrangement with the body's job done by a class
 * on the card itself.
 *
 * EVERY console, now. ella Hub and ella Platform were converted with the rest
 * of the Bootstrap work; ella Secure, Converse, Draw and Jobs kept a bare
 * `.panel`, which is a hand-written duplicate of a card. 289 call sites moved,
 * and the check was that nothing shifted: 77 panels on 27 screens across the
 * six, computed styles and bounding boxes, before and after. Every box
 * identical.
 *
 * So `.panel` declares what a card body is: a BLOCK. That matters, because a
 * block child of a flex column stretches to the full width and stops collapsing
 * its margins against its siblings, and several screens put a floated or
 * inline-block child in one.
 *
 * A plain `.card` is left alone, and is therefore a real Bootstrap card — which
 * is what makes `.card-header` and `.card-footer` size correctly if anything
 * ever wants them.
 *
 * The remaining step is to make the arrangement literal: `class="card panel"`
 * becomes `<div class="card"><div class="card-body">`, and this rule goes with
 * it. It moves the accordion header inset, which is a documented convention
 * with a test on it, so it is its own change rather than a rider on this one —
 * and it is now ONE change for six consoles rather than six.
 */
/* …and it does not break words.
 *
 * Bootstrap's `.card` sets `word-wrap: break-word`, which is right for a card
 * that might be handed a URL with no spaces in it, and wrong for a panel: the
 * property inherits, so every narrow cell inside one becomes breakable. ella
 * Converse's dashboard heatmap has a column of day names, and "Wed" came out as
 * "We" over "d" — which pushed the panel 72px taller and every panel below it
 * down the page. A panel has never broken words; this says so. */
.panel { display: block; overflow-wrap: normal; }


/* ── The console shell, once ──────────────────────────────────────────────────
 *
 * The sidebar, the topbar's two ends, the breadcrumb trail, the split-button row
 * actions, the concurrency banner's values. Every console has all of it, and
 * every console had its own copy: around 128 shell rules each, six times over.
 *
 * These 42 selectors were byte-identical in all six once two things were
 * normalised — whitespace, and the two names the estate has for the same token
 * (`--text`/`--ink`, `--muted`/`--slate-light`, `--white`/`#fff`). So moving
 * them here changes nothing anybody can see, which is exactly the point: it is
 * the subset that can be PROVED safe, and it makes drift on that subset
 * impossible. The confirmation dialog is what happens without that proof — one
 * copy kept a rule the others had dropped, and two consoles drew it sideways.
 *
 * Proved how: every one of these selectors' computed styles and bounding boxes
 * was measured on twelve screens across the six consoles, sidebar collapsed and
 * expanded, before and after. Twenty-three shell selectors x twenty-four page
 * states, zero differences.
 *
 * They are written in the vocabulary that resolves in ALL SIX. `--ink` and
 * `--slate-light` exist everywhere (the `admin.css` consoles alias them onto
 * their own `--text` and `--muted`); `--white` does not, so white is a literal.
 *
 * ── What is deliberately NOT here ───────────────────────────────────────────
 *
 * `estate.css` loads AFTER each console's own stylesheet, which is what lets it
 * override — and is why a rule cannot move here if a console also narrows it
 * inside a `@media` block. The unconditional copy would then land after the
 * narrowed one and win at every width. Five selectors are excluded for that
 * reason alone: `.topbar`, `.sidebar.expanded ~ .main-content`,
 * `.sidebar-logo-text .hub`, and the breadcrumb's two mobile-truncation rules.
 * Hoisting those two moved every console's separator and hid one outright,
 * which is what the measurement caught.
 *
 * The other 45 shared selectors genuinely differ. Some are per-console by
 * nature (`:root`, `body`, keyframe stops). Some are small drift nobody has
 * decided about — `.user-toggle` is padded 6px 12px in two consoles and
 * 6px 10px in four. And one is a real collision worth knowing about:
 * **`.field-row` means two different things.** It is a flex row of fields in
 * ella Hub and ella Platform, and `grid-column: 1 / -1` in the other four. That
 * one needs a rename before it can be shared, not a reconciliation.
 * ------------------------------------------------------------------------- */

.admin-wrapper { display: flex; min-height: 100vh; }
.crumbs {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--slate-light);
  font-size: 15px;
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.crumbs .sep { opacity: 0.5; flex-shrink: 0; }
.crumbs a:hover { color: var(--ink); }
.guard-values {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 14px;
  margin: 0;
  font-size: .86rem;
}
.guard-values dd { margin: 0; font-variant-numeric: tabular-nums; }
.guard-values dt { font-weight: 600; text-transform: capitalize; }
.notif-toggle:hover { color: var(--primary-ink); }
.row-actions {
  position: relative;
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  white-space: nowrap;
}
.row-actions .row-actions-primary {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  padding-right: 10px;
}
.row-actions .row-actions-primary, .row-actions .row-actions-toggle { margin: 0; }
/* BOTH HALVES ARE THE SAME HEIGHT, and this is what makes that true.
   
   `.btn` sets `align-self: center` globally — right for a button sitting in a
   flex row beside text, and wrong for half of a split control: the caret has
   less content than the label, so it centred itself at its own height and came
   out SHORTER. Measured at 35px against 25px, a 10px step in the middle of one
   control, on every DataTable row in the estate.
   
   `stretch` overrides that for these two only. The wrapper is already
   `align-items: stretch`; the children were opting out of it without meaning to. */
.row-actions .row-actions-primary,
.row-actions .row-actions-toggle { align-self: stretch; }
.row-actions .row-actions-toggle {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  margin-left: -1px;
  padding-left: 8px;
  padding-right: 8px;
  line-height: 1;
}
.row-actions .row-actions-toggle i { font-size: 11px; transition: transform 120ms ease; }
.row-actions .row-actions-toggle:not(:disabled):hover { z-index: 1; }
.row-actions .row-actions-toggle[aria-expanded="true"] i { transform: rotate(180deg); }
.row-actions.is-open .row-actions-menu { display: block; }
.segmented .seg-count {
  margin-left: .4em;
  font-size: max(12px, .8em);
  font-variant-numeric: tabular-nums;
  opacity: .65;
}
.segmented a.active .seg-count { opacity: .85; }
.sidebar-flyout a {
  display:flex;
  align-items:center;
  gap:11px;
  padding:9px 12px;
  border-radius:8px;
  color:#c9cdd6;
  text-decoration:none;
  font-size:14px;
  font-weight:500;
  white-space:nowrap;
}
.sidebar-flyout a i { width:18px; text-align:center; font-size: max(12px, .95em); opacity:.85; }
.sidebar-flyout a.active i { opacity:1; }
.sidebar-flyout a:hover { background:rgba(255,255,255,.08); color:#fff; }
.sidebar-flyout-title {
  font-size: 12px;
  font-weight:700;
  text-transform:uppercase;
  letter-spacing:.05em;
  color:#8890a0;
  padding:6px 12px 8px;
}
.sidebar-flyout::before {
  content:"";
  position:absolute;
  left:-10px;
  top:0;
  width:10px;
  height:100%;
}
.sidebar-header {
  height: var(--sidebar-header-h);
  display: flex;
  align-items: center;
  padding: 0 18px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
  flex-shrink: 0;
}
.sidebar-logo-mark {
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  filter: drop-shadow(0 0 7px rgba(var(--accent-rgb), 0.55));
}
.sidebar-logo-mark svg { width: 100%; height: 100%; display: block; }
.sidebar-menu li, .sidebar-menu ul { list-style: none; margin: 0; padding: 0; }
.sidebar-menu-item { position: relative; }
.sidebar-menu-item.has-flyout { position:relative; }
.sidebar-menu-item.has-flyout.flyout-active > .sidebar-menu-link {
  color:#FFFFFF;
  background:rgba(255,255,255,.10);
}
.sidebar-menu-item.has-flyout:hover > .sidebar-flyout, .sidebar-menu-item.has-flyout:focus-within > .sidebar-flyout {
  opacity:1;
  visibility:visible;
  transform:translateX(0);
}
.sidebar-menu::-webkit-scrollbar { width: 6px; }
.sidebar-menu::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.25); border-radius: 4px; }
.sidebar-menu:has(.sidebar-menu-link:hover) .sidebar-menu-link:not(:hover):not(.active) {
  opacity: 0.45;
}
.sidebar.expanded ~ .topbar { left: var(--sidebar-width); }
.sidebar.expanded, .sidebar:hover, .sidebar.hover-expanded { width: var(--sidebar-width); }
.sidebar:not(:hover):not(.expanded) .has-flyout .sidebar-menu-link::after { display:none; }
.topbar-help:hover { color: #2A57D0; }
.topbar-left { display: flex; align-items: center; gap: 14px; min-width: 0; flex: 1; }
.topbar-right { display: flex; align-items: center; gap: 10px; }
/* ── The audit diff ───────────────────────────────────────────────────────────
 *
 * `admin/_audit_changes.php` renders a change as what it was and what it
 * became. Here rather than in a console's own sheet because the partial is
 * shared: a table that looked different in each console would defeat the point
 * of one audit trail.
 *
 * Before and after are given colour, not just position. A three-column table
 * where two columns look identical makes the reader check the heading on every
 * row; a muted "was" against a solid "is" is legible at a glance, and the
 * colours are the estate's own rather than red-and-green — nothing here is an
 * error, and a change is not a failure. */
.audit-diff { margin: 0; font-size: 13.5px; }
.audit-diff th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--slate-light); }
.audit-diff td { vertical-align: top; word-break: break-word; }
.audit-diff .audit-field { font-weight: 600; white-space: nowrap; padding-right: 18px; }
.audit-diff .audit-from { color: var(--slate); text-decoration: line-through; text-decoration-color: var(--border-strong); }
.audit-diff .audit-to { color: var(--ink); font-weight: 600; }
.audit-detail-head { margin: 14px 0 6px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--slate-light); }
/* ── The one-line summary in the LIST, not the panel ──────────────────────
   A row's change is summarised in a table cell — "budget changed", "3 fields
   changed", or a recorded note. A note can be a sentence, and one long value in
   one cell sets the width of the whole table: ella Reward's audit list pushed
   the page sideways far enough to slide the content under the sidebar, on a
   1400px screen. Capped and ellipsised, with the full text in the title
   attribute and the real answer one click away in the panel. */
/* `inline-block`, and it is the whole rule. `max-width` does nothing to an
   inline element, so without it the cap is present, correct-looking and has no
   effect — which is exactly how this shipped the first time. */
.audit-summary { display: inline-block; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
tr.row-link { cursor: pointer; }

/* ══ The glossary, behind a button, in a modal ═════════════════════════════
 *
 * Here rather than in each console's own sheet for the reason `.row-actions`
 * and `.audit-diff` are here: the VIEW is shared (`admin/_glossary_modal.php`,
 * on the sync list), so a glossary that looked different in each console would
 * defeat the point of having one component.
 *
 * ella Secure wrote the original and it was right about the important part —
 * a term is a `<details>`, so the short answer is always visible and the long
 * one costs a click. Somebody scanning for a word reads twenty summaries; the
 * one person who needs the full explanation opens one. What has changed is
 * that it is a component rather than a screen, and its colours are tokens, so
 * the same glossary can open over any console.
 *
 * A TERM WITH NO LONG FORM IS NOT A DISCLOSURE. It renders open and drops the
 * "Show more", because a control that expands to reveal nothing teaches people
 * that the control is decoration — the same argument as the disabled chevron
 * on a row with one action. */
.glossary-intro { margin: 0 0 18px; color: var(--slate); font-size: 15px; line-height: 1.55; }
.glossary-search { margin: 0 0 16px; }
.glossary-section {
  margin: 24px 0 10px; padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
  color: var(--slate);
}
.glossary-section:first-of-type { margin-top: 2px; }
.glossary-term {
  border: 1px solid var(--border); border-radius: var(--radius);
  margin-bottom: 8px; background: var(--surface);
  transition: border-color .15s ease, box-shadow .15s ease;
}
.glossary-term[open] { border-color: var(--primary); box-shadow: 0 1px 3px rgba(var(--accent-rgb), .10); }
.glossary-term > summary {
  cursor: pointer; list-style: none; padding: 13px 15px;
  font-size: 15px; color: var(--ink);
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  user-select: none;
}
.glossary-term > summary::-webkit-details-marker { display: none; }
.glossary-word { font-weight: 700; flex: none; }
.glossary-summary { color: var(--slate); font-size: 14px; flex: 1; min-width: 200px; line-height: 1.5; }
.glossary-detail { padding: 0 15px 15px; font-size: 15px; line-height: 1.65; color: var(--ink); }
/* The affordance, only where there is something behind it. */
.glossary-term:has(> .glossary-detail) > summary::after {
  content: "Show more \25be"; margin-left: auto;
  font-size: 13px; font-weight: 600; color: var(--primary-ink);
  flex-shrink: 0; white-space: nowrap;
}
.glossary-term[open]:has(> .glossary-detail) > summary::after { content: "Show less \25b4"; }
.glossary-empty { color: var(--slate); font-size: 15px; margin: 18px 0 0; }
/* Filtered out by the search. `hidden` is not enough on a `<details>`, which
   the user agent gives `display: block` of its own. */
.glossary-term[hidden], .glossary-section[hidden] { display: none !important; }

/* ── A field that has been refused ─────────────────────────────────────────
 *
 * The message goes UNDER the control, in the danger ink, and it comes before
 * the hint rather than replacing it: the hint says what the field wants and the
 * error says what went wrong with what was typed, and somebody who has just got
 * it wrong is exactly the person who still needs the hint. */
.form-text { font-size: 14px; color: var(--slate); line-height: 1.5; }
.form-text-error { color: var(--danger-ink); font-weight: 600; }
.form-control.is-invalid, .form-select.is-invalid { border-color: var(--danger-ink); }
.form-control.is-invalid:focus, .form-select.is-invalid:focus {
  border-color: var(--danger-ink); box-shadow: 0 0 0 3px rgba(176, 42, 55, .16);
}

/* ── The floor, for the sizes a stylesheet cannot see ──────────────────────
 *
 * `sup` and `sub` have no size of their own: the user agent applies
 * `font-size: smaller`, which is about 0.72 of the parent. In a 15px date that
 * is 10.8px, and the estate prints ordinals — "11th", "3rd" — all over its
 * dashboards. Nothing in a stylesheet says 10.8px anywhere, which is why the
 * absolute sweep could not find them and a rendered-page audit could.
 *
 * `max()` rather than a flat 12px, so a superscript in a 34px figure is still
 * a superscript rather than a full-height character. */
sup, sub { font-size: max(12px, .72em); }

/* A monospace face runs visibly smaller than the text around it at the same
   size, which is why every console shrinks it — and then a `.mono` inside a
   `.small` is 11px. The floor is the same idea in the same place. */
.mono, code, kbd, samp { font-size: max(12px, .9em); }
/* Bootstrap's `.badge` is `.75em`, which in a 14px table cell is 10.5px. */
.badge { font-size: max(12px, .75em); }

/* ── The dashboard's heading and clock ─────────────────────────────────────
 *
 * `admin/_dashboard_head.php`, which is one file in six consoles. The clock had
 * five implementations and two stylesheets carrying a `.dashboard-clock-tz`
 * rule that no view used, so the rules live here now with the markup they are
 * about.
 *
 * The time is tabular-figure so the seconds do not shift the timezone sideways
 * once a second, which is the kind of movement the eye catches from across a
 * desk and nothing explains.
 */
.dashboard-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
/* The title carries the row's height and nothing else, so a heading with a
 * control in it lines up with one that has only the clock. */
.dashboard-heading .page-title { margin: 0; }
/* The gap under the heading belongs HERE, not to whatever happens to be first
 * on the page. Three consoles had nothing at all, so a stat card's 3px accent
 * rule sat against the title's descenders; the two with a line under the title
 * wrapped the whole thing in their own flex box to get it — and one of those
 * boxes was a flex container with a single child, which shrank the heading to
 * its content and put the clock beside the title instead of at the right edge.
 * A wrapper nobody has to remember cannot be forgotten by the next console. */
.dashboard-head { margin: 0 0 18px; }
.dashboard-sub {
  color: var(--slate);
  font-size: 14.5px;
  margin: 4px 0 0;
}
.dashboard-head-aside {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.dashboard-clock {
  color: var(--slate);
  font-size: 13px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  /* Stated rather than inherited. Five consoles said `.01em` in their own
   * stylesheet and the sixth inherits `-.01em` from its brand type, so the same
   * clock came out 12px narrower there and started 12px further along the row.
   * Chrome that is the same in six places has to say what it is in one. */
  letter-spacing: .01em;
}
/* Dimmer than the time, because it does not change and is read once. */
.dashboard-clock-tz { opacity: .78; }
/* Nothing to separate until the time has rendered. */
.dashboard-clock-when:empty ~ .dashboard-clock-sep,
.dashboard-clock-when:empty ~ .dashboard-clock-tz { display: none; }

/* A control is never wider than what it sits in.
 *
 * A fixed `width` or `size` on an input is a desktop measurement — `size="40"`
 * on a text field is about 344px — and on a 390px screen it pushed the page
 * sideways. `max-width: 100%` leaves every one of those alone at full width and
 * only bites where the container is narrower, which is exactly when it should.
 *
 * Not `width: 100%`: a 64px sort-order box beside a label is meant to be 64px,
 * and stretching it would be a different bug in the same place. */
input, select, textarea, .form-control, .form-select { max-width: 100%; }

/* ── The page must never scroll sideways ──────────────────────────────────────
 *
 * Measured across six consoles at 390px: 38 of 107 routes scrolled horizontally.
 * Two causes, both in shared chrome, which is why both fixes are here and not in
 * six stylesheets.
 *
 * The estate's rule is that only tables, diagrams and code blocks may be wider
 * than the viewport, each inside its OWN `overflow-x: auto` container. A page
 * that scrolls sideways on a phone moves the whole layout under the reader's
 * thumb — the sidebar and topbar included — so the thing they were reading is
 * not where they left it.
 *
 * ── 1. A DataTable was not in a scroll container at all ──────────────────────
 *
 * DataTables 2 lays its table out in `.dt-container > .dt-layout-table > …` and
 * gives none of them an overflow. A twelve-column table on a 390px screen
 * therefore widened the PAGE — ella Secure's lists screen to 1,142px, nearly
 * three viewports. `.table-responsive` solves this for a hand-written table and
 * cannot be applied here, because DataTables builds these wrappers itself after
 * the markup is written.
 *
 * On the row rather than the table: the row is full width, so the scrollbar
 * lands under the table where a thumb expects it, and the search box and pager
 * above and below stay put. */
.dt-container .dt-layout-table { overflow-x: auto; }

/* ── 2. The topbar could not shrink ───────────────────────────────────────────
 *
 * `.topbar-right` holds the search box, the account switcher, four icons and the
 * identity chip. The search input is a fixed width, so the row measured 386px on
 * a 390px screen before anything else was counted, and ran ~70px past the right
 * edge in EVERY console — the one defect on the list that was on every page
 * rather than some.
 *
 * `min-width: 0` is the part that actually does it: a flex item will not shrink
 * below its content's intrinsic width without it, whatever `flex-shrink` says.
 * That is why this looks like it should already work and does not.
 *
 * `flex: 0 1 auto` rather than `1 1 auto`: the right half may SHRINK but not
 * grow. Growing, it took its full width plus half the spare room, and the
 * breadcrumbs in the left half were cut to "Configure › De" on a 1440px screen
 * with empty space beside them — 286px of the 595 they needed. */
.topbar-left, .topbar-right { min-width: 0; }
.topbar-right { flex: 0 1 auto; justify-content: flex-end; }

/* The search takes what is left rather than a fixed width. Its own console
 * stylesheet sets the 210px it wants on a desktop; this caps it at what there
 * actually is, which on a wide screen is more than 210px and so changes nothing. */
.topbar-search { min-width: 0; flex: 0 1 auto; }
.topbar-search input { max-width: 100%; }

@media (max-width: 640px) {
  /* Below this the search box is a slot the identity chip and the bell need
   * more. Hidden rather than shrunk to nothing: a 20px text field is a control
   * that looks broken, and every console has a search screen of its own. */
  .topbar-search { display: none; }

  /* A plain table scrolls itself below this width.
   *
   * `.data-table` is ella Converse's, ella Draw's and ella Jobs' own table class
   * — 47 views use it and NOT ONE is inside a `.table-responsive`. That is a
   * convention rather than an oversight, so the fix belongs on the class and not
   * in 47 markup edits.
   *
   * `display: block` makes a table scrollable, at the cost of the table layout
   * algorithm — column widths become content-driven. That cost is only paid
   * below 640px, where a five-column table is going to scroll whatever we do and
   * precise column sizing is the least of it. Above this width nothing changes.
   *
   * Measured: ella Converse's retention screen was a 906px table on a 390px
   * screen, taking the whole page sideways with it.
   *
   * No `white-space: nowrap` — that would stop cells wrapping and make the table
   * WIDER, which is the opposite of the point. */
  table.data-table { display: block; overflow-x: auto; }

  /* The sidebar's hover flyout, which a phone has no way to open.
   *
   * `visibility: hidden` is not enough: it is `position: fixed` at
   * `left: var(--sidebar-width)` with a 220px minimum, so it sits past the right
   * edge of a 390px screen and a fixed element out there GROWS `scrollWidth` —
   * the page scrolled sideways because of a menu nobody could see, on every
   * screen of every console. It was the last cause on the list and the least
   * visible one. */
  .sidebar-flyout { display: none; }
}
