/* neweb — components.
   Consumes only the tokens from neweb.base.css. If a literal px value appears
   here that isn't a hairline or an icon size, it belongs in the scale instead. */

/* ============================== login ================================== */

.login {
  height: 100dvh; display: grid; place-items: center; padding: var(--s-5);
  background:
    radial-gradient(1200px 600px at 15% -10%, var(--brand-soft), transparent 60%),
    var(--canvas);
}
.login-card { width: 100%; max-width: 384px; padding: var(--s-6); }
.login-logo { height: 40px; width: auto; margin: 0 auto var(--s-5); }
.login-title { font-size: var(--t-xl); text-align: center; color: var(--nw-heading); }
.login-sub { text-align: center; color: var(--ink-3); font-size: var(--t-sm); margin-bottom: var(--s-5); }
.login-err {
  margin: 0 0 var(--s-3); padding: 9px 12px; border-radius: var(--r-md);
  font-size: var(--t-sm); color: var(--bad);
  background: color-mix(in srgb, var(--bad) 8%, var(--card));
  border: 1px solid color-mix(in srgb, var(--bad) 28%, var(--line));
}
.login-remember {
  display: flex; align-items: center; gap: var(--s-2);
  font-size: var(--t-sm); color: var(--ink-2); margin-bottom: var(--s-4); cursor: pointer;
}
.login-btn { width: 100%; justify-content: center; padding: 10px; }
.login-langs {
  display: flex; gap: var(--s-2); justify-content: center;
  margin-top: var(--s-5); padding-top: var(--s-4); border-top: 1px solid var(--line-soft);
}

/* =============================== rail ==================================
   The only chrome. Navigation, the alert badge and the user menu, in one
   column that collapses to icons. On a phone the same markup becomes a fixed
   bottom bar — no duplicate nav to keep in sync.
   ----------------------------------------------------------------------- */

/* NO overflow: hidden here.
   The user menu is positioned outside the rail's box (inset-inline-start:
   calc(100% + …)), so clipping the rail clipped the menu away completely: the
   avatar appeared dead, and because the sign-out buttons live in that menu there
   was no way to sign out at all. Labels do their own truncation via .rail-txt,
   and the hidden states use display:none, so nothing needs clipping here. */
.rail {
  display: flex; flex-direction: column;
  /* The card surface, not --nw-header-bg. The theme value in gs_themes was
     picked for a 56px top strip; stretched down a full-height rail it
     overwhelms everything else. The theme still drives the ACTIVE item, the
     brand accent and every heading, so the palette is unchanged — only the
     large flat area is neutral. To go back, swap this one line for
     var(--nw-header-bg). */
  background: var(--card);
  border-inline-end: 1px solid var(--line);
  color: var(--nw-header-ink);
  /* No width transition. The rail is a grid item, so its width comes from the
     track; animating `width` on top of that means the element lags the track,
     and while the animation is in flight the grid reports the OLD used track
     sizes. If the animation clock is ever stalled the rail never reaches its
     new width at all and the layout looks frozen — which is precisely what made
     the maximise state appear to do nothing. Track changes are instant. */
}

.rail-head {
  display: flex; align-items: center; gap: var(--s-1);
  height: 56px; padding: 0 var(--s-2);
  border-bottom: 1px solid var(--line-soft);
  flex: 0 0 auto;
}
.rail-brand {
  display: flex; align-items: center; gap: var(--s-2);
  min-width: 0; flex: 1; padding: var(--s-1);
  border-radius: var(--r-md); text-decoration: none; color: var(--ink);
}
.rail-brand:hover { background: var(--card-2); }
.rail-brand img { width: 26px; height: 26px; object-fit: contain; flex: 0 0 auto; }
.rail-brand .rail-txt { font-weight: 700; font-size: var(--t-md); }

.rail-collapse {
  width: 30px; height: 30px; flex: 0 0 auto;
  display: grid; place-items: center;
  border-radius: var(--r-md); color: var(--ink-3);
}
.rail-collapse:hover { background: var(--card-2); color: var(--ink); }
.rail-collapse > svg { width: 17px; height: 17px; transition: transform .2s var(--ease); }
/* Chevrons point the way the rail will move. */
.app:not(.rail-icons) .rail-collapse > svg { transform: rotate(180deg); }
[dir="rtl"] .app:not(.rail-icons) .rail-collapse > svg { transform: none; }
[dir="rtl"] .app.rail-icons .rail-collapse > svg { transform: rotate(180deg); }

.rail-nav, .rail-foot {
  display: flex; flex-direction: column; gap: 2px;
  padding: var(--s-2); flex: 0 0 auto;
}
.rail-foot { border-top: 1px solid var(--line-soft); }

/* The nav carries thirteen destinations now that the management screens moved
   out of the user menu. At 40px a row that is ~560px, which overflows a 768px
   laptop once head and foot are counted — so it scrolls rather than pushing the
   user button off the bottom of the viewport where nothing can reach it.

   overflow-x must be hidden: the labels are nowrap, so a 64px collapsed rail
   would otherwise grow a horizontal scrollbar across the icons. */
.rail-nav {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overflow-x: hidden;
  scrollbar-width: thin;
}
.rail-nav::-webkit-scrollbar { width: 6px; }
.rail-nav::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; }
.rail-nav::-webkit-scrollbar-track { background: transparent; }

/* Redundant now that .rail-nav grows: with both at flex:1 they would split the
   free space and the nav would start scrolling at half the rail's height. */
.rail-spacer { display: none; }

/* Divides tracking from the management screens. */
.rail-sep {
  flex: 0 0 auto;
  height: 1px; margin: var(--s-2);
  background: var(--line-soft);
}

.rail-btn {
  position: relative;
  display: flex; align-items: center; gap: var(--s-3);
  min-height: 40px; padding: 0 var(--s-2);
  border-radius: var(--r-md);
  color: var(--ink-2); text-decoration: none;
  white-space: nowrap;
  transition: background .12s var(--ease), color .12s var(--ease);
}
.rail-btn:hover { background: var(--card-2); color: var(--ink); }
.rail-btn.on { background: var(--brand-soft); color: var(--brand); font-weight: 600; }
/* Active marker, so the state reads even when collapsed to icons. */
.rail-btn.on::before {
  content: ""; position: absolute;
  inset-block: 7px; inset-inline-start: 0;
  width: 3px; border-radius: 0 3px 3px 0;
  background: var(--brand);
}
[dir="rtl"] .rail-btn.on::before { border-radius: 3px 0 0 3px; }

.rail-ico {
  position: relative; flex: 0 0 auto;
  width: 30px; height: 30px;
  display: grid; place-items: center;
}
.rail-ico > svg { width: 21px; height: 21px; }
.rail-txt { overflow: hidden; text-overflow: ellipsis; font-size: var(--t-md); }

/* Collapsed: hide labels and centre the icons. */
.app.rail-icons .rail-txt { display: none; }
.app.rail-icons .rail-btn { justify-content: center; padding: 0; }
.app.rail-icons .rail-brand { justify-content: center; flex: 0 0 auto; }
.app.rail-icons .rail-head { flex-direction: column; height: auto; padding: var(--s-2) 0; gap: var(--s-1); }

