/* =========================================================
   bridge.css (was ui-next.css) - the new UI, phase 0: tokens only.

   Every rule in this file lives under html[data-ui="next"]. That scoping is the
   whole reason a single cutover is survivable: both stylesheets ship for the
   life of the migration, so the app runs at every commit, a converted page can
   be reviewed the day it lands, and the switch for users is one flag rather than
   one enormous merge.

   The attribute costs one level of specificity on every rule. That is the price
   of the arrangement and it is deliberate: it also means nothing in here can
   accidentally win against Bootstrap on a page that has not been converted yet.

   The scope and Bootstrap both come out together, in phase 6, in separate
   commits. See docs/ui-concept/MIGRATION.md.

   PHASE 0 SCOPE: custom properties, and nothing else. This file changes no
   pixels today. It exists so that everything after it has somewhere to put
   values, and so the token layer can be reviewed on its own rather than
   arriving tangled up with a component.

   Layer 1 is taken verbatim from wwwroot/css/landing.css. Not approximated,
   copied. The point of the exercise is that the app and the marketing site are
   the same product, and that only holds if the numbers are identical.
   ========================================================= */

html[data-ui="next"] {
  /* Colour, type, radius, shadow and motion come from the design system's
     tokens.css (the old names here are aliases there, removed in A10). What
     stays is the portal's density: it is worked in all day, so it is denser
     than the brand's default body size. */
  --app-font: 14px;
  --app-lh: 1.5;
  --pad: 18px;
  --gap: 14px;
}

/* Phase 0 ends here. No selectors below this line yet.

   Phase 1 adds the shell (rail, topbar, pickers, drawer).
   Phase 2 adds the primitives (button, panel, table, toolbar, field, dialog).
   Neither should introduce a value that is not defined above; if one is needed,
   it belongs in this block, named, rather than inline at the call site. */

/* =========================================================
   PHASE 1: the shell
   Layout/PrinterVisionLayout.razor

   Deliberately CSS only. Not one line of Razor changes in this phase, which is
   not a stylistic preference: the shell holds the rail toggle, the impersonation
   picker, the warehouse picker, the notification bell and the account menu, and
   every one of those is wired to a handler. Restyling in place cannot lose a
   handler, so the parity check is green by construction rather than by
   inspection. Markup only gets touched where CSS genuinely cannot reach, and
   then one element at a time.

   The result is that the current DOM keeps its Bootstrap classes and simply
   stops being told to look like Bootstrap. .bg-dark is still on the sidebar; it
   just no longer wins.
   ========================================================= */

/* ---------- Page ground ---------- */
html[data-ui="next"] body{
  font-family:var(--font-sans);
  font-size:var(--app-font);
  line-height:var(--app-lh);
  color:var(--ink);
  background:var(--surface);
}
html[data-ui="next"] h1,
html[data-ui="next"] h2,
html[data-ui="next"] h3,
html[data-ui="next"] h4,
html[data-ui="next"] h5,
html[data-ui="next"] h6{
  font-family:var(--font-sans);
  font-weight:700;
  letter-spacing:-0.018em;
  color:var(--ink);
}

/* ---------- The rail ----------
   Was #212529. The dark rail was tried in the concept and rejected: it is not
   "more brand", it is a different material, applied to the one surface a user
   looks past all day. Light, with the brand living in a wash rather than a fill. */
html[data-ui="next"] .sidebar,
html[data-ui="next"] .sidebar.bg-dark{
  width:var(--rail-width);
  background:
    radial-gradient(150% 56% at 0% -2%,    color-mix(in srgb, var(--signal) 16%, transparent), transparent 74%),
    radial-gradient(150% 60% at 100% 102%, color-mix(in srgb, var(--signal) 12%, transparent), transparent 76%),
    radial-gradient(120% 40% at 92% 36%,   color-mix(in srgb, var(--signal) 6%, transparent), transparent 72%),
    var(--surface-raised);
  border-right:1px solid color-mix(in srgb, var(--signal) 10%, transparent);
  color:var(--ink);
}
/* Bootstrap's .bg-dark is a utility, and utilities are !important by design, so
   it wins on background-color no matter how specific the rule above gets. This
   is the one place in phase 1 that needs !important, and it needs it for a
   structural reason rather than a specificity accident. The gradients are set by
   background-image and are unaffected. */
html[data-ui="next"] .sidebar.bg-dark{ background-color:var(--surface-raised) !important; }
html[data-ui="next"] .sidebar.pv-text-white,
html[data-ui="next"] .sidebar .pv-text-white{ color:var(--ink) !important; }
html[data-ui="next"] .sidebar .text-white-50{ color:var(--ink-muted) !important; }

/* The brand block. No underline: the rail already ends at a border, and a second
   line 56px in just makes the top look like a stack of trays. */
html[data-ui="next"] .sidebar .brand{
  height:var(--topbar-height);
  padding:0 16px;
  font-family:var(--font-sans);
  border-bottom:0;
}
html[data-ui="next"] .sidebar .brand .h5{
  font-size:16px; font-weight:700; letter-spacing:-0.018em; color:var(--ink);
}
html[data-ui="next"] .sidebar .brand small{ color:var(--ink-muted); }

/* ---------- Nav items ---------- */
html[data-ui="next"] .sidebar .nav-link{
  display:flex; align-items:center; gap:9px;
  padding:8px 9px; margin-bottom:1px;
  font-size:13.5px; font-weight:500;
  color:var(--ink);
  border-radius:var(--radius-sm);
  transition:background-color .12s ease, color .12s ease;
}
html[data-ui="next"] .sidebar .nav-link:hover{ background:color-mix(in srgb, var(--ink) 4%, transparent); color:var(--ink); }
html[data-ui="next"] .sidebar .nav-link.active{
  background:color-mix(in srgb, var(--signal) 11%, transparent);
  color:var(--signal);
  font-weight:700;
}
/* Font Awesome stays for now; the icon set is a phase 0 decision that has not
   been made, and swapping it here would spread the change across every phase. */
html[data-ui="next"] .sidebar .nav-link i{
  width:16px; font-size:14px; text-align:center;
  color:var(--ink-muted);
  transition:color .12s ease;
}
html[data-ui="next"] .sidebar .nav-link:hover i{ color:var(--ink-muted); }
html[data-ui="next"] .sidebar .nav-link.active i{ color:var(--signal); }

/* The group label, and the rule above it. 11px uppercase needs 4.5:1, which
   --ink-4 does not reach on this ground; --ink-3 does. */
html[data-ui="next"] .sidebar small.text-uppercase{
  display:block; padding:0 9px; margin:14px 0 5px;
  font-size:11px; font-weight:700; letter-spacing:.07em;
  color:var(--ink-muted) !important;
}
html[data-ui="next"] .sidebar hr{
  border-color:var(--line) !important;
  opacity:1 !important;
  margin:14px 9px 0;
}

/* Submenu: one hairline down the left, children hanging off it. */
html[data-ui="next"] .sidebar .submenu{
  margin:1px 0 2px 17px;
  padding-left:9px;
  border-left:1px solid color-mix(in srgb, var(--signal) 13%, transparent);
}
html[data-ui="next"] .sidebar .submenu .nav-link{
  font-size:13px; padding:6px 9px;
}

/* --ink-4 measures 2.97:1 on this rail, and 11px text needs 4.5:1. --ink-3 is
   5.67:1. The copyright line is the least important text on screen, which is
   exactly why it gets set once and correctly rather than left to taste. */
html[data-ui="next"] .sidebar footer{
  padding:12px; font-size:11px; color:var(--ink-muted);
}
html[data-ui="next"] .sidebar footer small{ color:var(--ink-muted) !important; }

/* ---------- Topbar ----------
   Scoped with :not(.auth-head) throughout. These rules were written against the
   layout's topbar, which is a bare <header> in the current markup, so they were
   matched on the element. The sign-in screen's header is also a <header>, and it
   was inheriting the topbar's 56px height, its bottom border and its icon-button
   reset - a 10px shorter header with a rule under it that the concept does not
   draw. The exclusion is the smallest change that keeps both correct; the
   alternative, giving the layout's topbar a class, is a markup change to every
   page's chrome for a cosmetic fix here. */
html[data-ui="next"] header:not(.auth-head){
  height:var(--topbar-height);
  background:var(--surface-raised);
  border-bottom:1px solid var(--line);
}
/* The scope pickers are native selects in the current markup. Restyled rather
   than replaced: turning them into custom popovers is a markup change, and the
   impersonation picker in particular carries an @onchange that reloads the whole
   tenant. That belongs in its own commit, not in a reskin. */
html[data-ui="next"] header:not(.auth-head) select.form-select,
html[data-ui="next"] header:not(.auth-head) select{
  height:32px; padding:0 26px 0 10px;
  font-family:var(--font-sans); font-size:13px; font-weight:600;
  color:var(--ink);
  background-color:var(--surface-sunken);
  border:1px solid var(--line);
  border-radius:var(--radius-sm);
  box-shadow:none;
}
html[data-ui="next"] header:not(.auth-head) select:focus{
  border-color:var(--signal);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--signal) 10%, transparent);
  outline:none;
}
html[data-ui="next"] header:not(.auth-head) .form-label,
html[data-ui="next"] header:not(.auth-head) label,
html[data-ui="next"] header:not(.auth-head) .pv-muted{
  font-size:11.5px; color:var(--ink-muted) !important;
}

html[data-ui="next"] #userMenu{
  height:38px; padding:0 8px;
  border:1px solid transparent; border-radius:var(--radius-sm);
  background:transparent; color:var(--ink);
  transition:background-color .12s ease, border-color .12s ease;
}
html[data-ui="next"] #userMenu:hover{ background:color-mix(in srgb, var(--ink) 4%, transparent); border-color:var(--line); }

/* ---------- Content ---------- */
html[data-ui="next"] main{ padding:20px 22px 40px; }
html[data-ui="next"] main > div > h2:first-child,
html[data-ui="next"] main h2{ font-size:22px; margin-bottom:2px; }

/* ---------- Small screens ----------
   The current shell is only partly responsive via Bootstrap utilities. Below
   1024px the rail leaves the layout entirely and the content takes the width,
   rather than being squeezed into whatever 240px leaves behind. */
@media (max-width:1023px){
  html[data-ui="next"] .sidebar{
    position:fixed; top:0; bottom:0; left:0; z-index:90;
    translate:-100% 0;
    transition:translate .24s var(--ease);
  }
  html[data-ui="next"] body.rail-open .sidebar{ translate:0 0; box-shadow:var(--shadow-pop); }
  html[data-ui="next"] main{ padding:16px 14px 32px; }
}

/* =========================================================
   PHASE 2: the primitives

   Ordered by how often each appears in the Razor, counted rather than guessed,
   because the migration is worth most where the app actually leans:

     card 356 - card-body 226 - badge 173 - alert 172 - form-control 154
     form-label 134 - table 116 - btn-sm 116 - form-check 113 - spinner 111
     form-select 73 - card-header 73 - btn-outline-primary 53 - btn-primary 47

   Still CSS only, still no Razor touched. Everything below restyles classes the
   app already has, so the parity check cannot move.

   The rule the concept is built on, and this keeps: COLOUR IS LOAD-BEARING.
   Four status hues carry meaning, so panels stay white and chrome stays quiet.
   Colour spent on decoration is colour a reader can no longer trust.
   ========================================================= */

