/* ============================================================================
   VigilAI shared UI  —  .vg-icon · .vg-btn · .vg-state · .vg-skel · .vg-modal
   ----------------------------------------------------------------------------
   The pieces every page kept rebuilding by hand. Before this file there were
   five competing button vocabularies (.btn-primary, .edge-btn, .btn-edge-*,
   .btn-vg-primary, .vgt-btn) and .vg-btn itself lived inside a <style> block in
   a single page. Empty states, skeletons and dialogs were re-typed per page —
   the event dialog went as far as appending a <style> element to <head> at
   runtime, on every open.

   Loaded from _Layout AFTER vg-viz.css, so it is last in the chain and can only
   win. Consequences that are not optional:
     · no bare element selectors — this file is live on ~100 template pages
     · every class here was grepped against wwwroot + Pages before being used.
       .vg-empty was rejected: Account/UserProfile.cshtml already owns it.
       Hence .vg-state.
     · logical properties only (padding-inline, inset-inline-*, border-inline-*)
       so RTL needs no mirrored rules. This is the same contract vg-table.css
       and vg-forms.css work under.

   Not consolidated here, deliberately: .vg-iconbtn (shell header), .vgt-*
   (table), .btn-vg-primary (auth), .edge-btn* (edge pages). Each has live
   callers and its own spacing assumptions. .vg-btn is the target they migrate
   to; folding them in is a separate job with its own blast radius.
   ========================================================================= */


/* ============================================================================
   1 · Icon
   Lucide geometry, delivered by the inlined sprite in _VgIconSprite.cshtml and
   referenced same-document as <use href="#vgi-name">.

   The symbols carry only the attributes we never vary (fill, stroke,
   linecap, linejoin). stroke-width is deliberately absent from the sprite and
   set here instead: an element's own presentation attribute beats an inherited
   CSS value, so a stroke-width in the sprite could never be overridden by a
   size variant. Inherited properties do cross the <use> shadow boundary, which
   is what makes currentColor and stroke-width work at all.
   ========================================================================= */

.vg-icon {
  inline-size: var(--vg-icon-size, 18px);
  block-size:  var(--vg-icon-size, 18px);
  flex: 0 0 auto;
  stroke-width: var(--vg-icon-stroke, 2);
  vertical-align: -.15em;   /* optical centring against a lowercase x-height */
}

/* Stroke weight is not scale-invariant. A flat 2 reads heavy at 14px and thin
   at 28px, which is the main reason mixed icon sizes look sloppy. */
.vg-icon--xs { --vg-icon-size: 14px; --vg-icon-stroke: 2.25; }
.vg-icon--sm { --vg-icon-size: 16px; --vg-icon-stroke: 2;    }
.vg-icon--md { --vg-icon-size: 20px; --vg-icon-stroke: 1.9;  }
.vg-icon--lg { --vg-icon-size: 24px; --vg-icon-stroke: 1.75; }
.vg-icon--xl { --vg-icon-size: 32px; --vg-icon-stroke: 1.5;  }

/* Directional glyphs only — chevrons, arrows, log-in. Never on a clock or a
   flame, which read as broken when mirrored. */
[dir="rtl"] .vg-icon--flip { transform: scaleX(-1); }

.vg-icon--spin { animation: vg-icon-spin .9s linear infinite; }

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

/* The shell's icon buttons size their glyph with font-size, which does nothing
   to an <svg>. Here so those buttons are correct the moment they convert. */
.vg-iconbtn .vg-icon { --vg-icon-size: 20px; }


/* ============================================================================
   2 · Button
   Promoted from Pages/AiSettings/Index.cshtml, which had already built this on
   --vg-* tokens; that page's local copy is deleted. Note --ghost was used there
   but never defined, so those buttons rendered as base until now.
   ========================================================================= */

.vg-btn {
  --vg-btn-h: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--vg-space-2);
  block-size: var(--vg-btn-h);
  padding-inline: var(--vg-space-3);
  border: 1px solid var(--vg-line);
  border-radius: var(--vg-radius-sm);
  background: transparent;
  color: var(--vg-text);
  font-family: inherit;
  font-size: .8125rem;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;          /* <a class="vg-btn"> must match <button> */
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease,
              color .15s ease, opacity .15s ease;
}

.vg-btn:hover:not(:disabled):not([aria-disabled="true"]) {
  background: rgba(255, 255, 255, .05);
  border-color: var(--vg-line-hi, #33404f);
  color: var(--vg-text);
}

/* :focus-visible, not :focus — a mouse click on a button should not leave a
   ring behind, but a Tab landing on it must be unmistakable. */
.vg-btn:focus-visible {
  outline: 2px solid var(--vg-primary);
  outline-offset: 2px;
}

.vg-btn:disabled,
.vg-btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }

