/*
  Tera Drive workspace manager.

  The tokens below are lifted from the app, not re-picked by eye — the sage
  canvas and the status triple are Ocean's, from `src/theme/appThemes.ts`, and
  the koru geometry is `src/components/navigation/tabBarMetrics.ts`. A console
  that drifts from the phone stops looking like the same product, and colour is
  the first thing to go.

  Light only. The app's canvas does not currently change with the colour scheme
  (see the note in the handover), so a dark web console would be the only dark
  surface in the product.
*/

:root {
  /* Ocean preset — appThemes.ts */
  --canvas: #a7c4c9;
  --brand: #2f7780;
  --brand-pressed: #276169;
  --text-on-brand: #ffffff;

  /* The status triple. `ok` IS the brand colour; upcoming is navy, not amber. */
  --status-ok: #2f7780;
  --status-upcoming: #173d52;
  --status-urgent: #b3261e;

  --surface: #ffffff;
  /*
    THE GREYS CARRY MORE NOW, and the numbers are why.

    #f2f5f6 was 1.10:1 against white. A panel needs about 1.2 before its edge
    is findable without hunting for it, so every recessed box in this console
    — stat tiles, the conduct plate, the bar chart's own tracks — was a shape
    you had to already know was there. #e6edef is 1.19:1: still a whisper,
    but a whisper you can locate.

    It is a background everywhere it appears and a hover state in half of
    those, so darkening it cannot strand any text; the greys below were the
    half of this that could.
  */
  --surface-muted: #e6edef;

  /*
    A RECESSED SURFACE, WHICH IS NOT ALWAYS THE SAME GREY.

    Stat wells, chart tracks and the conduct plate are all "the shade a step
    below the thing they sit in", and on the console's white panels that is
    --surface-muted. On the vehicle card it is not: that card already has a
    pale blue block in it — the drive tile is --shell — and a grey well
    beside a blue one reads as two materials rather than one card.

    So the surfaces name the ROLE and the card redefines it. One override on
    `.sheet` moves every recessed shape on it at once, and nothing else in
    the product changes.
  */
  --card-surface: var(--surface-muted);
  --text: #111b1d;
  --text-secondary: #56676b;
  /*
    `--muted` HAS BEEN USED SINCE BEFORE IT EXISTED.

    Five rules across this file ask for it — an alert's timestamp, the place
    form's hint, the menu's items, the speeding badge's unit — and none of
    them got it: an unresolved custom property makes the declaration invalid
    at computed-value time, so `color: var(--muted)` silently inherits
    instead. Every one of those was quietly the colour of whatever it sat
    in, which is why the hints never looked like hints.

    It is an alias rather than a sixth grey: it always meant the secondary
    text colour, and two names for one value is how palettes drift.
  */
  --muted: var(--text-secondary);

  /*
    THE MIDDLE OF A SEVERITY RAMP, and deliberately not part of the status
    triple above — `upcoming` is navy because it means "not yet", which is a
    state. This is a degree: five over is the same KIND of thing as twenty
    over, less of it. Kept in step with TRAIL_AMBER in map.js, which draws
    the same band on the trail itself.
  */
  --warn: #d97a20;
  /*
    THE THIRD GREY WAS BELOW THE LINE, measurably. #7e8f93 is 3.37:1 on white
    and 3.07:1 on a muted panel — and it is used at 11 and 12 pixels, for
    chart axis labels and the notes under them, which is exactly the size
    that needs 4.5:1. It was not "a bit light"; it was unreadable to anyone
    whose eyes or screen were not perfect.

    #5f7176 is 5.11:1 on white and 4.66:1 on --surface-muted. Chosen against
    the WORSE of the two, because these labels sit on both and a value that
    only passes on white passes in the wrong half of the product.
  */
  --text-tertiary: #5f7176;
  /* And the hairlines with them: 0.08 and 0.06 over a near-white page are
     about a third of a shade, which reads as an edge that did not render
     rather than one drawn faintly on purpose. */
  --border: rgba(0, 0, 0, 0.14);
  --divider: rgba(0, 0, 0, 0.10);

  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 20px;
  --radius-2xl: 28px;
  --radius-pill: 999px;

  /*
    SPREAD IS HALF THE BLUR, the same rule the bar's tabs and the map's layer
    tab are drawn with: a shadow then reaches only in the direction it is
    offset, and does not haze the surface on the other three sides. The old
    pair had spread 0, so every card sat in a ring of grey.
  */
  --shadow-card: 0 10px 20px -10px rgba(16, 45, 52, 0.22);
  --shadow-raised: 0 18px 34px -17px rgba(16, 45, 52, 0.3);

  /* Koru — tabBarMetrics.ts. Ball radius is 0.75 × stroke, and both balls sit
     flush with the line's bottom edge, which swallows the line's own cap. */
  --koru-stroke: 6px;
  --koru-ball: 9px;
  --koru-span: 22px;
}

* { box-sizing: border-box; }

/*
  `hidden` MEANS HIDDEN. ONCE.

  The attribute's whole effect is a UA rule of `display: none`, and any class
  that sets `display` at all beats it on specificity. Every panel on this page
  is toggled with `.hidden = true` from JS, so each one that also had a
  `display: flex` needed a matching `[hidden]` override — and this file had
  grown NINE of them, added one at a time as each panel was caught showing
  itself. The tenth was the account chip, which is why a signed-out console
  offered you "Sign in" and "Sign out" at the same time under the words
  SIGNED OUT.

  This is the rule those nine were each approximating. `!important` is the
  right tool exactly once and this is the case it was invented for: an
  element saying it is not on the page outranks any opinion about how it
  would be laid out if it were.
*/
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
}

body {
  background: var(--canvas);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  font-size: 15px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

/* ---------------------------------------------------------------- layout */

/*
  A third for the rings, the rest for detail. The rail is the constant state
  of the workspace and the right-hand side is whatever you are looking at, so
  the rail does not scroll away with the content — it sticks.

  `minmax(320px, 33vw)`: a third of a wide screen, but never so narrow that a
  312-unit ring has to shrink into illegibility on a laptop.
*/
/*
  A RAIL, NOT A COLUMN.

  This was `minmax(320px, 33vw)`: a third of the window, whatever the window
  was. On a laptop that is 420px and on a 27" monitor it is 640px — of a
  vehicle list, a places list and one ring, all of which are as wide as a
  name and a number and no wider. The rest was empty sage, and the map — the
  thing the page is for — paid for every pixel of it.

  Clamped instead: never so narrow that a rego wraps, never so wide that it
  competes with the map.
*/
/*
  A BAR OF WHATEVER HEIGHT IT NEEDS, AND THE REST.

  `grid-template-rows: auto minmax(0, 1fr)` rather than a `--topbar-h`
  anybody has to keep in step with the bar's own padding and type. The first
  attempt hard-coded 56px, the bar came out 79, and every pane below it hung
  23px off the bottom of the window. The layout can measure; a constant
  cannot.

  `minmax(0, 1fr)` and not `1fr`: a grid row's default minimum is its
  content, so a long vehicle table would push the row taller than the window
  and take the map's full-height panes with it.
*/
.shell {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  height: 100vh;
}

/*
  The sections, the product and the account, on one line across the top.

  `position: sticky` rather than fixed: it scrolls with the Fleet and People
  tables, which are pages rather than panes, and stops at the top when it
  gets there. On the map view nothing scrolls, so it simply sits.
*/
/*
  NO SURFACE OF ITS OWN.

  It had a white wash and a hairline under it, which made it a strip laid
  ON the page rather than part of it — and since the rail below is bare
  sage, that hairline was a seam running across the top of the window
  separating two things that are the same thing. The bar, the rail and the
  page behind them are one field of sage now, and the only edge on the
  screen is the one that matters: where the sage stops and the pane begins.

  The blur went with the wash. `backdrop-filter` samples what is behind it,
  and what is behind this is a flat colour — blurring one colour returns
  that colour, so it was a per-frame compositing cost for no pixels.
*/
.topbar {
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 28px;
  /* Left inset matches the arch's, so the mark starts on the same line the
     column below it does. */
  padding: 12px 22px 12px var(--rail-pad);
  /*
    STATED, NOT INHERITED FROM THE WORDMARK. The bar's row in .shell is
    `auto`, so its height used to be whatever the brandmark measured — 50px,
    by accident of the mark being 26. A bar is a piece of the chassis and
    should be a number somebody chose.

    82 IS WHERE THE FLEET ARCH STARTS. The bar's underside, the top of the
    map and the top of the arch are all this line — the bar was brought down
    to meet the arch rather than the arch lifted to meet the bar, so the
    column keeps the breathing room it had above it.
  */
  min-height: 82px;
  /* The frame the centred nav measures itself against. */
  position: relative;
}

/*
  THE NAV IS CENTRED ON THE BAR, NOT ON WHAT IS LEFT OF IT.

  `margin: 0 auto` would centre it in the space the brandmark does not
  occupy, which puts it half the brandmark's width right of true centre —
  close enough to look like a mistake rather than a decision. Taking it out
  of the flow instead means it is centred on the window, and stays centred
  whatever the wordmark beside it does.
*/
/*
  CENTRED ON THE MAP, NOT ON THE WINDOW.

  The bar runs the full width but the rail sits under its left end, so a nav
  centred on the window reads as pushed left — it is centred on glass that is
  half covered. The well's centre is the window's centre shifted right by half
  the rail, which is what this says.
*/
.topbar nav {
  position: absolute;
  left: calc(50% + var(--rail-w) / 2);
  transform: translateX(-50%);
  /* Floor to ceiling of the bar, so its buttons can be the bar. */
  top: 0;
  bottom: 0;
}

/*
  ONLY WHILE THERE IS ROOM FOR IT. Out of the flow, the nav cannot push the
  wordmark aside — it simply sits on top of it. The brandmark takes ~144px
  and the nav ~352px, so below about 900 the two want the same pixels; there
  the nav goes back into the flow and the bar grows instead of overlapping.
*/
@media (max-width: 900px) {
  .topbar nav { position: static; transform: none; }
}

/*
  SIGN IN AND SIGN OUT ARE OFF THE BAR.

  Hidden rather than deleted: app.js wires the sign-in button, the account
  chip and the sign-out click directly by id, so removing the markup would
  throw mid-render — and the console's failure mode for a throw in there is
  to fall back to sample data without saying anything.

  NOTE THIS LEAVES NO WAY IN. An existing session still restores itself, but
  a signed-out visitor now has no control to sign in with. Wherever that
  belongs — the sidebar foot, a menu on the mark — it needs to go there.
*/
.topbar .account { display: none; }

/* On the bar the state is a quiet prefix to the address, not a heading. */
.topbar .account-mode { font-size: 10.5px; }
.topbar .account-who { color: #fff; }
.topbar .account-signout { color: rgba(255, 255, 255, 0.72); }
.topbar .account-signout:hover { color: #fff; }

/*
  THE RAIL'S WIDTH, NAMED. The grid lays the rail out with it and the topbar
  nav centres itself over what is left of the window with it. Two places, one
  number: a literal in either would drift the nav off the map well the first
  time the rail was resized.
*/
:root {
  --rail-w: clamp(272px, 22vw, 332px);
  /*
    The fleet column's own margin — and therefore the left edge of everything
    down that side of the window. The bar's brandmark is set from it too, so
    the mark, the wordmark and the arch below them share one vertical line.
    It deliberately does NOT collapse with the column: the logo should not
    step sideways when somebody hides the fleet.
  */
  --rail-pad: 30px;
  /*
    HOW FAR THE BAR HANGS PAST ITSELF for the current section. The map's own
    floating controls are centred on the same well the nav is, so they want
    the same pixels — this is the number that keeps them clear of each other.
  */
  /*
    How far the tab hangs past the bar on hover — the only time it hangs at
    all. Deep enough to hold the label clear of the corner radius: the
    label's bottom sits 22px past the bar, so anything under ~34 here clips
    the word on the curve.
  */
  --nav-drop: 38px;
  /*
    THE CONCAVE CORNER where a tab leaves its bar. Nothing is drawn AT a 90°
    junction, so this is the radius of the quarter-disc of bar colour that
    fills the corner beside the tab and curves away from it.
  */
  --tab-fillet: 12px;

  /* The layer tab: icons only at rest, icons plus their names on hover. */
  --rail-w-icons: 50px;
  --rail-w-named: 176px;
  /*
    THE LINE EVERY LEFT RAIL HANGS FROM. One value, read by the map's rail
    and the fleet's, so switching views never jumps the rail. 160px is half
    the map rail's built height — 7 buttons × 38px, 2 splits × 5px, 8 gaps
    × 4px, 12px of padding = 320px — which keeps the map rail exactly where
    centring kept it, while rails with fewer buttons start at the same line
    instead of a shallower centre.
  */
  --rail-top: calc(50% - 160px);
}

/*
  THE FLEET COLUMN, CLOSED. Setting the rail's width to zero is the whole of
  it: the shell's grid lays the column out with this and the top bar's nav
  centres itself on what is left of the window with this, so both follow from
  the one number and cannot disagree about whether the column is there.

  AFTER :root, NOT BEFORE IT. `:root` and `.fleet-hidden` both weigh (0,1,0)
  and both match <html>, so the one written later wins — placed above, this
  rule lost every time and the button toggled nothing.
*/
.fleet-hidden { --rail-w: 0px; }

/*
  A ZERO-WIDTH COLUMN IS STILL 60px OF PADDING. Padding does not shrink with
  the box, so the sidebar kept its 30px either side, overflowed the closed
  column, and — sitting at z-index 30 — painted that strip over the map and
  over the layer rail's own left edge.

  `visibility` as well as width, because a column you cannot see is still one
  the keyboard will tab into. It hides the subtree from the accessibility
  tree and from the tab order; the width is only what the eye sees.
*/
.fleet-hidden .sidebar {
  padding-left: 0;
  padding-right: 0;
  visibility: hidden;
}

.shell-body {
  min-height: 0;
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
  transition: grid-template-columns 280ms cubic-bezier(0.2, 0, 0, 1);
  /* `stretch`, so both panes are exactly the height of the row and can hold
     their own scrollbars. `start` left them content-height, which is what
     made the rail's foot land in the middle of the window. */
  align-items: stretch;
}

.sidebar {
  /* A grid item's min-width is `auto`, which is its content — without this
     the column refuses to close and the arch just squashes the map. */
  min-width: 0;
  overflow-x: hidden;
  /*
    Wider margins than the 20px it had, so the arch reads as set into the
    shell rather than wedged between its edges. No padding at the FOOT: the
    arch is meant to run out of the bottom of the window, and a floor under
    it would stop it short and put a lip back on the shape.
  */
  padding: 0 var(--rail-pad) 0;
  display: flex;
  flex-direction: column;
  gap: 20px;
  /* The grid row is already exactly the window less the bar, so the rail
     fills it and scrolls inside itself. That is also what gives the build
     stamp's `margin-top: auto` a floor to be pushed against. */
  height: 100%;
  min-height: 0;
  /*
    THE COLUMN DOES NOT SCROLL — THE ARCH INSIDE IT DOES.

    While the sidebar was the scroller, the arch was as tall as its contents,
    so on a page with the count discs it ended partway down and you scrolled
    to a hard flat edge. Its bottom is square BECAUSE it is meant to leave the
    window; an end you can see is the one thing the shape cannot have.
  */
  overflow: hidden;
}

/*
  Two lines, tight, at the head of the bar: the product over the workspace.
  It used to be the top of a rail with room to spare; on a 56px bar the
  leading has to come down or the two lines push the bar taller than the
  nav needs it to be.
*/
.brandmark {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 11px;
  padding: 0;
  flex: none;
  line-height: 1.2;
}

/*
  THE MARK CARRIES THE COLOUR OF THE TEXT IT SITS WITH, because it is set in
  `currentColor` through a mask rather than being a coloured picture. One
  file serves the pale topbar, the dark topbar and the sign-in screen.
*/
.brandmark .mark {
  flex: none;
  width: 30px;
  height: 26px;
  background-color: currentColor;
  -webkit-mask: url(brand/aethera-mark.png) center / contain no-repeat;
  mask: url(brand/aethera-mark.png) center / contain no-repeat;
}


.brandmark h1 {
  margin: 0;
  font-size: 18px;
  font-weight: 800;
  letter-spacing: -0.3px;
  color: var(--text-on-brand);
}

#workspace-name {
  font-size: 11.5px;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.72);
}

/*
  A row, not a column. Four stacked links cost ~200px of the rail's height,
  and the rail's job is to show three rings at once — a status rail you have
  to scroll is just a page.
*/
/*
  A row, on a bar, at last.

  This spent its life in a 282px rail: first as a column of four links
  costing 200px of the rail's height, then as a row that wrapped onto two
  lines because five section names do not fit across a rail at any size
  worth reading. On the top bar it is simply a row.
*/
.nav { display: flex; flex-direction: row; gap: 2px; padding: 0; flex: none; }

.nav button {
  appearance: none;
  border: 0;
  background: transparent;
  font: inherit;
  /* Five section names will not fit one line of a 282px rail at body size,
     and shrinking them until they do would make the page's own navigation
     the smallest text on it. So: tightened enough to wrap 3 + 2 rather than
     4 + 1, which reads as two rows of a menu instead of one row and an
     orphan. */
  font-size: 14px;
  color: rgba(255, 255, 255, 0.72);
  /*
    A FULL-HEIGHT SEGMENT OF THE BAR, not a chip sitting inside it.

    The nav is stretched to the bar's own box, so each button runs from the
    bar's top edge to its bottom edge. That is the whole trick: the lobe can
    then start at the bar's bottom edge exactly, instead of at an offset
    guessed from the bar's padding — and the bar's height stops depending on
    how big the current section's label happens to be.
  */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Wide enough that the five glyphs sit on an even rhythm rather than each
     hugging its own drawing, and that the widest label ("Bookings") has a
     lobe to appear in that is not narrower than the word. */
  min-width: 84px;
  padding: 0 6px;
  white-space: nowrap;
  cursor: pointer;
  position: relative;
  font-weight: 600;
}

.nav-icon {
  /* Sized for the bar it is in. These were 22 and 36 when the bar was 50 tall
     and read as small the moment it became 82 — a glyph has to grow with the
     surface under it or the bar just looks empty. */
  width: 28px;
  height: 28px;
  flex: none;
  transition:
    width 220ms cubic-bezier(0.2, 0, 0, 1),
    height 220ms cubic-bezier(0.2, 0, 0, 1),
    transform 220ms cubic-bezier(0.2, 0, 0, 1);
}

/*
  THE WORD LIVES BELOW THE BAR.

  `top: 100%` is the bar's bottom edge exactly — that only holds because the
  button is a full-height segment of the bar rather than a chip inside it.
  So the label sits in the space the lobe opens, and is simply not there when
  the lobe is closed.

  FADED, NEVER `display: none`. The button's accessible name is this text; a
  glyph with no name is a button a screen reader can only call "button".
*/
.nav-label {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  margin-top: 5px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
  opacity: 0;
  pointer-events: none;
  transition: opacity 140ms ease;
}

.nav button:hover .nav-label { opacity: 1; }

.nav button:hover { color: #fff; }

.nav button[aria-current="page"] { color: #fff; }

/*
  ============================================================
  THE BAR DEFORMS — it does not grow a menu
  ============================================================

  Hovering a section swells the bar downwards behind that button. The lobe is
  painted in the bar's OWN colour, so everything above the bar's bottom edge
  is invisible by construction and the only part you ever see is the piece
  hanging below it. That is what makes it read as one surface bulging rather
  than a panel appearing underneath.

  IT DOES NOT PUSH ITS NEIGHBOURS. Tonui's bar widens the hovered button and
  shoves the rest along, which it can afford with icons. Five text labels
  doing that would shuffle the whole row sideways on every hover — and the
  row is centred, so it would slide against the window as well.

  A NEGATIVE z-index IS DELIBERATE. The lobe has to sit behind its own label
  and it is an absolutely positioned pseudo-element, which would otherwise
  paint over it. Backgrounds of a stacking context are painted before its
  negative-z descendants, so this lands above the bar's own surface and below
  the text — exactly the one layer available.
*/
.nav button::before {
  content: '';
  position: absolute;
  z-index: -1;
  left: 0;
  right: 0;
  /*
    THE LOBE IS ONLY THE PART BELOW THE BAR.

    `top: 100%` is the bar's bottom edge exactly — the button is a full-height
    segment of the bar — so the lobe starts where the bar ends and grows down
    from there. It used to span the whole button, with the part over the bar
    invisible because it was the same colour.

    THAT IS WHAT MAKES A SHADOW POSSIBLE. A negative z-index puts this above
    the bar's own surface, so an element overlapping the bar cast onto it and
    drew a soft outline up both sides of the button — which is precisely what
    made it read as a card floating in front. With nothing overlapping the
    bar, there is nothing to haze it.
  */
  top: 100%;
  height: 0;
  background: var(--shell, #dce8ea);
  /* Square where it leaves the bar, soft where it ends — the shape reads as
     the bar stretching rather than as a tab clipped to a rectangle. */
  border-radius: 0 0 var(--radius-lg, 16px) var(--radius-lg, 16px);
  /*
    OFFSET DOWN AND PULLED IN. The 7px drop plus the -3px spread put the
    blur's first pixel about 4px below the bar's edge, so the bar keeps a
    clean line and only the tab casts. The negative spread also means the
    shadow is not painted at all while the height is 0 — a shrunk-to-nothing
    rect has nothing to cast — so it grows in with the tab instead of
    snapping on at the bar's edge.
  */
  /* Spread is half the blur, so nothing bleeds sideways onto the corner
     fillets — see the note on the layer tab's shadow. */
  box-shadow: 0 7px 12px -6px rgba(16, 45, 52, 0.3);
  transition: height 220ms cubic-bezier(0.2, 0, 0, 1);
}

.nav button:hover::before { height: var(--nav-drop); }

/*
  THE TWO CORNERS WHERE THE TAB LEAVES THE BAR.

  A tab dropping out of a straight edge meets it at a right angle on each
  side, and a right angle reads as two pieces butted together however well
  the colours match. These are quarter-discs of the bar's own colour sitting
  just outside the tab, hollowed out by a circle centred on their far corner
  — so the bar's underside runs into the tab's side along a curve.

  Absolutely positioned because the button is a flex container: a static
  pseudo-element here would become a flex item and sit in the row.
*/
.nav button::after {
  content: '';
  position: absolute;
  z-index: -1;
  top: 100%;
  left: calc(-1 * var(--tab-fillet));
  right: calc(-1 * var(--tab-fillet));
  height: var(--tab-fillet);
  pointer-events: none;
  background:
    radial-gradient(circle at bottom left,
      transparent 0 var(--tab-fillet), var(--shell, #dce8ea) var(--tab-fillet))
      left top / var(--tab-fillet) var(--tab-fillet) no-repeat,
    radial-gradient(circle at bottom right,
      transparent 0 var(--tab-fillet), var(--shell, #dce8ea) var(--tab-fillet))
      right top / var(--tab-fillet) var(--tab-fillet) no-repeat;
  opacity: 0;
  transition: opacity 140ms ease;
}

.nav button:hover::after { opacity: 1; }

/*
  THE SECTION YOU ARE IN IS BIGGER, not just marked.

  Its label steps up three points and the bar hangs further behind it, so the
  current section is the largest thing on the bar. The button's height comes
  from the bar rather than from its own text, so the larger label cannot make
  the bar grow — only the lobe changes.
*/
/*
  THE GLYPH GROWS WHEN YOU REACH FOR IT, and stays grown for the section you
  are in — 36px in a 50px bar either way, so it never leaves the bar. What
  separates the two is colour: brand teal for where you are, grey for where
  you are pointing.
*/
.nav button:hover .nav-icon,
.nav button[aria-current="page"] .nav-icon {
  width: 44px;
  height: 44px;
  stroke-width: 1.5;
}

/*
  NO TAB FOR THE SECTION YOU ARE IN.

  The tab is what you get for reaching towards a section, so leaving one out
  permanently under the current one made the bar look reached-for at rest —
  and it hung over the map's own controls the whole time you were on the map.
  Being there is said by the glyph instead. Hovering the current section
  still opens its tab, like any other.
*/

@media (prefers-reduced-motion: reduce) {
  .nav button::before,
  .nav-icon { transition: none; }
}

/* --------------------------------------------------------------- account */

/*
  Whose workspace this is, and whether the numbers below are real. Sits
  under the brandmark; white-on-sage like the rail headings — chrome, not
  content.
*/
.account {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0;
  min-width: 0;
}

.account-mode {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.82);
}

.account-signin {
  border: 1px solid rgba(255, 255, 255, 0.75);
  background: rgba(255, 255, 255, 0.55);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  padding: 6px 16px;
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.account-signin:hover { background: rgba(255, 255, 255, 0.75); }

.account-chip {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  max-width: 100%;
}

.account-who {
  font-size: 13px;
  font-weight: 700;
  color: var(--status-upcoming);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-signout {
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  color: rgba(23, 61, 82, 0.66);
  cursor: pointer;
  padding: 2px 0;
  text-decoration: underline;
  flex: none;
}

/* The sign-in dialog — the app's card, as a modal. */
#signin-dialog,
#signup-dialog {
  border: 0;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-raised);
  padding: 26px 28px;
  width: min(360px, 90vw);
}

#signin-dialog::backdrop,
#signup-dialog::backdrop { background: rgba(16, 45, 52, 0.45); }

#signin-dialog h2,
#signup-dialog h2 { margin: 0 0 4px; font-size: 20px; }

.signin-hint { margin: 0 0 18px; font-size: 13px; color: var(--text-secondary); }

#signin-form label,
#signup-form label:not(.signup-consent) {
  display: flex;
  flex-direction: column;
  gap: 5px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: 14px;
}

#signin-form input,
#signup-form input:not([type=checkbox]) {
  font: inherit;
  font-size: 15px;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-muted);
}

#signin-form input:focus-visible,
#signup-form input:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

.signin-error {
  margin: 0 0 12px;
  font-size: 13px;
  font-weight: 600;
  color: var(--status-urgent);
}

.signin-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

.button-quiet {
  border: 0;
  background: none;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  color: var(--text-secondary);
  cursor: pointer;
  padding: 8px 12px;
}

/* ------------------------------------------------------------ status rail */

/*
  ============================================================
  THE FLEET PILL
  ============================================================

  One white lozenge holding the ring and the vehicles under it, set into the
  shell rather than sitting on it — hence an INNER shadow. The shell is the
  frame; this is the thing the frame holds.

  THE RADIUS AND THE PADDING ARE ONE DECISION. A full pill radius makes the
  top cap a semicircle of half the pill's width, and the ring below fills the
  pill's width less the padding — which puts the ring's centre exactly on the
  cap's centre and leaves the same gap all the way round the curve. Change
  the padding without changing the ring and the ring stops nesting.
*/
.rail {
  display: flex;
  flex-direction: column;
  gap: 16px;
  /*
    IT RUNS TO THE FOOT OF THE RAIL. Sized by its contents it stopped
    wherever the roster happened to end, which made the shape a lozenge that
    the fleet had drawn rather than a column the fleet is kept in — and with
    a short fleet it sat as a stub in the top corner.

    `min-height: 0` so a long roster can still shrink it inside the sidebar's
    own scroller rather than pushing the build stamp off the bottom.
  */
  /*
    GROW TO FILL, NEVER SHRINK BELOW THE CONTENT.

    This was `1 1 auto` with `min-height: 0`, so that a long roster could
    shrink the arch inside the sidebar's own scroller. What it actually did
    was let the arch's BOX end above its contents: on the fleet page the
    three count discs spilled out of the bottom of the white, and the build
    stamp — laid out after the arch — landed in the middle of them. The
    sidebar scrolls; the arch should simply be as tall as what it holds.
  */
  /* Exactly the height of the column, never more: the arch's foot is always
     the window's foot, and everything it holds scrolls inside it. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 14px 14px 20px;
  /*
    AND NEITHER MAY ITS CHILDREN. A flex column shrinks its items by default,
    so the ring's section squeezed and the count discs inside it hung out of
    the bottom of the arch — past the white, and past the end of the sidebar's
    own scroll, so the last one could not be reached at all.
  */
  background: var(--surface, #fff);
  /*
    NO INTERIOR SHADOW, and this is why.

    The arch's cap is a semicircle 14px outside the ring, so ANY inset shadow
    traces a curve concentric with the ring — a second line running parallel
    to the ring's own, which is exactly the doubling it looked like. Offsets
    do not help: an inset shadow follows the border radius whichever way it
    is pushed. The depth is carried by the ring lifting off the arch instead.
  */
  /*
    AN ARCH, NOT A LOZENGE. Capped at the top where the ring nests into it,
    and cut square at the bottom so it leaves the window rather than closing
    — the shape carries on past the screen instead of ending on it. The 44px
    of bottom padding went with the cap it was clearing.
  */
  border-radius: var(--radius-pill, 999px) var(--radius-pill, 999px) 0 0;
}

.rail > * { flex-shrink: 0; }

.rail-item { display: flex; flex-direction: column; align-items: center; gap: 6px; }

.rail-item h2,
.roster-head h2 {
  margin: 0;
  align-self: flex-start;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  /*
    THIS WAS WHITE — 82% white, from when the rail was a dark column. On the
    pale shell it was already almost unreadable; on the white pill it would
    have been nothing at all.
  */
  color: var(--text-tertiary, #7e8f93);
}

/*
  "FLEET" IS NOT DRAWN, ONLY ANNOUNCED. The ring says "0 vehicles" across its
  own middle, so the word above it was labelling a label — and it has to come
  out of the flow for the ring to reach the top of the pill and nest in the
  cap. Kept in the DOM because it is the section's heading, and a screen
  reader still needs the outline.
*/
.rail-item h2 {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/*
  THE ROSTER SITS ON THE CANVAS, not on a white card. The sidebar already
  carries two heavy white shapes — the ring's face and the nav's glass — and a
  third slab under them turns a calm rail into a stack of panels. Navy on the
  sage reads at about 9:1, so the text does not need paper behind it to be
  legible; hairlines in white at low alpha are enough to separate the rows.
*/
.roster { display: flex; flex-direction: column; gap: 10px; }

.roster-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.roster-count {
  font-size: 12px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  /* White, for the same dead reason as the headings beside it. */
  color: var(--text-tertiary, #7e8f93);
}

.roster-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.roster-row {
  display: flex;
  align-items: center;
  gap: 10px;
  /*
    NO SIDE PADDING. The ring fills the arch's content box exactly, so zero
    here puts every row's ends on the ring's widest points — the column has
    one left edge and one right edge rather than three that nearly agree.
  */
  padding: 9px 0;
  /* The separators were WHITE, from the dark rail. Invisible on the arch. */
  border-top: 1px solid rgba(16, 45, 52, 0.09);
}

.roster-row:last-child { border-bottom: 1px solid rgba(16, 45, 52, 0.09); }

/*
  A place row IS the "show it to me" button, so it has to look pressable and
  answer the keyboard. The tint is the same white wash the rest of the rail
  uses for hover; the bleed into the padding is so the highlight reads as a
  band across the rail rather than a rectangle around the words.
*/
.place-row {
  cursor: pointer;
  border-radius: 8px;
  /*
    NO BLEED. It used to widen 8px each side so the hover highlight read as a
    band across the rail — which it did, on a rail whose edges nothing else
    lined up with. Inside the arch the ring sets the column's edges, and a row
    reaching past them puts three different left edges in one narrow column.
    The highlight is still a full-width band; the width is just the ring's.
  */
  transition: background 120ms ease;
}

/* Was a white wash, for a dark rail. On the white arch it was no wash. */
.place-row:hover { background: rgba(16, 45, 52, 0.05); }

.place-row:focus-visible {
  outline: 2px solid var(--brand, #2f7780);
  outline-offset: -2px;
}

@media (prefers-reduced-motion: reduce) {
  .place-row,
  .place-stats,
  .place-actions { transition: none; }
}

/* The same mark the map uses for a resting vehicle: a white rim around the
   status colour, so the two surfaces agree on what a vehicle looks like. */
.roster-dot {
  width: 11px;
  height: 11px;
  flex: none;
  border-radius: 50%;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.75);
}

.roster-who {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
}

.roster-who b {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--status-upcoming);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.roster-who small {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.3px;
  color: rgba(23, 61, 82, 0.6);
  white-space: nowrap;
}

.roster-empty {
  padding: 12px 0;
  font-size: 12.5px;
  color: rgba(23, 61, 82, 0.66);
  border-top: 1px solid rgba(16, 45, 52, 0.09);
}

/*
  Capped at 164, not the phone's 312. That cap was set when three rings had to
  share a laptop's height; with one ring it could have grown, and instead it
  shrank twice — the room went to the lists underneath, which are the things
  you actually read. A ring showing one number does not get more truthful at
  200px, and the vehicles and places below it were being pushed off a laptop
  screen to pay for the extra glass. The drawing is identical, just smaller,
  which is what a viewBox is for.
*/
.ring {
  position: relative;
  /* The full inner width of the pill — see the nesting note on .rail. It was
     capped at 164px, which left the ring floating in the middle of a column
     rather than being the thing the column is built around. */
  width: 100%;
  aspect-ratio: 1;
  /* So the counter inside can be sized against the ring rather than against
     a fixed pixel value that only looked right at 164px wide. */
  container-type: inline-size;
}

/*
  THE DISC IS WHITE AND SO IS THE PILL IT SITS IN, so the only thing that can
  separate them is depth. The pill is cut into the shell (inner shadow); the
  disc lifts out of the pill (drop shadow). On the SVG rather than on .ring,
  so the shadow follows the disc and its arcs and does not smear the counter
  drawn over the top of them.
*/
.ring-svg {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
  filter: drop-shadow(0 6px 14px rgba(16, 45, 52, 0.16));
}

/*
  CENTRED, AND SAID OUT LOUD.

  `align-items: center` centres each LINE'S BOX, and for a one-word label
  that box is the width of the word, so the text looked centred by accident.
  The moment a label was capped and wrapped — "speeding or idling", in the
  conduct ring — the box became wider than its lines and they set left
  inside it, a little off-centre under a value that was not.

  `text-align` is what was actually meant all along.
*/
.ring-centre {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  pointer-events: none;
}

/*
  Proportional figures, not tabular. Tabular-nums pads a "1" out to a "0"'s
  width, which opens a visible gap inside "10" at this size — the same reason
  the app's hero counter avoids them.
*/
/*
  SIZED AGAINST THE RING, not in pixels. These were 34px and 12px, which were
  a fifth and a fourteenth of a 164px ring; the ring is now as wide as the
  pill allows, and fixed sizes left a big disc with small type marooned in
  the middle of it. In container units they hold that proportion at any width.
*/
.ring-value {
  font-size: 23cqw;
  line-height: 1.05;
  font-weight: 800;
  letter-spacing: -0.03em;
}

.ring-label { font-size: 7cqw; font-weight: 600; color: var(--text-secondary); }

/* An arc is a target, so it should say so before it is clicked. */
.ring-hit { cursor: pointer; }
.ring-hit:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/*
  The back control, where the app puts it: inside the ring, above the counter.
  Its opacity and scale are driven from the sweep in ring.js, not from CSS —
  it has to arrive on the same curve as the arc that summoned it.
*/
.ring-back {
  position: absolute;
  top: 20%;
  left: 50%;
  transform: translateX(-50%);
  width: 30px;
  height: 30px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 50%;
  background: var(--brand);
  color: var(--text-on-brand);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
}

.ring-back svg { width: 17px; height: 17px; display: block; }
.ring-back:hover { background: var(--brand-pressed); }

/*
  Padding lives on the VIEW, not on .main, so a view that wants the whole
  pane — the map — can have it by zeroing its own padding instead of fighting
  its container's.
*/
/*
  ROUNDED WHERE IT TURNS A CORNER, SQUARE WHERE IT MEETS AN EDGE.

  The pane used to be rounded top-left AND bottom-left, which drew a curve
  at the bottom of the window with nothing on the other side of it — a
  corner implies something continues past it, and nothing does. The top-left
  corner is real: the bar is above and the rail is beside, and the curve is
  where the pane tucks in behind both.

  So the bottom (and the right, which never had one) runs flush to the
  window, and the map gets the last few pixels it was giving up to a
  decorative arc.
*/
.main {
  background: var(--surface-muted);
  border-top-left-radius: var(--radius-2xl);
  min-width: 0;
  min-height: 0;
  /* The pages inside scroll; the shell does not. A window-height document
     with a full-bleed map in it has nowhere to scroll TO, and a Fleet table
     that scrolled the whole page took the top bar off screen with it. */
  overflow-y: auto;
}

.view { --view-pad-x: 36px; --view-pad-top: 32px; padding: var(--view-pad-top) var(--view-pad-x) 56px; }

/* Except the map, which IS the pane: no padding, and the full height of it
   so the tiles run to the rounded corners. */
#view-map { padding: 0; height: 100%; }

/* `.view > header`, because the headers sit inside the sections. The old
   `.main > header` matched nothing and every header quietly stacked. */
.view > header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: 24px;
  flex-wrap: wrap;
}

.main h2 { margin: 0; font-size: 26px; font-weight: 800; letter-spacing: -0.5px; }
.main header p { margin: 4px 0 0; color: var(--text-secondary); }

/* ----------------------------------------------------------------- cards */

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px;
  margin-bottom: 24px;
}

/*
  LOZENGES, AND THE FIGURE CENTRED OVER ITS WORD — the same shape the map's
  controls are and the same stack the fleet ring reads with. They were square
  cards with the number in the top-left corner, which is the one arrangement
  on this screen that had no relative anywhere else in the console.
*/
.card {
  background: var(--surface);
  border-radius: var(--radius-pill);
  padding: 18px 24px;
  box-shadow: var(--shadow-card);
  text-align: center;
}

.card .figure { font-size: 38px; font-weight: 800; line-height: 1.05; letter-spacing: -0.03em; }

/*
  ============================================================
  THE FLEET SCREEN: FOUR DISCS AND A RAIL
  ============================================================

  The four counts become discs — the same figure-over-word the fleet ring
  reads with, at the size the eye can take in without reading. Scoped to this
  view: Reports uses the same `.card`, and its numbers are longer than a
  count, so a circle would squeeze them.
*/
/*
  THE TRAY THE DISCS SIT IN: a white shelf hanging from the pane's top edge,
  the way the map's window stands at the map's top. Square where it meets the
  edge, rounded where it leaves it, and shaded INWARD — an inset from above,
  so the discs read as resting in a recess rather than the tray floating over
  the page. It escapes the view's top padding to reach the true edge, and it
  arrives by dropping, on the rail's curve, every time the view is selected.
*/
/*
  ============================================================
  THE THREE STATES, IN THE FLEET COLUMN
  ============================================================

  A white tray used to hang out of the fleet page's top edge holding four
  discs, one of which repeated the ring's own number. The counts now sit in
  the column under that ring: the whole, then how it splits.

  THEY BELONG TO THE FLEET PAGE, so they are not there on any other. `:has()`
  because the column and the page are on opposite branches — the arch is in
  the rail, the page is in the pane — and nothing reaches sideways and up.
*/
#fleet-cards {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  /* `.rail-item` centres what it holds, which is right for the ring and wrong
     for this: shrink-to-fit sized the discs to their labels instead of to the
     column. */
  align-self: stretch;
  /* Out of the flow's way when it is not this page's turn: no height, no
     margin, nothing for the roster below to step around. */
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  transition: opacity 280ms ease, max-height 280ms cubic-bezier(0.2, 0, 0, 1);
}

/* Two discs at the column's full width, plus the gap — comfortably over the
   tallest the column can get (a 332px rail gives 276px discs). */
body:has(#view-fleet:not([hidden])) #fleet-cards {
  max-height: 620px;
  opacity: 1;
}

/*
  THE SAME DISC AS THE RING, four times down the column: same width, same
  circle, same white face lifted off the arch. The ring is the whole and
  carries arcs; these are its parts and carry a colour. Nothing else changes,
  so the column reads as one series rather than a headline and some footnotes.
*/
#fleet-cards .card {
  flex: none;
  /*
    THE RING'S WHITE DISC IS NOT ITS BOX. ring.js draws a 312 viewBox with a
    backplate of r=150 — 300 across, 96.15% of the width — leaving the margin
    its arcs swing through. A disc at the full 100% here came out ~9px wider
    than the one above it, which is exactly what "they don't look the same"
    was seeing.
  */
  width: calc(100% * 300 / 312);
  aspect-ratio: 1;
  border-radius: 50%;
  padding: 0;
  display: grid;
  place-content: center;
  text-align: center;
  background: var(--surface);
  box-shadow: 0 6px 14px rgba(16, 45, 52, 0.16);
  /* So the type inside scales with the disc, exactly as the ring's counter
     does — see .ring. Fixed sizes would strand small type in a big circle. */
  container-type: inline-size;
}

#fleet-cards .figure { font-size: 23cqw; line-height: 1.05; letter-spacing: -0.03em; }
#fleet-cards .label { font-size: 7cqw; margin-top: 0; line-height: 1.1; }

@media (prefers-reduced-motion: reduce) {
  #fleet-cards { transition: none; }
}

/*
  THE COLUMN, THE RAIL, THE TABLE. Keeping the DOM in reading order — heading,
  counts, filter, table — and letting the grid put the filter beside the table
  rather than above it. `align-content: start` so the rows stack from the top
  instead of spreading down a tall view.
*/
#view-fleet {
  display: grid;
  /*
    THE RAIL'S TRACK IS FIXED, so opening the rail overflows it and lands ON
    TOP of the table rather than shoving it sideways — the map's behaviour.
    The track is the rail's width MINUS the view padding, because the rail
    leaves the pane's true left edge (it escapes the padding to get there):
    padding + track + 16px gap puts the table's edge 16px off the rail's,
    whatever the padding is.
  */
  grid-template-columns: calc(var(--rail-w-icons) - var(--view-pad-x)) minmax(0, 1fr);
  /*
    The heading starts in the TABLE'S column, which lines it up over its table
    and keeps it out from under the rail hanging beside it. The counts used to
    take a row above this; they live in the fleet column now, so the row went
    with them.
  */
  grid-template-areas:
    ".    head"
    ".    cards"
    "rail panes";
  /*
    A THIRD, UNNAMED ROW soaks up whatever the content does not use, and the
    view is never shorter than the pane. Both exist for the rail: its sticky
    containing block is the rows it spans, and only a full-height span lets
    --rail-top measure against the PANE when the table is short — an empty
    fleet was clamping the rail up against the content instead.
  */
  grid-template-rows: auto auto auto 1fr;
  min-height: 100%;
  align-content: start;
  column-gap: 16px;
}

#view-fleet > header { grid-area: head; }
/*
  DIRECT CHILDREN ONLY, and that `>` is load-bearing.

  This said `#view-fleet .panel`, which was exact while the page held one
  panel. Folding Reports in brought three more, every one of them matched the
  same selector, and all four were assigned the single `table` area — so they
  stacked in one cell at identical coordinates and the last one painted won.
  The page rendered as though the vehicle table had been replaced by the
  expiry list.

  The reports block below is one child in its own row; the panels inside it
  are nested, so they flow normally.
*/
/*
  THE PILLS ARE PINNED. "How is the fleet" is the same question whichever
  pane is open, so the answer stays on screen while the pane under it
  scrolls. Its own row, above the rail, so it spans the full width rather
  than starting where the table does.
*/
#view-fleet > .cards {
  grid-area: cards;
  position: sticky;
  top: 0;
  z-index: 5;
  padding-bottom: 12px;
  /*
    THE PANE'S OWN BACKGROUND, not the page's. `--canvas` is the teal the
    shell sits on; painting the sticky strip with it drew a foreign band
    across the middle of a white pane. What has to be hidden is the content
    scrolling underneath, so the colour must be whatever `.main` is.
  */
  background: var(--surface-muted);
}

#view-fleet > .fleet-panes { grid-area: panes; }

/*
  ONE CHILD PER PANE, and the panels inside them flow normally. The rule this
  replaced was `#view-fleet .panel { grid-area: table; }` — exact while the
  page held a single panel, and quietly catastrophic once it held several:
  every one matched, all were assigned the same named area, and they stacked
  in one cell so only the last painted was visible.
*/
.fleet-pane { display: flex; flex-direction: column; gap: 18px; }

/* Separates the pane switch from the filters that only apply to one pane. */
.filter-divider {
  height: 1px;
  margin: 4px 6px;
  background: var(--divider);
}

/*
  THE FILTER IS THE MAP'S LAYER RAIL — and now genuinely in its place: a tab
  leaving the pane's left edge at mid-height, not a pill floating beside the
  table. Same shell colour, same discs growing into named pills, same fillets
  where it meets the edge, same slide when the view is selected.

  It spans every grid row so its sticky containing block is the whole view:
  the shared --rail-top then holds it at the same hanging line the map rail
  starts from, however far the table scrolls — switching views never jumps
  the rail. The negative margin walks it out of the view's padding to the
  pane's true edge.
*/
#fleet-filters {
  grid-row: 1 / -1;
  grid-column: 1;
  align-self: start;
  position: sticky;
  top: var(--rail-top);
  /* Above the panel it opens across. */
  z-index: 5;
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: stretch;
  width: var(--rail-w-icons);
  margin: 0 0 0 calc(-1 * var(--view-pad-x));
  padding: 6px;
  gap: 4px;
  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
  background: var(--shell, #dce8ea);
  /* The map rail's shadow: it only ever lands on the content to the right. */
  box-shadow: 7px 0 12px -6px rgba(16, 45, 52, 0.3);
  animation: map-rail-out 360ms cubic-bezier(0.2, 0, 0, 1) both;
  transition: width 240ms cubic-bezier(0.2, 0, 0, 1);
}

/* The same restart trick as the map rail: the animation only re-runs when
   the element's OWN display goes none → shown, so hide it with its view. */
#view-fleet[hidden] #fleet-filters { display: none; }

/* The same corner fillets that fuse the map rail to the shell's edge. */
#fleet-filters::before,
#fleet-filters::after {
  content: '';
  position: absolute;
  left: 0;
  width: var(--tab-fillet);
  height: var(--tab-fillet);
  pointer-events: none;
}

#fleet-filters::before {
  top: calc(-1 * var(--tab-fillet));
  background: radial-gradient(circle at top right,
    transparent 0 var(--tab-fillet), var(--shell, #dce8ea) var(--tab-fillet));
}

#fleet-filters::after {
  bottom: calc(-1 * var(--tab-fillet));
  background: radial-gradient(circle at bottom right,
    transparent 0 var(--tab-fillet), var(--shell, #dce8ea) var(--tab-fillet));
}

#fleet-filters:hover,
#fleet-filters:focus-within { width: var(--rail-w-named); }

#fleet-filters button {
  display: flex;
  align-items: center;
  gap: 9px;
  height: 38px;
  padding: 0 8px;
  border-radius: var(--radius-pill);
  /* The name is laid out before the rail is wide enough to hold it. */
  overflow: hidden;
  color: var(--status-upcoming);
}

#fleet-filters button svg { width: 21px; height: 21px; flex: none; }

#fleet-filters .filter-label {
  white-space: nowrap;
  font-size: 12.5px;
  font-weight: 600;
  opacity: 0;
  transition: opacity 150ms ease;
}

#fleet-filters:hover .filter-label,
#fleet-filters:focus-within .filter-label { opacity: 1; }

#fleet-filters button:hover { background: rgba(16, 45, 52, 0.06); }

#fleet-filters button[aria-pressed="true"] {
  background: var(--brand);
  color: var(--text-on-brand);
}