/* ---------- Panels (Bootstrap .card) ----------
   Flat, hairline, one soft shadow. Not the layered card-on-card the default
   gives you, which at this density reads as noise rather than depth. */
html[data-ui="next"] .card{
  background:var(--surface-raised);
  border:1px solid var(--line);
  border-radius:var(--radius-md);
  box-shadow:var(--shadow-1);
}
html[data-ui="next"] .card-header{
  background:transparent;
  border-bottom:1px solid var(--line);
  padding:13px var(--pad) 12px;
  font-family:var(--font-sans); font-weight:700; font-size:14px;
  color:var(--ink); letter-spacing:-0.012em;
}
html[data-ui="next"] .card-header.pv-bg-light{ background:transparent !important; }
html[data-ui="next"] .card-body{ padding:var(--pad); }
html[data-ui="next"] .card-title{
  font-family:var(--font-sans); font-size:14px; font-weight:700;
  color:var(--ink); margin-bottom:10px; letter-spacing:-0.012em;
}
html[data-ui="next"] .card.pv-bg-light{ background:var(--surface-sunken) !important; }
/* Every card in this app is written as `card border-0 shadow-sm`, so the hairline
   never appeared and the shadow stayed Bootstrap's. Both are utilities, so both
   are !important by design and have to be met in kind. Without this the panels
   float on a tinted ground with no edge, which is the one thing the concept's
   flat-panel treatment depends on. */
html[data-ui="next"] .card.border-0{ border:1px solid var(--line) !important; }
html[data-ui="next"] .card.shadow-sm,
html[data-ui="next"] .card.shadow{ box-shadow:var(--shadow-1) !important; }

/* ---------- Buttons ----------
   One height per size regardless of whether there is an icon. Bootstrap sizes
   buttons from their content, so an icon makes one 1.5px taller than its
   neighbour and any mixed row sits crooked. */
html[data-ui="next"] .btn{
  display:inline-flex; align-items:center; justify-content:center; gap:6px;
  min-height:32px; padding:0 13px;
  font-family:var(--font-sans); font-weight:700; font-size:13px; line-height:1;
  border-radius:var(--radius-sm); border:1px solid transparent;
  box-shadow:none;
  transition:background-color .15s ease, border-color .15s ease, color .15s ease;
}
html[data-ui="next"] .btn-sm{ min-height:28px; padding:0 10px; font-size:12.5px; }
html[data-ui="next"] .btn-lg{ min-height:38px; padding:0 16px; font-size:14px; }

html[data-ui="next"] .btn-primary,
html[data-ui="next"] .btn-save-config{
  /* PrinterVision: the primary action is solid ink. */
  background:var(--inverse); border-color:var(--inverse); color:var(--on-inverse);
}
html[data-ui="next"] .btn-primary:hover,
html[data-ui="next"] .btn-save-config:hover{ background:var(--inverse-hover); border-color:var(--inverse-hover); color:var(--on-inverse); }

html[data-ui="next"] .btn-secondary,
html[data-ui="next"] .btn-outline-secondary,
html[data-ui="next"] .btn-outline-primary{
  background:var(--surface-raised); color:var(--ink);
  border-color:var(--line); box-shadow:var(--shadow-1);
}
html[data-ui="next"] .btn-outline-primary:hover,
html[data-ui="next"] .btn-outline-secondary:hover,
html[data-ui="next"] .btn-secondary:hover{
  background:var(--surface-sunken); color:var(--ink); border-color:var(--line);
}
/* Destructive keeps the red it earns. */
html[data-ui="next"] .btn-danger{ background:var(--crit); border-color:var(--crit); color:var(--on-inverse); }
html[data-ui="next"] .btn-danger:hover{ background:color-mix(in srgb, var(--crit) 86%, var(--ink)); border-color:color-mix(in srgb, var(--crit) 86%, var(--ink)); }
html[data-ui="next"] .btn-success{ background:var(--good); border-color:var(--good); color:var(--on-inverse); }
html[data-ui="next"] .btn-link{
  min-height:auto; padding:0; border:0; background:none;
  color:var(--signal); font-weight:600; font-size:13px;
}
html[data-ui="next"] .btn-link:hover{ text-decoration:underline; text-underline-offset:2px; }
/* The rail toggle and the bell are written as `btn btn-link`, so stripping the
   padding and height off link buttons collapsed the hamburger from 40x38 to
   11x13 and left it almost unclickable. Icon-only buttons in the topbar are
   controls, not links, whatever their class says. */
html[data-ui="next"] header:not(.auth-head) .btn-link,
html[data-ui="next"] header:not(.auth-head) .btn{
  min-height:32px; min-width:32px; padding:0 8px;
  color:var(--ink-muted);
}
html[data-ui="next"] header:not(.auth-head) .btn-link:hover,
html[data-ui="next"] header:not(.auth-head) .btn:hover{
  color:var(--ink); background:color-mix(in srgb, var(--ink) 4%, transparent); text-decoration:none;
}
html[data-ui="next"] .btn:focus-visible{ outline:2px solid var(--signal); outline-offset:2px; box-shadow:none; }
html[data-ui="next"] .btn:disabled,
html[data-ui="next"] .btn.disabled{ opacity:.5; }

/* ---------- Tables ----------
   The header is a label row, not a band: uppercase, small, quiet, no fill. The
   rows carry the weight, because the rows are the data. */
html[data-ui="next"] .table{
  --bs-table-bg:transparent;
  font-size:13.5px; color:var(--ink); margin-bottom:0;
}
html[data-ui="next"] .table > thead{ background:transparent; }
html[data-ui="next"] .table > thead th,
html[data-ui="next"] .table > thead > tr > th{
  background:transparent;
  padding:9px 12px;
  font-family:var(--font-sans);
  font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
  color:var(--ink-muted);
  border-bottom:1px solid var(--line);
  white-space:nowrap;
}
html[data-ui="next"] .table > tbody > tr > td{
  padding:10px 12px;
  border-top:0; border-bottom:1px solid var(--line);
  vertical-align:middle;
}
html[data-ui="next"] .table > tbody > tr:last-child > td{ border-bottom:0; }
html[data-ui="next"] .table-hover > tbody > tr:hover > td{ background:color-mix(in srgb, var(--ink) 4%, transparent); }
html[data-ui="next"] .table-light,
html[data-ui="next"] .table > thead.table-light{ --bs-table-bg:transparent; background:transparent; }

/* ---------- Badges ----------
   The status vocabulary. These are the hues that carry meaning, which is the
   whole reason everything around them is grey. Tinted rather than solid: a wall
   of saturated pills is harder to scan than the words inside them. */
html[data-ui="next"] .badge{
  display:inline-flex; align-items:center; gap:5px;
  padding:3px 8px;
  font-family:var(--font-sans); font-size:11.5px; font-weight:600;
  letter-spacing:0; border-radius:var(--radius-xs);
}
html[data-ui="next"] .badge.bg-success{ background:color-mix(in srgb, var(--good) 11%, transparent) !important; color:var(--good) !important; }
html[data-ui="next"] .badge.bg-danger { background:color-mix(in srgb, var(--crit) 10%, transparent)  !important; color:var(--crit) !important; }
html[data-ui="next"] .badge.bg-warning{ background:color-mix(in srgb, var(--warn) 12%, transparent) !important; color:var(--warn) !important; }
html[data-ui="next"] .badge.bg-info   { background:color-mix(in srgb, var(--signal) 9%, transparent)  !important; color:var(--signal) !important; }
html[data-ui="next"] .badge.bg-secondary,
html[data-ui="next"] .badge.pv-bg-light  { background:var(--surface-sunken) !important; color:var(--ink-muted) !important; }
html[data-ui="next"] .badge.bg-primary{ background:color-mix(in srgb, var(--signal) 9%, transparent) !important; color:var(--signal) !important; }

/* ---------- Alerts ---------- */
html[data-ui="next"] .alert{
  padding:12px 14px;
  font-size:13px; line-height:1.55;
  border-radius:var(--radius-md); border-width:1px;
}
html[data-ui="next"] .alert-info   { background:color-mix(in srgb, var(--signal) 5%, transparent);  border-color:color-mix(in srgb, var(--signal) 14%, transparent);  color:var(--ink); }
html[data-ui="next"] .alert-success{ background:color-mix(in srgb, var(--good) 6%, transparent); border-color:color-mix(in srgb, var(--good) 16%, transparent); color:var(--ink); }
html[data-ui="next"] .alert-warning{ background:color-mix(in srgb, var(--warn) 7%, transparent); border-color:color-mix(in srgb, var(--warn) 18%, transparent); color:var(--ink); }
html[data-ui="next"] .alert-danger { background:color-mix(in srgb, var(--crit) 5%, transparent);  border-color:color-mix(in srgb, var(--crit) 16%, transparent);  color:var(--ink); }

/* ---------- Form controls ---------- */
html[data-ui="next"] .form-control,
html[data-ui="next"] .form-select{
  min-height:34px; padding:6px 10px;
  font-family:var(--font-sans); font-size:13.5px; color:var(--ink);
  background-color:var(--surface-sunken);
  border:1px solid var(--line); border-radius:var(--radius-sm);
  box-shadow:none;
  transition:border-color .15s ease, box-shadow .15s ease;
}
html[data-ui="next"] .form-control::placeholder{ color:var(--ink-muted); }
html[data-ui="next"] .form-control:focus,
html[data-ui="next"] .form-select:focus{
  background-color:var(--surface-raised);
  border-color:var(--signal);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--signal) 10%, transparent);
}
html[data-ui="next"] .form-control:disabled,
html[data-ui="next"] .form-select:disabled{ background-color:var(--surface-sunken); color:var(--ink-muted); }
html[data-ui="next"] .form-label{
  font-size:12.5px; font-weight:600; color:var(--ink); margin-bottom:5px;
}
html[data-ui="next"] .form-text{ font-size:12px; color:var(--ink-muted); }
html[data-ui="next"] .form-check-input{ border-color:var(--line); }
html[data-ui="next"] .form-check-input:checked{ background-color:var(--signal); border-color:var(--signal); }
html[data-ui="next"] .form-check-input:focus{ border-color:var(--signal); box-shadow:0 0 0 3px color-mix(in srgb, var(--signal) 10%, transparent); }
html[data-ui="next"] .form-check-label{ font-size:13.5px; color:var(--ink); }
html[data-ui="next"] .input-group-text{
  background:var(--surface-sunken); border-color:var(--line);
  color:var(--ink-muted); font-size:13px;
}

/* ---------- Modals ----------
   The app builds most of these by hand rather than with Bootstrap's JS, so the
   classes are its own. Same treatment either way. */
html[data-ui="next"] .modal-content,
html[data-ui="next"] .modal-card{
  background:var(--surface-raised);
  border:1px solid var(--line);
  border-radius:var(--radius-md);
  box-shadow:0 2px 4px color-mix(in srgb, var(--ink) 6%, transparent), 0 24px 60px -18px color-mix(in srgb, var(--ink) 42%, transparent);
}
html[data-ui="next"] .modal-header{
  padding:13px var(--pad);
  border-bottom:1px solid var(--line);
}
html[data-ui="next"] .modal-title,
html[data-ui="next"] .modal-header h5{
  font-family:var(--font-sans); font-size:15px; font-weight:700;
  color:var(--ink); letter-spacing:-0.012em;
}
html[data-ui="next"] .modal-body{ padding:var(--pad); }
html[data-ui="next"] .modal-footer{
  padding:12px var(--pad);
  border-top:1px solid var(--line);
  gap:8px;
}
html[data-ui="next"] .modal-backdrop{ background:color-mix(in srgb, var(--night) 42%, transparent); }