.badge {
  position: absolute; top: -2px; inset-inline-end: -2px;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: var(--r-pill); background: var(--bad); color: #fff;
  font-size: 10.5px; font-weight: 700; line-height: 17px; text-align: center;
  box-shadow: 0 0 0 2px var(--card);
}

.avatar {
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--brand); color: var(--brand-ink);
  font-size: var(--t-sm); font-weight: 700; text-transform: uppercase;
  display: grid; place-items: center;
}

/* --- user menu, anchored to the rail ---------------------------------- */

.rail-user { position: relative; }
.menu {
  position: absolute; bottom: 0; inset-inline-start: calc(100% + var(--s-2));
  min-width: 254px; padding: var(--s-2);
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--sh-3);
  z-index: var(--z-modal); color: var(--ink);
  max-height: 82dvh; overflow-y: auto;
}
.menu-head { padding: var(--s-2) var(--s-3) var(--s-3); }
.menu-name { font-weight: 600; }
.menu-mail, .menu-exp { font-size: var(--t-sm); overflow-wrap: anywhere; }
.menu-item {
  display: block; width: 100%; text-align: start;
  padding: 9px var(--s-3); border-radius: var(--r-md);
  font-size: var(--t-md); color: var(--ink); text-decoration: none;
}
.menu-item:hover { background: var(--card-2); }
.menu-item.danger { color: var(--bad); }
.menu-sep { height: 1px; background: var(--line-soft); margin: var(--s-2) 0; }
.menu-label { padding: var(--s-1) var(--s-3); font-size: var(--t-xs);
              text-transform: uppercase; letter-spacing: .04em; }
.menu-row { display: flex; gap: var(--s-1); padding: 0 var(--s-3) var(--s-2); flex-wrap: wrap; }
.menu-ver { padding: var(--s-2) var(--s-3) var(--s-1); font-size: var(--t-xs); }

/* --- phone: the rail is a bottom bar ---------------------------------- */
@media (max-width: 760px) {
  .rail {
    flex-direction: row; align-items: stretch;
    border-inline-end: 0; border-top: 1px solid var(--line);
    box-shadow: 0 -4px 14px -8px rgba(16,22,32,.3);
  }
  /* Brand, collapse chevron and the list toggle have no place in a bottom bar:
     the sheet grip replaces the toggle and the logo is not navigation. */
  .rail-head, .rail-spacer, .rail-list-toggle, .rail-sep { display: none; }
  .rail-nav, .rail-foot {
    flex-direction: row; flex: 1; gap: 0; padding: 0;
    border-top: 0; align-items: stretch;
  }

  /* Only about five targets fit at 390px, and there are now thirteen. The bar
     therefore SCROLLS horizontally rather than hiding the overflow: the previous
     rule hid Logbook and Settings outright, which was tolerable for two screens
     but would now make eight of them unreachable on a phone.

     `safe center` is what makes this work in both directions — it centres a
     short nav (a user whose privileges leave only three entries) but degrades to
     flex-start when the content overflows. Plain `center` or `space-around`
     makes the leading items impossible to scroll back to. flex-start is declared
     first as the fallback for anything that does not know the `safe` keyword. */
  .rail-nav {
    overflow-x: auto; overflow-y: hidden;
    justify-content: flex-start;
    justify-content: safe center;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;

    /* Snapping so the bar never RESTS on a half-cut icon, and a fade on the
       trailing edge so the cut that does remain reads as "there is more this
       way" instead of as a clipping bug. Without the fade the bar looked
       broken: a sliver of the sixth icon with no scrollbar (hidden above) and
       nothing to suggest it could be scrolled. */
    scroll-snap-type: x proximity;
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 22px), transparent 100%);
            mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 22px), transparent 100%);
  }
  [dir="rtl"] .rail-nav {
    -webkit-mask-image: linear-gradient(to left, #000 0, #000 calc(100% - 22px), transparent 100%);
            mask-image: linear-gradient(to left, #000 0, #000 calc(100% - 22px), transparent 100%);
  }
  /* A nav that fits needs no fade, and a mask would dim its last icon. */
  .rail-nav:not(:has(> :nth-child(7))) {
    -webkit-mask-image: none; mask-image: none;
  }
  .rail-nav > .rail-btn { scroll-snap-align: start; }
  .rail-nav::-webkit-scrollbar { display: none; }
  .rail-foot { flex: 0 0 auto; }
  .rail-btn {
    flex-direction: column; justify-content: center; gap: 2px;
    min-height: var(--tap); min-width: 54px; padding: 0 var(--s-1);
    border-radius: 0;
  }
  /* Labels come back as tiny captions — a bare icon bar is hard to learn. */
  .rail-txt,
  .app.rail-icons .rail-txt { display: block; font-size: 9.5px; font-weight: 600; max-width: 62px; }
  .app.rail-icons .rail-btn { flex-direction: column; padding: 0 var(--s-1); }
  .rail-btn.on { background: none; }
  /* Top edge instead of a side edge, since the bar is horizontal. */
  .rail-btn.on::before {
    inset: 0 auto auto 50%; transform: translateX(-50%);
    width: 26px; height: 3px; border-radius: 0 0 3px 3px;
  }
  .rail-ico { width: 24px; height: 22px; }
  .rail-ico > svg { width: 20px; height: 20px; }
  .rail-user .rail-txt { display: none; }

  /* Menu opens upward from the bar, full width. */
  .menu {
    position: fixed; inset-inline: var(--s-3);
    bottom: calc(var(--h-bar) + var(--safe-b) + var(--s-2));
    min-width: 0; max-height: 72dvh;
  }
}


/* ============================ device list ============================== */

.list {
  display: flex; flex-direction: column; min-width: 0;
  background: var(--card); border-inline-end: 1px solid var(--line);
  overflow: hidden;
}
.list-top { padding: var(--s-3); border-bottom: 1px solid var(--line-soft); }

.search { position: relative; display: flex; align-items: center; }
.search > svg {
  position: absolute; inset-inline-start: 10px;
  width: 15px; height: 15px; color: var(--ink-3); pointer-events: none;
}
.search-in {
  width: 100%; padding: 8px 30px 8px 32px;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--card-2);
}
[dir="rtl"] .search-in { padding: 8px 32px 8px 30px; }
.search-in:focus { border-color: var(--brand); background: var(--card); outline: none; }
.search-in::-webkit-search-cancel-button { display: none; }
.search-clear {
  position: absolute; inset-inline-end: 8px;
  width: 18px; height: 18px; border-radius: 50%;
  color: var(--ink-3); font-size: 15px; line-height: 1;
}
.search-clear:hover { background: var(--line); }

.list-filters { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-top: var(--s-2); }
.list-filters .chip { cursor: pointer; font-size: var(--t-xs); padding: 3px 9px; }
.list-meta {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: var(--s-2); font-size: var(--t-xs);
}
.btn.tiny { padding: 3px 8px; font-size: var(--t-xs); }