@media (prefers-reduced-motion: reduce) {
  #fleet-filters, #fleet-filters .filter-label { transition: none; }
  #fleet-filters, .map-rail { animation: none; }
}
.card .label { color: var(--text-secondary); font-size: 13px; font-weight: 600; margin-top: 2px; }

.panel {
  background: var(--surface);
  border-radius: var(--radius-2xl);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  margin-bottom: 24px;
}

.panel > h3 {
  margin: 0;
  padding: 18px 20px 14px;
  font-size: 15px;
  font-weight: 800;
}

.panel > .note {
  margin: 0;
  padding: 0 20px 16px;
  color: var(--text-secondary);
  font-size: 13px;
}

/* ---------------------------------------------------------------- tables */

table { width: 100%; border-collapse: collapse; }

th {
  text-align: left;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-tertiary);
  /* 26px, so the first column clears the panel's 28px corner rather than
     starting inside the curve. */
  padding: 10px 26px;
  border-bottom: 1px solid var(--divider);
  white-space: nowrap;
}

td {
  padding: 13px 26px;
  border-bottom: 1px solid var(--divider);
  vertical-align: middle;
}

tbody tr:last-child td { border-bottom: 0; }
/* The same wash the fleet arch's rows use, so a row reads the same wherever
   you meet one. `--surface-muted` was a flat grey panel colour. */
tbody tr:hover { background: rgba(16, 45, 52, 0.04); }

/* Eight columns will outrun a narrow window at any breakpoint, so the table
   scrolls inside its panel rather than the page scrolling sideways. */
.table-scroll { overflow-x: auto; }

/*
  THE VEHICLE COLUMN NEEDS A FLOOR. Auto table layout hands each column its
  share of what is left, and this one holds three stacked lines — name, plate,
  and year-make-model — so it was the first to be squeezed: "Toyota Hilux
  SR5" came apart over three rows while RUC and Service sat half empty. The
  table already scrolls, so taking room here costs nothing but a little
  scroll on a narrow window.
*/
#view-fleet th:first-child,
#view-fleet td:first-child { min-width: 172px; }

/*
  A DATE THAT IS NOT A FLAG. Same size and weight as the text inside a chip so
  the column still lines up, with none of the chip's colour or ground — the
  eye reads down a quiet column and stops on the two that are not quiet.
*/
.plain-date {
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
  padding: 3px 0;
}

/*
  A ROW OF FIELDS THAT WRAPS. The vehicle form asks for a dozen things because
  the table requires them; in one column that is a scroll, and a form you have
  to scroll is a form people abandon halfway.
*/
.field-row {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 4px;
}

.field-row label { flex: 1 1 140px; min-width: 0; }

.field-optional {
  font-weight: 500;
  color: var(--text-tertiary, #7e8f93);
  text-transform: none;
  letter-spacing: 0;
}

/*
  THE WORKING DIALOGS GET THE SAME SURFACE THE SIGN-IN ONE HAS. Both of
  these only ever set their width, so both rendered as a square-cornered
  white rectangle with the user agent's own border — the one surface in a
  console of 28px radii that looked like a browser default, because it was
  one. The recipe is lifted from #signin-dialog rather than invented, so
  there is one dialog treatment here and not three.
*/
#vehicle-dialog,
#team-dialog,
#place-dialog {
  border: 0;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-raised);
  padding: 26px 28px;
}

#vehicle-dialog::backdrop,
#team-dialog::backdrop,
#place-dialog::backdrop { background: rgba(16, 45, 52, 0.45); }

#vehicle-dialog h2,
#team-dialog h2,
#place-dialog h2 { margin: 0 0 14px; font-size: 20px; }

#vehicle-dialog { max-width: 620px; width: calc(100vw - 48px); }

/*
  ============================================================
  THE VEHICLE FORM'S FIELDS
  ============================================================

  SCOPED TO THE FORM, NOT TO THE DIALOG. This form has two homes — the
  centred "Add a vehicle" dialog and the vehicle card's right-hand column —
  and the one node moves between them. Anything hung off `#vehicle-dialog`
  would dress it in one place and leave it in the browser's own clothes in
  the other, which is exactly what happened: moved into the card it came out
  in the user agent's serif with square grey boxes around every field.

  IT WAS NEVER REALLY STYLED AT ALL. In the dialog the surrounding chrome
  carried it well enough that nobody looked; the move is what exposed it.
  The recipe below is lifted from `#signin-form` rather than invented, so the
  console has one field treatment and not two.
*/
#vehicle-form label {
  display: flex;
  flex-direction: column;
  gap: 5px;
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* The word `optional` is an aside inside a heading, so it drops the shout. */
#vehicle-form .field-optional {
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--text-tertiary);
}

/*
  `font: inherit` FIRST, and it is the line that matters. A native input or
  select does not inherit type — it uses the user agent's own — so without
  this the form is set in whatever the browser thinks a control should be
  while the labels beside it are in the product's face.
*/
#vehicle-form input,
#vehicle-form select {
  font: inherit;
  font-size: 15px;
  width: 100%;
  box-sizing: border-box;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-muted);
  color: var(--text);
}

/*
  EVERY DROPDOWN IS A PILL.

  Fully rounded and with the platform's arrow replaced, because a native
  select is the one control that cannot be half-styled: its chrome comes from
  the OS, so leaving `appearance` alone would keep a square grey button on the
  end of an otherwise rounded field. The chevron is the same drawn mark the
  trip picker uses, as a data URI so the form carries no second request.
*/
#vehicle-form select {
  -webkit-appearance: none;
  appearance: none;
  border-radius: var(--radius-pill);
  padding-right: 32px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6.5 8 10.5 12 6.5' fill='none' stroke='%23516b70' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 15px 15px;
  cursor: pointer;
}

/* A date field carries the platform's own picker button, which cannot be
   moved — so these stay rounded rather than pill, or the button sits in the
   curve. */
#vehicle-form input[type="date"] { padding-right: 8px; }

#vehicle-form input:focus-visible,
#vehicle-form select:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

/* ------------------------------------------------------- the place dialog */

#place-dialog { max-width: 520px; width: calc(100vw - 48px); }
#place-dialog select { width: 100%; }

.pd-address-label { display: block; margin-top: 14px; }

/* The field and its button are one control: a Find that wraps under the box
   it belongs to reads as a second, unrelated action. */
.pd-address { display: flex; gap: 8px; align-items: stretch; }
.pd-address input { flex: 1 1 auto; min-width: 0; }
.pd-address .button { flex: 0 0 auto; }

.pd-size { margin-top: 14px; }
.pd-size label { display: block; font-size: 13px; margin-bottom: 6px; }
.pd-size input[type='range'] { width: 100%; }

/*
  THE CANDIDATE LIST IS CAPPED AND SCROLLS. Nominatim answers with up to six
  and each is a full display_name — three of those stacked pushed the save
  button off the bottom of the dialog on a laptop.
*/
#pd-results { max-height: 132px; overflow-y: auto; }

/* ----------------------------------------------------------------- teams */