/* ---------- Tabs, list groups, progress and .pv-muted ----------
   Bridged further down, in the Bootstrap component section. They were declared
   here first and again there; same weight, so only the later pair ever applied
   and this copy was dead. One of the pairs disagreed with itself. */

/* ---------- Dropdowns ---------- */
html[data-ui="next"] .dropdown-menu{
  padding:5px; border:1px solid var(--line); border-radius:var(--radius-md);
  box-shadow:var(--shadow-pop); font-size:13.5px;
}
html[data-ui="next"] .dropdown-item{
  padding:7px 9px; border-radius:var(--radius-sm); color:var(--ink);
}
html[data-ui="next"] .dropdown-item:hover{ background:color-mix(in srgb, var(--ink) 4%, transparent); color:var(--ink); }
html[data-ui="next"] .dropdown-item.pv-text-crit:hover{ background:color-mix(in srgb, var(--crit) 7%, transparent); }
html[data-ui="next"] .dropdown-divider{ border-color:var(--line); margin:4px 0; }

/* ---------- Odds and ends ---------- */
html[data-ui="next"] .spinner-border{ color:var(--signal); border-width:2px; }
html[data-ui="next"] code{
  font-size:12px; color:var(--ink);
  background:var(--surface-sunken); padding:1px 5px; border-radius:4px;
}
html[data-ui="next"] hr{ border-color:var(--line); opacity:1; }


/* =========================================================
   PHASE 3 fixes: the concept rail in the app's layout

   The concept pairs a FIXED rail with a shell that is padded by --rail-width. The
   app's layout put the sidebar in flow as a flex item instead, so porting the
   concept's .rail{position:fixed} took the rail out of flow and left <main>
   starting at x=0, running underneath it. Visible content looked fine only
   because the page's own padding happened to clear it.
   ========================================================= */
html[data-ui="next"] .shell{ padding-left:var(--rail-width); }

/* Collapsing is driven by a class on the rail here, not a class on <body> as in
   the concept, so the width variable never changed and the rail stayed 236px
   while its labels hid. :has() lets the variable follow the rail's own state, so
   the rail and the shell padding stay in step from one declaration. */
html[data-ui="next"]:has(.rail.collapsed){ --rail-width:60px; }

@media (max-width:1023px){
  /* The rail leaves the layout entirely, so the shell takes the full width. */
  html[data-ui="next"] .shell{ padding-left:0; }
}


/* =========================================================
   PHASE 4: page markup on the concept's primitives

   The concept draws its sparkline as a bare <svg class="spark">. The app draws
   the same trend with ApexCharts, which insists on its own wrapper <div>, so
   the concept's rule cannot be reused as-is. This gives that wrapper the slot
   .spark occupies: pushed to the bottom of the stat tile, 30px tall.
   ========================================================= */
html[data-ui="next"] .stat .stat-spark{
  margin-top:auto; padding-top:12px; display:block; width:100%;
}
html[data-ui="next"] .stat .stat-spark .apexcharts-canvas{ margin:0 auto; }

/* ApexCharts writes its own font stack into every label. The charts sit in the
   concept's panels now, so they should read in the concept's body face at the
   concept's label size rather than in Helvetica at Apex's default. */
html[data-ui="next"] .panel.chart text,
html[data-ui="next"] .panel.chart .apexcharts-legend-text,
html[data-ui="next"] .panel.chart .apexcharts-tooltip{
  font-family:var(--font-sans) !important;
}
html[data-ui="next"] .panel.chart .apexcharts-text tspan,
html[data-ui="next"] .panel.chart .apexcharts-legend-text{
  font-size:11.5px !important; fill:var(--ink-muted); color:var(--ink-muted) !important;
}
html[data-ui="next"] .panel.chart .apexcharts-gridline{ stroke:var(--line); }

/* The spinner is Bootstrap's; nothing else on a converted page is. Size it to
   the concept's 13px icon so it does not tower over the text beside it. */
html[data-ui="next"] .spinner-border-sm{ width:13px; height:13px; border-width:1.5px; }

/* The concept closes its popovers from a document-level listener. Blazor has no
   equivalent without JS interop, so the layout renders an inert sheet under the
   open popover instead. It sits below .pop (z-index 80) and above everything
   else, so the first click anywhere lands on it and closes the menu rather than
   activating whatever was underneath - which is what the document listener does
   too, since it calls preventDefault on the way out. */
html[data-ui="next"] .pop-backdrop{
  position:fixed; inset:0; z-index:79; background:transparent;
}

/* =========================================================
   PHASE 5: page components on the concept's primitives

   docs/ui-concept/tools/to_concept.py swaps Bootstrap's component classes for
   the concept's across the pages. Two things it produces need a home here.
   ========================================================= */

/* The concept has no card footer, because none of its drafts needed one; seven
   places in the app do. Built from .tbl-foot, which is the same idea - a quiet
   strip under a panel's content, separated by a hairline rather than a rule. */
html[data-ui="next"] .panel-foot{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:10px var(--pad); border-top:1px solid var(--line);
  font-size:12px; color:var(--ink-muted); background:transparent;
}

/* Bootstrap utilities the pages still use by the thousand. These are
   one-property helpers, not components: rewriting 1,400 of them would change
   nothing a reader could see. Pointing them at the concept's tokens does.
   Bootstrap declares them !important, so these have to be met in kind. */
html[data-ui="next"] .pv-muted{ color:var(--ink-muted) !important; }
html[data-ui="next"] .pv-muted{ color:var(--ink-muted) !important; }
html[data-ui="next"] .pv-text-ink{ color:var(--ink) !important; }
html[data-ui="next"] .pv-text-signal{ color:var(--signal) !important; }
html[data-ui="next"] .pv-text-good{ color:var(--good) !important; }
html[data-ui="next"] .pv-text-warn{ color:var(--warn) !important; }
html[data-ui="next"] .pv-text-crit{ color:var(--crit) !important; }
html[data-ui="next"] .pv-text-signal{ color:var(--signal) !important; }
html[data-ui="next"] .pv-small, html[data-ui="next"] small{ font-size:12px; }
html[data-ui="next"] .pv-fw-semibold, html[data-ui="next"] .pv-fw-bold{
  font-weight:500; color:var(--ink);
}
html[data-ui="next"] .pv-bg-light{ background-color:var(--surface-sunken) !important; }
html[data-ui="next"] .pv-bg-raised{ background-color:var(--surface-raised) !important; }
html[data-ui="next"] .pv-border{ border-color:var(--line) !important; }
html[data-ui="next"] .pv-rounded{ border-radius:var(--radius-sm) !important; }
html[data-ui="next"] .pv-no-underline{ text-decoration:none; }

/* Alerts, list groups, accordions and input groups have no single concept
   counterpart, so to_concept.py leaves them alone and they are bridged here
   instead. Each still needs a per-use decision eventually; until then they at
   least sit on the concept's surface, line and radius rather than Bootstrap's. */
html[data-ui="next"] .alert{
  border-radius:var(--radius-md); border:1px solid var(--line);
  padding:11px 13px; font-size:13px; color:var(--ink); background:var(--surface-sunken);
}
html[data-ui="next"] .alert-info{ background:var(--signal-soft); border-color:color-mix(in srgb, var(--signal) 16%, transparent); }
html[data-ui="next"] .alert-success{ background:color-mix(in srgb, var(--good) 6%, transparent); border-color:color-mix(in srgb, var(--good) 20%, transparent); }
html[data-ui="next"] .alert-warning{ background:color-mix(in srgb, var(--warn) 7%, transparent); border-color:color-mix(in srgb, var(--warn) 22%, transparent); }
html[data-ui="next"] .alert-danger{ background:color-mix(in srgb, var(--crit) 6%, transparent); border-color:color-mix(in srgb, var(--crit) 22%, transparent); }
html[data-ui="next"] .list-group-item{
  border-color:var(--line); font-size:13px; color:var(--ink);
  background:transparent; padding:9px var(--pad);
}
/* .inp is width:100%, which is right on its own and wrong inside an input group:
   Bootstrap's .form-control carried flex:1 1 auto/width:1% there, so swapping the
   class made every grouped input claim the full row and wrap its addon onto the
   line above. Thirty-two input groups, all of them looking broken. */
html[data-ui="next"] .input-group{ display:flex; flex-wrap:nowrap; align-items:stretch; }
html[data-ui="next"] .input-group > .inp{
  flex:1 1 auto; width:1%; min-width:0;
  border-radius:0 var(--radius-sm) var(--radius-sm) 0; border-left:0;
}
html[data-ui="next"] .input-group > .inp:first-child{ border-radius:var(--radius-sm); border-left:1px solid var(--line); }
html[data-ui="next"] .input-group-text{
  display:flex; align-items:center; flex-shrink:0;
  background:var(--surface-raised); border:1px solid var(--line); color:var(--ink-muted);
  font-size:12.5px; padding:0 10px;
  border-radius:var(--radius-sm) 0 0 var(--radius-sm);
}
html[data-ui="next"] .input-group:focus-within .inp,
html[data-ui="next"] .input-group:focus-within .input-group-text{
  border-color:var(--signal);
}
html[data-ui="next"] .input-group:focus-within{
  border-radius:var(--radius-sm); box-shadow:0 0 0 3px color-mix(in srgb, var(--signal) 10%, transparent);
}
html[data-ui="next"] .input-group > .inp:focus{ box-shadow:none; }
html[data-ui="next"] .progress{ height:6px; border-radius:3px; background:var(--line); }
html[data-ui="next"] .progress-bar{ background:var(--signal); }

/* =========================================================
   Tabs

   Six pages carry a tab bar, in two shapes: `nav nav-tabs card-header-tabs`
   inside a panel head, and `nav nav-pills` sitting directly on the page. The
   concept has one: .tabs, a row of buttons over a hairline with the active one
   carrying a blue underline.

   Bridged rather than rewritten, for the same reason row/col is: <ul><li><button>
   and <div><button> produce the same row of buttons, and only the buttons are
   visible. The <li> does have to stop being the flex item, which is what
   display:contents does.

   card-header-tabs also has to be undone rather than restyled. It sets negative
   margins sized to .card-header's padding so the tabs bleed to the header's
   edges; inside a .panel-head those numbers are wrong, and on Analytics they
   pushed the row 88px past the viewport. That was a real overflow, found by
   sweeping the running app.
   ========================================================= */
html[data-ui="next"] .nav-tabs,
html[data-ui="next"] .nav-pills{
  display:flex; gap:2px; margin:0 0 18px; padding:0; list-style:none;
  border-bottom:1px solid var(--line); flex-wrap:wrap;
}
/* Inside a panel head the bar IS the head, so it carries no bottom rule or
   margin of its own: .panel-head already draws one. */
html[data-ui="next"] .panel-head .nav-tabs,
html[data-ui="next"] .panel-head .nav-pills{
  border-bottom:0; margin-bottom:0;
}
html[data-ui="next"] .card-header-tabs{ margin:0 !important; }
/* Only the tab bars' items. Unscoped it also hit the rail's .nav-item, which is the
   positioning box of the collapsed rail's flyout: with no box of its own the flyout
   was placed against the whole nav and opened at the top of the rail. */
