@import url("tokens.css");

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: var(--surface-0);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: var(--font-size-md);
  height: 100vh;
  overflow: hidden;
}

/* The operations screen, inside the shell rather than instead of it.

   This was a full viewport grid with its own brand cell, from before there
   was any shell to sit in: height 100vh made it exactly as tall as the window
   while the rail's topline was already using the first 56 of them, so the
   topline was covered by the map and the whole page ran one header height too
   long. The brand column went with it, because the rail carries the brand
   now and carrying it twice is what the shell exists to stop.

   min-height: 0 is load bearing on a grid inside a flex column: without it
   the row refuses to shrink below its content and the map pushes the page
   into a scrollbar it should never have. */
.app {
  display: grid;
  grid-template-columns: 264px 1fr 380px;
  grid-template-rows: 56px 1fr;
  grid-template-areas:
    "topbar topbar  topbar"
    "vlist  map     detail";
  flex: 1 1 auto;
  min-height: 0;
}

.brand {
  grid-area: brand;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-4);
  background: var(--surface-1);
  border-right: 1px solid var(--border-subtle);
  border-bottom: 1px solid var(--border-subtle);
  font-weight: 600;
  letter-spacing: 0.01em;
}

.topbar {
  grid-area: topbar;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: 0 var(--space-4);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-subtle);
}

/* Metric discipline: one primary metric, the rest recede. Six cards of
   equal weight means the eye lands nowhere. */
.kpi {
  display: flex;
  flex-direction: column;
  padding: var(--space-1) var(--space-3);
  border-left: 1px solid var(--border-subtle);
}

.kpi__label {
  font-size: var(--font-size-xs);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.kpi__value {
  font-size: var(--font-size-lg);
  color: var(--text-secondary);
}

.kpi--primary .kpi__value {
  font-size: var(--font-size-xl);
  color: var(--text-primary);
}

/* Total, moving, stopped and offline are one family: every truck falls in
   exactly one of the three states, and they sum to the total. Alert is not a
   fifth member of that family, it is a count of trucks already counted in one
   of those three, so it must not be drawn as another card of identical
   weight. Five identical cards is precisely what invited the reading that
   this row shows four exclusive states, and it is the same modelling error
   the map had.

   Drawn as the badge it is, in the same vocabulary as the badge on a vehicle
   row and the ring on the map: no border shared with the family, its own
   enclosure, and the alert wash token that exists for exactly this, a surface
   behind alert text. */
.kpi--badge {
  margin-left: var(--space-2);
  border: 1px solid var(--status-alert);
  border-radius: var(--radius-1);
  background: var(--status-alert-wash);
}

.kpi--badge .kpi__label,
.kpi--badge .kpi__value {
  color: var(--status-alert);
}

.vlist {
  grid-area: vlist;
  background: var(--surface-1);
  border-right: 1px solid var(--border-subtle);
  overflow-y: auto;
}

.map {
  grid-area: map;
  position: relative;
  background: var(--surface-0);
}

#map {
  position: absolute;
  inset: 0;
}

.detail {
  grid-area: detail;
  background: var(--surface-1);
  border-left: 1px solid var(--border-subtle);
  overflow-y: auto;
  padding: var(--space-4);
}

.detail__empty {
  color: var(--text-muted);
  font-size: var(--font-size-sm);
  line-height: 1.6;
}

.detail__name {
  margin: 0 0 var(--space-3) 0;
  font-size: var(--font-size-lg);
  color: var(--text-primary);
}

.detail__list {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  font-size: var(--font-size-sm);
}

.detail__list dt {
  color: var(--text-muted);
}

.detail__list dd {
  margin: 0;
  color: var(--text-secondary);
}

.vcard {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  cursor: pointer;
  transition: background 120ms ease;
}

.vcard:hover {
  background: var(--surface-2);
}

.vcard[aria-selected="true"] {
  background: var(--surface-2);
  box-shadow: inset 3px 0 0 var(--accent);
}

.vcard:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.vcard__name {
  font-weight: 500;
}

.vcard__meta {
  font-size: var(--font-size-sm);
  color: var(--text-muted);
}

.dot {
  width: var(--space-2);
  height: var(--space-2);
  border-radius: 50%;
  background: var(--status-offline);
}

.dot--moving { background: var(--status-moving); }
.dot--stopped { background: var(--status-stopped); }

