/*
 * The shared vocabulary for every VigiBus screen.
 *
 * Structure is taken from the two reference products in
 * docs/reference-screensots/ — bsj-monitoring-system (1).html for the shell,
 * lists, detail panel and dialog; roadcast-parent-tracking.html for the cards,
 * labelled values and map. Both declare a token set separating brand colour
 * from status colour, and both make no external requests at all. The palette
 * below is ours.
 *
 * Loaded by all thirteen pages. Precached in sw.js, which is hand-written —
 * shell.test.js guards that, because nothing else does.
 */

:root {
  /* brand / chrome — adopted from the VigiTrack design (oklch). The chrome runs
     as a navy→indigo gradient; --vb-navy stays the solid base for anything that
     references it directly. */
  --vb-navy:       oklch(0.28 0.058 250);
  --vb-navy-soft:  oklch(0.33 0.07 258);
  --vb-navy-2:     oklch(0.36 0.104 273);
  --vb-indigo:     oklch(0.36 0.104 273);
  --vb-navy-grad:  linear-gradient(100deg, oklch(0.28 0.058 250), oklch(0.36 0.104 273));
  --vb-primary:    oklch(0.34 0.098 272);   /* solid navy/indigo buttons */
  --vb-on-navy:    oklch(0.985 0.004 255);  /* text/icons on chrome */
  --vb-orange:     oklch(0.75 0.178 55);    /* accent: CTA, selection, avatar, Stale */
  --vb-orange-grad:linear-gradient(100deg, oklch(0.75 0.178 55), oklch(0.80 0.16 75));

  /* surface */
  --vb-page:    oklch(0.945 0.008 255);
  --vb-card:    oklch(0.995 0.002 255);
  --vb-surface: oklch(0.93 0.012 255);
  --vb-border:  oklch(0.89 0.012 255);
  --vb-text:    oklch(0.26 0.045 258);
  --vb-dim:     oklch(0.53 0.03 258);

  /* elevation */
  --vb-shadow:    0 1px 2px oklch(0.28 0.058 250 / .06), 0 8px 24px -12px oklch(0.28 0.058 250 / .25);
  --vb-shadow-lg: 0 2px 6px oklch(0.28 0.058 250 / .08), 0 18px 40px -18px oklch(0.28 0.058 250 / .4);

  /* status — by meaning. Stale is orange, by design; Idle stays amber (--vb-warn). */
  --vb-ok:      oklch(0.63 0.15 155);
  --vb-warn:    oklch(0.79 0.15 85);
  --vb-stale:   var(--vb-orange);
  --vb-late:    oklch(0.585 0.22 25);
  --vb-info:    oklch(0.62 0.13 235);
  --vb-early:   oklch(0.60 0.118 185);
  --vb-neutral: oklch(0.70 0.03 255);

  --vb-radius:      12px;
  --vb-radius-sm:   8px;
  --vb-radius-lg:   16px;
  --vb-radius-pill: 999px;
  --vb-gap:  12px;
  --vb-font: 14px/1.5 Helvetica, "Helvetica Neue", Arial, sans-serif;
  --vb-label:600 12px/1.3 Helvetica, Arial, sans-serif;

  /* Reserved strip along the bottom of a map for basemap attribution. */
  --vb-map-attr: 28px;
}

/* ==================== VigiTrack shell (sidebar + top bar) ==================== */
/* Scoped box-sizing for the shell's own chrome; the global reset is a later
   Foundation step (it needs a full per-page audit). */
.vb-sidebar, .vb-sidebar *, .vb-topbar, .vb-topbar *, .vb-menu, .vb-menu * { box-sizing: border-box; }

/* A fixed-viewport shell: the app fills the window and its own regions scroll,
   rather than the window. This restores a definite height for pages whose map
   fills the screen (they assume html,body { height:100% }), while ordinary pages
   scroll inside .vb-content. */
/* height twice, deliberately. On iOS Safari 100vh is the LARGE viewport — the
   height the window would have if the toolbars retracted — so a 100vh app is
   drawn about ninety pixels taller than what you can actually see, and
   overflow:hidden makes that strip unreachable rather than scrollable. 100svh
   is the small viewport, which is what is on screen with the toolbars showing.
   Not 100dvh: that changes as the toolbars slide, which resizes the map on
   every scroll gesture and needs a Leaflet invalidateSize() to not tear. The
   100vh line stays first as the fallback for anything older than Safari 15.4. */