.list-body { flex: 1; padding: var(--s-2); }

/* group heading */
.grp {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-3) var(--s-2) var(--s-1);
  font-size: var(--t-xs); font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-3); cursor: pointer;
}
.grp .chev { width: 13px; height: 13px; transition: transform .15s var(--ease); }
.grp.collapsed .chev { transform: rotate(-90deg); }
[dir="rtl"] .grp.collapsed .chev { transform: rotate(90deg); }
.grp-n { margin-inline-start: auto; font-weight: 600; }

/* device row */
.dev {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  grid-template-areas: "dot name io acts" "dot sub io acts";
  gap: 0 var(--s-2); align-items: center;
  padding: var(--s-2) var(--s-2);
  border-radius: var(--r-md); cursor: pointer;
  border: 1px solid transparent;
}
.dev:hover { background: var(--card-2); }
.dev.on { background: var(--brand-soft); border-color: var(--brand-line); }
.dev.inactive { opacity: .55; }
.dev > .dot  { grid-area: dot; margin-inline-end: var(--s-1); }
.dev-name {
  grid-area: name; font-weight: 600; font-size: var(--t-md);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dev-sub {
  grid-area: sub; font-size: var(--t-sm); color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dev-acts { grid-area: acts; display: flex; align-items: center; gap: 2px; }
.dev-act {
  width: 27px; height: 27px; border-radius: var(--r-sm);
  display: grid; place-items: center; color: var(--ink-3);
}
.dev-act:hover { background: var(--line); color: var(--ink); }
.dev-act > svg { width: 15px; height: 15px; }
.dev-act.on { color: var(--brand); }
.dev-badge {
  font-size: 9.5px; font-weight: 700; padding: 1px 5px;
  border-radius: var(--r-sm); background: var(--line); color: var(--ink-3);
}

/* Per-row status glyphs: engine, battery, signal. They sit in their own grid
   area between the text and the buttons, spanning both rows so they stay
   vertically centred against a two-line row. Non-interactive on purpose — the
   reading is in the tooltip; the row itself is the click target. */
.dev-io {
  grid-area: io; display: flex; align-items: center; gap: 3px;
  pointer-events: none;
}
.dev-io-i { display: grid; place-items: center; width: 16px; height: 16px; }
.dev-io-i > svg { width: 15px; height: 15px; stroke-width: 2.2; stroke-linecap: round; }
/* Every glyph has a resting grey so a row never gains or loses visual weight
   just because a sensor happens to be configured. */
.dev-io-i { color: var(--ink-3); }
.dev-io-i.eng.on { color: var(--warn, #FAB444); }
.dev-io-i.eng.off { color: var(--ink-3); opacity: .5; }
.dev-io-i.bat { color: var(--ink-3); }
.dev-io-i.sig.s2 { color: var(--ok, #9CC602); }
.dev-io-i.sig.s1 { color: var(--warn, #FAB444); }
.dev-io-i.sig.s0 { color: var(--ink-3); opacity: .45; }
/* The glyphs are the first thing to go when the panel is narrow: the name and
   the actions matter more. */
@media (max-width: 380px) { .dev-io { display: none; } }

/* Search plus the add control on one line. */
.list-tools { display: flex; align-items: center; gap: var(--s-2); }
.list-tools > .search { flex: 1 1 auto; min-width: 0; }
.list-tools > .head-btn { width: 34px; height: 34px; }
.list-tools > .head-btn.add { color: var(--brand); }
.list-tools > .head-btn.add:disabled { color: var(--ink-3); opacity: .45; cursor: not-allowed; }

/* poll state */
.list-foot {
  padding: var(--s-2) var(--s-3);
  border-top: 1px solid var(--line-soft); font-size: var(--t-xs);
}
.poll { display: flex; align-items: center; gap: var(--s-2); }
.poll-ring {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--ok); flex: 0 0 auto;
}
.poll.paused .poll-ring { background: var(--ink-3); }
.poll.stale  .poll-ring { background: var(--warn); animation: nw-pulse 1.4s ease-in-out infinite; }
.poll.error  .poll-ring { background: var(--bad); animation: nw-pulse 1s ease-in-out infinite; }
@keyframes nw-pulse { 50% { opacity: .28; } }

/* ============================== map UI ================================= */

.mapui { position: absolute; z-index: var(--z-map-ui); display: flex; gap: var(--s-2); }
.mapui-left  { inset-inline-start: var(--s-3); bottom: var(--s-3); align-items: flex-end; }
.mapui-right { inset-inline-end: var(--s-3); top: var(--s-3); flex-direction: column; align-items: flex-end; }

.opt, .layers {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--sh-2); overflow: hidden;
}
.opt-head, .layers-btn {
  display: flex; align-items: center; gap: var(--s-2);
  padding: 9px var(--s-3); font-size: var(--t-sm); font-weight: 600; color: var(--ink);
  width: 100%;
}
.opt-head > svg, .layers-btn > svg { width: 16px; height: 16px; }
.opt-head:hover, .layers-btn:hover { background: var(--card-2); }
.opt-body, .layers-body {
  padding: var(--s-2) var(--s-3) var(--s-3);
  border-top: 1px solid var(--line-soft);
  max-height: min(56vh, 460px); overflow-y: auto;
}
.opt-body { width: 258px; }
.layers-body { width: 214px; }

.opt-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-3); padding: 6px 0; font-size: var(--t-sm);
}
.opt-group {
  margin-top: var(--s-3); padding-top: var(--s-2);
  border-top: 1px solid var(--line-soft);
  font-size: var(--t-xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--ink-3);
}
.opt-group:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.opt-seg { display: flex; gap: var(--s-1); flex-wrap: wrap; }
.opt-seg .chip { cursor: pointer; }

.layer-item {
  display: flex; align-items: center; gap: var(--s-2);
  width: 100%; padding: 7px var(--s-2); border-radius: var(--r-md);
  font-size: var(--t-sm); text-align: start; color: var(--ink);
}
.layer-item:hover { background: var(--card-2); }
.layer-item.on { background: var(--brand-soft); color: var(--brand); font-weight: 600; }
.layer-swatch {
  width: 26px; height: 20px; border-radius: var(--r-sm);
  background: var(--line); flex: 0 0 auto; background-size: cover; background-position: center;
}

.countdown {
  padding: 7px var(--s-3); font-size: var(--t-sm); font-variant-numeric: tabular-nums;
  display: flex; align-items: center; gap: var(--s-2); white-space: nowrap;
}
.countdown.stale { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, var(--line)); }
.countdown.paused { color: var(--ink-3); }

.gl-gate {
  position: absolute; inset: 0; z-index: var(--z-map-ui);
  display: grid; place-items: center; padding: var(--s-6); background: var(--canvas);
}
.gl-card { max-width: 440px; padding: var(--s-6); display: grid; gap: var(--s-3); }
.gl-card h2 { font-size: var(--t-lg); }
.gl-card p { color: var(--ink-2); font-size: var(--t-md); }
.gl-card .btn { justify-self: start; }

/* MapLibre chrome, retinted to the theme */
.maplibregl-ctrl-group {
  background: var(--card) !important;
  border: 1px solid var(--line) !important;
  box-shadow: var(--sh-2) !important;
  border-radius: var(--r-md) !important;
}
.maplibregl-ctrl-group button + button { border-top-color: var(--line-soft) !important; }
.maplibregl-ctrl-attrib { font-size: 10.5px; }

/* ======================= overlay panels & drawer ======================== */

/* Absolute against .app, offset past the rail by the same variable the rail
   itself uses, so a panel covers the list and the map but never the nav. */
/* A panel overlays the MAP, not the whole shell.
   It used to start at var(--rail-w), which put it over the 350px device list as
   well: opening Alerts or Settings made the device list vanish, and the only way
   back was the Suivi button. Switching between screens while keeping the fleet
   in view is the whole point of a persistent list, so the panel starts after it.

   Both terms are custom properties that applyShell() writes as literal pixels,
   and --list-w is 0 when the list is folded or the shell is maximised, so a
   collapsed list correctly gives the panel the full width. The phone block in
   neweb.base.css overrides this to inset:0 — a panel is full-screen there. */
.panel {
  position: absolute; inset: 0;
  inset-inline-start: calc(var(--rail-w) + var(--list-w));
  z-index: var(--z-panel);
  display: flex; flex-direction: column; min-width: 0;
  background: var(--canvas); overflow: hidden;
}
.panel-head {
  display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-4) var(--s-5); background: var(--card);
  border-bottom: 1px solid var(--line);
}
.panel-head h2 { font-size: var(--t-lg); }
.panel-head .grow { flex: 1; }
.panel-body { flex: 1; overflow-y: auto; padding: var(--s-5); }

.drawer {
  position: absolute; inset-block: 0; inset-inline-end: 0;
  z-index: var(--z-drawer); width: min(430px, 100%);
  display: flex; flex-direction: column;
  background: var(--card); border-inline-start: 1px solid var(--line);
  box-shadow: var(--sh-3);
  animation: nw-slide .22s var(--ease);
}
@keyframes nw-slide { from { transform: translateX(100%); } }
[dir="rtl"] .drawer { animation-name: nw-slide-rtl; }
@keyframes nw-slide-rtl { from { transform: translateX(-100%); } }

.drawer-head {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-3) var(--s-3) var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--line-soft);
}
.drawer-title { display: flex; align-items: center; gap: var(--s-2); min-width: 0; flex: 1; }
.drawer-title strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.drawer-tabs { display: flex; gap: 2px; padding: var(--s-2) var(--s-3) 0; border-bottom: 1px solid var(--line-soft); }
.dtab {
  padding: 7px var(--s-3); font-size: var(--t-sm); font-weight: 600;
  color: var(--ink-3); border-bottom: 2px solid transparent;
}
.dtab:hover { color: var(--ink-2); }
.dtab.on { color: var(--brand); border-bottom-color: var(--brand); }
.drawer-body { flex: 1; padding: var(--s-4); }