#team-dialog { max-width: 560px; width: calc(100vw - 48px); }
#team-dialog select { width: 100%; }

/*
  A TEAM'S COLOUR IS A DOT, NOT A FILLED CHIP. The colour is an identifier
  the eye scans a column for, and six saturated lozenges in a table read as
  six status flags — which is the one thing this console has trained people
  to believe a coloured pill means. A dot beside the name carries the same
  identity and claims none of the urgency.
*/
.team-name { display: inline-flex; align-items: center; gap: 8px; }

.team-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--muted);
}

.team-dot.brand  { background: var(--brand); }
.team-dot.navy   { background: var(--status-upcoming); }
.team-dot.warn   { background: var(--warn); }
.team-dot.urgent { background: var(--status-urgent); }
.team-dot.deep   { background: var(--brand-pressed); }
.team-dot.muted  { background: var(--text-tertiary); }

/*
  THE PICKERS SCROLL RATHER THAN GROW. A workspace with sixty drivers would
  otherwise make this dialog taller than the window, and a form whose submit
  button is below the fold is a form people abandon. Each list keeps its own
  scroll so the actions stay put no matter how large the fleet is.
*/
.pick-set {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 10px 12px 12px;
  margin: 14px 0 0;
  min-width: 0;
}

.pick-set legend {
  padding: 0 6px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.pick-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 2px 12px;
  max-height: 168px;
  overflow-y: auto;
}

.pick {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 4px;
  border-radius: 8px;
  font-size: 13px;
  cursor: pointer;
  min-width: 0;
}

.pick:hover { background: var(--surface-muted); }
.pick input { flex: 0 0 auto; margin: 0; }
.pick span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The taken-vehicle note sits under the plate it belongs to, not beside it. */
.pick .pick-held { color: var(--text-tertiary); font-size: 11px; }

/*
  DISBAND SITS AWAY FROM SAVE. It is the one irreversible control in this
  dialog, and putting it in the same cluster as Cancel and Create is how a
  crew gets deleted by somebody aiming for the button next to it.
*/
.signin-actions .spacer { flex: 1 1 auto; }
#team-delete { color: var(--status-urgent); }

/* Nothing to report, said once and quietly. */
.nothing { font-size: 13px; }

/* Names are two words and the table already scrolls; wrapping "Kamaka
   Osgood" onto two lines cost every row its height for nothing. */
#view-fleet td:nth-child(2) { white-space: nowrap; }

.plate {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 700;
  letter-spacing: 0.5px;
}

.muted { color: var(--text-secondary); }
.tiny { font-size: 12px; }
.right { text-align: right; }
.nowrap { white-space: nowrap; }

/* ------------------------------------------------------- chips and pills */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
}

.chip::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}

.chip.ok { color: var(--status-ok); background: rgba(47, 119, 128, 0.12); }
.chip.upcoming { color: var(--status-upcoming); background: rgba(23, 61, 82, 0.12); }
.chip.urgent { color: var(--status-urgent); background: rgba(179, 38, 30, 0.12); }
.chip.plain { color: var(--text-secondary); background: var(--surface-muted); }
.chip.plain::before { display: none; }

.role {
  display: inline-block;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 700;
  background: var(--surface-muted);
  color: var(--text-secondary);
}

.role.owner { background: rgba(47, 119, 128, 0.14); color: var(--brand); }

/* --------------------------------------------------------------- filters */

/*
  ONE CONTROL, NOT FOUR BUTTONS. The map's time window is this same class and
  has always been drawn as a segmented group; the fleet's copy was four loose
  outlined pills. Same component, same picture — the group carries the
  surface and the border, and the options inside it carry nothing until one
  is chosen.
*/
.filters {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  width: fit-content;
  margin-bottom: 18px;
  padding: 4px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

.filters button {
  appearance: none;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  border: 0;
  background: transparent;
  color: var(--text-secondary);
  padding: 6px 16px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}

.filters button:hover { color: var(--brand-pressed); }

.filters button[aria-pressed="true"] {
  background: var(--brand);
  color: var(--text-on-brand);
}

/* --------------------------------------------------------------- buttons */

.button {
  appearance: none;
  font: inherit;
  font-weight: 700;
  border: 0;
  background: var(--brand);
  color: var(--text-on-brand);
  padding: 10px 18px;
  border-radius: var(--radius-md);
  cursor: pointer;
}

.button:hover { background: var(--brand-pressed); }
.button.secondary { background: var(--surface); color: var(--text); box-shadow: inset 0 0 0 1px var(--border); }
.button.secondary:hover { background: var(--surface-muted); }

/* ------------------------------------------------------------------ bars */

.bars { display: flex; flex-direction: column; gap: 10px; padding: 4px 20px 20px; }
.bar-row { display: grid; grid-template-columns: 64px minmax(0, 1fr) 84px; align-items: center; gap: 12px; }
.bar-track { background: var(--surface-muted); border-radius: var(--radius-pill); height: 10px; overflow: hidden; }
.bar-fill { background: var(--brand); height: 100%; border-radius: var(--radius-pill); }

/* ------------------------------------------------------------------- map */

/* The map view is the one view with no padding: the pane IS the map. */
#view-map { padding: 0; }

/*
  The water colour, which is `--canvas` — see the note in map.js. It shows for
  the instant before a renderer paints, and behind Google's tiles as they
  arrive, so it has to be the map's own ground rather than a neutral grey.
*/
.map-wrap {
  position: relative;
  /* The pane, not the window: the bar above it is real height and a map
     measured against `100vh` hangs exactly that far off the bottom. */
  height: 100%;
  background: var(--canvas);
}

.map { position: absolute; inset: 0; z-index: 1; }
.map > div { width: 100%; height: 100%; }

/*
  THE MAP FADES IN AS THE COVER FADES OUT — see loadFinish in app.js.

  Held back rather than hidden by default: a console whose scripts never ran
  shows a map rather than a blank pane, because nothing would have added
  this class. The two durations match the cover's so the pair reads as one
  crossfade rather than a reveal followed by an arrival.
*/
.map.is-veiled { opacity: 0; }
.map { transition: opacity 420ms ease; }

/*
  ============================================================
  THE COVER, WHILE THE FLEET AND THE MAP ARE READ
  ============================================================

  WHITE, AND OPAQUE. Not a translucent scrim over a half-drawn map — the
  point is that there is nothing worth looking at yet, and a map fading up
  through a veil while its tiles are still arriving is the mess this
  replaces.

  IT COVERS THE MAP AND NOT THE CONTROLS' PANE. The rail, the window tabs
  and the count all sit above it; they are chrome that is already correct
  and freezing them behind a white sheet would make the whole pane look
  broken rather than busy.
*/
.map-cover {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  background: var(--surface);
  transition: opacity 420ms ease;
}

/* Gone, and out of the way of the map's own gestures. */
.map-cover.is-gone {
  opacity: 0;
  pointer-events: none;
}

.map-cover-disc {
  position: relative;
  width: 120px;
  height: 120px;
  display: grid;
  place-items: center;
}

.map-cover-koru {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}

/*
  THE COUNT SITS IN THE HOLE, not under the ring. Tabular figures because
  this number changes while you are looking at it, and proportional ones
  make the whole thing twitch sideways every time a 1 becomes a 4.
*/
.map-cover-count {
  position: relative;
  font-size: 19px;
  font-weight: 800;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/*
  NO ANIMATION FOR ANYONE WHO ASKED FOR NONE. The ring still fills — it is
  information, not decoration — but it snaps between true readings instead
  of gliding, and the crossfade becomes a cut.
*/
@media (prefers-reduced-motion: reduce) {
  .map-cover, .map { transition: none; }
}

.map-svg { width: 100%; height: 100%; display: block; }

/*
  The floating controls are the app's glass, in CSS: the translucent fill and
  the hairline rim are `glassFill` and `glassRim` from colors.ts, and the blur
  is what BlurView does natively. Backdrop-filter needs something behind it to
  refract, which a full-bleed map finally provides — the reason the koru could
  not have this, the map can.
*/


/* Which map is under the fleet — the other question, so the other corner. */
.map-renders {
  position: absolute;
  top: 18px;
  right: 18px;
  z-index: 6;
  margin: 0;
}

.map-wrap .filters button {
  background: rgba(255, 255, 255, 0.55);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
  backdrop-filter: blur(16px) saturate(1.5);
  border: 1px solid rgba(255, 255, 255, 0.75);
  color: var(--text);
  box-shadow: var(--shadow-card);
}

.map-wrap .filters button[aria-pressed="true"] {
  background: var(--brand);
  border-color: transparent;
  color: var(--text-on-brand);
}

.filters button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/*
  The vehicle pulse. Colour arrives as the group's `fill`, set per pin in
  map.js, so these rules never have to know the three tones.

  `transform-box: fill-box` is the load-bearing line. An SVG element's default
  transform origin is the USER SPACE origin, not the shape — and this map's
  user space is centred on Auckland, so a plain `scale()` on a circle at
  x=280 y=-140 would fling it toward the middle of the harbour instead of
  growing it where it stands. Pinning the box to the shape and the origin to
  its centre is what makes the ring expand around the vehicle.
*/
.pin-pulse-halo,
.pin-pulse-ring {
  transform-box: fill-box;
  transform-origin: center;
}

.pin-pulse-halo { animation: tera-pulse-halo 2.6s ease-in-out infinite; }
.pin-pulse-ring { animation: tera-pulse-ring 2.6s ease-out infinite; }

/*
  STOPPED BREATHES SLOWER. The colour carries the state, but a difference in
  cadence is readable across a yard of pins without decoding either hue —
  and it keeps the map calm, since most vehicles are parked most of the time.
*/
.pin-pulse.is-idle .pin-pulse-halo { animation-duration: 4.2s; }
.pin-pulse.is-idle .pin-pulse-ring { animation-duration: 4.2s; }

/* The steady halo is what makes it glow; the expanding ring is what makes it
   pulse. One without the other is either a smudge or a ripple. */
@keyframes tera-pulse-halo {
  0%, 100% { opacity: 0.16; transform: scale(1); }
  50% { opacity: 0.28; transform: scale(1.04); }
}

/*
  A LOT SMALLER THAN IT WAS. 3.4x was chosen for a ring around a 15px dot;
  applied to a plate seventy pixels wide it threw a wash across half the
  harbour and merged with its neighbours. 1.35 keeps the ripple inside the
  space the marker already occupies, which is what makes a dozen of them
  readable at once.
*/
@keyframes tera-pulse-ring {
  0% { opacity: 0.4; transform: scale(1); }
  /* Gone well before the cycle ends, so there is a beat of rest between
     ripples — a continuous wash reads as a rendering fault, not a heartbeat. */
  70%, 100% { opacity: 0; transform: scale(1.35); }
}

/*
  THE HOVER CARD'S LIVE DOT, on the pin's cadence.

  Same 2.6s as `tera-pulse-ring` and the same shape of idea — a ripple out of
  a solid centre — because a card and the pin it belongs to beating at
  different rates would read as two unrelated signals. It rides on a halo
  BEHIND the dot, so the dot itself never resizes: the text beside it has to
  stay readable, and a row that breathes is harder to read than one that sits
  still while its light moves.

  ONLY WHILE THE FIX IS FRESH. `is-live` is set from the same stale window the
  pin's speed uses; without it the halo is transparent and the dot is muted
  ink, so a vehicle that stopped reporting hours ago is not given a heartbeat.
*/
.card-ago-halo {
  transform-box: fill-box;
  transform-origin: center;
  opacity: 0;
}

.card-ago.is-live .card-ago-halo {
  animation: tera-pulse-dot 2.6s ease-out infinite;
}

@keyframes tera-pulse-dot {
  0% { opacity: 0.45; transform: scale(1); }
  /* Gone before the cycle ends, so there is a beat of rest between ripples —
     the same reason the pin's ring finishes early. */
  70%, 100% { opacity: 0; transform: scale(2.8); }
}

/*
  Somebody who has asked not to see motion should not get a throbbing map. The
  halo stays and holds its colour, because the COLOUR is the signal here and
  only the movement is decoration — switching it off entirely would take the
  information away with the animation.
*/
@media (prefers-reduced-motion: reduce) {
  .pin-pulse-halo { animation: none; opacity: 0.26; }
  .pin-pulse-ring { animation: none; opacity: 0; }
}

/* The dot keeps its colour and stops moving, for the reason above it. */
@media (prefers-reduced-motion: reduce) {
  .card-ago.is-live .card-ago-halo { animation: none; opacity: 0; }
}

.empty { padding: 28px 20px; text-align: center; color: var(--text-secondary); }

@media (max-width: 900px) {
  .shell { grid-template-columns: 1fr; }
  .sidebar { position: static; max-height: none; padding: 16px; }
  .nav { flex-direction: row; flex-wrap: wrap; }
  .main { border-radius: var(--radius-2xl) var(--radius-2xl) 0 0; }
  .view { --view-pad-x: 18px; --view-pad-top: 24px; padding: var(--view-pad-top) var(--view-pad-x) 48px; }
  #view-map { padding: 0; }

  /* Side by side rather than a column of three, so the rings do not push the
     content a full screen down on a narrow window. */
  .rail { flex-direction: row; gap: 16px; padding: 0; }
  .rail-item { flex: 1; min-width: 0; }
  .ring-value { font-size: 34px; }
}

/* The map's cursor says what a click will do — a crosshair means "this
   click puts something here", which is the only warning anybody needs. */
.map-wrap.placing .map { cursor: crosshair; }

/*
  ARRIVALS AND DEPARTURES, STACKED BOTTOM-RIGHT.

  Bottom, because the top corners are already the map's own controls and an
  alert that covers them takes the map away to tell you about it. They fade
  themselves out: an arrival is worth interrupting for once, not worth
  keeping on screen until somebody tidies it away.
*/
.map-alerts {
  position: absolute;
  right: 18px;
  /* Clear of the attribution line, which is a licence condition and cannot
     be covered by our own furniture. */
  bottom: 52px;
  z-index: 7;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  pointer-events: none;
}

.map-alert {
  background: rgba(255, 255, 255, 0.75);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
  backdrop-filter: blur(18px) saturate(1.5);
  border: 1px solid rgba(255, 255, 255, 0.8);
  box-shadow: var(--shadow-card);
  border-radius: var(--radius-xl);
  padding: 10px 16px;
  font-size: 13px;
  max-width: 320px;
  animation: map-alert-in 220ms ease both;
}

.map-alert strong { font-weight: 700; }
.map-alert .map-alert-when { color: var(--muted); }

@keyframes map-alert-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .map-alert { animation: none; }
}

/*
  PLACES IN THE RAIL. The same row rhythm as the fleet above it, because
  they are read together — the two halves of "where should this be, and
  where is it".
*/
.places-head { margin-top: 26px; }
.place-name { font-weight: 700; }

/* THE YARD IS NAMED, NOT DECORATED. A star or a pin would need explaining;
   the word does not. */
.place-home {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.82);
}

.place-size {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.68);
  font-variant-numeric: tabular-nums;
}

/*
  The row's actions sit at its end and stay quiet until the row is under the
  pointer: a rail of eight places should read as a list, not as sixteen
  buttons.
*/
.place-actions {
  display: flex;
  align-items: center;
  /* Workplace + Personal + Remove came to exactly the width of the cell —
     no slack at all, so any font substitution would have wrapped them onto
     a second line and undone the constant row height above. */
  gap: 5px;
  opacity: 0;
  /* Invisible AND untouchable: a transparent button that still swallows the
     row's own click would make "click the place to see it" a lie in exactly
     the spot the pointer lands. */
  pointer-events: none;
}

.place-row:hover .place-actions,
.place-row:focus-within .place-actions { opacity: 1; pointer-events: auto; }

.place-action {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.5);
  color: inherit;
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  /* Tight enough that the buttons are no taller than the stats sentence
     they replace — that is what keeps the row height constant on hover. */
  padding: 2px 7px;
  line-height: 1.35;
  border-radius: var(--radius-pill);
  cursor: pointer;
  white-space: nowrap;
}

.place-action:hover { background: rgba(255, 255, 255, 0.18); }