.vg-btn--primary {
  background: var(--vg-primary);
  border-color: var(--vg-primary);
  color: #fff;
}
.vg-btn--primary:hover:not(:disabled):not([aria-disabled="true"]) {
  background: #0a9ef5;
  border-color: #0a9ef5;
  color: #fff;
}

/* Borderless. For toolbars where a row of outlined buttons would read as a
   fence — the border is doing no work when the group is already grouped. */
.vg-btn--ghost {
  border-color: transparent;
  background: transparent;
  color: var(--vg-muted);
}
.vg-btn--ghost:hover:not(:disabled):not([aria-disabled="true"]) {
  background: rgba(255, 255, 255, .06);
  border-color: transparent;
  color: var(--vg-text);
}

/* Tinted rather than filled. A solid red button is a fine primary action on a
   confirm dialog and far too loud sitting in a toolbar. */
.vg-btn--danger {
  border-color: rgba(252, 66, 74, .38);
  background: rgba(252, 66, 74, .10);
  color: #ff8b90;
}
.vg-btn--danger:hover:not(:disabled):not([aria-disabled="true"]) {
  background: rgba(252, 66, 74, .18);
  border-color: rgba(252, 66, 74, .55);
  color: #ffa8ac;
}

.vg-btn--subtle {
  background: var(--vg-surface-2);
  border-color: var(--vg-line);
  color: var(--vg-muted);
}
.vg-btn--subtle:hover:not(:disabled):not([aria-disabled="true"]) { color: var(--vg-text); }

.vg-btn--sm { --vg-btn-h: 28px; font-size: .75rem;   padding-inline: var(--vg-space-2); }
.vg-btn--lg { --vg-btn-h: 40px; font-size: .875rem;  padding-inline: var(--vg-space-4); }

/* Square. inline-size is pinned to the height token so --sm/--lg stay square. */
.vg-btn--icon {
  inline-size: var(--vg-btn-h);
  padding-inline: 0;
}

.vg-btn--block { display: flex; inline-size: 100%; }

/* Pressed toggles (the density and column menus, the collapse toggle). */
.vg-btn[aria-pressed="true"] {
  background: var(--vg-primary-soft);
  border-color: var(--vg-primary-line);
  color: var(--vg-text);
}

/* Loading. Content is hidden rather than removed so the button keeps its width
   and the row does not reflow mid-request. */
.vg-btn[data-loading="true"] {
  position: relative;
  pointer-events: none;
  color: transparent;
}
.vg-btn[data-loading="true"] > * { visibility: hidden; }
.vg-btn[data-loading="true"]::after {
  content: "";
  position: absolute;
  inline-size: 15px;
  block-size: 15px;
  border: 2px solid currentColor;
  border-block-start-color: transparent;
  border-radius: 50%;
  color: var(--vg-text);
  animation: vg-icon-spin .7s linear infinite;
}
.vg-btn--primary[data-loading="true"]::after { color: #fff; }

/* Groups: one seam between neighbours instead of two stacked borders. */
/* Wraps: a group wide enough to overflow a phone would otherwise push the whole page
   into horizontal scroll (two 150px actions need 323px against ~312px at 360px wide).
   A group that already fits is unaffected — wrapping only ever engages when it must. */
.vg-btn-group { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--vg-space-2); }

/* --tight is a segmented control: the buttons share a seam via a -1px inset and
   first/last-child radii, so a wrap would break the join. It must stay on one line. */
.vg-btn-group--tight { gap: 0; flex-wrap: nowrap; }
.vg-btn-group--tight .vg-btn { border-radius: 0; }
.vg-btn-group--tight .vg-btn:first-child { border-start-start-radius: var(--vg-radius-sm); border-end-start-radius: var(--vg-radius-sm); }
.vg-btn-group--tight .vg-btn:last-child  { border-start-end-radius:   var(--vg-radius-sm); border-end-end-radius:   var(--vg-radius-sm); }
.vg-btn-group--tight .vg-btn + .vg-btn   { margin-inline-start: -1px; }



/* ============================================================================
   2b · Form control
   `.vg-input` was already being asked for by the two face-confirmation dialogs
   and by Face Gallery, but it was only ever defined inside the AI Settings
   page's own <style> block. Everywhere else it resolved to nothing, so those
   selects fell back to the browser's native control — a white box with black
   text and square corners, 19px tall, sitting on a dark dialog. This is the
   primitive those callers already assumed existed.

   AI Settings keeps its local copy: same values, and a page <style> block wins
   on source order regardless, so that page renders exactly as before.

   `color-scheme: dark` is what paints the native select popup and its arrow
   dark. A background-image chevron would look sharper but has to be flipped
   for Arabic, and the filter row on Events History already sets the precedent.
   ========================================================================= */

