/* =====================================================================
   FleetView — design tokens per design-brief.md. Montserrat everywhere
   (headings, body, tables, KPI numbers) -- see --font-sans below for why
   this is a single-typeface system, not a display/body split. Brand red
   reserved for critical status + the single primary CTA — never a
   generic UI accent.
   ===================================================================== */
:root {
  --brand-red: #ca1212;
  --brand-red-hover: #a80f0f;
  --brand-red-glow: rgba(202,18,18,.28);
  --ink: #15151a;
  --ink-2: #1e1e24;
  --surface: #eef0f2;
  --surface-white: #ffffff;
  --border: #e2e4e8;
  --text-dim: #46464d;
  /* Was #86868f -- computed contrast on white is 3.61:1, below the 4.5:1
     minimum HIG requires for text up to 17pt (this token is used at
     11.5-13px, e.g. .kpi-foot/.page-sub, everywhere on the site). This
     value computes to 5.05:1, comfortably clear, still reads as secondary
     against --text-dim/--ink for primary content. */
  --muted: #6e6e76;
  --ok: #1a7f4f;
  --ok-soft: #e5f3ec;
  --warn: #b5790a;
  --warn-soft: #fbf1dc;
  --critical: var(--brand-red);
  --critical-soft: #fbe3e3;
  --neutral: #9a9a9f;
  --neutral-soft: #ececee;

  /* categorical, fixed order — never cycled, never red */
  --cat-1: #2a78d6;
  --cat-2: #eb6834;
  --cat-3: #1baf7a;
  --cat-4: #8a5fd1;
  --cat-5: #c9a227;
  --cat-6: #4a4a4f;

  /* Marketing's year-over-year revenue lines (marketing.js) — exact palette
     ported from gatlinburg's daily_revenue.py YEAR_COLORS, named here so it
     lives in the same token system as every other chart color instead of
     as raw hex inside marketing.js. Current year solid; prior years use
     color-mix() at the chart's own opacity instead of a separate rgba(),
     so the line and its legend swatch always share one source color. */
  --year-current: #ff8c00;
  --year-prior-1: #39ff6e;
  --year-prior-2: #5599ff;
  /* Same reasoning for the holiday-band overlay (also marketing.js) — one
     token feeds both the translucent band fill (.holiday-band below) and
     the solid legend dot, instead of two independent hardcoded colors. */
  --holiday-band: #ffcc00;

  /* Brand wordmark chips (brandBadge() in app.js) — a completely separate
     token set from status colors above. Never reuse --brand-red/--ok/--warn
     here: those carry meaning (critical / good / caution), these are purely
     identity color for a make with no licensed logo asset on file. Hues
     picked to stay clear of red (~0°), green (~150°) and amber (~40°) so a
     brand chip is never mistaken for a status pill at a glance. All paired
     with white text — dark enough backgrounds for real contrast, not just
     "looks fine". */
  --brand-canam: #5b3aa6;
  --brand-honda: #1a5fa8;
  --brand-kawasaki: #a3195b;
  --brand-polaris: #167a86;
  --brand-yamaha: #9a4a1f;
  --brand-mahindra: #3d4f8a;
  --brand-roxor: #52606d;

  /* Rounder across the board -- part of the Liquid Glass pass (see .sidebar/
     .topbar/.dd-menu/.float-panel below): Apple's modern continuous-corner
     language reads as soft/floating, which a 5-8px radius undercut even
     though the shadows/materials around it were already there. */
  --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 20px;
  --shadow-sm: 0 1px 3px rgba(17,17,17,.05);
  /* 2026-08-24: both tiers came down a notch as part of the same de-bulk
     pass as .card below -- --shadow-md drives .kpi-clickable's hover and
     .dd-menu, --shadow-lg drives .float-panel/modals; both were reading as
     "floating a centimeter off the page" at their old blur/offset. */
  --shadow-md: 0 4px 14px rgba(17,17,17,.06), 0 1px 2px rgba(17,17,17,.04);
  --shadow-lg: 0 14px 32px rgba(17,17,17,.14);
  --ease: cubic-bezier(.4,0,.2,1);

  /* 2026-08-24: one typeface, site-wide. Tried Big Shoulders Display for
     headings, then the brand's own Hiqual for titles/KPI numbers -- direct
     product feedback both times was that a second display face read as
     mismatched against Montserrat everywhere else, not more distinctive.
     Hierarchy (page titles vs KPI numbers vs labels vs body) now comes
     entirely from weight/size/case, same font throughout. */
  --font-sans: 'Montserrat', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  --sidebar-w: 246px;
  --sidebar-collapsed-w: 68px;
}

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
/* 2026-08-05 product ask ("modernize the white/light surfaces, very light
   gradients") — scoped strictly to light chrome; the dark .sidebar is
   explicitly excluded per instruction, untouched below. Kept deliberately
   subtle: a fixed diagonal wash a few percent darker at the corners than
   flat --surface (#eef0f2), not a colored/vignette gradient — this site
   already got called out once this session for reading "too AI" when it
   had loud decorative color, so the bar here is "you have to look for it
   to notice it", not "you notice it immediately". `fixed` attachment (not
   `scroll`) so it reads as one continuous surface behind the whole page
   rather than restarting on every panel as a long page scrolls. */