.vb-app {
  display: flex;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  background: var(--vb-page);
}

.vb-sidebar {
  flex: 0 0 248px; width: 248px;
  align-self: flex-start; position: sticky; top: 0; height: 100vh; height: 100svh;
  display: flex; flex-direction: column;
  background: var(--vb-navy-grad); color: var(--vb-on-navy); font: var(--vb-font);
}

/* overflow:hidden so a half-updated cache (fresh markup, stale CSS) can never
   let the logo bleed across the page again. */
.vb-side-brand { display: flex; align-items: center; gap: 10px; padding: 14px 16px; color: var(--vb-on-navy); text-decoration: none; overflow: hidden; }
.vb-brand-full { height: 30px; width: auto; max-width: 100%; display: block; }
.vb-brand-mark { height: 28px; width: 28px; display: none; }

.vb-side-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 6px 10px 12px; }
.vb-side-group { margin-bottom: 8px; }
.vb-side-label {
  font: var(--vb-label); text-transform: uppercase; letter-spacing: .06em;
  color: color-mix(in oklab, var(--vb-on-navy) 50%, transparent); padding: 10px 10px 6px;
}

.vb-nav-item {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px; margin: 1px 0;
  border-radius: 10px; min-height: 40px; font-size: 14px; text-decoration: none;
  color: color-mix(in oklab, var(--vb-on-navy) 78%, transparent);
}
.vb-nav-item:hover { background: color-mix(in oklab, var(--vb-on-navy) 10%, transparent); color: var(--vb-on-navy); }
.vb-nav-item[aria-current="page"] {
  background: color-mix(in oklab, var(--vb-on-navy) 15%, transparent);
  color: var(--vb-on-navy); font-weight: 600;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--vb-on-navy) 10%, transparent);
}
.vb-nav-item.is-soon { color: color-mix(in oklab, var(--vb-on-navy) 40%, transparent); cursor: default; }
.vb-nav-item.is-soon:hover { background: none; }
/* 11px, not 10. Ten was the smallest text anywhere in the product and it sits in
   the sidebar of all fifteen staff screens, so it was the one thing every page
   failed the phone audit on. */
.vb-soon {
  margin-left: auto; font-style: normal; font-size: 11px; text-transform: uppercase;
  letter-spacing: .04em; padding: 2px 6px; border-radius: 999px;
  background: color-mix(in oklab, var(--vb-on-navy) 12%, transparent);
}
.vb-ic { width: 18px; height: 18px; flex: 0 0 18px; }

.vb-side-foot {
  padding: 10px 16px; font-size: 11px;
  color: color-mix(in oklab, var(--vb-on-navy) 55%, transparent);
  border-top: 1px solid color-mix(in oklab, var(--vb-on-navy) 12%, transparent);
}

.vb-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }

/* Ordinary pages scroll here. Full-height pages (Monitor, Playback) opt into
   fill mode via <meta name="vb-layout" content="fill">, and staff-nav.js adds
   .is-fill: then the region does not scroll and lays its content out as a
   column, so a child with flex:1 (the map) fills it. */
.vb-content { flex: 1; min-width: 0; min-height: 0; overflow: auto; }
.vb-content.is-fill { overflow: hidden; display: flex; flex-direction: column; }

.vb-topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: 12px; padding: 10px 16px; min-height: 56px;
  background: var(--vb-navy-grad); color: var(--vb-on-navy); font: var(--vb-font);
}

.vb-side-toggle, .vb-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border: 0; border-radius: 10px;
  background: transparent; color: var(--vb-on-navy); cursor: pointer;
}
.vb-side-toggle:hover, .vb-icon-btn:hover { background: color-mix(in oklab, var(--vb-on-navy) 12%, transparent); }
.vb-icon-btn .vb-ic, .vb-side-toggle .vb-ic { width: 20px; height: 20px; }

.vb-breadcrumb { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 14px; color: color-mix(in oklab, var(--vb-on-navy) 60%, transparent); }
.vb-crumb-sep { width: 14px; height: 14px; opacity: .7; }
.vb-crumb-here {
  padding: 6px 12px; border-radius: 999px; font-weight: 600; color: var(--vb-on-navy);
  background: color-mix(in oklab, var(--vb-on-navy) 15%, transparent);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 42vw;
}

