/* neweb — tokens, reset, layout primitives.
   ---------------------------------------------------------------------------
   Load order: neweb.base.css then neweb.css.

   The brand colours (--nw-brand, --nw-ink, --nw-header-*) are injected by
   bootstrap.php from the active gs_themes row, validated as hex. Everything
   here consumes them and adds what the theme has no concept of: a scale.

   trackdz.base.css has NO spacing/radius/type/z-index tokens — every value
   there is a literal — so this scale is new and is the single source of truth.
   Do not hardcode a px value in neweb.css if a token exists.

   ## Layout model

   There is no top bar. One left rail carries navigation, the alert badge and
   the user menu, in three widths:

       .app                  rail expanded, labels visible   (--w-rail-open)
       .app.rail-icons       rail collapsed to icons         (--w-rail)
       .app.chrome-hidden    rail AND list hidden, map full  (0)

   and the device column is independently collapsible (.list-collapsed). Below
   760px the rail becomes a fixed bottom bar and the list becomes a bottom
   sheet over a full-bleed map — that breakpoint is the whole reason the shell
   is a grid with named areas rather than absolute positioning.
   --------------------------------------------------------------------------- */

:root {
  /* --- brand fallbacks. bootstrap.php overrides these from the DB theme. --- */
  --nw-brand:       #2B82D4;
  --nw-brand-warn:  #FAB444;
  --nw-brand-ok:    #9CC602;
  --nw-brand-mute:  #808080;
  --nw-ink:         #444444;
  --nw-header-bg:   #FFFFFF;
  --nw-header-line: #F5F5F5;
  --nw-header-ink:  #808080;
  --nw-header-sel:  #F5F5F5;
  --nw-heading:     #2B82D4;
  --nw-heading-2:   #808080;

  /* --- derived brand steps. color-mix keeps them in step with the theme, so
         a new accent colour needs no further edits here. ------------------- */
  --brand:       var(--nw-brand);
  --brand-dark:  color-mix(in srgb, var(--nw-brand) 78%, #000);
  --brand-soft:  color-mix(in srgb, var(--nw-brand) 12%, #fff);
  --brand-line:  color-mix(in srgb, var(--nw-brand) 30%, #fff);
  --brand-ink:   #fff;

  /* --- surfaces (light) --- */
  --canvas:      #ECEBEE;
  --card:        #FFFFFF;
  --card-2:      #F7F8FA;
  --line:        #E3E5EA;
  --line-soft:   #EFF1F4;
  --ink:         var(--nw-ink);
  --ink-2:       #6B7280;
  --ink-3:       #9AA0AB;
  --overlay:     rgba(16,22,32,.42);

  /* --- vehicle status. Shared with neweb.markers.js, which reads these via
         getComputedStyle so there is ONE definition. --------------------- */
  --st-moving:   #34A853;
  --st-speeding: #E53935;
  --st-idle:     #F4B400;
  --st-static:   #2F7BDA;
  --st-offline:  #9AA0A6;

  /* --- semantic --- */
  --ok:    #2DA44E;
  --warn:  #B26A00;
  --bad:   #D93025;
  --info:  #0163AA;

  /* --- spacing: a 4px base, named by use not by number --- */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 20px; --s-6: 24px; --s-8: 32px; --s-10: 40px;

  /* --- radii --- */
  --r-sm: 6px; --r-md: 10px; --r-lg: 14px; --r-xl: 20px; --r-pill: 999px;

  /* --- type --- */
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
          Arial, "Noto Sans", "Noto Kufi Arabic", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
  --t-xs: 11px; --t-sm: 12.5px; --t-md: 13.5px; --t-lg: 15px;
  --t-xl: 18px; --t-2xl: 22px;

  /* --- elevation --- */
  --sh-1: 0 1px 2px rgba(16,22,32,.06), 0 1px 1px rgba(16,22,32,.04);
  --sh-2: 0 4px 12px -4px rgba(16,22,32,.16);
  --sh-3: 0 16px 36px -14px rgba(16,22,32,.34);

  /* --- layout --- */
  --w-rail:      64px;      /* collapsed, icons only */
  --w-rail-open: 232px;     /* expanded, labels visible */
  /* ONE width for every left-hand column: the device list AND every side panel
     (Alerts, Reports, Drivers, Logbook, Places, Users). They used to be 350 and
     440, so the columns never lined up and the map jumped as you moved between
     screens. PAJ's own --devices-menu-width is 350; Places needed 440 for its
     form; 400 serves both without spending the extra map width everywhere. */
  --w-list:      400px;
  --h-bar:       58px;      /* phone bottom bar */
  --tap:         44px;      /* minimum touch target */

  /* iOS home indicator / notch. Zero on everything else. */
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);

  /* --- z-index: one ladder, no ad-hoc numbers --- */
  --z-map: 1; --z-map-ui: 20; --z-panel: 40; --z-rail: 50;
  --z-sheet: 55; --z-drawer: 70; --z-modal: 90; --z-toast: 100; --z-boot: 110;

  --ease: cubic-bezier(.22,.61,.36,1);
}

/* Dark theme. Explicit [data-theme] wins in BOTH directions so the in-app
   toggle can override the OS preference — trackdz declared a dark block but
   never wired the attribute and hardcoded light colours in 49 places, so its
   media query produces a broken half-dark page. Everything here goes through
   tokens precisely so that cannot happen again. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --canvas: #121212; --card: #1C1C1D; --card-2: #232325;
    --line: #2A2B2E; --line-soft: #232426;
    --ink: #E6E7EA; --ink-2: #A3A7B0; --ink-3: #7C818B;
    --overlay: rgba(0,0,0,.6);
    --brand-soft: color-mix(in srgb, var(--nw-brand) 22%, #121212);
    --brand-line: color-mix(in srgb, var(--nw-brand) 40%, #121212);
    --sh-1: 0 1px 2px rgba(0,0,0,.5);
    --sh-2: 0 4px 12px -4px rgba(0,0,0,.6);
    --sh-3: 0 16px 36px -14px rgba(0,0,0,.7);
  }
}
:root[data-theme="dark"] {
  --canvas: #121212; --card: #1C1C1D; --card-2: #232325;
  --line: #2A2B2E; --line-soft: #232426;
  --ink: #E6E7EA; --ink-2: #A3A7B0; --ink-3: #7C818B;
  --overlay: rgba(0,0,0,.6);
  --brand-soft: color-mix(in srgb, var(--nw-brand) 22%, #121212);
  --brand-line: color-mix(in srgb, var(--nw-brand) 40%, #121212);
  --sh-1: 0 1px 2px rgba(0,0,0,.5);
  --sh-2: 0 4px 12px -4px rgba(0,0,0,.6);
  --sh-3: 0 16px 36px -14px rgba(0,0,0,.7);
}

/* --- reset ---------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font: var(--t-md)/1.45 var(--font);
  color: var(--ink);
  background: var(--canvas);
  -webkit-font-smoothing: antialiased;
  /* The shell owns all scrolling; a body scrollbar would drag the map. */
  overflow: hidden;
  /* Stops iOS Safari bouncing the whole page when a panel is scrolled to its end. */
  overscroll-behavior: none;
  /* Kills the 300ms tap delay and double-tap zoom on the map chrome. */
  touch-action: manipulation;
}
h1,h2,h3,h4 { margin: 0; font-weight: 600; }
p { margin: 0; }
img, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; padding: 0; }
a { color: var(--info); }
[hidden] { display: none !important; }