/*
  THE ADD-A-PLACE PANEL.

  Same glass as the rest of the map's furniture, on the right where the
  button that opens it is, and narrow enough that the map beside it is still
  the bigger thing on screen — the whole job is judging a circle against the
  ground, and a form that covers the ground cannot be filled in honestly.
*/
.place-form {
  position: absolute;
  top: 66px;
  right: 18px;
  z-index: 8;
  width: 320px;
  max-height: calc(100vh - 140px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 18px;
  border-radius: var(--radius-xl);
  background: rgba(255, 255, 255, 0.82);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
  backdrop-filter: blur(18px) saturate(1.5);
  border: 1px solid rgba(255, 255, 255, 0.85);
  box-shadow: var(--shadow-card);
  color: var(--text);
}

.place-form-title { margin: 0; font-size: 15px; font-weight: 800; }

.place-field { display: flex; flex-direction: column; gap: 6px; font-size: 12px; font-weight: 700; }

.place-field input[type="text"] {
  font: inherit;
  font-weight: 500;
  font-size: 14px;
  padding: 9px 12px;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(23, 61, 82, 0.22);
  background: #fff;
  color: var(--text);
}

.place-field input[type="text"]:focus {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}

.place-search { display: flex; gap: 8px; }
.place-search input { flex: 1; min-width: 0; }
.place-search .button { flex: none; }

.place-note { margin: 0; font-size: 12px; color: var(--muted); }
/*
  A GEOCODER RETURNS GUESSES, PLURAL, and the honest thing is to show them.
  "1 Queen Street, Auckland" comes back as three different streets in three
  different suburbs; picking the first silently would put a worksite in the
  wrong city and look decisive doing it.
*/
.place-results {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 180px;
  overflow-y: auto;
}

.place-result {
  text-align: left;
  font: inherit;
  font-size: 12px;
  line-height: 1.35;
  padding: 8px 10px;
  border-radius: 10px;
  border: 1px solid rgba(23, 61, 82, 0.16);
  background: rgba(255, 255, 255, 0.7);
  cursor: pointer;
  width: 100%;
}

.place-result:hover { background: #fff; border-color: var(--brand); }

.place-size-field { display: flex; flex-direction: column; gap: 8px; }
.place-field input[type="range"] { width: 100%; accent-color: var(--brand); }

.place-form-actions { display: flex; gap: 8px; justify-content: flex-end; }

.place-save {
  background: var(--brand);
  color: var(--text-on-brand);
  border-color: transparent;
}

.place-save:disabled { opacity: 0.45; cursor: not-allowed; }

/*
  THE DRAG HANDLE. Big enough to grab on a trackpad, and obviously a handle
  rather than a vehicle — the map already uses a filled disc with a white
  rim for "a vehicle is here", so this is hollow.
*/
.place-handle {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--brand);
  box-shadow: var(--shadow-card);
  cursor: grab;
}

.place-handle:active { cursor: grabbing; }

/* The manual escape hatch reads as an option, not as a search result. */
.place-manual {
  border-style: dashed;
  font-weight: 700;
}

/* The "road only" qualifier on a search result — a caveat, not a heading. */
.place-result-note {
  color: var(--muted);
  font-weight: 600;
}

/* Work or personal: a two-button switch, sized like the map's own pills. */
.place-purpose { display: flex; gap: 8px; }

.place-purpose button {
  flex: 1;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  padding: 8px 12px;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(23, 61, 82, 0.22);
  background: #fff;
  color: var(--text);
  cursor: pointer;
}

.place-purpose button[aria-pressed="true"] {
  background: var(--brand);
  border-color: transparent;
  color: var(--text-on-brand);
}

/*
  A PLACE'S NUMBERS, under its name in the rail. Small, tabular, and quiet:
  they are there to be glanced at while looking for something else.
*/
.place-stats {
  font-size: 12px;
  color: rgba(255, 255, 255, 0.72);
  font-variant-numeric: tabular-nums;
  /* One line, always. The row's height is a promise the list makes to the
     eye scanning it; a sentence that wraps on some places and not others
     breaks it, and the ellipsis is a cheaper apology than a ragged rail. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Somebody is there RIGHT NOW — the one number worth a colour. */
.place-here {
  font-weight: 800;
  color: #fff;
}

.place-purpose-chip {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.6);
}

/*
  A PLACE ROW IS TWO LINES, not a row of columns.

  It borrowed `.roster-who` from the vehicle list above it, which lays its
  children out in a row — so the name, the chip and the whole stats line
  competed for one narrow rail and wrapped mid-phrase ("1 vehicle here /
  now · 4 visits · / last 12:15"). A place has a name and a sentence about
  it; those stack.
*/
.place-who {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 2px;
  min-width: 0;
  flex: 1;
}

/*
  THE SECOND LINE HOLDS TWO THINGS AND SHOWS ONE.

  The stats sentence and the owner's buttons share the row, and hovering
  swaps which one you see. Both are in the same grid cell, so the row is
  exactly as tall reading as it is acting — a list of places that grew a
  line every time the pointer crossed it was the single most restless thing
  on this screen.
*/
.place-stats,
.place-actions {
  grid-row: 2;
  grid-column: 1;
  transition: opacity 120ms ease;
}

.place-row:hover .place-stats,
.place-row:focus-within .place-stats { opacity: 0; }

.place-title {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
}

/*
  THE OCCUPANCY DISC: a place with something in it.

  Sized and weighted like the vehicle pins' open disc, because it is the same
  gesture — a mark on the map growing into a thing you can read. The scale
  and opacity are driven from map.js with ring.js's easing, so nothing here
  animates on its own; this is only what it looks like when it arrives.
*/
/* Under the vehicle pins: the disc is the setting, the pins are the subject.
   MapLibre gives markers no z-order of their own, so this is the only place
   the question gets answered. */
.place-busy { z-index: 0; }

.place-busy {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  /* A starting size only: map.js sizes every disc to its place's own ground
     radius on every camera move, with a floor and a ceiling. These two are
     what the element is before the first of those runs. */
  width: 64px;
  height: 64px;
  border-radius: 50%;
  color: #fff;
  box-shadow: var(--shadow-card);
  border: 3px solid rgba(255, 255, 255, 0.85);
  /* Grown from the middle: a disc that expands from its own centre reads as
     the place opening up, not as a pin dropping onto it. */
  transform-origin: 50% 50%;
  pointer-events: none;
  text-align: center;
  padding: 4px;
  box-sizing: border-box;
}

.place-busy-name {
  font-size: 11px;
  font-weight: 800;
  line-height: 1.1;
  /* A long name gets two lines and then stops, rather than pushing the count
     out of its own circle. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 62px;
}

.place-busy-count {
  font-size: 17px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/*
  An empty place's circle is the SAME circle — same colour, same weight,
  same size as its own radius. Only the count is missing, because there is
  nothing to count; the place is no less a place for being empty.

  It used to be smaller and dimmed, which made "nothing here" look like a
  different kind of object rather than the same object with a different
  answer in it.
*/
.place-busy-empty .place-busy-name { font-size: inherit; }

/* The build stamp: small, dim, and always there. Not decoration — it is the
   answer to "are you looking at what I just shipped". */
/*
  ON the arch, not under it. It used to be pushed to the floor of the rail by
  `margin-top: auto`; the arch now owns that floor and runs off the bottom of
  the window, so the stamp is lifted out of the flow and set over it.

  It was also 45% WHITE — the last of the colours left over from the dark
  rail. Invisible on the pale shell, and on the white arch it would have been
  gone completely.
*/
.account-build {
  /*
    IN THE FLOW, AT THE FOOT OF THE COLUMN.

    It was absolute at the sidebar's bottom, which was right while the arch
    ended at the window. The arch now runs past the window on the fleet page
    and the column scrolls — and an absolute box in a scroll container is
    placed against the scrolled content, so the stamp came to rest in the gap
    between two of the count discs.
  */
  margin-top: 14px;
  text-align: center;
  padding: 0;
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--text-tertiary, #7e8f93);
  opacity: 0.8;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}


/*
  THE MAP'S OPTIONS, AS A RAIL OF ICONS.

  One glass strip down the left edge, holding three layer switches, a
  divider, the picture switch, another divider and — for an owner — the one
  action. Left rather than right because MapLibre's zoom owns the right and
  the attribution owns the bottom, and those are not ours to take.

  Vertically centred: a strip pinned to the top of a tall map leaves its
  buttons a long way from the middle of the reader's attention, and pinned
  to the bottom it fights the attribution. Centred, it is the same reach
  whatever shape the window is.
*/
/*
  ============================================================
  THE LAYER RAIL IS A TAB OUT OF THE LEFT EDGE
  ============================================================

  The same move the nav makes downward out of the bar, turned through ninety
  degrees: the shell's colour, a radius on the two corners that are free, and
  the nav tab's shadow rotated so it casts to the right. It was a floating
  glass pill sitting on the map; as a tab it belongs to the shell around the
  map instead of to the map, which is what it actually is — chrome.

  Closed it is a sliver at the edge, so the map keeps the space.
*/
.map-rail {
  position: absolute;
  left: 0;
  /* The shared hanging line — see --rail-top. For THIS rail it lands where
     `top: 50%` centring used to put it. */
  top: var(--rail-top);
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 6px;
  background: var(--shell, #dce8ea);
  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
  /*
    SPREAD IS HALF THE BLUR, so the shadow cannot bleed on the sides the
    offset is not pushing it. At -3 it reached ~3px past the rail's top and
    bottom edges and smudged the corner fillets there — which read as a faint
    gradient at the junction and made the tab look stuck onto the shell
    rather than part of it. At -6 the blur begins exactly at those edges and
    the only place any shadow lands is on the map to the right.
  */
  box-shadow: 7px 0 12px -6px rgba(16, 45, 52, 0.3);
  /*
    A PERCENTAGE IN A TRANSLATE RESOLVES AGAINST THE ELEMENT'S OWN WIDTH, so
    this leaves exactly --rail-tab-w showing whatever the buttons inside
    happen to measure. Nothing here has to be told how wide the rail is, and
    adding a button to it cannot break the closed state.
  */
  /*
    OUT ALREADY, and it arrives by sliding — every time the map is selected,
    not only on first load.

    An animation restarts when an element's OWN display goes none → shown.
    Hiding the view around it is not enough: with `hidden` on the ancestor
    this element still computes `display: flex`, so the animation is never
    re-triggered and the slide happened exactly once per page load. Measured,
    not assumed — the rule below is what makes it true.
  */
  align-items: stretch;
  width: var(--rail-w-icons);
  animation: map-rail-out 360ms cubic-bezier(0.2, 0, 0, 1) both;
  transition: width 240ms cubic-bezier(0.2, 0, 0, 1);
}

@keyframes map-rail-out {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

/*
  The view is already unrendered when it is hidden, so this changes nothing
  you can see. What it changes is the RAIL'S OWN computed display, which is
  the only thing the animation restart keys off. See the note above.
*/
#view-map[hidden] .map-rail { display: none; }

/*
  HOVER WIDENS IT AND THE DISCS BECOME PILLS. The icons never move — the
  button grows to the right of them and the name arrives in the space that
  opens, which is the same bargain the top bar's tabs make.
*/
/*
  THE PILLS STEP DOWN FOR THE TAB.

  `:has()` because the two are on opposite branches of the tree — the nav is
  in the bar, the pills are in the map — and no combinator reaches sideways
  and up. Where it is not supported the rule is simply dropped: the pills
  stay at the top and a tab briefly covers them, which is the same thing that
  happened before any of this and not worth a script to avoid.

  `:focus-visible` alongside `:hover`, so tabbing along the bar moves them
  too rather than dropping a tab onto pills that stayed put.
*/
body:has(.nav button:hover) .map-window,
body:has(.nav button:focus-visible) .map-window {
  top: calc(var(--nav-drop) + 12px);
}

@media (prefers-reduced-motion: reduce) {
  .map-window { transition: none; }
}

.map-rail:hover,
.map-rail:focus-within { width: var(--rail-w-named); }

/*
  THE SAME CORNER, TURNED. The layer tab leaves the shell's left edge rather
  than the bar's underside, so its two right angles are above and below it and
  the hollowing circle sits on the far side. Absolutely positioned for the
  same reason as the nav's — the rail is a flex column.
*/
.map-rail::before,
.map-rail::after {
  content: '';
  position: absolute;
  left: 0;
  width: var(--tab-fillet);
  height: var(--tab-fillet);
  pointer-events: none;
}

.map-rail::before {
  top: calc(-1 * var(--tab-fillet));
  background: radial-gradient(circle at top right,
    transparent 0 var(--tab-fillet), var(--shell, #dce8ea) var(--tab-fillet));
}

.map-rail::after {
  bottom: calc(-1 * var(--tab-fillet));
  background: radial-gradient(circle at bottom right,
    transparent 0 var(--tab-fillet), var(--shell, #dce8ea) var(--tab-fillet));
}

.map-rail-button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--status-upcoming);
  /* Full width of whatever the rail currently is: a 38px disc while it shows
     icons, a pill once it has widened. One radius serves both — 999px on a
     square is a circle. */
  width: 100%;
  height: 38px;
  display: flex;
  align-items: center;
  gap: 9px;
  /* 8px each side of a 21px glyph in a 38px disc leaves it centred to half a
     pixel, so the icon does not visibly shift when the pill grows past it. */
  padding: 0 8px;
  border-radius: 999px;
  /* The name is laid out before the rail is wide enough to hold it. */
  overflow: hidden;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}

.map-rail-button svg { flex: none; }

.map-rail-label {
  white-space: nowrap;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  opacity: 0;
  transition: opacity 150ms ease;
}

.map-rail:hover .map-rail-label,
.map-rail:focus-within .map-rail-label { opacity: 1; }

.map-rail-button svg { width: 21px; height: 21px; display: block; }

.map-rail-button:hover { background: rgba(23, 61, 82, 0.09); }

/*
  ON MEANS FILLED. These are switches, not links, so the state has to be
  readable without hovering anything: a filled brand disc for on, plain
  glass for off. `aria-pressed` is the same fact for a screen reader, which
  is why the styling hangs off it rather than off a class.
*/
.map-rail-button[aria-pressed="true"] {
  background: var(--brand);
  color: var(--text-on-brand);
}

.map-rail-button[aria-pressed="true"]:hover { background: var(--brand-pressed); }

.map-rail-button:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* The one button that does something rather than showing something. */
.map-rail-add { color: var(--brand); }

.map-rail-split {
  align-self: center;
  width: 22px;
  height: 1px;
  background: rgba(23, 61, 82, 0.16);
  margin: 2px 0;
}

@media (prefers-reduced-motion: reduce) {
  .map-rail-button, .map-rail, .map-rail-label { transition: none; }
  .map-rail { animation: none; }
}

/*
  The count, top-centre over the map. It is a caption for what the rail has
  been set to, so it sits away from the rail rather than in it — an icon
  strip with one word-pill wedged into it reads as a broken icon.
*/
/*
  The map's own caption, top-centre: which trail is being drawn and how many
  vehicles are on screen. Away from the layer rail on the left and the zoom
  on the right, which are the two places a reader's hands already are.
*/
.map-window {
  position: absolute;
  /*
    UP AGAINST THE MAP'S TOP EDGE, and out of the way only when it has to be.

    It used to sit permanently below the nav's deepest tab, which cost 36px
    of map on every screen to avoid a collision that only happens while
    somebody is pointing at the bar. Now it stands at the top and steps down
    for the tab, on the same curve and over the same distance the tab comes
    down — so the two move as one thing rather than one dodging the other.
  */
  top: 14px;
  transition: top 220ms cubic-bezier(0.2, 0, 0, 1);
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

/*
  `.map-window .filters` IS GONE. The window row moved into the vehicle
  card's Past trips tile — see `.sheet-windows` — because choosing three days
  of trail is a question about one vehicle and was standing over the whole
  map. `.map-window` itself stays: it still carries the count and the
  following chip.
*/

/*
  FOLLOWING — the map's own chrome, sitting with the count it belongs beside.

  It takes clicks, unlike `.map-chip`, because it is the off switch as well
  as the announcement. The brand fill rather than the glass the rest of the
  chrome uses: this is a mode the map is IN, not a fact about what it holds,
  and it should be findable without reading.
*/
.map-follow {
  position: relative;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: var(--text-on-brand);
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
  box-shadow: var(--shadow-card);
  cursor: pointer;
}

/* The dot that says this is live, not a label somebody left behind. */
.map-follow::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  animation: map-follow-pulse 1.8s ease-in-out infinite;
}

.map-follow::after { content: '\00d7'; opacity: 0.75; }
.map-follow:hover { background: var(--brand-pressed); }
.map-follow:hover::after { opacity: 1; }

@keyframes map-follow-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

@media (prefers-reduced-motion: reduce) {
  .map-follow::before { animation: none; }
}

.map-chip {
  position: relative;
  z-index: 5;
  background: rgba(255, 255, 255, 0.62);
  -webkit-backdrop-filter: blur(16px) saturate(1.5);
  backdrop-filter: blur(16px) saturate(1.5);
  border: 1px solid rgba(255, 255, 255, 0.78);
  padding: 7px 15px;
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 700;
  color: var(--status-upcoming);
  box-shadow: var(--shadow-card);
  pointer-events: none;
}


/* =====================================================================
   THE VEHICLE PAGE — the phone's screen, expanded on the map.
   =====================================================================

   Transcribed from the app's VehicleDetail: a left-aligned header that
   says how the vehicle IS before what it is, the render floating free on
   its own contact shadow, a frosted pane carrying where-it-is beside
   how-fast, the action circles, then one card with the odometer and the
   koru gauges.

   IT GROWS OUT OF THE CARD, ON THE MAP. It used to arrive as a full-screen
   scrim: the whole console dimmed, the map went away, and a page about one
   vehicle took the screen from the thing that was giving it context. Now it
   is a panel that expands from whatever was clicked and floats over the
   pane, the same way the place panel and the pin cards do — the map stays
   where it was and stays live underneath.

   Every measurement inside is still the app's own — spacing.md is 12, the
   stage is 344 x 261, the dials are 66, the gauge disc is 62 — and they are
   sized against a phone's column. Stretched to a desktop width this would
   not be the same screen; it would be a different design made of the same
   parts.
*/

/*
  A LAYER THAT DOES NOT CATCH ANYTHING.

  `pointer-events: none` and no background at all: this element exists only
  to give the panel a fixed coordinate space. A scrim — even a transparent
  one — would swallow every click meant for the map underneath, which is
  precisely the takeover this stopped being.
*/
/*
  ONE CLOCK FOR THE CARD AND ITS CROWN.

  They are two elements that have to read as one object, so the duration and
  the curve are named here, on the layer that holds both, rather than written
  out twice. Written twice they drifted — 680ms against 260ms, and two
  different curves — and the ledge visibly raced the card it belongs to.

  IN IS SLOWER THAN OUT on purpose: opening is the gesture and deserves to be
  watched, closing is getting out of the way.
*/
.sheet-layer {
  --sheet-ease: cubic-bezier(0.4, 0, 0.2, 1);
  --sheet-in: 680ms;
  --sheet-out: 320ms;

  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
}

/*
  THE INVISIBLE PANE OF GLASS OVER THE TILES.

  An overlay that holds the map still while a card is being read: it cannot
  be dragged, wheel-zoomed or clicked through, and it is the dismiss target
  for whatever is open.

  IT LIVES INSIDE THE MAP CONTAINER, put there by map.js, and that is not
  tidiness — it is the only place it works. `#map-canvas` is
  `position: absolute; z-index: 1`, which makes it a STACKING CONTEXT, so
  everything inside it is sealed into z-index 1 as far as the rest of the
  page is concerned. A guard outside that context sits above the whole map
  whatever number it is given, including above the place panel it is meant
  to be protecting — which is exactly what made a panel's cards and its
  menu unclickable.

  z-index 3, from inside: above the tiles and below the furniture that
  floats on them — a place's pinned panel (5), the count (5), the layer
  rail (6). That ordering IS the feature.
*/
.sheet-guard {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: transparent;
}

/*
  The sage IS the page on the phone — the frosted panes inside only read as
  frost because there is a flat colour behind them to be milky against. A
  white sheet here would have made every one of them invisible.

  Position and size are written by app.js at open time, clamped inside the
  pane the reader is looking at: a panel that hangs off the side of the map
  is a panel that has forgotten what it is floating on.
*/
.sheet {
  position: absolute;
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--radius-2xl);
  /*
    THE PLACE PANEL'S GREY, not the page's sage.

    It grows out of that panel and lands on the map beside it, so the two
    have to be made of the same material — a sage box opening out of a grey
    one reads as a different object arriving rather than as the same one
    getting bigger. The frosted panes inside still read as frost against
    it: `--surface-muted` is flat and pale, which is all a frost needs to
    be milky against.
  */
  background: var(--surface-muted);
  box-shadow: var(--shadow-raised);
  color: var(--text);
  /*
    THE FOUR NUMBERS THAT MAKE THE MORPH, on ring.js's curve and ring.js's
    two durations: opening is the gesture, closing is getting out of the
    way, so closing is quicker.

    Declared here rather than in script so a browser that is not animating —
    a hidden tab, reduced motion — lands on the end state instead of on
    frame one. That is the lesson the occupancy disc taught this codebase
    the expensive way.
  */
  transition:
    left var(--sheet-in) var(--sheet-ease),
    top var(--sheet-in) var(--sheet-ease),
    width var(--sheet-in) var(--sheet-ease),
    height var(--sheet-in) var(--sheet-ease),
    border-radius var(--sheet-in) var(--sheet-ease),
    opacity 200ms linear;
}

.sheet.is-shutting {
  transition:
    left var(--sheet-out) var(--sheet-ease),
    top var(--sheet-out) var(--sheet-ease),
    width var(--sheet-out) var(--sheet-ease),
    height var(--sheet-out) var(--sheet-ease),
    border-radius var(--sheet-out) var(--sheet-ease),
    opacity 260ms linear 60ms;
}

@media (prefers-reduced-motion: reduce) {
  .sheet, .sheet.is-shutting, .sheet-body,
  .sheet-crown, .sheet-crown.is-shutting { transition: none; }
}

/*
  BACK SITS OVER THE PAGE, at its own top-left corner — the same place the
  app puts it, and outside the scrolling part so it cannot scroll away from
  the reader who needs it.
*/
/*
  ============================================================
  THE CROWN
  ============================================================

  A second panel behind the card, 25px taller, so exactly that strip shows
  above it. Same material as the card — it is the same object, seen from
  behind — but a shade darker so the two read as stacked rather than as one
  box with a badly drawn edge.
*/
.sheet-crown {
  position: absolute;
  pointer-events: auto;
  z-index: -1;
  display: flex;
  /*
    THE ROW LIVES IN THE PEEK, NOT THE BOX. The crown is as tall as the card
    plus 75px, so centring the controls in it would put them behind the card.
    Pinned to the top and padded by (75 - 40) / 2, which centres the 40px
    buttons inside the strip that actually shows.
  */
  align-items: flex-start;
  /* Back, maximise and minimise cluster on the LEFT — they all act on this
     window — with the edit dots alone on the right, because that one acts on
     the vehicle. The plate stretches between them and stays centred. */
  gap: 8px;
  /* (55 - 40) / 2, so the 40px controls sit centred in the 55px strip. */
  padding: 8px 12px 0;
  border-radius: var(--radius-2xl);
  color: var(--text);
  opacity: 0;

  /*
    LIQUID GLASS, level 2 — it floats over the map, which is what that level
    is for. Level 1 is for panels resting on the page, and this is not
    resting on anything.
  */
  background: var(--glass-2-bg);
  -webkit-backdrop-filter: var(--glass-2-blur);
  backdrop-filter: var(--glass-2-blur);
  box-shadow: var(--glass-2-shadow), var(--glass-edge);

  /*
    AND IT FADES OUT BY 150px.

    The crown is as tall as the card plus its peek, but only the top of it is
    ever meant to be seen — below that it is a backing sheet, and a full
    height of blurred glass behind an opaque card is a blur nobody looks at
    and a compositor layer the size of the pane.

    A MASK RATHER THAN A GRADIENT BACKGROUND, because backdrop-filter cannot
    be faded by a background: the blur applies to the whole element or none
    of it. The mask fades the element itself — glass, blur, shadow and all —
    so what is left below 150px is genuinely nothing.
  */
  -webkit-mask-image: linear-gradient(180deg, #000 0, #000 55px, transparent 150px);
  mask-image: linear-gradient(180deg, #000 0, #000 55px, transparent 150px);

  /*
    THE CROWN MOVES WITH THE CARD, on the card's clock.

    IT USED TO HAVE ITS OWN: 260ms on a springier curve against the card's
    680ms, so the ledge arrived at its destination before the card was half
    way there and took a different path getting there. Two pieces of one
    object visibly disagreeing about where they were going — which is exactly
    what it looked like.

    The numbers live on `.sheet-layer` now and BOTH read them, so they cannot
    drift apart again. Only the OPACITY is still the crown's own, and that is
    deliberate: it fades in after the card has somewhere for it to sit behind
    — see the note in `expandSheet`. Arriving late is a choice; travelling at
    a different speed was not.
  */
  transition:
    opacity 180ms ease,
    left var(--sheet-in) var(--sheet-ease),
    top var(--sheet-in) var(--sheet-ease),
    width var(--sheet-in) var(--sheet-ease),
    height var(--sheet-in) var(--sheet-ease),
    border-radius var(--sheet-in) var(--sheet-ease);
}

/* Closing is quicker than opening, for the card and therefore for this. */
.sheet-crown.is-shutting {
  transition:
    opacity 180ms ease,
    left var(--sheet-out) var(--sheet-ease),
    top var(--sheet-out) var(--sheet-ease),
    width var(--sheet-out) var(--sheet-ease),
    height var(--sheet-out) var(--sheet-ease),
    border-radius var(--sheet-out) var(--sheet-ease);
}

.sheet-crown.is-up { opacity: 1; }

/*
  THE STRIP IS 25px AND ITS CONTENTS HAVE TO FIT IT. Buttons at 20px and a
  plate at 11px sit inside that with a pixel to spare; anything larger and
  the ledge grows to meet it, which would put the card's own top edge back
  under the controls it was moved out from under.
*/
/*
  40px CONTROLS IN A 75px LEDGE. Centred in it rather than pinned to the top,
  so the strip reads as a bar with things in it rather than as a card with
  buttons falling off the edge.
*/
.sheet-crown-button {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.55);
  color: var(--status-upcoming);
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(16, 34, 38, 0.12);
}

.sheet-crown-button:hover { background: #ffffff; }
.sheet-crown-button svg { width: 20px; height: 20px; }

/*
  THE EDIT PANEL, BESIDE THE CARD.

  `showModal` centres a dialog through UA styles and a top-layer placement
  that ordinary positioning does not reach — `inset: 0` plus `margin: auto` is
  what does the centring, so overriding the inset is what undoes it. Left and
  top are then written from the card's measured rectangle in `placeBeside`.

  THE BACKDROP GOES ALMOST CLEAR. The point of moving the panel out here is to
  keep the vehicle visible while it is edited; a dimmed sheet behind it would
  give that back. It still takes the click that closes on a backdrop press, so
  the escape is unchanged — it simply stops hiding what it covers.
*/
/*
  THE EDIT FORM, WEARING THE CARD'S CLOTHES.

  It is the same `<form>` the "Add a vehicle" dialog uses, moved into the
  card's right-hand column — so it arrives dressed as a dialog: its own
  surface, its own padding, a footer pinned to the bottom of a box that no
  longer exists. These undo that, and only that. Everything about the fields
  themselves is left to the dialog's own rules, because the two have to keep
  looking like one form.

  IT SCROLLS IN THE COLUMN, not on the page: the card is a fixed height and
  the form is taller than it on a short window.
*/
.sheet-edit-pane { display: flex; flex-direction: column; }

/*
  THE CALENDAR, OVER THE PANEL IT BELONGS TO.

  FILLS THE EDIT SIDE rather than hanging under the date box. A popover
  anchored inside a scrolling column either clips at the column's edge or
  escapes it and floats over the map; a month grid wants this width anyway.

  THE FORM BEHIND IS BLURRED, NOT HIDDEN. Blurring keeps the shape of the
  panel you were editing — the same fields, out of focus, underneath the thing
  that is momentarily in front of them — where `display: none` would read as
  having navigated somewhere else and lost the form.

  `pointer-events: none` on the blurred form matters as much as the blur: a
  field you can still click through a veil is a field somebody will click.
*/
.sheet-edit-pane { position: relative; }

.sheet-edit-pane.is-picking > form {
  filter: blur(5px);
  opacity: 0.55;
  pointer-events: none;
  user-select: none;
}

.sheet-cal {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 2px 2px 6px 0;
  /* Its own ground, so the blurred form does not show THROUGH the days. */
  background: color-mix(in srgb, var(--card-surface) 88%, transparent);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  border-radius: var(--radius-lg);
  animation: tera-cal-in 160ms cubic-bezier(0.2, 0, 0, 1);
}

.sheet-cal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.sheet-cal-head .sheet-history-title { margin: 0; }

/* The grid scrolls inside the layer; the heading and Done stay put. */
.sheet-cal-body { flex: 1; min-height: 0; overflow-y: auto; }

/*
  IN THE PANEL, THE MONTHS ARE NOT A POPOVER. `calendar.js` draws its grid as
  one so it can escape a 180px sidebar on the deliveries screen; here it has
  the panel's full width already, and a second floating box inside a floating
  box is one too many.
*/
.sheet-cal .cal-pop {
  position: static;
  width: auto !important;
  max-height: none !important;
  box-shadow: none;
  border: 0;
  padding: 0;
  background: none;
}

.sheet-cal .cal-months { max-height: none; }

@keyframes tera-cal-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .sheet-cal { animation: none; }
}

.sheet-edit-pane form {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  margin: 0;
  padding: 0 2px 4px 0;
  background: none;
  border: 0;
  box-shadow: none;
  max-width: none;
  width: auto;
}

/* The heading is the column's, above the form — see `mountEditForm`. */
.sheet-edit-pane form > h2 { display: none; }

/*
  THE ACTIONS SIT ON THE COLUMN'S FLOOR, the way the dialog's sat on its own.
  Sticky rather than fixed so a long form scrolls under them and Save is
  never the thing you have to hunt for.
*/
.sheet-edit-pane form > .signin-actions {
  position: sticky;
  bottom: 0;
  margin: 8px 0 0;
  padding: 10px 0 2px;
  background: linear-gradient(to top, var(--card-surface) 72%, transparent);
}

/*
  THE GAP THE PLATE USED TO FILL. The crown is three window controls on the
  left and one vehicle control on the right; with the plate gone there has to
  be something between them or they collapse into a single cluster and the
  distinction the split was drawn for disappears.
*/
.sheet-crown-spacer { flex: 1 1 auto; }

/*
  THE PLATE, ON THE CARD.

  It sat on the crown — a floating strip above the card — which put the one
  thing that NAMES the card outside it. Reading downward you met three
  buttons, a registration, more buttons, and only then the card it belonged
  to. On the card it is the first thing in the thing it describes.
*/
.sheet-plate {
  /* `.sheet-body` is a flex column whose render stretches to fill it, and a
     plain block in there gets shrunk to nothing. It keeps its own height. */
  flex: none;
  margin: 2px 14px 0;
  text-align: center;
  font-size: 17px;
  font-weight: 800;
  letter-spacing: 0.08em;
  line-height: 24px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
  MINIMISED: the card folds away and the ledge is all that is left. Height
  rather than display, so it folds instead of vanishing, and the crown keeps
  just enough of itself to be a bar worth clicking.
*/
.sheet.is-collapsed {
  height: 0 !important;
  opacity: 0;
  pointer-events: none;
  transition: height 260ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 160ms ease;
}

.sheet-crown.is-collapsed {
  height: 55px !important;
  cursor: pointer;
  align-items: center;
  padding-top: 0;
  /* Nothing below to fade into, so the mask comes off — otherwise the bar
     would dissolve halfway down its own height. */
  -webkit-mask-image: none;
  mask-image: none;
}

.sheet-back {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 2;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.72);
  color: var(--status-upcoming);
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(10, 10, 12, 0.16);
}

.sheet-back:hover { background: #fff; }
.sheet-back svg { width: 20px; height: 20px; }

/*
  The page itself scrolls inside the panel. A phone screen is taller than a
  laptop's map pane by a hundred pixels or so, and the alternatives were
  cropping the render or shrinking the layout until it stopped being the
  app's layout.
*/
.sheet-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  /*
    52px AT THE TOP WAS ROOM FOR THE BACK BUTTON, which floated over the
    render. It lives on the crown now and the card's top edge is clear, so
    that padding was a band of nothing above the vehicle.
  */
  padding: 10px 0 0;
  /* Its width is written by app.js and held there through the whole
     animation, so the panel clips the page rather than re-wrapping it on
     every frame. `flex: none` on the cross axis is what stops the flex
     parent overriding that while it is narrower than the contents. */
  flex-shrink: 0;
  /* Late, once there is a panel-shaped thing to be inside — the same
     arrival the map's cards give their contents. */
  transition: opacity 300ms ease 260ms;
}

/* -------------------------------------------------- how it is, what it is */

.sheet-header { padding: 0 12px 2px; display: flex; flex-direction: column; gap: 3px; }

.sheet-status { display: flex; align-items: center; gap: 8px; }

/* The dot repeats the word's colour rather than replacing it, so the state
   survives greyscale and a screen reader alike. */
.sheet-dot {
  width: 11px;
  height: 11px;
  flex: none;
  border-radius: 50%;
}

.sheet-status h2 {
  margin: 0;
  font-size: 22px;
  line-height: 28px;
  font-weight: 700;
  color: var(--text);
}

.sheet-sub {
  margin: 0;
  font-size: 12px;
  line-height: 16px;
  font-weight: 500;
  color: var(--text-secondary);
}

/* ------------------------------------------------------------- the stage */

/*
  THE RENDER TAKES THE HEIGHT. It was pinned to 279px, which on a card the
  shape of a phone left a tall empty column under it and the tiles floating
  in the middle of nowhere. Growing instead: the vehicle sits centred in
  whatever room is left above the tiles, which is the arrangement the card
  was drawn for.

  THE FLOOR IS THE OLD FIXED HEIGHT, so a short pane cannot squeeze the
  vehicle to nothing — below that the body scrolls, which it already does.
*/
.sheet-stage-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 344px; /* the render is square now — see .sheet-stage */
  /* The bay is absolutely placed against this box. */
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: center;
}



/*
  SHORTER THAN THE RENDER, on purpose: the PNGs are square with a
  transparent margin, so a taller image in a shorter box crops that margin
  away and the vehicle comes out bigger than any frame would let it be.
*/
/*
  THE BAY SITS UNDER BOTH LAYERS — the picture and the model — so whichever
  one is showing stands in it. Anchored to the bottom of the wrap rather
  than centred: a floor that floats halfway up the card is not a floor.
*/
/*
  PINNED TO THE RENDER'S OWN BOX, not stretched across the card.

  It was 100% of the wrap with `slice`, which is fine at phone width and
  falls apart maximised: the SVG scales to COVER a box three times wider,
  so the bay blows up, crops, and the two side lines end up near-vertical
  and a lane apart. Sized to the render instead — 344x261, the same square
  the vehicle is drawn in — so the markings stay the vehicle's markings at
  every width the card can be.
*/
.sheet-bay {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 344px;
  height: 261px;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 0;
  /* 760ms and this curve are the model's own turn — see DRIVE_MS and the
     ease in vehicle-3d.js. The bay leaving and the road arriving are one
     event, and two halves on different curves read as two. */
  transition: opacity 760ms cubic-bezier(0.65, 0, 0.35, 1);
}

/*
  A DRIVING VEHICLE IS NOT PARKED IN ANYTHING. The bay is the strongest
  "this is standing still" signal on the card — a marked bay under a car
  that has turned onto a road would contradict the whole picture — so it
  goes as the road comes in.

  THE CLASS IS SET FROM THE LOADER, not from the reading: only a vehicle
  whose model actually rendered has a road to swap to. See vehicleStage.
*/
.sheet-stage-wrap.is-driving .sheet-bay { opacity: 0; }

/*
  SQUARE, NOT A LETTERBOX.

  261 was 344 x 0.76, and the 0.76 was there when the vehicle was framed
  against this box's height: a shorter box meant a bigger vehicle. It also
  meant `overflow: hidden` cut the render 40px under the wheels, and the
  three.js shadow — which falls toward the camera, so straight down the
  screen — ended in a hard horizontal line. That line is what read as the
  model being sliced off rather than coming down lower.

  The framing is measured across now (see vehicle-3d.js), so the height is
  free and this is the same square the render itself is: nothing is cropped
  and the shadow lands inside the box.
*/
.sheet-stage {
  position: relative;
  z-index: 1;
  height: 344px;
  width: 100%;
  display: grid;
  place-items: center;
  overflow: hidden;
}

/* The picture, which is the answer until the model has drawn one — see
   vehicle-3d.js. Same fade length as the model's, so the swap crosses
   rather than blinks. */
.sheet-render {
  width: 344px;
  height: 344px;
  display: block;
  transition: opacity 260ms ease;
}

/*
  The model, laid exactly over the picture it replaces so the swap has no
  movement in it. Fading in rather than appearing: the file is 2.4MB and
  arrives whenever it arrives, and a vehicle that pops into existence a
  second after the page opened reads as a glitch.
*/
.sheet-model {
  position: absolute;
  inset: 0;
  display: block;
  transition: opacity 260ms ease;
}

/* ------------------------------------------------------------- the glass */

/*
  THE FROST IS THE WHITE, NOT THE BLUR — the app's own scar, and it applies
  here for the same reason. What is behind these panes is the page's flat
  sage, and blurring one colour returns that colour however hard you blur
  it. The milkiness has to be added; the blur only does the edge work where
  the render and the shadow smear through.
*/
/*
  IT DOES NOT SHRINK, and this was a real bug rather than a precaution.

  A flex item's default is `0 1 auto` — it gives up height when the column
  is short — and this pane also clips its own overflow. On a laptop-height
  card the sum of the render, this pane and the two blocks is a few dozen
  pixels over, and the difference came entirely out of here: measured at
  33px tall against 78px of content, so the dials were cut in half and
  nothing said so. The card scrolls, which is what it is for.
*/
.sheet-glass {
  flex: none;
  margin: 0 12px;
  border-radius: 28px;
  border: 1px solid rgba(255, 255, 255, 0.9);
  /*
    THE FROST IS 44% WHITE ON THE PHONE, AND 44% IS NOT ENOUGH HERE.

    The app's panes are that white over the sage canvas, where the milkiness
    has a colour to be milky against. This page sits on `--surface-muted`,
    which is already a pale grey — 44% white over it lands within a couple
    of steps of the ground and the pane disappears. The MATERIAL is the
    same idea (white at low alpha, specular rim, blurred edge); the alpha is
    the number that has to answer to what is behind it.
  */
  background: rgba(255, 255, 255, 0.78);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  backdrop-filter: blur(18px) saturate(1.4);
  overflow: hidden;
}

.sheet-pane {
  /*
    NEVER SQUEEZED, ONLY SCROLLED PAST. This pane clips its overflow, and it
    sits in a flex column — so with the default `flex-shrink: 1` any content
    added below it does not make the sheet scroll, it makes the pane quietly
    lose its own bottom. The action row was still rendered and still
    clickable, just 217px below the visible edge.

    `.sheet-body` has `overflow-y: auto` for exactly this and was never
    reaching it.
  */
  flex-shrink: 0; margin-top: 12px; }

/* LEFT TO RIGHT, not pushed to both ends. The strip is a row of
   instruments now — control, speed, limit — and `space-between` put a gap
   through the middle of one object. */
.sheet-stage-footer {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  padding: 6px 10px;
}

/* 66 to match the dials exactly — same height, same baseline. */
.sheet-journey {
  flex-shrink: 1;
  min-width: 0;
  height: 66px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 3px;
  padding-left: 4px;
}

.sheet-journey-line {
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  font-size: 12px;
  line-height: 16px;
}

.sheet-journey-line svg { width: 13px; height: 13px; flex: none; }
.sheet-journey-line:first-child svg { color: var(--brand); }
.sheet-journey-line:last-child svg { color: var(--text-secondary); }

.sheet-journey-where {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sheet-journey-dwell { color: var(--text-secondary); }

/* ------------------------------------------------------------- the dials */

.sheet-dials { display: flex; gap: 10px; flex: none; }

/*
  A mark standing ON the glass, and this is not the card shadow: that one is
  tuned for white on sage — 4px down, 14px of blur, 8% — and on a pale frost
  it has nothing to darken against. Tighter and stronger is what makes a
  disc sit on the pane rather than be printed into it.
*/
.sheet-dial {
  width: 66px;
  height: 66px;
  border-radius: 50%;
  background: #fff;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 6px rgba(10, 10, 12, 0.16);
}

/* The limit is a road sign because that is the one shape everybody reads
   without a caption: a white disc inside a red ring. */
.sheet-sign { border: 5px solid #c8102e; }

.sheet-dial-number {
  font-size: 22px;
  line-height: 26px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.sheet-dial-number.is-over { color: var(--status-urgent); }

.sheet-dial-unit { font-size: 12px; line-height: 14px; color: var(--text-secondary); }

/* ----------------------------------------------------------- the actions */

.sheet-actions {
  display: flex;
  justify-content: space-between;
  padding: 16px 8px 12px;
}

.sheet-action {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
}

.sheet-action:disabled { cursor: not-allowed; opacity: 0.45; }

.sheet-action-circle {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: #fff;
  display: grid;
  place-items: center;
  color: var(--status-upcoming);
  box-shadow: 0 2px 6px rgba(10, 10, 12, 0.16);
}

.sheet-action-circle svg { width: 21px; height: 21px; }

.sheet-action:hover:not(:disabled) .sheet-action-circle { background: var(--surface-muted); }

.sheet-action-label {
  font-size: 12px;
  line-height: 16px;
  color: var(--text-secondary);
}

/* ----------------------------------------------- the odometer and gauges */

/* Inside the glass, so no surface of its own: one pane, one plate of
   numbers. The hairline still separates them. */
.sheet-card { overflow: hidden; }

.sheet-odometer {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 14px 18px;
}

.sheet-odometer-label {
  flex: 1;
  font-size: 12px;
  line-height: 16px;
  color: var(--text-secondary);
}

.sheet-odometer-value {
  font-size: 20px;
  line-height: 25px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.sheet-divider { height: 1px; background: var(--border); }

/*
  Left-aligned to the odometer row's own margin, not centred: two gauges
  floating in the middle of a full-width card read as leftovers.
*/
.sheet-gauges {
  display: flex;
  gap: 30px;
  padding: 14px 18px 16px;
}

.sheet-gauge { display: flex; flex-direction: column; align-items: center; gap: 4px; }

/* THE KORU SITS ON A DISC, like every other mark on this pane. Drawn
   straight onto the frost it was the one thing here with no ground under
   it, and a bare arc reads as an unfinished one. */
.sheet-gauge-disc {
  position: relative;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: #fff;
  display: grid;
  place-items: center;
  box-shadow: 0 2px 6px rgba(10, 10, 12, 0.16);
}

.sheet-gauge-disc svg { position: absolute; inset: 0; }

.sheet-gauge-value {
  position: relative;
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.sheet-gauge-caption { font-size: 12px; line-height: 16px; color: var(--text-secondary); }

/* ---------------------------------------------- what survives the actions */

/* Service keeps its row because no gauge draws it: the gauges are the three
   obligations with a fixed window, and a service interval is not one. */
.sheet-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 12px 12px 0;
  padding: 14px 18px;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.78);
  border: 1px solid rgba(255, 255, 255, 0.9);
  font-size: 14px;
}

.sheet-row-label { flex: 1; font-weight: 600; }
.sheet-row-value { color: var(--text-secondary); font-variant-numeric: tabular-nums; }

/* A fleet-table row is a way into a vehicle now, so it has to look it. */
.fleet-row { cursor: pointer; }
.fleet-row:hover { background: var(--surface-muted); }
.fleet-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/*
  A PANEL PARKED ON THE MAP, rather than a mark riding a coordinate.

  MapLibre's own markers are positioned with `translate(-50%, -50%)` off
  their coordinate and moved on every frame of a pan. This is the same
  centring, done once, in the map's own container — so the panel stays
  where it opened while the map moves underneath it.

  z-index above the marker layer: the panel is an answer to a question the
  marks raised, and an answer behind the question is no answer.
*/
.map-pinned {
  position: absolute;
  z-index: 5;
  transform: translate(-50%, -50%);
  pointer-events: auto;
}

/*
  THE PLACE MENU — edit or remove this place, from the map itself.

  Pinned to the panel's own top-right corner by map.js, which knows the
  panel's height and therefore where its corner is. DOM rather than SVG
  because a menu is a list of buttons with words on them, and everything
  else in that panel is drawn only because it has to morph.
*/
.place-menu {
  position: absolute;
  z-index: 2;
  transform: translateX(-100%);
}

.place-menu-dots {
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.place-menu-dots:hover { background: rgba(23, 61, 82, 0.08); color: var(--text); }
.place-menu-dots svg { width: 18px; height: 18px; display: block; }

.place-menu-list {
  position: absolute;
  top: 34px;
  right: 0;
  min-width: 156px;
  padding: 5px;
  display: flex;
  flex-direction: column;
  border-radius: 14px;
  background: #fff;
  border: 1px solid var(--border);
  box-shadow: var(--shadow-raised);
}

.place-menu-item {
  appearance: none;
  border: 0;
  background: none;
  font: inherit;
  font-size: 13px;
  text-align: left;
  padding: 8px 10px;
  border-radius: 9px;
  cursor: pointer;
  color: var(--text);
  white-space: nowrap;
}

.place-menu-item:hover { background: var(--surface-muted); }

/* The one destructive item keeps its own colour — the only warning a menu
   this short has room for. */
.place-menu-item.is-danger { color: var(--status-urgent); }
.place-menu-item.is-danger:hover { background: rgba(179, 38, 30, 0.08); }

/*
  THE SPEEDING BADGE — how fast, standing on the stretch where it happened.

  A red line says a vehicle was over the limit and cannot say by how much,
  which is the first thing anybody asks of one. This is the same speed mark
  the vehicle's own pin carries — white disc, the figure in the product's
  numeric face, km/h underneath — with an urgent rim, because this one is a
  finding rather than a reading.

  ONE PER SPEEDING RUN, at its fastest fix. See speedingRuns in map.js: a
  badge per beacon report would stack a dozen near-identical numbers on one
  stretch of road.
*/
.speed-flag {
  position: relative;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: #fff;
  /* The rim carries the severity; amber is the middle band the status
     triple does not have — see TRAIL_AMBER in map.js for why it exists. */
  border: 3px solid var(--status-urgent);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  box-shadow: 0 3px 8px rgba(16, 45, 52, 0.24);
  /* Inert by default — nothing here should take a click meant for the map or
     a mark under it. A badge with a second face turns this back on for
     itself; see `.is-flippable`. */
  pointer-events: none;
  /* The track slides inside the disc, so the disc is the window. */
  overflow: hidden;
  /* Room under the disc for the tail, so the marker's bottom edge — which
     is what MapLibre parks on the coordinate — is the tail's tip. */
  margin-bottom: 9px;
}

/*
  THE TAIL POINTS AT THE LINE. Without it the disc is a number floating near
  some road; with it there is no question which stretch it belongs to. Drawn
  as a bordered triangle so the rim carries on around the point, the same
  way the vehicle plate's tail continues its own fill.
*/
.speed-flag::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 10px solid var(--status-urgent);
}

/*
  THE MIDDLE BAND. Five to ten over is worth marking and is not the same
  finding as twenty over; the amber says so without competing with the red
  beside it.
*/
.speed-flag.is-warn { border-color: var(--warn); }
.speed-flag.is-warn::after { border-top-color: var(--warn); }
.speed-flag.is-warn .speed-flag-number { color: var(--warn); }

/*
  A GROUP, when several findings land within a disc's width of each other.
  The exclamation is the one glyph that means "look here" without naming a
  kind — which is right, because a group can hold three different kinds.
*/
.speed-flag-many {
  flex-direction: row;
  gap: 3px;
  /* No track inside a group — the exclamation and the count are the whole
     content, and there is nothing to slide to. */
  overflow: visible;
}

.speed-flag-bang {
  font-size: 19px;
  font-weight: 800;
  line-height: 1;
  color: var(--status-urgent);
}

.speed-flag-many.is-warn .speed-flag-bang { color: var(--warn); }

.speed-flag-count {
  font-size: 17px;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--status-upcoming);
}

/*
  TWO FACES ON ONE TRACK. Each is the full width of the disc, so translating
  the track by exactly 100% swaps which one is in the window — no measuring,
  and it stays right if the disc is ever resized.
*/
.speed-flag-track {
  display: flex;
  width: 100%;
  height: 100%;
  transition: transform 260ms cubic-bezier(0.4, 0, 0.2, 1);
}

.speed-flag-face {
  flex: 0 0 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
}

.speed-flag.is-flipped .speed-flag-track { transform: translateX(-100%); }

/* Rebuilt in the turned-over state: land there, do not slide there. A poll
   that made every flipped badge animate again would look like the map
   twitching once every ten seconds. */
.speed-flag.is-restoring .speed-flag-track { transition: none; }
/* A rebuilt pill arrives at its width rather than growing into it again. */
.speed-flag.is-restoring { transition: none; }

/* Only a badge with something behind it answers the pointer. */
.speed-flag.is-flippable { pointer-events: auto; cursor: pointer; }

.speed-flag.is-flippable:focus-visible {
  outline: 2px solid var(--status-upcoming);
  outline-offset: 2px;
}

/*
  THE LIMIT READS AS A LIMIT, not as a second speed. Navy rather than the
  severity's colour, because the limit is not the finding — it is the thing
  the finding was measured against, and coloring it red would make the rule
  look like the offence.
*/
.speed-flag-limit .speed-flag-number { color: var(--status-upcoming); }

/*
  THE BADGE ASKS A QUESTION, so it stops being a badge while it does.

  A 52px disc has room for a number, not for a number and a question and two
  answers. Turning it over therefore also widens it: the same element grows
  sideways into a pill, which reads as the mark opening up rather than as a
  dialog appearing somewhere else on the map. The tail underneath keeps
  pointing at the same stretch of road throughout, so there is never any
  doubt which finding is being asked about.

  THE WIDTH IS ANIMATED, NOT SWAPPED. Two elements — a disc that vanishes and
  a pill that appears — would lose the connection between the number somebody
  clicked and the question they are now being asked.
*/
.speed-flag.is-asking {
  width: 188px;
  border-radius: var(--radius-pill);
  transition: width 220ms cubic-bezier(0.22, 1, 0.36, 1);
}

.speed-flag-ask {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 4px;
  white-space: nowrap;
}

.speed-flag-ask-claim {
  font-size: 14px;
  font-weight: 800;
  color: var(--status-upcoming);
}

.speed-flag-ask-question {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
}

/*
  THE TWO ANSWERS, AND THERE IS NO THIRD.

  No field, no dropdown, no "other". The only things that can be said here
  are yes and no, because a place to type what the limit "should" be is a
  place to write 100 on a suburban street — see the note in
  20260821010000_speed_limit_reports.sql for why that is the whole design
  rather than a simplification.
*/
.speed-flag-verdict {
  width: 26px;
  height: 26px;
  flex: none;
  border: 0;
  border-radius: 50%;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  pointer-events: auto;
  color: #fff;
}

.speed-flag-verdict.is-correct { background: var(--brand); }
.speed-flag-verdict.is-wrong { background: var(--text-secondary); }
.speed-flag-verdict.is-correct:hover { background: var(--brand-pressed); }
.speed-flag-verdict.is-wrong:hover { background: var(--status-upcoming); }

/* Answered: the buttons go, the word stays. Nobody needs to be asked twice,
   and a pill that still offers a choice after one was made invites somebody
   to wonder whether the first one counted. */
.speed-flag-ask.is-answered .speed-flag-verdict { display: none; }
.speed-flag-ask.is-answered .speed-flag-ask-question { color: var(--brand); font-weight: 700; }

@media (prefers-reduced-motion: reduce) {
  .speed-flag.is-asking { transition: none; }
}

/*
  A FINDING THAT ARRIVES.

  During a replay the badges are revealed as the mark drives past the fixes
  that caused them, and something appearing between two frames with no
  movement at all reads as a rendering glitch rather than an event. The pop
  says "this just happened", which is the whole reason the replay reveals
  them in order instead of laying them all out at the start.

  ONLY ON THE FIRST PAINT OF A GIVEN MARK. Every camera move rebuilds these
  markers, and a replay following a vehicle moves the camera on every frame
  — so the class is added only where the previous paint had no such mark.
  See shownFlags in map.js.

  BOTTOM ORIGIN because the marker is anchored at its bottom: the tail's tip
  stays on its coordinate while the disc grows above it, so the badge swells
  out of the road rather than sliding onto it.
*/
@keyframes speed-flag-arrive {
  from { transform: scale(0.3); opacity: 0; }
  70%  { transform: scale(1.12); opacity: 1; }
  to   { transform: scale(1); opacity: 1; }
}

.speed-flag-arrive > .speed-flag {
  transform-origin: bottom center;
  animation: speed-flag-arrive 340ms cubic-bezier(0.22, 1, 0.36, 1);
}

@media (prefers-reduced-motion: reduce) {
  .speed-flag-track { transition: none; }
  .speed-flag-arrive > .speed-flag { animation: none; }
}

.speed-flag-number {
  font-size: 17px;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--status-urgent);
}

.speed-flag-unit {
  font-size: 8px;
  font-weight: 700;
  line-height: 1;
  margin-top: 1px;
  color: var(--muted);
  /* "g brake" and "g accel" are longer than "km/h" and must not wrap
     inside a 52px disc. */
  white-space: nowrap;
}

/*
  THE TRANSPORT BAR IS GONE — `.replay`, its play button, drive picker,
  scrub, clock and the chip naming the vehicle.

  Which trip is now the picker in the card's Past trips block, and whose is
  the card being open. What stayed is below: the mark the replay drops on
  the route, which is still how a chosen trip says where it began.
*/

/*
  THE MARK THAT WALKS THE ROUTE.

  Deliberately not the vehicle's plate: the plate says where the vehicle IS,
  and two of those on one map — one live, one an hour ago — is the map
  contradicting itself. A speed dial instead, rimmed in the vehicle's own
  colour, because the number that changes as it moves is the point.
*/
.replay-mark {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: #fff;
  border: 4px solid var(--brand);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  box-shadow: 0 3px 10px rgba(16, 45, 52, 0.28);
  pointer-events: none;
}

.replay-mark-number {
  font-size: 18px;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.replay-mark-unit {
  font-size: 8px;
  font-weight: 700;
  line-height: 1;
  margin-top: 1px;
  color: var(--muted);
}

/* ---------------------------------------------------------------- beacon
   Pairing a phone to a vehicle, from the vehicle's own card.

   The reveal block is deliberately the loudest thing in the sheet. It is on
   screen for one moment in a token's life — `beacon_tokens` keeps a hash and
   nothing else, so there is no second chance to read the address — and a
   quiet, tasteful presentation of a credential you cannot recover is a
   design that loses people their afternoon. */

.beacon-card {
  /* Its own glass, because it sits in the scrolling body beside the pane
     rather than inside it.

     `flex-shrink: 0` is the load-bearing line. `.sheet-body` is a flex
     column, so a new child does not lengthen it — it takes space from its
     siblings, and `.sheet-pane` clips instead of scrolling. Adding this card
     silently cut 125px off the bottom of the action row: "On the map" and
     "Follow" were still there, still clickable, just no longer visible.
     Measured with the card removed the pane clips by nothing at all, which
     is how the cause was pinned down. */
  flex-shrink: 0;
  margin-top: 10px;
  padding: 14px 18px 16px;
  border-radius: var(--radius-xl);
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

.beacon-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 10px;
}

.beacon-title {
  flex: 1;
  font-size: 12px;
  line-height: 16px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.beacon-state {
  font-size: 13px;
  line-height: 18px;
  color: var(--text-secondary);
}

.beacon-empty {
  margin: 0 0 10px;
  font-size: 13px;
  line-height: 18px;
  color: var(--text-tertiary);
}

.beacon-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-top: 1px solid var(--divider);
}

.beacon-row-label {
  flex: 1;
  font-size: 14px;
  line-height: 20px;
  color: var(--text);
}

.beacon-row-state {
  font-size: 13px;
  line-height: 18px;
  color: var(--text-tertiary);
}

.beacon-revoke {
  border: 0;
  background: none;
  padding: 4px 6px;
  border-radius: var(--radius-md);
  font: inherit;
  font-size: 13px;
  color: var(--status-urgent);
  cursor: pointer;
}

.beacon-revoke:hover { background: var(--surface-muted); }
.beacon-revoke:disabled { opacity: 0.5; cursor: default; }

.beacon-button {
  width: 100%;
  margin-top: 12px;
  padding: 10px 14px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--brand);
  color: var(--text-on-brand);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.beacon-button:hover { background: var(--brand-pressed); }

/* While an address is on screen the button's job is the opposite one, and it
   should not look like the thing that creates another credential. */
.beacon-button.is-done {
  background: var(--surface);
  color: var(--text-secondary);
  border: 1px solid var(--border);
}

.beacon-button.is-done:hover { background: var(--surface-muted); }
.beacon-button:disabled { opacity: 0.6; cursor: default; }

.beacon-reveal {
  margin-top: 12px;
  padding: 14px;
  border-radius: var(--radius-lg);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  text-align: center;
}

.beacon-once {
  margin: 0 0 12px;
  font-size: 13px;
  line-height: 18px;
  color: var(--text-secondary);
}

.beacon-qr {
  width: 180px;
  margin: 0 auto 12px;
}

/* The SVG carries its own quiet zone, so it fills the frame exactly. */
.beacon-qr svg {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
}

.beacon-address {
  display: block;
  /* The address has no spaces and must not be silently clipped — a truncated
     URL that looks complete is how a beacon gets configured wrong. */
  overflow-wrap: anywhere;
  word-break: break-all;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  line-height: 16px;
  color: var(--text);
  user-select: all;
}

.beacon-copy {
  margin-top: 10px;
  padding: 6px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  font: inherit;
  font-size: 13px;
  color: var(--text);
  cursor: pointer;
}

.beacon-copy:hover { background: var(--surface-muted); }

/* The pair button when the vehicle already has a beacon. Not an error state —
   it is the correct state most of the time — so it reads as unavailable
   rather than as something having gone wrong. */
.beacon-button.is-blocked,
.beacon-button:disabled.is-blocked {
  background: var(--surface-muted);
  color: var(--text-tertiary);
  border: 1px solid var(--border);
  opacity: 1;
}

/* A smooth scroll to a card that was already half in view looks like nothing
   happened, so the arrival is marked rather than merely performed. */
.beacon-card.is-sought {
  animation: beacon-sought 1.2s ease-out;
}

@keyframes beacon-sought {
  0%, 100% { box-shadow: var(--shadow-card); }
  25% { box-shadow: 0 0 0 3px var(--brand); }
}

@media (prefers-reduced-motion: reduce) {
  .beacon-card.is-sought { animation: none; box-shadow: 0 0 0 3px var(--brand); }
}

/* "Find nearby" is the second way to do the same job, so it reads as the
   quieter of the two rather than competing with pairing. */
.beacon-button.beacon-secondary {
  margin-top: 8px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
}
.beacon-button.beacon-secondary:hover { background: var(--surface-muted); }
.beacon-revoke.beacon-link { color: var(--brand); }

/* A fault, wherever a vehicle is named. Says "look here" and deliberately not
   what — idling, a quiet beacon and a vehicle moving with no ignition are not
   comparable, and ranking them into one colour scale would invent a judgement
   the data does not support. */
.fault-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 16px;
  margin-left: 6px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--status-urgent);
  color: #ffffff;
  font-size: 11px;
  line-height: 1;
  font-weight: 700;
  vertical-align: middle;
}

/* A fault beside the gauges: the same disc, in the urgent colour. */
.sheet-gauge-disc.fault-disc {
  background: var(--status-urgent);
  border-color: var(--status-urgent);
}
.sheet-gauge-disc.fault-disc .sheet-gauge-value { color: #ffffff; }

/* And a true circle wherever the inline badge is used — `min-width` plus a
   flex parent is what turned it into a full-width bar. */
.fault-dot {
  width: 16px;
  flex: 0 0 auto;
  align-self: center;
}

/* Six digits, read off a screen across a workshop and typed back. Big,
   monospaced and widely spaced so a transposition is visible. */
.beacon-code-field {
  display: block;
  width: 100%;
  margin: 4px 0 10px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 28px;
  letter-spacing: 8px;
  text-align: center;
}

/* Beacon health: quiet when everything is fine, and only ever one line. This
   sits on a card somebody opens to answer a different question, so it must
   not compete with the pairing controls until something is actually wrong. */
.beacon-health {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  min-height: 18px;
  margin-top: 2px;
}

/* A named fault, where the red dot would say "something" and this says what. */
.beacon-warn {
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  background: var(--status-urgent);
  color: #ffffff;
  font-size: 11px;
  line-height: 16px;
  font-weight: 600;
}

/* A reading older than an hour: still true, no longer news. Dimmed rather
   than hidden, because "the beacon is linked and has said nothing since
   Tuesday" is itself worth seeing — hiding it would look like no beacon. */
.beacon-health-stale {
  opacity: 0.45;
}

/* ============================================================
   USAGE CHARTS
   ============================================================ */

/*
  THE HEAD CARRIES THE PANEL'S RIGHT INSET. .panel has no padding of its own
  — every child pads itself, and `.panel > h3` uses 20px — so a control
  placed at the far end of this row had nothing holding it in and sat two
  pixels from the panel's edge, on top of a 28px corner radius. A raised
  pill group overhanging the corner of the surface it belongs to is what
  broke the boundary of this panel.
*/
.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding-right: 20px;
}

/* The headline numbers, before any chart. A chart answers "how does this
   compare"; a number answers "what is it", and most visits want the second. */
.usage-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 14px;
  margin: 20px 0 4px;
}

/*
  RECESSED, NOT RAISED, and this is the whole point of it. A stat box sits
  inside a panel, so it must read as a well cut into that panel rather than
  as a second card floating on top of one. A flat muted fill and no shadow
  is what says "part of this thing"; white-on-white with a rim of its own
  says "another thing", and five of those in a row is the card soup this
  screen had. Do not add it to a glass recipe — see the note there.
*/
.usage-stat {
  background: var(--card-surface);
  border-radius: var(--radius-md);
  padding: 16px 18px;
}

.usage-stat-value { font-size: 24px; font-weight: 700; line-height: 1.2; }
.usage-stat-label { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }

/*
  "SHOW ME WHERE" — the only stat on this card with somewhere to go.

  IN THE CORNER RATHER THAN UNDER THE LABEL, so it cannot push the figure
  out of line with the stats either side of it: these read as a row of
  numbers and a taller box in the middle breaks that.

  IT IS A REAL BUTTON WITH A REAL LABEL. The glyph alone is a pin, which
  says "a place" and not "which place" — the accessible name carries the
  speed and the date, which is the whole content of the answer.
*/
.usage-stat { position: relative; }

/* ON THE BOX'S CENTRE LINE. Pinned to the top it sat above the figure it
   belongs to and level with nothing — the stat is two lines centred in a
   tall well, and a control for it reads as attached only when it shares
   that line. */
.usage-stat-jump {
  position: absolute;
  top: 50%;
  right: 12px;
  transform: translateY(-50%);
  width: 28px;
  height: 28px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--brand);
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
}

.usage-stat-jump svg { width: 15px; height: 15px; display: block; }
.usage-stat-jump:hover { background: var(--brand); color: #ffffff; border-color: transparent; }
.usage-stat-jump:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Side by side on a desktop, stacked on a phone — a pie squeezed into a
   half-width column on a handset is unreadable, and these get looked at on
   phones in yards. */
.usage-charts {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--divider);
}

@media (min-width: 820px) {
  .usage-charts { grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); }
}

.chart { margin: 0; }

/*
  A DONUT HAS A NATURAL SIZE and stretching it to the column width does not
  make it more informative — it just makes a 200-unit viewBox render at nine
  hundred pixels. Capped and centred, so the panel breathes and the legend
  sits under a circle rather than beside a wall.
*/
.chart-donut svg { max-width: 260px; margin: 0 auto; }
.chart-donut { display: flex; flex-direction: column; align-items: center; }

/*
  THE FUEL PANE HOLDS ONE RING PER UNIT, and how many there are depends on the
  fleet: an all-diesel yard draws one, a mixed yard draws litres and kWh side
  by side. auto-fit rather than the two fixed columns above, so a single ring
  fills the row and stays centred instead of hugging the left edge of a
  column sized for a bar chart that is not there.
*/
#fuel-charts { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
#fuel-charts:empty { display: none; }
#usage-pie .chart-legend { width: 100%; max-width: 300px; }
.chart figcaption {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 8px;
}
.chart svg { width: 100%; height: auto; display: block; overflow: visible; }

.chart-legend { display: flex; flex-direction: column; gap: 10px; margin-top: 20px; }
.chart-legend-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.chart-swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.chart-legend-value { margin-left: auto; color: var(--text-secondary); font-variant-numeric: tabular-nums; }

.chart-empty { color: var(--text-tertiary); font-size: 13px; padding: 12px 0; }

/* ============================================================
   LIQUID GLASS — a depth system, not a skin
   ============================================================

   WHAT MAKES GLASS READ AS GLASS, in the order it matters:

     1. It refracts what is behind it      backdrop-filter: blur + saturate
     2. It catches light on its edge       a bright 1px inset at the top
     3. It sits ABOVE something            a shadow with real distance
     4. It has thickness                   a second, tighter inner shadow

   Most "glassmorphism" does the first and stops, which is why it reads as a
   blurry rectangle rather than a pane. The edge highlight is what the eye
   actually reads as a surface, and it is one line.

   WHY CSS AND NOT A SHADER LIBRARY. The obvious alternative captures the DOM
   to a texture every frame and refracts it in WebGL. It looks superb on a
   static page. This console redraws a live map every five seconds, runs on
   phones in yards, and ships no third-party JavaScript — which is why its
   CSP is three lines. backdrop-filter is GPU-composited by the browser
   itself, costs nothing per frame, and degrades to a solid surface where it
   is unsupported.

   SATURATE, NOT JUST BLUR. Blurring alone greys out what is behind it.
   Pushing saturation back up is what keeps the blue underneath looking like
   blue rather than like fog, and it is the difference between glass and
   frosted plastic.
*/

:root {
  /* The tint. Drawn from --brand so the glass belongs to the palette rather
     than sitting on top of it as neutral white. */
  --glass-tint: 47, 119, 128;

  /* FOUR LEVELS OF DEPTH. Each is a complete recipe, not a blur value — the
     shadow and the edge have to move together or the illusion breaks. */

  /* 0 — flush. Sits ON the canvas: no lift, just refraction. */
  --glass-0-bg: rgba(255, 255, 255, 0.55);
  --glass-0-blur: blur(16px) saturate(160%);
  --glass-0-shadow: 0 1px 2px rgba(16, 34, 38, 0.04);

  /* 1 — resting. Panels and cards. The workhorse. */
  --glass-1-bg: rgba(255, 255, 255, 0.62);
  --glass-1-blur: blur(22px) saturate(175%);
  --glass-1-shadow:
    0 1px 1px rgba(16, 34, 38, 0.04),
    0 8px 24px -6px rgba(16, 34, 38, 0.12);

  /* 2 — raised. Things that float over content: rails, pills, controls. */
  --glass-2-bg: rgba(255, 255, 255, 0.70);
  --glass-2-blur: blur(28px) saturate(185%);
  --glass-2-shadow:
    0 1px 1px rgba(16, 34, 38, 0.05),
    0 12px 32px -8px rgba(16, 34, 38, 0.18),
    0 2px 8px -2px rgba(16, 34, 38, 0.08);

  /* 3 — lifted. Sheets and dialogs, clearly off the page. */
  --glass-3-bg: rgba(255, 255, 255, 0.78);
  --glass-3-blur: blur(40px) saturate(190%);
  --glass-3-shadow:
    0 1px 1px rgba(16, 34, 38, 0.06),
    0 24px 60px -12px rgba(16, 34, 38, 0.28),
    0 4px 16px -4px rgba(16, 34, 38, 0.10);

  /*
    THE EDGE. A bright inset along the top and a dark one along the bottom:
    light falls from above, so the upper rim catches it and the lower rim is
    in the pane's own shadow. This is the single detail that turns a blurred
    box into something with a surface.
  */
  --glass-edge:
    inset 0 1px 0 rgba(255, 255, 255, 0.65),
    inset 0 -1px 0 rgba(16, 34, 38, 0.05);

  /* A tighter version for small controls, where a 1px rim is proportionally
     enormous and reads as a border rather than a highlight. */
  --glass-edge-fine:
    inset 0 0.5px 0 rgba(255, 255, 255, 0.55),
    inset 0 -0.5px 0 rgba(16, 34, 38, 0.04);
}

/*
  THE RECIPES. Applied to the classes that already exist, so nothing in the
  markup or the JavaScript has to change — a redesign that touched the DOM
  would put every behaviour in this console at risk for a visual change.

  ONLY SURFACES. .usage-stat and .table-scroll USED TO BE IN THIS LIST and
  that was the single thing most wrong with this screen: they live INSIDE a
  .panel, so glassing them gave a container and its contents the same
  elevation, the same white and the same rim, and the grouping the panel
  exists to express disappeared. It is the mistake the .map-window note
  below describes, made one level further in — a depth system with four
  levels is worth nothing if everything is put on level one.

  Worse, backdrop-filter over a panel that is itself frosted white blurs a
  blur. "A flat fill blurs to the same flat fill and the effect disappears
  entirely" is written twenty lines above about the canvas; it was just as
  true inside every panel, which is why those boxes read as flat rectangles
  wearing a shadow rather than as glass.
*/
.panel,
.cards > * {
  background: var(--glass-1-bg);
  -webkit-backdrop-filter: var(--glass-1-blur);
  backdrop-filter: var(--glass-1-blur);
  box-shadow: var(--glass-1-shadow), var(--glass-edge);
  border: 1px solid rgba(255, 255, 255, 0.5);
}

/* Controls that float over the map. */
/*
  .map-window IS NOT HERE ON PURPOSE. It is a full-width flex wrapper that
  positions the pills over the map, not a control — glassing it painted a
  pale band right across the map, which is a good demonstration of why a
  design layer has to be applied to things that are actually surfaces rather
  than to every class that happens to sit in front of something.
*/
.map-rail-button,
.filters button,
.map-note {
  background: var(--glass-2-bg);
  -webkit-backdrop-filter: var(--glass-2-blur);
  backdrop-filter: var(--glass-2-blur);
  box-shadow: var(--glass-2-shadow), var(--glass-edge-fine);
  border: 1px solid rgba(255, 255, 255, 0.55);
}

/*
  PRESSED IS DEEPER, NOT DARKER. A glass control that is on should look
  pushed INTO the surface — the shadow moves inside and the tint takes the
  brand colour. Darkening it instead reads as "disabled" to most people.
*/
.map-rail-button[aria-pressed='true'],
.filters button[aria-pressed='true'] {
  background: rgba(var(--glass-tint), 0.92);
  color: var(--text-on-brand);
  box-shadow:
    inset 0 2px 6px rgba(16, 34, 38, 0.28),
    inset 0 -1px 0 rgba(255, 255, 255, 0.18),
    0 1px 2px rgba(16, 34, 38, 0.10);
  border-color: rgba(var(--glass-tint), 0.5);
}

/*
  DEPTH ON HOVER, an actual lift rather than a colour change. Two pixels is
  enough to read as movement and small enough not to reflow anything.
*/
.map-rail-button:not([aria-pressed='true']):hover,
.filters button:not([aria-pressed='true']):hover {
  transform: translateY(-1px);
  box-shadow: var(--glass-3-shadow), var(--glass-edge-fine);
}

.map-rail-button,
.filters button,
.panel {
  transition:
    transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
    box-shadow 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
    background 180ms ease;
}

/*
  THE CANVAS BEHIND IT ALL. Glass needs something worth refracting: a flat
  fill blurs to the same flat fill and the effect disappears entirely. Two
  wide, soft brand-tinted pools give the panes something to bend.

  fixed, so the gradients do not slide under a scrolling page and give away
  that they are painted rather than lit.
*/
body {
  background-color: var(--canvas);
  background-image:
    radial-gradient(1100px 700px at 12% -8%, rgba(255, 255, 255, 0.62), transparent 60%),
    radial-gradient(900px 620px at 92% 8%, rgba(var(--glass-tint), 0.28), transparent 62%),
    radial-gradient(760px 520px at 68% 102%, rgba(23, 61, 82, 0.18), transparent 64%);
  background-attachment: fixed;
}

/*
  NO BACKDROP-FILTER, NO PROBLEM. Where it is unsupported the panes fall back
  to a near-opaque surface — the layout is identical and the text is if
  anything easier to read. Nothing here is load-bearing.
*/
@supports not (backdrop-filter: blur(1px)) {
  .panel, .cards > *,
  .map-rail-button, .filters button {
    background: rgba(255, 255, 255, 0.94);
  }
}

/*
  MOTION IS A PREFERENCE. The lift is decoration; the state is carried by
  colour and aria-pressed, so removing the movement loses nothing.
*/
@media (prefers-reduced-motion: reduce) {
  .map-rail-button, .filters button, .panel { transition: none; }
  .map-rail-button:hover, .filters button:hover { transform: none; }
}

/* ============================================================
   LIQUID GLASS, PART TWO — the shell, and a field worth refracting
   ============================================================

   The first pass glassed the controls and left the room they sit in flat.
   Panes over an unlit background read as pale rectangles: the effect needs
   something behind it with structure and colour, and it needs the shell
   itself to be part of the material rather than a frame around it.
*/

/*
  A DEEPER FIELD.

  The canvas was a single pale wash — blur a flat colour and you get the same
  flat colour, so every pane above it had nothing to bend. This lays down
  three brand-tinted pools and a fine grid, which is what gives the glass
  something to distort and the eye something to read depth against.

  THE GRID IS ALMOST INVISIBLE ON PURPOSE — two per cent black at 64px. Below
  a pane it is smeared into a soft texture; beside one it is a whisper. It is
  there to make the blur *legible*, not to be noticed.
*/
body {
  background-color: #93b4bb;
  background-image:
    linear-gradient(rgba(16, 34, 38, 0.020) 1px, transparent 1px),
    linear-gradient(90deg, rgba(16, 34, 38, 0.020) 1px, transparent 1px),
    radial-gradient(1200px 780px at 8% -10%, rgba(255, 255, 255, 0.70), transparent 62%),
    radial-gradient(1000px 700px at 96% 4%, rgba(var(--glass-tint), 0.42), transparent 60%),
    radial-gradient(900px 640px at 62% 108%, rgba(23, 61, 82, 0.34), transparent 62%);
  background-size: 64px 64px, 64px 64px, auto, auto, auto;
  background-attachment: fixed;
}

/*
  THE TOP BAR AS A PANE, not a painted strip. It floats over everything, so
  it takes level 3 — the highest — and the map slides under it.
*/
.topbar {
  background: linear-gradient(
    180deg,
    rgba(var(--glass-tint), 0.86),
    rgba(23, 61, 82, 0.82)
  );
  -webkit-backdrop-filter: var(--glass-3-blur);
  backdrop-filter: var(--glass-3-blur);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    inset 0 -1px 0 rgba(16, 34, 38, 0.18),
    0 10px 30px -10px rgba(16, 34, 38, 0.42);
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}

/* The sidebar is a pane the content sits ON, so it rests at level 1 and lets
   the field show through — a solid column here would cut the room in half. */
.sidebar {
  background: var(--glass-1-bg);
  -webkit-backdrop-filter: var(--glass-1-blur);
  backdrop-filter: var(--glass-1-blur);
  box-shadow: var(--glass-1-shadow), var(--glass-edge);
  border-right: 1px solid rgba(255, 255, 255, 0.5);
}

/*
  ============================================================
  THREE DIMENSIONS, HONESTLY
  ============================================================

  Depth here is not perspective tricks — a fleet console tilted in 3D space
  would be a toy. It is the same thing a real stack of glass does: each level
  is further from the page, so its shadow is longer and softer, its blur is
  stronger, and its edge catches a little more light.

  Four levels, and the rule is that a pane may only sit above what it
  overlaps. The rail floats over the map (2), the map window floats over the
  map (2), a dialog floats over everything (3), and content rests on the
  page (1).
*/

/* Level 3 for anything that covers the page. */
.sheet, .modal, .dialog, .place-form, .beacon-reveal {
  background: var(--glass-3-bg);
  -webkit-backdrop-filter: var(--glass-3-blur);
  backdrop-filter: var(--glass-3-blur);
  box-shadow: var(--glass-3-shadow), var(--glass-edge);
  border: 1px solid rgba(255, 255, 255, 0.6);
}

/*
  THE SPECULAR SWEEP. A real pane catches a band of light across its upper
  face. One gradient, no animation, pinned to the top third — it is what
  stops a large panel reading as a flat translucent slab.

  ::before rather than a background layer so it cannot fight whatever the
  element already paints, and pointer-events none so it never eats a click.
*/
.panel, .sidebar, .sheet, .place-form {
  position: relative;
  isolation: isolate;
}

.panel::before,
.sheet::before,
.place-form::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 38%;
  border-radius: inherit;
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.42),
    rgba(255, 255, 255, 0) 100%
  );
  pointer-events: none;
  z-index: -1;
}