.vb-topbar-tools { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.vb-tool { position: relative; }

.vb-search {
  display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 999px;
  background: color-mix(in oklab, var(--vb-on-navy) 12%, transparent); color: var(--vb-on-navy);
}
.vb-search .vb-ic { width: 16px; height: 16px; opacity: .8; }
.vb-search input { width: 180px; background: transparent; border: 0; outline: 0; color: var(--vb-on-navy); font: inherit; }
.vb-search input::placeholder { color: color-mix(in oklab, var(--vb-on-navy) 60%, transparent); }

.vb-school {
  display: flex; align-items: center; gap: 8px; padding: 6px 12px 6px 8px; border-radius: 999px;
  border: 1px solid color-mix(in oklab, var(--vb-on-navy) 15%, transparent);
  background: color-mix(in oklab, var(--vb-on-navy) 8%, transparent);
  color: color-mix(in oklab, var(--vb-on-navy) 85%, transparent); font-size: 13px;
}
.vb-school .vb-ic { width: 16px; height: 16px; }

.vb-profile { position: relative; }
.vb-profile-btn {
  display: flex; align-items: center; gap: 9px; padding: 5px 10px 5px 6px;
  border: 0; border-radius: 999px; cursor: pointer; font: inherit; color: var(--vb-on-navy);
  background: color-mix(in oklab, var(--vb-on-navy) 12%, transparent);
}
.vb-profile-btn:hover { background: color-mix(in oklab, var(--vb-on-navy) 18%, transparent); }
.vb-avatar {
  width: 30px; height: 30px; flex: 0 0 30px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--vb-orange-grad); color: #fff; font-size: 12px; font-weight: 800;
}
.vb-profile-id { display: flex; flex-direction: column; line-height: 1.15; text-align: left; }
.vb-profile-name { font-size: 13px; font-weight: 700; white-space: nowrap; }
.vb-profile-role { font-size: 11px; white-space: nowrap; color: color-mix(in oklab, var(--vb-on-navy) 70%, transparent); }
.vb-caret { width: 14px; height: 14px; opacity: .7; }

.vb-menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60; display: none;
  min-width: 210px; padding: 6px; background: var(--vb-card); color: var(--vb-text);
  border-radius: 12px; box-shadow: var(--vb-shadow-lg);
}
.is-menu-open > .vb-menu { display: block; }
.vb-menu button, .vb-menu a {
  display: block; width: 100%; text-align: left; padding: 9px 12px; border: 0;
  border-radius: 8px; background: transparent; color: var(--vb-text); font: inherit;
  font-size: 14px; cursor: pointer; text-decoration: none;
}
.vb-menu button:hover, .vb-menu a:hover { background: var(--vb-surface); }
.vb-menu a[aria-current="page"] { color: var(--vb-primary); font-weight: 600; }
.vb-menu .vb-signout { color: var(--vb-late); font-weight: 600; }
.vb-menu-sep { border: 0; border-top: 1px solid var(--vb-border); margin: 5px 4px; }
.vb-menu-head { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--vb-dim); padding: 6px 12px 4px; }
.vb-menu-empty { padding: 10px 12px; color: var(--vb-dim); font-size: 13px; }
.vb-menu-who { padding: 8px 12px 6px; display: flex; flex-direction: column; line-height: 1.25; }
.vb-menu-who b { font-size: 13.5px; font-weight: 700; }
.vb-menu-who span { font-size: 11.5px; color: var(--vb-dim); }

.vb-side-backdrop { display: none; }

/* rail: desktop collapse to icons */
.vb-app.is-rail .vb-sidebar { flex-basis: 66px; width: 66px; }
.vb-app.is-rail .vb-brand-full,
.vb-app.is-rail .vb-side-label,
.vb-app.is-rail .vb-nav-item span,
.vb-app.is-rail .vb-soon,
.vb-app.is-rail .vb-side-foot { display: none; }
.vb-app.is-rail .vb-brand-mark { display: block; }
.vb-app.is-rail .vb-nav-item { justify-content: center; }
.vb-app.is-rail .vb-side-brand { justify-content: center; }