html[data-ui="next"] :is(.nav-tabs, .nav-pills) > .nav-item{ display:contents; }
html[data-ui="next"] .nav-tabs .nav-link,
html[data-ui="next"] .nav-pills .nav-link{
  font-family:var(--font-sans); font-weight:500; font-size:13px;
  color:var(--ink-muted); background:none; border:0;
  border-bottom:2px solid transparent; border-radius:0;
  padding:9px 13px; margin:0; cursor:pointer;
  transition:color .12s ease, border-color .12s ease;
}
html[data-ui="next"] .nav-tabs .nav-link:hover,
html[data-ui="next"] .nav-pills .nav-link:hover{ color:var(--ink); background:none; }
html[data-ui="next"] .nav-tabs .nav-link.active,
html[data-ui="next"] .nav-pills .nav-link.active,
html[data-ui="next"] .nav-pills .show > .nav-link{
  color:var(--signal); border-bottom-color:var(--signal); font-weight:700;
  background:none;
}
html[data-ui="next"] .nav-tabs .nav-link:focus-visible,
html[data-ui="next"] .nav-pills .nav-link:focus-visible{
  outline:2px solid var(--signal); outline-offset:-2px;
}

/* =========================================================
   Topbar: long user names must not widen the page

   Found by sweeping the running app, not by looking at it. DisplayName is
   Identity.Name, which for an Entra account is the full email - 223px of
   unbreakable text. The concept's .topbar sets `> * { flex-shrink:0 }` because
   its sample name is "Christoph V.", so nothing there ever needed to give way.

   With a real name the sum of the topbar's children became its min-content
   width, .main.shell is a flex item and so defaults to min-width:auto, and the
   shell grew to fit: 88px past the viewport, on every page, with the topbar
   itself reporting no overflow because it had simply won the argument.

   Two guards, because either alone leaves the other latent:
     - min-width:0 on the shell, so no child can ever widen the page again;
     - the user chip may shrink and ellipsis, which is the same treatment the
       concept already gives .impersonating for exactly this reason.
   ========================================================= */
html[data-ui="next"] .shell,
html[data-ui="next"] .main{ min-width:0; }
html[data-ui="next"] .topbar{ min-width:0; }