/* 16px on form controls, or iOS Safari zooms the viewport on focus — the single
   most common "the mobile site is broken" complaint. */
@media (max-width: 760px) {
  input, select, textarea { font-size: 16px; }
}

:where(button, a, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* --- app shell ------------------------------------------------------------
   Grid with named areas, so the rail and the list can each animate to zero
   width and the map gets a real size change (which NWM.invalidate() turns into
   a debounced map.resize()) rather than being overlapped.
   ------------------------------------------------------------------------- */

/* grid-template-columns is deliberately NOT transitioned.
   Animating it buys very little — the rail's own contents already animate — and
   it makes the shell's most important state change (does the map get the space
   or not?) depend on the animation clock running. A layout change this
   load-bearing should be unconditional, so the tracks switch instantly.

   minmax(0, 1fr) rather than 1fr on the map track: a bare 1fr has an automatic
   minimum, so one over-wide child (a long device name, a wide popup) can push
   the column past the viewport and produce horizontal page scroll — the classic
   cause of a mobile page that scrolls sideways. */
/* The two side columns are sized by their ELEMENTS (an explicit width on .rail
   and .list), not by fixed track sizes, and they are removed with display:none
   when hidden.

   Fixed 0-width tracks were tried first and did not work: with
   `grid-template-columns: 0 0 minmax(0,1fr)` winning the cascade —  verified by
   walking the matched rules — the grid still reported used tracks of
   `232px 350px 858px`, and neither !important nor an inline declaration nor
   min-width:0 on the children changed it. Letting the items drive `auto` tracks
   sidesteps that entirely: display:none removes the item, so an auto track with
   nothing in it collapses to zero. It is also easier to reason about — one
   place owns each width. */
/* Each side column has exactly ONE width declaration, and the shell states
   change a custom property rather than adding a competing rule. Selector-based
   overrides were tried first and produced a cascade that could not be reasoned
   about: with both `.app > .rail` and the more specific
   `.app.rail-icons > .rail` matching and the latter declared later, the computed
   width still resolved to the former's value. Routing every state through
   --rail-w / --list-w removes specificity from the question altogether — there
   is only one place that sets width, so there is nothing to lose to. */
/* FLEX, not grid, and this is the crux of the whole shell.
   ---------------------------------------------------------------------------
   The shell was originally a grid with named areas and the column widths in
   `grid-template-columns`. It could not be made to work, and the reason is
   worth recording because it is invisible from the markup:

     grid-template-columns does not re-resolve when a custom property it
     references changes.

   Measured directly: after toggling the state, the inline value was
   `--rail-w: var(--w-rail)` and the COMPUTED value of --rail-w was `64px`, while
   `grid-template-columns` still reported `232px 350px 858px`. The track list was
   stale. That is also why nothing else worked along the way — literal per-class
   track values, !important, an inline track list, min-width:0 on the children:
   all of them were fighting a cached track list rather than a cascade problem.

   Flex has no equivalent caching: a width change on a flex item is applied. So
   the two side columns are flex items with an explicit width and the map takes
   the rest. The panels and the drawer, which used grid areas, are now absolutely
   positioned against .app and offset by the same --rail-w.
   --------------------------------------------------------------------------- */
.app {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  position: relative;          /* containing block for the panels and drawer */
  height: 100dvh;
  --rail-w: var(--w-rail-open);
  --list-w: var(--w-list);
}
/* State-driven widths are set INLINE on #app by applyShell() in
   neweb.core.js; the two declarations above are the first-paint defaults. */

/* min-width: 0 is LOAD-BEARING, not tidiness.
   A grid item's automatic minimum size is min-content, so with the default
   `min-width: auto` the rail and the list refuse to shrink below their content
   AND the browser keeps their 0-width tracks at that content size. The effect:
   setting `grid-template-columns: 0 0 1fr` changed nothing at all — verified
   here, where even an !important declaration left the tracks at
   `232px 350px 858px`, and adding `min-width: 0` to the children immediately
   produced `0px 0px 1440px`.

   So without this line BOTH shell controls silently did nothing: the
   "hide device list" toggle and the maximise button. min-height: 0 is the same
   guarantee for the phone layout, where the areas stack into rows. */
/* Widths live here, on the items. min-width:0 keeps a long child (a device name,
   a wide popup) from forcing a column past the viewport, which is the usual
   cause of a page that scrolls sideways on a phone. */
/* flex: 0 0 <width> so neither side column grows or shrinks; the map absorbs
   everything else. min-width:0 stops a long child (a device name, a wide popup)
   from forcing a column wider than its width and pushing the page sideways. */
.app > .rail  {
  /* width is assigned inline as a literal px value by applyShell(); flex-basis
     stays auto so it follows that width instead of competing with it. */
  flex: 0 0 auto; width: var(--rail-w);
  min-width: 0; z-index: var(--z-rail);
  /* Deliberately NOT overflow:hidden. The user menu is positioned outside this
     box, so clipping here clipped it away entirely — the avatar looked dead and,
     because the sign-out buttons live in that menu, there was no way to sign
     out at all. Labels truncate themselves via .rail-txt and the hidden states
     use display:none, so nothing here needs clipping.
     The layout harness asserts `overflow: visible` on the rail for this reason. */
}

.app > .list  {
  flex: 0 0 auto; width: var(--list-w);
  min-width: 0; z-index: var(--z-panel); overflow: hidden;
}

.app > .stage {
  flex: 1 1 auto; min-width: 0;
  position: relative; overflow: hidden; z-index: var(--z-map);
}

/* Hidden means removed from layout, so the auto track collapses to nothing and
   the map genuinely gets the space. */
.app.list-collapsed > .list { display: none; }
.app.chrome-hidden  > .rail,
.app.chrome-hidden  > .list { display: none; }

/* The map is the persistent substrate: mounted once, never display:none.
   Hiding its container at init makes MapLibre initialise at 0x0 and stay blank
   until an explicit resize(), which is why panels overlay it instead. */
#map { position: absolute; inset: 0; }

/* --- boot veil ----------------------------------------------------------- */
.boot {
  position: fixed; inset: 0; z-index: var(--z-boot);
  display: grid; place-items: center;
  background: var(--canvas);
  transition: opacity .25s var(--ease);
}
.boot.hide { opacity: 0; pointer-events: none; }
.boot .spin {
  width: 30px; height: 30px; border-radius: 50%;
  border: 3px solid var(--brand-line); border-top-color: var(--brand);
  animation: nw-spin .7s linear infinite;
}
@keyframes nw-spin { to { transform: rotate(360deg); } }

/* --- primitives ---------------------------------------------------------- */

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-1);
}

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);
  min-height: 34px; padding: 7px 13px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--card);
  font-size: var(--t-md);
  transition: background .12s var(--ease), border-color .12s var(--ease);
}
.btn:hover { background: var(--card-2); }
.btn.primary { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
.btn.primary:hover { background: var(--brand-dark); border-color: var(--brand-dark); }
.btn.ghost { border-color: transparent; background: none; }
.btn.ghost:hover { background: var(--card-2); }
.btn[disabled] { opacity: .5; pointer-events: none; }
.btn.danger { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 35%, var(--line)); }
.btn.tiny { min-height: 0; padding: 3px 8px; font-size: var(--t-xs); }
@media (max-width: 760px) { .btn { min-height: var(--tap); } }