/*
 * Below 1200: the sidebar still takes its full 248px here -- the drawer does
 * not arrive until 768 -- so the bar has to give something up, and it gives up
 * labels to keep tools. 1200 rather than 1024 because 1024 was measured, not
 * assumed: the full bar needs 847px and a 1024 window leaves it 776. The "VigiTrack" root repeats the wordmark two
 * centimetres to its left; the school pill repeats the sidebar footer; the
 * profile's name and role move into the menu the avatar opens.
 *
 * Without this the breadcrumb is squeezed to 41px and the page name -- the one
 * thing in the bar that says where you are -- ellipsises to a letter and a dot.
 * Nothing scrolls while it happens, so the sideways-scroll audit passes;
 * tools/browser-checks/shell-chrome.py measures it instead.
 */
@media (max-width: 1199px) {
  .vb-search input { width: 120px; }
  .vb-crumb-root, .vb-crumb-sep, .vb-school, .vb-profile-id { display: none; }
}

/* phone: the sidebar becomes a drawer, and a full-height page becomes a page */
@media (max-width: 767px) {
  /*
   * Fill mode stops being a frame and becomes a page.
   *
   * `is-fill` means "do not scroll; the map absorbs whatever height is left".
   * That is right on a desktop, where there is height to spare, and it is how
   * both map screens were unusable on a phone: every other band is fixed, so
   * the map is the only thing that gives, and once the fixed bands exceed the
   * screen the map is squeezed past nothing and the rest is clipped away
   * unreachably. Measured at 390x844: Monitor's map got 31% of the screen and
   * its bus list none at all; at 360x640 the map got 3% and three filters were
   * off-screen entirely.
   *
   * So below 768 the region scrolls, and each page gives its map an explicit
   * height instead of the leftovers. Nothing is ever clipped, and the map is
   * the size it was asked to be rather than the size nothing else wanted.
   */
  .vb-content.is-fill { overflow-y: auto; }
  .vb-sidebar {
    position: fixed; left: 0; top: 0; z-index: 46; height: 100vh; height: 100svh;
    transform: translateX(-100%); transition: transform .2s ease; box-shadow: var(--vb-shadow-lg);
  }
  .vb-app.is-drawer-open .vb-sidebar { transform: translateX(0); }
  .vb-app.is-drawer-open .vb-side-backdrop { display: block; position: fixed; inset: 0; z-index: 45; background: rgba(15, 23, 42, .45); }
  .vb-search, .vb-school, .vb-profile-id, .vb-crumb-root { display: none; }
}
/* Leaflet draws its zoom buttons at 30px, which is under a fingertip on every
   map screen we have. Its own stylesheet is vendored and left alone; this is the
   one place that has to override it, so it sits with the shell rather than being
   repeated on each map page. */
@media (max-width: 700px) {
  .leaflet-control-zoom a { width: 40px !important; height: 40px !important; line-height: 40px !important; }
}

@media (prefers-reduced-motion: reduce) { .vb-sidebar { transition: none; } }
/* ================== end VigiTrack shell ================== */

/*
 * Why brand orange is absent from every status rule below.
 *
 * The reference product's warning amber is #f5972a and our brand orange is
 * #e07b1f — nearly the same hue. A reader given both cannot tell chrome from a
 * caution state. So caution is --vb-warn, deliberately deeper and browner, and
 * brand orange appears only in chrome. shell.test.js enforces this.
 */

/* ============================ 2. card =========================== */

/* A shadow rather than a border, following the reference. A hairline border
   around a white card on a grey page reads as a table cell; the shadow reads as
   a sheet of paper, which is what these are. */
.vb-card {
  background: var(--vb-card);
  border-radius: var(--vb-radius);
  box-shadow: var(--vb-shadow);
  padding: 16px 18px;
  font: var(--vb-font);
  color: var(--vb-text);
}

.vb-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  padding: 16px;
}

.vb-card-head {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 16px;
  font-weight: 700;
  padding-bottom: 10px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--vb-border);
}

/* ====================== 3. labelled value ======================= */

.vb-pair { display: flex; justify-content: space-between; gap: 14px; }

.vb-lab {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--vb-dim);
  font-size: 13px;
}

.vb-val { font-size: 16px; font-weight: 700; line-height: 1.35; }

/* ============================ 4. chip =========================== */

/* Tinted ground, saturated text of the same hue — the reference's pattern. The
   colour is the meaning; there is no default-coloured chip on purpose. */