html[data-ui="next"] .userchip{ flex-shrink:1; min-width:0; }
html[data-ui="next"] .userchip .who{ min-width:0; max-width:210px; overflow:hidden; }
html[data-ui="next"] .userchip .who b,
html[data-ui="next"] .userchip .who span{
  display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* The scope pickers hold warehouse and company names, which are also arbitrary
   length, so they may shrink too. Only the shrinking: the concept's own
   .scope .s-value already sets overflow, text-overflow, white-space and
   min-width:0, and repeating it here achieved nothing except a min-width the
   concept then overrode. Checked by reading the computed value rather than
   assuming the rule I wrote was the one in force. */
html[data-ui="next"] .scope{ min-width:0; flex-shrink:1; }
/* In the popover the name has a whole line, so it wraps rather than truncates. */
html[data-ui="next"] .pop-user .who b{ overflow-wrap:anywhere; }

/* =========================================================
   Mobile: the two things the phone sweep found

   1. Grids whose columns are an inline style.

      The concept's responsive rules target .grid-4 and .grid-2-1 by class, which
      an inline grid-template-columns beats every time. The dashboard's stat row
      sets its columns inline because the tile count is licence-dependent, so at
      375px it stayed three-across at 95px a column instead of stacking.

      Matched on the inline style and overridden, which is the concept's own
      idiom - it does exactly this for dialog grids at max-width:700px.

   2. Definition lists.

      .dl is `grid-template-columns:auto 1fr`, and a grid item defaults to
      min-width:auto, so an unbreakable string - a full email in the dashboard's
      debug block - pushed the row 40px past a phone viewport rather than
      wrapping.
   ========================================================= */
@media (max-width:1180px){
  html[data-ui="next"] .page .grid[style*="repeat("]{
    grid-template-columns:repeat(2,minmax(0,1fr)) !important;
  }
}
@media (max-width:640px){
  html[data-ui="next"] .page .grid[style*="repeat("],
  html[data-ui="next"] .page .grid[style*="minmax(0,2.1fr)"]{
    grid-template-columns:minmax(0,1fr) !important;
  }
}

html[data-ui="next"] .dl{ min-width:0; }
html[data-ui="next"] .dl dd{
  margin:0; min-width:0; overflow-wrap:anywhere; color:var(--ink);
}

/* =========================================================
   The mobile drawer

   The concept opens its drawer with `body.rail-open`, set from JS. Blazor sets
   classes on components, not on <body>, so these mirror the concept's own rules
   at the same breakpoint, keyed to the class the layout actually writes.

   Found by tapping the hamburger on a phone: it toggled .collapsed, which the
   concept explicitly neutralises below 1024px, so the rail stayed off-canvas and
   the navigation was unreachable. Nothing about the page looked wrong, which is
   why only using it caught this.
   ========================================================= */
@media (max-width:1023px){
  html[data-ui="next"] .rail{
    width:236px; translate:-100% 0;
    transition:translate .24s var(--ease);
    z-index:90; box-shadow:none;
  }
  /* :has() rather than a body class, for the same reason --rail-width uses it: the
     state lives on the rail, and this is what lets the rest of the page read it. */
  html[data-ui="next"] .rail.open{ translate:0 0; box-shadow:var(--shadow-pop); }
  html[data-ui="next"]:has(.rail.open) .rail-scrim{ opacity:1; visibility:visible; }

  /* The rail is either over the page or off it, so the 60px icon variant and its
     hover flyouts are meaningless here. This repeats the concept's own override,
     scoped to the app's class. */
  html[data-ui="next"] .rail.collapsed{ width:236px; }

  /* --rail-width is 0 below this width, so the shell is full-bleed. The desktop
     :has(.rail.collapsed) rule sets it on <html>; body is nearer, so it wins. */
  html[data-ui="next"] body,
  html[data-ui="next"] .shell{ --rail-width:0px; }
  html[data-ui="next"] .shell{ padding-left:0; }
}

html[data-ui="next"] .rail-scrim{
  position:fixed; inset:0; z-index:85;
  background:color-mix(in srgb, var(--night) 42%, transparent);
  opacity:0; visibility:hidden;
  transition:opacity .2s ease, visibility .2s;
}
/* Above the breakpoint the rail is a column, so the scrim must never be able to
   catch a click - not merely be transparent. */
@media (min-width:1024px){
  html[data-ui="next"] .rail-scrim{ display:none; }
}

/* A flex child defaults to min-width:auto, so .pv-grow - which exists purely
   to take the remaining space - could not give any of it back. On the company
   list at 375px the row was 313px wide and its children summed to 437, so the
   page scrolled sideways by 157. Setting min-width:0 is the standing companion
   to that utility and is what makes text-truncate inside one work at all. */
html[data-ui="next"] .pv-grow{ min-width:0; }

/* And where the buttons alone are wider than the row, let them wrap rather than
   push the page. */
@media (max-width:640px){
  html[data-ui="next"] .btn-group{ flex-wrap:wrap; }
}

/* A row of chips beside a name - the company list's "3 users / 2 sites /
   3 warehouses" - is a flex row that never wrapped, so on a phone the last chip
   sat 9px past the edge. Chips are independent labels; wrapping them is what the
   concept's own .stat-foot does. Scoped with :has() to rows that actually hold
   one, rather than making every d-flex in the app wrap. */
html[data-ui="next"] .pv-flex:has(> .chip){ flex-wrap:wrap; row-gap:4px; }

/* A critical chip.

   The concept's chip set is chip-on, chip-off and chip-warn - no red - because
   nothing in its drafts is a chip that means "broken". The app has several:
   an access point that is Offline rather than degraded, a device that is Offline
   rather than idle.

   Mapping both onto chip-warn cost a real distinction. NetworkAlerts had
   `Severity == Offline ? "bg-danger" : "bg-warning"` and both sides came out
   chip-warn, so offline and degraded became the same colour.

   Built the way .chip-warn is, on --red-dark, which the concept already owns and
   already reserves for exactly this. */
/* Two classes, not one: concept.css loads AFTER this file, and its
   plain .chip sets colour at the same specificity, so a single .chip-crit lost
   the cascade and rendered grey. .chip.chip-crit outranks it. Caught by reading
   the computed colour rather than trusting the rule was written. */
html[data-ui="next"] .chip.chip-crit{
  background:color-mix(in srgb, var(--crit) 6%, transparent);
  border-color:color-mix(in srgb, var(--crit) 20%, transparent);
  color:var(--crit);
}

/* =========================================================
   The rail's copyright line

   It read as heavy because it was: nothing ever set its colour, so it inherited
   --ink-2 from the body and sat at the same weight as the nav labels above it,
   at 12px from the small/.pv-small bridge, over a border, wrapped onto two lines.
   63px of footer for a line nobody needs to read.

   --ink-3 rather than --ink-4, for the reason the concept gives for .nav-group:
   #8b97a6 on white is 2.97:1, and this is 11px, which needs 4.5:1. --ink-3 is
   5.66:1 and is still a drop from 11.6:1, which is most of what made it shout.

   Element-qualified because the ported concept sheet loads after this one and
   its .rail .rail-foot would otherwise win on source order - the same trap
   .chip-crit fell into.
   ========================================================= */
html[data-ui="next"] .rail footer.rail-foot{
  border-top:0;
  padding:6px 10px 10px;
  font-size:11px;
  line-height:1.45;
  color:var(--ink-muted);
}
html[data-ui="next"] .rail footer.rail-foot small{
  font-size:inherit;
  color:inherit;
}

/* A "buttons on the left, status on the right" row cannot hold both on a phone.
   On Site Survey the buttons took the width and the status text was squeezed to
   56px with its content hanging 48px past the viewport. justify-content-between
   only distributes space; it does not decide what happens when there is none.

   Below 640px such a row stacks instead. .page-head is unaffected: it is not a
   .pv-flex, and the concept already turns it into a column at 1023px. */
@media (max-width:640px){
  html[data-ui="next"] .page .pv-flex.pv-justify-between{
    flex-wrap:wrap; row-gap:8px;
  }
  html[data-ui="next"] .page .pv-flex.pv-justify-between > *{ min-width:0; }
}

/* ==========================================================================
   Pre-auth screen: the four gaps between the concept's login.html and three
   real routes.
   ========================================================================== */

/* The concept defines .notice twice. concept.css has the red error block used
   on the sign-in card; concept-pages.css redefines it as a blue informational
   row for metrics that take no filters. Both are ported with the same
   html[data-ui="next"] prefix, and the pages one is ported second, so at equal
   specificity it wins - the sign-in error came out blue and laid out as a flex
   row with no icon slot. Qualifying by .auth-card raises this to (0,3,0) and
   restores concept.css's version where it belongs, without touching the pages
   one where it also belongs. */
html[data-ui="next"] .auth-card .notice{
  display:block;
  border-radius:var(--radius-sm);padding:12px 14px;margin:0 0 18px;
  background:color-mix(in srgb, var(--crit) 5%, transparent);border:1px solid color-mix(in srgb, var(--crit) 20%, transparent);
  font-size:inherit;line-height:inherit;color:inherit;
}
html[data-ui="next"] .auth-card .notice p{font-size:12.5px;color:var(--ink);margin:0;}
html[data-ui="next"] .auth-card .notice .hint{margin-top:6px;color:var(--ink-muted);}

/* The reset-link confirmation is not a failure, so it does not get the failure
   colours. Same block, the blue of concept-pages.css. */
html[data-ui="next"] .auth-card .notice.notice-info{
  margin:16px 0 0;
  background:color-mix(in srgb, var(--signal) 5%, transparent);border-color:color-mix(in srgb, var(--signal) 14%, transparent);
}

/* The concept writes its "Back to sign in" line as an inline style, once, in a
   template literal. Three routes means three of them, so it is a class. Values
   are the concept's, unchanged. */
html[data-ui="next"] .auth-card .auth-alt{
  text-align:center;margin:18px 0 0;font-size:12.5px;color:var(--ink-muted);
}
html[data-ui="next"] .auth-card .auth-alt-tight{margin-top:6px;}

/* A short numbered list in the auth card: what to do when the account is fine
   but nothing is assigned to it. The card has no list styling of its own,
   because the concept's three cards are all forms. Concept type and ink. */
html[data-ui="next"] .auth-card .auth-steps{
  margin:0 0 18px; padding-left:18px;
  font-size:13px; line-height:1.65; color:var(--ink);
}
html[data-ui="next"] .auth-card .auth-steps li{ margin-bottom:2px; }

/* The wide scene. The terms card holds a scrolling document, and the concept's
   400px column is a form width, not a reading width. One centred column, and
   the pitch stands down: whoever is reading this has already signed in. */
html[data-ui="next"] .auth-body.is-wide{
  grid-template-columns:1fr; max-width:720px;
}
html[data-ui="next"] .auth-body.is-wide .auth-card{
  max-width:100%; justify-self:center;
}

/* The notice heading follows the notice. concept.css:813 paints every
   .notice-head var(--crit), which is right in the concept because it draws
   exactly one notice: the red sign-in error. The app has four tones, so a
   warning came out with a red heading on an amber ground.

   Doubled class to clear the ported rule, which is the same weight and loads
   after this file. */
html[data-ui="next"] .notice.notice-warning .notice-head{ color:var(--warn); }
html[data-ui="next"] .notice.notice-success .notice-head{ color:var(--good); }
html[data-ui="next"] .notice.notice-info .notice-head{ color:var(--signal); }

/* Validation text under a .field. The concept has no invalid state on the
   sign-in card because its forms do not submit; the app's forgot-password form
   is a real EditForm and does. Sized to the field label, coloured to the
   failure red already used by .notice. */
html[data-ui="next"] .auth-card .field-err{
  display:block;margin-top:5px;font-size:12px;color:var(--crit);
}

/* The concept's pre-auth header is 66px: 18px of padding either side of a 30px
   row, and what sets that row's height is the EN/NL switch. The app has no
   localization at all - no resx, no IStringLocalizer, no RequestLocalization -
   so that control is not shipped, which left the header 2px shorter than the
   concept draws it. Pinned rather than faked: the chrome matches, without a
   language switch that changes nothing. */
html[data-ui="next"] .auth-head{ min-height:66px; }

/* The concept never collapses the sign-in screen. .auth-body is grid-template-
   columns:1fr 1fr at every width, and the only responsive rule in the whole
   pre-auth design hides the mark below 900px. Below about 700px that leaves the
   card around 250px wide with its contents spilling past the right edge - the
   concept's own file does the same, so this is a gap in the design rather than
   something lost in porting.
   Filled at 900px, which is the breakpoint the concept already chose for the
   mark, so nothing new is invented: below it the two columns stack, the pitch
   keeps its text now that the mark behind it is gone, and the card stops being
   pinned to the right. Above it the concept is untouched.
   Written as .auth .auth-body to clear the ported rule, which loads after this
   file and would otherwise win at equal specificity. */
@media (max-width:900px){
  html[data-ui="next"] .auth .auth-body{
    grid-template-columns:1fr;
    align-items:start;
    gap:32px;
  }
  html[data-ui="next"] .auth .auth-card{
    justify-self:stretch;
    width:100%;
    max-width:420px;
    margin:0 auto;
  }
  html[data-ui="next"] .auth .auth-pitch{ max-width:420px; margin:0 auto; }
  html[data-ui="next"] .auth .auth-pitch .lede{ max-width:none; }
}

/* Narrow phones: the footer's two groups no longer fit on one line, and the
   header's padding is more than a 375px screen can spare. */
@media (max-width:520px){
  html[data-ui="next"] .auth .auth-head{ padding:14px 16px; min-height:0; }
  html[data-ui="next"] .auth .auth-body{ padding:24px 16px 32px; }
  html[data-ui="next"] .auth .auth-card{ padding:22px 18px; }
  html[data-ui="next"] .auth .auth-foot{
    flex-direction:column; align-items:flex-start; gap:8px; padding:16px 16px 24px;
  }
}

/* The concept styles .field inputs by type, and lists only email and password.
   Its own sign-up card has a type=text field for the full name, so that one
   input renders as a browser default - 155px wide, square corners, grey border -
   next to two properly styled ones. Same values as concept.css:792, extended to
   the types a form actually uses. Scoped to .auth-card so no other page changes. */
html[data-ui="next"] .auth-card .field input[type=text],
html[data-ui="next"] .auth-card .field input[type=tel],
html[data-ui="next"] .auth-card .field input:not([type]){
  width:100%;font-family:var(--font-sans);font-size:14px;color:var(--ink);
  background:var(--surface-raised);border:1px solid var(--line);
  border-radius:var(--radius-sm);padding:9px 11px;
  transition:border-color .15s ease, box-shadow .15s ease;
}

/* Bootstrap Reboot sets line-height:inherit on form controls, so every input in
   next mode inherits the body's 1.5 where the concept, which has no Bootstrap,
   leaves the browser default of normal. The effect is quiet and everywhere: a
   .field input renders 41px against the concept's 37, a .inp 38.25 against 33.8.
   Nothing looks broken, it is just consistently four pixels wrong, on every form
   in the app. Restored to what the concept's own file computes. */
html[data-ui="next"] .inp,
html[data-ui="next"] .auth-card .field input,
html[data-ui="next"] input.inp,
html[data-ui="next"] select.inp,
html[data-ui="next"] textarea.inp{
  line-height:normal;
}

/* ---------- Filter bars ----------
   Five analytics pages build the same filter bar: a .row of col-lg-3 blocks,
   each a label over a control. Two of those blocks hold a PAIR of inputs, and
   that is where it breaks. A date input has an intrinsic minimum width - the
   date text plus the picker button, about 142px in Chrome - and a flex item
   cannot shrink below its intrinsic minimum unless something sets min-width.
   So the two date inputs occupy 292px inside a 251px column and the second one
   is drawn straight over the Time Range label and field next to it. Measured on
   /operations/analytics: date-end spans x462..604, time-start begins at x579.

   The concept does not lay filters out on a twelve column grid at all; it uses
   .toolbar, which is flex with wrap. That is also the fix, because a wrapping
   flex row cannot overlap: when the items stop fitting they move to the next
   line instead of on top of each other. So the filter row becomes what the
   concept would have drawn, and the pairs are given a basis wide enough for
   their real contents.
   Scoped by ":has(> [class*=col-] > .lbl)", the filter-bar signature: a row
   whose columns begin with a label. Ordinary content rows are untouched. */
@media (min-width:768px){
  html[data-ui="next"] .panel-body > .row:has(> [class*="col-"] > .lbl){
    display:flex; flex-wrap:wrap; gap:12px; margin:0;
  }
  html[data-ui="next"] .panel-body > .row:has(> [class*="col-"] > .lbl) > [class*="col-"]{
    flex:1 1 200px; width:auto; max-width:none; padding:0;
  }
  /* 2 x 142 + 8 of gap, and 2 x 122 + 8, measured rather than guessed. */
  html[data-ui="next"] .panel-body > .row:has(> [class*="col-"] > .lbl)
    > [class*="col-"]:has(.inp[type=date]){ flex-basis:300px; }
  html[data-ui="next"] .panel-body > .row:has(> [class*="col-"] > .lbl)
    > [class*="col-"]:has(.inp[type=time]){ flex-basis:260px; }
}

/* Inside a pair, the two controls share their column evenly. min-width:0 is the
   part that matters: without it these never shrink, whatever the flex values
   say, because min-width defaults to auto on a flex item. */
html[data-ui="next"] .pv-flex > .inp[type=date],
html[data-ui="next"] .pv-flex > .inp[type=time]{
  flex:1 1 0; width:auto; min-width:0;
}

/* ---------- KPI tiles ----------
   The concept's .k-val is white-space:nowrap with no min-width, which is right
   for the values it was drawn with: "-71", "4", "18.2". The app also puts words
   in there - the unknown state reads "No data" - and a flex item that cannot
   wrap and cannot shrink pushes its sibling out. .k-thr has margin-left:auto, so
   what got pushed was the threshold, straight through the tile's right border:
   measured on /dashboard, "< -75 dBm" ended at x508 against a tile edge at 502.
   The value now yields space rather than the tile losing its contents. Truncated
   rather than wrapped, because a two-line value would push the tile taller than
   its neighbours in the same grid row. */
html[data-ui="next"] .kpi-tile .k-val{
  min-width:0; overflow:hidden; text-overflow:ellipsis;
}
html[data-ui="next"] .kpi-tile .k-thr{ flex-shrink:0; }
/* Belt and braces: nothing in a tile may paint outside it. */
html[data-ui="next"] .kpi-tile{ overflow:hidden; }

/* The concept's .k-unit sits 1px off the value, which is correct for the units
   it was drawn with: "%" and "dBm" set tight against the number. The app's rules
   also carry worded units, and 1px rendered them as "0access points", "0drops",
   "0scans". A unit that is a word takes a real space; a percent sign does not.  */
html[data-ui="next"] .kpi-tile .k-unit.k-unit-word{ margin-left:4px; }

/* Pagination.razor now emits the concept's .tbl-foot directly. Bootstrap's own
   .pagination rules are gone from the markup, but the component sits inside a
   .panel-body p-0 on the alerts panel, where the footer needs the same hairline
   the concept gives it after a table. */
html[data-ui="next"] .panel-body > .tbl-foot{ border-top:1px solid var(--line); }
html[data-ui="next"] .pager button:disabled{ opacity:.4; cursor:default; }
html[data-ui="next"] .pager button:disabled:hover{ background:var(--surface-raised); color:var(--ink-muted); }

/* ---------- Rail: collapse, and the footer note ----------
   Two separate complaints, both about the same 220ms.

   1. The rail animates its width; the content beside it did not. .shell is
      padded by --rail-width, and a custom property change applies instantly, so the
      whole page jumped 176px in one frame while the rail glided. The rail looked
      smooth and everything else looked broken. Padding is animatable, so it is
      animated, on the same duration and curve the rail already uses.

   2. The brand disappeared rather than collapsed. The concept hides the wordmark
      with display:none, which cannot be transitioned - the word was there in one
      frame and gone the next, while the logo beside it slid. Given width, opacity
      and margin instead, so the word closes up into the logo. Overriding a
      display:none needs more specificity than the ported rule (0,5,1) has, hence
      span.word.

   The labels further down the rail are left on the concept's visually-hidden
   clip: they sit behind their icons rather than beside them, so there is no
   horizontal distance for them to travel and fading them only adds noise. */
html[data-ui="next"] .shell{
  transition:padding-left .22s var(--ease);
}
html[data-ui="next"] .rail .brand{
  overflow:hidden;
  transition:padding .22s var(--ease), gap .22s var(--ease);
}
html[data-ui="next"] .rail .brand span.word{
  display:inline-block;
  max-width:140px;
  opacity:1;
  white-space:nowrap;
  transition:max-width .22s var(--ease), opacity .14s linear;
}
html[data-ui="next"] .rail.collapsed .brand span.word{
  display:inline-block;
  max-width:0;
  opacity:0;
}
/* Below 1024px the rail is a drawer and collapsed means nothing (concept.css
   switches the 60px variant off there), so the drawer shows the name in full. */
@media (max-width:1023px){
  html[data-ui="next"] aside.rail.collapsed .brand{ justify-content:flex-start; padding:0 16px; gap:9px; }
  html[data-ui="next"] .rail.collapsed .brand span.word{ display:inline-block; max-width:140px; opacity:1; }
}
/* The logo itself must not be squeezed while the word closes up beside it. */
html[data-ui="next"] .rail .brand img{ flex-shrink:0; }

@media (prefers-reduced-motion:reduce){
  html[data-ui="next"] .shell,
  html[data-ui="next"] .rail .brand,
  html[data-ui="next"] .rail .brand span.word{ transition:none; }
}

/* The copyright is a footnote, not content. It was reading at --ink-3, the same
   weight as the navigation labels above it, which put it in the same visual rank
   as things you are meant to click. Dropped to the lightest grey the palette
   has that is still legible against the rail. */
html[data-ui="next"] .rail footer.rail-foot,
html[data-ui="next"] .rail footer.rail-foot small{
  color:var(--ink-muted);
}

/* .grid-2-1 is align-items:start in the concept, which is right for its usual
   contents: a wide chart beside a shorter list, where stretching the list would
   leave a tall empty box under it. On the dashboard both cells are charts, and
   ApexCharts does not render them to the same height even though both are asked
   for 350px - the area chart draws a legend under the plot and comes out 365,
   the donut does not and comes out 350. The panels then measured 464 and 449,
   which reads as a mistake rather than as a deliberate ragged edge.
   Stretched only when every cell in the row is a chart panel, so the concept's
   chart-beside-list rows keep the alignment it chose for them. */
html[data-ui="next"] .grid-2-1:has(> .panel.chart):not(:has(> :not(.panel.chart))){
  align-items:stretch;
}

/* ==========================================================================
   The CaptureTech gradient on filled controls.

   The brand sweep is the one already on the Copilot help button:
   linear-gradient(135deg, #00baff, #0064cd), which is --ct-primary to --ct-dark
   in app.css. The concept fills its primary surfaces with flat --blue (#0064cd),
   the dark end of that same sweep, so this replaces the fill on the four
   controls that carry it: primary buttons, the toggle when on, and the two user
   avatars.

   Left flat on purpose: the 7px notification dot and the 2px route progress bar.
   A two-stop gradient across seven pixels is not a gradient, it is a slightly
   wrong colour.

   CONTRAST, measured rather than assumed. White on the dark end (#0064cd) is
   5.67:1, comfortably past the 4.5:1 that normal text needs. White on the light
   end (#00baff) is 2.22:1, which does not pass. So on a wide button the label
   starts on a failing background and ends on a passing one. That is a real
   regression against the flat fill and it is why the sign-in button was made
   solid earlier in this migration. Two things reduce it: the sweep is 135deg,
   so on a control that is wider than it is tall the light corner occupies only
   the top left rather than the whole left half, and the stops are pulled so the
   light end is short. If it still reads too light, changing --ct-grad-from to
   #0083d3 puts the whole control past 4.5:1 and keeps the sweep.
   ========================================================================== */

/* Primary buttons. The concept's .btn-blue is (0,2,0); this matches it and
   loads first, so it needs the extra class to win. */
html[data-ui="next"] .btn.btn-blue{
  background:var(--inverse);
  color:var(--on-inverse);
  border-color:transparent;
}
html[data-ui="next"] .btn.btn-blue:hover{ background:var(--inverse-hover); }
html[data-ui="next"] .btn.btn-blue:disabled,
html[data-ui="next"] .btn.btn-blue.disabled{ background:var(--inverse); }

/* The toggle, when it is on. label.switch rather than .switch: the ported concept
   sheet carries the identical selector at var(--signal) and loads after this file,
   so at equal specificity it won and the concept's own switch stayed flat while
   every Bootstrap one beside it had the gradient. The extra element selector
   settles it. */
html[data-ui="next"] label.switch input:checked + .track{ background:var(--inverse); }
/* A switch that cannot be changed (a safety alert is always on) reads as fixed, not as live. */
html[data-ui="next"] label.switch:has(input:disabled){ cursor:not-allowed; }
html[data-ui="next"] label.switch input:disabled + .track{ opacity:.45; }

/* Both user avatars: the one in the topbar chip and the larger one in the
   dropdown underneath it.

   Qualified with .topbar, which both of them sit inside. Without it these
   selectors are character for character the ones in the ported concept sheet,
   which loads after this file and therefore wins the tie - the avatars stayed
   flat #0064cd while the buttons beside them had already changed. The same trap
   .chip-crit and .rail-foot fell into earlier in this migration. */
html[data-ui="next"] .topbar .userchip .avatar,
html[data-ui="next"] .topbar .pop-user .avatar{
  background:var(--inverse);
  /* The initials sat high in the circle. Two separate reasons.

     align-items:center centres the line box, and the line box inherits
     --app-lh 1.5, so its half-leading is split around a baseline nowhere near
     the middle. line-height:1 takes that out.

     What is left is that capitals have no descender. The line box reserves room
     under the baseline that CV never uses, so even a perfectly centred line box
     leaves the letters riding high. Measured against the real baseline, not the
     line box: 1.25px high before, 0.5px after line-height alone.

     Padding finishes it. The avatars are border-box at a fixed size, so
     padding-top shrinks the content box and moves the centred line down by half
     of it: 1px of padding buys 0.5px of shift. */
  line-height:1;
}
html[data-ui="next"] .topbar .userchip .avatar{ padding-top:1px; }
html[data-ui="next"] .topbar .pop-user .avatar{ padding-top:2px; }

/* The toggles. The app builds these with Bootstrap's .form-switch rather than
   the concept's .switch, so the rule above for .switch matches nothing here;
   there are 21 form-switches across 10 files and no .switch anywhere.

   Bootstrap puts the knob in background-image and the track in
   background-color, so a gradient cannot simply replace the colour. It goes in
   as a second background layer instead, behind the knob. The knob is redeclared
   rather than inherited: it is the same white circle Bootstrap draws
   (viewBox -4 -4 8 8, r=3), written out here so this does not depend on the
   internals of their data URI surviving an upgrade. */
html[data-ui="next"] .form-switch .form-check-input:checked{
  background-color:transparent;
  border:0;
  /* Belt and braces: paint the gradient against the border box explicitly, so
     this survives anything that reintroduces a border later. */
  background-origin:border-box;
  background-image:
    url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"),
    var(--inverse);
  background-repeat:no-repeat, no-repeat;
  background-position:right center, center;
  background-size:contain, cover;
}

/* Plain checkboxes and radios keep the flat fill. They are 16px square and
   carry a tick mark of their own in background-image; a two-stop sweep across
   that is not visible, and layering it would mean redrawing their glyph too. */

/* The switch when it is OFF. Only the checked state was handled above, so the
   two halves of the same control came from two different designs: on was the
   concept's filled pill with a white knob, off was Bootstrap's outlined white
   pill with a grey knob. Same box, 28x14 with a 28px radius in both cases, but
   one reads as a solid object and the other as an empty outline.

   Matched to concept.css:1231, which fills the track with rgba(10,25,50,0.15)
   and carries no border at all, and given the same white knob as the on state so
   the only thing that changes between them is the track colour and which end the
   knob sits at. */
html[data-ui="next"] .form-switch .form-check-input{
  background-color:color-mix(in srgb, var(--ink) 15%, transparent);
  /* No border at all, rather than a transparent one. Bootstrap's 1px border
     insets the padding box, and a background IMAGE is sized against the padding
     box by default while a background COLOUR always paints the full border box.
     That is why the two states did not match: the off track is a colour and
     stayed a pill, the on track is a gradient and was painted 1px short on every
     side, which is precisely where the 28px corner radius lives. The fill lost
     its corners and read as a rectangle. With no border the two boxes coincide
     and both states paint the same shape. */
  border:0;
  background-image:
    url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
  background-position:left center;
  background-size:contain;
}

/* No ring when you click a toggle, a ring when you tab to one.
   :focus-visible does not separate those for a checkbox: Chrome matches it on a
   plain mouse click, so styling through it still put a halo around the pill
   every time it was pressed. pv-focus.js marks <html> while the keyboard is
   in use, and the ring hangs off that instead. The global
   :focus-visible{outline:2px solid} is cleared either way, because it draws a
   rounded rectangle around a shape that is not one. */
html[data-ui="next"] .form-check-input:focus,
html[data-ui="next"] .form-check-input:focus-visible{
  outline:none;
  box-shadow:none;
}
html[data-ui="next"].kbd .form-check-input:focus-visible{
  box-shadow:0 0 0 3px color-mix(in srgb, var(--signal) 25%, transparent);
}

/* The selected page in the rail. The row keeps the concept's tint and the icon
   keeps the concept's --blue; only the label changes, from flat --blue to the
   sweep. The gradient is on the .label span rather than the row, because
   background-clip:text clips every background on the element it is set on - put
   it on the row and the tint disappears with it.

   Contrast, since this is text on a near-white tint rather than white on a
   fill. The tint composites to rgb(237,244,252) over the rail, and against that
   the dark end of the sweep measures 5.11:1 while the light end measures 2.00:1.
   The label is 13px at weight 700, which is not large text by the WCAG
   definition, so 4.5:1 is the bar and the leading characters do not clear it.
   Same trade as the buttons and the same single fix: --ct-grad-from at #0083d3
   lifts the lightest point to 3.65:1, and a flat #0064cd would be 5.11:1
   throughout. Measured in the browser, not estimated. */
html[data-ui="next"] .rail a.item.active{ background:var(--signal-soft); }
html[data-ui="next"] .rail a.item.active > svg{ color:var(--signal); }
html[data-ui="next"] .rail a.item.active .label{
  background:var(--inverse);
  -webkit-background-clip:text;
  background-clip:text;
  color:transparent;
}

/* The copyright, collapsing on the same curve as the wordmark above it.
   It used to be removed with d-none from the Blazor state, so it did not
   animate at all and it did not go until the server had answered: the rail was
   already 90ms into its slide while the copyright was still sitting there, and
   then it vanished in one frame. Same two properties, same duration and easing
   as .brand span.word, and driven by .rail.collapsed, which the click handler
   sets immediately. */
html[data-ui="next"] .rail footer.rail-foot{ overflow:hidden; white-space:nowrap; }
html[data-ui="next"] .rail footer.rail-foot small{
  /* One line each: the copyright, then the maker. */
  display:block;
  max-width:228px;
  opacity:1;
  transition:max-width .22s var(--ease), opacity .14s linear;
}
html[data-ui="next"] .rail.collapsed footer.rail-foot small{
  max-width:0;
  opacity:0;
}

@media (prefers-reduced-motion:reduce){
  html[data-ui="next"] .rail footer.rail-foot small{ transition:none; }
}


/* ---------- Setting rows on the configuration tabs ----------
   The concept lays a setting out as .set-row: the name and its explanation on
   the left, the control on the right, everything in the text column starting at
   the same x. The app builds these with Bootstrap's .form-check, which puts the
   control on the LEFT and then indents the explanations with ms-4 to try to line
   them up under the label. It does not line up: measured on /configuration, the
   label sits at x=347 and both description lines at x=371, so every setting on
   the page has its own text hanging 24px to the right of its heading.

   Re-laid out as a grid without touching the markup. Column one holds the label
   and every description, so they share one left edge; column two holds the
   control, on the right where the concept puts it. */
html[data-ui="next"] .panel-body .form-check.form-switch{
  display:grid;
  grid-template-columns:minmax(0,1fr) auto;
  column-gap:20px;
  align-items:center;
  padding-left:0;
}
html[data-ui="next"] .panel-body .form-check.form-switch > .form-check-input{
  grid-column:2; grid-row:1;
  margin-left:0; margin-top:0; float:none;
  align-self:center;
}
html[data-ui="next"] .panel-body .form-check.form-switch > .form-check-label{
  grid-column:1; grid-row:1;
  min-width:0;
}
/* Everything after the label stacks under it in the same column. The ms-4 that
   was doing the indenting is overridden rather than removed from ten files, and
   it is a Bootstrap utility, so it has to be met in kind. */
html[data-ui="next"] .panel-body .form-check.form-switch > :not(.form-check-input):not(.form-check-label){
  grid-column:1;
  margin-left:0 !important;
}

/* ---------- Notice variants ----------
   The concept draws one .notice, in red, for the sign-in error. Several screens
   need the same block in other tones: the configuration tabs report a licence
   result that can be success, warning or danger, and the operations dashboard
   warns when its detail query fails. Same geometry as concept.css:809, only the
   two colours change, so a warning does not have to borrow the failure red. */
html[data-ui="next"] .notice{
  border-radius:var(--radius-sm);padding:12px 14px;margin-bottom:14px;
  background:color-mix(in srgb, var(--crit) 5%, transparent);border:1px solid color-mix(in srgb, var(--crit) 20%, transparent);
  font-size:13px;line-height:1.5;color:var(--ink);
}
/* Qualified .notice.notice-x, not .notice-x on its own. The concept defines
   .notice twice - concept.css:809 in failure red, concept-pages.css:1055 in
   blue - and the port keeps both, so the blue one lands after this file and
   wins at equal specificity. Every tone rendered blue, whatever it said. One
   more class puts the tone back in front of it. */
html[data-ui="next"] .notice.notice-success{
  background:color-mix(in srgb, var(--good) 6%, transparent); border-color:color-mix(in srgb, var(--good) 22%, transparent);
}
html[data-ui="next"] .notice.notice-warning,
html[data-ui="next"] .notice.notice-warn{
  background:color-mix(in srgb, var(--warn) 6%, transparent); border-color:color-mix(in srgb, var(--warn) 22%, transparent);
}
html[data-ui="next"] .notice.notice-info{
  background:color-mix(in srgb, var(--signal) 5%, transparent); border-color:color-mix(in srgb, var(--signal) 14%, transparent);
}
html[data-ui="next"] .notice.notice-danger{
  background:color-mix(in srgb, var(--crit) 5%, transparent); border-color:color-mix(in srgb, var(--crit) 20%, transparent);
}

/* A .notice can carry a close button, since the alert bars it replaced were
   dismissible. Bootstrap's .btn-close is a background-image X on a bare button;
   given the concept has no equivalent, it is toned down to match .dlg-x rather
   than left at Bootstrap's weight. */
html[data-ui="next"] .notice{ position:relative; }
html[data-ui="next"] .notice .btn-close{
  position:absolute; top:8px; right:8px;
  width:22px; height:22px; padding:0; opacity:.45;
  border:0; background-size:9px;
}
html[data-ui="next"] .notice .btn-close:hover{ opacity:.8; }

/* The concept's own switch, used by <Switch>. Ported CSS already styles .switch;
   this only guards the case where one sits in a table cell, where the row's
   line-height would otherwise stretch it. */
html[data-ui="next"] td > .switch{ vertical-align:middle; }

/* The sort caret in a table header. Sized to the header's own 11px label rather
   than the body text, and set on the baseline of the word it follows. */
html[data-ui="next"] .sort-caret{
  width:11px; height:11px; margin-left:4px;
  display:inline-block; vertical-align:-1px;
  color:var(--ink-muted); flex-shrink:0;
}
html[data-ui="next"] th .sort-caret{ color:var(--signal); }

/* The icon component. Sized to the text it sits with and inheriting its colour,
   which is the point of an inline svg over a webfont glyph. */
html[data-ui="next"] .ico{
  width:15px; height:15px; flex-shrink:0;
  display:inline-block; vertical-align:-2px;
}
html[data-ui="next"] .btn .ico{ width:14px; height:14px; vertical-align:0; }
html[data-ui="next"] .ico-lg{ width:22px; height:22px; }
/* The concept states its good and warn greens and ambers inside .status-good
   and .status-warn and nowhere else, so anything that needs them has to copy
   the value. These give them a name. Same numbers, checked against
   concept.css. */

html[data-ui="next"] .ico-sm{ width:12px; height:12px; }
/* A dimmed icon takes its tint from the icon, not from a Bootstrap text
   colour, so a page cannot mute the glyph and the label by different rules. */
html[data-ui="next"] .ico-mute{ color:var(--ink-muted); }

/* ---------- Section label ----------
   A quiet heading over a group of panels. The concept labels groups in the rail
   and in the workflow panes but never on a page, so this follows those: 11px,
   500, uppercase. --ink-3 not --ink-4, for the same reason the rail's own
   nav-group carries a comment about it: 11px uppercase is small text and needs
   4.5:1, which --ink-4 does not reach on white. */
html[data-ui="next"] .section-label{
  font-size:11px; font-weight:500; letter-spacing:.05em; text-transform:uppercase;
  color:var(--ink-muted); margin:0 0 8px;
}

/* ---------- Section head ----------
   A title with its actions, over a group of panels. The concept has .page-head
   for the page and .panel-head for one panel, and nothing in between, because
   its own drafts never nest that deep. The config tabs do: each renders inside
   a page that already has a head, so a second .page-head would claim to be the
   page. Same flex row, panel-head's type size. */
html[data-ui="next"] .section-head{
  display:flex; align-items:center; justify-content:space-between;
  gap:12px; margin-bottom:14px;
}
html[data-ui="next"] .section-head > h3{
  margin:0; font-size:13.5px; font-weight:600; color:var(--ink);
}
html[data-ui="next"] .section-head .acts{ display:flex; align-items:center; gap:8px; }

/* ---------- Chip on a brand surface ----------
   The release-note dialog puts a version chip on the CaptureTech gradient. The
   concept has no chip for that, because the concept has no brand-coloured
   surfaces at all - every panel head is white. Rather than add a variant to the
   closed ChipKind set for one site, the exception lives here and says so. */
html[data-ui="next"] .chip.on-brand{
  background:var(--surface-raised); border-color:transparent; color:var(--signal);
}

/* The release-note dialog's head, which the app paints in the brand gradient.
   Same exception as .chip.on-brand above, and the same reasoning: the concept
   has no brand-coloured surfaces, so it has no head for one. */
html[data-ui="next"] .dlg-head.on-brand{
  background:var(--inverse); border-bottom-color:transparent;
}
html[data-ui="next"] .dlg-head.on-brand h3{ color:var(--surface-raised); }
html[data-ui="next"] .dlg-head.on-brand .dlg-x{ color:color-mix(in srgb, var(--on-inverse) 82%, transparent); }
html[data-ui="next"] .dlg-head.on-brand .dlg-x:hover{
  color:var(--surface-raised); background:color-mix(in srgb, var(--on-inverse) 16%, transparent);
}

/* ---------- Sub head ----------
   A quiet heading over a group inside a panel body. Same three declarations the
   concept gives .dl-group > h4, which is the only group heading it draws;
   .section-label above is the other one, and the difference is where they sit:
   that one labels a group of panels, this one labels a group inside one. */
html[data-ui="next"] .sub-head{
  margin:0 0 8px; font-size:12px; font-weight:600; color:var(--ink-muted);
}

/* ---------- Entity head ----------
   The block at the top of a detail sheet that says what you are looking at.
   Seven modals had their own copy of the circle, at two sizes; the two sizes
   were the only real difference and they are kept, because 80px stacked over a
   sheet and 50px in a row are two different jobs.

   Not from the concept: none of its drafts show a device or an operator. */
html[data-ui="next"] .ent-head{
  display:flex; align-items:center; gap:12px; margin-bottom:16px;
}
html[data-ui="next"] .ent-head.stack{
  flex-direction:column; text-align:center; gap:10px; margin-bottom:20px;
}
html[data-ui="next"] .ent-avatar{
  width:50px; height:50px; flex-shrink:0; border-radius:50%;
  background:var(--inverse); color:var(--surface-raised);
  display:flex; align-items:center; justify-content:center;
}
html[data-ui="next"] .ent-head.stack .ent-avatar{ width:80px; height:80px; }
html[data-ui="next"] .ent-avatar .ico{ width:20px; height:20px; }
html[data-ui="next"] .ent-head.stack .ent-avatar .ico{ width:30px; height:30px; }
html[data-ui="next"] .ent-text{ flex:1; min-width:0; }
html[data-ui="next"] .ent-text > b{
  display:block; font-family:var(--font-sans); font-size:15px; font-weight:700;
  color:var(--ink); letter-spacing:-0.012em;
}
html[data-ui="next"] .ent-text > span{
  display:block; font-size:12.5px; color:var(--ink-muted); margin-top:2px;
}
html[data-ui="next"] .ent-side{ flex-shrink:0; }
html[data-ui="next"] .ent-head.stack .ent-side{ margin-top:2px; }

/* The cookie banner's own title. It is not a page section and not a panel head,
   so it keeps its own element; this only gives it the concept's heading type
   instead of the browser's h5 default. */
html[data-ui="next"] .cookie-consent-text > h3{
  margin:0 0 6px; font-family:var(--font-sans); font-size:15px;
  font-weight:700; color:var(--ink); letter-spacing:-0.012em;
}

/* A table row that is present but not in effect - a warehouse the user has no
   access to. Bootstrap's .table-light was doing this, which also meant "header"
   on three other tables. */
html[data-ui="next"] .tbl tr.row-quiet > td{ color:var(--ink-muted); background:var(--surface-sunken); }

/* The panel a deep link points at, so arriving from an alert email lands
   somewhere obvious. Was "border border-primary", which is Bootstrap drawing a
   second border over the one the panel already has. */
html[data-ui="next"] .panel.is-linked{ border-color:var(--signal); }

/* ---------- History tables ----------
   The device and operator history tables, which want every column on one line
   and a tighter row than the default. Was four copies in four modals; the two
   that truncated the message column at 200px and the two that used 250px were
   not two decisions, so 250 it is. */
/* th is already nowrap in the concept, and its padding is 9px var(--pad),
   which is what the old 8px 16px !important was approximating. So only the
   cells need saying. */
html[data-ui="next"] .history-table th,
html[data-ui="next"] .history-table td{ white-space:nowrap; padding:8px 16px; }
html[data-ui="next"] .history-table .message-cell{
  max-width:250px; overflow:hidden; text-overflow:ellipsis;
}

/* ---------- Embedded map ----------
   The map sits flush against the top of its container and rounds with it. */
html[data-ui="next"] .map-embed{ border-radius:var(--radius-md) var(--radius-md) 0 0; overflow:hidden; }
html[data-ui="next"] .map-embed iframe{ display:block; }
html[data-ui="next"] .map-coordinates{ font-size:12px; color:var(--ink-muted); }

/* A table whose rows hold controls of different heights, so the text beside
   them lines up with their middles rather than their tops. */
html[data-ui="next"] table.tbl.align-middle td{ vertical-align:middle; }

/* ---------- Config tab strip ----------
   Drawn by both TabbedConfiguration and SystemSettings, which each had their
   own copy; SystemSettings lost its base and hover when the modal chrome was
   cleaned out of its style block. One rule, both pages. */
html[data-ui="next"] .config-tabs .nav-link{
  color:var(--ink-muted); border:1px solid transparent; transition:all .2s ease;
}
html[data-ui="next"] .config-tabs .nav-link:hover{
  color:var(--signal); background:color-mix(in srgb, var(--signal) 8%, transparent);
}
html[data-ui="next"] .config-tabs .nav-link.active{
  color:var(--surface-raised); background:var(--inverse); border-color:transparent;
}

/* Bootstrap's accordion, the last component with no bridge at all. Two screens
   use one - the release notes and the company list - and both were drawn
   entirely by bootstrap.min.css: a blue tinted open header, a blue focus ring,
   a 1rem body and rounded corners that no other block on the page has.

   The concept has no accordion, but it does have a disclosure: .icon-btn and
   .userchip both take [aria-expanded="true"] and answer with color-mix(in srgb, var(--ink) 4%, transparent) and
   var(--ink), not with a colour. Same answer here. */
html[data-ui="next"] .accordion{
  --bs-accordion-border-color:var(--line);
  --bs-accordion-btn-focus-box-shadow:none;
  --bs-accordion-active-bg:color-mix(in srgb, var(--ink) 4%, transparent);
  --bs-accordion-active-color:var(--ink);
  --bs-accordion-border-radius:var(--radius-md);
  --bs-accordion-inner-border-radius:var(--radius-md);
}
html[data-ui="next"] .accordion-item{
  background:transparent; border-color:var(--line); color:var(--ink);
}
html[data-ui="next"] .accordion-button{
  padding:11px var(--pad); font-size:13.5px; font-weight:500;
  color:var(--ink); background:transparent; box-shadow:none;
}
html[data-ui="next"] .accordion-button:hover{ background:color-mix(in srgb, var(--ink) 4%, transparent); }
html[data-ui="next"] .accordion-button:not(.collapsed){
  color:var(--ink); background:color-mix(in srgb, var(--ink) 4%, transparent); box-shadow:none;
}
html[data-ui="next"] .accordion-button:focus{
  border-color:var(--signal); box-shadow:0 0 0 3px color-mix(in srgb, var(--signal) 10%, transparent);
}
/* The chevron is a background image carrying its own blue. Recolour it the way
   the rest of the concept's carets are: ink-4 closed, ink-2 open. */
html[data-ui="next"] .accordion-button::after{
  width:13px; height:13px; background-size:13px;
  filter:grayscale(1) opacity(.55);
}
html[data-ui="next"] .accordion-button:not(.collapsed)::after{
  filter:grayscale(1) opacity(.8);
}
html[data-ui="next"] .accordion-body{
  padding:12px var(--pad) 14px; font-size:13px; color:var(--ink);
}
/* Flush is what the rules above already draw; this only stops Bootstrap
   putting the side borders and the outer radius back. */
html[data-ui="next"] .accordion-flush .accordion-item{
  border-left:0; border-right:0; border-radius:0;
}

/* A joined row of buttons. Bootstrap squares the inner corners against its own
   radius; the concept's is --r-ctl, so the ends have to be restated or the
   group comes out with one rounded side and one square one. */
html[data-ui="next"] .btn-group > .btn{ border-radius:0; }
html[data-ui="next"] .btn-group > .btn:first-child{
  border-top-left-radius:var(--radius-sm); border-bottom-left-radius:var(--radius-sm);
}
html[data-ui="next"] .btn-group > .btn:last-child{
  border-top-right-radius:var(--radius-sm); border-bottom-right-radius:var(--radius-sm);
}

/* The list group's container and its hoverable rows. Only .list-group-item was
   bridged, so the wrapper kept Bootstrap's radius and an -action row still lit
   up in Bootstrap's grey. */
html[data-ui="next"] .list-group{ border-radius:var(--radius-sm); }
html[data-ui="next"] .list-group-flush{ border-radius:0; }
html[data-ui="next"] .list-group-item-action:hover,
html[data-ui="next"] .list-group-item-action:focus{
  background:color-mix(in srgb, var(--ink) 4%, transparent); color:var(--ink);
}
html[data-ui="next"] .list-group-item.active{
  background:var(--signal-soft); border-color:var(--line); color:var(--ink);
}

/* The three size and wrapper modifiers that had no rule of their own, so
   Bootstrap was still sizing them. Values are the concept's .btn-sm. */
html[data-ui="next"] .btn-group-sm > .btn{
  padding:6px 10px; font-size:12.5px; min-height:28px;
}
html[data-ui="next"] .input-group-sm > .inp,
html[data-ui="next"] .input-group-sm > .input-group-text,
html[data-ui="next"] .input-group-sm > .btn{
  padding:6px 10px; font-size:12.5px; min-height:28px;
}
/* .accordion-collapse only wraps; the concept's hairline lives on the item and
   the padding on the body, so it must not add either of its own. */
html[data-ui="next"] .accordion-collapse{ border:0; }

/* ---------------------------------------------------------------------------
   Blocks never touch.

   The concept's vertical rhythm is .page-section, 20px apart. Pages that stack
   a panel or a grid directly, without wrapping each in a section, got nothing:
   the battery tab put a filter card, a row of tier cards, a chart row and a
   table in one container with 0px between all four, and the filter cards had
   three grid rows with 0px between them.

   Keyed on the two blocks rather than on their container, because the container
   is usually a page's own div - .battery-health-container and friends - and
   there is no list of those to maintain.

   :not(.grid) > because inside a grid the parent does the spacing, and adding a
   margin to a grid item shifts it against its own row.

   Block flow makes this self-normalising: this margin-top collapses with
   .page-section's existing margin-bottom, so a section followed by a panel is
   20px apart, not 34.

   :is() rather than the pairs written out. The first version listed six of them
   by hand and left .page-section out of the vocabulary, so a section following a
   grid still got nothing - which is what every configuration tab does.
   --------------------------------------------------------------------------- */
html[data-ui="next"] :not(.grid) > :is(.panel, .grid, .tbl-wrap, .page-section, .pv-readouts)
                   + :is(.panel, .grid, .tbl-wrap, .page-section, .pv-readouts){ margin-top:var(--gap); }

/* ---------------------------------------------------------------------------
   Support at the foot of the rail: pinned under the scrolling nav and outside
   its sections, on a hairline of its own, so it never reads as part of System.

   The lock on a submenu item (Optimization Labs when it is not switched on):
   a quiet trailing glyph, not a colour, so a locked page still reads as a page.

   On a phone a page with seven tabs (Settings) is wider than the screen: the
   strip scrolls sideways rather than pushing the page past the viewport.
   --------------------------------------------------------------------------- */
html[data-ui="next"] .rail .rail-support{
  flex-shrink:0; padding:6px 10px; border-top:1px solid var(--line);
}
html[data-ui="next"] .rail .rail-support .item{margin-bottom:0;}
html[data-ui="next"] .rail.collapsed .rail-support{padding:6px;}
html[data-ui="next"] .rail .rail-lock{display:inline-flex;align-items:center;color:var(--ink-muted);}
html[data-ui="next"] .rail.collapsed .rail-lock{display:none;}
html[data-ui="next"] .rail.collapsed .nav-item:hover > .submenu .rail-lock,
html[data-ui="next"] .rail.collapsed .nav-item:has(:focus-visible) > .submenu .rail-lock{display:inline-flex;margin-left:auto;}
/* A tab strip never wraps a label or widens its column: the printer page's nine tabs in a two-thirds column broke
   "Operation log" over two lines and pushed "Console" past the page edge. It scrolls sideways in its own box. */
html[data-ui="next"] .tabs{overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;min-width:0;}
html[data-ui="next"] .tabs button{white-space:nowrap;flex-shrink:0;}
@media (max-width:700px){
  html[data-ui="next"] .tabs{scrollbar-width:none;}
  html[data-ui="next"] .tabs::-webkit-scrollbar{display:none;}
}

/* =========================================================
   The rail's background mark is positioned (z-index 0) and the menu was not, so
   the mark painted OVER the lower menu items ("Users" sat under it at 900px
   high). The menu and its footer are lifted above it; the nav keeps scrolling.
   ========================================================= */
html[data-ui="next"] .rail nav,
html[data-ui="next"] .rail .rail-support,
html[data-ui="next"] .rail footer.rail-foot{ position:relative; z-index:1; }

/* A facet's checkboxes are inline, so their labels ran into the next box
   ("Honeywell[ ]Zebra"). Each sits on its own line with room between them. */
html[data-ui="next"] .pv-facet{ display:flex; flex-direction:column; align-items:flex-start; gap:4px; }

/* =========================================================
   Printer list on a phone: one card per printer (status, name, model, address
   and location) instead of a table nine columns wide. Columns marked
   .pv-hide-card stay on the desktop table only.
   ========================================================= */
@media (max-width:640px){
  html[data-ui="next"] .pv-cardlist table.tbl thead{ display:none; }
  html[data-ui="next"] .pv-cardlist table.tbl,
  html[data-ui="next"] .pv-cardlist table.tbl tbody{ display:block; width:100%; }
  html[data-ui="next"] .pv-cardlist table.tbl tr{
    display:grid; grid-template-columns:1fr auto; gap:2px 12px;
    padding:12px 14px; border-bottom:1px solid var(--line);
  }
  html[data-ui="next"] .pv-cardlist table.tbl td{ display:block; padding:0; border:0; }
  html[data-ui="next"] .pv-cardlist table.tbl td.pv-card-title{ grid-column:1; grid-row:1; }
  html[data-ui="next"] .pv-cardlist table.tbl td.pv-card-status{ grid-column:2; grid-row:1 / span 2; text-align:right; }
  html[data-ui="next"] .pv-cardlist table.tbl td.pv-hide-card{ display:none; }
  html[data-ui="next"] .pv-cardlist table.tbl td:not(.pv-card-title):not(.pv-card-status){ grid-column:1; font-size:13px; color:var(--ink-muted); }
}
.pv-text-warn{ color:var(--warn); }