.field { display: block; margin-bottom: var(--s-3); }
.field > .lbl {
  display: block; margin-bottom: var(--s-1);
  font-size: var(--t-xs); font-weight: 600; letter-spacing: .03em;
  text-transform: uppercase; color: var(--ink-3);
}
.input {
  width: 100%; padding: 8px 11px;
  border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--card);
}
.input:focus { border-color: var(--brand); outline: none; }
@media (max-width: 760px) { .input { min-height: var(--tap); } }

.chip {
  display: inline-flex; align-items: center; gap: var(--s-1);
  padding: 4px 11px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--card);
  font-size: var(--t-sm); color: var(--ink-2); white-space: nowrap;
}
.chip.on { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
@media (max-width: 760px) { .chip { min-height: 32px; padding: 6px 13px; } }

.dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; background: var(--st-offline); }
.dot.moving   { background: var(--st-moving); }
.dot.speeding { background: var(--st-speeding); }
.dot.idle     { background: var(--st-idle); }
.dot.static   { background: var(--st-static); }
.dot.offline  { background: var(--st-offline); }

.sw {                                   /* toggle switch */
  --sw-w: 38px; --sw-h: 22px;
  position: relative; width: var(--sw-w); height: var(--sw-h);
  border-radius: var(--r-pill); background: var(--line);
  flex: 0 0 auto; transition: background .15s var(--ease);
}
.sw::after {
  content: ""; position: absolute; top: 3px; inset-inline-start: 3px;
  width: 16px; height: 16px; border-radius: 50%; background: #fff;
  box-shadow: var(--sh-1); transition: transform .15s var(--ease);
}
.sw.on { background: var(--brand); }
.sw.on::after { transform: translateX(16px); }
[dir="rtl"] .sw.on::after { transform: translateX(-16px); }
.sw[aria-disabled="true"] { opacity: .45; pointer-events: none; }