/* There is no .dot--alert, and there must not be one. A dot on this row means
   what the truck is doing, and a truck is doing exactly one of moving,
   stopped or offline. A fix that cannot be trusted is true on top of one of
   those, so it is the badge below rather than a fourth colour this dot can
   take. The rule that decides it lives in one place only, hasAlert() in
   vehicle-panel.js.

   A word, not only a colour: an operator who cannot separate this red from
   the greys next to it reads the badge because it is written out. It sits in
   the third column .vcard already reserves, beside the state dot rather than
   in place of it. */
.badge {
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-1);
  font-size: var(--font-size-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  line-height: 1;
}

.badge--alert {
  border: 1px solid var(--status-alert);
  background: var(--status-alert-wash);
  color: var(--status-alert);
}

@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/* A blank map should say why it is blank, rather than looking broken. */
.asset-notice {
  position: absolute;
  inset: var(--space-6);
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-6);
  background: var(--surface-1);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-2);
  color: var(--text-secondary);
  text-align: center;
}

.asset-notice__title {
  margin: 0;
  font-size: var(--font-size-lg);
  color: var(--text-primary);
}

.asset-notice__list {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--font-size-sm);
}

.asset-notice__fix {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--text-muted);
}

/* The local fallback still renders a working map (Zimbabwe), unlike the
   "cannot render at all" state above, so this sits in a corner instead of
   covering the map the way the full .asset-notice overlay does. It exists
   so a user who suddenly cannot zoom outside Zimbabwe finds out why instead
   of assuming the map is broken. */
.asset-notice--corner {
  inset: auto var(--space-4) var(--space-4) auto;
  justify-content: flex-start;
  align-items: flex-start;
  max-width: 320px;
  padding: var(--space-4);
  text-align: left;
}

/* The map's own status key. Each truck sits on a marker drawn in its status
   colour, and colour on its own is not a signal an operator with a colour
   vision deficiency can read, so every mark the map draws appears here beside
   its word, drawn the same way. */
.legend {
  position: absolute;
  left: var(--space-4);
  bottom: var(--space-4);
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-2) var(--space-3);
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-2);
  color: var(--text-secondary);
  font-size: var(--font-size-xs);
}

/* The three states in one run, the badge in another. A truck is in exactly
   one of the three; the badge is true or false on top of whichever it is, so
   listing all four side by side was the map telling an operator there are
   four things a truck can be. The rule between the two runs is what says that
   without a sentence, and the badge's own wording finishes the job. */
.legend__group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
}

.legend__group--badge {
  padding-left: var(--space-4);
  border-left: 1px solid var(--border-subtle);
}

.legend__item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Reads as a modifier rather than a label: the states are named, this
   qualifies the mark beside it. */
.legend__modifier {
  color: var(--text-muted);
}

/* A ring, not a disc, because that is what the map draws: the truck itself
   sits inside it, and a filled mark would put a green truck on a green disc
   and lose both. */
.legend__mark {
  flex: none;
  width: 16px;
  height: 16px;
  border: 2px solid var(--status-stopped);
  border-radius: 50%;
  background: transparent;
}

.legend__mark--moving {
  border-color: var(--status-moving);
}

/* Standing still carries a dark pad behind the truck as well as the ring, so
   the state survives being unable to separate this grey from the moving
   green. */
.legend__mark--stopped {
  border-color: var(--status-stopped);
  background: var(--surface-0);
}

/* The badge, drawn the way the map draws it: a state's own mark, unchanged,
   with a second ring around it. Stopped is the state it is shown over here
   because it has to be shown over something, and the point of the mark is
   that the state underneath survives. Two rings rather than one is the
   channel that does not depend on separating this red from that grey. The
   second ring is a shadow rather than a real box because it has to sit
   outside the border without changing the mark's own size and pushing the row
   around. */
.legend__mark--badged {
  border-color: var(--status-stopped);
  background: var(--surface-0);
  box-shadow: 0 0 0 2px var(--surface-1), 0 0 0 4px var(--status-alert);
}

/* Smaller and dimmer, the same way the map draws it: a truck that has gone
   quiet should recede rather than compete with one that is reporting. */
.legend__mark--offline {
  width: 12px;
  height: 12px;
  border-color: var(--status-offline);
  background: var(--surface-0);
  opacity: 0.8;
}

/* MapLibre's own attribution control. The credit is a licence condition, so
   it is always on screen; this only makes it sit in the dark theme instead of
   glaring white over it.

   Both of the control's own classes are named here, and .map in front of
   them, for a reason worth writing down: maplibre-gl.css styles this element
   as .maplibregl-ctrl.maplibregl-ctrl-attrib, two classes, and it loads after
   this file, so anything at or below that weight loses the tie. A first pass
   here used .map .maplibregl-ctrl-attrib, which ties exactly, and the result
   was a rule that half applied: the colours it declared won because the
   vendor rule does not set them, while the background and padding it declared
   silently lost and the credit sat in a white bar over a dark map. */