/*
  THE BRAND WASH ON .usage-stat IS GONE. It layered a blue gradient over a
  glass box to stop it reading as "empty chrome" — but the box only read as
  empty because it had been given a surface it should never have had. The
  gradient was already dead code by the time the flat-panels pass below
  reset the same property; the fix is the recession, not a tint over it.
*/

.usage-stat-value {
  color: var(--brand-pressed);
  letter-spacing: -0.02em;
}

/* Cards lift on hover, the same two pixels the controls use — one motion
   vocabulary rather than a different idea per component. */
.cards > *:hover {
  transform: translateY(-2px);
  box-shadow: var(--glass-2-shadow), var(--glass-edge);
}
.cards > * { transition: transform 200ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 200ms cubic-bezier(0.2, 0.8, 0.2, 1); }

@media (prefers-reduced-motion: reduce) {
  .cards > *:hover { transform: none; }
  .cards > * { transition: none; }
}

@supports not (backdrop-filter: blur(1px)) {
  .topbar { background: linear-gradient(180deg, #2f7780, #173d52); }
  .sidebar, .sheet, .modal, .dialog, .place-form { background: rgba(255, 255, 255, 0.95); }
}

/* ============================================================
   THE WRAP, AND THE RING
   ============================================================ */

/*
  THE SHELL WRAPS THE CONTENT AND CASTS ONTO IT.

  The top bar and the sidebar are one continuous surface that the map and the
  panels slide beneath. What sells that is not the glass — it is the shadow
  they throw INWARD, onto the thing they overlap. Without it the two are
  simply adjacent rectangles; with it, one is clearly in front.

  Longer and softer than a normal elevation shadow, because the shell is the
  furthest-forward surface on the page and its shadow has the most distance
  to travel.
*/
.topbar {
  position: relative;
  z-index: 40;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    inset 0 -1px 0 rgba(16, 34, 38, 0.18),
    0 14px 34px -12px rgba(16, 34, 38, 0.45),
    0 2px 6px rgba(16, 34, 38, 0.16);
}

.sidebar {
  position: relative;
  z-index: 30;
  box-shadow:
    var(--glass-edge),
    14px 0 34px -14px rgba(16, 34, 38, 0.38),
    2px 0 6px rgba(16, 34, 38, 0.10);
}

/*
  THE RING AS A GLASS DISC.

  It was a white circle with an arc on it. As glass it becomes a lens sitting
  on the field — the count reads through it, and the arc rides on its
  surface. This is the one piece of the console people look at first, so it
  is worth the extra layer.

  The pale inner glow is what gives a flat disc its curvature: brighter at
  the top left where light would land, falling away to the lower right.
*/
.ring {
  border-radius: 50%;
  background:
    radial-gradient(120% 120% at 28% 22%,
      rgba(255, 255, 255, 0.92),
      rgba(255, 255, 255, 0.55) 46%,
      rgba(var(--glass-tint), 0.16) 100%);
  -webkit-backdrop-filter: var(--glass-2-blur);
  backdrop-filter: var(--glass-2-blur);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.90),
    inset 0 -2px 6px rgba(var(--glass-tint), 0.14),
    0 10px 28px -8px rgba(16, 34, 38, 0.28),
    0 2px 6px -2px rgba(16, 34, 38, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.65);
}

/* The number reads as being under the glass rather than printed on it. */
.ring-value {
  color: var(--brand-pressed);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55);
}