/*
 * The hidden attribute means hidden, whatever else a class says.
 *
 * Every primitive here sets a display — .vb-chip is inline-flex, .vb-modal is
 * flex — and each of those beats the browser's own [hidden] { display: none },
 * because a class selector outranks a bare attribute one. So markup that says
 * hidden renders anyway, and the author sees a control that will not go away
 * and no reason for it.
 *
 * Twice on 19 August 2026: a camera button visible before a bus was selected,
 * and a panel that would not open because it had borrowed a class whose display
 * it then had to fight. The same shape as the bare button{} rule that beat three
 * class rules in one day in August — specificity here is a trap the whole file
 * walks into, and this is the one line that closes it.
 */
[hidden] { display: none !important; }

.vb-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: var(--vb-font);
  font-size: 12.5px;
  font-weight: 600;
  padding: 4px 12px;
  border-radius: 14px;
  background: #eef1f5;
  color: var(--vb-dim);
  border: 0;

  /* Stated, not inherited. Several pages carry a bare `button { width: 100% }`
     for their full-width primary actions, and a chip is not one of those — it
     hugs its label. A class does not beat an element rule for a property it
     never mentions. */
  width: auto;
  flex: 0 0 auto;
  align-self: center;
}

/* Tinted ground, darker same-hue text — derived from the status tokens so the
   later dark palette shifts them for free. Stale is orange (by decision); Idle
   is amber (--vb-warn). */
.vb-chip.is-ok      { background: color-mix(in oklab, var(--vb-ok)      15%, transparent); color: color-mix(in oklab, var(--vb-ok)      78%, black); }
.vb-chip.is-warn    { background: color-mix(in oklab, var(--vb-warn)    24%, transparent); color: color-mix(in oklab, var(--vb-warn)    58%, black); }
.vb-chip.is-stale   { background: color-mix(in oklab, var(--vb-stale)   18%, transparent); color: color-mix(in oklab, var(--vb-stale)   70%, black); }
.vb-chip.is-late    { background: color-mix(in oklab, var(--vb-late)    15%, transparent); color: color-mix(in oklab, var(--vb-late)    82%, black); }
.vb-chip.is-info    { background: color-mix(in oklab, var(--vb-info)    15%, transparent); color: color-mix(in oklab, var(--vb-info)    82%, black); }
.vb-chip.is-early   { background: color-mix(in oklab, var(--vb-early)   16%, transparent); color: color-mix(in oklab, var(--vb-early)   76%, black); }
.vb-chip.is-neutral { background: color-mix(in oklab, var(--vb-neutral) 24%, transparent); color: var(--vb-dim); }

/* A chip that reveals something is a control and must look and behave like one.
   A chip that reveals nothing must not invite the press. */
button.vb-chip:not(:disabled) { cursor: pointer; }
button.vb-chip:disabled { cursor: default; }
button.vb-chip:focus-visible { outline: 2px solid var(--vb-info); outline-offset: 2px; }

/* ============================ 5. tile =========================== */

.vb-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 14px;
}

.vb-tile {
  background: var(--vb-card);
  border-radius: var(--vb-radius);
  box-shadow: var(--vb-shadow);
  padding: 14px 16px;
  font: var(--vb-font);
}

.vb-tile .vb-tile-label {
  font-size: 12px;
  color: var(--vb-dim);
  margin-bottom: 4px;
  text-transform: uppercase;
  letter-spacing: .03em;
}

.vb-tile .vb-tile-value { font-size: 21px; font-weight: 800; color: var(--vb-text); }

}

.vb-tile .vb-tile-value.is-ok      { color: var(--vb-ok); }
.vb-tile .vb-tile-value.is-warn    { color: var(--vb-warn); }
.vb-tile .vb-tile-value.is-stale   { color: var(--vb-stale); }
.vb-tile .vb-tile-value.is-late    { color: var(--vb-late); }
.vb-tile .vb-tile-value.is-info    { color: var(--vb-info); }
.vb-tile .vb-tile-value.is-early   { color: var(--vb-early); }
.vb-tile .vb-tile-value.is-neutral { color: var(--vb-neutral); }

.vb-tile .vb-tile-unit {
  font-size: 13px;
  font-weight: 600;
  color: var(--vb-dim);
  margin-left: 3px;
}

/* ============================ 6. list =========================== */

.vb-list { display: flex; flex-direction: column; font: var(--vb-font); }

.vb-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 8px;
  border-radius: 8px;
  transition: background .12s;
}