.muted  { color: var(--ink-3); }
.mono   { font-family: var(--font-mono); font-size: var(--t-sm); }
.scroll { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin;
          -webkit-overflow-scrolling: touch; }
.empty  { padding: var(--s-8) var(--s-5); text-align: center; color: var(--ink-3); font-size: var(--t-sm); }

.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* --- toasts -------------------------------------------------------------- */
.toasts {
  position: fixed; z-index: var(--z-toast);
  bottom: calc(var(--s-5) + var(--safe-b)); inset-inline-end: var(--s-5);
  display: flex; flex-direction: column; gap: var(--s-2);
  pointer-events: none;
}
.toast {
  max-width: 380px; padding: 10px 14px;
  border-radius: var(--r-md); background: var(--card);
  border: 1px solid var(--line); border-inline-start: 3px solid var(--brand);
  box-shadow: var(--sh-2); font-size: var(--t-sm);
  animation: nw-toast .2s var(--ease);
}
.toast.bad  { border-inline-start-color: var(--bad); }
.toast.warn { border-inline-start-color: var(--warn); }
.toast.ok   { border-inline-start-color: var(--ok); }
@keyframes nw-toast { from { opacity: 0; transform: translateY(6px); } }

@media (max-width: 760px) {
  /* Above the bottom bar, full width, so a toast is never behind the nav. */
  .toasts {
    inset-inline: var(--s-3); bottom: calc(var(--h-bar) + var(--safe-b) + var(--s-3));
    align-items: stretch;
  }
  .toast { max-width: none; }
}