@supports not (backdrop-filter: blur(1px)) {
  .ring { background: #ffffff; }
}

/* ============================================================
   ONE SHELL, PALE, WRAPPING A ROUNDED WELL
   ============================================================

   The bar was a deep brand gradient and the sidebar pale glass — two
   materials meeting at a hard line, which reads as two components rather
   than one surface. Both are now the same pane, so the shell is a single
   L-shaped piece of glass with the content set into it.

   THE RADIUS IS ON THE WELL, NOT ON THE SHELL. Rounding the bar's own
   corners would float it away from the edges of the window and leave slivers
   of background at the top. Rounding the INSIDE corner — where the bar and
   the sidebar meet the content — is what makes the shell look like it wraps
   around something, because that is exactly what a wrapping surface does.
*/

.topbar {
  background: var(--glass-1-bg);
  -webkit-backdrop-filter: var(--glass-1-blur);
  backdrop-filter: var(--glass-1-blur);
  border-bottom: 1px solid rgba(255, 255, 255, 0.55);
  box-shadow:
    var(--glass-edge),
    0 14px 34px -14px rgba(16, 34, 38, 0.30),
    0 2px 6px rgba(16, 34, 38, 0.08);
}

/*
  THE TEXT HAS TO TURN OVER WITH THE SURFACE.

  Every one of these was white for a dark bar. Left alone on a pale pane they
  would be white-on-white — invisible, and the kind of thing that ships
  because the developer never signed out to look at the header they changed.
*/
.brandmark { color: var(--brand); }
.brandmark h1 { color: var(--text); }
#workspace-name { color: var(--text-secondary); }

.nav button { color: var(--text-secondary); }
.nav button:hover { color: var(--brand-pressed); }
.nav button[aria-current="page"] { color: var(--brand-pressed); }
.nav .koru { color: var(--brand); }

.topbar .account-mode { color: var(--text-tertiary); }
.topbar .account-who { color: var(--text); }
.topbar .account-signout { color: var(--text-secondary); }
.topbar .account-signout:hover { color: var(--brand-pressed); }

/* The sign-in button was pale-on-dark; on a pale bar it needs to be the
   brand, or it disappears into the surface it is sitting on. */
.account-signin {
  background: var(--brand);
  color: var(--text-on-brand);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.24),
    0 2px 8px -2px rgba(16, 34, 38, 0.28);
}
.account-signin:hover { background: var(--brand-pressed); }

/*
  THE WELL. The content area's top-left corner is rounded and inset, so the
  shell appears to curve around it. The overflow clip is what makes the map
  actually follow the curve rather than square itself off underneath.
*/
.shell-body > *:last-child {
  border-top-left-radius: var(--radius-xl, 20px);
  overflow: hidden;
  position: relative;
}

/*
  A hairline inside the curve, catching light the way the outer edges of the
  panes do. Without it the well is a hole; with it, it is a rim.
*/
.shell-body > *:last-child::after {
  content: '';
  position: absolute;
  inset: 0;
  border-top-left-radius: inherit;
  box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.55);
  pointer-events: none;
  z-index: 3;
}

@supports not (backdrop-filter: blur(1px)) {
  .topbar { background: rgba(255, 255, 255, 0.95); }
}

/* ============================================================
   FLAT SHELL, ONE PIECE, SHADOW ONLY INTO THE WELL
   ============================================================

   TWO THINGS WERE WRONG AND THEY HAD THE SAME CAUSE.

   The shell was translucent over a field painted with three radial pools, so
   the pools showed straight through it — the bar was pale at one end and
   tinted at the other, which reads as a gradient nobody asked for. Glass over
   a busy background is only good where the background is worth seeing; on
   the chrome it just makes the chrome look uneven.

   And the bar cast a drop shadow across its full width, which landed on the
   sidebar directly below it. Two pieces of the same material with a shadow
   between them cannot read as one surface — the shadow IS the seam.

   SO: THE SHELL IS FLAT AND OPAQUE, and it casts nothing onto itself. The
   only shadow is INSIDE the well, thrown by the shell onto the content it
   wraps. One piece of pale blue with a hole in it.
*/

/* A single flat tone for the whole shell. Not a gradient, not translucent —
   the same value on the bar and the sidebar so the corner where they meet
   has nothing to give it away. */
:root { --shell: #dce8ea; }

/*
  THE HEIGHT OF A BLOCK ON THE VEHICLE CARD, named because two things are
  now required to be the same height: the two blocks under the render, and
  the conduct panel in the column beside them. They line up across a divider
  where nothing makes them line up automatically, so the day one of them
  changes, the other has to change with it or the row visibly breaks.
*/
:root { --tile-h: 236px; }

.topbar,
.sidebar {
  background: var(--shell);
  background-image: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/*
  NO SHADOW ON THE SHELL AT ALL. Not softened, not shortened — removed. A
  shadow on the bar falls on the sidebar; a shadow on the sidebar falls on
  the bar. Either one draws the seam this is trying to erase.
*/
.topbar {
  box-shadow: none;
  border-bottom: none;
}

.sidebar {
  box-shadow: none;
  border-right: none;
}

/*
  THE DEPTH MOVES INSIDE THE WELL. The shell is the front surface, so the
  content is what sits behind it — and the shadow belongs on the thing that
  is further away, cast by the edges above and to the left of it.

  This is also the honest direction: the map really is behind the shell.
*/
.shell-body > *:last-child::after {
  box-shadow:
    inset 2px 2px 5px rgba(16, 34, 38, 0.13),
    inset 8px 8px 22px -8px rgba(16, 34, 38, 0.16);
}

/*
  NO SPECULAR SWEEPS. Each of those was a gradient laid over a panel, and
  they are the other half of the streaky look.
*/
.panel::before,
.sheet::before,
.place-form::before { display: none; }

/* Flat panels. Still lifted by their shadow, no longer washed by a gradient. */
.panel,
.cards > * {
  background: rgba(255, 255, 255, 0.86);
  background-image: none;
}

/* The ring loses its radial fill and keeps its lift. */
.ring {
  background: #ffffff;
  background-image: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow:
    0 10px 26px -10px rgba(16, 34, 38, 0.26),
    0 2px 5px -2px rgba(16, 34, 38, 0.10);
  border: 1px solid rgba(255, 255, 255, 0.9);
}

/*
  THE FIELD STOPS BEING A GRADIENT TOO. The pools existed to give the glass
  something to refract, and the only glass left is over the map — which has
  tiles of its own and needs no help. A flat canvas underneath means nothing
  streaks through anything.
*/
body {
  background-color: var(--shell);
  background-image: none;
}

/* A button that reads as a link: the swap between sign in and sign up is a
   navigation, not an action, and a second solid button beside Cancel would
   compete with the one that matters. */
.linklike {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--brand);
  text-decoration: underline;
  cursor: pointer;
}
.linklike:hover { color: var(--brand-pressed); }

.signin-swap { margin-top: 14px; }

/* The consent row is a checkbox and a sentence, aligned at the top so a
   wrapped sentence does not drag the box down with it. */
.signup-consent {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 4px 0 2px;
  font-size: 13px;
  line-height: 1.45;
}
.signup-consent input { margin-top: 3px; flex: none; }
.signup-consent a { color: var(--brand); }

/* The same line carries a refusal and a "check your email". Green rather
   than red when it is news rather than a fault. */
.signin-error.is-notice { color: var(--status-ok); }

/* ============================================================
   DELIVERIES
   ============================================================

   Two columns: the day's runs on the left, the selected run on the right.
   The run list is narrow on purpose — it is a chooser, not a table — and the
   detail gets the room, because that is where the work happens.
   ============================================================ */

/* ------------------------------------------------ the maximised card */

/*
  FOLLOWING IS A TWO-PANE VIEW.

  The card holds the left edge and the map is inset by its width, so the
  vehicle being followed is centred in what is left. The transition is on
  `left` and `width` because those are what `placeSheet` writes; the card
  travels rather than reappearing somewhere else, which is the difference
  between the map making room and the card jumping.
*/
.sheet.is-following {
  transition:
    left 320ms cubic-bezier(0.4, 0, 0.2, 1),
    top 320ms cubic-bezier(0.4, 0, 0.2, 1);
}

/*
  THE CARD'S OWN SHADE. Everything recessed on this card is the pale blue
  the drive tile already is, rather than the console's grey — see
  --card-surface. The card is one object and its wells should look cut into
  it, which they cannot do in a colour the card does not otherwise use.
*/
.sheet { --card-surface: var(--shell); }

/*
  THE RING KEEPS ITS WHITE DISC, and it is the one thing on this card that
  is deliberately NOT --card-surface.

  It was painted the plate's own colour when the rest went blue, which made
  it vanish — and with it the only thing holding the arcs off the panel.
  `ring.js` calls the disc the face the ring sits ON: white against the
  plate is what makes the koru a raised object rather than three marks
  printed on a rectangle. Its shadow stays for the same reason.
*/

/*
  THE CARD IS THE CARD AT ANY SIZE.

  The first version of this made the render shorter and the tiles squatter
  when maximised, so growing the card rearranged the very thing it was
  meant to show more of. It holds its phone column instead — same 420px,
  same stage, same blocks — and the graphs appear beside it.
*/
/*
  NO `min-height: 0` HERE, deliberately, and it is the other half of the fix
  above. That value lets a flex item shrink below its own contents, which is
  right for a scroller and wrong for this: `.sheet-body` is the scroller,
  and it can only scroll if the column inside it is allowed to be taller
  than it is. With the min at 0 the column shrank to fit, nothing
  overflowed, no scrollbar appeared, and the contents were squeezed instead.

  `min-width: 0` STAYS. Maximised the body is a ROW, and that is the axis
  along which this column must be allowed to be narrow.
*/
.sheet-main {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}

.sheet.is-max .sheet-body {
  flex-direction: row;
  align-items: stretch;
  gap: 8px;
}

/* Fixed, not a fraction: a fraction would widen with the card and take the
   vehicle's spacing with it. */
.sheet.is-max .sheet-main { flex: 0 0 420px; }

.sheet.is-max .sheet-history {
  flex: 1 1 auto;
  min-width: 0;
  overflow-y: auto;
  border-left: 1px solid var(--divider);
}

/*
  THE TWO COLUMNS END ON THE SAME LINES.

  The conduct panel and the two blocks are the bottom of their own columns
  and they were neither starting nor ending together, which across a
  divider reads as one of them having slipped. `margin-top: auto` in a flex
  column pushes the panel down to meet them, the shared height makes the
  bottoms agree, and the history's bottom padding drops to the blocks' own
  16px margin so the last line is one line.

  ONLY WHERE THERE ARE TWO COLUMNS. Stacked, the history is underneath the
  card rather than beside it and there is nothing to line up with — an auto
  margin there would just open a hole above the panel.
*/
@media (min-width: 821px) {
  .sheet.is-max .sheet-history {
    display: flex;
    flex-direction: column;
    padding-bottom: 16px;
  }

  /*
    THE FREE SPACE GOES AROUND THE DISTANCE CHART, not all of it below.

    The auto margin used to sit on the conduct panel alone, which pinned the
    panel to the foot of the column — correct, and it did it by opening a
    single dead band above itself. At a wide pane that band was 135px of
    nothing between the day bars and the speeding ring, which read as a chart
    that had failed to finish rather than as breathing room.

    Moving BOTH auto margins onto the distance chart splits that space evenly
    above and below it, so the chart sits centred between the four stat boxes
    and the speeding panel. The panel stays exactly where it was: the two
    margins consume all the free space between them, and it is the last child,
    so it still lands flush with the bottom of the card beside it.
  */
  .sheet.is-max .sheet-chart:not(.sheet-conduct) { margin: auto 0; }
  .sheet.is-max .conduct-panel { min-height: var(--tile-h); }
}

/* Below that there is no room for two columns, so it goes back to stacking
   and the history sits under the card as before. */
@media (max-width: 820px) {
  .sheet.is-max .sheet-body { flex-direction: column; }
  /* `0 0 auto`, not `1 1 auto`: stacked, the history is a sibling competing
     for the same height, and a shrinkable card gave up its render to make
     room for the charts — the very swap this whole change was about. It
     keeps its own height and the body scrolls, which it already does. */
  .sheet.is-max .sheet-main { flex: 0 0 auto; }
  .sheet.is-max .sheet-history { border-left: 0; overflow-y: visible; }
}

.sheet-history { padding: 0 14px 18px; }

.sheet-history-title {
  margin: 4px 6px 12px;
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

/*
  FOUR ACROSS, NOT THREE AND A STRAY. `auto-fit` at 150px puts three in a
  620px column and drops the fourth onto a row of its own, where it reads as
  a leftover rather than as the last of a set. Four fixed columns hold the
  row together; below that width the stack takes over anyway.
*/
.sheet-history .usage-summary { padding: 0; margin-bottom: 18px; }

@media (min-width: 900px) {
  .sheet.is-max .sheet-history .usage-summary {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 10px;
  }
  .sheet.is-max .sheet-history .usage-stat-value { font-size: 21px; }
}

.sheet-chart { margin: 0; }

/*
  THE CONDUCT RING.

  CAPPED AND CENTRED. `.ring` is `width: 100%` with a square aspect because
  in the rail it IS the column — here the column is six hundred pixels wide
  and an unconstrained ring would be a six-hundred-pixel circle with a
  legend lost beside it.

  AND STRIPPED OF THE DISC. The rail's ring sits on a white plate with a
  border and a shadow, which is right against the sage shell and wrong
  against the card's own white: it would draw a faintly visible square
  behind the arcs. The SVG keeps its own drop shadow, which is what makes
  the arcs sit above the page.
*/
.sheet-conduct { margin-top: 4px; }

/*
  THE PLATE THE RING SITS ON.

  ROUNDED, AND THE SAME MUTED SURFACE the stat boxes use, so it reads as one
  more panel in this column rather than as a card that wandered in from
  another screen.

  RING LEFT, KEY RIGHT. Stacked, the ring left a band of white down both
  sides of itself and the three rows sat under it looking like a separate
  list. Beside each other they are one object — and the key stops being a
  legend you look up and becomes the ring's own labels.

  THE RING DOES NOT FLEX. `flex: 0 0 auto` on a fixed width: a shrinkable
  circle would take the whole panel's width change on itself and the key
  would keep its own, which is the wrong half to move.
*/
.conduct-panel {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 18px 20px;
  margin-top: 8px;
  border-radius: var(--radius-xl);
  background: var(--card-surface);
}

/*
  192, WHICH IS THE PANEL'S CONTENT HEIGHT. The plate is --tile-h tall with
  18px of padding top and bottom, so 200px is all there is; 192 takes it and
  leaves a hair.
*/
.sheet-ring {
  flex: 0 0 auto;
  width: 192px;
  margin: 0;
  background: transparent;
  border: 0;
  box-shadow: none;
}

/*
  THE LABEL WRAPS, AND GROWING THE RING ALONE WOULD NOT HAVE FIXED IT.

  `.ring-label` is sized in `cqw` — a share of the ring's own width — so the
  text grows exactly as fast as the circle does and "speeding or idling"
  overhangs the band at every size. It is a hole problem, not a size
  problem: the label sits BELOW the centre, where a circle is narrower than
  its diameter, and eighteen characters on one line were wider than the
  chord at that height.

  56% is measured against that chord rather than against the ring: at the
  label's offset the hole is about 109px across on a 168px ring, and a cap
  under it is what makes the line break instead of spill. Two short lines
  sit inside the circle where one long one never could.

  ONLY THIS RING. The fleet's says "vehicles" — one word, comfortably
  inside — and a cap there would break it onto two lines for no reason.
*/
.sheet-ring .ring-label {
  max-width: 56%;
  line-height: 1.25;
  text-wrap: balance;
}

.conduct-side {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 12px;
}

.conduct-legend { margin: 0; }

/* Inside the panel now, so it needs no margin of its own — the panel's
   own padding is the only margin it needs. */
.conduct-side .chart-note { margin: 0; }

/* Below this the panel is narrower than a ring and a three-column key can
   sit side by side in, so it stacks and centres. */
@media (max-width: 560px) {
  .conduct-panel { flex-direction: column; }
  .conduct-legend { width: 100%; }
}

/* A note under a chart, in the same voice as a figcaption but below the
   picture: it qualifies what was just drawn rather than naming it. */
.chart-note {
  margin: 10px 2px 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-tertiary);
}
.sheet-chart figcaption {
  font-size: 12px;
  color: var(--text-secondary);
  margin-bottom: 8px;
}

/* ------------------------------------------------- the vehicle sheet tiles */

/*
  TWO BLOCKS, EQUAL WIDTH, one question each. Wide and flat rather than
  cards with their own shadow: they sit inside the sheet, which is already
  a raised surface, and a card on a card is the card soup this stylesheet
  spent a whole pass removing.
*/
/*
  INSET FROM THE CARD'S EDGES. They used to run to the full width of the
  body, which was right when panels stacked below them and the card's own
  padding carried the margin. With the tiles as the last thing on the card,
  a block touching the bottom-left and bottom-right corners reads as
  something cut off rather than something finished.
*/
.sheet-tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin: 14px 14px 16px;
}