.vb-row:hover { background: #f4f7fd; }
.vb-row.is-selected { background: #e9f1ff; }

.vb-row-name {
  font-size: 13.5px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.vb-row-meta { font-size: 11.5px; color: var(--vb-dim); }
.vb-row-end { margin-left: auto; display: flex; align-items: center; gap: 8px; }

.vb-dot { width: 9px; height: 9px; border-radius: 50%; background: #b9c0cf; flex-shrink: 0; }
.vb-dot.is-ok      { background: var(--vb-ok); }
.vb-dot.is-warn    { background: var(--vb-warn); }
.vb-dot.is-stale   { background: var(--vb-stale); }
.vb-dot.is-late    { background: var(--vb-late); }
.vb-dot.is-info    { background: var(--vb-info); }
.vb-dot.is-early   { background: var(--vb-early); }
.vb-dot.is-neutral { background: var(--vb-neutral); }

/* =========================== 7. detail ========================== */

/* A sheet that rises over the page rather than a new page. Selecting a bus on
   Monitor loses the operator's context every time it navigates away. */
.vb-detail {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--vb-card);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -4px 24px rgba(20, 30, 60, .18);
  z-index: 18;
  transform: translateY(110%);
  transition: transform .28s ease;
  font: var(--vb-font);
}

.vb-detail.is-open { transform: translateY(0); }

.vb-detail-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 18px;
  border-bottom: 1px solid var(--vb-border);
  font-size: 16px;
  font-weight: 700;
}

.vb-detail-body { padding: 16px 18px 18px; }

@media (prefers-reduced-motion: reduce) {
  .vb-detail { transition: none; }
}

/* ============================ 8. map ============================ */

.vb-map-shell { position: relative; overflow: hidden; }

/*
 * Contain Leaflet's own stacking.
 *
 * Leaflet numbers its panes z-index 200–700, and with the map container left at
 * z-index:auto those numbers resolve in the page's *root* stacking context —
 * above the bar (z-index 30) and anything inside it. A menu opened from the bar
 * then renders behind the map, visible only where it clears the map's edge.
 * Isolating the container keeps those panes to themselves, so the z-index scale
 * on this page (bar 30, detail 18, controls 10) means what it says.
 */
.leaflet-container { isolation: isolate; }

/*
 * A map paints above anything that merely follows it.
 *
 * `isolation` above stops Leaflet's 200–700 escaping into the page. It does not
 * stop the map itself winning, and that is a different bug: Leaflet positions
 * its own container, and a positioned box paints above an in-flow one whatever
 * the source order says. So a bar laid out *after* a map is painted *behind* it
 * wherever the two overlap — and they overlap the moment a phone squeezes the
 * map's parent below the map's own min-height.
 *
 * That is not hypothetical. On a 390x664 phone Playback's whole transport —
 * play, step, scrub — was drawn inside the map's box and buried under it:
 * invisible, and tapping the play button panned the map. It measured perfectly:
 * getBoundingClientRect reports the box, which was exactly where it should be.
 * Only a hit test finds it, which is why tools/browser-checks/phone-map-room.py
 * asks elementFromPoint rather than measuring.
 *
 * `.vb-above-map` is the fix and it is a promise, not a nudge: put it on
 * whatever carries the controls for a map, and it stays reachable however badly
 * the space runs out.
 */
.vb-above-map { position: relative; z-index: 1; }

/* Controls sit above the reserved attribution strip rather than beside it. The
   reference keeps its Recentre button clear of the credit by geometry — a
   centred button narrow enough to miss a right-aligned line — which holds until
   somebody changes a width. Reserving the strip holds regardless. */
.vb-map-ctl {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(var(--vb-map-attr) + 12px);
  z-index: 10;
  display: flex;
  gap: 8px;
}

.vb-map-attr {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--vb-map-attr);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 10px;
  z-index: 9;
  font: var(--vb-font);
  font-size: 11.5px;
  color: #444;
  pointer-events: none;
}

.vb-map-attr a { pointer-events: auto; color: var(--vb-info); text-decoration: none; margin-left: 8px; }

/* =========================== 9. modal =========================== */

.vb-modal {
  position: fixed;
  left: 50%;
  top: 74px;
  transform: translateX(-50%);
  width: min(1060px, calc(100vw - 60px));
  max-height: calc(100vh - 100px);
  overflow-y: auto;
  background: var(--vb-card);
  border-radius: var(--vb-radius);
  box-shadow: 0 16px 55px rgba(15, 25, 50, .38);
  z-index: 40;
  display: none;
  flex-direction: column;
  font: var(--vb-font);
}

.vb-modal.is-open { display: flex; }

.vb-modal-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 15px 20px;
  border-bottom: 1px solid var(--vb-border);
  font-size: 15px;
  font-weight: 700;
}