/* key/value rows used by the drawer and panels */
.kv { display: grid; grid-template-columns: minmax(96px, 38%) 1fr; gap: var(--s-1) var(--s-3);
      font-size: var(--t-md); }
.kv > dt { color: var(--ink-3); }
.kv > dd { margin: 0; overflow-wrap: anywhere; }
.sec { margin-bottom: var(--s-5); }
.sec > h3 {
  font-size: var(--t-xs); font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: var(--s-2);
}

/* ============================== markers ================================ */

/* Only the selected device gets a DOM marker (its callout). Everything else is
   drawn by the GeoJSON layers in neweb.markers.js, so there is no per-marker
   projection cost on the poll. */
.callout {
  min-width: 244px; max-width: 300px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--sh-3);
  padding: var(--s-3); font-size: var(--t-sm);
}
.callout-top { display: flex; align-items: center; gap: var(--s-2); margin-bottom: var(--s-2); }
.callout-name {
  font-size: var(--t-md); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
  /* Takes the slack so the close button is pinned to the trailing edge. */
  flex: 1; min-width: 0;
}

/* Close. Hides the bubble and keeps the device selected. Full --tap target on a
   phone, where this is the only way to get the map back without another pick. */
.callout-x {
  flex: 0 0 auto; width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: var(--r-md); color: var(--ink-3);
  margin: -4px -4px -4px 0;
}
.callout-x:hover { background: var(--card-2); color: var(--ink); }
.callout-x > svg { width: 15px; height: 15px; }
@media (max-width: 760px) {
  .callout-x { width: var(--tap); height: var(--tap); margin: -8px -8px -8px 0; }
  .callout-x > svg { width: 18px; height: 18px; }
}

/* Status chip. Colour comes from the same state palette as the marker rings, so
   the bubble and the dot on the map can never disagree. */
/* The SAME --st-* tokens .dot uses in neweb.base.css, so the chip and the marker
   ring can never disagree about a device's state. Not --ok/--bad/--warn: those
   are the semantic feedback colours and drift from the status palette. */