/*
  TALLER, and squarer for it. At 14px of padding these were letterbox strips
  under a card the shape of a phone; the reference pair they are modelled on
  are blocks, not bars. The height comes from padding rather than a fixed
  size so a two-line subtitle still fits rather than being clipped by it.
*/
.sheet-tile {
  border-radius: var(--radius-lg);
  padding: 24px 18px 26px;
  /* TWICE THE HEIGHT. 118 was already taller than the strips they started
     as; these are blocks now, and the reading sits low in them with the
     title at the top and air between. */
  min-height: var(--tile-h);
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: var(--surface-muted);
  color: var(--text);
}

/* The reading sits at the bottom of the block, under whatever room the
   title leaves — so the two tiles align on their numbers even when one
   subtitle wraps and the other does not. */
.sheet-tile .sheet-tile-value { margin-top: auto; }

/*
  PAST TRIPS TAKES THE WHOLE ROW. One block, not one of a pair — see
  `pastTripsPanel` for why Next problem stands down while it is open.
*/
.sheet-tiles.is-single { grid-template-columns: 1fr; }

/*
  THE WINDOWS, ALONG THE TOP.

  A ROW, and it fits because the block is now full width: four labels at
  12.5px come to roughly 260px of text, against a card that is 420 at most
  and 392 of usable width at its narrowest. `flex: 1` shares what is left
  equally rather than sizing each to its own word, so the four read as one
  segmented control instead of four buttons of different lengths.

  NOT `.filters`. Borrowing that class brought a pill background, a border
  and a shadow that had to be unset one by one, and the unsetting lost a
  race with `.filters`' own `flex-wrap` — which is what staggered these into
  two ragged rows on a white blob. Its own class owns its own layout.
*/
.sheet-windows {
  display: flex;
  gap: 4px;
  margin: 12px 0 0;
}

.sheet-windows button {
  flex: 1;
  min-width: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--surface);
  padding: 7px 4px;
  font-size: 12.5px;
  text-align: center;
  white-space: nowrap;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease;
}

.sheet-windows button:hover { color: var(--text); }

/* FILLED FOR THE ONE THE MAP IS DRAWING — the same fill the follow dial
   takes when it is on, because this is the same kind of fact: a mode the
   map is in, not an action waiting to be taken. */
.sheet-windows button[aria-pressed="true"] {
  background: var(--brand);
  color: var(--text-on-brand);
}

.sheet-windows button:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/*
  WHICH TRIP — the pop-down under the windows.

  A NATIVE `<select>`, not a drawn one. The list is as long as the window is
  wide and the platform already solves scrolling, keyboard, and opening
  upward when there is no room below; a hand-built menu inside a card that
  itself slides and rescales would have to solve all of that again and would
  be the thing that broke on a short screen.

  `appearance: none` and a drawn chevron, because the stock control carries
  the OS's own border and would be the one square-cornered object on a card
  made of rounded ones.
*/
.sheet-trips {
  margin-top: 10px;
  width: 100%;
  min-width: 0;
  -webkit-appearance: none;
  appearance: none;
  border: 0;
  border-radius: var(--radius-md);
  background-color: var(--surface);
  /* The chevron, as a data URI so the card carries no second request. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6.5 8 10.5 12 6.5' fill='none' stroke='%23516b70' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 16px 16px;
  padding: 9px 32px 9px 12px;
  font: inherit;
  font-size: 13px;
  color: var(--text);
  cursor: pointer;
}

.sheet-trips:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The block is sized by what is in it — windows, picker and a line of
   count — which already exceeds `--tile-h`. */
.sheet-tile.is-past { padding-bottom: 18px; }

/* Under the picker rather than pushed to the bottom of the block: it
   describes the list it sits beneath, and `margin-top: auto` would strand it
   at the foot of a tall card away from what it counts. */
.sheet-tile.is-past .sheet-tile-sub { margin-top: 10px; }

/*
  HOW FAST IT IS GOING, UNDER THE TILE'S TITLE.

  WHITE ON THE TILE'S BLUE, because this is the one LIVE reading on a card
  where everything else summarises the last thirty days. It should look like
  an instrument sitting on the block rather than more of the block's text.

  IT KEEPS ITS HEIGHT WHEN PARKED. A pill that shrank to a word would move
  the number below it every time the vehicle stopped, and the two tiles
  would stop agreeing on where their readings sit.
*/
/* The row itself: the dials and whatever is happening right now. */
.sheet-live {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  min-width: 0;
}

/*
  FOLLOW IS A DIAL, which is why it takes `.sheet-dial` rather than a shape
  of its own: it sits in a row with the speedometer and the limit sign, and
  a pill among circles reads as a different kind of thing that wandered in.
*/
.sheet-follow {
  padding: 0;
  border: 1px solid var(--border);
  color: var(--text);
  cursor: pointer;
  transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
}

.sheet-follow svg { width: 24px; height: 24px; display: block; }
.sheet-follow:hover { border-color: var(--brand); color: var(--brand); }
.sheet-follow:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* FILLED WHILE IT IS ON, because this is a mode rather than an action: the
   map is being held, and a control that looks identical either way leaves
   the reader to work out which by watching the map. */
.sheet-follow.is-on {
  background: var(--brand);
  border-color: transparent;
  color: var(--text-on-brand);
}

/*
  A STATE THAT IS TRUE NOW. Quieter than the Next problem tile on purpose —
  that block is the thing to DO something about, and these two are the
  vehicle's current condition. A second red block of the same weight would
  make the card look like it was reporting two emergencies.
*/
.sheet-watch {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 12px;
  border-radius: var(--radius-pill);
  background: var(--surface);
  border: 1px solid var(--border);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  color: var(--text);
}

/* The colour is on the dot rather than the whole chip: the words stay
   readable and the severity still carries at a glance. */
.sheet-watch-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
}

.sheet-watch.is-idling .sheet-watch-dot { background: var(--warn); }
.sheet-watch.is-quiet .sheet-watch-dot { background: var(--status-urgent); }
/* Speeding is the one of the three with somebody's licence attached to it,
   so it takes the urgent red rather than the warning amber idling wears. */
.sheet-watch.is-speeding .sheet-watch-dot { background: var(--status-urgent); }

/*
  THE SAME TYPE AS EVERYTHING ELSE. It was 11px at weight 800, uppercased
  and tracked out — one family with the rest of the console, but nothing
  else in the product is set that way, so the two words above each number
  read as a different typeface. Sentence case at the body size, told apart
  from the reading below it by weight and a little transparency.
*/
.sheet-tile-title {
  font-size: 12px;
  font-weight: 600;
  opacity: 0.7;
}

.sheet-tile-value {
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -0.02em;
  margin-top: 4px;
  /* A fault kind arrives as "moving unpowered" and must not shove the tile
     beside it out of shape. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
  CAPITALISED ONLY WHERE IT IS A SENTENCE. `text-transform: capitalize` on
  every value turned "12.3 km" into "12.3 Km" and "40 days" into "40 Days" —
  it was added for fault kinds, which arrive as snake_case from the database
  ("moving_unpowered"), and units are the one thing it must not touch.
*/

/*
  IT WRAPS NOW. `nowrap` was right when these were 118px strips and a second
  line would have burst them; at 236px there is room, and truncating
  "Nothing recorded in the last three days" to "…in the l…" wasted a block
  that is mostly air. Two lines, then the ellipsis.
*/
.sheet-tile-sub {
  font-size: 12px;
  margin-top: 4px;
  opacity: 0.78;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

/*
  THE COLOUR IS THE ANSWER, so it is filled rather than tinted — these are
  the two things somebody opened the sheet to read, and a pale wash would
  make them look like the panels below rather than the headline above them.

  WHITE TEXT ON ALL THREE STATES, including amber: --warn is dark enough at
  full strength to carry it, and switching to dark text on one of three
  otherwise identical tiles reads as a rendering fault.
*/
/*
  THE DRIVE TILE IS THE SHELL'S OWN BLUE.

  It has been --brand (which is the SAME HEX as --status-ok, so a healthy
  vehicle showed two identical teal blocks) and then ink, which solved the
  collision by being the only near-black rectangle in the product.

  --shell is the colour the window is already painted, so the block belongs
  to the app rather than announcing itself, and it still cannot be confused
  with the status tile beside it. INK TEXT, not white: at #dce8ea white is
  unreadable, and the ink is what every other pale surface here uses.

  It also keeps the pair honest. Every colour on this sheet carries status
  meaning and distance is not a verdict — a van that drove far did neither
  well nor badly, and a neutral is the only truthful thing to paint it.
*/
.sheet-tile.is-drive { background: var(--shell); color: var(--text); }
.sheet-tile.is-ok { background: var(--status-ok); color: var(--text-on-brand); }
.sheet-tile.is-upcoming { background: var(--warn); color: #ffffff; }
.sheet-tile.is-urgent { background: var(--status-urgent); color: #ffffff; }

/* Stacked on a narrow sheet: two 22px numbers side by side in half a phone
   width truncate to nothing useful. */
@media (max-width: 420px) {
  .sheet-tiles { grid-template-columns: 1fr; }
}

/* -------------------------------------------------------------- settings */

/* A field and the button that commits it are one control, so they share a
   line — a Save that wraps under its input reads as unrelated to it. */
/*
  ============================================================
  SETTINGS
  ============================================================

  A SETTING IS A SENTENCE AND A CONTROL, on one row. The page gave each of
  its two settings a panel of its own, which spent a heading and a card's
  worth of chrome on one field — and made two unrelated-looking screens out
  of the only two things this page does.

  THE WORDS COME FIRST AND THE CONTROL SITS TO THE RIGHT, which is the order
  they are read in: what this is, then what it is set to. On a narrow pane
  the control drops underneath rather than squeezing the sentence, because
  the sentence is the part that cannot be guessed.
*/
.setting-panel { padding: 14px 16px 8px; }
.setting-panel > h3 { padding: 0 0 4px; font-size: 15px; font-weight: 800; }

.setting {
  display: flex;
  align-items: flex-start;
  gap: 24px;
  padding: 14px 0;
  border-top: 1px solid var(--divider);
}
.setting:first-of-type { border-top: 0; }

.setting-head { flex: 1 1 auto; min-width: 0; }
.setting-name { display: block; font-size: 14px; font-weight: 700; }

.setting-note {
  display: block;
  margin-top: 3px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-secondary);
  max-width: 62ch;
}

/*
  THE PRIVACY NOTE IS NOT A CAPTION. It is the one paragraph on this page
  somebody has a legal duty to have read, so it keeps body size and ink
  rather than being shrunk to make the row tidy.
*/
.setting-note.is-weighty { font-size: 13px; color: var(--text); }

/* Narrow on purpose: the sentence on the left is the part that must not be
   squeezed, so the control takes a fixed lane and its label wraps in it. */
.setting-control { flex: 0 0 auto; padding-top: 2px; max-width: 210px; }

@media (max-width: 720px) {
  .setting { flex-direction: column; gap: 10px; }
}

/*
  A SWITCH, NOT A TICKBOX.

  The browser's checkbox is the right ELEMENT and the wrong picture: this
  control turns fleet tracking on, and a 13px system tick beside it reads as
  a preference. Built from the real input so it stays a checkbox to the
  keyboard, to screen readers and to the change handler that saves it —
  `appearance: none` and two boxes are the whole of the difference.
*/
.setting-switch {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  font-size: 13px;
}

.setting-switch input {
  appearance: none;
  -webkit-appearance: none;
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  margin: 0;
}

.setting-switch-track {
  flex: none;
  position: relative;
  width: 46px;
  height: 26px;
  border-radius: var(--radius-pill);
  background: var(--surface-muted);
  border: 1px solid var(--border);
  transition: background 160ms ease, border-color 160ms ease;
}

/* The knob. `left` rather than a transform so the track can keep its border
   without the knob riding over it. */
.setting-switch-track::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: 0 1px 3px rgba(16, 34, 38, 0.28);
  transition: left 160ms ease;
}

.setting-switch input:checked + .setting-switch-track {
  background: var(--brand);
  border-color: transparent;
}
.setting-switch input:checked + .setting-switch-track::after { left: 22px; }

.setting-switch input:focus-visible + .setting-switch-track {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.setting-switch input:disabled + .setting-switch-track { opacity: 0.5; }
.setting-switch:has(input:disabled) { cursor: default; color: var(--text-secondary); }

.setting-switch-label { min-width: 0; line-height: 1.35; }

.setting-row {
  display: flex;
  gap: 10px;
  align-items: stretch;
  padding: 0;
  max-width: 560px;
}

.setting-row input { flex: 1 1 auto; min-width: 0; }
.setting-row .button { flex: 0 0 auto; }

.setting-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 20px 6px;
  font-size: 14px;
  cursor: pointer;
}

.setting-toggle input { flex: 0 0 auto; margin: 0; }

/* Disabled is a statement, not a glitch: the note underneath says who may
   change these, so the control only has to look unavailable rather than
   broken. */
.setting-row input:disabled,
.setting-toggle input:disabled { opacity: 0.55; cursor: not-allowed; }
.setting-toggle:has(input:disabled) { cursor: default; color: var(--text-secondary); }

#view-settings .signin-error { margin: 0 20px 14px; }

/* ---------------------------------------------------------------- awards */

.awards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
  padding: 4px 20px 4px;
}

/*
  RECESSED, LIKE THE STAT BOXES, and for the same reason: these sit inside a
  panel and must read as part of it rather than as four more cards floating
  on top of one. See the note on .usage-stat.
*/
.award {
  background: var(--surface-muted);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  min-width: 0;
}

.award-title {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.award-winner {
  font-size: 17px;
  font-weight: 700;
  margin-top: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.award-value { font-size: 13px; color: var(--brand-pressed); font-weight: 600; }

/* The reason travels with the award — an award nobody can account for is a
   rumour. Small, but present, not hidden behind a hover. */
.award-detail { font-size: 11px; color: var(--text-tertiary); margin-top: 6px; line-height: 1.35; }

/* Quiet, because it is a caption on an occasion rather than a control. */
.award-period {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.awards-empty { padding: 4px 20px 12px; color: var(--text-tertiary); font-size: 13px; }

/* ------------------------------------------------------------- locations */

/*
  THE TWO HEADER ACTIONS SHARE A SLOT and only one is ever shown — see
  showLocationPane. Laid out as a row anyway so that swapping them does not
  move the heading beside them by a pixel.
*/
.locations-actions { display: flex; align-items: center; gap: 10px; }

#location-filters { margin-bottom: 18px; }

/*
  THE PANES ARE PLAIN BLOCKS, not a grid. The fleet page needs named areas
  because a rail, a sticky card row and a pane column share one grid; this
  page is a filter row and one pane under it, and giving it the same
  machinery would be borrowing a solution to a problem it does not have.
*/
.location-pane { display: flex; flex-direction: column; gap: 18px; }

/* The place name keeps the rail's typography so the same place reads the
   same in both places it appears. */
#place-rows .place-title { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
#place-rows .place-name { font-weight: 600; }

/*
  THE TWO LABELS ARE WHITE, AND THEY HAVE TO BE RECOLOURED HERE.

  `.place-home` and `.place-purpose-chip` are rgba(255,255,255,…) because the
  rail they were written for sits on the map's dark glass. Reused on a white
  panel they were painted white on white — present in the markup, read by a
  screen reader, and invisible to everybody else, which is the worst of both.

  Recoloured rather than re-declared: WORKPLACE is the one that changes what
  the fleet does, so it takes the brand; personal is a note and takes the
  tertiary grey.
*/
#place-rows .place-home { color: var(--brand); }
#place-rows .place-purpose-chip { color: var(--text-tertiary); }

/*
  THE NUMBERS AND THE ACTIONS DO NOT WRAP. "180 m" broke across two lines in
  a column wide enough for it, and the four owner actions stacked into a
  block three rows deep — both because the table was being squeezed to fit
  rather than allowed to scroll. .table-scroll already exists for exactly
  this; letting these cells keep their width is what hands it the job.
*/
#place-rows td.num,
#place-rows td:nth-child(6) { white-space: nowrap; }

/* The row acts, so it has to look like it does. */
#place-rows .place-row { cursor: pointer; }
#place-rows .place-row:hover { background: var(--surface-muted); }
#place-rows .place-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

#place-rows td:last-child {
  white-space: nowrap;
  display: flex;
  justify-content: flex-end;
  gap: 2px;
}

#place-rows td:last-child .button-quiet { padding: 4px 8px; }

/* Room for a name plus its chip before the column starts fighting. */
#place-rows td:first-child { min-width: 190px; }

.deliveries-day {
  display: flex;
  align-items: center;
  gap: 10px;
}

.deliveries-day label {
  color: var(--muted);
  font-size: 0.86rem;
}

.deliveries-day input[type='date'] {
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  font: inherit;
}

.deliveries-grid {
  display: grid;
  grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
  gap: 18px;
  align-items: start;
}

/* One column below the width where two would each be too narrow to read. */
@media (max-width: 900px) {
  .deliveries-grid { grid-template-columns: minmax(0, 1fr); }
}

.run-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.run-item > button {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  text-align: left;
  font: inherit;
  cursor: pointer;
}

.run-item > button:hover { background: var(--surface-muted); }

/* The chosen run reads as chosen without moving anything: a brand edge and a
   tint, no size change, so the list does not reflow as you click down it. */
.run-item.is-current > button {
  border-color: var(--brand);
  background: var(--glass-tint);
}

.run-name { font-weight: 600; }
.run-meta { color: var(--muted); font-size: 0.82rem; }

.run-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.run-assign {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.86rem;
  color: var(--muted);
}

.run-assign select,
.run-assign input[type='time'] {
  padding: 6px 9px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  font: inherit;
  color: inherit;
}

.run-summary {
  margin: 6px 0 12px;
  color: var(--muted);
  font-size: 0.88rem;
}

table.stops { width: 100%; border-collapse: collapse; }
table.stops th,
table.stops td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--divider);
  text-align: left;
  vertical-align: top;
}
table.stops th { color: var(--muted); font-weight: 600; font-size: 0.8rem; }

.stop-seq { width: 2.2rem; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Numbers line up under each other or they cannot be compared at a glance. */
table.stops .num,
.stop-eta {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.stop-eta { font-weight: 600; }

.stop-address { display: block; }

/*
  A STOP THAT COULD NOT BE PLACED IS STILL A STOP. It stays in the list,
  greyed with the reason beside it, because deleting somebody's typing
  because a geocoder shrugged is the wrong answer — and a run that silently
  drops the address nobody could find is worse than one that says so.
*/
.stop-unplaced { background: color-mix(in srgb, var(--status-urgent) 6%, transparent); }

.stop-warn {
  display: inline-block;
  margin-top: 2px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--status-urgent) 14%, transparent);
  color: var(--status-urgent);
  font-size: 0.74rem;
}

.stop-actions { white-space: nowrap; text-align: right; }

.stop-move {
  padding: 2px 7px;
  min-width: 1.7rem;
  line-height: 1.3;
  font-size: 0.85rem;
}

.stop-add { margin-top: 14px; display: flex; flex-direction: column; gap: 7px; }

.stop-add label { color: var(--muted); font-size: 0.86rem; }

.stop-add textarea {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  font: inherit;
  resize: vertical;
}

.stop-add-actions { display: flex; align-items: center; gap: 10px; }

/* ============================================================
   THE DATE RANGE PICKER
   ============================================================

   Two fields, a weekday strip, then months as sections down a scroller.
   Proportions follow the design: generous square-ish cells with the number
   in the top-left corner, not centred — a number in the corner leaves the
   rest of the cell free for what a day might later have to carry (a run
   count, a badge), and a centred one does not.
   ============================================================ */

.cal { display: flex; flex-direction: column; gap: 14px; }

.cal-fields { display: flex; align-items: center; gap: 12px; }

.cal-field {
  flex: 1 1 0;
  min-width: 0;
  padding: 13px 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  font: inherit;
  color: inherit;
}

.cal-field::placeholder { color: var(--muted); }
.cal-field:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

.cal-arrow { color: var(--muted); font-size: 1.1rem; }

.cal-week,
.cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 8px;
}

.cal-weekday {
  padding: 7px 0;
  border-radius: var(--radius-md);
  background: var(--surface-muted);
  color: var(--muted);
  text-align: center;
  font-size: 0.9rem;
  font-weight: 600;
}

.cal-months { max-height: 420px; overflow-y: auto; }

/*
  The month name is a section heading inside a scroller, so it sticks: scroll
  into November and it should still say November rather than leaving you to
  count grids upwards.
*/
.cal-month-name {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: 16px 0 8px;
  padding: 6px 0;
  border-bottom: 1px solid var(--divider);
  background: var(--surface);
  color: var(--muted);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.08em;
}
.cal-months > .cal-month-name:first-child { margin-top: 0; }

.cal-day,
.cal-blank {
  aspect-ratio: 1.1;
  border-radius: var(--radius-lg);
}

.cal-day {
  display: block;
  width: 100%;
  padding: 12px;
  border: 1px solid transparent;
  background: var(--surface-muted);
  color: inherit;
  font: inherit;
  font-size: 1.05rem;
  font-weight: 700;
  text-align: left;
  vertical-align: top;
  cursor: pointer;
}

.cal-day:hover { background: color-mix(in srgb, var(--brand) 10%, var(--surface-muted)); }
.cal-day:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* Dimmed, still clickable — see the note in calendar.js. */
.cal-day.is-past { color: color-mix(in srgb, var(--muted) 55%, transparent); }

.cal-day.is-blocked {
  color: color-mix(in srgb, var(--muted) 40%, transparent);
  cursor: default;
}
.cal-day.is-blocked:hover { background: var(--surface-muted); }

/* Today is marked by an underline under the number alone, not a filled cell:
   a fill would compete with the selection, which is the thing that matters. */
.cal-day.is-today { text-decoration: underline; text-underline-offset: 3px; }

.cal-day.is-from,
.cal-day.is-to {
  background: var(--brand);
  border-color: var(--brand);
  color: #fff;
}

.cal-day.is-between {
  background: color-mix(in srgb, var(--brand) 16%, var(--surface-muted));
}