/* ==================== 10. segmented control ===================== */

.vb-seg { display: inline-flex; gap: 6px; font: var(--vb-font); }

.vb-seg button {
  border: 1px solid var(--vb-border);
  background: var(--vb-card);
  color: var(--vb-text);
  font: inherit;
  padding: 7px 16px;
  border-radius: 8px;
  cursor: pointer;
}

.vb-seg button[aria-pressed="true"] {
  background: var(--vb-navy);
  border-color: var(--vb-navy);
  color: #fff;
  font-weight: 600;
}

/* ========================= 11. progress ========================= */

.vb-progress { display: flex; gap: 3px; align-items: stretch; height: 8px; }

.vb-progress span {
  flex: 1;
  border-radius: 2px;
  background: var(--vb-border);
}

.vb-progress span.is-served  { background: var(--vb-ok); }
.vb-progress span.is-current { background: var(--vb-info); }
.vb-progress span.is-late    { background: var(--vb-late); }
.vb-progress span.is-warn    { background: var(--vb-warn); }

/* ========================== 12. empty =========================== */

/* A count of zero is a fact and renders as "0". Two tiles on the fleet wall
   render a label with no figure at all, which reads as a screen that failed to
   load rather than as a fleet with nothing in that state. */
.vb-empty { color: var(--vb-dim); font: var(--vb-font); font-style: normal; }

.vb-empty-note { color: var(--vb-dim); font-size: 13px; padding: 12px 0; }

/* ======================== responsive ============================ */

/* 700px is where Trips overflows today. The references break at 760 and 1100;
   ours is narrower because our staff screens are primarily operator displays
   and the links are worth keeping inline for as long as they fit. */
@media (max-width: 700px) {
  .vb-bar-links {
    display: none;
    position: absolute;
    top: 56px;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    height: auto;
    background: var(--vb-navy);
    padding-bottom: 8px;
  }

  .vb-bar-links.is-open { display: flex; }
  .vb-bar-links a { height: 44px; padding: 0 16px; }

  .vb-bar-menu {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: #fff;
    font-size: 20px;
    cursor: pointer;
  }

  .vb-cards { grid-template-columns: 1fr; gap: 14px; padding: 14px; }
}

/* ---- Parent share-link: monitor control, modal and copy buttons ---------- */
/* Explicit selectors, so these never fall back to the bare button{} element
   rule every page carries (which has repeatedly won and broken controls). */
.vc-share { display: flex; gap: 8px; padding: 8px 0; flex-wrap: wrap; }
.vc-share .share-btn { border: 0; border-radius: 6px; padding: 8px 14px; font-size: 14px;
                       font-weight: 600; cursor: pointer; }