/* =========================================================================
   RESPONSIVE
   =========================================================================
   Three regimes. The desktop one is the default above; these narrow it.
   ------------------------------------------------------------------------- */

/* --- tablet: keep all three columns but tighten, and start with icons ---- */
@media (max-width: 1180px) {
  :root { --w-list: 310px; --w-rail-open: 210px; }
}

/* --- small tablet: the rail is icons-only and cannot expand over content -- */
/* Both bounds, not just max-width. `.app:not(.chrome-hidden) > .rail` has
   higher specificity than the phone block's `.app > .rail`, so a bare
   max-width:980px let this explicit width win inside the phone range too and
   pinned the bottom bar to 64px instead of the full viewport width — the rail
   rendered as a 64px stub in the corner. Range-bounding it is the fix; raising
   the phone block's specificity instead would just move the problem. */
@media (min-width: 761px) and (max-width: 980px) {
  :root { --w-list: 288px; }
  /* An expanded rail would eat a third of the width here, so labels are
     suppressed and the rail overlays instead of pushing when opened. */
  .app, .app.rail-icons { --rail-w: var(--w-rail); }
  .app.rail-open > .rail {
    position: fixed; inset-block: 0; inset-inline-start: 0;
    width: var(--w-rail-open); box-shadow: var(--sh-3);
    z-index: var(--z-modal);
  }
  .app.rail-open .rail-txt { display: block; }
}