body {
  font-family: var(--font-sans);
  /* 2026-08-24: reverted the colored radial glows (blue/purple) added
     2026-08-13 -- combined with Liquid Glass's own saturate() boost on the
     chrome layers, two competing colored light sources plus a saturated
     blur read as an artificial "Instagram filter" rather than Apple's
     actual restraint. Apple's own surfaces sit on a quiet, near-neutral
     ground precisely so the ONE deliberate effect (the glass) is what
     provides depth -- not a colorful backdrop for it to filter. Back to a
     plain, quiet diagonal wash, no color. */
  background: linear-gradient(165deg, #fafbfc 0%, var(--surface) 45%, #e4e6e9 100%);
  background-attachment: fixed;
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  /* Never set anywhere on the site -- every block of running text (empty
     states, table cells that wrap, chart captions) was on the browser's
     default ~1.2, noticeably tighter than the comfortable reading measure
     HIG's Typography reference assumes. Tight single-purpose UI text
     (.kpi-value, uppercase labels) already sets its own line-height and is
     unaffected by this. */
  line-height: 1.5;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
select { font-family: inherit; }

.app-shell { display: flex; min-height: 100vh; }

/* ---------- sidebar ---------- */
/* Liquid Glass, "Regular" variant (per HIG: navigation panels are one of
   the named good candidates). Nothing real sits behind the desktop sidebar
   (it's a flex column, not an overlay) -- a strong saturate() boost there
   was blurring literally nothing into an artificial-looking tint instead
   of real depth, so this stays high-opacity with no saturate boost at all
   on desktop; the genuine floating-over-content case is the mobile
   full-screen drawer (768px block below), same background. */
.sidebar {
  width: var(--sidebar-w); flex: 0 0 var(--sidebar-w);
  background: linear-gradient(185deg, rgba(21,21,26,.95) 0%, rgba(14,14,17,.97) 100%);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  color: #f0f0f0;
  display: flex; flex-direction: column;
  position: sticky; top: 0; height: 100vh;
  border-right: 1px solid rgba(255,255,255,.08);
  overflow: hidden;
  transition: width .28s var(--ease), flex-basis .28s var(--ease);
}
/* Collapsed — a slim icon-only rail, not width:0. The toggle lives inside
   the sidebar itself (.sidebar-brand-top, next to the logo) so it's still
   reachable when collapsed — it doesn't live in the topbar. */
.app-shell.sidebar-collapsed .sidebar { width: var(--sidebar-collapsed-w); flex: 0 0 var(--sidebar-collapsed-w); }
/* Fixed width (not flex, which on this column-direction container governs
   height) on each direct child keeps content from reflowing/wrapping as the
   collapse animation shrinks the parent — sidebar-nav keeps its own
   flex:1 (vertical grow) untouched. */
.sidebar > * { width: var(--sidebar-w); min-width: var(--sidebar-w); }
.app-shell.sidebar-collapsed .sidebar > * { width: var(--sidebar-collapsed-w); min-width: var(--sidebar-collapsed-w); }

.sidebar-toggle-btn {
  width: 32px; height: 32px; flex: none; display: flex; align-items: center; justify-content: center;
  position: relative;
  background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.08); border-radius: var(--radius-sm);
  color: rgba(255,255,255,.7); transition: background .15s var(--ease), color .15s var(--ease);
}
.sidebar-toggle-btn:hover { background: rgba(255,255,255,.12); color: #fff; }
/* Icon reflects what clicking it DOES, not the current state's name: expanded
   shows the "collapse panel" glyph (rectangle + arrow tucking in, like a
   door closing), collapsed shows the hamburger (universal "open menu").
   Both icons sit stacked (position:absolute, same slot) and cross-fade via
   opacity instead of one snapping to display:none. */
.sidebar-toggle-btn svg { position: absolute; opacity: 0; transition: opacity .16s var(--ease); }
.sidebar-toggle-btn .icon-close { opacity: 1; }
.app-shell.sidebar-collapsed .sidebar-toggle-btn .icon-menu { opacity: 1; }
.app-shell.sidebar-collapsed .sidebar-toggle-btn .icon-close { opacity: 0; }

.sidebar-brand {
  padding: 18px 16px 18px;
  border-bottom: 1px solid rgba(255,255,255,.07);
}
.sidebar-brand-top {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  transition: gap .2s var(--ease);
}
.brand-word {
  height: 36px; width: auto; display: block; min-width: 0; max-width: 200px; max-height: 36px; overflow: hidden;
  object-fit: contain; object-position: left center;
  opacity: 1; transition: opacity .16s var(--ease), max-width .22s var(--ease), max-height .22s var(--ease);
}
/* Collapsed: no room for a wordmark in the 68px icon rail — fades out same
   as every other rule-9-11 element (opacity+max-width, never display:none).
   2026-08-05 fix: max-height:0 was missing here — .sidebar-brand-top is a
   COLUMN flex container when collapsed, so max-width:0 alone (a row-axis
   constraint) did nothing to this element's height; it kept silently
   occupying its full 36px of vertical space while invisible, which is
   exactly the ~60px dead gap between the toggle button and the brand-mark
   below it the product owner spotted ("demasiado espacio libre entre el
   logo y el ícono de menú"). */
.app-shell.sidebar-collapsed .brand-word { opacity: 0; max-width: 0; max-height: 0; }
/* Icon-only mark — same file as the browser tab favicon (<link rel="icon">
   in index.html), so the collapsed rail's identity matches the tab icon
   instead of going blank. Opposite fade of .brand-word: hidden while
   expanded (the wordmark already carries the brand then), shown only in
   the icon rail. */
/* border-radius added 2026-08-05 (product feedback) -- matches
   .sidebar-toggle-btn's own --radius-sm right above it in the collapsed
   icon rail, instead of sitting there as a sharp square next to it. */
.brand-mark { height: 0; width: 0; opacity: 0; display: block; object-fit: contain; border-radius: var(--radius-sm); transition: opacity .16s var(--ease), height .22s var(--ease), width .22s var(--ease); }
.app-shell.sidebar-collapsed .brand-mark { height: 38px; width: 38px; opacity: 1; }
/* Global store filter, now inside the sidebar. Kept visible even when the
   sidebar is collapsed to its icon rail — unlike a nav link (which just
   points somewhere and can wait for a click to re-expand), this control
   changes what every page currently shows; hiding it behind a re-expand
   step would make the whole app look filtered-to-nothing with no visible
   explanation. That's the same reasoning that already keeps
   .sidebar-toggle-btn itself visible at all widths (rules 9-11) — this is
   the one other control that earns the same exception. */
.sidebar-filter {
  display: flex; align-items: center; gap: 7px; min-width: 0;
  transition: gap .2s var(--ease);
}
.sidebar-filter-icon { flex: none; color: rgba(255,255,255,.45); transition: opacity .14s var(--ease); }

/* Custom dropdown trigger — the button that stands in for the old native
   <select>. Same visual footprint/transitions the <select> had (colors,
   collapse-to-32px behavior) so swapping the component doesn't shift
   anything else in the sidebar. */
.sidebar-filter .dd { min-width: 0; }
.sidebar-filter .dd-trigger {
  display: flex; align-items: center; gap: 6px; min-width: 0;
  border: none; background: transparent; color: rgba(255,255,255,.62); font: inherit; font-weight: 500;
  font-size: 13.5px; padding: 9px 4px 9px 2px; border-radius: var(--radius-sm); outline: none;
  /* 140px -> 190px -> 250px (2026-08-05 product feedback, two rounds: the
     first bump to 190px/160px still clipped the longest real store name,
     "Sevierville Off-Road Park" (25 chars), which measures ~190px at this
     font/weight — needs more than the 160px the label had. This class
     despite its name no longer lives inside the 246px sidebar (--sidebar-w)
     at all -- see index.html, #sidebarFilter is mounted in .topbar's own
     3rd grid column now, which has much more room -- so there's no longer
     a tight parent width forcing a small cap here. 250px is the trigger's
     own budget; the label below gets 220px of it, leaving room for the 6px
     gap + 11px chevron + padding. */
  max-width: 250px;
  transition: max-width .22s var(--ease), padding .2s var(--ease), font-size .16s var(--ease), color .15s var(--ease), background .15s var(--ease);
}
.sidebar-filter .dd-trigger:hover, .sidebar-filter .dd-trigger[aria-expanded="true"] { color: #fff; }
.sidebar-filter .dd-trigger-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; transition: opacity .14s var(--ease), max-width .2s var(--ease); max-width: 220px; opacity: 1; }
.sidebar-filter .dd-trigger-chevron { flex: none; width: 11px; height: 11px; color: #cfd0d6; transition: transform .2s var(--ease); }
.sidebar-filter .dd-trigger[aria-expanded="true"] .dd-trigger-chevron { transform: rotate(180deg); }

.app-shell.sidebar-collapsed .sidebar-brand { padding: 16px 0 16px; }
.app-shell.sidebar-collapsed .sidebar-brand-top { flex-direction: column; gap: 12px; }
/* Collapsed: the funnel icon disappears (redundant with the trigger's own
   chevron) and the trigger itself shrinks to a small square showing only
   that chevron — mirrors the toggle button's own 32x32 icon-only footprint
   (rules 9-11). Label fades via opacity/max-width (not display:none), same
   animated-collapse treatment as every other sidebar label (rule 11). */
/* Scoped to .sidebar specifically (not just .app-shell.sidebar-collapsed
   .sidebar-filter) — the filter also lives in .topbar now, and .app-shell's
   collapsed class is an ancestor of both; without this scoping, collapsing
   the sidebar would incorrectly also shrink the topbar's copy. */
.app-shell.sidebar-collapsed .sidebar .sidebar-filter { justify-content: center; }
.app-shell.sidebar-collapsed .sidebar .sidebar-filter-icon { opacity: 0; width: 0; position: absolute; pointer-events: none; }
.app-shell.sidebar-collapsed .sidebar .sidebar-filter .dd-trigger-label { opacity: 0; max-width: 0; }
.app-shell.sidebar-collapsed .sidebar .sidebar-filter .dd-trigger {
  max-width: 32px; width: 32px; padding: 7px 0; justify-content: center; gap: 0;
}

/* Floating option list for .dd — portaled onto <body> (see mountDropdown()
   in app.js) so it isn't clipped by .sidebar's overflow:hidden.
   2026-08-25 product ask ("los dropdowns no tienen nada que ver con el
   diseño"): this used to be solid var(--ink) (near-black) -- made sense
   back when the sidebar's own Store filter was the only place this
   component appeared, but mountDropdown is now also used for the
   topbar's Period/Fleet Mode filters (see .topbar-filters below), whose
   trigger sits in the topbar's light glass chrome, not the dark sidebar.
   A near-black popup dropping out of a light trigger read as a foreign,
   un-themed control -- exactly the same "belongs to this app's actual
   light chrome, not a dark theme" call already made for .search-results
   right below (see its own comment), so this now matches that light
   card treatment instead of inventing a third style. Portaled onto
   <body>, so it reads as a normal floating overlay from either trigger
   context (dark sidebar or light topbar), same as any light context
   menu popping over a dark surface elsewhere. */
/* 2026-08-24: reverted the glass treatment here -- HIG's own rule is to
   "limit to the most important functional elements" and a small, short-
   lived popover like this is exactly the case that doesn't need it; at
   this size the blur read as an effect rather than a material. Back to
   solid, same as before. */
/* 2026-08-28: squared and de-bulked to match the rest of the app. The
   2026-08-27 pass flattened every content surface (rule J: white fill, one
   hairline, square corners) but never touched the overlays, so menus kept
   12px corners and a wide ambient shadow and read as a different product
   floating over the page -- called out directly: "el diseño de eso no tiene
   nada que ver con el diseño que estamos manejando".
   An overlay still needs to separate from the white content behind it, and
   a hairline alone cannot do that white-on-white -- so it keeps a shadow,
   but a TIGHT contact one rather than the 40px ambient it had. Same
   treatment on all three overlays (.dd-menu, .dim-flyout, .period-panel);
   they are one component family and must not drift apart again. */
.dd-menu {
  position: fixed; z-index: 1000; max-height: 320px; overflow-y: auto;
  background: var(--surface-white);
  border: 1px solid var(--border); border-radius: 0;
  box-shadow: 0 6px 16px -6px rgba(17,17,17,.22); padding: 6px;
  opacity: 0; transform: translateY(-4px) scale(.97); pointer-events: none; transform-origin: top left;
  transition: opacity .16s var(--ease), transform .16s var(--ease);
}
.dd-menu.open { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.dd-option {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%;
  background: none; border: none; text-align: left; color: var(--text-dim); font: inherit;
  font-size: 13px; font-weight: 500; padding: 8px 10px; border-radius: 0;
  transition: background .13s var(--ease), color .13s var(--ease);
}
.dd-option:hover { background: var(--surface); color: var(--ink); }
.dd-option.active { color: var(--ink); font-weight: 600; }
.dd-option-check { width: 13px; height: 13px; flex: none; color: var(--brand-red); opacity: 0; transition: opacity .13s var(--ease); }
/* Small UI-control icons (dropdown chevron/check) had drifted to three
   different stroke widths (2.2/2.4/2.6) added ad hoc as each was built --
   CSS wins over the SVG's own stroke-width attribute, so this consolidates
   them without touching the markup. Content icons (sidebar nav, 1.8) are
   already uniform and left alone -- this is scoped to controls only. */
.dd-trigger-chevron, .dd-option-check, .nav-group-chevron { stroke-width: 2.2; }
.dd-option.active .dd-option-check { opacity: 1; }
/* Expanded: filter sits first (left), toggle second (right) — filter is in
   the logo's old slot. Collapsed: flex-direction switches to column (rule
   above) and this keeps the toggle stacked on top, filter below, regardless
   of DOM order. */
.app-shell.sidebar-collapsed .sidebar-toggle-btn { order: -1; }

/* min-height:0 + overflow-y:auto, not just flex:1: a flex child's default
   min-height is its content, so without this a tall nav pushes the context
   block and the account row off the bottom of a short viewport instead of
   scrolling itself. Only became reachable once fixed blocks were added
   below the nav. */
.sidebar-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 12px; display: flex; flex-direction: column; gap: 3px; transition: padding .22s var(--ease); }
.nav-group { display: flex; flex-direction: column; }
.nav-group-label {
  display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; margin: 0;
  font-family: inherit; background: none; border: none; cursor: pointer; text-align: left;
  font-size: 10.5px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
  color: rgba(255,255,255,.32); padding: 10px 12px 6px; max-height: 30px; opacity: 1; overflow: hidden;
  transition: opacity .14s var(--ease), max-height .2s var(--ease), padding .2s var(--ease), color .15s var(--ease);
}
.nav-group-label:hover { color: rgba(255,255,255,.55); }
.nav-group-chevron { width: 12px; height: 12px; flex: none; opacity: .6; transition: transform .2s var(--ease); }
.nav-group.nav-group-collapsed .nav-group-chevron { transform: rotate(-90deg); }
/* Links inside a group animate open/closed the same way the rest of the
   sidebar animates (max-height + opacity, never display:none) — collapsing
   a group you don't use just clips its link list to nothing. */
.nav-group-links {
  display: flex; flex-direction: column; gap: 3px; overflow: hidden;
  max-height: 400px; opacity: 1;
  transition: max-height .22s var(--ease), opacity .16s var(--ease);
}
.nav-group.nav-group-collapsed .nav-group-links { max-height: 0; opacity: 0; }
/* The per-group chevron only makes sense with the sidebar expanded — in
   icon-only mode there's no room for it and the label itself is already
   hidden (rule below), so force every group's links back open so the icon
   rail never goes missing just because a group was collapsed earlier. */
.app-shell.sidebar-collapsed .nav-group.nav-group-collapsed .nav-group-links { max-height: 400px; opacity: 1; }
/* =====================================================================
   The dimension switcher (2026-08-27)
   =====================================================================
   "en vez de mostrar filtros y todas esas cosas, dimensiona la app." This
   replaced the topbar's Fleet Mode dropdown. It is styled as a distinct
   block from the nav below it on purpose: the nav answers "which page",
   this answers "which operation", and the second scopes the first. Hence
   its own surface (an inset panel rather than transparent rows) and the
   divider under it -- a user should never mistake a dimension for a
   fourteenth nav link.
   Vertical, not a horizontal segmented control: measured at the sidebar's
   246px, "Off-Road Park" alone needs ~90px, so three across would leave
   ~60px each and force truncation to "Off-R…". */
/* The context block: which dimension, and which location inside it. It
   sits between the nav and the account row, divided from the nav by a
   hairline and by whatever vertical slack the nav leaves (.sidebar-nav is
   flex:1, so this is genuinely pinned to the bottom, not merely last).
   That separation is the whole point -- above the nav these three rows
   were 14px from the first nav link and read as more navigation. Hairline
   and flat fill, not a raised panel: rule E/J, nothing in this app floats. */
.sidebar-context {
  padding: 12px 12px 10px;
  border-top: 1px solid rgba(255,255,255,.07);
  transition: padding .22s var(--ease);
}
.dim-switcher { display: flex; flex-direction: column; gap: 2px; }
/* The store dropdown, back in the sidebar under the switcher it narrows.
   Indented to the same 11px the dimension rows use so label text in both
   starts on one edge, and given a hairline above it so "which dimension"
   and "which location within it" read as two steps of one question rather
   than four items in a list. */
.sidebar-context .sidebar-filter {
  margin-top: 8px; padding-top: 8px; padding-left: 11px;
  border-top: 1px solid rgba(255,255,255,.06);
  /* 10px, not the 7px this component uses in the topbar. Measured: the two
     rows' icons already share a left edge at x=23, but a dimension row is
     16px icon + 10px gap (label at 49) while this one is a 14px icon + the
     trigger's own 2px padding, landing its label at 46. A 3px stagger
     between two stacked labels is visible and reads as sloppy, so the gap
     absorbs the icons' 2px size difference instead. */
  gap: 10px;
}
.dim-option {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 11px; border: none; border-radius: var(--radius-sm);
  background: transparent; color: rgba(255,255,255,.62);
  font: inherit; font-size: 12.5px; font-weight: 600; text-align: left;
  cursor: pointer; white-space: nowrap; overflow: hidden;
  transition: background .15s var(--ease), color .15s var(--ease);
}
.dim-option:hover { background: rgba(255,255,255,.05); color: #fff; }
/* The active dimension gets a filled surface, not the nav's left-edge red
   bar. The bar means "you are on this page"; a filled pill means "this is
   the set you are looking at" -- two different kinds of current, so they
   should not share one visual. */
.dim-option.active {
  background: rgba(255,255,255,.10); color: #fff;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.12);
}
.dim-option:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }
.dim-icon { width: 16px; height: 16px; flex: none; opacity: .9; }
.dim-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; transition: opacity .2s var(--ease), max-width .2s var(--ease); }
.dim-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The chosen store, on its own dimension's row. :empty collapses it so a
   dimension viewed whole keeps a single-line row -- the second line appears
   only when there is actually something narrower to report. */
.dim-store {
  font-size: 10.5px; font-weight: 500; color: rgba(255,255,255,.45);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dim-store:empty { display: none; }
.dim-option.active .dim-store { color: rgba(255,255,255,.62); }
/* The "there is more to the right" affordance. Hidden until the row is
   hovered or focused so three permanent chevrons don't sit in the rail
   competing with the counts. */
.dim-more {
  width: 13px; height: 13px; flex: none; margin-left: 2px;
  color: rgba(255,255,255,.5); opacity: 0;
  transition: opacity .15s var(--ease);
}
.dim-option:hover .dim-more, .dim-option:focus-visible .dim-more,
.dim-option[aria-expanded="true"] .dim-more { opacity: 1; }
/* Unit count, so the split is legible where the choice is made. Tabular so
   the three numbers line up as a column rather than drifting by digit
   width. */
.dim-count {
  flex: none; font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
  color: rgba(255,255,255,.42); transition: opacity .2s var(--ease), max-width .2s var(--ease);
}
.dim-option.active .dim-count { color: rgba(255,255,255,.7); }
/* Collapsed rail: same treatment the nav links get -- label and count fade
   to zero width rather than display:none, so the collapse animates as one
   motion (CLAUDE.md rule 11). The icon is what remains, and each dimension
   has a distinct one. */
.app-shell.sidebar-collapsed .sidebar-context { padding: 10px 8px 8px; }
.app-shell.sidebar-collapsed .sidebar-context .sidebar-filter { padding-left: 0; justify-content: center; }
.app-shell.sidebar-collapsed .dim-option { justify-content: center; gap: 0; padding: 9px 0; }
.app-shell.sidebar-collapsed .dim-text,
.app-shell.sidebar-collapsed .dim-count { opacity: 0; max-width: 0; }
.app-shell.sidebar-collapsed .dim-more { display: none; }
.app-shell.sidebar-collapsed .dim-icon { width: 18px; height: 18px; }

.sidebar-nav a {
  display: flex; align-items: center; gap: 12px; position: relative;
  padding: 11px 14px; border-radius: var(--radius-sm);
  font-size: 13px; font-weight: 500; color: rgba(255,255,255,.62);
  transition: background .15s var(--ease), color .15s var(--ease), justify-content .2s var(--ease), padding .2s var(--ease), gap .2s var(--ease);
}
.sidebar-nav a:hover { background: rgba(255,255,255,.05); color: #fff; }
.sidebar-nav a.active { background: linear-gradient(90deg, var(--brand-red-glow), rgba(202,18,18,.03)); color: #fff; font-weight: 600; }
.sidebar-nav a.active::before {
  content: ''; position: absolute; left: -12px; top: 50%; translate: 0 -50%;
  width: 3px; height: 20px; border-radius: 0 3px 3px 0; background: var(--brand-red);
  opacity: 1; transition: opacity .12s var(--ease);
}
.sidebar-nav .icon { width: 18px; height: 18px; flex: none; opacity: .95; transition: width .2s var(--ease), height .2s var(--ease); }
/* Collapsed: icon rail has room to spare with the label gone, so the icons
   themselves grow a bit rather than sitting at the same size they had next
   to text (product ask, 2026-08-25). */
.app-shell.sidebar-collapsed .sidebar-nav .icon { width: 20px; height: 20px; }
.sidebar-nav a .label {
  opacity: 1; max-width: 160px; overflow: hidden; white-space: nowrap;
  transition: opacity .14s var(--ease), max-width .2s var(--ease);
}
/* Collapsed nav — icon only, centered; label fades + shrinks to 0 width
   instead of snapping to display:none (still in the DOM either way, and
   each link keeps a native `title` tooltip for collapsed hover). The active
   bar fades out rather than repositioning — at this width it'd sit right
   against .sidebar's clipped edge and either vanish or look glued on. */
.app-shell.sidebar-collapsed .nav-group-label { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; }
.app-shell.sidebar-collapsed .sidebar-nav { padding: 18px 10px; }
.app-shell.sidebar-collapsed .sidebar-nav a { justify-content: center; padding: 12px 0; gap: 0; }
.app-shell.sidebar-collapsed .sidebar-nav a .label { opacity: 0; max-width: 0; }
.app-shell.sidebar-collapsed .sidebar-nav a.active::before { opacity: 0; }

/* Profile row — avatar + name + status, pinned at the very bottom of the
   sidebar. Whoever's actually signed in (see server.js's Google OAuth gate
   + GET /api/me) — real photo, real name, real email, not a placeholder.
   The <img> defaults to the Unseen Pass mark until /api/me resolves
   client-side (see app.js), which is why it's `object-fit:cover` (works for
   both a square logo and a circular Google photo, not just one). */
.sidebar-foot {
  padding: 16px 22px; display: flex; align-items: center; gap: 11px;
  border-top: 1px solid rgba(255,255,255,.07);
  transition: padding .22s var(--ease), justify-content .22s var(--ease), gap .22s var(--ease);
}
.sidebar-foot-avatar {
  width: 32px; height: 32px; border-radius: 50%; flex: none; overflow: hidden;
  background: #fff;
}
.sidebar-foot-avatar img { width: 100%; height: 100%; object-fit: cover; }
.sidebar-foot-text {
  min-width: 0; flex: 1; max-width: 160px; opacity: 1; overflow: hidden;
  transition: opacity .14s var(--ease), max-width .2s var(--ease);
}
.sidebar-foot-title { font-size: 13px; font-weight: 700; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sidebar-foot-sub {
  margin-top: 2px; font-size: 10.5px; color: rgba(255,255,255,.4);
  display: flex; align-items: center; gap: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sidebar-foot-logout {
  flex: none; width: 26px; height: 26px; border-radius: var(--radius-sm);
  display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,.35);
  opacity: 1; transition: background .15s var(--ease), color .15s var(--ease), opacity .14s var(--ease), width .2s var(--ease), margin .2s var(--ease);
}
.sidebar-foot-logout:hover { background: rgba(255,255,255,.08); color: #fff; }
.sidebar-foot-sub .pulse-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); flex: none; box-shadow: 0 0 0 3px rgba(26,127,79,.25); }
/* gap:0 here is required, not cosmetic: flexbox `gap` inserts space
   between EVERY pair of children regardless of their own width, so even
   with .sidebar-foot-text/-logout both animated to 0 width below, the
   base 11px gap either side of them was still reserved -- pushing the
   visible avatar ~11px left of true center (2026-08-05 product feedback:
   "la foto de perfil debe estar centrada"; same class of bug as the
   collapsed brand-word one above, just gap instead of height). */
.app-shell.sidebar-collapsed .sidebar-foot { padding: 14px 0; justify-content: center; gap: 0; }
.app-shell.sidebar-collapsed .sidebar-foot-text { opacity: 0; max-width: 0; }
.app-shell.sidebar-collapsed .sidebar-foot-logout { opacity: 0; width: 0; margin: 0; }

/* ---------- topbar ---------- */
/* Logo (left) + search (true center) + an empty balancing slot (right).
   A plain flex row with justify-content:center on the search would center
   it against whatever space is left AFTER the logo — visually off-center,
   pulled right, against the full width of the bar. A 3-column grid where
   the outer columns are equal `1fr`s (not auto-sized to their content)
   fixes that: both sides claim identical leftover space regardless of the
   logo's actual width, so the middle column sits on the true horizontal
   center of .topbar, not just the center of "everything after the logo". */
/* Height history: 58px -> 46px (2026-08-04, felt too tall on every page) ->
   51px (2026-08-04, product feedback the other way: 46px felt cramped once
   seen next to the rest of the site). 51px is a deliberate middle ground,
   not a reversion to 58px — still noticeably tighter than the original, but
   with a bit more breathing room above/below the search box and the
   store-filter dd-trigger than 46px allowed. This is shared chrome (every
   page sits under it: Dashboard/Fleet/GPS/Financials/Bookings/Marketing/
   Vehicle Detail) and nothing hardcodes its height elsewhere — gps.js reads
   it live via `topbar.offsetHeight` for its full-bleed map sizing, so this
   number is safe to tune without hunting for other places to update. */
/* Liquid Glass, light/"Regular" variant -- this is the one surface on the
   site where content genuinely scrolls underneath a sticky functional
   element (every page below GPS), so the blur has real content to pick up
   colors from as things scroll past, not just a static backdrop. */
.topbar {
  height: 51px; background: linear-gradient(180deg, rgba(255,255,255,.82) 0%, rgba(242,244,246,.85) 100%);
  backdrop-filter: blur(20px) saturate(1.1);
  -webkit-backdrop-filter: blur(20px) saturate(1.1);
  border-bottom: 1px solid var(--border);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 18px;
  padding: 0 28px; position: sticky; top: 0; z-index: 20;
}
.topbar-brand { justify-self: start; display: flex; align-items: center; min-width: 0; }
.topbar-search-wrap { justify-self: center; display: flex; justify-content: center; width: 100%; position: relative; }
/* Live search-match preview (Miro spec: "el buscador le falta el preview de
   coincidencias") — anchored under .topbar-search-wrap (centered, same
   width as the input) rather than the input itself, so it doesn't need its
   own relative-position wrapper. Light card styling (not .dd-menu's dark
   theme) since it visually belongs to the topbar's light chrome, not the
   sidebar's dark one. */
/* Same reasoning as .dd-menu above -- reverted to solid for a small,
   transient popover. */
.search-results {
  position: absolute; top: calc(100% + 8px); width: 500px; max-height: 340px; overflow-y: auto;
  background: var(--surface-white);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg); padding: 6px; z-index: 30;
  opacity: 0; transform: translateY(-4px); pointer-events: none; transition: opacity .14s var(--ease), transform .14s var(--ease);
}
.search-results.open { opacity: 1; transform: translateY(0); pointer-events: auto; }
.search-result {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%;
  background: none; border: none; text-align: left; color: var(--ink); font: inherit;
  font-size: 13px; padding: 9px 10px; border-radius: var(--radius-sm); cursor: pointer;
}
.search-result:hover, .search-result.is-active { background: var(--surface); }
.search-result-id { font-weight: 700; }
.search-result-model { color: var(--muted); font-size: 12px; margin-left: 8px; }
.search-result-meta { color: var(--muted); font-size: 11.5px; flex: none; }
.search-results-empty { padding: 10px; font-size: 12.5px; color: var(--muted); }
/* The global fleet-mode filter (primary — ATV Park/UTV Park/Street) and the
   store filter (secondary, Street-only — see updateStoreFilterVisibility()
   in app.js) sit in this third grid column (previously an empty balancing
   spacer) — both reuse the exact .sidebar-filter/.dd markup and component,
   just anchored to the topbar's right edge instead of the sidebar's
   top-left. Dark-on-light colors below override the sidebar's dark-on-dark
   defaults for this one context. */
.topbar-filters { justify-self: end; display: flex; align-items: center; gap: 20px; min-width: 0; }
/* The store dropdown only exists at all under Street mode — an instant
   appear/disappear driven by the mode dropdown's value, not a continuous
   collapse to animate (unlike the sidebar-collapse opacity/max-width
   treatment elsewhere), so a plain display:none is the right tool here. */
.topbar-filters .sidebar-filter.is-hidden { display: none; }
/* The Fleet Mode slot on the two dashboards, where the scope is fixed by
   the page and there is nothing to choose (see applyModeFilterLock in
   app.js). Deliberately styled as the dropdown's own resting label minus
   the affordances -- same size, weight and colour, no chevron, no hover,
   no pointer -- so the header row keeps its shape and the slot reads as
   information rather than as a control someone disabled. */
.filter-locked {
  display: inline-flex; align-items: center; min-width: 0;
  font-size: 13.5px; font-weight: 500; color: var(--muted);
  padding: 9px 4px 9px 2px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* `display` here is a class selector and so outranks the UA stylesheet's
   own `[hidden] { display: none }` -- without this the chip stayed visible
   on every page after leaving a dashboard, sitting next to the real
   dropdown so the header showed a fleet filter twice. */
.filter-locked[hidden] { display: none; }
.topbar.hero-bg .filter-locked { color: rgba(255,255,255,.92); }
.topbar .sidebar-filter-icon { color: var(--muted); }
.topbar .dd-trigger { color: var(--text-dim); }
.topbar .dd-trigger:hover, .topbar .dd-trigger[aria-expanded="true"] { color: var(--ink); }
.topbar-search {
  width: 500px; display: flex; align-items: center; gap: 10px;
  /* Vertical padding scaled up proportionally with the topbar's 46px -> 51px
     bump above (6px -> 7px, same ~+11% the bar itself grew) so the search
     box's own height grows in step and it keeps reading as centered/at-home
     in the taller bar, rather than the same small pill floating in more
     empty space around it. */
  border-bottom: 1.5px solid var(--border); padding: 7px 4px; color: var(--muted); font-size: 14px;
  transition: border-color .15s var(--ease);
}
.topbar-search:focus-within { border-color: var(--brand-red); }
/* 2026-08-24: a fixed 500px search box was already tight in the
   769-1100px tablet range (Period + Fleet Mode + Location filters on
   the right, mobile-menu-btn hidden on the left with nothing to fill
   that column) -- adding the breadcrumb into that same left column
   surfaced it as an actual overlap instead of just "tight." Root-caused
   to the search box itself never having a mid-range size step (only
   500px on desktop, 100% below the mobile breakpoint), so it's the one
   that shrinks here instead of constraining every side column to guess
   around it. Same reasoning applies to .topbar-filters on the other
   side: `justify-self: end` sizes it to its own content's width, not
   its 1fr track, so its 3 dropdowns (each up to 220px per
   .dd-trigger-label's own max-width, shared with the sidebar's dark-
   theme instance of this same class) could overflow leftward into the
   search box exactly like .topbar-brand did on the left -- `stretch` +
   a tighter label cap here fixes it the same way. */
@media (min-width: 769px) and (max-width: 1100px) {
  .topbar-search { width: 260px; }
  .topbar-filters { justify-self: stretch; }
  .topbar-filters .dd-trigger-label { max-width: 70px; }
}
.topbar-search input { border: none; background: none; outline: none; font: inherit; color: var(--ink); flex: 1; }
.btn {
  display: inline-flex; align-items: center; gap: 6px; border: none; border-radius: var(--radius-sm);
  padding: 9px 16px; font-size: 13px; font-weight: 600;
}
.btn-primary { background: var(--brand-red); color: #fff; box-shadow: 0 4px 14px var(--brand-red-glow); }
.btn-primary:hover { background: var(--brand-red-hover); }
.btn-ghost { background: var(--surface); color: var(--ink); border: 1px solid var(--border); }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* 28/32/60 were three unrelated numbers -- 32/32/64 keeps the same visual
   weight but lands on an 8px rhythm the rest of the page can actually
   share (.section below, .card padding, .grid gap already did). */
/* 2026-08-24 "ultra responsive" product ask: previously no max-width at
   all (see git history), so grid-3/grid-4 KPI rows on an ultra-wide
   monitor (2560px+) stretched into cards ~800px wide, mostly dead space
   around a small number -- the opposite of responsive. 1600px caps that
   without affecting anything below it (a 1440px laptop never hits the
   cap). .page.page-wide (GPS/Fleet's full-bleed map) already resets this
   to `none` below, so the one page that deliberately wants edge-to-edge
   width is unaffected. */
/* No max-width, no centring -- CLAUDE.md rule 3b ("`.page` no tiene
   max-width/centrado -- usa el 100% del ancho disponible"). A later
   "ultra responsive" pass had capped this at 1600px with `margin: 0 auto`,
   which quietly broke that rule and, on any viewport wider than
   1600 + sidebar, pushed every page's content right of the dashboard hero
   sitting above it (the hero renders into #heroSlot, a SIBLING of .page,
   so it never picked up the centring margin). At 1920 that was a visible
   37px step: (1674 - 1600) / 2. Removed rather than mirrored onto the
   hero -- product direction was to move the content out to the hero, and
   that is also what rule 3b already said. The tile grid absorbs the extra
   width by fitting more columns, which is design rule A. */
.page { padding: 32px 32px 64px; width: 100%; }

/* Full-bleed layout modifier — a page opts in (see fleet.js) when its main
   content (the live map) should own the whole content column edge-to-edge
   instead of sitting in the standard padded/max-width box. Reset to plain
   `.page` on every route change (see app.js render()) so it never leaks to
   other pages. */
/* Height comes from JS (fitPageHeight in fleet.js) pinning this element to
   exactly (viewport - topbar) — a vh-minus-guessed-pixels CSS value drifted
   out of sync with the breadcrumb's real rendered height and left a blank
   gap under the map. flex-direction:column + overflow:hidden here, and
   flex:1/min-height:0 all the way down (.map-full-bleed → .map-shell →
   .map-body → #map), makes the map the thing that actually absorbs
   whatever height is left — never too tall, never short. */
.page.page-wide {
  max-width: none; padding: 0; display: flex; flex-direction: column; overflow: hidden;
}
/* Site-wide breadcrumb (the <nav class="breadcrumb"> markup itself, from
   app.js's breadcrumb()) now renders inline inside the topbar
   (#topbarBreadcrumb, its own rule further down) on every screen size --
   this used to be a separate full-width .breadcrumb-bar row, its own
   Liquid Glass surface sticky-pinned below the topbar, removed
   2026-08-24 as part of the mobile header reorganization (a
   disconnected third bar was part of what read as "caotico"). */

.map-full-bleed { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* The map IS the page here — no margin, nothing below it to make room for. */
.map-full-bleed .map-shell { border-radius: 0; border-left: none; border-right: none; flex: 1; min-height: 0; display: flex; flex-direction: column; }
.map-full-bleed .map-body { flex: 1; min-height: 0; position: relative; }

h1, h2, h3 { font-family: var(--font-sans); font-weight: 800; margin: 0; letter-spacing: .2px; }
/* 2026-08-24 product ask: every page's biggest title (this is always the
   one .page-title <h1> per page -- Dashboard/Financials/Fleet/etc. all
   share this exact rule already, so "make every title the same size"
   was already true structurally; this just adds the uppercase transform)
   goes uppercase. Tracking flipped from -.4px to +.2px going uppercase --
   negative tracking (right for mixed-case display type, tightening the
   ascender/descender gaps) reads as cramped on all-caps text, which has
   no ascenders/descenders to tighten and instead needs a little room
   between letters, same reasoning as the small uppercase labels
   elsewhere on the site (.kpi-label etc. all use positive tracking).
   text-wrap:balance keeps a two-word title like "Fleet Dashboard" from
   ever breaking after just the first word if the column narrows.
   2026-08-24: 34px -> clamp(20px, 4vw, 28px), replacing the two fixed
   breakpoint overrides that used to live in the media queries below (now
   removed -- a fixed override at exactly 768px/420px would have fought
   the clamp at those widths instead of the two ever meeting). Scales
   smoothly from 20px up to a 28px ceiling reached around 700px viewport
   width and held flat above that -- smaller everywhere than the old
   34/26/22px steps, and no more visible jump between them. */
.page-title { font-size: clamp(20px, 4vw, 28px); margin-bottom: 4px; letter-spacing: .2px; line-height: 1.1; text-transform: uppercase; text-wrap: balance; animation: title-in .3s var(--ease) both; }
.page-sub { color: var(--muted); font-size: 13px; margin-bottom: 24px; animation: title-in .3s var(--ease) both; animation-delay: 30ms; }
@keyframes title-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .page-title, .page-sub { animation: none; }
}
/* Dashboard hero bands (2026-08-24, revised) -- real photos, one per
   fleet (Park: guests on the Off-Road Park trail; Street: UTVs along the
   river trail, added later at the same treatment -- "exactamente igual
   a como estaba aplicado a park dashboard"). Every other page keeps the
   plain .page-title/.page-sub header; this is opt-in per dashboard, not
   a site-wide texture.
   .dashboard-hero holds every property that doesn't vary by fleet
   (padding, shadow, overflow); .hero-park/.hero-street (added by
   dashboard.js/dashboard-street.js respectively) hold just the
   background-image + position pair for that fleet's photo, so the two
   dashboards share one mechanism instead of two parallel copies of it.
   2026-08-24, second revision: originally lived inside `.page` with
   negative margins canceling that container's own padding/max-width --
   worked until the "ultra responsive" pass capped `.page` at 1600px,
   after which the hero (a child of `.page`) went narrower than the
   uncapped .topbar sitting right above it on wide screens, a visible
   gutter mismatch ("quita el padding a los lados"). Moved to #heroSlot,
   a sibling of `.page` (see index.html, between the topbar and the
   content column) that dashboard.js targets directly -- now it's full
   width the same way .topbar is, for the same reason (neither is a
   child of the capped container), no margin math needed.
   Also merged what used to be a separate .breadcrumb-bar row into the
   topbar itself (#topbarBreadcrumb, top-left, next to the mobile menu
   button) -- direct feedback that stacking topbar + breadcrumb-bar +
   hero as three visually bordered pieces read as "floating," not as one
   structured block. Two pieces (topbar, hero) flush against each other
   is simpler to make seamless than three. (2026-08-24: .breadcrumb-bar
   itself is gone now, mobile included -- see #topbarBreadcrumb's own
   rule further down.)
   Square corners, no card shadow -- reads as a page cover/header the
   way a nav bar does, not as a peer of the KPI cards below it (see
   Design Guideline — Liquid Glass: "Do NOT use ... for decorative
   purposes within the content area ... creates unnecessary complexity
   and confusing visual hierarchy" -- cards signal "data lives here,"
   and a photo isn't data).
   Uniform overlay per direct product feedback ("el overlay opaco no
   debe ser degradado, debe ser uniforme") -- was a top-to-bottom
   gradient (light at top, dark at bottom); now a single flat rgba wash,
   same opacity everywhere.
   background-size/-position math (2026-08-24, replacing `cover`):
   .topbar and .dashboard-hero previously each used `background-size:
   cover`, which scales an image to fill *that element's own* aspect
   ratio -- since the topbar (very wide, 51px tall) and the hero (same
   width, ~150px tall) have different aspect ratios, `cover` rendered
   the photo at two different effective zoom levels, so the tree line
   jumped in scale right at the seam between them ("se ve desfasado").
   Fixed by sizing both at `100% auto` instead -- width-based, not
   aspect-fit, so at any given viewport width both elements (same width,
   confirmed via getBoundingClientRect) render the image at the *exact
   same* pixel scale. With scale unified, the two background-position-y
   percentages were solved algebraically (measured topbarHeight=51px,
   heroHeight=151px, rendered image height at that width=899px, via the
   CSS spec's own background-position formula
   offset=(container-image)*P%) so that .topbar's bottom edge and
   .dashboard-hero's top edge reveal the exact same row of the source
   image -- a real seam-free continuation, not an approximation. This
   also keeps the earlier fix for the viewport-*height* instability
   (background-attachment stays at its default `scroll`, sizing against
   each element's own box) while finally solving the *width*-driven
   scale mismatch too. */
#heroSlot { flex: none; }
.dashboard-hero {
  position: relative; overflow: hidden; padding: 56px 32px 28px;
  /* Bottom edge shadow -- previously nothing marked where the photo
     header ends and the page content begins, just a flat color change
     (dark photo directly abutting light gray content) -- direct
     feedback that it "still doesn't look structured." A soft shadow
     along the bottom edge only (not a full card shadow/border on all
     sides, which would recreate the "looks like a card" problem from
     two passes ago) gives the header section a definite boundary
     without going back to a bordered box. */
  box-shadow: 0 6px 16px -6px rgba(10,12,16,.35);
}
.dashboard-hero.hero-park {
  background-image: linear-gradient(rgba(10,12,16,.4), rgba(10,12,16,.4)), url('../img/park-hero.avif');
  background-size: 100% auto; background-position: center 54%;
}
/* Street's river-trail photo -- "solo que a este ponle center center".
   Kept at center/50% per that instruction (this photo is naturally
   balanced around its own middle, unlike Park's distinctly stacked sky/
   mountain/people composition, so a plain center crop looks right on
   its own). First pass also put .topbar.street-hero-bg at 50%, on the
   assumption that matching percentages would look matched -- direct
   feedback proved that wrong ("se nota el salto de la imagen, esta mal
   cuadrada"): with two different container heights, the *same*
   percentage produces *overlapping* windows into the image, not
   adjacent ones, which shows as the image jumping backwards right at
   the seam. .topbar.street-hero-bg's own comment has the fix -- solved
   backwards from this element's 50% for the one topbar percentage that
   actually continues from here, the same technique Park's pair used,
   just solved in the other direction since this element's value was the
   fixed constraint here instead of an open variable. */
.dashboard-hero.hero-street {
  background-image: linear-gradient(rgba(10,12,16,.4), rgba(10,12,16,.4)), url('../img/street-hero.avif');
  background-size: 100% auto; background-position: center center;
}
/* The reserved-photo state (2026-08-27). Every page gets the hero band
   now, but only Park and Street have artwork so far -- the rest are
   waiting on photos ("deja espacio para la imagen, yo te las pasaré
   luego"). This holds their space with the same box, padding, shadow and
   white type as a photographed hero, so nothing about the page shifts
   when the picture lands: dropping one in is adding a
   `.dashboard-hero.hero-<page>` rule with the same two properties the
   Park/Street rules above carry (background-image with the dimming
   gradient in front, and background-size/position for the crop) plus one
   line in app.js's HERO_IMAGES.
   Deliberately NOT a grey placeholder box or an "image goes here" slug:
   an unphotographed page should look finished, not unfinished. The angle
   and the two stops come from the dimmed photos themselves -- this is
   roughly what park-hero.avif averages to under its own .4 scrim, so the
   band reads as the same component with the picture turned off rather
   than as a different, flatter header.
   Vertical (180deg), not the angled gradient a standalone band would
   want: the topbar above continues this treatment (.pending-hero-bg) and
   the two have to meet without a seam, exactly as the photo pairs do. A
   vertical ramp makes the hero's top edge one flat color, so the topbar
   is simply that same color and the join is seamless by construction --
   no equivalent of alignHeroPhotoSeam()'s crop math needed. */
.dashboard-hero.hero-pending {
  background-image: linear-gradient(180deg, #23262e 0%, #191c22 60%, #101318 100%);
}
.dashboard-hero .page-title, .dashboard-hero .page-sub { color: #fff; }
.dashboard-hero .page-sub { color: rgba(255,255,255,.82); margin-bottom: 4px; }
/* TrackHawk-inspired detail: a solid pill instead of the soft-tint pills
   used for status elsewhere on the site -- deliberately NOT reused site-
   wide (that system was already accessibility-tuned), just borrowed here
   for one high-contrast badge sitting directly on a photo, where a soft
   tint would disappear against the image instead of reading as a chip.
   border-radius eased from 999px (a full pill, floating with no relation
   to anything else on the page) down to --radius-sm -- the same corner
   language every other rounded element on the site already uses, so
   this reads as "this site's chrome" instead of a generic badge shape. */
.dashboard-hero-badge {
  position: absolute; top: 20px; right: 32px;
  background: var(--brand-red); color: #fff; font-weight: 700; font-size: 12px;
  letter-spacing: .3px; text-transform: uppercase; padding: 6px 14px;
  border-radius: var(--radius-sm); box-shadow: 0 2px 8px rgba(0,0,0,.25);
}
@media (max-width: 768px) {
  .dashboard-hero { padding: 40px 18px 18px; }
  .dashboard-hero-badge { top: 14px; right: 18px; }
}
@media (max-width: 420px) {
  .dashboard-hero-badge { position: static; display: inline-block; margin-bottom: 10px; }
}
/* Continuing the same photo up through the topbar above the hero
   (2026-08-24 product ask: "todo hasta el menu de arriba con el fondo
   de ahi" -- no hard edge where the topbar begins, and "todo tiene que
   ser un solo bloque" -- no visible border seam between them either).
   .topbar already runs a translucent white Liquid Glass gradient (see
   its own rule) -- .park-hero-bg swaps that white tint for the same
   dark dimming gradient as .dashboard-hero, and drops the border that
   used to separate it from the row below (there's no separate row now,
   replaced by the intentional divider border added below instead).
   Sizing/position math lives on .dashboard-hero's own rule above (the
   two are a solved, seam-free pair) -- .park-hero-bg is toggled on
   .topbar by app.js's router (shared chrome rendered once for the whole
   app, not per-page markup dashboard.js can reach on its own).
   Desktop/tablet only (min-width 769px): reskinning the topbar's search
   input for light-on-dark isn't worth it below that width -- mobile
   keeps the standalone hero from an earlier pass (still gets the photo,
   just not the topbar-continuity treatment). */
/* No divider before this on desktop -- .topbar-brand's other child
   (.mobile-menu-btn) is hidden at the same min-width:769px breakpoint
   (see its own rule), so a border-left here would just be an orphaned
   line with nothing to its left. On mobile this instead sits in its own
   grid row (see the max-width:768px block further down). */
.topbar-breadcrumb { display: block; font-size: 12.5px; font-weight: 600; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* `<nav class="breadcrumb">` (app.js's breadcrumb()) has no base
   display style of its own (a <nav> defaults to block) -- forced
   `inline` here so its "Home"/sep/current children flow as one
   continuous run of inline content instead of each getting their own
   box, which is what lets .topbar-breadcrumb's own
   overflow:hidden/text-overflow:ellipsis (needed in the narrow
   769-1100px tablet range -- see .topbar-brand's own comment below)
   produce a real "…" glyph instead of a hard clip; the sep/current
   spans get their own visual spacing from their own margin (see
   .crumb-sep), not from a flex gap. */
.topbar-breadcrumb .breadcrumb { display: inline; }
.topbar-breadcrumb .crumb-sep { margin: 0 6px; }
.topbar-breadcrumb a { color: var(--muted); }
.topbar-breadcrumb a:hover { color: var(--ink); }
.topbar-breadcrumb .crumb-sep { opacity: .5; }
.topbar-breadcrumb .crumb-current { color: var(--ink); font-weight: 700; }
@media (min-width: 769px) {
  /* .topbar-brand's base rule is `justify-self: start` (fine when its
     only content was the near-zero-width mobile button) -- "start"
     means the grid item sizes to fit its own content instead of
     filling its 1fr track, so once a real breadcrumb lives here it
     rendered at its full intrinsic width and visually overlapped the
     centered search box next to it instead of being constrained by the
     track and truncating. `stretch` (the grid default, just re-stated
     since the base rule overrides it) makes it actually fill the track,
     so .topbar-breadcrumb's own overflow:hidden/ellipsis has a real
     width to truncate against. */
  .topbar-brand { justify-self: stretch; min-width: 0; }
  .topbar.hero-bg {
    /* A visible border instead of relying on the (now uniform, not
       gradient) overlay to imply where the topbar's controls end and
       the hero's title begins -- direct feedback: "que se usen
       divisiones de borde en el header y no un overlay opaco." Light,
       low-opacity white reads as a division on the photo the same way
       --border reads as a division on the site's normal white cards. */
    border-bottom: 1px solid rgba(255,255,255,.25);
  }
  .topbar.park-hero-bg {
    background-image: linear-gradient(rgba(10,12,16,.4), rgba(10,12,16,.4)), url('../img/park-hero.avif');
    /* 100% auto + 42% -- see .dashboard-hero.hero-park's own comment for
       the full math; these two values are the solved pair that makes
       this element's bottom edge and the hero's top edge show the same
       row of the source image, AT THE HERO HEIGHT THEY WERE MEASURED
       AGAINST. That height isn't actually constant (.page-sub can wrap
       to a second line in some viewport widths, growing the hero taller
       and throwing this fixed percentage off) -- app.js's
       alignHeroPhotoSeam() overwrites this via inline style with the
       exact value for the hero's REAL rendered height, on every render
       and on resize. This CSS value only matters as the fallback shown
       for the instant before that JS runs (or if it doesn't, e.g.
       scripting disabled) -- close enough to correct not to flash
       obviously wrong, not required to be pixel-exact itself anymore. */
    background-size: 100% auto; background-position: center 42%;
  }
  .topbar.street-hero-bg {
    background-image: linear-gradient(rgba(10,12,16,.4), rgba(10,12,16,.4)), url('../img/street-hero.avif');
    /* NOT center center -- direct feedback: "se nota el salto de la
       imagen, esta mal cuadrada." Using the same 50% on both elements
       doesn't produce continuity, it produces *overlap*: with
       background-size:100% auto, .dashboard-hero.hero-street's own 50%
       reveals rows [381,532] of the rendered image (measured at this
       width) while topbar's 50% would reveal [431,482] -- entirely
       *inside* that range, not adjacent to it. Scrolling from topbar's
       bottom edge (row 482) into the hero's top edge (row 381) means
       jumping backwards 101 rows, a real, visible discontinuity, not a
       subtle mismatch. Solved the same way as Park's pair (CSS spec's
       offset=(container-image)*P%): kept the hero at the requested
       center/50% and solved backwards for the one topbar percentage
       (~38.3%) whose bottom edge lands exactly on the hero's own top
       edge at 50% -- "center" stays honored for the hero itself, the
       topbar just isn't independently centered anymore.
       Same caveat as .park-hero-bg above: 38.3% was solved for one
       specific hero height, and that height isn't actually constant
       (confirmed -- this exact seam broke again once .page-sub wrapped
       to 2 lines in the 769-849px range). app.js's alignHeroPhotoSeam()
       recomputes and overwrites this via inline style for whatever the
       hero's real rendered height is; this value is just the fallback
       shown before that runs. */
    background-size: 100% auto; background-position: center 38.3%;
  }
  .topbar.pending-hero-bg { background-image: none; background-color: #23262e; }
  .topbar.hero-bg .sidebar-filter-icon { color: rgba(255,255,255,.75); }
  .topbar.hero-bg .dd-trigger { color: rgba(255,255,255,.92); }
  .topbar.hero-bg .dd-trigger:hover, .topbar.hero-bg .dd-trigger[aria-expanded="true"] { color: #fff; }
  .topbar.hero-bg .topbar-search { border-bottom-color: rgba(255,255,255,.4); color: rgba(255,255,255,.75); }
  .topbar.hero-bg .topbar-search input { color: #fff; }
  .topbar.hero-bg .topbar-search input::placeholder { color: rgba(255,255,255,.6); }
  .topbar.hero-bg .topbar-breadcrumb, .topbar.hero-bg .topbar-breadcrumb a { color: rgba(255,255,255,.85); }
  .topbar.hero-bg .topbar-breadcrumb a:hover { color: #fff; }
  .topbar.hero-bg .topbar-breadcrumb .crumb-current { color: #fff; }
}
.section-title { font-family: var(--font-sans); font-weight: 800; font-size: 23px; display: flex; align-items: center; gap: 9px; margin-bottom: 14px; letter-spacing: -.1px; }
.section-title .bar { width: 4px; height: 19px; background: linear-gradient(180deg, var(--brand-red), var(--brand-red-hover)); border-radius: 3px; display: inline-block; }

.grid { display: grid; gap: 16px; }
/* .grid-5 (2026-08-25): Street Dashboard's Fleet Status row only, once
   Tire Rotation joins Unit Status/Service Reminders/Belt Replacement/
   Unresolved Issues -- same breakpoint behavior as .grid-4 (2-up under
   1100px, 1-up under 700px), just one more column at full width. */
.grid-5 { grid-template-columns: repeat(5, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 1100px) { .grid-5, .grid-4 { grid-template-columns: repeat(2,1fr); } .grid-3{grid-template-columns:repeat(2,1fr);} }
@media (max-width: 700px) { .grid-5, .grid-4, .grid-3, .grid-2 { grid-template-columns: 1fr; } }

/* 2026-08-05: cards got a diagonal gradient + a pronounced two-layer shadow
   (10px offset, 28px blur) to read as "real depth" against the page's own
   gradient. 2026-08-24 product feedback: with 10+ of these stacked on one
   screen (Dashboard), that shadow plus --radius-lg's 16px corners plus the
   gradient compounded into a heavy, "abultado" page -- every card looked
   like it was floating a full centimeter off the surface. Flattened to a
   near-white surface with only the faintest top-left/bottom-right tint (a
   card still isn't flat-flat, just no longer the dominant visual event),
   a single tight contact shadow instead of the wide ambient one, and a
   tighter radius so cards read as cards, not pills. */
/* 2026-08-27: flattened the rest of the way, so the whole app speaks the
   language the Dashboard settled on ("aplica este diseño a las demás
   páginas"). The de-bulk pass above got .card from "floating a centimeter
   off the page" down to a soft card, but soft was still a different
   component than a Dashboard tile: side by side, a chart panel cast a
   shadow and curved its corners while the tiles beside it were square
   hairline cells. Three properties carried that difference and all three
   are gone -- the diagonal gradient (a flat white surface now), the
   contact shadow (none), and the radius (square, the same reasoning the
   tiles were squared for on 2026-08-26: a data surface is a table, and
   tables have square cells). What remains is exactly what a tile is: white
   fill, one hairline border. Panels and tiles are now one component with
   two sizes instead of two components that happen to sit together. */
.card {
  background: var(--surface-white);
  border: 1px solid var(--border);
  border-radius: 0;
  box-shadow: none;
  padding: 20px;
  /* Every card fades/lifts in on its own mount — since mount.innerHTML is
     replaced wholesale on each route render, this is genuinely fresh DOM
     each time, so it also doubles as a page-transition cue for free, no JS
     involved. Purely decorative -- opacity/transform only, nothing that
     affects layout while it plays.
     2026-08-24: shortened from 16px/.55s/70ms-steps -- Apple's own Motion
     guidance is explicit that frequent interactions should generally skip
     added motion entirely, and this dashboard gets looked at many times a
     day. Kept as a brief, barely-there settle instead of removing outright
     (still "a page transition happened", just not a show every time). */
  animation: card-in .22s var(--ease) both;
}
@keyframes card-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
/* Staggered by position, but compressed into ~120ms total instead of ~500ms
   -- enough for the row to read as one soft wave, not a wait. */
.grid > .card:nth-child(1), .table-wrap.card:nth-child(1) { animation-delay: 0ms; }
.grid > .card:nth-child(2) { animation-delay: 20ms; }
.grid > .card:nth-child(3) { animation-delay: 40ms; }
.grid > .card:nth-child(4) { animation-delay: 60ms; }
.grid > .card:nth-child(5) { animation-delay: 80ms; }
.grid > .card:nth-child(6) { animation-delay: 100ms; }
.grid > .card:nth-child(7) { animation-delay: 120ms; }
.grid > .card:nth-child(n+8) { animation-delay: 120ms; }
@media (prefers-reduced-motion: reduce) {
  .card { animation: none; }
}
.section { margin-bottom: 32px; }

/* 2026-08-25 content-design pass ("interfaces reales" product feedback,
   the two reference dashboards): border-radius only, scoped to kpi-card --
   .card's own --radius-md (12px)/padding/shadow were already tuned soft
   and generous in the 2026-08-24 de-bulk pass (see .card above), squarely
   inside the reference's "12-16px, soft/no border" range, so those stay
   untouched. This nudges just the radius to the top of that range for a
   slightly softer/rounder read, matching the reference's own cards. */
.kpi-card { position: relative; overflow: hidden; border-radius: var(--radius-lg); }
.kpi-card::after {
  content: ''; position: absolute; top: 0; left: 0; width: 3px; height: 100%; background: var(--border);
}
/* A subtle top-to-bottom gradient instead of a flat fill -- same two colors
   either way, just given a little life instead of reading as a static
   sticker on the card's edge. */
.kpi-card.accent-critical::after { background: linear-gradient(180deg, var(--critical), color-mix(in srgb, var(--critical) 70%, black)); }
.kpi-card.accent-ok::after { background: linear-gradient(180deg, var(--ok), color-mix(in srgb, var(--ok) 70%, black)); }
/* 2026-08-25 product feedback: a tinted background + tinted border on
   accent-critical (from an earlier iteration of this hierarchy pass) read
   as decorative red overuse, not "this needs attention" -- reverted.
   Rule 6 (CLAUDE.md) stays as-is: --brand-red is reserved for critical
   state + the page's one primary CTA, nothing else. The edge bar above
   is kept exactly as it landed after that revert -- untouched below. */
/* Icon+label row (kpiIconHtml() in app.js). The "Attention" chip that used
   to live here was removed 2026-08-25 per product feedback ("no sirve") --
   the red edge bar (.kpi-card.accent-critical::after) plus the bold dark
   value already carry the critical signal. flex-wrap stays: a source tag
   (.source-tag, see sourceTag() in app.js) can still share this row with a
   long label at kpi-compact's ~150-180px width (Vehicle Detail) or the
   2-up grid breakpoint below, and should wrap rather than get clipped. */
.kpi-card .kpi-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 6px 8px; flex-wrap: wrap; }
.kpi-card .kpi-icon { width: 13px; height: 13px; flex-shrink: 0; margin-right: 6px; vertical-align: -2px; color: var(--muted); }
/* Click-through cards (kpi()'s opts.href — Dashboard summary tiles linking
   to the real page that owns that detail) get a visible affordance so they
   don't look identical to the plain, non-interactive KPI cards elsewhere. */
.kpi-card.kpi-clickable { cursor: pointer; transition: transform .12s var(--ease), box-shadow .12s var(--ease); }
.kpi-card.kpi-clickable:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
/* The KPI card currently driving a table filter (Parts' inventory coverage
   modes). A border, not a fill: a tinted card was rejected for status
   colour 2026-08-25, and the same reasoning applies to selection -- the
   card must read as "this one is active", not as "this one is alarming". */
.kpi-card.kpi-active { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.kpi-card.kpi-clickable:focus-visible { outline: 2px solid var(--brand-red); outline-offset: 2px; }
/* 2026-08-24: eased from 700 to 600, same "just a pinch lighter" product
   feedback as .kpi-value below -- still bold enough to read as a label
   at 11.5px, one step down from the card's heaviest text.
   2026-08-25: now a flex row (icon + text) instead of plain block text --
   see .kpi-icon above. */
.kpi-card .kpi-label { display: inline-flex; align-items: center; min-width: 0; font-size: 11.5px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .6px; }
/* Montserrat/--font-sans, weight 700 (2026-08-24: eased back from 800 --
   "just a pinch" lighter, per product feedback -- still the heaviest
   text on the card so it reads as the clear visual anchor).
   30px -> clamp(18px, 3.2vw, 24px), same reasoning as .page-title just
   above -- replaces the two fixed breakpoint overrides that used to live
   in the media queries below (removed, they'd fight the clamp at those
   exact widths otherwise). */
/* font-variant-numeric: tabular-nums removed 2026-08-25 -- this is a
   standalone display value (dataviz's stat-tile contract), not a column of
   numbers that needs vertical digit alignment; tabular figures give every
   digit a fixed '0'-width and read loose at display sizes. Proportional
   (the font's default) is the correct figure style here -- table columns
   elsewhere in the app keep tabular-nums on their own rules, untouched. */
.kpi-card .kpi-value { font-family: var(--font-sans); font-weight: 700; font-size: clamp(18px, 3.2vw, 24px); line-height: 1.2; margin-top: 6px; letter-spacing: -.3px; color: var(--ink); }
/* 2026-08-25 content-design pass: dropped the red color this used to carry
   ("Numbers default to bold black/dark-ink, not color" -- product
   direction after rejecting the accent-critical wash). Weight alone is
   the number's own emphasis now; the red edge bar next to the card
   (.kpi-card.accent-critical::after) is the "needs attention" signal --
   keeping color/weight as separate tools instead of stacking both on the
   one element rule 6 is strictest about. */
.kpi-card .kpi-value.critical { font-weight: 800; }
.kpi-card .kpi-foot { font-size: 12px; color: var(--muted); margin-top: 8px; }
/* Real-content list (2026-08-25, e.g. Belt Replacement's worst-overdue
   units on the Street Dashboard) -- a couple of short rows of actual data
   under the footer instead of only a count + link, same "who/what exactly"
   pattern as the reference dashboard's named-customer/product rows. Kept
   deliberately spare (no card, no icons of its own) so it reads as part of
   this KPI card, not a second component bolted underneath. */
.kpi-card .kpi-reallist { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.kpi-card .kpi-reallist-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: 12px; }
.kpi-card .kpi-reallist-name { flex: 1 1 auto; min-width: 0; color: var(--text-dim); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kpi-card .kpi-reallist-meta { color: var(--critical); font-weight: 700; font-size: 11px; flex-shrink: 0; }
/* Compact breakdown chips (e.g. Unresolved Issues' per-store counts) --
   replaces a run-on "Store A 32 · Store B 3 · ..." sentence tacked onto the
   footer, which made that one card's footer wrap to several lines while
   every sibling stayed one line (2026-08-25 "se ve ... desbalanceado"
   product feedback). Chips read as discrete tags at a glance instead of
   dense prose, wrap independently of the sentence above, and don't force
   the card taller than a sibling with a short footer. */
.kpi-card .kpi-breakdown { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
.kpi-card .kpi-chip { display: inline-flex; align-items: baseline; gap: 4px; font-size: 11px; font-weight: 500; color: var(--text-dim); background: var(--surface); border-radius: 999px; padding: 2px 8px; line-height: 1.4; }
.kpi-card .kpi-chip b { font-weight: 700; color: var(--ink); }
/* Small color dot inside a chip when the chip is naming one segment of a
   colored breakdown above it (Unit Status) -- the chip text itself always
   stays a text token (never the series color, per dataviz's own rule),
   this dot is the one place identity color actually shows. */
.kpi-card .kpi-chip-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; display: inline-block; }
/* Same reallist row markup as the belt-replacement worst-overdue list
   above, reused for Tech Performance's top-3 leaderboard -- but a rank +
   completion count isn't a problem the way "mi over" is, so it must not
   inherit .kpi-reallist-meta's default --critical red (that would read as
   an alert on a purely informational ranking). */
.kpi-card .kpi-reallist-meta.neutral { color: var(--muted); font-weight: 600; }

/* ---------- card sparklines/mini-charts (2026-08-25 content-design pass,
   sparklineSvg()/statusBarSvg()/miniBarRow() in dashboardShared.js) --
   only ever appended via kpi()'s existing opts.extra slot, so every rule
   below sits inside the same "block under the footer" spot .kpi-reallist/
   .kpi-breakdown already established -- no new slot on the card itself. */
.kpi-card .kpi-spark-wrap { margin-top: 8px; }
.kpi-card .kpi-spark { display: block; width: 100%; height: 36px; }
.kpi-card .kpi-segbar-wrap { margin-top: 8px; }
.kpi-card .kpi-segbar { display: block; width: 100%; height: 5px; }
.kpi-card .kpi-minibar-block, .kpi-card .kpi-meter-block { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.kpi-card .kpi-minibar-row { display: flex; align-items: center; gap: 8px; font-size: 11px; }
.kpi-card .kpi-minibar-label { flex: 0 0 auto; min-width: 56px; max-width: 110px; color: var(--text-dim); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kpi-card .kpi-minibar { flex: 1 1 auto; min-width: 40px; }
.kpi-card .kpi-minibar-value { flex: 0 0 auto; font-weight: 600; color: var(--text-dim); font-size: 11px; }
/* Period-over-period trend badge — color follows whether the direction of
   change is desirable for that specific metric (set by the caller via
   `deltaGood`), not just the raw sign of the number: e.g. spend going down
   is "good" (green), open work orders going down is "good", active
   vehicles going down is "bad".
   2026-08-25: `.kpi-delta` itself is now a small soft-background pill
   (same shape as the reference admin dashboard's "↓36.8%" chip) instead of
   plain colored text -- the caption ("vs last month") stays a separate
   plain-gray sibling in `.kpi-delta-row`, not inside the colored pill, so
   the pill stays exactly as small/specific as the reference's (just the
   arrow + percentage, never a colored sentence). */
.kpi-delta-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 7px; }
.kpi-delta { display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 700; padding: 2px 8px; border-radius: 999px; }
.kpi-delta-arrow { font-size: 9px; }
.kpi-delta-good { color: var(--ok); background: var(--ok-soft); }
.kpi-delta-bad { color: var(--critical); background: var(--critical-soft); }
.kpi-delta-flat { color: var(--muted); background: var(--neutral-soft); }
.kpi-delta-caption { font-weight: 500; color: var(--muted); font-size: 11.5px; }
/* Vehicle Detail's 4 KPI cards sit in a narrower column (next to the
   viewer, not the full page width like Dashboard/Financials' grid-4 rows),
   so the default kpi-value would crowd or wrap in ~150-180px of card
   width. Same card, same label/foot sizing — just a smaller number.
   2026-08-24: 22px -> clamp(14px, 2.4vw, 18px), same proportional scale
   as the base .kpi-value's own clamp() just above. */
.kpi-card.kpi-compact .kpi-value { font-size: clamp(14px, 2.4vw, 18px); margin-top: 4px; }
/* Tighter than the base .kpi-card's 20px/22px padding + 6px/8px inner
   margins — at kpi-compact's smaller 28px value there was visible dead air
   below the foot text (2026-08-04 product feedback: "the KPI row on
   Vehicle Detail feels too airy"). Scoped to .kpi-compact only (also used
   by Work Orders' "By Priority" row, see workorders.js) so
   Dashboard/Financials/Bookings/Parts' regular .kpi-card sizing is
   untouched. */
.kpi-card.kpi-compact { padding: 13px 16px 11px; }
.kpi-card.kpi-compact .kpi-foot { margin-top: 5px; }
/* Vehicle Detail's own KPI row sits noticeably tighter than the page-wide
   .grid-4 rows used elsewhere (Dashboard etc.) — same reasoning as above,
   scoped via the ancestor class so other pages' 16px .grid gap is
   untouched. */
.vehicle-top-grid .grid-4 { gap: 12px; }
/* Slightly less breathing room below the whole top grid than the site-wide
   30px .section default — this specific compound selector (not a bare
   `.section` override) only fires here, other pages' `.section` spacing is
   untouched. */
.vehicle-top-grid.section { margin-bottom: 24px; }

/* Vehicle Detail's top section: viewer (narrower) + KPI row/usage chart
   (wider) side by side. Below 1000px the two columns get too tight for
   4 KPI cards plus a chart to stay readable, so it drops to a single
   stacked column — same breakpoint spirit as .grid-4's own collapse. */
@media (max-width: 1000px) {
  .vehicle-top-grid { grid-template-columns: 1fr !important; }
}

/* Rental Frequency + $ Generated side by side, each taking half the width
   the single Rental Frequency card used to occupy (2026-08-04 product ask).
   This used to stack based on a viewport media query (max-width: 640px) —
   but that breakpoint was picked around "narrow phone", while the actual
   width available to these two cards is set by .vehicle-top-grid's own
   0.8fr/1.2fr split above (see vehicle.js), which shrinks this column
   continuously as the viewport narrows well before hitting 640px or even
   before .vehicle-top-grid itself collapses at 1000px. Two independent
   viewport breakpoints (1000px for the outer grid, 640px for this one)
   fought each other: in the ~640-1000px viewport range the outer grid was
   still side-by-side, so this column only ever got its narrow 1.2fr share
   (as little as ~330-420px), yet the 640px rule never fired because the
   *viewport* was still wide — result: two ~150-200px-wide charts crammed
   into a column that was already too narrow for them, confirmed with
   Playwright ("sigue comprimido" follow-up, 2026-08-04).

   Fixed by switching to a container query on the actual column that sizes
   these cards (`.vehicle-top-right-col`, see vehicle.js) instead of the
   viewport — the two charts now stack based on how much width THEY
   actually have, regardless of which state the outer grid happens to be
   in. This also means the transition reads as one continuous rule instead
   of two breakpoints landing at different times by coincidence. */
.usage-split-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; min-height: 0; }
.vehicle-top-right-col { container-type: inline-size; }
/* 600px chosen (not the round 640px carried over from the old viewport
   rule) because it's specifically wide enough to cover the case Playwright
   caught at a 900px browser viewport: .vehicle-top-grid has already
   collapsed to one column there (below its own 1000px breakpoint), handing
   this column ~590px — comfortably below 600px, so it correctly stacks
   instead of splitting into two ~230px "cuadrado chico" halves. */
@container (max-width: 600px) {
  .usage-split-grid { grid-template-columns: 1fr; }
}
/* Rental Frequency / Revenue Generated each got noticeably more cramped
   once they went from one full-width card to sharing a row (2026-08-04
   product feedback: "se siente comprimido"). The two-column split itself
   stays — just loosen up what's inside each half: a touch more internal
   padding than the site's base .card (20px/22px) so the chart isn't flush
   against the card edge, plus the wider 22px gap above between the two
   halves so they read as two distinct, unhurried cards rather than one
   crowded row. */
.usage-card { padding: 22px 26px 20px; }

.pill {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 11px; border-radius: 7px;
  font-size: 11.5px; font-weight: 700; white-space: nowrap; letter-spacing: .1px;
  /* max-width:100% + overflow/text-overflow below is the fix for a Fleet-
     table bug (2026-08-25 screenshot: "PARTS UNIT"/"Active (Observed)"
     bleeding into the Meter/Location text next to them) -- a longer status
     label's pill has no width of its own, so at a narrower table width
     (fixed-layout column % gets smaller in absolute px, or the fleet-table
     min-width breakpoint) the pill just kept growing past its <td>'s
     boundary and visually sat on top of the next cell's content, since a
     table cell's default overflow is visible. Capping the pill to its own
     cell's width and ellipsis-truncating instead means it can never grow
     wider than the column that contains it, at any viewport -- the
     long-label cases now truncate in place ("PARTS UNI…") with the full
     text still available via pill()'s own `title` (see app.js), rather
     than overlapping the next column illegibly. Safe everywhere else
     .pill is used (Work Orders, Vehicle Detail, Financials POs...) too --
     this only ever engages when a pill's container is actually narrower
     than its content, which previously just silently overflowed there too.
  */
  max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.pill::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.pill.status-good { background: var(--ok-soft); color: var(--ok); }
.pill.status-warn { background: var(--warn-soft); color: var(--warn); }
.pill.status-critical { background: var(--critical-soft); color: var(--critical); }
.pill.status-neutral { background: var(--neutral-soft); color: var(--neutral); }

/* ---------- brand wordmark chip (brandBadge() in app.js) ---------- */
/* brandBadge() is only called from vehicle.js's header today (list views —
   Fleet roster, Dashboard's Cost & Reliability table — print the make as
   plain text via normalizeMake(), no chip/logo, per design decision: the
   logo only earns its place where a single vehicle is the whole point of
   the screen). Typographic stand-in for a real brand logo — bold
   Montserrat over a flat brand-accent color (see --brand-* tokens above)
   — for makes without a sourced logo file (ROXOR, Unknown). */
.brand-badge {
  display: inline-flex; align-items: center; justify-content: center; padding: 3px 9px 4px; border-radius: 5px;
  font-family: var(--font-sans); font-weight: 700; font-size: 13px; line-height: 1.1;
  letter-spacing: .2px; color: #fff; white-space: nowrap; vertical-align: middle;
}
.brand-badge.brand-unknown { background: var(--neutral-soft); color: var(--muted); font-family: var(--font-sans); font-weight: 600; }
.brand-badge.brand-sm { padding: 2px 7px 3px; font-size: 11.5px; }
.brand-badge.brand-lg { padding: 8px 16px 9px; font-size: 22px; border-radius: 7px; }

/* Real brand logo (brandBadge() in app.js, when /img/brands/{slug}.{svg|png}
   exists) — sits inline where the typographic chip otherwise would. The six
   sourced files have very different aspect ratios (Honda near-square,
   Polaris ~8:1 wide) — fixing only `height` (old approach) left wildly
   different rendered widths, which reads as visually inconsistent in a row
   of badges. Instead `.brand-logo` is a fixed-WIDTH box and the <img> inside
   uses `object-fit: contain` so every logo centers and scales down
   proportionally to fit the same box, regardless of its native aspect
   ratio — no stretching, no cropping. */
.brand-logo { display: inline-flex; align-items: center; justify-content: center; width: 72px; height: 21px; vertical-align: middle; }
.brand-logo.brand-sm { width: 56px; height: 16px; }
.brand-logo.brand-lg { width: 148px; height: 44px; }
.brand-logo img { display: block; width: 100%; height: 100%; object-fit: contain; }

/* ---------- compact filter bar — a couple of dropdowns, not a wall of pills ---------- */
.filter-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; align-items: center; }
.filter-select {
  border: 1px solid var(--border); background: var(--surface-white); color: var(--text-dim);
  border-radius: var(--radius-sm); padding: 9px 32px 9px 14px; font-size: 12.5px; font-weight: 600;
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2386868f' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat; background-position: right 10px center; background-size: 13px;
}
.filter-select:focus { outline: none; border-color: var(--brand-red); }
.filter-count { font-size: 11.5px; color: var(--muted); font-weight: 600; }

/* Filter-row dropdown trigger — same visual footprint as .filter-select
   above (border/background/radius/padding/font/chevron), for filters that
   use mountDropdown() instead of a plain native <select> so their OPEN
   option list is themed too (a native <select>'s popup is OS-painted, no
   matter what CSS targets the closed control — see mountDropdown()'s own
   comment in app.js). 2026-08-25 product ask ("arregla el diseño de
   fleet, que se vea mas uniforme"): Fleet's Type/Status filters were the
   plain <select> version; this lets them become real .dd instances that
   look identical closed AND match every other themed dropdown in the app
   once open (same light .dd-menu popup as the topbar's Period/Fleet Mode
   filters, now that .dd-menu itself is light-themed — see its own
   comment above). */
.filter-row .dd-trigger {
  display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 150px;
  border: 1px solid var(--border); background: var(--surface-white); color: var(--text-dim);
  border-radius: var(--radius-sm); padding: 9px 12px 9px 14px; font-size: 12.5px; font-weight: 600;
  outline: none; transition: border-color .15s var(--ease), color .15s var(--ease);
}
.filter-row .dd-trigger:hover, .filter-row .dd-trigger[aria-expanded="true"] { border-color: var(--brand-red); color: var(--ink); }
.filter-row .dd-trigger-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filter-row .dd-trigger-chevron { flex: none; width: 13px; height: 13px; color: #86868f; transition: transform .2s var(--ease); }
.filter-row .dd-trigger[aria-expanded="true"] .dd-trigger-chevron { transform: rotate(180deg); }

table { width: 100%; border-collapse: collapse; font-size: 13px; }
/* Fleet roster's table only — table-layout:auto (the site default above)
   sizes each column off its own widest cell across all 150 rows in the
   DOM at once (no virtualization), so one long Model/Trim string anywhere
   in the list could blow the Make/Model column out disproportionately,
   squeezing Type/Status/Location/Meter into uneven leftover gaps (2026-08-05
   product feedback: "el espaciado de las columnas se ve raro"). Fixed
   layout + an explicit colgroup (see fleet.js) makes column widths
   deterministic regardless of content length. Scoped to this one table,
   not the base `table` rule above — other pages' tables still want
   auto-sizing (their content doesn't vary this wildly row to row). */
.fleet-table { table-layout: fixed; }
/* Only the two columns whose content can realistically run long
   (Make/Model incl. Trim, and Location — e.g. "Sevierville Off-Road Park")
   get nowrap+ellipsis; the rest (Unit/Type/Status/Meter) are short enough
   at these fixed widths that forcing an ellipsis everywhere would just
   hide values with no visual cue why. fleet.js sets a `title` attribute
   on both so the full text is still available on hover. */
.fleet-table td:nth-child(2), .fleet-table td:nth-child(5) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fleet-table .fleet-row-make span { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* Solid header band (2026-08-24, TrackHawk-inspired detail) -- was
   transparent, sitting flush with the card's own white background with
   only the bottom border to separate it from row 1. A filled header row
   gives every table the same clear "this is the header" band TrackHawk's
   tables have, without touching row striping/hover or the pill/badge
   system elsewhere. */
th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--muted); font-weight: 700; padding: 11px 14px; border-bottom: 1px solid var(--border); background: var(--surface); }
td { padding: 12px 14px; border-bottom: 1px solid var(--border); color: var(--text-dim); }
tbody tr:nth-child(even) { background: color-mix(in srgb, var(--surface) 45%, transparent); }
tbody tr:hover { background: var(--critical-soft); cursor: pointer; }
.table-wrap { overflow-x: auto; }
/* Scrolling variant, ported 2026-08-27: ~460px = a sticky header (~35px)
   plus 10 body rows at ~42px each, so a long table shows a consistent
   window instead of running the page down. The header stays pinned, or a
   scrolled list loses its column labels. */
.table-wrap-scroll { max-height: 460px; overflow-y: auto; }
.table-wrap-scroll thead th { position: sticky; top: 0; background: var(--surface-white); z-index: 1; }
/* Opt-in modifier (2026-08-25 product ask, Work Orders' two tables) --
   caps a table to roughly 10 visible rows with its own internal vertical
   scroll instead of letting a long table push the rest of the page down
   indefinitely. Scoped as a modifier, not a change to .table-wrap
   itself, since most other tables on the site (Fleet, Financials, Parts)
   are fine growing to their natural height. `th`'s own background is
   already opaque (see its rule above), so sticky headers need no extra
   background override here to stay readable over scrolling rows. */
.table-wrap.table-wrap-capped { max-height: 500px; overflow-y: auto; }
.table-wrap.table-wrap-capped thead th { position: sticky; top: 0; z-index: 1; }

/* Most Used Parts (parts.js) — one <details> per part instead of a flat
   table row, since each part's usage breakdown (which vehicles, how much
   spent on each) is itself a small table. Native <details>/<summary> gets
   expand/collapse for free with zero JS and no new dependency. */
.most-used-parts-list { display: flex; flex-direction: column; gap: 10px; }
.most-used-part { padding: 0; }
.most-used-part summary {
  list-style: none; cursor: pointer; padding: 14px 22px; font-size: 14px;
  display: flex; align-items: center; gap: 6px;
}
.most-used-part summary::-webkit-details-marker { display: none; }
.most-used-part summary::before {
  content: '\25B8'; color: var(--muted); font-size: 11px; transition: transform .15s var(--ease); flex: none;
}
.most-used-part[open] summary::before { transform: rotate(90deg); }
.most-used-part summary:hover { background: var(--surface); border-radius: var(--radius-lg); }
.table-nested { margin: 0 22px 16px; width: calc(100% - 44px); }
.table-nested th, .table-nested td { padding: 8px 10px; }

.bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 11px; font-size: 13px; }
/* Work Orders' two by-technician charts (2026-08-25 product ask: "que
   estos dos graficos tambien sean clickeables sus partes") -- a whole
   row (Completed by Technician, one bar = one tech = one filter) or one
   segment within a row (Workload by Technician, each color = a
   different status for that tech) filters the table below. Brightening
   on hover (not a background swap, which would fight the bar's own
   color) is the same affordance direction as .kpi-clickable without
   copying its card-specific shadow/lift, which wouldn't read right on a
   thin bar segment. */
.bar-row-clickable { cursor: pointer; border-radius: 4px; transition: background .12s var(--ease); }
.bar-row-clickable:hover { background: var(--surface); }
.workload-seg { cursor: pointer; transition: filter .12s var(--ease); }
.workload-seg:hover { filter: brightness(1.15); }
.bar-row .label { width: 150px; flex: none; color: var(--text-dim); }
.bar-row .track { flex: 1; height: 7px; background: var(--surface); border-radius: 4px; overflow: hidden; }
.bar-row .fill { height: 100%; border-radius: 4px; }
.bar-row .val { width: 40px; text-align: right; flex: none; font-weight: 700; }
/* Denser variant of .bar-row — additive modifier, base .bar-row rules above
   (used as-is by financials.js's spend-by-category chart) are untouched.
   Used by Dashboard's "Fleet Composition" block, which is deliberately the
   lowest-priority/most-reference-only section on that page (see
   dashboard.js) — smaller type, thinner track, tighter spacing so it reads
   as a footnote rather than another full-size chart. */
.bar-row.compact { margin-bottom: 7px; font-size: 12px; }
.bar-row.compact .label { width: 118px; }
.bar-row.compact .track { height: 5px; }
.bar-row.compact .val { width: 32px; }
/* Small uppercase sub-header used above a compact bar-row group when two
   of them share one card/section-title (see "Fleet Composition" on
   Dashboard) — same visual language as .kpi-label, not a second
   .section-title (that stays reserved for one per section, per CLAUDE.md). */
/* Same role as .kpi-label (small uppercase eyebrow) -- was .5px vs .6px
   for no reason other than being added separately; now one shared value. */
.compo-subhead { font-size: 11.5px; color: var(--muted); font-weight: 700; text-transform: uppercase; letter-spacing: .6px; margin-bottom: 10px; }

/* Data-source tag (sourceTag() in app.js). Peek Pro renders its own real
   wordmark (.source-logo, an <img>) -- 2026-08-25 product feedback: "pongas
   el logo de peek pro... sin color verdecito ni nada, respetando la
   marca" rejected an earlier invented-teal-pill version. .source-tag
   (plain neutral text, no color) is the fallback for a source with no
   sourced logo file yet (currently just Fleetio) -- same "brand-unknown"
   restraint .brand-badge already uses rather than inventing another color. */
.source-logo { height: 20px; width: auto; flex: none; vertical-align: middle; opacity: .85; }
.source-logo-fleetio { height: 18px; }
.source-logo-pinnacle { height: 24px; }
.source-tag {
  display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px;
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .3px;
  white-space: nowrap; vertical-align: middle; flex: none;
}
.source-tag-neutral { background: var(--neutral-soft); color: var(--muted); }

/* Dashboard columns (2026-08-25 product ask, "tipo como columnas") --
   scoped to dashboardShared.js's sectionHtml()/dashboard-street.js's
   Bookings block ONLY. Deliberately NOT a change to .grid/.grid-3/.grid-4/
   .grid-5 used by every other page's KPI rows -- those stay row-wrapping
   grids; only the two Fleet Dashboards adopt this side-by-side-columns
   shape, since that's the only place this was asked for. Each column
   stacks its own cards vertically (gap matches .grid's own 16px); columns
   sit side by side down to the same 1100px breakpoint the rest of the KPI
   grid already collapses at, below which they stack full-width instead of
   squeezing into unreadably narrow strips. */
/* =====================================================================
   Dashboard layout — bounded tile grid (2026-08-26, third pass)
   =====================================================================
   Product feedback on the grouped-list pass: "se ve DEMASIADO raro."
   Measured on the rendered page at 1920 rather than guessed, the previous
   layout's real numbers were:

     lane width ................. 757px  (Park has only two lanes)
     sparkline .................. 721x36px, a 20:1 aspect ratio
     gap, metric label -> its
     source logo ................ 467-562px

   i.e. one metric was stretched across 757px to present ~200px of
   content; its trend line was flattened into a near-straight streak by a
   20:1 box; and its Fleetio/Peek Pro badge sat half a screen away from
   the label it belongs to, reading as an unrelated element. That is the
   weirdness — not the flat styling, which stays.

   The rule this violated (see FleetView design rules 1-3 in CLAUDE.md):
   a stat NEVER stretches to fill its container. Width is absorbed by
   fitting MORE tiles per row, not by making fewer tiles wider — which is
   what every dashboard worth copying does, and what the old vertical
   "lane" model structurally could not do, since lane width was just
   (page width / number of lanes).

   So the vertical lanes are gone. A domain is a SECTION stacked down the
   page, and its metrics are a responsive grid of bounded tiles inside it.
   The section's surface is one white sheet; the 1px lattice between
   tiles comes from a 1px grid gap over a --border-colored backdrop, so
   every divider is a true hairline and no tile owns a border of its own.
   Flat, hairline-separated, no floating cards — the part of the previous
   pass that was right is kept intact. */

.dash-section { margin-bottom: 22px; }
.dash-section:last-child { margin-bottom: 0; }
/* Section headings use .section-title — the app's own shared pattern
   (CLAUDE.md rule 3: red bar + 23px/700, one CSS rule site-wide, never
   duplicated or overridden per page). Earlier passes of this redesign
   invented a 11.5px grey uppercase label here instead, which is exactly
   what product feedback called out: "no hay un orden preestablecido según
   la app" — the dashboard had opted out of the site's established
   heading hierarchy and so read as a flat field of tiles with no
   structure above them. Nothing is restyled here; the sections just use
   the real rule. Only the spacing below it is set, since .section-title's
   own 14px margin is tuned for a heading sitting directly above body
   content rather than above a bounded grid surface. */
.dash-section > .section-title { margin-bottom: 11px; }
/* A section-level source badge (Maintenance is entirely Fleetio-sourced,
   so the mark belongs on the heading once rather than on all five tiles).
   margin-left:auto pushes it to the far end of the heading row; the
   explicit height keeps it from inflating .section-title's line box. */
.dash-section > .section-title .section-sources {
  margin-left: auto; display: inline-flex; align-items: center; gap: 14px;
}
/* Per-brand heights, because equal pixel heights do NOT read as equal
   size: each asset devotes a different share of its box to actual glyphs.
   Peek Pro is a compact wordmark that fills its box, Fleetio is mark +
   wordmark, and Pinnacle is a two-line lockup whose "Pinnacle" word is
   only ~3/4 of the image height. Tuned so the three read as one family
   ("los logos uniformízalos, el de peek pro un poco mas grande, pinnacle
   un poquito mas pequeño") rather than to one shared number. */
.dash-section > .section-title .source-logo { height: 18px; opacity: .8; }
.dash-section > .section-title .source-logo-peekpro { height: 21px; }
.dash-section > .section-title .source-logo-pinnacle { height: 19px; }

/* auto-fill (not auto-fit): auto-fit would collapse empty tracks and let
   a lone tile stretch to the full row -- exactly the failure being fixed.
   auto-fill keeps the track width honest, so a section with one metric
   renders one normal-width tile and leaves the rest of the row empty,
   which is correct. */
/* The lattice comes from borders ON the cells, not from a 1px grid gap
   over a --border-coloured backdrop. The gap trick is the tidier CSS, but
   it paints the backdrop through any track a cell doesn't occupy: a
   7-tile section in a 5-column grid left three EMPTY tracks on row two
   rendering as solid grey blocks, which read as broken. Cell borders
   simply aren't drawn where there's no cell, so a partial last row ends
   cleanly in white, the way a table's does. */
.dash-grid {
  display: grid;
  /* 300px, not a rounder guess: measured at 1400 the widest tile header
     needs 260px (label "Tech Performance" 177px + Fleetio badge 76px +
     gap), and below that the badge wrapped onto its own line, making that
     one tile taller than every sibling in its row. 260 + 34px of tile
     padding = 294, rounded to 300 for headroom on longer labels. */
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  /* The grid paints NOTHING itself. It used to be one white sheet with the
     hairlines cut into it, which works only while every track is
     occupied: a 6-tile section in a 5-column grid left four empty tracks
     on its last row rendering as a single borderless white slab ~250px
     tall, which read as the layout having broken. Painting per tile
     instead means trailing space is simply page background, and a last
     row holding one tile is just one tile. */
  background: transparent;
  /* Uniform tile height in addition to the equal 1fr columns
     ("asigna espacios uniformes en altura y ancho"). 248px is measured,
     not picked: the tallest tile's content needs 245px (Tech Performance
     and Inspections on Park -- label + value + delta + caption + either a
     top-3 list or a sparkline). minmax() rather than a flat 248px so a
     tile whose content ever outgrows that pushes its row taller instead
     of being clipped by the tile's own overflow:hidden; in practice every
     row resolves to exactly 248px. `1fr` was wrong here -- it sizes every
     row to the tallest row in the WHOLE grid, so a trailing row holding
     one sparse tile inherited the full height of a row full of charts.
     2026-08-27, once this grid became every page's KPI row and not just
     the Dashboard's: 248px was measured against the Dashboard's own
     richest tile (label + value + delta + caption + a top-3 list or a
     sparkline) and is far too tall for a tile carrying none of that.
     Re-measured across all nine pages, the tallest tile in any row now
     needs 203px (Dashboard) while an ordinary page tile needs 108-147px
     -- so a fixed 248 left 100-140px of dead space under every number on
     Work Orders, Parts, Issues, Financials and Bookings. That is the
     vertical form of the exact rule this layout exists to enforce: a stat
     never stretches to fill its container. The floor is 150px now (enough
     that a bare label+value tile still reads as a tile rather than as a
     table row) and every row above it sizes to its own tallest tile --
     rows stay uniform because that is what a grid row does, without any
     row being padded out to a height nothing in it needs. */
  grid-auto-rows: minmax(150px, auto);
}

/* Few tiles: track count follows the CONTENT, not the viewport (2026-09-01).
   auto-fill sizes tracks from the window, so a 1610px row always made five
   300px tracks -- a section with three tiles filled 967px and left 643px
   (40%) blank, and one with two left 60%. Measured on Financials, Bookings
   and Issues; it reads as a broken layout, not as a deliberate ragged edge.
   The fix is NOT to let them stretch: rule A exists because a stat spread
   across 757px to show 200px of content is exactly what this grid replaced.
   So the column count matches the tile count, and the track is capped.
   420px comes from the measurement, not taste: across five pages the median
   tile's intrinsic width is 242px and the 90th percentile is 371px, so 420
   clears almost every tile without letting any become a lane. (One outlier,
   Tire Rotation, wants 739px for a long caption that wraps regardless.)
   :has(> :last-child:nth-child(N)) is the "exactly N children" test. */
.dash-grid:has(> :last-child:nth-child(2)) { grid-template-columns: repeat(2, minmax(300px, 420px)); }
.dash-grid:has(> :last-child:nth-child(3)) { grid-template-columns: repeat(3, minmax(300px, 420px)); }
.dash-grid:has(> :last-child:nth-child(4)) { grid-template-columns: repeat(4, minmax(300px, 420px)); }
/* Square, not rounded (2026-08-26: "no le pongas esos redondeos raros a
   las que eran tabs"). A radius clipped the four corner tiles into
   rounded shapes while every interior tile stayed square, so the corners
   read as a different component than their neighbours. A data grid is a
   table; tables have square cells.
   Each tile carries all four borders and pulls its right/bottom edge back
   by 1px, so a shared edge between two tiles collapses into a single
   hairline instead of doubling. */
.dash-grid > * {
  background: var(--surface-white); min-width: 0;
  border: 1px solid var(--border);
  margin: 0 -1px -1px 0;
}

/* A tile. Same markup kpi() has always produced -- it just stops being a
   card: no border, no radius, no shadow, no hover lift. The lattice
   around it is the only structure. */
.dash-grid .kpi-card {
  /* border deliberately NOT reset here -- .dash-grid > * above owns the
     tile's hairline, and this selector is more specific, so a `border:
     none` here would silently erase it. */
  background: var(--surface-white); box-shadow: none; border-radius: 0;
  /* 12px horizontal, 18px vertical -- not a stylistic choice. The section
     heading's text starts 13px in from the section's left edge (.bar's
     4px + .section-title's 9px gap); a tile's content started at 19px
     (1px border + 18px padding), so every tile's label sat 6px right of
     the heading above it -- the offset called out 2026-08-26 ("el ancho
     de lo de abajo tiene que estar alineado con el ancho del titulo, hay
     un desfase ahi"), clearly visible at 3x zoom. 1px border + 12px
     padding = 13px, so tile content and heading text now share one left
     edge. Verified in the DOM, not by eye. */
  padding: 18px 12px; overflow: hidden; animation: none;
  display: flex; flex-direction: column; align-items: stretch; gap: 0;
}
.dash-grid .kpi-card.kpi-clickable { transition: background .15s var(--ease); }
.dash-grid .kpi-card.kpi-clickable:hover { background: #f7f8fa; transform: none; box-shadow: none; }
.dash-grid .kpi-card.kpi-clickable:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }

/* Rule 2: label, value, delta and source badge are ONE cluster. The
   badge lives in the tile's own header row, which is now at most a tile
   wide (272-380px) instead of a 757px lane -- so the 467-562px void
   between a label and its source logo cannot recur by construction. */
.dash-grid .kpi-card .kpi-head { flex: none; gap: 4px 8px; margin-top: 0; }
.dash-grid .kpi-card .kpi-value { margin-top: 7px; }
.dash-grid .kpi-card .kpi-delta-row { margin-top: 6px; }
.dash-grid .kpi-card .kpi-foot { margin-top: 6px; }
.dash-grid .kpi-card .kpi-icon {
  width: 13px; height: 13px; margin: 0 7px 0 0; padding: 5px; flex: none;
  box-sizing: content-box; background: rgba(21, 21, 26, .05); border-radius: 8px;
}

/* Rule 3: a chart keeps a sane aspect. Pinned to the bottom of the tile
   (margin-top:auto) so that in a grid row where one tile is taller, the
   trend lines still sit on a shared baseline instead of floating at
   different heights. At a 300-380px track this lands around 6:1 to 8:1,
   against the 20:1 that made every line look flat and decorative. */
.dash-grid .kpi-card .kpi-spark-wrap { margin-top: auto; padding-top: 10px; }
.dash-grid .kpi-card .kpi-spark { height: 46px; }
.dash-grid .kpi-card .kpi-segbar-wrap,
.dash-grid .kpi-card .kpi-breakdown,
.dash-grid .kpi-card .kpi-reallist,
.dash-grid .kpi-card .kpi-minibar-block,
.dash-grid .kpi-card .kpi-meter-block { margin-top: 9px; }
.dash-grid .kpi-card .kpi-reallist,
.dash-grid .kpi-card .kpi-minibar-block,
.dash-grid .kpi-card .kpi-meter-block { padding-top: 9px; }

/* No accent edge bar on ANY tile (2026-08-26: "quita esas rayas rojas de
   algunas tabs"). It rendered on every card originally (green for
   accent-ok, grey by default); trimming it to critical-only still left
   red stripes scattered across the grid. The exceptions now live in the
   Fleet Brief's queue at the top of the page, which is the one place
   that is supposed to carry them -- a second, quieter copy of the same
   signal down here just speckles the grid. A critical value still reads
   heavier via .kpi-value.critical. */
.dash-grid .kpi-card::after { display: none; }

@media (max-width: 560px) { .dash-grid { grid-template-columns: 1fr; } }

/* =====================================================================
   Fleet Status tiles
   =====================================================================
   2026-08-26: "el fleet status no cuadra con lo de abajo que es diseño de
   cuadros rectangulares" + "lo de la derecha se ve muy vacio". The brief
   used to be its own language -- two borderless panels floating on the
   page ground, split by a hairline, the queue rendered as wide rows whose
   short text left several hundred px of empty space before the chevron.
   It is built from the page's own pieces now: a .dash-section heading over
   a .dash-grid, with the queue items as ordinary kpi() tiles. Same border,
   same 248px height, same column tracks as Maintenance and Business, so
   the vertical rules line up all the way down the page.
   Every tile occupies exactly one track; nothing spans. */

/* =====================================================================
   Fleet overview strip
   =====================================================================
   2026-08-26: "que eso sea un ligero overview de todo, que no pertenezca a
   los tabs de abajo del dashboard, tengo que ver esa info un poquito
   mejor."
   Deliberately NOT the tile language. The sections below are bordered
   rectangles in a ruled grid; this sits directly on the page ground with a
   single hairline closing it off, so it reads as a page-level summary
   rather than one more section of the same kind. No surface, no border, no
   card.
   It is also much denser than what it replaces: a full-width band plus
   four 248px tiles (~420px) restating five numbers became one row of
   roughly a third the height, which is what "verlo un poquito mejor"
   actually needed -- the figures are closer together, so they can be
   compared at a glance instead of scanned across a grid. */
/* Small column gap so the photo sits close to the figure it illustrates
   ("acorta el espacio entre la imagen y lo de la derecha"); the metrics
   block re-establishes its own separation via margin-left below, so
   tightening here does not run the two halves together. */
.fleet-overview {
  display: flex; align-items: flex-start; gap: 18px 14px; flex-wrap: wrap;
  padding-bottom: 20px; margin-bottom: 24px;
  border-bottom: 1px solid var(--border);
}
/* A manufacturer studio photo on a white seamless, and a JPEG, so it has
   no alpha at all -- dropped straight onto the page's light-grey ground it
   would render as a white rectangle around the vehicle. multiply blends
   that white (and the soft contact shadow under the tyres) into the page
   instead of committing a hand-cut cutout of the asset.
   Sized by width with height:auto because the sources are landscape;
   forcing a square box would letterbox them.
   The left 11% is clipped away and the same amount reclaimed with a
   negative margin ("corta un poco por la izquierda lo del utv para que no
   se mande a la derecha todo"): both studio shots carry a band of empty
   seamless on that edge, so the vehicle appeared to float inward and
   pushed the whole strip right. clip-path alone would hide the band but
   still occupy its width -- the negative margin is what actually moves the
   row back. Applied as a percentage so it holds at any .fo-mark width and
   for either dashboard's photo. */
.fo-mark {
  --fo-w: 156px;
  flex: none; width: var(--fo-w); height: auto; object-fit: contain;
  mix-blend-mode: multiply; align-self: center;
  /* --fo-crop is a unitless fraction set per photo (see briefHtml). The
     clip hides the blank band; the matching negative margin is what
     actually pulls the row back left, since a clipped area still occupies
     its width. Both derive from --fo-w, so the crop stays correct at every
     breakpoint without a second hardcoded pixel value. */
  clip-path: inset(0 0 0 calc(var(--fo-crop, 0) * 100%));
  margin-left: calc(-1 * var(--fo-crop, 0) * var(--fo-w));
}
.fo-status { flex: 1 1 420px; min-width: 0; }
.fo-figure-row { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; }
.fo-figure { font-size: 34px; font-weight: 800; color: var(--ink); letter-spacing: -1.2px; line-height: 1; }
.fo-figure-unit { font-size: 15px; font-weight: 700; color: var(--muted); margin-left: 2px; letter-spacing: 0; }
.fo-figure-meta { font-size: 12.5px; color: var(--muted); font-weight: 600; }
.fo-figure-meta b { color: var(--text-dim); font-weight: 700; margin-left: 6px; }

/* Segmented status bar. Square ends, matching the page's corner language;
   a 2px gap between fills is dataviz's spacer rule; min-width keeps a
   1-unit segment visible instead of collapsing to nothing. */
.status-bar { display: flex; gap: 2px; height: 30px; overflow: hidden; }
.status-seg { min-width: 4px; display: flex; align-items: center; padding: 0 10px; overflow: hidden; }
.status-seg-label { font-size: 11px; font-weight: 700; color: #fff; white-space: nowrap; letter-spacing: .3px; }
/* Every segment is named here, including the ones too small or too light
   to hold an inline label -- identity is never colour-alone. */
.status-bar-legend { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 10px; }
.status-legend-item { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; }
.status-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.status-legend-label { color: var(--text-dim); font-weight: 600; }
.status-legend-count { font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }

/* Exceptions carry the sections' card treatment -- white surface, square
   1px border -- while the availability bar to their left stays plain on
   the page ground. That split is the point (2026-08-26: "que se vean como
   las tarjetas de abajo... para que se note que son metricas y no
   estatus"): within one strip, the left half is fleet STATE and the right
   half are countable METRICS, and giving only the metrics the card
   language says so without a word of explanation.
   Deliberately smaller than a .dash-grid tile (no fixed 248px height, no
   icon chip) so the strip still reads as a page-level summary rather than
   a fourth section. */
.fo-exceptions { flex: 0 1 auto; display: flex; flex-wrap: wrap; gap: 10px; margin-left: auto; padding-left: 16px; }
/* Taller than the three lines need (2026-08-26: "aumente el alto de esas
   tarjetitas") -- the extra air is what lets a card of small type hold its
   own beside the availability bar. The height comes from min-height rather
   than padding so the content still starts at the top edge ("alinee los
   contenidos arriba de las cajitas") and the slack falls to the bottom,
   which keeps the three numbers on one line across cards. Type sizes and
   widths stay put so the strip's rhythm is unchanged. */
.fo-exc {
  display: flex; flex-direction: column; gap: 2px;
  min-height: 129px;
  padding: 16px 16px 17px; color: inherit; cursor: pointer;
  background: var(--surface-white);
  border: 1px solid var(--border);
  transition: background .15s var(--ease);
}
.fo-exc:hover { background: #f7f8fa; }
.fo-exc:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }
/* The live alert -- a unit moving with no reservation -- is the only item in
   this strip that is happening RIGHT NOW; the others are backlogs that will
   still be there tomorrow. So it gets the tinted rectangle while they keep
   the plain white one, which is also what stops the strip from going mostly
   red: three of these four tiles are sev-critical, and tinting all of them
   would make the urgent one no easier to spot than the rest.
   Same --critical-soft over --brand-red as .live-alert-card on the GPS page,
   deliberately: it is the same alert, so it should look like it. Hover has
   to be restated because .fo-exc:hover above would otherwise wash the tint
   back to grey. */
.fo-exc.is-live { background: var(--critical-soft); border-color: var(--brand-red); }
.fo-exc.is-live:hover { background: #f8d8d8; }
.fo-exc.is-live .fo-exc-note { color: var(--text-dim); }
/* tabular figures so the counts line up as a row of comparable numbers */
.fo-exc-n {
  font-size: 25px; font-weight: 800; color: var(--ink); line-height: 1.1;
  letter-spacing: -.6px; font-variant-numeric: tabular-nums;
}
/* The severity marker is a dot on the label, not a bar on a card -- this
   isn't a tile, so it doesn't reintroduce the "rayas rojas en las tabs"
   that were removed from the sections below. */
.fo-exc-label {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 700; color: var(--text-dim); white-space: nowrap;
}
.fo-exc-label::before { content: ''; width: 6px; height: 6px; border-radius: 50%; flex: none; background: var(--neutral); }
.fo-exc.sev-critical .fo-exc-label::before { background: var(--critical); }
.fo-exc.sev-warn .fo-exc-label::before { background: var(--warn); }
.fo-exc-note { font-size: 11px; color: var(--muted); white-space: nowrap; }
.fo-clear {
  display: flex; align-items: center; gap: 9px;
  font-size: 14px; font-weight: 700; color: var(--ok); padding-top: 6px;
}
.fo-clear svg { width: 18px; height: 18px; flex: none; }
@media (max-width: 900px) {
  .fleet-overview { gap: 16px 20px; }
  .fo-mark { --fo-w: 112px; }
  .fo-exceptions { flex-basis: 100%; margin-left: 0; padding-left: 0; }
}

/* ---------- inline SVG line chart (Weekly Spend trend) — zero-dependency,
   built by hand instead of pulling in a chart library for one time-series. */
.linechart-wrap { position: relative; }
.linechart-wrap svg { display: block; width: 100%; height: auto; overflow: visible; }
.linechart-grid line { stroke: var(--border); stroke-width: 1; }
/* 2026-08-04 product feedback: the axis labels ("2"/"1"/"0", "May 1"/"Jun 14")
   read as flat/gray "ghost text" at the old 10.5px/400-weight/--muted combo —
   asked for "a middle ground" between that and the old condensed display
   font (which is why this isn't just bumping size). Poppins Semibold at a
   touch larger size plus --text-dim (a full step darker than --muted) gives
   the numbers real presence without going back to a heavy/condensed look. */
.linechart-axis-label { font-family: var(--font-sans); font-weight: 600; font-size: 11.5px; fill: var(--text-dim); }
.linechart-line { fill: none; stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.linechart-area { opacity: .1; }
.linechart-dot { fill: var(--surface-white); stroke-width: 2.5; cursor: pointer; }
.linechart-dot:hover, .linechart-dot.active { r: 5; }
.linechart-hit { fill: transparent; cursor: pointer; }
.linechart-tooltip {
  position: absolute; pointer-events: none; z-index: 5; background: var(--ink); color: #fff;
  font-size: 11.5px; font-weight: 600; padding: 6px 10px; border-radius: 6px; transform: translate(-50%, -100%);
  white-space: nowrap; opacity: 0; transition: opacity .12s var(--ease); box-shadow: 0 4px 12px rgba(0,0,0,.2);
}
.linechart-tooltip.visible { opacity: 1; }
.linechart-tooltip .tt-sub { font-weight: 400; color: var(--muted); font-size: 10.5px; }

/* Scatter/dot variant (Financials "Profit Over Time") — reuses .linechart-wrap/
   -grid/-axis-label/-hit/-tooltip verbatim (same axes/tooltip plumbing), but
   points are filled solid (not the white-centered line-chart tick) since
   there's no connecting line/area, and each dot's own color (--ok/--critical)
   carries the positive/negative-profit signal instead of one shared color. */
.dotchart-dot { stroke: none; cursor: pointer; }
.dotchart-dot:hover, .dotchart-dot.active { r: 6; }
.dotchart-zero-line { stroke: var(--text-dim); stroke-width: 1.5; stroke-dasharray: 3 3; }

/* ---------- inline SVG bar chart (Vehicle Detail's Rental Frequency + $
   Generated) — reuses the line chart's grid/axis-label/tooltip/hit-rect
   classes above (same visual language, same tooltip box). Back to bars per
   2026-08-04 product feedback ("barras nomás, está bien"), replacing the
   dots+line experiment from the previous iteration — see usageBarChart() in
   vehicle.js. Two color variants so the two side-by-side charts (counts vs
   dollars) read as distinct series: --cat-1 (chart blue, already the site's
   default "just a series" color — see financials.js's lineChart()) for
   Rental Frequency's counts, --cat-3 (chart green) for $ Generated, an
   intuitive "money = green" association that's still a category-chart
   token, not --ok/--brand-red, so it never gets read as a status pill. */
/* 2026-08-04 product feedback ("un poquito más colorido"): bars at .85
   opacity read as washed-out next to the fuller --cat-1/--cat-3 tokens used
   elsewhere on the site (Fleet by Location bars, etc). Bumped to full
   opacity so the blue/green reads with real body; hover/active no longer
   needs its own opacity step since there's nowhere higher to go — it gets a
   subtle brightness bump instead to still register as a distinct state. */
.usage-bar { opacity: 1; cursor: pointer; transition: filter .12s var(--ease); }
.usage-bar.zero { fill: var(--border); }
.usage-bar:hover, .usage-bar.active { filter: brightness(1.15); }
.usage-bar-count { fill: var(--cat-1); }
.usage-bar-money { fill: var(--cat-3); }

/* Rental Frequency card (vehicle.js) stretches its chart to fill the full
   height of the card instead of leaving dead space below a short fixed-height
   SVG — the card's own height already flexes to match the 3D viewer next to
   it (see .usage-card/.viewer3d-tall), so the chart needs to flex with it
   rather than assume a fixed pixel height. min-height:0 at each flex level
   is required so the flex children can actually grow past their intrinsic
   content size instead of the browser defaulting to auto/content-based
   sizing.
   2026-08-04 product feedback ("el gráfico está comprimido"): verified in a
   real browser (not just reasoning about the CSS) that the flex-stretch
   chain above already works correctly on desktop widths — the SVG does
   fill 100% of .usage-chart-fill's height there (confirmed via
   getBoundingClientRect, ~323px of a ~448px card). The actual bug only
   shows up once .vehicle-top-grid collapses to a single column below
   1000px (see that media query above): the map card and this right-hand
   column stop being same-row grid siblings, so they stop cross-stretching
   to match each other's height, and this whole column's height becomes
   "however tall its own content wants to be" — which, because
   .usage-chart-fill/.usage-chart-wrap both have min-height:0 (needed for
   the desktop stretch case) and no floor of their own, measured at a
   collapsed ~51px in a real render, i.e. the chart happily shrinks to
   almost nothing once nothing above is forcing it taller. flex-direction
   was also missing on .usage-chart-fill (relying on default `row` +
   align-items:stretch to hand its single child a cross-axis/height size) —
   harmless either way in testing, but made the rule read as fragile, so
   made explicit here too. The real fix is the min-height floor below: it
   guarantees a legible chart height in the collapsed/no-shared-row case
   without touching the desktop case (which already exceeds the floor).

   2026-08-04 follow-up ("sigue comprimido"): the 230px floor above was
   itself the bug, not just the CSS that led to it. Verified with Playwright
   against the live preview across real laptop viewport widths (not just
   the 320/768/1440 stock breakpoints) that the 1000px collapse of
   .vehicle-top-grid is not a rare narrow-phone case — sidebar is a fixed
   246px, so a common 1280px laptop viewport only has ~1034px of content
   width to begin with, and anything with the browser not maximized
   (1024-1366px range, very common) lands at or below the 1000px collapse
   point as its normal, everyday state. So the 230px floor was the ANSWER
   most laptop users actually saw, and it read as small. Bumped to 300px —
   confirmed via Playwright screenshots at 900/999/1024/1200px that this no
   longer feels like a token/placeholder chart height. */
.usage-card { min-height: 0; }
/* Header chrome above the chart (section title + sub-copy) trimmed via
   vehicle.js's own inline margins on those two elements — see the
   Rental Frequency block there — so more of the card's height (which
   flexes to match the map/viewer next to it) reads as actual chart, not
   preamble. Matters most for vehicles with little/no rental activity in
   the 90-day window, where the plotted line is mostly flat/zero and a tall
   header on top of it made the whole card feel like an oversized empty box
   (2026-08-04 product feedback). Chart data/window logic is unchanged. */
.usage-chart-fill { flex: 1; display: flex; flex-direction: column; min-height: 300px; }
.usage-chart-wrap.linechart-wrap { flex: 1; display: flex; flex-direction: column; min-height: 0; width: 100%; }
.usage-chart-wrap.linechart-wrap svg { flex: 1; width: 100%; height: 100%; min-height: 0; }

/* segmented-pill tab control (visually distinct from a plain underline tabbar) */
.tabbar {
  display: inline-flex; gap: 3px; background: var(--surface); border-radius: 999px;
  padding: 4px; margin-bottom: 22px; overflow-x: auto; border: 1px solid var(--border);
}
.tabbar button {
  background: none; border: none; border-radius: 999px; padding: 9px 18px;
  font-size: 12.5px; font-weight: 600; color: var(--muted); white-space: nowrap;
  transition: background .16s var(--ease), color .16s var(--ease);
}
.tabbar button.active { color: #fff; background: var(--ink); }

/* Subtle crossfade for content that swaps "of a sudden" — tab content
   (vehicle.js Overview/Service/Inspection/Financials, marketing.js's
   store/Daily-Cumulated chart), applied via fadeSwap() in app.js. Same
   timing/easing as every other transition on the site, just on opacity. */
.fade-swap { transition: opacity .16s var(--ease); }
.fade-swap.is-fading { opacity: 0; }

/* ---------- GPS — full-bleed live tracking view, styled after
   /root/Fleet's Live Tracking layer (dark topbar, floating tool rail + panel) ---------- */
.map-shell { position: relative; border-radius: var(--radius-lg); overflow: hidden; border: 1px solid var(--border); box-shadow: var(--shadow-md); }

/* ---------- Live tracking — ported from /root/Fleet/heatmap.html's Live
   Tracking layer: heading-arrow marker pill, live vehicle cards, moving/
   stopped speed badges. ---------- */
.live-marker-icon { background: transparent; border: none; }
.live-pill { display: flex; align-items: center; gap: 5px; }
.live-pill-arrow { flex-shrink: 0; display: block; filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); }
.live-pill-text {
  display: flex; flex-direction: column; line-height: 1.15; background: rgba(21,21,26,.88);
  padding: 3px 8px; border-radius: 8px; white-space: nowrap;
}
.live-pill-code { font-family: var(--font-sans); font-size: 13px; font-weight: 700; color: #fff; letter-spacing: .2px; }
.live-pill-status { font-size: 10px; font-weight: 500; }

.live-store-group { margin-bottom: 14px; }
.live-store-group:last-child { margin-bottom: 0; }
.live-store-label {
  font-size: 10.5px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase;
  color: var(--muted); margin-bottom: 6px;
}
.live-card {
  display: block; width: 100%; text-align: left; background: var(--surface-white); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 9px 11px; margin-bottom: 6px; font-family: inherit;
}
.live-card:hover { border-color: var(--muted); }
/* A unit with no live GPS ping: present, but dimmed and inert -- it is not
   a button because there is no map position to focus. Ported 2026-08-27. */
.live-card-unavailable { cursor: default; opacity: .55; }
.live-card-unavailable:hover { border-color: var(--border); background: none; }
.live-card.active { background: var(--critical-soft); border-color: var(--brand-red); }
.live-card-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.live-card-name { font-size: 12.5px; font-weight: 700; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.live-card-speed { font-size: 10.5px; font-weight: 700; padding: 2px 8px; border-radius: 999px; flex: none; }
.live-card-speed.moving { background: var(--ok-soft); color: var(--ok); }
.live-card-speed.stopped { background: var(--neutral-soft); color: var(--muted); }
.live-card-seats { font-size: 11px; color: var(--muted); margin-top: 4px; }
.live-card-renter { margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border); display: flex; flex-direction: column; gap: 4px; }
.live-card-renter-row { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--text-dim); }
.live-card-renter-row .icon { color: var(--brand-red); flex: none; }
.live-card-renter-row a { color: var(--text-dim); text-decoration: none; }
.live-card-renter-row a:hover { color: var(--brand-red); text-decoration: underline; }
.live-card-renter-name { font-weight: 600; }
.live-card-renter-row .overdue { color: var(--critical); font-weight: 700; }

/* Speeding/aggressive-driving alerts feed — lives INSIDE the same
   float-panel as the Live Vehicles list (design rule 7: one floating
   element per zone, not a second loose box next to it), as a section
   between the panel title and the vehicle list. Live-only — see the
   comment on ALERT_WARN_MPH/ALERT_CRITICAL_MPH in gps.js for why there's no
   historical alert log here. Capped height + its own scroll so a busy
   moment (many units alerting at once) can't push the vehicle list below
   the fold. */
.live-alerts { flex: none; max-height: 190px; overflow-y: auto; margin-bottom: 14px; }
.live-alerts-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.live-alerts-head span:first-child {
  font-size: 10.5px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; color: var(--muted);
}
.live-alerts-count {
  font-size: 10.5px; font-weight: 700; padding: 1px 7px; border-radius: 999px;
  background: var(--critical-soft); color: var(--critical);
}
.live-alerts-empty { font-size: 11.5px; color: var(--muted); margin: 0; }
.live-alert-card {
  display: block; width: 100%; text-align: left; border-radius: var(--radius-sm);
  padding: 8px 10px; margin-bottom: 6px; font-family: inherit; border: 1px solid var(--border);
  background: var(--surface-white);
}
.live-alert-card:last-child { margin-bottom: 0; }
.live-alert-card:hover { border-color: var(--muted); }
.live-alert-card.level-warn { background: var(--warn-soft); border-color: var(--warn); }
.live-alert-card.level-critical { background: var(--critical-soft); border-color: var(--brand-red); }
.live-alert-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.live-alert-name { font-size: 12.5px; font-weight: 700; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.live-alert-meta { font-size: 10.5px; color: var(--muted); margin-top: 3px; }

/* The live vehicle panel is the map's only floating control now (the old
   tool-rail toggle made sense back when there were multiple panels to
   switch between; with just this one, always-visible panel, a separate
   button to show/hide it was one more disconnected floating box and dead
   space for no reason — so it's just always here, no toggle). It carries
   renter/seat detail, so it needs more room than a plain filter panel, and
   should stretch with the taller full-bleed map instead of stopping short
   partway down it. */
.float-panel.live-panel { width: 300px; display: flex; flex-direction: column; }
.float-panel.live-panel #liveVehicleList { flex: 1; overflow-y: auto; }
/* GPS has no page-title up top like Dashboard/Financials/etc. do
   (it's full-bleed, the map IS the page) — this panel is the only large
   text on the page, so it carries the page's own title, at the exact same
   size/spacing as every other page's .page-title (see design rules at the
   top of this file), not a smaller section label. */
.live-panel-title { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 20px; }
.live-panel-title .page-title { margin-bottom: 0; }

/* Fills essentially the whole viewport below the topbar/breadcrumb — the
   point of the full-bleed layout above is that this map IS the page. */
#map { height: 100%; min-height: 320px; }

/* The one place on the site this effect is closest to what HIG actually
   describes -- a control genuinely floating over real content (the map),
   with the map's own colors showing through the blur as it pans. Regular
   variant, on the more opaque end (.78) since it carries renter/phone
   detail text, not just an icon. */
.float-panel {
  position: absolute; top: 16px; left: 16px; z-index: 500; display: none;
  width: 260px; max-height: calc(100% - 32px); overflow-y: auto;
  background: linear-gradient(165deg, rgba(255,255,255,.85) 0%, rgba(238,240,242,.87) 100%);
  backdrop-filter: blur(20px) saturate(1.15);
  -webkit-backdrop-filter: blur(20px) saturate(1.15);
  border: 1px solid var(--border); border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg); padding: 14px;
}
.viewer3d { height: 260px; border-radius: var(--radius-md); background: linear-gradient(180deg, var(--surface) 0%, var(--surface-white) 100%); border: 1px solid var(--border); position: relative; }
/* Vehicle Detail's "Vehicle View"/"3D View" card: taller + narrower than
   the site-wide default .viewer3d box, so the vehicle reads as the visual
   anchor of the page instead of splitting the header 50/50 with the KPI
   row. Shared by both the procedural/GLTF viewer and the photo carousel,
   same as the base height above. */
.viewer3d-tall { height: 500px; }
@media (max-width: 900px) { .viewer3d-tall { height: 320px; } }
.viewer3d canvas { border-radius: var(--radius-md); }
.viewer3d .viewer-hint { position: absolute; bottom: 8px; right: 12px; font-size: 10.5px; color: var(--muted); }
.viewer3d .viewer-placeholder-badge { position: absolute; top: 8px; left: 10px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); background: var(--surface-white); padding: 3px 8px; border-radius: 999px; border: 1px solid var(--border); }

/* Real-photo "fake 360" carousel for Vehicle Detail (see modelPhotos.js) —
   shares .viewer3d's box (size/border/gradient) and its badge/hint classes
   above, just swaps the canvas for an <img> + prev/next controls so the
   two modes feel like one system, not two different widgets. */
.viewer3d.photo-viewer { overflow: hidden; cursor: grab; touch-action: pan-y; }
.viewer3d.photo-viewer.is-dragging { cursor: grabbing; }
.photo-viewer-img { width: 100%; height: 100%; object-fit: contain; display: block; user-select: none; -webkit-user-drag: none; }
.photo-viewer-nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 32px; height: 32px; border-radius: 999px; display: flex; align-items: center; justify-content: center;
  background: var(--surface-white); color: var(--ink); border: 1px solid var(--border); box-shadow: var(--shadow-sm, 0 2px 8px rgba(0,0,0,.08));
  cursor: pointer; transition: background .15s var(--ease);
}
.photo-viewer-nav:hover { background: var(--surface); }
.photo-viewer-prev { left: 10px; }
.photo-viewer-next { right: 10px; }

.empty-state { text-align: center; padding: 60px 20px; color: var(--muted); }
.empty-state h3 { font-size: 18px; margin-bottom: 8px; color: var(--text-dim); }

/* table rows used as a store-group header (Parts page — design rule 8:
   vehicle/part lists group by store, never a flat list across locations) */
tr.table-group-row { cursor: default; }
tr.table-group-row:hover { background: transparent; }
tr.table-group-row td {
  background: var(--surface); font-family: var(--font-sans); font-weight: 700; font-size: 11.5px;
  letter-spacing: .4px; text-transform: uppercase; color: var(--text-dim); padding: 9px 14px;
}

.loading-bar { height: 2.5px; background: var(--brand-red); position: fixed; top: 0; left: 0; z-index: 999; transition: width .3s var(--ease); box-shadow: 0 0 8px var(--brand-red-glow); }

/* ---------- Marketing — store tabs (color-coded per store identity, same
   separate-palette reasoning as --brand-* vehicle-make chips — never
   --brand-red, which stays reserved for critical state + primary CTA) ---------- */
.store-tabs { display: flex; gap: 2px; flex-wrap: wrap; margin-bottom: 18px; border-bottom: 1px solid var(--border); }
.store-tab {
  display: inline-flex; align-items: center; gap: 7px; background: none; border: none;
  border-bottom: 3px solid transparent; padding: 10px 14px; font-size: 12.5px; font-weight: 600;
  color: var(--muted); font-family: var(--font-sans); cursor: pointer;
  transition: color .15s var(--ease), border-bottom-color .15s var(--ease);
}
/* Active tab: ink text over the app's own red rule, the same mark every
   other active thing in this app carries. It used to be a per-store
   hue piped in through --tab-color. */
.store-tab.active { color: var(--ink); font-weight: 700; border-bottom-color: var(--brand-red); }

.revenue-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 18px; }
.revenue-range { display: flex; gap: 14px; flex-wrap: wrap; }
.revenue-range label { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--muted); }
.revenue-range input[type="date"] {
  border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 7px 10px; font-size: 12.5px;
  font-family: var(--font-sans); color: var(--text-dim); background: var(--surface-white);
}
.revenue-range input[type="date"]:focus { outline: none; border-color: var(--brand-red); }

/* Multi-year overlay line chart — reuses .linechart-* (grid/axis-label/
   tooltip) from the Weekly Spend chart above; these are the extra pieces
   specific to year-over-year revenue: several stroke colors on one chart,
   translucent holiday-range bands, and a "Today" cutoff marker. */
.revenue-line { stroke-width: 2.25; fill: none; stroke-linejoin: round; stroke-linecap: round; }
.holiday-band { fill: color-mix(in srgb, var(--holiday-band) 7%, transparent); }
/* Alternating baselines: consecutive holidays sit only days apart, so at
   a year's width their labels overlapped into unreadable mush
   ("Presidents'" printed straight through "Valentine's"). Odd ones drop
   a line instead of being dropped altogether. */
.holiday-band-label { font-family: var(--font-sans); font-size: 9.5px; fill: var(--muted); }
.holiday-band-label:nth-of-type(even) { transform: translateY(11px); }
.today-line { stroke: var(--brand-red); stroke-width: 1.5; stroke-dasharray: 4 3; }
.today-label { font-family: var(--font-sans); font-size: 10.5px; font-weight: 700; fill: var(--brand-red); }
.revenue-hit { fill: transparent; cursor: crosshair; }

.legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: 12px; color: var(--muted); margin-top: 10px; }
.legend .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 5px; }

.back-link { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--muted); margin-bottom: 14px; font-weight: 600; }
.back-link:hover { color: var(--ink); }

.detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; }
.detail-header .meta { display: flex; gap: 18px; flex-wrap: wrap; font-size: 13px; color: var(--muted); margin-top: 6px; }
/* Vehicle Detail's "#ID / model name / [logo]" line — the vehicle's own
   fleet ID (e.g. "#116") IS this page's .page-title now (same 34px Big
   Shoulders weight as every other page's title, per design rule 3c: exactly
   one .page-title, full hierarchy) — 2026-08-05 product request: the ID is
   what identifies THIS specific unit, so it carries the page-title weight;
   the model/trim name (which can also be a long factory trim string, see
   .vehicle-model-sub below) is now a smaller, muted subtitle underneath it
   instead of competing for page-title-level weight. It still also appears
   in .meta ("Vehicle #116") for anyone scanning that row instead of the
   title — left alone, not worth de-duplicating for one repeated token.
   Up to 3 items sit in the row (title group / thumbnail? / brand logo) —
   the title group carries the flex so it still pushes the logo to the far
   right; `justify-content: space-between` would instead spread equal gaps
   across all 3 once a thumbnail is added, pulling the title away from the
   thumb. */
.vehicle-identity { display: flex; align-items: center; gap: 16px; }
.vehicle-title-group { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
.vehicle-title-group .page-title { margin-bottom: 0; }
.vehicle-model-sub {
  font-family: var(--font-sans); font-size: 20px; font-weight: 600; color: var(--muted); line-height: 1.25;
}
.detail-header .meta b { color: var(--text-dim); font-weight: 600; }

/* Small identity thumbnail next to the brand logo — real photo only (see
   vehicle.js's comment on `thumbPhoto` for why placeholder-3D vehicles skip
   this instead of showing a shrunk procedural silhouette). Deliberately
   inside its own bordered/rounded box, no bleed past its frame — this is a
   dense data tool, not a marketing landing page. */
.vehicle-thumb {
  flex: none; width: 96px; height: 64px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--surface); overflow: hidden;
  box-shadow: 0 1px 3px rgba(0,0,0,.06);
}
.vehicle-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Groups the photo thumb + brand logo into one identity card instead of
   two independently-framed/independently-sized things sitting loose next
   to each other -- direct feedback that the previous mobile fix (each
   wrapping onto its own line) still "se ve desordenado." One shared
   border/shadow/background reads as a single grouped element; the thumb
   drops its own border/shadow (nested double-framing looks worse, not
   better) and the logo gets a divider instead, so the whole thing scans
   as "the vehicle's photo ID," not two unrelated floating badges. */
.vehicle-media {
  display: flex; align-items: center; flex: none;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-white); overflow: hidden;
  box-shadow: 0 1px 3px rgba(0,0,0,.06);
}
.vehicle-media .vehicle-thumb { border: none; box-shadow: none; background: none; }
.vehicle-media .brand-logo, .vehicle-media .brand-badge { margin: 0 14px; }
.vehicle-media .brand-logo:not(:only-child), .vehicle-media .brand-badge:not(:only-child) { padding-left: 14px; border-left: 1px solid var(--border); margin-left: 0; }

/* Fleet roster table — same idea as .vehicle-thumb above but sized for a
   dense table row, not a page header: a light/small identity glance, not
   a showcase. No border/shadow at this size (would read as visual noise
   repeated 150 times down a column) — just the photo, or nothing at all
   for the ~124 models without one (see fleet.js's comment). */
.fleet-row-make { display: flex; align-items: center; gap: 10px; }
.fleet-row-thumb { flex: none; width: 44px; height: 30px; object-fit: cover; border-radius: 4px; }

/* Vehicle Detail's live-location card — shares .viewer3d-tall's height/
   responsive breakpoint so the top grid's row heights don't jump depending
   on whether a vehicle has live GPS, just swaps the gradient placeholder
   look for a real Leaflet map (or the empty state below). */
.vehicle-map { border-radius: var(--radius-md); border: 1px solid var(--border); overflow: hidden; position: relative; transition: box-shadow .2s var(--ease), border-color .2s var(--ease); }
.vehicle-map-empty { display: flex; align-items: center; justify-content: center; background: var(--surface); }
.vehicle-map-empty .empty-state { padding: 0; }
/* "Replaying now" indicator (design rule 6 — brand-red reserved for
   critical/active state) — on from the moment Play is clicked (even while
   the route is still loading) until close(), so the card itself reads as
   "recording" the whole time a rental route is on screen. */
.vehicle-map.is-replaying { border-color: var(--brand-red); box-shadow: inset 0 0 0 2px var(--brand-red), 0 0 16px var(--brand-red-glow); }

/* ---------- Rental route playback (Vehicle Detail → Rentals tab) ----------
   Ported from /root/Fleet/heatmap.html's Rental Sessions player. Pink
   (#f472b6, Tailwind pink-400) is used ONLY for this real-GPS-route
   feature — a deliberate second accent color, distinct from --brand-red
   (reserved for critical state + primary CTA per design rule 6) and from
   --ok/--warn (normal fleet status), so a replayed route never reads as an
   alert or a status pill. */
.session-player-bar {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1000;
  display: none; align-items: center; gap: 10px;
  background: rgba(255,255,255,.96); border-top: 1px solid var(--border);
  padding: 9px 12px; font-family: var(--font-sans); font-size: 12.5px; color: var(--text-dim);
}
.session-player-bar.visible { display: flex; }
.session-player-controls { display: none; align-items: center; gap: 10px; flex: 1; min-width: 0; }
.session-play-btn {
  flex: none; width: 30px; height: 30px; border-radius: 50%; border: none;
  background: var(--ink); color: #fff; display: flex; align-items: center; justify-content: center;
  transition: background .15s var(--ease);
}
.session-play-btn:hover { background: #000; }
.session-progress-bg { flex: 1; height: 5px; background: var(--border); border-radius: 4px; overflow: hidden; cursor: pointer; }
.session-progress-fill { height: 100%; width: 0%; background: #f472b6; border-radius: 4px; }
.session-clock {
  flex: none; min-width: 66px; text-align: right; font-variant-numeric: tabular-nums;
  font-weight: 600; color: var(--text-dim);
}
.session-player-message { flex: 1; color: var(--muted); }
/* Speed-alert legend, shown only while a route is actually playing (never
   empty/permanent) — lives inline in the player bar rather than a second
   floating panel over the map (design rule 4/7). */
.session-speed-legend { display: none; align-items: center; gap: 12px; flex: none; }
.session-speed-legend-item { display: flex; align-items: center; gap: 5px; font-weight: 600; color: var(--text-dim); white-space: nowrap; }
.session-speed-legend-item i { display: inline-block; width: 12px; height: 3px; border-radius: 2px; }
.session-close-btn {
  flex: none; width: 25px; height: 25px; border-radius: 50%; border: 1px solid var(--border);
  background: var(--surface-white); color: var(--muted); font-size: 12.5px; line-height: 1;
  transition: color .15s var(--ease), border-color .15s var(--ease);
}
.session-close-btn:hover { color: var(--ink); border-color: var(--ink); }
/* "Back to live" pill appended into #liveLocationTitle (the Live Location
   card's .section-title) while a rental is playing — not a second floating
   panel (design rule 4/7), just an inline action in the header that's
   already there, same as any other title-row button on the site. */
.live-back-btn {
  font-family: var(--font-sans); font-size: 11.5px; font-weight: 700; color: var(--muted);
  background: var(--surface); border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px;
  margin-left: auto; transition: color .15s var(--ease), border-color .15s var(--ease);
}
.live-back-btn:hover { color: var(--ink); border-color: var(--ink); }
/* Rentals tab row action — pink to match the route it draws, but a table
   button, not a page CTA, so it stays a soft tint rather than a solid fill
   (--brand-red-style prominence is reserved for the one real primary CTA
   per page, which this isn't). */
.session-row-play {
  font-family: var(--font-sans); font-size: 12px; font-weight: 700; color: #be185d;
  background: #fdf2f8; border: 1px solid #fbcfe8; border-radius: var(--radius-sm); padding: 5px 11px;
  transition: background .15s var(--ease);
}
.session-row-play:hover { background: #fce7f3; }

/* ---------- Live Location speedometer (speedGauge.js) — only rendered when
   the card actually has a live GPS snapshot (see vehicle.js); the empty
   state ("No live GPS signal") never gets one. Sits top-right of the mini
   map, offset far enough from the edge to clear Leaflet's own zoom control
   (also top-right, ~26px wide + 10px inset — see mountLiveVehicleMap) so
   the two floating elements never overlap, per design rule 4/7: the map's
   only other floating piece besides the zoom control, carrying real live
   data rather than sitting there as a bare icon. ---------- */
.speed-gauge {
  position: absolute; top: 10px; right: 46px; z-index: 1000;
  background: rgba(255,255,255,.94); border: 1px solid var(--border); border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm); padding: 6px 8px 5px; display: flex; flex-direction: column; align-items: center;
  pointer-events: none;
}
.speed-gauge-track { fill: none; stroke: var(--border); stroke-width: 6; stroke-linecap: round; }
.speed-gauge-progress { fill: none; stroke: var(--neutral); stroke-width: 6; stroke-linecap: round; transition: d .35s var(--ease); }
.speed-gauge.is-moving .speed-gauge-progress { stroke: var(--ok); }
.speed-gauge-needle { stroke: var(--text-dim); stroke-width: 2.25; stroke-linecap: round; transition: x2 .4s var(--ease), y2 .4s var(--ease); }
.speed-gauge.is-moving .speed-gauge-needle { stroke: var(--ink); }
.speed-gauge-pivot { fill: var(--text-dim); }
.speed-gauge-readout { display: flex; align-items: baseline; gap: 3px; margin-top: -6px; }
/* The mph reading is a value, not a heading — Poppins in both the moving
   and "Stopped" states (the latter already used --font-sans; unifying
   here instead of switching moving state away from it, 2026-08-04
   typography pass — a value shouldn't change font family when the state
   changes). */
.speed-gauge-value { font-family: var(--font-sans); font-weight: 700; font-size: 15px; color: var(--ink); letter-spacing: .2px; }
.speed-gauge.is-stopped .speed-gauge-value { font-size: 11px; color: var(--muted); font-family: var(--font-sans); font-weight: 700; }
.speed-gauge-unit { font-size: 9.5px; color: var(--muted); font-weight: 600; }

.leaflet-popup-content { font-family: var(--font-sans); font-size: 13px; }

/* ---------- Availability grid (availability.js) — store x day forward-
   looking free/total counts, each cell a real button (not a plain table
   cell) so clicking one loads the vehicle-level breakdown into the detail
   card below. Tone follows the same good/warn/critical language as .pill,
   scaled up to a bigger tap target since this is the thing the page is
   actually for, not a decoration next to the real content. */
.avail-table th:first-child, .avail-table td:first-child { width: 220px; }
.avail-cell {
  font-family: var(--font-sans); font-size: 13.5px; font-weight: 700; width: 100%;
  border-radius: 0; padding: 9px 10px; border: 1px solid var(--border);
  transition: border-color .15s var(--ease), background .15s var(--ease);
  display: flex; flex-direction: column; gap: 5px; text-align: left;
}
/* Sequential ink ramp: light = idle, dark = most of the fleet out earning.
   One hue, five steps, no status colours -- see occLevel() in
   availability.js for why the old good/warning/critical mapping was not
   just decorative but inverted. Text flips to white on the two darkest
   steps so contrast holds at both ends of the ramp. */
.avail-cell-l0 { background: var(--surface-white); color: var(--muted); }
.avail-cell-l1 { background: #e7eaef; color: var(--text-dim); }
.avail-cell-l2 { background: #c8cedb; color: var(--ink); }
.avail-cell-l3 { background: #6d7488; color: #fff; }
.avail-cell-l4 { background: var(--ink); color: #fff; }
.avail-cell-l3 .avail-occ-track, .avail-cell-l4 .avail-occ-track { background: rgba(255,255,255,.25); }
/* Distinct from l0 by texture, not by another colour: "we have no data" and
   "nobody booked" are different facts and must not look alike. */
.avail-cell-nodata {
  background: repeating-linear-gradient(135deg, var(--surface) 0 6px, var(--surface-white) 6px 12px);
  color: var(--muted); font-weight: 600;
}
.avail-cell.is-selected { border-color: currentColor; }
.avail-cell-free { font-size: 16px; }
.avail-cell-slash { opacity: .7; font-weight: 600; margin-right: 3px; }
/* Grid buttons don't want the whole-row hover tint the base table rules
   apply to every <tr> (that's meant for clickable data rows, not a row that
   just hosts 3 independent buttons) — the buttons already carry their own
   hover/selected state above. */
.avail-table tbody tr:hover { background: transparent; cursor: default; }

/* ---------- 2026-08-07 rewrite: occupancy % + mini progress bar inside
   each grid cell (replaces the old bare "free/total" count) — every cell
   now shows the number that actually drives the color (occupancy), not
   just a derived count, per the "muy soso" feedback that the old grid was
   text-only with no sense of degree. ---------- */
.avail-cell-pct { font-size: 19px; line-height: 1; }
.avail-occ-track { height: 4px; border-radius: 3px; background: rgba(0,0,0,.1); overflow: hidden; }
.avail-occ-fill { height: 100%; background: currentColor; border-radius: 3px; transition: width .3s var(--ease); }
.avail-cell-sub { font-family: var(--font-sans); font-size: 11px; font-weight: 600; opacity: .85; }

/* ---------- Street unit timeline (per-store/day detail) — the main visual
   of the Street section: one row per in-service unit, a 9am-9pm track with
   colored blocks wherever that unit is booked. Replaces the old plain
   free/reserved table — a glance down the column of tracks shows which
   units are busy all day vs. mostly open, which a text table couldn't. */
.avail-timeline-hours { position: relative; height: 14px; margin: 0 0 6px 202px; font-size: 10.5px; color: var(--muted); font-weight: 600; }
.avail-timeline-hours span { position: absolute; transform: translateX(-50%); }
.avail-timeline-hours span:first-child { transform: translateX(0); }
.avail-timeline-hours span:last-child { transform: translateX(-100%); }
.avail-timeline-list { display: flex; flex-direction: column; }
.avail-timeline-row {
  display: flex; align-items: center; gap: 14px; padding: 8px 4px;
  border-bottom: 1px solid var(--border); cursor: pointer; transition: background .12s var(--ease);
}
.avail-timeline-row:hover { background: var(--surface); }
.avail-timeline-row:last-child { border-bottom: none; }
.avail-timeline-unit { flex: 0 0 188px; display: flex; align-items: center; gap: 9px; min-width: 0; }
.avail-timeline-id { font-weight: 700; font-size: 12.5px; }
.avail-timeline-model { font-size: 10.5px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.avail-timeline-track { position: relative; flex: 1; height: 24px; background: var(--surface); border-radius: 5px; overflow: hidden; }
.avail-timeline-seg { position: absolute; top: 0; bottom: 0; background: var(--critical-soft); border-left: 3px solid var(--critical); }
.avail-timeline-badge { flex: 0 0 auto; font-size: 11.5px; font-weight: 700; width: 96px; text-align: right; }
.avail-timeline-badge-ok { color: var(--ok); }
.avail-timeline-badge-warn { color: var(--warn); }
.avail-timeline-badge-critical { color: var(--critical); }

/* ---------- Off-Road Park / ATV Park shift + unit-chip detail ---------- */
.avail-subhead { font-family: var(--font-sans); font-weight: 700; font-size: 12.5px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .4px; margin: 18px 0 8px; }
.avail-subhead:first-child { margin-top: 0; }
.avail-shift-list { display: flex; flex-direction: column; }
.avail-shift-row { display: flex; align-items: center; gap: 12px; padding: 7px 4px; border-bottom: 1px solid var(--border); }
.avail-shift-row:last-child { border-bottom: none; }
.avail-shift-time { flex: 0 0 76px; font-weight: 700; font-size: 12.5px; }
.avail-shift-track { position: relative; flex: 1; height: 14px; background: var(--surface); border-radius: 5px; overflow: hidden; }
.avail-shift-fill { position: absolute; top: 0; left: 0; bottom: 0; border-radius: 5px; }
.avail-shift-fill-ok { background: var(--ok); }
.avail-shift-fill-warn { background: var(--warn); }
.avail-shift-fill-critical { background: var(--critical); }
.avail-shift-count { flex: 0 0 auto; width: 110px; text-align: right; font-size: 11.5px; color: var(--muted); font-weight: 600; }
.avail-unit-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.avail-unit-chip {
  display: flex; align-items: center; gap: 8px; background: var(--surface); border-radius: 6px;
  padding: 6px 12px 6px 6px; font-size: 12px; cursor: pointer; transition: background .12s var(--ease);
}
.avail-unit-chip:hover { background: var(--border); }
.avail-unit-thumb, .avail-unit-thumb-ph { flex: none; width: 36px; height: 25px; border-radius: 4px; background: #fff; object-fit: cover; }
.avail-unit-chip-sub { color: var(--muted); font-size: 10.5px; }

/* =====================================================================
   Mobile (phones) — the site previously only handled tablet/desktop grid
   collapse (1100px/700px breakpoints above). Charts (.linechart-wrap svg)
   and tables (.table-wrap) already scale/scroll on their own via
   viewBox+100% width and overflow-x:auto respectively — nothing to add
   there. The sidebar becomes a full-screen overlay below (see comment
   right above that block) rather than the desktop icon-rail collapse,
   since a permanently-visible narrow rail was still reported as not
   feeling right on a phone -- floating menu, full screen when open, empty
   otherwise, was the actual ask.
   ===================================================================== */

/* Reachable at any width, but only actually visible on phones (below) --
   lives in .topbar-brand, a sibling of .sidebar rather than a child of it,
   specifically so it stays clickable even while the sidebar itself is
   transformed off-screen (a position:fixed descendant of a transformed
   ancestor is positioned relative to THAT ancestor, not the viewport --
   this button had to live outside .sidebar entirely to avoid that trap). */
.mobile-menu-btn {
  display: none;
  width: 34px; height: 34px; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-white); color: var(--ink);
  transition: background .15s var(--ease);
}
.mobile-menu-btn:hover { background: var(--surface); }

@media (max-width: 768px) {
  .mobile-menu-btn { display: flex; }
  /* 2026-08-24 product ask: mobile header "se ve caotico" -- the old
     2-row layout (hamburger+search, then filters right-aligned with a
     big dead gap to their left) plus a separately-chromed
     .breadcrumb-bar row below it read as three disconnected pieces each
     following a different alignment rule, not one organized header.
     Rebuilt as 3 left-aligned rows sharing one flow direction:
     hamburger+search, breadcrumb, filters. `display: contents` on
     .topbar-brand (a flex wrapper that would otherwise force
     .mobile-menu-btn and #topbarBreadcrumb to share ONE grid cell)
     makes its box disappear for layout purposes so its two children
     become direct grid items of .topbar instead, each independently
     placed into its own row via grid-area -- no DOM changes needed,
     desktop's structure (where .topbar-brand stays a normal flex
     container) is untouched. #topbarBreadcrumb used to be hidden below
     769px entirely (mobile had its own separate .breadcrumb-bar/
     #breadcrumbSlot instead, now deleted from index.html/app.js
     entirely) -- shown here too now, one render target for both screen
     sizes instead of two parallel ones. */
  .topbar {
    height: auto;
    grid-template-columns: auto 1fr;
    grid-template-areas: "hamburger search" "breadcrumb breadcrumb" "filters filters";
    row-gap: 8px;
    padding: 8px 12px;
  }
  .topbar-brand { display: contents; }
  .mobile-menu-btn { grid-area: hamburger; }
  .topbar-breadcrumb { grid-area: breadcrumb; display: block; }
  .topbar-search-wrap { grid-area: search; }
  .topbar-filters {
    /* flex-start, not flex-end -- the old right-alignment was the one
       row breaking the left-to-right flow every other row in the
       header follows, which read as arbitrary rather than organized. */
    grid-area: filters; justify-self: stretch; justify-content: flex-start;
    flex-wrap: wrap; gap: 8px 10px;
  }
  .topbar-search { width: 100%; min-width: 0; }
  .topbar-search input { min-width: 0; }
  .sidebar-filter .dd-trigger { max-width: 130px; }
  .page { padding: 16px 14px 40px; }
  /* .page-title/.kpi-value font-size no longer set here -- both are
     clamp()-based now (see their own rules) and already scale smoothly
     through this breakpoint; a fixed override here would fight the
     clamp exactly at 768px instead of the two ever agreeing. */
  .page-sub { font-size: 12px; margin-bottom: 18px; }
  .section-title { font-size: 19px; }
  .grid { gap: 10px; }
  .card { padding: 16px 16px; }
  /* Filter rows (Fleet/Work Orders/Inspections' Type/Status <select>s)
     already wrap (flex-wrap: wrap) -- just let each take the full row
     instead of a cramped half-width, and drop the search-results preview
     to the input's own width instead of a fixed 500px overflowing the
     viewport. */
  .filter-select { flex: 1 1 140px; }
  .search-results { width: min(500px, calc(100vw - 24px)); }

  /* ---------- sidebar becomes a full-screen floating overlay ---------- */
  /* position:fixed pulls it out of .app-shell's flex flow entirely, so
     .main naturally takes the full viewport width underneath with no
     extra override needed, in either state -- open or closed, the content
     column never shifts, only what's stacked visually on top of it. */
  .sidebar {
    position: fixed; inset: 0;
    /* Leaflet's own control panes sit around z-index 1000 too -- tied at
       the same value, the map's #map div (later in the DOM than <aside>)
       won every stacking tie, so its zoom control rendered ON TOP of the
       drawer instead of under it, open or not. High enough to beat that
       and anything else on the page outright, not just nudge past it. */
    z-index: 3000;
    width: 100vw !important; min-width: 100vw !important; max-width: 100vw;
    height: 100vh;
    transform: translateX(-100%);
    transition: transform .3s var(--ease);
  }
  /* Shadow only while actually open -- box-shadow travels with the element
     under transform, and blur radius extends past its own edge regardless
     of position. At exactly translateX(-100%) the (now off-screen)
     element's right edge sits precisely at the viewport's left boundary,
     so a shadow declared unconditionally on .sidebar was bleeding a faint
     permanent strip into view along the left edge even while "hidden". */
  .app-shell:not(.sidebar-collapsed) .sidebar { box-shadow: 0 0 40px rgba(0, 0, 0, .35); }
  .sidebar > * { width: 100% !important; min-width: 0 !important; }
  /* Same class the desktop icon-rail collapse already uses (rule 10/11 --
     one toggle button, one meaning: "the menu is out of the way" — this
     media query is what changes *how* that's expressed at this width, not
     a second, separate mobile mode). Not-collapsed = open, full screen;
     collapsed = the default, fully off-canvas. */
  .app-shell:not(.sidebar-collapsed) .sidebar { transform: translateX(0); }
  .app-shell.sidebar-collapsed .sidebar { transform: translateX(-100%); }
  /* The sidebar's own internal toggle (top of the drawer) becomes the
     close button once the drawer covers the whole screen -- the icon
     already flips to the "collapse" glyph in this state (rule 10), which
     reads correctly as "close" here too. */
  .sidebar-nav { padding: 22px 16px; gap: 6px; }
  .nav-group-label, .sidebar-nav a { font-size: 15px; padding: 12px 10px; }

  /* Fleet's table-layout:fixed (see the comment on .fleet-table above) is a
     deliberate desktop choice -- those same percentage widths just have no
     room to resolve into at phone widths, so cell text was overflowing its
     own column into the next one (Status "Out of Service" bleeding into
     Location, etc.) instead of the clean truncation .fleet-table was
     designed around. A min-width lets the columns resolve at a size that
     actually fits their content again, same as a desktop tab -- .table-wrap
     already scrolls horizontally (overflow-x:auto), so this is genuinely
     viewable, just sideways, instead of visually broken in place. */
  .fleet-table { min-width: 620px; }
  /* Every OTHER data table on the site (Vehicle Detail's Work
     Orders/Service History/Inspections/Financials/Rentals tabs, and any
     other page whose table isn't .fleet-table specifically) had no
     mobile min-width at all -- unlike .fleet-table above, these just
     shrank to fit the phone width, squeezing multi-word cells like
     "Replace door handle and reinstall door skin" or a full date into a
     column a few characters wide instead of scrolling. Same fix, same
     reasoning, applied generally: .table-wrap already scrolls
     horizontally, so a min-width here means genuinely viewable (sideways)
     instead of squeezed. :not(.fleet-table) so this doesn't fight the
     more deliberate 620px above -- .table-nested (parts.js's per-part
     asset breakdown) isn't inside a .table-wrap at all, so it's
     unaffected either way. */
  .table-wrap table:not(.fleet-table) { min-width: 560px; }

  /* GPS's live-vehicles panel was sized (300px, absolute top:16px/left:16px)
     for a desktop map with room to spare -- on a phone that's most of the
     screen, leaving almost no map visible underneath it. Turned into a
     bottom sheet instead: full width, capped height, so the map actually
     fills the top of the screen like the page's own full-bleed layout
     intends, and the vehicle list becomes a scrollable strip you pull up
     rather than a panel eclipsing the one thing (the map) this page is for. */
  .float-panel {
    left: 8px; right: 8px; top: auto; bottom: 8px;
    width: auto; max-height: 42vh;
  }
  .float-panel.live-panel { width: auto; }

  /* Vehicle Detail's "#ID / model name / thumb / brand logo" row
     (.vehicle-identity) is a desktop-width assumption -- .vehicle-thumb
     (96px) + .brand-logo.brand-lg (148px) + two 16px gaps eat ~276px
     before the title group (flex:1, min-width:0, so it obediently
     shrinks instead of overflowing) gets anything, leaving it maybe
     80-90px wide on a 390px phone. A long, unhyphenatable trim string
     like "Talon 1000R FOX Live Valve" has no way to fit that other than
     wrapping after nearly every word, reading as broken rather than
     just narrow. Forcing the title group to its own full-width line
     (flex-basis:100% inside a wrapping row) gives it the whole content
     width to work with; the thumb + logo wrap onto their own second
     line together, still comfortably sized at ~260px combined. */
  .vehicle-identity { flex-wrap: wrap; row-gap: 10px; }
  .vehicle-title-group { flex-basis: 100%; }
}
/* .page-title/.kpi-value no longer need a sub-420px override -- both are
   clamp()-based (see their own rules) and already reach their floor
   (20px/18px) well before this width. */

/* ---------- Issues modal (Inspections' "Open Issues" card, 2026-08-26) ---
   The first real modal/dialog on this site -- everything "floating" so far
   (.dd-menu, .float-panel) is a small anchored popover, not a full
   page-blocking dialog, so there's nothing to reuse wholesale here. Follows
   the same token vocabulary those already use (--shadow-lg for the panel,
   var(--surface-white)/var(--border)/var(--radius-md)) so it still reads as
   the same design system, just a new component shape. Issues has no
   dedicated page of its own yet, so this is how its full list is reached
   from Inspections instead of a route. */
.modal-overlay {
  position: fixed; inset: 0; z-index: 4000;
  background: rgba(21, 21, 26, .5);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  animation: modal-overlay-in .16s var(--ease) both;
}
@keyframes modal-overlay-in { from { opacity: 0; } to { opacity: 1; } }
.modal-panel {
  background: var(--surface-white);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  width: 100%; max-width: 1100px; max-height: 85vh;
  padding: 24px;
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 16px;
  animation: modal-panel-in .18s var(--ease) both;
}
@keyframes modal-panel-in {
  from { opacity: 0; transform: translateY(8px) scale(.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
.modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
.modal-close {
  flex: none; width: 32px; height: 32px; border-radius: var(--radius-sm);
  background: var(--surface); border: 1px solid var(--border); color: var(--muted);
  font-size: 20px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background .13s var(--ease), color .13s var(--ease);
}
.modal-close:hover { background: var(--critical-soft); color: var(--critical); }

/* Row grain matches inspections.js's other clickable table rows (cursor +
   hover), plus an expand-in-place affordance since there's no per-issue
   detail page to route to. */
.issues-row { cursor: pointer; transition: background .13s var(--ease); }
.issues-row:hover { background: var(--surface); }
.issues-row.is-expanded { background: var(--surface); }
.issues-detail-row td { padding: 0; }
.issues-detail {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px 20px;
  padding: 16px 14px; background: var(--surface); border-top: 1px solid var(--border);
}
.issues-detail-full { grid-column: 1 / -1; }
.issues-detail-label {
  display: block; font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .5px; color: var(--muted); margin-bottom: 2px;
}

@media (max-width: 700px) {
  .modal-panel { padding: 16px; max-height: 90vh; }
  .issues-detail { grid-template-columns: repeat(2, 1fr); }
}

/* prefers-reduced-motion for the sidebar chrome (web-interface-guidelines:
   "honor prefers-reduced-motion"). The collapse still works -- it just
   snaps instead of easing, and hover feedback stays instant rather than
   fading. */
@media (prefers-reduced-motion: reduce) {
  .sidebar, .sidebar > *, .sidebar-nav, .sidebar-context, .sidebar-foot,
  .dim-option, .dim-label, .dim-count, .dim-icon,
  .sidebar-filter, .sidebar-filter .dd-trigger, .sidebar-filter .dd-trigger-label {
    transition: none;
  }
}

/* =====================================================================
   The dimension flyout (2026-08-28)
   =====================================================================
   The store list hangs off its dimension row to the right, on hover or
   focus ("el dropdown que se vea a la derecha al hacer hover en offroad
   park"). Clicking the ROW itself means the whole dimension; clicking a
   row IN here means that dimension narrowed to one location.
   Fixed-positioned and portaled onto <body> by app.js, because .sidebar
   has overflow:hidden for its collapse animation and would clip it. */
.dim-flyout {
  position: fixed; z-index: 1000; display: none;
  min-width: 190px; max-width: 260px; padding: 5px;
  background: #1b1b21; border: 1px solid rgba(255,255,255,.10);
  border-radius: 0;
  /* The one place in this app that carries a shadow, and it is not
     decoration: this panel floats over page content that is already
     white and bordered, so a hairline alone would not separate it from
     whatever is behind. Rule E/J govern surfaces IN the layout; a
     transient overlay is a different case, same as .dd-menu's own. */
  box-shadow: 0 6px 16px -6px rgba(0,0,0,.5);
}
.dim-flyout.open { display: block; }
.dim-flyout-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 9px 10px; border: none; border-radius: 0; background: transparent;
  color: rgba(255,255,255,.72); font: inherit; font-size: 12.5px; font-weight: 500;
  text-align: left; cursor: pointer;
  transition: background .12s var(--ease), color .12s var(--ease);
}
.dim-flyout-item:hover { background: rgba(255,255,255,.07); color: #fff; }
.dim-flyout-item:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }
.dim-flyout-item.active { color: #fff; font-weight: 600; }
.dim-flyout-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dim-flyout-check { width: 13px; height: 13px; flex: none; color: var(--ok); }

/* Touch and other hover-less pointers get no flyout at all -- there is no
   hover to open it with, and a tap has to stay unambiguous ("tap the row =
   this whole dimension"). They keep the plain store dropdown instead,
   which is why that control still exists rather than having been deleted
   when the flyout replaced it on the desktop. */
@media (hover: hover) and (pointer: fine) {
  .sidebar-context .sidebar-filter { display: none; }
}
@media (hover: none), (pointer: coarse) {
  .dim-flyout { display: none !important; }
}
@media (prefers-reduced-motion: reduce) {
  .dim-flyout, .dim-flyout-item, .dim-more { transition: none; }
}

/* =====================================================================
   The reporting-window bar (2026-08-28)
   =====================================================================
   Replaced the plain "Monthly" dropdown. Two problems it had: the span
   name never said WHICH month you were looking at, and the window could
   only ever end today -- "how did last month compare" was unanswerable
   except through a card's own vs-prior delta. Arrows step a whole span at
   a time; the dates in the middle are the dropdown's trigger, so picking
   a span is still one click.
   The arrows sit OUTSIDE the label rather than as a second menu, so
   stepping costs one click and never hides where you are. */
.period-bar { gap: 2px; }
.period-step {
  flex: none; width: 26px; height: 26px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: none; background: transparent; border-radius: var(--radius-sm);
  color: var(--muted); cursor: pointer;
  transition: background .14s var(--ease), color .14s var(--ease);
}
.period-step svg { width: 15px; height: 15px; }
.period-step:hover:not(:disabled) { background: rgba(17,17,17,.06); color: var(--ink); }
.period-step:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }
/* Disabled rather than hidden at the current window: a control that
   vanishes leaves the row jumping by 26px every time you reach the
   present, and a greyed arrow says "this is the edge" where a missing one
   says nothing. */
.period-step:disabled { opacity: .28; cursor: default; }
/* Tabular figures so the dates don't reflow the bar as the window steps --
   "Jul 30 – Aug 28" and "Jul 1 – Jul 30" must occupy the same width, or
   the arrows shift under the cursor between clicks. */
.period-bar .dd-trigger-label { font-variant-numeric: tabular-nums; }
.topbar.hero-bg .period-step { color: rgba(255,255,255,.75); }
.topbar.hero-bg .period-step:hover:not(:disabled) { background: rgba(255,255,255,.12); color: #fff; }
@media (prefers-reduced-motion: reduce) { .period-step { transition: none; } }

/* The window picker panel: presets on top, two real date fields under them
   ("tienes que seleccionar manualmente la fecha"). Portaled onto <body> and
   clamped to the viewport's right edge, since this control sits at the far
   right of the topbar. */
.period-trigger {
  display: flex; align-items: center; gap: 6px; min-width: 0;
  border: none; background: transparent; color: rgba(255,255,255,.62);
  font: inherit; font-weight: 500; font-size: 13.5px;
  padding: 9px 4px 9px 6px; border-radius: var(--radius-sm); cursor: pointer; outline: none;
}
.topbar .period-trigger { color: var(--muted); }
.topbar.hero-bg .period-trigger { color: rgba(255,255,255,.92); }
.period-trigger:hover { color: var(--ink); }
.topbar.hero-bg .period-trigger:hover { color: #fff; }
.period-trigger:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }
.period-trigger-label { font-variant-numeric: tabular-nums; white-space: nowrap; }
.period-trigger[aria-expanded="true"] .dd-trigger-chevron { transform: rotate(180deg); }

.period-panel {
  position: fixed; z-index: 1000; display: none;
  width: 452px; padding: 0; overflow: hidden;
  background: var(--surface-white); border: 1px solid var(--border);
  border-radius: 0;
  box-shadow: 0 6px 16px -6px rgba(17,17,17,.22);
}
.period-panel.open { display: block; }
.period-preset {
  padding: 7px 4px; border: 1px solid var(--border); border-radius: 5px;
  background: var(--surface-white); color: var(--text-dim);
  font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
  transition: background .12s var(--ease), color .12s var(--ease), border-color .12s var(--ease);
}
.period-preset:hover { background: var(--surface); color: var(--ink); }
.period-preset.active { background: var(--ink); border-color: var(--ink); color: #fff; }
.period-preset:focus-visible { outline: 2px solid var(--brand-red); outline-offset: 1px; }
@media (prefers-reduced-motion: reduce) { .period-preset { transition: none; } }

/* ---- window picker: shortcuts beside a range calendar ---------------- */
/* Two columns, because the two ways of answering "which window" are not
   the same size of decision: a shortcut is one click and covers the common
   case, the calendar is for the specific range. Stacking them made the
   panel tall and made the calendar look like an afterthought. */
.period-body { display: grid; grid-template-columns: 138px 1fr; }
.period-shortcuts {
  display: flex; flex-direction: column; gap: 2px;
  padding: 10px; border-right: 1px solid var(--border); background: var(--surface);
}
.period-preset {
  padding: 8px 10px; border: none; border-radius: 0; background: transparent;
  color: var(--text-dim); font: inherit; font-size: 12.5px; font-weight: 600;
  text-align: left; cursor: pointer; white-space: nowrap;
  transition: background .12s var(--ease), color .12s var(--ease);
}
.period-preset:hover { background: rgba(17,17,17,.06); color: var(--ink); }
.period-preset.active { background: var(--ink); color: #fff; }
.period-preset:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }

.period-cal { padding: 10px 12px 12px; }
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.cal-title { font-family: var(--font-sans); font-size: 13px; font-weight: 700; color: var(--ink); }
.cal-nav {
  width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
  border: none; background: transparent; border-radius: 0; color: var(--muted); cursor: pointer;
  transition: background .12s var(--ease), color .12s var(--ease);
}
.cal-nav svg { width: 14px; height: 14px; }
.cal-nav:hover:not(:disabled) { background: rgba(17,17,17,.06); color: var(--ink); }
.cal-nav:disabled { opacity: .28; cursor: default; }
.cal-nav:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }
.cal-dow, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.cal-dow span {
  text-align: center; font-family: var(--font-sans); font-size: 10px; font-weight: 700;
  letter-spacing: .04em; color: var(--muted); padding-bottom: 4px;
}
.cal-grid { gap: 1px 0; }
.cal-pad { display: block; }
.cal-day {
  height: 30px; border: none; background: transparent; cursor: pointer;
  font: inherit; font-size: 12px; font-weight: 500; color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  /* Square, and edge-to-edge within its cell, so a selected span reads as
     one continuous bar across the week instead of a row of separate pills. */
  border-radius: 0;
  transition: background .1s var(--ease), color .1s var(--ease);
}
.cal-day:hover:not(:disabled) { background: rgba(17,17,17,.07); color: var(--ink); }
.cal-day.in-range { background: var(--surface); color: var(--ink); }
.cal-day.is-edge { background: var(--ink); color: #fff; font-weight: 700; }
/* Today is marked even when it is outside the window, so the calendar
   always says where "now" is relative to what you are looking at. */
.cal-day.is-today:not(.is-edge) { box-shadow: inset 0 0 0 1px var(--border); }
.cal-day.is-future { opacity: .25; cursor: default; }
.cal-day:focus-visible { outline: 2px solid var(--brand-red); outline-offset: -2px; }

.period-foot {
  padding: 9px 12px; border-top: 1px solid var(--border); background: var(--surface-white);
  font-family: var(--font-sans); font-size: 11.5px; font-weight: 600; color: var(--muted);
}
.period-hint.picking { color: var(--brand-red); }

/* The flyout names its own dimension, which is what tells you what you are
   looking at in the collapsed icon rail, where the row's label is hidden. */
.dim-flyout-head {
  padding: 6px 10px 8px; margin-bottom: 3px;
  border-bottom: 1px solid rgba(255,255,255,.08);
  font-size: 10.5px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: rgba(255,255,255,.45);
}
@media (prefers-reduced-motion: reduce) { .period-preset, .cal-day, .cal-nav { transition: none; } }

/* The basemap is OpenStreetMap desaturated to grey here rather than a
   ready-made grey tile service -- see gps.js for the two providers that
   failed first (CARTO now needs a key; Esri's grey canvas has no tiles
   past zoom 16 at this fleet's coordinates).
   Scoped to .leaflet-tile-pane, NOT the map container: markers, popups and
   the vehicle pills live in their own panes and must keep their colour --
   a stopped unit and a speeding one are told apart by it. */
.leaflet-tile-pane { filter: grayscale(1) brightness(1.06) contrast(.92); }
@media (prefers-reduced-motion: reduce) { .leaflet-tile-pane { filter: grayscale(1); } }

/* Marketing's scope line, standing in for the private From/To fields it
   used to carry (see marketing.js). States which window the chart is
   drawing and points at the one control that changes it. */
.revenue-scope { margin: 0; font-size: 12.5px; color: var(--muted); }
.revenue-scope b { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
/* The Daily/Cumulated switch and the store tabs were the last rounded
   controls on this page; squared to match every other control in the app. */
.tabbar, .tabbar button { border-radius: 0; }
.store-tab { border-radius: 0; }
/* Occupancy legend -- a sequential ramp is unreadable without one. */
.occ-legend { display: flex; align-items: center; gap: 8px; margin: -4px 0 14px; font-size: 11px; color: var(--muted); font-family: var(--font-sans); font-weight: 600; }
.occ-legend-swatches { display: flex; }
.occ-legend-swatches i { width: 26px; height: 10px; display: block; border: 1px solid var(--border); border-right: none; }
.occ-legend-swatches i:last-child { border-right: 1px solid var(--border); }

/* A stale-extract warning, not an error: the number isn't wrong, it just
   stops earlier than the window asked for. Warning tone, and it removes
   itself when the data catches up (:empty / [hidden]). */
.revenue-stale {
  margin: 6px 0 0; font-size: 12px; font-weight: 600; color: var(--warn);
}
.revenue-stale[hidden] { display: none; }

/* Neutral, not warning: nothing is wrong, the chart is explaining which
   window it could actually superimpose. */
.revenue-wrap-note { margin: 4px 0 0; font-size: 12px; color: var(--muted); }
.revenue-wrap-note[hidden] { display: none; }

/* Cross-check: the count beside a section heading. Inherits the heading's
   own weight but sits in muted ink -- it is scale, not the subject. */
.cc-count {
  font-size: 14px; font-weight: 700; color: var(--muted);
  font-variant-numeric: tabular-nums; margin-left: 2px;
}
/* Rows drill through to the vehicle, so they need to look like it. */
#pageMount .fleet-table tr[data-asset] { cursor: pointer; }