.vc-share .share-btn.primary { background: var(--vb-orange); color: #fff; }
.vc-share .share-btn.quiet { background: var(--vb-navy-soft, #24406a); color: #fff; }

.share-modal-back { position: fixed; inset: 0; background: rgba(17, 34, 68, .45);
                    display: flex; align-items: center; justify-content: center; z-index: 4000; }
.share-modal { background: #fff; color: var(--vb-text); border-radius: 10px; width: min(440px, 92vw);
               max-height: 88vh; overflow: auto; box-shadow: 0 12px 40px rgba(0,0,0,.3); }
.share-modal-head { display: flex; justify-content: space-between; align-items: center;
                    padding: 12px 16px; border-bottom: 1px solid var(--vb-border); }
.share-modal-body { padding: 16px; }
.share-modal-foot { padding: 12px 16px; border-top: 1px solid var(--vb-border); text-align: right; }
.share-modal .share-btn { border: 0; border-radius: 6px; padding: 9px 16px; font-size: 15px;
                          font-weight: 600; cursor: pointer; }
.share-modal .share-btn.primary { background: var(--vb-orange); color: #fff; }

.share-label { display: block; margin: 10px 0; }
.share-label > span { display: block; font-size: 13px; color: var(--vb-dim); margin-bottom: 4px; }
.share-field { width: 100%; padding: 8px; font-size: 15px; box-sizing: border-box; }
.share-hint { font-size: 13px; color: var(--vb-dim); margin: 10px 0; }
.share-err { color: var(--vb-warn); font-size: 14px; margin: 8px 0; }
.share-row { display: flex; align-items: center; gap: 8px; margin: 10px 0; flex-wrap: wrap; }
.share-value { background: #f2f4f7; padding: 4px 8px; border-radius: 4px; font-size: 15px;
               word-break: break-all; }
.share-value-label { font-size: 13px; color: var(--vb-dim); }
/* The copy/revoke/close buttons — explicit, over the element rule. */
.share-copy { background: var(--vb-navy); color: #fff; border: 0; border-radius: 4px;
              padding: 5px 10px; font-size: 13px; cursor: pointer; }

/* ==================== VigiTrack primitives (Foundation) ==================== */

/* A glassy elevated surface — the design's card-elevated: a translucent card
   over a subtle blur. Falls back to a solid card where backdrop-filter is
   unsupported (the 88% mix still reads as a card). */
.vb-glass {
  background: color-mix(in oklab, var(--vb-card) 88%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  border: 1px solid var(--vb-border);
  border-radius: var(--vb-radius);
  box-shadow: var(--vb-shadow);
}

/* Button roles. Each is a class, so the bare `button { width:100% }` element rule
   that pages carry cannot reach a control that opts into one of these. Navy is
   the default action (New, Choose file); orange is the strong CTA (Save, Assign,
   play); outline is secondary; danger is destructive. */
.vb-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  width: auto; min-height: 0; height: 38px; margin: 0; padding: 0 16px;
  border: 0; border-radius: 10px; font: inherit; font-weight: 600;
  cursor: pointer; white-space: nowrap; text-decoration: none;
}
.vb-btn:disabled { opacity: .5; cursor: not-allowed; }
.vb-btn:focus-visible { outline: 2px solid color-mix(in oklab, var(--vb-primary) 50%, transparent); outline-offset: 2px; }
.vb-btn-primary { background: var(--vb-primary); color: #fff; }
.vb-btn-primary:hover:not(:disabled) { filter: brightness(1.1); }
.vb-btn-accent  { background: var(--vb-orange-grad); color: #fff; }
.vb-btn-accent:hover:not(:disabled) { filter: brightness(1.05); }
.vb-btn-outline { background: var(--vb-card); color: var(--vb-text); box-shadow: inset 0 0 0 1px var(--vb-border); }
.vb-btn-outline:hover:not(:disabled) { background: var(--vb-surface); }
.vb-btn-danger  { background: var(--vb-late); color: #fff; }
.vb-btn-danger:hover:not(:disabled) { filter: brightness(1.05); }

/* Form controls. */
.vb-field { display: flex; flex-direction: column; gap: 6px; }
.vb-field > label, .vb-label-text {
  font: var(--vb-label); text-transform: uppercase; letter-spacing: .04em; color: var(--vb-dim);
}
.vb-input, .vb-select {
  width: 100%; height: 40px; padding: 0 12px; box-sizing: border-box;
  border: 1px solid var(--vb-border); border-radius: 10px;
  background: var(--vb-card); color: var(--vb-text); font: inherit;
}
.vb-input:focus, .vb-select:focus {
  outline: 2px solid color-mix(in oklab, var(--vb-primary) 45%, transparent); outline-offset: 1px;
  border-color: var(--vb-primary);
}
.vb-check { width: 18px; height: 18px; accent-color: var(--vb-orange); }

.vb-form-grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
@media (min-width: 768px) {
  .vb-form-grid { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
}

/* Amber/blue advisory callout (Roster "runs out", Playback footer note). */
.vb-callout {
  border-radius: var(--vb-radius); padding: 12px 14px; font-size: 13.5px; line-height: 1.5;
  border: 1px solid var(--vb-border); background: var(--vb-surface); color: var(--vb-text);
}
.vb-callout.is-warn { background: color-mix(in oklab, var(--vb-warn) 14%, var(--vb-card)); border-color: color-mix(in oklab, var(--vb-warn) 40%, transparent); }
.vb-callout.is-info { background: color-mix(in oklab, var(--vb-info) 12%, var(--vb-card)); border-color: color-mix(in oklab, var(--vb-info) 35%, transparent); }