/* --- phone: rail becomes a bottom bar, list becomes a sheet -------------- */
@media (max-width: 760px) {
  /* rail and list are both position:fixed here (a bottom bar and a sheet), so
     they leave the flex flow entirely and .stage is the only item left.
     Stay FLEX, in column: with `display: block` the stage — which has no height
     of its own, only an absolutely positioned map inside it — collapsed to zero
     and the whole map area rendered blank grey. As a column flex item its
     `flex: 1 1 auto` fills the viewport height. */
  .app { flex-direction: column; }

  /* CRITICAL: on a phone `list-collapsed` means the sheet is PEEKING, not gone.
     The desktop rule above removes it from layout with display:none, which would
     leave the phone with no device list and no way back to it — the toggle would
     look like it had deleted the list. Put it back. */
  .app.list-collapsed > .list { display: flex; }

  /* Bottom bar. Fixed rather than in-flow so the map keeps the full height and
     the sheet can slide over it. */
  /* .app.rail-icons must be matched, not just .app: specificity beats source
     order, so the desktop `.app.rail-icons { --rail-w: var(--w-rail) }` rule
     otherwise wins here too and pins the bottom bar to 64px — it rendered as a
     narrow stub in the corner instead of spanning the screen. */
  /* Both are position:fixed here, so the width comes from inset-inline: 0 and
     the var-driven width must be neutralised. Leaving --list-w at 0 collapsed
     the bottom sheet to nothing — it measured 1px wide. */
  .app > .rail, .app > .list { width: auto; }
  .app > .rail {
    position: fixed; inset-inline: 0; bottom: 0; width: auto;
    height: calc(var(--h-bar) + var(--safe-b));
    padding-bottom: var(--safe-b);
    z-index: var(--z-rail);
  }
  .app.chrome-hidden > .rail { display: none; }

  /* The list becomes a bottom sheet in three positions: peeking, half, full.
     width:auto is required because the desktop rule sets an explicit
     var(--w-list); left as 350px it would ignore inset-inline and leave the
     sheet narrower than the screen. */
  .app > .list {
    position: fixed; inset-inline: 0;
    bottom: calc(var(--h-bar) + var(--safe-b));
    height: 62dvh;
    max-height: calc(100dvh - var(--h-bar) - var(--safe-b) - var(--safe-t) - var(--s-6));
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    box-shadow: var(--sh-3);
    z-index: var(--z-sheet);
    transition: height .24s var(--ease);
    overflow: hidden;
  }
  /* Peeking is a HEIGHT change, not a translate.
     translateY moved the whole box down past its `bottom` anchor, so the sheet
     slid over the bottom bar and hid the navigation completely. Shrinking the
     height keeps the sheet bottom pinned to the top of the bar, which is what
     "peeking" should mean — the grip, the search box and the filter chips stay
     reachable and the bar stays visible. */
  /* 190px, not 124: at 124 the peek ended right after the filter chips, so a
     collapsed sheet showed no devices whatsoever. This leaves the search box,
     the chips and the first row or two visible. */
  .app.list-collapsed > .list,
  .app.list-peek > .list { height: 190px; }
  .app.list-full > .list { height: calc(100dvh - var(--h-bar) - var(--safe-b) - var(--safe-t) - var(--s-4)); }

  /* Panels and the drawer go full-screen above the bar. */
  .app > .panel,
  .app > .drawer {
    position: fixed; inset: 0;
    bottom: calc(var(--h-bar) + var(--safe-b));
    width: auto; max-width: none;
    border-radius: 0; border: 0;
  }
}

@media (max-width: 400px) {
  :root { --h-bar: 54px; }
}

/* Landscape phone: very little height, so the sheet must not swallow the map. */
@media (max-height: 480px) and (orientation: landscape) {
  .app > .list { height: 78dvh; }
  .app.list-collapsed > .list, .app.list-peek > .list { transform: translateY(calc(78dvh - 96px)); }
}