.callout-chip {
  display: inline-block; margin-bottom: var(--s-2);
  padding: 2px 8px; border-radius: var(--r-pill);
  font-size: var(--t-xs); font-weight: 700; color: #fff;
  background: var(--st-offline);
}
.callout-chip.moving   { background: var(--st-moving); }
.callout-chip.speeding { background: var(--st-speeding); }
.callout-chip.static   { background: var(--st-static); }
.callout-chip.offline  { background: var(--st-offline); }
/* #F4B400 against white is only ~2:1, so idle takes dark text. */
.callout-chip.idle     { background: var(--st-idle); color: #3A2E00; }

.callout .kv { grid-template-columns: auto 1fr; font-size: var(--t-sm); }

.callout-addr {
  margin-top: var(--s-2); padding-top: var(--s-2);
  border-top: 1px solid var(--line-soft);
  font-size: var(--t-xs); line-height: 1.35;
  overflow-wrap: anywhere;
}
.callout-addr:empty { display: none; border-top: 0; margin: 0; padding: 0; }

/* Action row: icon over label, evenly divided. Five fit at the 300px cap; a
   sixth would wrap, which is why the set is fixed and Navigation drops out when
   the device has no fix. */
/* THREE per row, wrapping onto a second row as needed.
   It used to be `grid-auto-flow: column` with `grid-auto-columns: 1fr`, which
   forces everything onto ONE row however many items there are. That was written
   when the row held five, with a comment saying a sixth would wrap — then Share
   was added, making six, and instead of wrapping they squeezed until the labels
   overflowed the callout. A fixed column count cannot do that: six becomes 3+3,
   and the count varies legitimately anyway, because Commands and Share are
   hidden for a read-only account and Navigate needs a GPS fix. */
.callout-acts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-1); margin-top: var(--s-3);
  padding-top: var(--s-2); border-top: 1px solid var(--line-soft);
}
/* With one or two actions, three tracks would leave them stranded at the left. */
.callout-acts:not(:has(> :nth-child(3))) {
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
}
.callout-act {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: var(--s-1) 2px; border-radius: var(--r-md);
  color: var(--brand); text-decoration: none;
  font-size: 10px; font-weight: 600; line-height: 1.15; text-align: center;
}
.callout-act:hover { background: var(--brand-soft); }
.callout-act > span:last-child {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.callout-ico { display: grid; place-items: center; width: 20px; height: 20px; }
.callout-ico > svg { width: 18px; height: 18px; }
/* service status arrives with literal <font color="red"> from fn_objects.php:241,
   so it is detected and replaced with this class rather than injected as HTML */
.svc-late { color: var(--bad); font-weight: 600; }

/* ============================== alerts ================================= */

.alert-row {
  display: block; width: 100%; text-align: start;
  padding: var(--s-3) var(--s-4);
  margin-bottom: var(--s-2);
  border-inline-start: 3px solid var(--c, var(--brand));
  cursor: pointer;
}
.alert-row:hover { background: var(--card-2); }
/* Not yet acknowledged in THIS browser: the platform stores no read state, so
   this is deliberately local rather than pretending to be a server flag. */
.alert-row.fresh { box-shadow: var(--sh-2); }
.alert-row.fresh .alert-kind { background: var(--c, var(--brand)); color: #fff; }

.alert-top { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
.alert-top strong { font-size: var(--t-md); }
.alert-top .muted { margin-inline-start: auto; font-size: var(--t-sm); white-space: nowrap; }

.alert-kind {
  font-size: var(--t-xs); font-weight: 700; letter-spacing: .02em;
  padding: 2px 8px; border-radius: var(--r-pill);
  background: var(--card-2); color: var(--c, var(--brand));
  border: 1px solid var(--line);
}
.alert-desc { margin-top: var(--s-1); font-size: var(--t-sm); color: var(--ink-2); overflow-wrap: anywhere; }
.alert-meta { margin-top: 2px; font-size: var(--t-xs); }

/* ===================== shell controls & maximise ======================== */

/* Generic icon button. Still used by the drawer close and panel headers now
   that the top bar is gone. */
.head-btn {
  position: relative;
  width: 38px; height: 38px; border-radius: var(--r-md);
  display: grid; place-items: center; color: inherit;
  transition: background .12s var(--ease);
  flex: 0 0 auto;
}
.head-btn:hover { background: var(--card-2); }
.head-btn > svg { width: 19px; height: 19px; }
@media (max-width: 760px) { .head-btn { width: var(--tap); height: var(--tap); } }

/* Square map-chrome button, sized to match the layers/options cards. */
.mapui-btn {
  width: 36px; height: 36px;
  display: grid; place-items: center;
  color: var(--ink-2);
}
.mapui-btn:hover { color: var(--ink); background: var(--card-2); }
.mapui-btn > svg { width: 17px; height: 17px; }
.mapui-btn[aria-pressed="true"] { color: var(--brand); border-color: var(--brand-line); }
@media (max-width: 760px) { .mapui-btn { width: var(--tap); height: var(--tap); } }

/* While maximised, every overlay control is hidden except one restore affordance,
   so the map is genuinely full-bleed but the chrome can always be recovered
   (Escape works too — see neweb.core.js). */
.app.chrome-hidden .mapui-left,
.app.chrome-hidden .layers { display: none; }
.app.chrome-hidden .mapui-right { inset-block-start: calc(var(--s-3) + var(--safe-t)); }

/* Bottom-sheet grab handle. Desktop has no sheet, so it is hidden there. */
.sheet-grip { display: none; }
@media (max-width: 760px) {
  .sheet-grip {
    display: grid; place-items: center;
    width: 100%; height: 26px; flex: 0 0 auto;
    touch-action: none;                 /* the drag is handled in JS */
  }
  .sheet-grip > span {
    width: 40px; height: 4px; border-radius: var(--r-pill);
    background: var(--line); display: block;
  }
  .sheet-grip:active > span { background: var(--ink-3); }
}

/* Map overlay controls must clear the phone bottom bar and the sheet. */
@media (max-width: 760px) {
  .mapui-left {
    inset-inline-start: var(--s-2);
    bottom: calc(var(--h-bar) + var(--safe-b) + 118px);
  }
  .mapui-right { inset-inline-end: var(--s-2); top: calc(var(--s-2) + var(--safe-t)); }
  .opt-body { width: min(78vw, 280px); }
  .layers-body { width: min(70vw, 240px); }
  .countdown { font-size: var(--t-xs); padding: 5px var(--s-2); }
  /* MapLibre's own controls would collide with the layers card. */
  .maplibregl-ctrl-top-right { margin-top: 46px; }
}

/* ========================= panel content blocks ========================= */

/* Stat tiles — the fleet summary at the top of Statistics. auto-fit so the row
   reflows from six across on a desktop to two on a phone with no breakpoint. */
.tiles {
  display: grid; gap: var(--s-2);
  grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
}
.tile { padding: var(--s-3) var(--s-4); }
.tile-v { font-size: var(--t-2xl); font-weight: 700; line-height: 1.1; color: var(--nw-heading); }
.tile-k { font-size: var(--t-xs); color: var(--ink-3); text-transform: uppercase;
          letter-spacing: .03em; margin-top: 2px; }

/* Hand-drawn bar chart. A grid, so the labels and values align down the column
   however long the names are — no measuring, no chart library. */
.chart { display: grid; gap: 6px; }
.chart-row {
  display: grid; grid-template-columns: minmax(72px, 26%) 1fr auto;
  align-items: center; gap: var(--s-3); font-size: var(--t-sm);
}
.chart-lbl { color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-track { background: var(--card-2); border-radius: var(--r-pill); height: 10px; overflow: hidden; }
.chart-bar {
  display: block; height: 100%; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--brand), color-mix(in srgb, var(--brand) 55%, #fff));
}
.chart-val { font-variant-numeric: tabular-nums; color: var(--ink); white-space: nowrap; }

/* Generic record list, used by Logbook / Sub-accounts / Places. Not a <table>:
   these endpoints return a variable number of jqGrid cells, so a wrapping row
   of chips degrades gracefully where fixed columns would not. */
.tbl { display: grid; gap: var(--s-2); }
.tbl-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-2) var(--s-4);
  padding: var(--s-3) var(--s-4);
}
.tbl-c { font-size: var(--t-sm); color: var(--ink-2); }
.tbl-c.strong { font-size: var(--t-md); font-weight: 600; color: var(--ink); flex: 1 1 auto; }

@media (max-width: 760px) {
  .panel-body { padding: var(--s-3); }
  .panel-head { padding: var(--s-3) var(--s-4); }
  .chart-row { grid-template-columns: minmax(60px, 34%) 1fr auto; font-size: var(--t-xs); }
}

/* ---- device panel header (the fold control) --------------------------- */

.list-head {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-3) var(--s-2) var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--line-soft);
  flex: 0 0 auto;
}
.list-title { font-size: var(--t-lg); }
.list-n { font-size: var(--t-sm); margin-inline-end: auto; }
.list-fold > svg { width: 17px; height: 17px; }
[dir="rtl"] .list-fold > svg { transform: scaleX(-1); }