.map .maplibregl-ctrl.maplibregl-ctrl-attrib {
  padding: var(--space-1) var(--space-2);
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-1);
  color: var(--text-muted);
  font-family: var(--font-ui);
  font-size: var(--font-size-xs);
}

.map .maplibregl-ctrl-attrib a {
  color: var(--text-secondary);
}

.map .maplibregl-ctrl-attrib a:hover {
  color: var(--accent);
}

/* Sign in. Every colour, size and radius comes from tokens.css; three tokens
   were added there for this screen (--accent-hover, --status-alert-wash and
   --font-size-input) rather than inlining the values here. An operator meets
   this screen before anything else, so it is the first thing that has to look
   like a product rather than a form. */
.login {
  display: flex;
  align-items: center;
  justify-content: center;
  /* dvh, not vh: on a phone, 100vh includes the browser chrome, so the card
     sits low and the button can fall under the address bar. This is the one
     screen most likely to be opened on a phone. */
  min-height: 100dvh;
  padding: var(--space-4);
}

.login__card {
  width: 100%;
  max-width: 360px;
  padding: var(--space-6);
  background: var(--surface-1);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-2);
  box-shadow: var(--shadow-2);
}

.login__brand {
  margin: 0;
  font-size: var(--font-size-lg);
  font-weight: 600;
  letter-spacing: 0.01em;
}

.login__lede {
  margin: var(--space-2) 0 var(--space-5);
  color: var(--text-secondary);
  font-size: var(--font-size-sm);
}

.login__error {
  margin: 0 0 var(--space-4);
  padding: var(--space-3);
  border-radius: var(--radius-1);
  background: var(--status-alert-wash);
  border: 1px solid var(--status-alert);
  color: var(--text-primary);
  font-size: var(--font-size-sm);
}

.login__field {
  display: block;
  margin-bottom: var(--space-4);
}

.login__label {
  display: block;
  margin-bottom: var(--space-2);
  color: var(--text-secondary);
  font-size: var(--font-size-sm);
}

.login__input {
  width: 100%;
  padding: var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-1);
  color: var(--text-primary);
  font-family: var(--font-ui);
  font-size: var(--font-size-input);
}

.login__input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.login__submit {
  width: 100%;
  margin-top: var(--space-2);
  padding: var(--space-3);
  background: var(--accent);
  border: none;
  border-radius: var(--radius-1);
  color: var(--surface-0);
  font-family: var(--font-ui);
  font-size: var(--font-size-md);
  font-weight: 600;
  cursor: pointer;
}

.login__submit:hover {
  background: var(--accent-hover);
}

.login__submit:focus-visible {
  outline: 2px solid var(--text-primary);
  outline-offset: 2px;
}

/* Sign out. Pushed to the far end of the topbar, away from the KPI figures an
   operator reads constantly, so it is findable without being in the way of
   the numbers. Quiet by default and only picking up weight on hover: this is
   the one control on the screen nobody wants to hit by accident. */
.topbar__signout {
  margin-left: auto;
}

.signout {
  padding: var(--space-2) var(--space-3);
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-1);
  color: var(--text-secondary);
  font-family: var(--font-ui);
  font-size: var(--font-size-sm);
  cursor: pointer;
}

.signout:hover {
  border-color: var(--accent);
  color: var(--text-primary);
}

.signout:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Says the map is not live. Sits over the map at the top, spanning it, so it
   is read before the markers underneath are believed. It is not a toast: a
   stale map is a condition rather than an event, and it stays up until a poll
   succeeds and connection-banner.js hides it again.

   --status-loading, which is this system's amber, rather than the alert red.
   Nothing has broken irrecoverably; what is on screen is simply older than it
   looks, and red beside a red alert badge would say the two are the same kind
   of thing. */
.stale-banner {
  position: absolute;
  top: var(--space-4);
  left: var(--space-4);
  right: var(--space-4);
  z-index: 3;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-1);
  border: 1px solid var(--border-strong);
  border-left: var(--space-1) solid var(--status-loading);
  border-radius: var(--radius-2);
  color: var(--text-primary);
  font-size: var(--font-size-sm);
  line-height: 1.45;
  box-shadow: var(--shadow-2);
}

/* hidden is an attribute, and a display rule elsewhere in a stylesheet can
   quietly beat it. Said here so the banner cannot be shown by the cascade
   when the script believes it is hidden. */
.stale-banner[hidden] {
  display: none;
}