.vg-input {
  block-size: 36px;
  inline-size: 100%;
  min-inline-size: 0;
  padding-inline: var(--vg-space-3);
  border: 1px solid var(--vg-line);
  border-radius: var(--vg-radius-sm);
  background: var(--vg-surface-2);
  color: var(--vg-text);
  font-family: inherit;
  font-size: .8125rem;
  color-scheme: dark;
  transition: border-color .15s ease, box-shadow .15s ease;
}

.vg-input:hover:not(:disabled) { border-color: var(--vg-line-hi, #33404f); }

.vg-input:focus-visible {
  outline: none;
  border-color: var(--vg-primary);
  box-shadow: 0 0 0 3px var(--vg-primary-soft);
}

.vg-input::placeholder { color: var(--vg-faint); }
.vg-input:disabled { opacity: .55; cursor: not-allowed; }

select.vg-input   { cursor: pointer; }
textarea.vg-input { block-size: auto; padding-block: var(--vg-space-2); }


/* ============================================================================
   3 · Event tone + badge
   Detection types are colour-coded on more than one page, and they were doing it
   differently: the AI dashboard had six tones, Events History had three (fire,
   smoke, everything-else) and painted the "everything else" badge bright blue,
   so a baggage alert and an intrusion looked identical.

   A tone class sets --vg-tone; every child reads it. Pair with VG.eventTone()
   in vg-ui.js, which is the single mapping from a Lookups event name to a tone.
   ========================================================================= */

.vg-tone-fire    { --vg-tone: var(--vg-danger); }
.vg-tone-smoke   { --vg-tone: var(--vg-warning); }
.vg-tone-zone    { --vg-tone: var(--vg-success); }
.vg-tone-inout   { --vg-tone: var(--vg-primary); }
.vg-tone-face    { --vg-tone: #8f5fe8; }   /* the template's --info; the vg palette has no violet */
.vg-tone-default { --vg-tone: var(--vg-muted); }

/* Tinted, not filled. A grid of saturated badges competes with the imagery it
   sits on and stops any one alert standing out. */
.vg-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: 6px;
  font-size: .7rem;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
  color: var(--vg-tone, var(--vg-muted));
  background: color-mix(in srgb, var(--vg-tone, #6b7783) 14%, transparent);
}

.vg-badge--solid {
  color: #fff;
  background: var(--vg-tone, var(--vg-muted));
}


/* ============================================================================
   4 · State — empty · loading · error
   One block for "there is nothing to show" and "we could not show it". Modelled
   on .vgt-empty (vg-table.css) so a table's empty state and a panel's read as
   the same family.
   ========================================================================= */

.vg-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--vg-space-2);
  padding: var(--vg-space-8) var(--vg-space-5);
  text-align: center;
}

/* The icon sits on a plate rather than floating as a lone hairline glyph. At the
   size an empty result region actually occupies, a bare 34px outline icon reads
   as debris in a void; the plate gives it presence without adding colour. */
.vg-state__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: 56px;
  block-size: 56px;
  margin-block-end: var(--vg-space-2);
  border-radius: 16px;
  background: var(--vg-surface-2);
  color: var(--vg-muted);
  --vg-icon-size: 26px;
  --vg-icon-stroke: 1.6;
}

.vg-state__title { font-size: 1rem;     font-weight: 600; color: var(--vg-text);  margin: 0; }
.vg-state__hint  { font-size: .8125rem; color: var(--vg-muted); margin: 0; max-inline-size: 44ch; line-height: 1.55; }
.vg-state__action { margin-block-start: var(--vg-space-4); }

.vg-state--ok    .vg-state__icon { background: rgba(0, 210, 91, .10);  color: var(--vg-success); }
.vg-state--error .vg-state__icon { background: rgba(252, 66, 74, .10); color: var(--vg-danger); }

/* Framed: for a state that replaces a whole result set. The dashed edge marks
   out the region the results would have filled, so the page does not just look
   like it stopped rendering. */
.vg-state--framed {
  min-block-size: 300px;
  border: 1px dashed var(--vg-line);
  border-radius: var(--vg-radius);
  background: rgba(255, 255, 255, .012);
}

/* Inline variant for a state that has to sit inside an existing scroll area
   rather than own the panel. */
.vg-state--inline { padding: var(--vg-space-5) var(--vg-space-4); }
.vg-state--inline .vg-state__icon { inline-size: 44px; block-size: 44px; --vg-icon-size: 20px; }


/* ============================================================================
   5 · Skeleton
   Separate from .vg-state on purpose: a shimmer is a placeholder for content
   that is coming, not a message about content that is absent. Conflating them
   is why "loading" and "empty" look identical on so many pages.
   ========================================================================= */

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

.vg-skel__row {
  display: flex;
  align-items: center;
  gap: var(--vg-space-3);
  padding: var(--vg-space-3);
  border: 1px solid var(--vg-line-soft);
  border-radius: var(--vg-radius-sm);
}

.vg-skel__thumb {
  inline-size: 56px;
  block-size: 56px;
  border-radius: 8px;
  flex: 0 0 auto;
}

.vg-skel__lines { display: flex; flex-direction: column; gap: var(--vg-space-2); flex: 1; min-inline-size: 0; }

.vg-skel__line { block-size: 9px; border-radius: 4px; }
.vg-skel__line--short { inline-size: 42%; }
.vg-skel__line--mid   { inline-size: 68%; }

.vg-skel__thumb,
.vg-skel__line {
  background: linear-gradient(90deg,
    var(--vg-surface-2) 25%,
    #2b3139 37%,
    var(--vg-surface-2) 63%);
  background-size: 400% 100%;
  animation: vg-skel-shimmer 1.4s ease infinite;
}

@keyframes vg-skel-shimmer {
  from { background-position: 100% 50%; }
  to   { background-position: 0 50%; }
}

/* The sweep carries a direction, so it has to follow the reading direction. */
[dir="rtl"] .vg-skel__thumb,
[dir="rtl"] .vg-skel__line { animation-direction: reverse; }


/* ============================================================================
   6 · Modal
   Replaces the runtime injectModalStyles() that appended a <style> to <head>.
   The JS side (VG.modal in vg-ui.js) owns focus, the Escape listener and the
   scroll lock; this file owns nothing but appearance.
   ========================================================================= */

.vg-modal {
  position: fixed;
  inset: 0;
  z-index: 20000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--vg-space-4);
  background: rgba(0, 0, 0, .72);
  backdrop-filter: blur(4px);
  animation: vg-modal-in .18s ease;
}