/* On a phone the sheet is closed with the grip, and the header would just cost
   vertical space on the smallest screens. */
@media (max-width: 760px) {
  .list-head { padding: var(--s-1) var(--s-2) var(--s-2) var(--s-4); }
  .list-fold { display: none; }
}

/* A plain count next to an icon, not an alert — so it uses the neutral surface
   rather than the red the alert badge needs. */
.badge.count {
  background: var(--ink-3);
  box-shadow: 0 0 0 2px var(--card);
}
.rail-btn.on .badge.count { background: var(--brand); }

/* =========================================================================
   Report definitions (neweb.reports.js)
   ========================================================================= */

.rep-form { padding: var(--s-4); margin-bottom: var(--s-4); }

/* The saved-definition row: name and metadata stack, actions sit at the end.
   Wraps rather than truncates, because a report name plus four metadata terms
   does not fit beside three buttons at the 350px-list desktop width. */
.rep-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1 1 14rem;
}
.rep-main > .tbl-c { min-width: 0; overflow-wrap: anywhere; }

/* Multi-device picker. Scrolls internally and is capped, so a 300-device fleet
   cannot push the Save button off the bottom of the form. */
.pick { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
.pick-bar { display: flex; align-items: center; gap: var(--s-2); }
.pick-bar > .muted { margin-inline-start: auto; font-size: var(--t-sm); }
.pick-list {
  max-height: 14rem;
  overflow-y: auto;
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  padding: var(--s-1);
}
.pick-row {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-2);
  border-radius: var(--r-sm);
  cursor: pointer;
  min-height: var(--tap);
  min-width: 0;
}
.pick-row:hover { background: var(--card-2); }
.pick-row > span { min-width: 0; overflow-wrap: anywhere; }
.pick-row > input { flex: 0 0 auto; width: 16px; height: 16px; accent-color: var(--brand); }

/* =========================================================================
   Brand assets, light / dark pair
   =========================================================================
   Two <img> per location, one shown per theme. A single image plus a CSS
   filter was the obvious alternative and does not work here: the mark is navy
   ink PLUS red signal arcs, and any filter strong enough to lighten the navy
   for a dark rail (invert, or brightness) also drags the red off-brand.

   The selectors mirror the token blocks in neweb.base.css exactly, including
   the two-way [data-theme] override, so the in-app Appearance toggle beats the
   OS preference in BOTH directions. Getting only half of that right is how
   trackdz ended up with a permanently-dark logo on a light page. */
.brand-dark { display: none; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brand-light { display: none; }
  :root:not([data-theme="light"]) .brand-dark  { display: block; }
}
:root[data-theme="dark"] .brand-light { display: none; }
:root[data-theme="dark"] .brand-dark  { display: block; }
/* Explicit light must undo a dark OS preference. */
:root[data-theme="light"] .brand-light { display: block; }
:root[data-theme="light"] .brand-dark  { display: none; }

/* Sizing is NOT repeated here: .rail-brand img above already pins 26x26 with
   object-fit, and a second rule further down the file would silently win on
   source order. The mark's master is only 32px, so it must never be scaled up. */

/* =========================================================================
   Maintenance (neweb.maint.js)
   ========================================================================= */

/* Capped width: these are label/value rows, and across a 1100px panel the value
   ends up so far from its label that the pair stops reading as one line. */
.mnt-card { padding: var(--s-4); margin-bottom: var(--s-4); max-width: 820px; }

.mnt-head {
  display: flex; align-items: center; gap: var(--s-2);
  margin-bottom: var(--s-3);
  padding-bottom: var(--s-2); border-bottom: 1px solid var(--line-soft);
}
.mnt-name { font-size: var(--t-lg); min-width: 0; overflow-wrap: anywhere; }

/* A schedule row: name and interval stack, status and actions sit at the end.
   Wraps rather than truncating — a service name plus an interval plus a status
   plus two buttons does not fit on one line at the narrow end. */
.mnt-svc {
  display: flex; align-items: center; gap: var(--s-3);
  flex-wrap: wrap;
}
.mnt-svc-main {
  display: flex; flex-direction: column; gap: 2px;
  min-width: 0; flex: 1 1 12rem;
}
/* The gap matters: without it the name and "every 10000 km" rendered as one
   run-together word, because both are plain spans in a column flex box. */
.mnt-svc-main > .muted { font-size: var(--t-xs); }
.mnt-svc > .opt-seg { flex: 0 0 auto; }

/* Report range chooser and generated-report archive. */
.rep-range { padding: var(--s-4); margin-bottom: var(--s-4); max-width: 460px; }
.rep-arch-box { margin-top: var(--s-5); padding-top: var(--s-4); border-top: 1px solid var(--line-soft); }
.rep-arch { gap: var(--s-3); }

/* =========================================================================
   Commands (neweb.cmd.js) and share links (neweb.share.js)
   ========================================================================= */

.cmd-row { gap: var(--s-3); }
.cmd-body {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--t-xs); color: var(--ink-2);
  overflow-wrap: anywhere; max-width: 100%;
}
.cmd-send { max-width: 460px; }
.cmd-target { font-weight: 600; margin-bottom: var(--s-2); }

/* A command reaches the vehicle. The confirmation step says so, and says it in
   a way that is hard to skim past. */
.cmd-warn {
  margin: 0 0 var(--s-3); padding: var(--s-2) var(--s-3);
  border-radius: var(--r-md);
  font-size: var(--t-sm); color: var(--bad);
  background: color-mix(in srgb, var(--bad) 8%, var(--card));
  border: 1px solid color-mix(in srgb, var(--bad) 28%, var(--line));
}

.share-box { max-width: 560px; }
.share-row {
  display: flex; align-items: flex-start; gap: var(--s-3);
  padding: var(--s-2) 0; border-bottom: 1px solid var(--line-soft);
  flex-wrap: wrap;
}
.share-row:last-child { border-bottom: 0; }
.share-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 14rem; }
.share-main > .muted { font-size: var(--t-xs); }
/* Selectable, not a link: opening it would hand the operator's own session a
   page meant for someone without one. */
.share-url { font-size: var(--t-xs); font-family: ui-monospace, Menlo, Consolas, monospace; }

/* Last-resort cap. The anchor flip in neweb.markers.js handles the common case,
   but on a short viewport neither side has room for seven rows plus an address
   plus six actions — so the bubble scrolls internally rather than being clipped
   by the map canvas. dvh, not vh, because a mobile browser's toolbars change it. */
.callout { max-height: 68dvh; overflow-y: auto; overscroll-behavior: contain; }

/* Privacy policy link on the sign-in card. Quiet by default — it is a legal
   requirement and a reassurance, not a call to action — but a real link with a
   real target size, not 10px grey text nobody can hit on a phone. */