/* The ends of a range square off towards the middle, so a run of days reads
   as one continuous bar rather than seven separate tiles. */
.cal-day.is-from { border-top-right-radius: 6px; border-bottom-right-radius: 6px; }
.cal-day.is-to { border-top-left-radius: 6px; border-bottom-left-radius: 6px; }
.cal-day.is-between { border-radius: 6px; }

/* The left column of the deliveries view: when, then what. */
.deliveries-side { display: flex; flex-direction: column; gap: 18px; }

.run-day {
  margin: 10px 0 2px;
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.run-list > .run-day:first-child { margin-top: 0; }

/* ---- the calendar as a popover ------------------------------------------
   Anchored under the fields so the grid gets the width it was designed at
   rather than whatever the surrounding column happens to spare. */
.cal { position: relative; }

.cal-pop {
  display: none;
  position: fixed;
  z-index: 200;
  overflow-y: auto;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-xl);
  background: var(--surface);
  box-shadow: var(--shadow-raised);
}

.cal-pop.is-open { display: block; }

/* Inside the popover the strip and grid are siblings again, so the weekday
   letters keep sitting over their own columns. */
.cal-pop .cal-week { margin-bottom: 10px; }

/* The Fleet page carries its own actions now that the export button joined it. */
.view-actions { display: flex; align-items: center; gap: 10px; }

/*
  The rule between the fleet list and the numbers about it. A heading alone
  was not enough separation on a page this long — the charts read as more
  table until something drew a line under the table first.
*/
.section-rule {
  margin: 26px 0 14px;
  padding-top: 18px;
  border-top: 1px solid var(--divider);
  color: var(--muted);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}


/* ------------------------------------------------------------ fleet page
   THE GRAPH, AND WHAT IT IS MADE OF.

   The page reads downward: the answer across the top, the vehicles it was
   drawn from underneath. See `renderFleetChart`.
   ------------------------------------------------------------------------ */

/*
  ============================================================
  THE TRIP TRANSPORT — play, pause, speed
  ============================================================

  IT SITS IN THE PAST TRIPS TILE, under the trip it acts on, rather than
  floating at the foot of the map where the old bar was. A video player puts
  its controls at the bottom of the picture because the picture IS the
  subject; here the subject is a trip chosen from a list two lines above, and
  controls that far from it have to be re-found every time.
*/
.trip-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}

.trip-play,
.trip-rate {
  flex: none;
  height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  box-shadow: var(--shadow-card);
}

.trip-play { width: 38px; }
.trip-play svg { width: 18px; height: 18px; display: block; }

/* Wider than tall: it holds "1x" rather than a glyph, and a circle around
   two characters reads as a cramped badge. */
.trip-rate {
  min-width: 48px;
  padding: 0 12px;
  font-size: 13px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.trip-play:hover,
.trip-rate:hover { border-color: var(--brand); color: var(--brand); }

.trip-play:focus-visible,
.trip-rate:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Filled while it is doing the thing — playing, or running at double. The
   same statement the map rail's switches make. */
.trip-play.is-on,
.trip-rate.is-on {
  background: var(--brand);
  border-color: transparent;
  color: #ffffff;
}

/*
  ============================================================
  THE ENTITY CARD — people, teams, places, members, invites
  ============================================================

  ONE SHAPE FOR EVERY LIST OF THINGS. Four pages carried tables of five to
  seven columns where most cells held a single short phrase, which is a table
  used for alignment rather than for comparison. A card puts the name of the
  thing next to the thing and reads in the order somebody asks: who, then
  what about them.

  THE GRID FILLS, IT DOES NOT STRETCH. `auto-fill` rather than `auto-fit`, so
  a page with two drivers on it shows two cards at their proper width instead
  of two cards half a metre wide.
*/
.entity-panel { padding: 14px 16px 16px; }
.entity-panel > h3 { padding: 0 0 12px; font-size: 15px; font-weight: 800; }

.entity-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
}

.entity-card {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  width: 100%;
  padding: 10px 12px;
  border: 0;
  border-radius: var(--radius-lg);
  background: var(--surface-muted);
  text-align: left;
  font: inherit;
  color: var(--text);
}

/* Only the ones that go somewhere look like they do. */
.entity-card.is-live { cursor: pointer; }
.entity-card.is-live:hover { background: var(--surface); box-shadow: var(--shadow-card); }
.entity-card.is-live:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.entity-crest { flex: none; display: block; line-height: 0; }
.entity-crest svg { display: block; width: 52px; height: 52px; }

.entity-body { min-width: 0; flex: 1 1 auto; display: block; }

/*
  EVERY LINE TRUNCATES. A crew of eight names and a driver with four vehicles
  both produce a line longer than any card, and a card that grows to fit one
  of them breaks the row it is in. The whole string is on the card's tooltip.
*/
.entity-title,
.entity-line {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.entity-title { font-size: 14px; font-weight: 700; }
.entity-line { font-size: 12px; margin-top: 1px; }
.entity-line.is-muted { color: var(--text-secondary); }

.entity-aside { flex: none; display: flex; align-items: center; }

/*
  THE SCORE READS AS A GRADE, NOT AS A CHIP. It is the one number on a driver
  card and the only reason to compare two of them, so it is set at a size
  that can be scanned down a column of cards.
*/
.entity-score { display: block; text-align: center; min-width: 46px; }
.entity-score-value { display: block; font-size: 20px; font-weight: 800; line-height: 1.1; }
.entity-score-label { display: block; font-size: 10px; color: var(--text-secondary); }
.entity-score.is-ok .entity-score-value { color: var(--status-ok); }
.entity-score.is-upcoming .entity-score-value { color: var(--status-upcoming); }
.entity-score.is-urgent .entity-score-value { color: var(--status-urgent); }
.entity-score.is-quiet .entity-score-value { color: var(--text-tertiary); font-size: 18px; }

/* The owner's actions on a place card: quiet until wanted, and never the
   card's own click — see the note in `renderPlaceCards`. */
.entity-actions {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}
.entity-actions .button-quiet { padding: 2px 6px; font-size: 11px; }

.entity-empty {
  grid-column: 1 / -1;
  margin: 0;
  padding: 6px 2px;
  font-size: 13px;
  color: var(--text-secondary);
}

/* The plan's note sits under its pills rather than inside a panel. */
.workspace-note { margin: 0 0 20px; }

/*
  A pill whose value is a word, not a count — see `renderPlan`.

  NO `text-transform: capitalize`. It was there to tidy a lowercase tier and
  it Title-Cased every phrase it met: "Sample data" became "Sample Data" and
  "Signed out" became "Signed Out". The values that need a capital get one
  where they are built, which is the only place that knows whether a string
  is a name or a sentence.
*/
.card .figure.is-word {
  font-size: 22px;
  letter-spacing: 0;
  line-height: 1.3;
}

/*
  ============================================================
  THE COMBINED FLEET PAGE FITS ON ONE SCREEN
  ============================================================

  Mileage folded into this page, which means five panels where there were
  three, and the brief was no scrolling. Everything below is that budget:
  the two charts share a line instead of stacking, the panels lose the
  generous padding they wear on pages that scroll, and the figure boxes are
  set at the size a summary deserves rather than the size a headline does.

  The floor is about 900px of window height. Below that the page scrolls,
  which is the right failure — a layout that keeps shrinking to fit ends up
  illegible rather than compact.
*/
.fleet-usage-bar {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 0 0 12px;
  padding: 10px 14px;
}

/* The figures take the room; the range buttons take what they need. */
.fleet-usage-bar .usage-summary {
  flex: 1 1 auto;
  margin: 0;
  gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}

.fleet-usage-bar .usage-stat { padding: 8px 12px; }
.fleet-usage-bar .usage-stat-value { font-size: 20px; }
.fleet-usage-bar .usage-stat-label { font-size: 11px; }
.fleet-usage-bar .filters { flex: none; margin: 0; }

/*
  THIRTY DAYS ON THE LEFT, SIX MONTHS ON THE RIGHT.

  Not an even split, and not a close one: the graph has the ring standing in
  its column as well as thirty columns of bars, so it takes a little over two
  thirds. Six month rows want enough width for a label, a track and a figure
  and nothing beyond that. `minmax(0, …)` on both, because a grid
  track otherwise refuses to go below its content and the chart measures
  itself against a column wider than the page.
*/
/*
  THE TWO PANELS ARE THE SAME HEIGHT, and it is a fixed one.

  `align-items: start` let each be as tall as its contents, so the graph — a
  pixel drawing at a set size — sat short beside seven rows of months and the
  row had a step in it. Stretching alone would not have fixed it: the SVG
  does not grow to its box, it is DRAWN at a size.

  So the row declares the height and both sides take it: the months panel
  fills it, and the chart reads it and draws to it (see `renderFleetChart`).
  One number, stated once, and the two sides cannot disagree.
*/
.fleet-grid {
  --fleet-row-h: 248px;
  display: grid;
  grid-template-columns: minmax(0, 2.1fr) minmax(0, 1fr);
  gap: 12px;
  align-items: stretch;
  margin: 0 0 12px;
}

.fleet-grid > * { margin: 0; height: var(--fleet-row-h); }

/*
  THE FIGURES LINE UP AS A COLUMN.

  `.bar-row` right-aligns them into an 84px cell, which lines up their right
  EDGES and not their digits — proportional numerals put the comma of 6,430
  somewhere else entirely from the comma of 14,210, and a column of figures
  that does not line up is a column you cannot scan. Tabular numerals fix the
  width of every digit, so the thousands sit under the thousands.
*/
.fleet-months .bar-row > :last-child,
.fleet-months .bar-row.is-total > :last-child { font-variant-numeric: tabular-nums; }

/*
  THE TOTAL IS THE SAME ROW, RULED OFF.

  It lands in the same three columns as the months so the figure is directly
  under theirs — which is the only way a sum reads as the sum of what is
  above it. The rule is what stops it reading as a seventh month.
*/
.fleet-months .bar-row.is-total {
  margin-top: 6px;
  padding-top: 7px;
  border-top: 1px solid var(--divider);
  font-weight: 700;
}

/* Six month rows, set to sit level with the chart beside them rather than
   filling a panel built for a page that scrolls. */
.fleet-months { padding: 14px 16px 12px; }
/*
  THE MONTHS PANEL IS WHAT SETS THE ROW'S HEIGHT, not the chart beside it —
  seven rows and a heading outgrew a 190px drawing. Every measurement here is
  therefore a page measurement, which is why the rows are tighter than the
  same component is anywhere else in the console.
*/
.fleet-months > h3 { padding: 0 0 8px; font-size: 13px; color: var(--text-secondary); }
.fleet-months .bars { padding: 0; }
.fleet-months .bar-row { padding: 2px 0; }

.fleet-chart {
  margin: 0 0 12px;
  padding: 12px 16px 6px;
  /* The tooltip is placed against this box, so it has to be the containing
     block for it — see `place` in `bindDayTip`. */
  position: relative;
}

/*
  THE RING AND THE BARS, SIDE BY SIDE.

  The ring is a fixed 168px and the bars take the rest. Not a fraction each:
  the ring is a circle and does not get better with width, while the bars are
  thirty columns that do — every pixel this row gains should go to them.
*/
.fleet-chart-row {
  display: flex;
  align-items: center;
  gap: 20px;
}

.fleet-chart-bars {
  /* `min-width: 0` is what lets the bars SHRINK inside a flex row. Without
     it a flex item refuses to go below its content's width, the row grows
     past the panel, and the chart measures itself against a box wider than
     the page. */
  flex: 1 1 auto;
  min-width: 0;
}

/*
  WHICH MEASURE THE GRAPH IS DRAWING.

  THE SHARE RING STOOD HERE. Two discs beside the caption replace it: there
  are exactly two measures and they are the same kind of control as the dials
  on the vehicle card, which is the language this product already speaks for
  "pick one of these".

  THE LIT ONE IS FILLED, NOT OUTLINED. An outline reads as available; a
  filled disc reads as chosen, and with only two of them that difference has
  to carry the whole state.
*/
/*
  THEY STAND WHERE THE RING STOOD — a column down the left of the graph,
  not a pair of controls tucked under the caption. The ring held that
  position because it was the summary and the bars were the detail; the
  choice of measure holds it now for the same reason. It reads as the thing
  the graph is answering to.
*/
.fleet-modes {
  flex: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  padding-right: 14px;
}

.fleet-mode {
  flex: none;
  width: 52px;
  height: 52px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text-secondary);
  cursor: pointer;
  box-shadow: var(--shadow-card);
}

.fleet-mode svg { width: 24px; height: 24px; display: block; }
.fleet-mode:hover { border-color: var(--brand); color: var(--brand); }
.fleet-mode:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.fleet-mode.is-on {
  background: var(--brand);
  border-color: transparent;
  color: #ffffff;
}

/* The fuel view is rows, not columns, so it takes the room the bars had. */
.fleet-fuel { padding-top: 8px; }
.fleet-fuel .bar-row { padding: 6px 0; }
.fleet-fuel .bar-row > :last-child { font-variant-numeric: tabular-nums; }

/*
  THE COLUMNS FLOW UP — AND THE CHART IS NEVER HOSTAGE TO THE ANIMATION.

  THE OBVIOUS VERSION OF THIS IS A BUG I HAD JUST FINISHED FIXING. A bar
  animating from `scaleY(0)` is at `scaleY(0)` for as long as the animation
  is running, and an animation only progresses when the frame clock ticks. A
  backgrounded tab, a stalled compositor, a page rendered while hidden — and
  the chart is a blank panel with thirty invisible columns in it. That is the
  card crown's disappearing act with different geometry: a resting state
  nobody can see, reachable only by a clock nothing guarantees.

  SO THE ANIMATION IS A CLASS THAT IS TAKEN OFF AGAIN, by a `setTimeout` in
  `renderFleetChart` — and a timeout is not on the frame clock, so it fires
  whatever the compositor is doing. Animation finished: removing the class
  changes nothing. Animation never started: the class comes off anyway and
  the bars are simply there. Full height is the state this chart rests in,
  and the rise is something that happens on the way to it.

  NO `animation-delay` either, for a smaller version of the same reason: a
  stagger by delay needs `fill-mode: backwards` to hold each bar down while
  it waits, which is one more way to be stuck at zero. Every column starts
  together and they finish at staggered times, set per column in
  `renderFleetChart`. The wave reads the same way round.
*/
@keyframes fleet-grow {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

.fleet-chart .day-chart.is-growing .bar-grow {
  /* `fill-box` makes the origin the stack's own bounding box, whose bottom
     is the baseline it should grow from. */
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: fleet-grow var(--grow-ms, 520ms) cubic-bezier(0.2, 0.8, 0.2, 1);
}

.fleet-chart .bar-hit { cursor: default; }

/*
  THE HOVER READOUT. Level 2 glass: it floats over the chart, which is what
  that level is for.
*/
.chart-tip {
  position: absolute;
  z-index: 4;
  /* It sits under the pointer and must never eat the mouseleave that hides
     it, nor block the column next door. */
  pointer-events: none;
  min-width: 104px;
  padding: 8px 10px;
  border-radius: 12px;
  background: var(--glass-2-bg);
  -webkit-backdrop-filter: var(--glass-2-blur);
  backdrop-filter: var(--glass-2-blur);
  box-shadow: var(--glass-2-shadow), var(--glass-edge);
  color: var(--text);
  font-size: 12px;
  line-height: 1.35;
}

.chart-tip-day { font-size: 11px; color: var(--text-secondary); }
.chart-tip-total { font-size: 16px; font-weight: 800; margin-top: 1px; }
.chart-tip-row { display: flex; align-items: center; gap: 6px; margin-top: 5px; }
.chart-tip-name { flex: 1 1 auto; }
.chart-tip-km { font-weight: 700; font-variant-numeric: tabular-nums; }

/*
  A READER WHO HAS ASKED FOR LESS MOTION GETS THE CHART, NOT THE FLOURISH.
  The bars are information; the rise is decoration on top of it.
*/
@media (prefers-reduced-motion: reduce) {
  .fleet-chart .day-chart.is-growing .bar-grow { animation: none; }
}

/* A narrow pane cannot carry both across; the ring goes above the bars
   rather than squeezing thirty columns into half a phone. */
/* A narrow pane cannot carry both across; the months go under the graph
   rather than squeezing thirty columns into half a phone. The row's fixed
   height goes with the row. */
@media (max-width: 920px) {
  .fleet-grid { grid-template-columns: minmax(0, 1fr); }
  .fleet-grid > * { height: auto; }
}

/*
  THE CHART HOLDS ITS HEIGHT.

  `.chart svg` is `width: 100%; height: auto` everywhere else, which turns a
  fixed viewBox into an aspect ratio — right inside the vehicle card's 420px
  column, and 500px tall across a full-width page. This one is drawn 1:1 in
  pixels (see `renderFleetChart`) and says so, so the page keeps room for the
  vehicles underneath it.
*/
/*
  THE DRAWING IS ITS OWN SIZE. `height: auto` here would turn the viewBox
  into an aspect ratio again — the bug this chart was rewritten to avoid —
  so it states the height it was drawn at and nothing scales it.
*/
.fleet-chart .day-chart { width: 100%; display: block; }

/* The bars fill what the caption and the buttons leave. */
.fleet-chart { display: flex; flex-direction: column; }
.fleet-chart .fleet-chart-row { flex: 1 1 auto; min-height: 0; align-items: stretch; }
.fleet-chart .fleet-chart-bars { flex: 1 1 auto; }
.fleet-chart .fleet-chart-bars { display: flex; flex-direction: column; min-height: 0; }
.fleet-chart .bars { flex: 1 1 auto; min-height: 0; }

/* No inset of its own: the panel already provides the margin, and padding
   here is width the drawing is measured against but never gets. */
.fleet-chart .bars { padding: 0; }

/*
  THE FIGURES ACROSS THE TOP ARE A SUMMARY, not the subject. At the shared
  `.card` size four of them filled the screen above the graph they introduce,
  so on this page they are set smaller and squatter.
*/
/*
  THE SPACING IS PART OF THE BUDGET NOW.

  This page has to hold five blocks without scrolling, and it was spending
  86px on air it did not need: 32 above and 56 below the whole view, plus 36
  between the summary pills and what they introduce. A bottom margin that
  size is right on a page you scroll through — it stops the last panel
  sitting on the window edge — and is simply lost height on a page whose
  whole point is that there is nothing below the fold.

  SCOPED TO THIS VIEW. Every other page still scrolls and still wants the
  room.
*/
#view-fleet { padding-top: 18px; padding-bottom: 12px; }

#report-cards { gap: 10px; margin-bottom: 12px; padding-bottom: 0; }

#report-cards .card { padding: 12px 16px; min-height: 0; }
#report-cards .figure { font-size: 26px; }
#report-cards .label { font-size: 11px; }

.fleet-chart figcaption {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 700;
  color: var(--text-secondary);
}

.fleet-chart .chart-legend { margin-top: 14px; }

/*
  THE PICKS SIT IN A ROW, not a column: there are at most three and they are
  peers being compared, which is the same reason the graph stacks them rather
  than drawing three charts.
*/
.fleet-picked {
  display: grid;
  /* 260, not 200: the koru takes 74 of it and the figures beside it should
     not be the thing that wraps. */
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 12px;
}

/*
  THE FOUR MOST URGENT, ALONG THE FOOT.

  A row of four and an arrow. Not a grid of `auto-fit` cards: this row must
  keep its height whatever is in it, because it is the last thing above the
  fold and a second line of cards would put it below one.
*/
.fleet-warnings {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
  gap: 10px;
  margin: 10px 0 0;
}

/* No arrow to make room for when there is nothing behind it. */
.fleet-warnings:not(:has(.fleet-warning-next)) { grid-template-columns: repeat(4, minmax(0, 1fr)); }

.fleet-warnings-clear {
  grid-column: 1 / -1;
  padding: 12px 16px;
  font-size: 13px;
  color: var(--text-secondary);
}
.fleet-warnings-clear p { margin: 0; }

/*
  A CARD THAT IS ALSO A BUTTON. It is the whole rectangle rather than a link
  inside one, because the thing being clicked is the vehicle and the card IS
  the vehicle — a hit area smaller than the mark would be a card that looks
  pressable in places.
*/
.fleet-warning {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 8px 12px 8px 8px;
  border: 0;
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  text-align: left;
  font: inherit;
  color: var(--text);
  cursor: pointer;
}

.fleet-warning:hover { box-shadow: var(--shadow-raised); }
.fleet-warning:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.fleet-warning-koru { flex: none; display: block; line-height: 0; }
.fleet-warning-koru svg { display: block; width: 50px; height: 50px; }

.fleet-warning-body { min-width: 0; display: block; }

/* Every line truncates rather than wrapping: four cards on one row is the
   layout, and a long nickname must not be what breaks it. */
.fleet-warning-who,
.fleet-warning-what,
.fleet-warning-plate {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fleet-warning-who { font-size: 13px; font-weight: 700; }
.fleet-warning-what { font-size: 12px; margin-top: 1px; }
.fleet-warning-plate {
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--text-tertiary);
  margin-top: 1px;
}

/* The fault is the reason the card is here, so it is the coloured line —
   the vehicle's own name stays ink, as it is everywhere else. */
.fleet-warning.is-urgent .fleet-warning-what { color: var(--status-urgent); font-weight: 600; }
.fleet-warning.is-upcoming .fleet-warning-what { color: var(--status-upcoming); font-weight: 600; }

/* Tall as the cards beside it, so the row has one edge. */
.fleet-warning-next {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 0 12px;
  border: 0;
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  color: var(--text-secondary);
  cursor: pointer;
}

.fleet-warning-next:hover { color: var(--brand); box-shadow: var(--shadow-raised); }
.fleet-warning-next:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.fleet-warning-next svg { width: 20px; height: 20px; display: block; }
.fleet-warning-count { font-size: 10px; font-variant-numeric: tabular-nums; white-space: nowrap; }

/*
  DENSER WHEN THERE ARE MORE THAN THREE.

  Six cards wrap to two rows at any comfortable size, and two rows plus the
  warnings strip is more height than a 900px window has. More items in less
  room each is the ordinary answer to that, and it only applies at the
  density that needs it — pick three or fewer and the cards are unchanged.
*/
/*
  THE FIGURE MUST NOT TRUNCATE, AND THAT IS WHAT SIZES THIS.

  A clipped name reads as a name that did not fit. A clipped NUMBER — "10,99…"
  — is a different number, and this card exists to state one. So the crescent,
  the gaps and the right-hand padding are all set at whatever leaves room for
  the widest figure a fleet plausibly produces, and the name takes what is
  left.
*/
.fleet-picked.is-dense { grid-template-columns: repeat(auto-fit, minmax(152px, 1fr)); gap: 10px; }
.fleet-picked.is-dense .fleet-pick { gap: 7px; padding: 8px 18px 8px 9px; }
.fleet-picked.is-dense .fleet-pick-koru svg { width: 40px; height: 40px; }
.fleet-picked.is-dense .fleet-pick-name { font-size: 11px; }
.fleet-picked.is-dense .fleet-pick-value { font-size: 14px; margin-top: 1px; }
.fleet-picked.is-dense .fleet-pick-sub { font-size: 10px; margin-top: 0; }
.fleet-picked.is-dense .fleet-pick-drop { top: 0; right: 2px; font-size: 14px; padding: 3px 4px; }

/*
  THE VEHICLE ON THE LEFT, WHAT IT DID ON THE RIGHT — the map's own card,
  laid out the same way round, so a reader coming from a pin recognises the
  row rather than learning a second arrangement of the same facts.
*/
/*
  IT IS A CARD, SO IT IS ON PAPER.

  It was `--surface-muted` on a page that is very nearly that colour, which
  meant there was no rectangle — just a vehicle and some text floating on the
  background with a rounded corner you had to look for. The house card is
  white with `--shadow-card` under it, the same as the figures along the top
  and every panel on the page, and that is what makes it read as a thing
  sitting ON something rather than a patch of the page.
*/
.fleet-pick {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  /* Room on the right for the cross in the corner, so a long nickname never
     runs underneath it. */
  padding: 8px 32px 8px 12px;
}

/* The crescent is drawn at its own natural size and does not stretch: the
   koru's geometry is a solved ratio and a squashed one is a different mark. */
.fleet-pick-koru { flex: none; display: block; line-height: 0; }
/*
  58, DOWN FROM 74 IN TWO STEPS, AND BOTH WERE THE SAME ARGUMENT.

  This row is the last thing on a page that must not scroll, so its height is
  spent against the warnings strip below it. The crescent sets that height —
  it is taller than the three lines of text beside it — so every pixel off it
  is a pixel off the bottom of the page. At 58 the vehicle inside is still
  the thing you recognise the card by, which is the whole job.
*/
.fleet-pick-koru svg { display: block; width: 58px; height: 58px; }

.fleet-pick-body { flex: 1 1 auto; min-width: 0; }

.fleet-pick-name {
  font-weight: 700;
  font-size: 14px;
  /* A long nickname shortens rather than pushing the cross off the card. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
  THE CROSS LIVES IN THE CORNER OF THE CARD, not at the end of the row.

  Taken out of the flex flow and pinned, so it is in the same place on every
  card whatever the vehicle is called and however tall the row runs — a
  close control that moves about is one the eye has to find each time.

  It stays quiet until reached for: dropping a vehicle is reversible and
  should not compete with the number beside it.
*/
.fleet-pick-drop {
  position: absolute;
  top: 6px;
  right: 8px;
  border: 0;
  background: none;
  /* A 17px glyph is a small target; the padding is the hit area. */
  padding: 4px 6px;
  font-size: 17px;
  line-height: 1;
  color: var(--text-tertiary);
  cursor: pointer;
}
.fleet-pick-drop:hover { color: var(--text); }

.fleet-pick-value {
  margin-top: 2px;
  /* 17, not 20: this row is the last thing above the warnings strip and its
     height is spent against it. The figure is still the biggest thing on the
     card, which is all it needs to be. */
  font-size: 17px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

/*
  ONE LINE EACH, TRUNCATED.

  These wrapped, and in the dense layout that was the difference between a
  62px card and a 111px one — "Toyota Hilux SR5 / HLX773 · 148 h moving" over
  three lines in a 90px column, which then set the height of the whole row.
  A plate that cuts off is readable; a row of cards that has doubled in
  height is what pushes the page past the fold.
*/
.fleet-pick-value,
.fleet-pick-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.fleet-pick-sub { margin-top: 2px; font-size: 12px; color: var(--text-secondary); }

.fleet-pick-hint {
  margin: 0;
  font-size: 13px;
  color: var(--text-secondary);
}

/*
  A PICKED VEHICLE WEARS ITS OWN SHADE in the roster, so the list, the cards
  and the bars all agree without anybody counting positions. `--pick-shade` is
  set per row by `renderRoster`.

  THE WASH, AND NOT A BAR DOWN THE EDGE. It had an inset rule as well, and a
  coloured ledge pinned to the left of a selected list row is the single most
  over-used gesture in a generated interface — it reads as a template rather
  than as this product. The tint alone carries the shade, which is the whole
  job: it is the same colour as the vehicle's bars and its card below.
*/
.roster-row.is-picked {
  background: color-mix(in srgb, var(--pick-shade, var(--brand)) 12%, transparent);
}