.vg-modal__box {
  position: relative;
  display: flex;
  flex-direction: column;
  inline-size: min(100%, 640px);
  max-block-size: min(90vh, 900px);
  background: var(--vg-surface);
  border: 1px solid var(--vg-line);
  border-radius: var(--vg-radius);
  box-shadow: 0 24px 64px rgba(0, 0, 0, .55);
  overflow: hidden;
  animation: vg-modal-rise .2s ease;
}

.vg-modal--sm    .vg-modal__box { inline-size: min(100%, 420px); }
.vg-modal--lg    .vg-modal__box { inline-size: min(100%, 860px); }
/* Split is the media viewer — a fixed detail rail beside a frame the operator is
   trying to read faces and plates off. 1000px left the snapshot at 640px wide on
   a 1080p screen with the rest of the dialog empty; the extra width all goes to
   the media pane, since the rail's inline-size does not change. */
.vg-modal--split .vg-modal__box { inline-size: min(100%, 1440px); }

/* Confirm. No header — the question IS the content, and a title bar above a
   one-line question is just chrome. */
.vg-modal--confirm .vg-modal__box { text-align: start; }
.vg-modal--confirm .vg-modal__head { display: none; }

.vg-confirm { display: flex; align-items: flex-start; gap: var(--vg-space-3); }

.vg-confirm__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  inline-size: 40px;
  block-size: 40px;
  border-radius: 11px;
  color: var(--vg-tone, var(--vg-danger));
  background: color-mix(in srgb, var(--vg-tone, #fc424a) 14%, transparent);
}

.vg-confirm__body { min-inline-size: 0; }
.vg-confirm__title { margin: 0; font-size: .95rem; font-weight: 600; color: var(--vg-text); }
.vg-confirm__text { margin: 6px 0 0; font-size: .8125rem; line-height: 1.5; color: var(--vg-muted); }

.vg-modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--vg-space-3);
  padding: var(--vg-space-4) var(--vg-space-5);
  border-block-end: 1px solid var(--vg-line);
  flex: 0 0 auto;
}

.vg-modal__title { font-size: .95rem; font-weight: 600; color: var(--vg-text); margin: 0; }

.vg-modal__close {
  --vg-btn-h: 30px;
  flex: 0 0 auto;
  margin-inline-start: auto;
}

.vg-modal__body { padding: var(--vg-space-5); overflow: auto; flex: 1 1 auto; }
.vg-modal__body--flush { padding: 0; }

.vg-modal__foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--vg-space-2);
  padding: var(--vg-space-3) var(--vg-space-5);
  border-block-start: 1px solid var(--vg-line);
  flex: 0 0 auto;
}

/* Two-pane layout: detail rail beside media. Panes follow DOM order, and the
   seam is border-inline-start, so RTL mirrors without a second rule. */
.vg-modal__split { display: flex; min-block-size: 0; flex: 1 1 auto; }
.vg-modal__split > .vg-modal__pane { overflow: auto; min-inline-size: 0; }
.vg-modal__split > .vg-modal__pane + .vg-modal__pane { border-inline-start: 1px solid var(--vg-line); }