.login-legal { margin: 14px 0 0; text-align: center; }
.login-legal a {
  color: var(--ink-2);
  font-size: var(--t-sm);
  text-decoration: none;
  padding: 6px 8px;
  border-radius: var(--r-sm);
}
.login-legal a:hover,
.login-legal a:focus-visible { color: var(--brand); text-decoration: underline; }

/* ---------------------------------------------------------------------------
   Places
   The tabs carry a count, as the classic's tab headers do — how many markers,
   routes and zones you have is the first thing you want to know. */
.pl-tabs { margin: 0 0 12px; }
.pl-tabs .chip-num {
  margin-inline-start: 6px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--card-2);
  font-size: var(--t-xs);
}
.pl-tabs .chip.on .chip-num { background: rgba(255,255,255,.25); }

/* A swatch that means the same thing as the map: the pin for a marker, the
   stored colour for a route or a zone. */
.pl-row { align-items: center; gap: 10px; }
.pl-swatch { flex: 0 0 auto; width: 18px; height: 18px; display: grid; place-items: center; }
.pl-swatch.dot { border-radius: 50%; box-shadow: 0 0 0 2px var(--card) inset; }

/* The draw banner. It sits above the list and reports the vertex count, because
   the geometry column truncates silently past its width and losing a 60-vertex
   geofence at save time is unacceptable. */
.pl-draw {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 12px; margin: 0 0 12px;
  border-inline-start: 3px solid var(--warn);
}
.pl-draw .warn { color: var(--warn); font-weight: 600; font-size: var(--t-sm); }
.pl-draw .opt-seg { margin-inline-start: auto; }

.pl-form { padding: 14px 16px; display: grid; gap: 10px; }
.pl-form h3 { margin: 0 0 4px; }
.fld { display: grid; grid-template-columns: minmax(120px, 34%) 1fr; align-items: center; gap: 10px; }
.fld-k { color: var(--ink-2); font-size: var(--t-sm); }
.fld input[type="text"], .fld input[type="number"], .fld select, .fld textarea {
  width: 100%; padding: 7px 9px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--card); color: var(--ink); font: inherit;
}
.fld input[type="color"] { width: 46px; height: 30px; padding: 2px; border: 1px solid var(--line);
                           border-radius: var(--r-sm); background: var(--card); }
.fld input[type="checkbox"] { justify-self: start; width: 18px; height: 18px; }
@media (max-width: 560px) { .fld { grid-template-columns: 1fr; gap: 4px; } }

/* ---------------------------------------------------------------------------
   Users
   The row has to answer three questions at a glance: who, what role, and what
   they may DO to devices. The old version answered none of them — it printed a
   name, a type and a bare feature count. */
.usr-search { margin: 0 0 10px; }
.usr-search input {
  width: 100%; padding: 8px 11px;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--card); color: var(--ink); font: inherit;
}
.usr-count { margin: 0 0 8px; font-size: var(--t-sm); }
.usr-row { align-items: center; gap: 11px; }

.usr-av {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--brand); color: #fff;
  font-size: var(--t-sm); font-weight: 700;
}
.usr-av.off { background: var(--ink-2); }

.usr-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }

/* The role, as a chip whose colour carries the rank. Staff are deliberately the
   loudest: privileges.type is what setUserSessionCPanel() turns into
   cpanel_privileges, so an admin row should not look like any other row. */