@keyframes vg-modal-in   { from { opacity: 0; } }
@keyframes vg-modal-rise { from { opacity: 0; transform: translateY(8px); } }

@media (max-width: 767.98px) {
  .vg-modal__split { flex-direction: column; }
  .vg-modal__split > .vg-modal__pane + .vg-modal__pane {
    border-inline-start: 0;
    border-block-start: 1px solid var(--vg-line);
  }
}


/* ============================================================================
   7 · Reduced motion
   Nothing in the vg- layer honoured this before. Spinners keep turning because
   a frozen spinner reads as a hung request; everything decorative stops.
   ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  .vg-skel__thumb,
  .vg-skel__line {
    animation: none;
    background: var(--vg-surface-2);
  }

  .vg-modal,
  .vg-modal__box { animation: none; }

  .vg-btn { transition: none; }

  .vg-icon--spin,
  .vg-btn[data-loading="true"]::after { animation-duration: 1.8s; }
}

/* =============================================================================
   Bootstrap 5 spacing utilities the template never shipped
   -----------------------------------------------------------------------------
   assets/css/style.css is Bootstrap v4.5.3, which names its horizontal spacing
   utilities mr-* / ml-* / pr-* / pl-* and has no gap-* at all. Bootstrap 5 renamed
   them to the logical me-* / ms-* / pe-* / ps-* and added gap-*, and the codebase is
   written against those newer names — 484 times across 57 pages. Every one of
   them is currently a no-op, which is why icons all over the console sit hard
   against the text they label and flex rows have no spacing.

   Defined here rather than by rewriting the markup to mr-*, because the app is
   bilingual: the logical properties below flip correctly under [dir="rtl"],
   where Bootstrap 4's physical margin-right would not. !important matches the
   utility semantics of both frameworks.
   ============================================================================= */
.me-0 { margin-inline-end: 0 !important; }
.me-1 { margin-inline-end: .25rem !important; }
.me-2 { margin-inline-end: .5rem !important; }
.me-3 { margin-inline-end: 1rem !important; }
.me-4 { margin-inline-end: 1.5rem !important; }
.me-5 { margin-inline-end: 3rem !important; }
.me-auto { margin-inline-end: auto !important; }

.ms-0 { margin-inline-start: 0 !important; }
.ms-1 { margin-inline-start: .25rem !important; }
.ms-2 { margin-inline-start: .5rem !important; }
.ms-3 { margin-inline-start: 1rem !important; }
.ms-4 { margin-inline-start: 1.5rem !important; }
.ms-5 { margin-inline-start: 3rem !important; }
.ms-auto { margin-inline-start: auto !important; }

.pe-0 { padding-inline-end: 0 !important; }
.pe-1 { padding-inline-end: .25rem !important; }
.pe-2 { padding-inline-end: .5rem !important; }
.pe-3 { padding-inline-end: 1rem !important; }

.ps-0 { padding-inline-start: 0 !important; }
.ps-1 { padding-inline-start: .25rem !important; }
.ps-2 { padding-inline-start: .5rem !important; }
.ps-3 { padding-inline-start: 1rem !important; }

/* gap only takes effect on flex/grid containers, so it is inert everywhere else. */
.gap-0 { gap: 0 !important; }
.gap-1 { gap: .25rem !important; }
.gap-2 { gap: .5rem !important; }
.gap-3 { gap: 1rem !important; }
.gap-4 { gap: 1.5rem !important; }
.gap-5 { gap: 3rem !important; }


/* =============================================================================
   Bootstrap 5 → Bootstrap 4.5.3, part 2: components and remaining utilities
   -----------------------------------------------------------------------------
   The spacing block above covers me-* / ms-* / pe-* / ps-* / gap-*. A sweep of every
   class used in Pages/, Views/ and wwwroot/js against every selector that is
   effective STANDALONE in a loaded stylesheet (a rule like `.vg-auth-modal
   .btn-close` does not count — it only works inside that ancestor) turned up 24
   more Bootstrap 5 names across 56 files, every one of them inert:

     btn-close        29 files   an invisible 16x6 box where the close ✕ should be
     form-switch      16 files   renders as a plain square checkbox
     g-3 / g-4 / g-2  20 files   row gutters fall back to Bootstrap 4's 30px
     btn-close-white  11 files   (see btn-close)
     form-select      11 files   unstyled native control, light-mode drop arrow
     fw-bold           9 files   nothing marked bold was bold
     fw-semibold       5 files
     fs-3 / fs-5 / fs-6   7 files   headings never took their intended size
     d-grid            3 files   full-width button rows never became grids
     text-start / text-end   4 files
     visually-hidden   3 files   "Loading..." SHOWN next to spinners
     form-select-sm / -md    3 files
     top-0 / start-0 / end-0   1 file   an unpositioned fixed toast host
     dropdown-menu-end    1 file
     bg-opacity-50 / -75  1 file

   Written as logical properties throughout: the app is bilingual, and these are
   the Bootstrap 5 names precisely because Bootstrap 5 made them direction-aware.
   Bootstrap 4's physical mr-* / text-right equivalents would not flip under
   [dir="rtl"], which is the reason not to "fix" the markup instead.

   Page-scoped versions of some of these already exist — auth.css and
   vg-people.css both draw their own .btn-close, vg-webhooks.css its own select.
   Those keep winning on specificity and load order; this file is only the floor.
   ============================================================================= */