.usr-type {
  padding: 1px 8px; border-radius: 999px;
  font-size: var(--t-xs); font-weight: 600;
  background: var(--card-2); color: var(--ink-2);
}
.usr-type.staff { background: #fdecea; color: #8a1c13; }
.usr-type.mgr   { background: #e8f1fb; color: #14507f; }
.usr-type.sub   { background: #eef2f6; color: #4a5765; }
.usr-type.ro    { background: #fff8e6; color: #7a5a10; }
.usr-badge.off  { padding: 1px 8px; border-radius: 999px; font-size: var(--t-xs);
                  background: var(--card-2); color: var(--ink-2); }
.usr-why { font-size: var(--t-xs); text-align: end; max-width: 15ch; }

.usr-form { padding: 14px 16px; display: grid; gap: 12px; }
.usr-form-head { display: grid; gap: 2px; }
.usr-form-head h3 { margin: 0; }
.fld-hint { display: block; font-size: var(--t-xs); color: var(--ink-2); font-weight: 400; }
.usr-flags { border-top: 1px solid var(--line); padding-top: 10px; }
.usr-flags h4 { margin: 0 0 8px; font-size: var(--t-sm); color: var(--ink-2);
                text-transform: uppercase; letter-spacing: .03em; }
.usr-flag-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 6px 14px; }
.usr-flag { display: flex; align-items: center; gap: 8px; font-size: var(--t-sm); }
.usr-flag input { width: 16px; height: 16px; }
.usr-msg:not(:empty) { color: var(--bad); font-size: var(--t-sm); }

@media (prefers-color-scheme: dark) {
  .usr-type.staff { background: #4a1512; color: #ffb4ab; }
  .usr-type.mgr   { background: #123048; color: #a8d1f5; }
  .usr-type.ro    { background: #3d3218; color: #f0d999; }
}

/* ---------------------------------------------------------------------------
   SIDE panels: a column beside the map rather than a sheet over it.

   Places needed this first — a POI is placed by clicking the map and a geofence
   is drawn on it, so a panel spanning the whole stage leaves nothing to draw on.
   The same is true of every list that refers to somewhere: an alert, a trip in
   the logbook and a driver all have a position, and reading one while the map is
   hidden means clicking through and losing your place.

   So it is opt-in per panel via data-side, and the set is declared in index.php
   rather than inferred here. Statistics is deliberately NOT in it: a dashboard
   wants the width.

   The width is --w-list, the same token the device column uses, so the two line
   up and the map does not jump as you move between screens. */
.panel[data-side] {
  inset-inline-end: auto;
  width: min(var(--w-list), 46vw);
  border-inline-end: 1px solid var(--line);
  box-shadow: var(--sh-2);
}

/* Retracted: the column folds away and the map takes the space. The reopen
   control is .panel-fold, which lives outside the panel so a module rebuilding
   its own innerHTML cannot wipe it. */
.app.panel-collapsed .panel[data-side] {
  width: 0; border-inline-end: 0; box-shadow: none; overflow: hidden;
}
.app.panel-collapsed .panel[data-side] > * { display: none; }

.panel-fold {
  position: absolute; z-index: calc(var(--z-panel) + 1);
  top: 50%; transform: translateY(-50%);
  inset-inline-start: calc(var(--rail-w) + var(--list-w) + min(var(--w-list), 46vw));
  width: 18px; height: 54px;
  display: grid; place-items: center;
  background: var(--card); color: var(--ink-3);
  border: 1px solid var(--line); border-inline-start: 0;
  border-radius: 0 var(--r-md) var(--r-md) 0;
  box-shadow: var(--sh-1);
}
[dir="rtl"] .panel-fold { border-radius: var(--r-md) 0 0 var(--r-md); }
.panel-fold:hover { color: var(--ink); }
.panel-fold > svg { width: 14px; height: 14px; transition: transform .18s var(--ease); }
.app.panel-collapsed .panel-fold {
  inset-inline-start: calc(var(--rail-w) + var(--list-w));
}
.app.panel-collapsed .panel-fold > svg { transform: rotate(180deg); }
[dir="rtl"] .panel-fold > svg { transform: rotate(180deg); }
[dir="rtl"] .app.panel-collapsed .panel-fold > svg { transform: none; }

/* On a phone there is no room for a column beside the map, so a side panel
   behaves like every other screen and the map is reached by closing it. */
@media (max-width: 760px) {
  .panel[data-side] {
    inset-inline-end: 0;
    width: auto;
    border-inline-end: 0;
    box-shadow: none;
  }
  .panel-fold { display: none; }
}

/* ============================ modal dialog ==============================
   Used by the add/edit device form. One shared shell rather than a per-feature
   overlay, so keyboard dismissal and backdrop behaviour are defined once.

   z-index sits on --z-modal, above the drawer: a dialog opened from the device
   drawer must not appear behind it. */
.nw-modal-back {
  position: fixed; inset: 0; z-index: var(--z-modal);
  display: grid; place-items: center;
  padding: var(--s-4);
  background: rgba(15, 20, 28, .45);
  overflow: auto;
}
.nw-modal {
  width: min(520px, 100%);
  max-height: calc(100vh - var(--s-6));
  display: flex; flex-direction: column;
  padding: 0;
}
.nw-modal-head {
  display: flex; align-items: center; gap: var(--s-2);
  padding: var(--s-3) var(--s-3) var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--line-soft);
}
.nw-modal-head > h3 { flex: 1 1 auto; margin: 0; font-size: var(--t-lg); }
.nw-modal-head > .head-btn { font-size: 22px; line-height: 1; }
.nw-modal-body {
  padding: var(--s-4); overflow: auto;
  display: flex; flex-direction: column; gap: var(--s-3);
}
.nw-modal-sub {
  display: flex; align-items: baseline; gap: var(--s-3);
  font-size: var(--t-sm);
}
.nw-modal-sub > .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.nw-modal-note {
  margin: 0; padding: var(--s-2) var(--s-3);
  border-radius: var(--r-md); background: var(--card-2);
  font-size: var(--t-sm); color: var(--ink-2);
}
.nw-modal-note.warn { background: var(--warn-soft, rgba(250, 180, 68, .16)); color: var(--ink); }
.nw-modal-ro {
  display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3);
  padding-top: var(--s-2); border-top: 1px solid var(--line-soft);
  font-size: var(--t-sm); color: var(--ink-2);
}
.nw-modal-err {
  margin: 0; padding: var(--s-2) var(--s-3);
  border-radius: var(--r-md);
  background: var(--danger-soft, rgba(214, 69, 69, .12));
  color: var(--danger, #C0392B); font-size: var(--t-sm);
}
.nw-modal-acts {
  display: flex; justify-content: flex-end; gap: var(--s-2);
  padding-top: var(--s-2); border-top: 1px solid var(--line-soft);
}
@media (max-width: 560px) {
  .nw-modal-back { padding: 0; place-items: end stretch; }
  .nw-modal { width: 100%; max-height: 92vh; border-radius: var(--r-lg) var(--r-lg) 0 0; }
}

/* ======================== Statistics metric cards ========================
   The cpanel Stats page's own pattern (inc/inc_cpanel.stats.php:16): an icon, a
   large value, a small label and a coloured top border, in a grid that reflows
   by itself. Reproduced with this app's tokens rather than its hardcoded hexes,
   so dark mode and the customer theme both keep working.

   The tone is the MEANING, not a colour — 'bad' is red in light mode and a
   lighter red in dark, and a future theme can move both without touching the
   module that decides a number is bad. */
.stat-grid { grid-template-columns: repeat(auto-fit, minmax(136px, 1fr)); }

.stat-card {
  position: relative;
  padding: var(--s-3) var(--s-2);
  text-align: center; text-decoration: none; color: inherit;
  border-top: 3px solid var(--line);
  transition: transform .12s var(--ease), box-shadow .12s var(--ease);
}
/* Only the ones that go somewhere lift, so the affordance is honest. */
a.stat-card:hover { transform: translateY(-2px); box-shadow: var(--sh-2); }
a.stat-card:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.stat-ico { display: grid; place-items: center; margin-bottom: 5px; color: var(--ink-3); }
.stat-ico > svg { width: 20px; height: 20px; }
.stat-card.tone-good .stat-ico { color: var(--ok); }
.stat-card.tone-warn .stat-ico { color: var(--warn); }
.stat-card.tone-bad  .stat-ico { color: var(--bad); }
.stat-card.tone-plain .stat-ico { color: var(--brand); }
.stat-v {
  font-size: var(--t-2xl); font-weight: 700; line-height: 1.05;
  color: var(--nw-heading);
}
.stat-k {
  font-size: var(--t-xs); color: var(--ink-3); margin-top: 4px;
  line-height: 1.25;
}
.stat-note { font-size: var(--t-xs); margin-top: var(--s-2); }

.stat-card.tone-good { border-top-color: var(--ok); }
.stat-card.tone-good .stat-v { color: var(--ok); }
.stat-card.tone-warn { border-top-color: var(--warn); }
.stat-card.tone-warn .stat-v { color: var(--warn); }
.stat-card.tone-bad  { border-top-color: var(--bad); }
.stat-card.tone-bad  .stat-v { color: var(--bad); }
.stat-card.tone-plain { border-top-color: var(--brand); }

/* --------------------------- POI icon picker ----------------------------
   141 pins is too many for a <select> of file names — nobody recognises
   "service-hotel-4" as a bed. A grid of the actual art, with a filter, and the
   current choice both highlighted and scrolled to. */
.pl-icons { display: grid; gap: var(--s-2); }
.pl-icons-head { display: flex; align-items: center; gap: var(--s-2); }
.pl-icons-cur {
  width: 30px; height: 30px; flex: 0 0 auto; object-fit: contain;
  padding: 3px; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--card);
}
.pl-icons-head > input { flex: 1 1 auto; min-width: 0; }
.pl-icons-grid {
  display: grid; gap: 4px;
  grid-template-columns: repeat(auto-fill, minmax(38px, 1fr));
  /* Bounded and scrollable: unbounded, 141 pins push the Save button off the
     bottom of a 400px side panel. */
  max-height: 190px; overflow-y: auto;
  padding: 4px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--card-2);
}
.pl-icon {
  display: grid; place-items: center;
  aspect-ratio: 1; padding: 3px;
  border: 1px solid transparent; border-radius: var(--r-sm);
  background: var(--card);
}
.pl-icon:hover { border-color: var(--line); }
.pl-icon.on { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); }
.pl-icon > img { width: 100%; height: 100%; object-fit: contain; }