/* ---- Close button --------------------------------------------------------
   Bootstrap 5 draws the glyph as a background-image and ships nothing in the
   markup, so under Bootstrap 4 the element collapses to an empty box. Drawn to
   match the one auth.css already defines for the sign-in dialogs. */
.btn-close {
  box-sizing: content-box;
  inline-size: 14px;
  block-size: 14px;
  padding: 8px;
  border: 0;
  border-radius: 8px;
  opacity: .75;
  cursor: pointer;
  appearance: none;
  background: transparent
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23eef2f6' stroke-width='1.8' stroke-linecap='round'%3E%3Cpath d='M4 4l8 8M12 4l-8 8'/%3E%3C/svg%3E")
    center / 14px 14px no-repeat;
  transition: opacity .16s ease, background-color .16s ease;
}

.btn-close:hover { opacity: 1; background-color: rgba(255, 255, 255, .08); }
.btn-close:focus-visible { outline: 2px solid var(--vg-primary, #0090e7); outline-offset: 2px; }
.btn-close:disabled { opacity: .25; pointer-events: none; }

/* The stroke above is already light, so the Bootstrap 5 "white" modifier is a
   no-op rather than a second glyph. Several pages also carry their own
   `filter: invert(1)` for it, which this neutralises. */
.btn-close-white { filter: none; }


/* ---- Select --------------------------------------------------------------
   Bootstrap 4 calls this .custom-select and styles .form-control separately, so
   a .form-select kept the browser default: a light control with a light drop
   arrow, on a dark console. Sized to sit level with a .form-control beside it. */
.form-select {
  display: block;
  inline-size: 100%;
  block-size: calc(1.5em + .75rem + 2px);
  padding-block: .375rem;
  padding-inline: .75rem 2rem;
  font-size: .875rem;
  font-weight: 400;
  line-height: 1.5;
  color: var(--vg-text, #eef2f6);
  background-color: var(--vg-surface, #171b21);
  border: 1px solid var(--vg-line, #262b33);
  border-radius: var(--vg-radius-sm, 10px);
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239aa7b4' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .75rem center;
  background-size: 14px;
}

[dir="rtl"] .form-select { background-position: left .75rem center; }

.form-select:focus {
  outline: 0;
  border-color: var(--vg-primary, #0090e7);
  box-shadow: 0 0 0 3px var(--vg-primary-soft, rgba(0, 144, 231, .12));
}

.form-select:disabled { opacity: .55; cursor: not-allowed; }

/* Native option lists take the page background on some platforms and the system
   one on others; naming both keeps them readable either way. */
.form-select option { background-color: var(--vg-surface, #171b21); color: var(--vg-text, #eef2f6); }

.form-select-sm { block-size: calc(1.5em + .5rem + 2px); padding-block: .25rem; padding-inline: .5rem 1.75rem; font-size: .78rem; }
.form-select-lg { block-size: calc(1.5em + 1rem + 2px);  padding-block: .5rem;  padding-inline: 1rem 2.25rem;  font-size: 1rem; }
/* Not a Bootstrap size — one page invented it. Treated as the default. */
.form-select-md { block-size: calc(1.5em + .75rem + 2px); }


/* ---- Switch --------------------------------------------------------------
   Bootstrap 4's equivalent is .custom-switch, which needs different markup
   (.custom-control + .custom-control-label). Rather than rewrite 16 pages, the
   Bootstrap 5 contract — .form-check.form-switch > input.form-check-input — is
   implemented here. It has to override Bootstrap 4's
   `.form-check-input { position: absolute }`, which would otherwise take the
   track out of flow and leave the label overlapping it. */
/* Laid out with flex rather than Bootstrap's padding-plus-negative-margin idiom.

   That idiom only holds while the two halves agree: .form-switch supplied
   padding-inline-start:2.5em and the input pulled itself back with
   margin-inline-start:-2.5em. Any page that restyles .form-check breaks the pairing —
   Camera Details has an inline `.form-check { padding: .5rem }`, which (being in the
   document, and equal specificity) wins over this file and left the switch dragged
   32px outside its own card.

   It also depended on the label being inline, and style.css sets
   `.form-check .form-check-label { display: block }` at higher specificity than
   anything here, so the label sat on the line BELOW the toggle. Both are the
   misalignment in the bug report.

   Flex needs neither: the wrapper's padding stays whatever the page wants, and the
   label cannot drop to its own line because it is a flex item. */
.form-check.form-switch {
  display: flex;
  align-items: center;
  gap: .5em;
  padding-inline-start: 0;
}

/* Higher specificity than style.css's `.form-check .form-check-label`. */
.form-check.form-switch .form-check-label {
  display: inline-block;
  padding-inline-start: 0;
  margin-block-end: 0;
}

.form-switch .form-check-input {
  position: relative;
  float: none;
  flex: 0 0 auto;
  inline-size: 2em;
  block-size: 1em;
  margin-block-start: 0;
  margin-inline-start: 0;
  vertical-align: middle;
  appearance: none;
  cursor: pointer;
  background-color: #2a313a;
  border: 1px solid var(--vg-line, #262b33);
  border-radius: 2em;
  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='%239aa7b4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: left center;
  background-size: contain;
  transition: background-position .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out;
}

.form-switch .form-check-input:checked {
  background-color: var(--vg-primary, #0090e7);
  border-color: var(--vg-primary, #0090e7);
  background-position: right center;
  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='%23ffffff'/%3E%3C/svg%3E");
}

/* The knob travels toward the inline end, which is leftward in Arabic. */
[dir="rtl"] .form-switch .form-check-input         { background-position: right center; }
[dir="rtl"] .form-switch .form-check-input:checked { background-position: left center; }

.form-switch .form-check-input:focus-visible {
  outline: 2px solid var(--vg-primary, #0090e7);
  outline-offset: 2px;
}

.form-switch .form-check-input:disabled { opacity: .5; cursor: not-allowed; }


/* ---- Range and label -----------------------------------------------------
   Neither .form-range nor .form-label exists in Bootstrap 4 (its names are
   .custom-range and a bare <label>), and neither is defined anywhere in this
   app's CSS. An undefined .form-range falls back to the native control's
   intrinsic width — about 129px — while the scale row rendered beside it with
   .d-flex.justify-content-between spans the full column, so the "60s" end label
   sat far to the right of a stub slider. That is the second half of the Camera
   Details misalignment.

   The native control is kept rather than re-skinned: the markup sets
   `accent-color`, which themes track and thumb in one declaration and avoids
   maintaining ::-webkit-slider-thumb and ::-moz-range-thumb separately. */
.form-range {
  inline-size: 100%;
  block-size: 1.5rem;
  padding: 0;
  background: transparent;
  cursor: pointer;
  vertical-align: middle;
}

.form-range:disabled { opacity: .5; cursor: not-allowed; }

.form-range:focus-visible {
  outline: 2px solid var(--vg-primary, #0090e7);
  outline-offset: 2px;
}

/* Matches what the template's bare `label` rule already does, so this changes
   nothing visually — it just makes the class mean something when it is put on a
   <span> or <div>, which is what Bootstrap 5 markup assumes. */
.form-label {
  display: inline-block;
  margin-block-end: .5rem;
}


/* ---- Row gutters ---------------------------------------------------------
   Bootstrap 4 has no g-*; its rows always carry the fixed 15px column padding,
   so `row g-3` asked for 16px and silently got 30px. Scoped to .row so only the
   rows that opt in change, and driven by --vg-gutter-* rather than Bootstrap
   5's --bs-gutter-* so it cannot collide if the template is ever upgraded. */
.row[class*="g-"]     { margin-inline: calc(var(--vg-gutter-x, 1.5rem) * -.5); margin-block-start: calc(var(--vg-gutter-y, 0rem) * -1); }
.row[class*="g-"] > * { padding-inline: calc(var(--vg-gutter-x, 1.5rem) * .5);  margin-block-start: var(--vg-gutter-y, 0rem); }

.row.g-0 { --vg-gutter-x: 0rem;   --vg-gutter-y: 0rem; }
.row.g-1 { --vg-gutter-x: .25rem; --vg-gutter-y: .25rem; }
.row.g-2 { --vg-gutter-x: .5rem;  --vg-gutter-y: .5rem; }
.row.g-3 { --vg-gutter-x: 1rem;   --vg-gutter-y: 1rem; }
.row.g-4 { --vg-gutter-x: 1.5rem; --vg-gutter-y: 1.5rem; }
.row.g-5 { --vg-gutter-x: 3rem;   --vg-gutter-y: 3rem; }

.row.gx-0 { --vg-gutter-x: 0rem; }   .row.gx-1 { --vg-gutter-x: .25rem; } .row.gx-2 { --vg-gutter-x: .5rem; }
.row.gx-3 { --vg-gutter-x: 1rem; }   .row.gx-4 { --vg-gutter-x: 1.5rem; } .row.gx-5 { --vg-gutter-x: 3rem; }

.row.gy-0 { --vg-gutter-y: 0rem; }   .row.gy-1 { --vg-gutter-y: .25rem; } .row.gy-2 { --vg-gutter-y: .5rem; }
.row.gy-3 { --vg-gutter-y: 1rem; }   .row.gy-4 { --vg-gutter-y: 1.5rem; } .row.gy-5 { --vg-gutter-y: 3rem; }


/* ---- Type ---------------------------------------------------------------- */
.fw-lighter  { font-weight: lighter !important; }
.fw-light    { font-weight: 300 !important; }
.fw-normal   { font-weight: 400 !important; }
.fw-medium   { font-weight: 500 !important; }
.fw-semibold { font-weight: 600 !important; }
.fw-bold     { font-weight: 700 !important; }
.fw-bolder   { font-weight: bolder !important; }

.fst-italic { font-style: italic !important; }
.fst-normal { font-style: normal !important; }

/* Bootstrap 5's fs-1..6 track its h1..h6 sizes. */
.fs-1 { font-size: 2.5rem !important; }
.fs-2 { font-size: 2rem !important; }
.fs-3 { font-size: 1.75rem !important; }
.fs-4 { font-size: 1.5rem !important; }
.fs-5 { font-size: 1.25rem !important; }
.fs-6 { font-size: 1rem !important; }

/* Bootstrap 4 only ships the physical text-left/right. These follow the page. */
.text-start { text-align: start !important; }
.text-end   { text-align: end !important; }


/* ---- Layout -------------------------------------------------------------- */
.d-grid        { display: grid !important; }
.d-inline-grid { display: inline-grid !important; }

.float-start { float: inline-start !important; }
.float-end   { float: inline-end !important; }

/* Offsets. Bootstrap 5 names these start/end so they follow the writing
   direction; Bootstrap 4 shipped no positioning utilities at all, which left a
   `position-fixed top-0 end-0` toast host sitting in normal flow. */
.top-0       { top: 0 !important; }
.top-50      { top: 50% !important; }
.top-100     { top: 100% !important; }
.bottom-0    { bottom: 0 !important; }
.bottom-50   { bottom: 50% !important; }
.bottom-100  { bottom: 100% !important; }
.start-0     { inset-inline-start: 0 !important; }
.start-50    { inset-inline-start: 50% !important; }
.start-100   { inset-inline-start: 100% !important; }
.end-0       { inset-inline-end: 0 !important; }
.end-50      { inset-inline-end: 50% !important; }
.end-100     { inset-inline-end: 100% !important; }

.translate-middle   { transform: translate(-50%, -50%) !important; }
.translate-middle-x { transform: translateX(-50%) !important; }
.translate-middle-y { transform: translateY(-50%) !important; }

.dropdown-menu-end { inset-inline-start: auto !important; inset-inline-end: 0 !important; }


/* ---- Screen-reader-only --------------------------------------------------
   Bootstrap 4 calls this .sr-only. Until now every
   `<span class="visually-hidden">Loading...</span>` inside a spinner rendered
   as visible text next to it. */
.visually-hidden,
.visually-hidden-focusable:not(:focus):not(:focus-within) {
  position: absolute !important;
  inline-size: 1px !important;
  block-size: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}


/* ---- Background opacity --------------------------------------------------
   Bootstrap 5 builds these from --bs-*-rgb channels that Bootstrap 4's flat
   .bg-* utilities never expose, so there is no generic shim. Only the pairs
   actually in use are defined; add the combination if a page needs another. */
.bg-dark.bg-opacity-50 { background-color: rgba(0, 0, 0, .5) !important; }
.bg-dark.bg-opacity-75 { background-color: rgba(0, 0, 0, .75) !important; }

/* ── Licence banner ───────────────────────────────────────────────────────────
   Slim strip above the page body on on-premise deployments. Hidden by default
   (.d-none) and revealed by vg-license-banner.js only when there is something
   worth saying. Colours are literal rather than tokenised because this must
   still render on the handful of pages that set Layout = null. */
.vg-license-banner {
    display: flex;
    align-items: center;
    gap: .55rem;
    margin: 0 0 1rem;
    padding: .6rem 1rem;
    border-radius: .4rem;
    border: 1px solid transparent;
    font-size: .875rem;
    line-height: 1.4;
}

.vg-license-banner i { font-size: 1.05rem; flex: none; }

.vg-license-banner--info    { background: #eaf3fb; border-color: #bcd9f0; color: #1b4f79; }
.vg-license-banner--warning { background: #fdf3e2; border-color: #f0d9a8; color: #7a5310; }
.vg-license-banner--danger  { background: #fbeaea; border-color: #f0bcbc; color: #7a1b1b; }

[dir="rtl"] .vg-license-banner { flex-direction: row; }
