/* BlackCurrent in-app stylesheet.

   Loaded by tesla-manager.html/page on every JVM-served HTML
   response (auth, /app/*, admin, waitlist).  Shared design
   tokens + base typography + .shell + .btn + .site-header come
   from /css/style.css (marketing CSS, served first); this file
   carries everything specific to the in-app surfaces -- count-up
   tweens, battery icon, severity bands, delta bars, live-pulse
   indicator, paper grain, etc.

   Editable as a plain CSS file -- changes pick up on next browser
   refresh without a JVM restart, since the static handler reads
   from the classpath via io/resource at request time. */
  /* Some additional warning/danger/info tint+border tokens the
     marketing site doesn't need but the app's status banners use.
     Battery-severity fills (--bat-*) are scoped to the in-app
     battery icon and SoC chart bands; they're functional /
     semantic colors (universal iOS-style green/amber/red), not
     brand chrome -- the trend line, table headers, etc. stay
     in --brand. */
  :root {
    --fg-disabled: #C4B7BD;
    --danger: #b91c1c; --danger-hover: #991b1b;
    --danger-tint: #fee2e2; --danger-border: #f87171;
    --warning-border: #fbbf24;
    --info: #1e3a8a; --info-tint: #dbeafe; --info-border: #93c5fd;
    --bat-good: #16a34a; --bat-good-tint: #dcfce7;
    --bat-warn: #d97706; --bat-warn-tint: #fef3c7;
    --bat-low:  #dc2626; --bat-low-tint:  #fee2e2;
    /* Activity-category colours -- shared by the dashboard activity
       bar segments and their legend dots. */
    --act-driving:  #7B1948;
    --act-charging: #166534;
    --act-idle:     #b45309;
    --act-offline:  #D8CFD4;
    /* Chart chrome -- gridlines a touch lighter than --rule so the
       data line dominates; axis text muted. */
    --chart-grid: #F1EAEE;
    --chart-axis: #8B7A82;
    /* ---- Motion system.  One small vocabulary of
       durations + easings so every /app surface moves with the same
       cadence instead of a scatter of magic-number milliseconds.
         --dur-fast   : hover, focus, show/hide of buttons + controls.
         --dur-medium : view-swap entries, row cross-fades, card accents.
         --dur-slow   : informational tweens that report a value change
                        (count-ups, battery fill) -- long enough to read.
         --dur-pulse  : ambient loops (live dot, charge breath).
       --ease-out is the house curve (a soft decelerate, used almost
       everywhere); --ease-pop adds a hair of overshoot for marker/dot
       arrivals.  These are the SAME numbers already in use, named once. */
    --dur-fast: 120ms;
    --dur-medium: 240ms;
    --dur-entry: 420ms;
    --dur-slow: 1800ms;
    --dur-pulse: 1800ms;
    --ease-out: cubic-bezier(.2, .7, .2, 1);
    --ease-in-out: cubic-bezier(.45, 0, .55, 1);
    --ease-pop: cubic-bezier(.2, .9, .3, 1.35);
  }
  /* Wrap auth + /app body content in a sensible default column.
     Marketing pages use .shell directly on their own containers;
     /app pages get this padding by default. */

  /* App shell.  /app pages are a FIXED-HEIGHT
     flex column: the body is pinned to the viewport and never scrolls; the
     header sizes to its own content (including the <=900px nav wrap, so
     there is no --header-h variable to keep in sync); and a full-bleed
     .app-scroll viewport takes the remaining height and scrolls internally.
     Document height is therefore CONSTANT, so an in-app view swap cannot
     move the body/scrollbar -- the SSE progressive re-render collapses then
     expands INSIDE .app-scroll, invisibly.  (Measured on prod: 7 doc-height
     jumps of 300-515px across drives/analytics swaps -> 0.)  Scoped to
     body.app-body so auth + marketing pages keep normal document flow.

     svh (not dvh): because the body itself never scrolls, a mobile toolbar
     can't collapse against it, so svh is the stable no-jitter choice; the
     plain 100vh line is the pre-svh fallback. */
  body.app-body {
    padding: 0;
    margin: 0;
    height: 100vh;          /* fallback for browsers without svh */
    height: 100svh;         /* stable small-viewport height (preferred) */
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  body.app-body > .site-header { flex: 0 0 auto; }
  /* The scroll viewport.  `.app-scroll' is the production wrapper; `.shell'
     is listed as a fallback ONLY for the deploy-ordering window -- the CSS
     (Cloudflare Pages) ships in ~1 min but the wrapper (JVM html.clj) lands
     on the slower instance-refresh, so for a few minutes .shell is still the
     direct child of the fixed-height body.  Whichever is the direct child
     scrolls; once the wrapper deploys, .shell is no longer a direct child so
     only .app-scroll matches.  No clipped, unscrollable intermediate. */
  body.app-body > .app-scroll,
  body.app-body > .shell {
    flex: 1 1 auto;
    min-height: 0;          /* let the flex child shrink so it can scroll */
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;   /* no scroll-chaining to the locked body */
  }
  /* App base typography + i18n text resilience.
     Body text rides the fluid --text-base; long words (German/Finnish
     compounds, Polish case forms) break cleanly instead of overflowing --
     hyphens:auto rides the per-locale <html lang>, break-word is the
     no-overflow backstop.  Every European language expands 30-100% over
     English, so text MUST be allowed to wrap. */
  main#app-content {
    display: block;
    font-size: var(--text-base);
    overflow-wrap: break-word;
    hyphens: auto;
  }
  /* Brand-colored inline links inside prose content.  Scoped to
     `p > a' and excludes button-like classes so .cta-primary,
     .cta-secondary, .btn keep their own background+color treatments
     (the white text on brand-color background, etc).  The marketing
     site styles links contextually via .btn / .site-header__nav etc;
     app + auth pages use bare <p><a> for cross-links (sign-up <->
     sign-in, privacy page) and need a default. */
  main.prose-shell p > a:not(.cta-primary):not(.cta-secondary):not(.btn):not([class*="btn--"]),
  main#app-content p > a:not(.cta-primary):not(.cta-secondary):not(.btn):not([class*="btn--"]) {
    color: var(--brand); text-decoration: underline;
    text-underline-offset: 2px; }
  main.prose-shell p > a:not(.cta-primary):not(.cta-secondary):not(.btn):not([class*="btn--"]):hover,
  main#app-content p > a:not(.cta-primary):not(.cta-secondary):not(.btn):not([class*="btn--"]):hover {
    color: var(--brand-deep); }
  /* Auth-page button overlap with site .btn.  Existing markup uses
     a.cta-primary / a.cta-secondary / button[type=submit] / button.btn-danger.
     Map them to the site's --brand palette so they look identical
     to the marketing-site buttons without rewriting every form. */
  a.cta-primary, a.cta-secondary,
  button[type="submit"], button.btn-danger {
    display: inline-block; padding: 0.7rem 1.4rem;
    background: var(--brand); color: #fff; text-decoration: none;
    border: 0; border-radius: var(--r-sm); font-size: 1rem; cursor: pointer;
    font-weight: 600; font-family: inherit; line-height: 1.2;
    /* Show/hide + state polish: background, a 1px
       hover lift, and opacity all ease on the fast token so a button
       enabling/disabling or pressing never snaps.  transform/opacity are
       compositor-only; the lift is small enough not to nudge layout. */
    transition: background var(--dur-fast) var(--ease-out),
                transform var(--dur-fast) var(--ease-out),
                opacity var(--dur-fast) var(--ease-out),
                box-shadow var(--dur-fast) var(--ease-out); }
  a.cta-primary:hover, button[type="submit"]:hover { background: var(--brand-deep); }
  a.cta-primary:hover, a.cta-secondary:hover,
  button[type="submit"]:hover, button.btn-danger:hover { transform: translateY(-1px); }
  a.cta-primary:active, a.cta-secondary:active,
  button[type="submit"]:active, button.btn-danger:active { transform: translateY(0); }
  a.cta-secondary { background: var(--brand-tint); color: var(--brand-text); }
  a.cta-secondary:hover { background: var(--brand-tint-hover); }
  button.btn-danger { background: var(--danger); }
  button.btn-danger:hover { background: var(--danger-hover); }
  /* Disabled controls (e.g. the recheck / submit buttons while their
     Datastar round-trip runs via data-attr:disabled) fade to a muted
     state rather than blinking off -- the opacity transition above
     carries it.  No hover lift while disabled. */
  a.cta-primary[disabled], a.cta-secondary[disabled],
  button[type="submit"][disabled], button.btn-danger[disabled],
  button[disabled] {
    opacity: 0.55; cursor: not-allowed; transform: none; }
  section.cta { margin: 2rem 0; }
  section.cta p { margin: 0.6rem 0; }
  section.explainer { margin: 1.5rem 0; color: var(--fg-muted); }
  form label { display: block; margin: 0.9rem 0; font-weight: 500; }
  form input { display: block; width: 100%; max-width: 420px;
               padding: 0.55rem 0.7rem; margin-top: 0.25rem;
               font-size: 1rem; border: 1px solid var(--rule-strong);
               border-radius: var(--r-sm); box-sizing: border-box;
               background: var(--bg-elev); color: var(--fg);
               font-family: inherit;
               transition: border-color 120ms ease, box-shadow 120ms ease; }
  form input:focus { outline: none; border-color: var(--brand);
                     box-shadow: 0 0 0 3px var(--brand-tint); }
  form input[type="checkbox"] { width: auto; display: inline; margin-right: 0.4rem; }
  form input[type="hidden"] { display: none; }
  ul.errors { background: var(--danger-tint); border: 2px solid var(--danger-border);
              padding: 0.6rem 1rem 0.6rem 2.2rem; border-radius: var(--r-md);
              color: var(--danger); font-weight: 600; }
  div.upgrade-banner { background: var(--warning-tint);
                       border: 1px solid var(--warning-border);
                       border-left: 3px solid var(--warning-border);
                       padding: 0.75rem 1rem; border-radius: var(--r-md);
                       margin-bottom: 1.5rem; }
  p.tagline { font-size: 1.1rem; color: var(--fg-muted); }
  p.empty { color: var(--fg-soft); }
  .danger-zone { margin-top: 2rem; padding-top: 1rem;
                 border-top: 1px dashed var(--rule-strong); }
  .danger-zone p { font-size: var(--text-sm); color: var(--fg-muted); }
  /* Charging log */
  main.charge-page { max-width: 920px; }
  p.charge-summary { color: var(--fg-muted); }
  section.charge-pending { margin: 1rem 0; color: var(--fg-muted); }
  label.filter-label { display: flex; align-items: baseline; gap: 0.6rem;
                       margin: 1rem 0; font-weight: 500; }
  label.filter-label > span { flex: 0 0 auto; }
  input.filter-input { display: inline-block; width: 100%; max-width: 360px;
                       margin: 0; padding: 0.5rem 0.7rem;
                       border: 1px solid var(--rule-strong); border-radius: 4px;
                       font-size: var(--text-sm); box-sizing: border-box;
                       background: var(--bg-elev); color: var(--fg);
                       font-family: inherit;
                       transition: border-color 120ms ease, box-shadow 120ms ease; }
  input.filter-input:focus { outline: none; border-color: var(--brand);
                             box-shadow: 0 0 0 3px var(--brand-tint); }
  .range-control { display: flex; align-items: baseline; gap: 0.6rem;
                   margin: 1rem 0; }
  .range-control__label { flex: 0 0 auto; color: var(--fg-muted);
                          font-size: var(--text-xs); font-weight: 600;
                          text-transform: uppercase; letter-spacing: 0.04em; }
  select.range-control__select { padding: 0.45rem 0.7rem;
                          border: 1px solid var(--rule-strong); border-radius: 4px;
                          font-size: var(--text-sm); background: var(--bg-elev);
                          color: var(--fg); font-family: inherit; cursor: pointer;
                          transition: border-color 120ms ease, box-shadow 120ms ease; }
  select.range-control__select:focus { outline: none; border-color: var(--brand);
                          box-shadow: 0 0 0 3px var(--brand-tint); }
  a.site-header__admin { color: var(--brand-bright); }
  .admin-count { color: var(--fg-muted); font-size: var(--text-sm); }
  .admin-section { margin: 1.5rem 0; }
  .admin-section .stats-row { display: flex; flex-wrap: wrap; gap: 1rem;
                              margin: 0.75rem 0 1rem; }
  .admin-section .stats-row .stat { background: var(--bg-elev);
                              border: 1px solid var(--rule); border-radius: 6px;
                              padding: 0.6rem 0.9rem; min-width: 8rem; }
  .stat__label { color: var(--fg-muted); font-size: var(--text-2xs);
                 text-transform: uppercase; letter-spacing: 0.04em; }
  .stat__value { font-size: 1.4rem; font-weight: 600;
                 font-variant-numeric: tabular-nums; }
  table.admin-table { width: 100%; border-collapse: collapse;
                      font-variant-numeric: tabular-nums; background: var(--bg-elev);
                      border: 1px solid var(--rule); border-radius: 6px;
                      overflow: hidden; font-size: var(--text-xs); margin: 0.5rem 0 1rem; }
  table.admin-table th, table.admin-table td {
                      padding: 0.5rem 0.7rem; text-align: left;
                      border-bottom: 1px solid var(--rule); }
  table.admin-table th { background: var(--brand-tint); color: var(--brand-text);
                      font-weight: 600; font-size: var(--text-2xs);
                      text-transform: uppercase; letter-spacing: 0.04em; }
  table.admin-table tbody tr:last-child td { border-bottom: 0; }
  table.admin-table tbody tr:hover { background: var(--bg-soft); }
  button.btn-danger.admin-purge { padding: 0.25rem 0.6rem; font-size: var(--text-xs); }
  /* Users-table: client-side filter, compact per-row access controls,
     and subscription-status badges. */
  input.admin-filter { width: 100%; max-width: 22rem; margin: 0 0 0.75rem;
                       padding: 0.4rem 0.6rem; font-size: var(--text-sm);
                       border: 1px solid var(--rule); border-radius: 6px;
                       background: var(--bg-elev); color: var(--fg); }
  input.admin-filter:focus { outline: none; border-color: var(--brand);
                       box-shadow: 0 0 0 3px var(--brand-tint); }
  td.admin-access { white-space: nowrap; }
  /* Dimmed monospace user-id prefix beside each email -- eyeball
     correlation with logs without expanding the full UUID. */
  .admin-uid { font-family: var(--font-mono); font-size: var(--text-xs);
               color: var(--fg-soft); opacity: 0.7; user-select: all; }
  /* Bulk-clear controls above the rollups table. */
  .admin-rollup-tools { display: flex; gap: 0.5rem; margin: 0 0 0.75rem; }
  /* Account home: profile + billing facts. */
  .account-section { margin: 0 0 2rem; }
  .account-facts { display: grid; grid-template-columns: max-content 1fr;
                   gap: 0.35rem 1.25rem; margin: 0.5rem 0 0; }
  .account-facts dt { color: var(--fg-muted); font-size: var(--text-sm); }
  .account-facts dd { margin: 0; }
  .account-facts dd code { font-family: var(--font-mono); font-size: var(--text-sm); }
  td.admin-access button, button.admin-resend {
                       padding: 0.2rem 0.5rem; font-size: var(--text-2xs);
                       margin: 0.1rem 0.15rem 0.1rem 0; }
  .sub-badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 999px;
               font-size: var(--text-2xs); font-weight: 600; text-transform: uppercase;
               letter-spacing: 0.03em; }
  .sub-badge.sub-active, .sub-badge.sub-trial {
               background: var(--bat-good-tint); color: var(--bat-good); }
  .sub-badge.sub-past-due { background: var(--bat-warn-tint); color: var(--bat-warn); }
  .sub-badge.sub-canceled { background: var(--danger-tint); color: var(--danger); }
  a.stripe-link { font-size: var(--text-2xs); color: var(--fg-muted); white-space: nowrap; }
  /* Subscribe / billing wall (the gated page). */
  .subscribe-status { font-size: 1.05rem; }
  .subscribe-status--ended { color: var(--danger); font-weight: 600; }
  .subscribe-price { font-size: 1.1rem; margin: 1rem 0; }
  main.subscribe-shell form { margin: 1.25rem 0; }
  .subscribe-footnote { color: var(--fg-muted); font-size: var(--text-xs); }
  table.charge-log, table.vehicles-list {
                     width: 100%; border-collapse: collapse;
                     font-variant-numeric: tabular-nums;
                     background: var(--bg-elev); border: 1px solid var(--rule);
                     border-radius: 6px; overflow: hidden; }
  /* Fixed layout (driven by a server-rendered <colgroup>) so re-sorting
     never recomputes column widths -- the variable Trip column otherwise
     reflows every column, jittering the header + body.  Long trip
     headlines wrap inside their pinned column instead of widening it. */
  table.charge-log.is-fixed { table-layout: fixed; }
  table.charge-log.is-fixed td, table.charge-log.is-fixed th { overflow-wrap: anywhere; }
  table.charge-log th, table.charge-log td,
  table.vehicles-list th, table.vehicles-list td {
    padding: 0.55rem 0.8rem; text-align: left;
    border-bottom: 1px solid var(--rule); font-size: var(--text-sm); }
  table.charge-log th, table.vehicles-list th {
                        background: var(--brand-tint); color: var(--brand-text);
                        font-weight: 600; font-size: var(--text-xs);
                        text-transform: uppercase; letter-spacing: 0.04em; }
  table.charge-log tbody tr:last-child td,
  table.vehicles-list tbody tr:last-child td { border-bottom: 0; }
  /* Layout stability:
     the PRIMARY mechanism is ordering -- the pager + CSV
     trailers render ABOVE the .table-reserve__rows block (server side)
     so the <table> is the LAST element in the widget; its progressive
     growth extends downward into empty space and shifts nothing below
     it.  The .table-reserve wrapper's inline min-height (from the
     server, carried through the idiomorph swap via data-preserve-attr)
     is retained as defense-in-depth so the box does not visibly collapse
     mid-fill.  Per-row min-block-size is a FLOOR, not a fixed height: a
     cell whose content wraps to two lines at a narrow width (long trip
     addresses) grows the row past it rather than clipping, and the
     wrapped rows simply make the box taller. */
  .table-reserve__rows { display: block; }
  .table-reserve table.charge-log tbody tr { min-block-size: 45px; }
  .table-reserve--thumbs table.charge-log tbody tr { min-block-size: 63px; }
  /* The dashboard's windowed summary block reserves its box the same
     way (inline min-height from the server).  flow-root keeps the
     children's margins INSIDE the box so the reserved arithmetic holds
     (collapsed-through margins would leak past the min-height). */
  .dash-reserve { display: flow-root; }
  table.charge-log tbody tr:hover,
  table.vehicles-list tbody tr:hover { background: var(--bg-soft); }
  /* Sortable column headers.  A sortable <th> wraps an <a.th-sort> that
     toggles ?sort=&dir= through the canonical nav flow; the active column
     carries aria-sort, which drives the direction caret.  Inactive
     sortable headers show a faint up/down hint so the affordance is
     discoverable; plain (non-sortable) headers have no <a> and no caret. */
  th .th-sort { display: inline-flex; align-items: baseline; gap: 0.35em;
                color: inherit; text-decoration: none; cursor: pointer; }
  th .th-sort:hover { text-decoration: underline; }
  th .th-sort::after { content: "\2195"; opacity: 0.3; font-size: 0.85em; }
  th[aria-sort="ascending"]  .th-sort::after { content: "\25B2"; opacity: 1; }
  th[aria-sort="descending"] .th-sort::after { content: "\25BC"; opacity: 1; }
  /* Account cog menu (member-facing dropdown in the header cluster).
     Reuses .nav-group dropdown mechanics; right-aligns since it sits at
     the right edge, and the cog glyph is a stroked SVG. */
  .user-menu__summary { display: inline-flex; align-items: center; padding: 0.3rem; }
  .user-menu__cog { stroke: currentColor; fill: none; stroke-width: 2;
                    stroke-linecap: round; stroke-linejoin: round;
                    display: block; opacity: 0.85; }
  .user-menu__summary:hover .user-menu__cog { opacity: 1; }
  .user-menu .nav-group__menu { right: 0; left: auto; }
  .user-menu .logout-form { margin: 0; display: block; }
  /* Sign out: the blanket button[type="submit"] brand-fill near the top of
     this file (specificity 0,1,1) leaked into the dropdown, rendering Sign
     out as a solid maroon button amid plain-text rows.  Override it back to
     a BOLD text menu row that matches the sibling links, so the menu reads
     as one consistent list.  Scoped to .user-menu so auth-page submit
     buttons keep their filled style. */
  .user-menu .logout-form button[type="submit"] {
    display: block; width: 100%; text-align: left;
    padding: 0.45rem 0.7rem;
    background: transparent; color: var(--brand-text);
    font-size: 0.95rem; font-weight: 700; line-height: 1.2;
    border: 0; border-top: 1px solid var(--rule); border-radius: 0;
    cursor: pointer;
    transition: background-color 120ms ease, color 120ms ease; }
  .user-menu .logout-form button[type="submit"]:hover {
    background: var(--brand-tint); color: var(--brand-text); transform: none; }
  td.charger-cell { white-space: nowrap; }
  td.delta-cell { color: var(--success); font-weight: 600; }
  span.pill { display: inline-block; padding: 0.15rem 0.55rem;
              border-radius: 999px; font-size: var(--text-xs);
              font-weight: 600; line-height: 1.4; }
  span.pill-home         { background: var(--success-tint); color: var(--success); }
  span.pill-supercharger { background: var(--warning-tint); color: var(--warning); }
  span.pill-destination  { background: var(--info-tint);    color: var(--info); }
  /* Drive detail - composed to fit a ~1440x900 desktop viewport without
     scrolling: header-level detail nav, compact title, map height capped
     to the viewport, map + stats side by side, charts below, then the
     variable-length soundtrack LAST -- keeping the charts directly under
     the fixed-height map+stats stabilises their vertical position across
     Next/Prev. */
  nav.detail-nav { display: flex; align-items: center;
                   justify-content: space-between; gap: 1rem;
                   margin: 0.75rem 0 0.25rem; }
  nav.detail-nav .detail-nav__pager { display: inline-flex; gap: 0.5rem; }
  nav.detail-nav a { display: inline-block; padding: 0.35rem 0.8rem;
                     background: var(--brand-tint); color: var(--brand-text);
                     border-radius: 4px; text-decoration: none;
                     font-weight: 600;
                     transition: background 120ms ease; }
  nav.detail-nav a:hover { background: var(--brand-tint-hover); }
  nav.detail-nav a.detail-nav__back { background: transparent;
                                      color: var(--brand); padding-left: 0; }
  nav.detail-nav a.detail-nav__back:hover { background: transparent;
                                            color: var(--brand-deep); }
  nav.detail-nav span.disabled { display: inline-block;
                                 padding: 0.35rem 0.8rem;
                                 color: var(--fg-disabled); font-weight: 600; }
  h1.detail-title { font-size: var(--text-xl); margin: 0.4rem 0 0.6rem; }
  section.drive-detail { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
                         gap: 1rem 1.5rem; align-items: start; margin: 0.75rem 0; }
  /* Reserve the stats box to its fully-enriched height so the charts below
     sit at a stable vertical position across Next/Prev despite the variable
     enriched-row count (energy / efficiency / destination / ended-at /
     odometer / arrival vary 8<->14 rows).  A
     sparse drive leaves a little whitespace rather than shrinking.  The
     .stat-pair auto-fit grid collapses to ONE column once the container is
     narrow, doubling the stacked row count, so the narrow floor is taller. */
  section.drive-detail dl.drive-stats { min-height: 19rem; }
  @container app-main (max-width: 44rem) {
    section.drive-detail dl.drive-stats { min-height: 31rem; }
  }
  svg.drive-track { width: 100%; height: auto; max-width: 100%;
                    border: 1px solid var(--rule); border-radius: 6px;
                    background: var(--bg-soft); display: block; }
  /* Series line charts (speed/SoC, charge SoC/power) scale to their
     figure -- the inline SVG carries a viewBox but no width attribute,
     so without this it renders at its intrinsic 720px viewBox width and
     overflows the figure on a phone, giving the whole page a horizontal
     scrollbar. */
  svg.drive-series { width: 100%; height: auto; max-width: 100%; display: block;
                     /* Draggable scrub surface: ew-resize hints it,
                        touch-action:none lets a finger drag the cursor instead
                        of scrolling the page off the chart. */
                     cursor: ew-resize; touch-action: none; }
  section.drive-detail svg.drive-track { max-height: clamp(260px, 38vh, 430px); }
  /* <drive-map> live tile map (progressive enhancement over the SVG). */
  drive-map { display: block; }
  drive-map .drive-map__canvas { width: 100%; aspect-ratio: 2 / 1;
                                 border: 1px solid var(--rule); border-radius: 6px;
                                 background: var(--bg-soft); }
  section.drive-detail drive-map .drive-map__canvas {
                                 aspect-ratio: auto;
                                 height: clamp(260px, 38vh, 430px); }
  /* Speed/SoC (drive) and SoC/power (charge) chart pair.  Phone-FIRST:
     a SINGLE full-width column by default, so the track can never be
     wider than the content area.  The
     prior `repeat(auto-fit, minmax(min(380px,100%),1fr))' still resolved
     its track to the 380px branch whenever the grid's own width reached
     ~380 (the `100%' in `min()' is the grid's resolved width, which is
     circular at the breakpoint): the grid then took a 380px min-content
     width and, sitting at the ~24px content inset of a 390px phone,
     overflowed to a 404px right edge -> page horizontal scrollbar even
     though the SVG inside fit at width:100%.  A media query that only
     goes two-up once there is genuinely room (>=820px) removes the fixed
     min entirely on phones. */
  .drive-charts { display: grid; grid-template-columns: 1fr;
                  gap: 0 1rem; min-width: 0; max-width: 100%; }
  @container app-main (min-width: 50rem) {
    .drive-charts { grid-template-columns: 1fr 1fr; }
  }
  /* min-width:0 is a belt on the figure too (a grid item defaults to
     `min-width:auto', refusing to shrink below its content min-content;
     the .chart-head carries a `white-space:nowrap' latest chip);
     max-width:100% caps the border-box regardless of padding. */
  .drive-charts figure.drive-chart { margin: 0.5rem 0;
                                     min-width: 0; max-width: 100%; }
  .leaflet-container { font: inherit; }
  /* Scrub arrow marker: the divIcon wrapper rotates to heading. */
  .leaflet-marker-icon.drive-arrow { background: none; border: 0; }
  .drive-arrow__rot { display: block; transform-origin: 50% 50%;
                      transition: transform 60ms linear;
                      filter: drop-shadow(0 1px 1px rgba(0,0,0,0.35)); }
  /* Time-scrub slider under the map. */
  .drive-scrub { display: flex; align-items: center; gap: 0.75rem;
                 margin: 0.75rem 0 0.25rem; }
  .drive-scrub__range { flex: 1; accent-color: #7B1948; cursor: pointer; }
  .drive-scrub__time { font-variant-numeric: tabular-nums; font-weight: 600;
                       color: var(--fg-muted); min-width: 4ch; text-align: right; }
  /* Chart cursor + dot injected by drive-map.js on scrub. */
  svg.drive-series .scrub-cursor { stroke: var(--fg-muted); stroke-width: 1; opacity: 0.55; }
  svg.drive-series .scrub-dot { stroke: #fff; stroke-width: 1; }
  /* Speed/SoC label that rides the moving map arrow. */
  .leaflet-tooltip.drive-cursor-label { background: #7B1948; color: #fff; border: 0;
                                        border-radius: 4px; font-weight: 600;
                                        font-variant-numeric: tabular-nums;
                                        padding: 2px 6px; box-shadow: 0 1px 4px rgba(0,0,0,0.3); }
  .leaflet-tooltip.drive-cursor-label::before { border-top-color: #7B1948; }
  /* Client-side reverse-geocoded address (<geo-addr>); shows a subtle
     placeholder until it resolves. */
  geo-addr .geo-text:empty::before { content: "…"; color: var(--fg-muted); }
  /* min-width:0 + overflow-wrap so a long destination ('Cafe - bar
     Paragraf - remont lokalu do grudnia 2026 r.') in the trip headline
     breaks instead of forcing the page wider than the viewport
     (the stats grid was one overflow source, this headline line was the
     other). */
  .trip-route { margin: 0 0 0.5rem; font-weight: 600; color: var(--fg);
                min-width: 0; overflow-wrap: anywhere; }
  /* Vehicle telemetry/pairing status badges + the add-virtual-key warning. */
  .tel-badge { display: inline-block; padding: 2px 8px; border-radius: 999px;
               font-size: var(--text-xs); font-weight: 600; white-space: nowrap; }
  .tel-badge.is-ok      { background: #dcfce7; color: #166534; }
  .tel-badge.is-warn    { background: #fee2e2; color: #b91c1c; }
  .tel-badge.is-pending { background: #e0f2fe; color: #075985; }
  .tel-badge.is-unknown { background: var(--bg-soft); color: var(--fg-muted); }
  .key-warn { background: #fff1f2; border: 1px solid #f3c5c5; border-left: 4px solid #b91c1c;
              border-radius: 8px; padding: 1rem 1.25rem; margin: 1rem 0; }
  .key-warn h2 { margin: 0 0 0.25rem; color: #b91c1c; font-size: 1.1rem; }
  .key-warn ol { margin: 0.5rem 0 1rem; padding-left: 1.25rem; }
  .key-warn__actions { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }
  /* min-width:0 lets the stats grid shrink inside its detail-grid
     column instead of establishing a min-content width wider than the
     viewport; the grid ITEMS (dt/dd) default to `min-width:auto' and
     would otherwise refuse to shrink below their content, blowing the
     `auto' column wide on a long address. */
  /* Each label/value is glued into a .stat-pair cell (label left, value
     right); the cells flow into an auto-fit grid -- one tidy column on a
     phone, two/three grouped columns on a wide detail pane -- so a value
     never drifts to the far edge away from its label the way the old
     `auto 1fr' across a wide column did.
     min-width:0 lets the box + pairs shrink inside the detail grid's right
     column instead of forcing a min-content width wider than the viewport. */
  dl.drive-stats { margin: 0; min-width: 0;
                   display: grid;
                   grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
                   gap: 0 1.5rem; font-variant-numeric: tabular-nums;
                   font-size: var(--text-sm);
                   background: var(--bg-elev); border: 1px solid var(--rule);
                   border-radius: 6px; padding: 0.7rem 1rem; }
  dl.drive-stats .stat-pair { display: flex; align-items: baseline;
                              justify-content: space-between; gap: 0.75rem;
                              min-width: 0; padding: 0.32rem 0; }
  dl.drive-stats dt { color: var(--fg-muted); font-size: var(--text-xs);
                      min-width: 0; }
  dl.drive-stats dd { margin: 0; font-weight: 600; text-align: right;
                      min-width: 0; overflow-wrap: anywhere; }
  nav.drive-pager { display: flex; justify-content: space-between;
                    margin: 1rem 0; }
  nav.drive-pager a { display: inline-block; padding: 0.45rem 0.9rem;
                      background: var(--brand-tint); color: var(--brand-text);
                      border-radius: 4px; text-decoration: none;
                      font-weight: 600;
                      transition: background 120ms ease; }
  nav.drive-pager a:hover { background: var(--brand-tint-hover); }
  nav.drive-pager span.disabled { display: inline-block;
                                  padding: 0.45rem 0.9rem;
                                  color: var(--fg-disabled); font-weight: 600; }
  @container app-main (max-width: 44rem) {
    section.drive-detail { grid-template-columns: 1fr; }
  }
  /* Trip-as-headline drive-log cell, estimated-distance mark, soundtrack. */
  .trip-headline { display: block; font-weight: 600; }
  .trip-when { display: block; font-size: var(--text-xs); color: var(--fg-muted);
               font-weight: 400; }
  abbr.est-mark { margin-left: 0.25rem; color: var(--fg-muted); font-size: var(--text-xs);
                  text-decoration: none; cursor: help; }
  section.drive-soundtrack { margin: 1.25rem 0; }
  section.drive-soundtrack h2 { font-size: var(--text-base); margin: 0 0 0.5rem; }
  section.drive-soundtrack ol { margin: 0; padding-left: 1.25rem; }
  section.drive-soundtrack li { margin: 0.2rem 0; }
  .track-artist { color: var(--fg-muted); }
  /* Dashboard -- /app summary + nav cards */
  main.dashboard { max-width: 920px; }
  p.dashboard-period { color: var(--fg-muted); }
  section.summary-stats { display: grid;
                          grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
                          gap: var(--space-sm); margin: var(--space-md) 0 var(--space-lg); }
  section.summary-stats .stat { background: var(--bg-elev);
                                border: 1px solid var(--rule);
                                border-radius: 6px; padding: var(--space-sm) var(--space-md); }
  section.summary-stats .stat-value { font-size: var(--display-sm); font-weight: 700;
                                      color: var(--brand-text);
                                      white-space: nowrap;
                                      font-variant-numeric: tabular-nums; }
  /* Dashboard 'battery now' glyph reads in brand at healthy levels, to
     match the /app/battery hero (warn/low keep their alert colours). */
  section.summary-stats .stat-battery.bat-good .bat-fill,
  section.summary-stats .stat-battery.bat-good .bat-cap  { fill: var(--brand); }
  section.summary-stats .stat-battery.bat-good .bat-body { stroke: var(--brand); }
  /* @property-driven count-up animation on dashboard tiles.
     Each SSE re-render updates the inline `style="--n: <new>"`.
     The browser transitions the registered integer custom property
     from its previous computed value to the new one, and the CSS
     counter() function renders the interpolated integer in ::before.
     CSS-only: no JS, no signal patches, idiomorph preserves the
     element instance across morphs so the prior value is the
     transition start. Browsers without @property support fall
     through to the var() substitution and snap to the new value
     instantly -- equivalent to no animation, no broken rendering. */
  @property --n  { syntax: '<integer>'; initial-value: 0; inherits: false; }
  @property --ki { syntax: '<integer>'; initial-value: 0; inherits: false; }
  @property --kd { syntax: '<integer>'; initial-value: 0; inherits: false; }
  @property --p  { syntax: '<integer>'; initial-value: 0; inherits: false; }
  /* Count-up tween: 1800ms with strong ease-out so the digit
     visibly rolls to a stop (was 800ms which read as 'instant'
     to several testers).  The settle-wobble animation is layered
     on top with animation-delay matching the tween duration --
     it kicks in just as the digit lands. animation-name is set
     inline per-render by the server (parity of the new value:
     wobble-a for odd, wobble-b for even); a different name on
     re-render is the CSS-only signal the browser uses to
     re-fire the animation across idiomorph-preserved elements. */
  .tween-int { counter-reset: nn var(--n);
               transition: --n 1800ms cubic-bezier(.1,.85,.15,1);
               display: inline-block;
               animation-duration: 1800ms;
               animation-timing-function: cubic-bezier(.3,.7,.3,1); }
  .tween-int::before { content: counter(nn); }
  .tween-pct { counter-reset: pp var(--p);
               transition: --p 1800ms cubic-bezier(.1,.85,.15,1);
               display: inline-block;
               animation-duration: 1800ms;
               animation-timing-function: cubic-bezier(.3,.7,.3,1); }
  .tween-pct::before { content: counter(pp) '%'; }
  .tween-km  { counter-reset: ii var(--ki) dd var(--kd);
               transition: --ki 1800ms cubic-bezier(.1,.85,.15,1),
                           --kd 1800ms cubic-bezier(.1,.85,.15,1);
               display: inline-block;
               animation-duration: 1800ms;
               animation-timing-function: cubic-bezier(.3,.7,.3,1); }
  .tween-km::before { content: counter(ii) '.' counter(dd) ' km'; }
  /* Generic count-up tiles -- same @property trick
     as .tween-int / .tween-km, but the unit suffix is a CSS custom
     property (--unit, a quoted string) so one class serves every
     analytics figure (kWh, Wh/km, +%, plain count).  .tween-num is the
     integer form; .tween-dec is integer.tenths for one-decimal figures.
     Both reuse the registered --n / --ki / --kd integer properties and
     the wobble keyframes, so they tween + settle identically to the
     dashboard tiles.  Set --unit inline (default empty). */
  .tween-num { counter-reset: nn var(--n);
               transition: --n 1800ms cubic-bezier(.1,.85,.15,1);
               display: inline-block;
               animation-duration: 1800ms;
               animation-timing-function: cubic-bezier(.3,.7,.3,1); }
  .tween-num::before { content: counter(nn) var(--unit, ''); }
  .tween-dec { counter-reset: ii var(--ki) dd var(--kd);
               transition: --ki 1800ms cubic-bezier(.1,.85,.15,1),
                           --kd 1800ms cubic-bezier(.1,.85,.15,1);
               display: inline-block;
               animation-duration: 1800ms;
               animation-timing-function: cubic-bezier(.3,.7,.3,1); }
  .tween-dec::before { content: counter(ii) '.' counter(dd) var(--unit, ''); }
  /* Wobble: runs in parallel with the count-up @property
     transition (both 1800ms, both start at t=0).  Combined
     motion: digit pops on impact, holds slightly enlarged while
     the value rolls, dips below unity, settles.

     CSS animations only restart when `animation-name' changes
     (per spec -- duration / delay / timing-function changes
     modify the running animation in place but do NOT reset to
     t=0).  Idiomorph preserves the element across SSE morphs,
     so a single fixed name would only fire once on first mount.

     Two identical keyframes -- `wobble-a' and `wobble-b' --
     selected per render by value parity.  Adjacent integer
     changes (+/-1, +/-3, etc.) flip parity and re-fire the
     animation.  Same-parity changes (+/-2, +/-4 SoC jumps when
     telemetry sampling is sparse) skip that tick's wobble -- the
     count-up transition still runs, so the value change is
     still visible, just without the extra punctuation.

     Keyframe magnitudes are driven by CSS custom properties so
     callers can opt into a louder or quieter wobble per element
     (e.g., a battery hero alert vs. a dashboard tile) by
     setting the var on the element or a parent class.  Defaults
     match the current dashboard look.

     Note: CSS does not allow var() in @keyframes selector
     percentages -- only in property values -- so the timing
     stops (12% / 42% / 72%) are hard-coded.  Only the
     magnitudes vary. */
  .tween-int, .tween-pct, .tween-km, .tween-num, .tween-dec {
    --wobble-peak-scale: 1.06;
    --wobble-peak-y: -2px;
    --wobble-dip-scale: 0.98;
    --wobble-dip-y: 1px;
    --wobble-reb-scale: 1.015;
    --wobble-reb-y: 0px;
  }
  @keyframes wobble-a {
    0%   { transform: scale(1) translateY(0); }
    12%  { transform: scale(var(--wobble-peak-scale)) translateY(var(--wobble-peak-y)); }
    42%  { transform: scale(var(--wobble-dip-scale))  translateY(var(--wobble-dip-y)); }
    72%  { transform: scale(var(--wobble-reb-scale))  translateY(var(--wobble-reb-y)); }
    100% { transform: scale(1) translateY(0); }
  }
  @keyframes wobble-b {
    0%   { transform: scale(1) translateY(0); }
    12%  { transform: scale(var(--wobble-peak-scale)) translateY(var(--wobble-peak-y)); }
    42%  { transform: scale(var(--wobble-dip-scale))  translateY(var(--wobble-dip-y)); }
    72%  { transform: scale(var(--wobble-reb-scale))  translateY(var(--wobble-reb-y)); }
    100% { transform: scale(1) translateY(0); }
  }
  section.summary-stats .stat-label { font-size: var(--text-xs);
                                      color: var(--fg-soft);
                                      text-transform: uppercase;
                                      letter-spacing: 0.05em;
                                      font-weight: 600;
                                      margin-top: 0.2rem; }
  nav.dashboard-cards { display: grid;
                        grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
                        gap: var(--space-sm); margin: var(--space-md) 0 var(--space-lg); }
  nav.dashboard-cards a.card { display: block; padding: var(--space-md);
                               background: var(--brand-tint);
                               border: 1px solid var(--brand-tint-hover);
                               border-radius: 6px; color: inherit;
                               text-decoration: none;
                               transition: background 120ms ease; }
  nav.dashboard-cards a.card:hover { background: var(--brand-tint-hover); }
  nav.dashboard-cards a.card h2 { margin: 0 0 0.3rem;
                                  font-size: 1.05rem;
                                  color: var(--brand-text); }
  nav.dashboard-cards a.card p { margin: 0;
                                 font-size: var(--text-xs);
                                 color: var(--fg-muted); }
  p.signed-in-as { color: var(--fg-muted); font-size: var(--text-xs); }
  /* ---- Chart panels.  Every data chart (drive series, battery SoC,
     degradation, tires) sits in the same hairline card with an
     eyebrow header row: metric label left, latest-value chip right.
     The data visualizations ARE the product's imagery, so they get
     the most generous surface treatment on the page. */
  figure.drive-chart, section.battery-chart {
    margin: 1.1rem 0;
    background: var(--bg-elev);
    border: 1px solid var(--rule);
    border-radius: var(--r-lg);
    padding: 1rem 1.15rem 0.8rem;
  }
  .chart-head { display: flex; align-items: baseline;
                justify-content: space-between; gap: 1rem;
                margin: 0 0 0.65rem; }
  .chart-label { font-size: var(--text-xs); font-weight: 600;
                 color: var(--fg-soft);
                 text-transform: uppercase; letter-spacing: 0.06em; }
  .chart-latest { font-family: var(--font-mono);
                  font-variant-numeric: tabular-nums;
                  font-weight: 600; font-size: 1.05rem;
                  color: var(--brand); white-space: nowrap; }
  /* Axis + grid chrome inside any chart SVG.  text uses the mono
     stack so timestamps + tick values read instrument-grade. */
  svg.drive-series .gridline, svg.battery-soc .gridline,
  svg.battery-deg .gridline {
    stroke: var(--chart-grid); stroke-width: 1; }
  svg.battery-soc .gridline--base { stroke: var(--rule-strong); }
  svg.drive-series .tick, svg.battery-soc .tick, svg.battery-deg .tick {
    stroke: var(--chart-axis); stroke-width: 1; }
  svg.drive-series text.ax, svg.battery-soc text.ax, svg.battery-deg text.ax {
    font-family: var(--font-mono); font-size: 10.5px;
    fill: var(--chart-axis); font-variant-numeric: tabular-nums; }
  svg.drive-series text.ax-soft, svg.battery-soc text.ax-soft,
  svg.battery-deg text.ax-soft { fill: var(--fg-soft); font-size: 10px; }
  /* Endpoint dot -- white halo ring so it sits crisply on the line. */
  .series-end { stroke: #fff; stroke-width: 1.5; }
  .deg-dot    { stroke: #fff; stroke-width: 1.25; }
  /* Multi-line chart legend (tires). */
  .chart-legend { display: flex; gap: 1rem; flex-wrap: wrap;
                  margin-top: 0.55rem; }
  .chart-legend__item { display: inline-flex; align-items: center;
                        gap: 0.4rem; font-size: var(--text-xs);
                        color: var(--fg-muted); }
  .chart-legend__dot { display: inline-block; width: 9px; height: 9px;
                       border-radius: 3px; }

  /* Battery SoC trend + degradation SVGs */
  svg.battery-soc, svg.battery-deg {
    width: 100%; height: auto; max-width: 100%; display: block; }
  /* When a heading/blurb lives inside the chart panel (degradation),
     pull it tight to the top so the panel doesn't read double-headed. */
  section.battery-chart > h2 { margin: 0.15rem 0 0.35rem; font-size: var(--text-lg); }
  section.battery-chart > p.muted { margin: 0 0 0.9rem; font-size: var(--text-sm);
                                    color: var(--fg-soft); max-width: 60ch; }
  p.battery-legend { display: flex; gap: 0.5rem; align-items: center;
                     margin: 0.6rem 0 0; font-size: var(--text-xs);
                     color: var(--fg-muted); }
  span.legend-pill { display: inline-block; padding: 0.1rem 0.5rem;
                     border-radius: 999px; font-size: var(--text-xs);
                     font-weight: 600; line-height: 1.5;
                     background: var(--brand-tint); color: var(--brand-text); }
  span.legend-pill--bat-good { background: var(--bat-good-tint); color: var(--bat-good); }
  span.legend-pill--bat-warn { background: var(--bat-warn-tint); color: var(--bat-warn); }
  span.legend-pill--bat-low  { background: var(--bat-low-tint);  color: var(--bat-low); }
  /* Battery icon -- universal iOS/macOS-style filled-bar gauge.
     Three severity buckets driven by a class on the wrapping
     element; fill scale is a smoothly-tweened @property so the
     bar visibly grows on SoC change. Brand purple stays for the
     chart polyline + chrome; this icon is information color. */
  @property --soc-frac { syntax: '<number>'; initial-value: 0; inherits: false; }
  .bat-icon { display: inline-block; vertical-align: middle;
              line-height: 0; }
  .bat-icon svg { display: block; width: 56px; height: 26px; }
  .bat-icon .bat-body { fill: none; stroke: var(--fg-muted);
                        stroke-width: 1.5; }
  .bat-icon .bat-cap  { fill: var(--fg-muted); }
  .bat-icon .bat-fill { transform-origin: left center;
                        transform: scaleX(var(--soc-frac, 0));
                        transition: --soc-frac 800ms cubic-bezier(.2,.7,.2,1),
                                    fill 600ms ease; }
  .bat-icon.bat-good .bat-fill { fill: var(--bat-good); }
  .bat-icon.bat-warn .bat-fill { fill: var(--bat-warn); }
  .bat-icon.bat-low  .bat-fill { fill: var(--bat-low); }
  .bat-icon.bat-good .bat-body { stroke: var(--bat-good); }
  .bat-icon.bat-warn .bat-body { stroke: var(--bat-warn); }
  .bat-icon.bat-low  .bat-body { stroke: var(--bat-low);
                                 animation: bat-low-pulse 1600ms ease-in-out infinite; }
  .bat-icon.bat-good .bat-cap  { fill: var(--bat-good); }
  .bat-icon.bat-warn .bat-cap  { fill: var(--bat-warn); }
  .bat-icon.bat-low  .bat-cap  { fill: var(--bat-low); }
  /* Low-SoC pulse on the icon outline only -- subtle 'attention'
     signal that doesn't move the bar fill itself. Honors
     prefers-reduced-motion. */
  @keyframes bat-low-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
  }
  /* Larger battery on the dashboard 'battery now' stat tile.  Sits
     above the percent number so the icon + value read as one
     glanceable unit. */
  .stat-battery .bat-icon svg { width: 88px; height: 40px; }
  .stat-battery .stat-battery-row {
    display: flex; align-items: center; gap: 0.6rem;
    margin-bottom: 0.15rem; }
  /* Hero battery block on /app/battery -- big, glanceable, with
     the level legend baked in.  Stays inside design-system spacing
     and uses --bg-elev card surface like other panels. */
  section.battery-hero { display: grid;
                         grid-template-columns: auto 1fr;
                         align-items: center; gap: var(--space-md);
                         background: var(--bg-elev);
                         border: 1px solid var(--rule);
                         border-radius: 6px; padding: var(--space-md) var(--space-md);
                         margin: var(--space-md) 0 var(--space-md);
                         /* A container so the hero % scales with the PANEL's
                            width (cqi), not the viewport -- it kept hitting a
                            46px cap on a wide screen and read as static. */
                         container-type: inline-size; }
  section.battery-hero .bat-icon svg { width: 112px; height: 50px; }
  /* Hero glyph reads in BRAND at healthy levels, not the loud success-green
     (which clashed as the single non-purple slab on the page) -- the green
     "Healthy" pill above carries the severity signal. Warn/low keep their
     amber/red alert colours: rare, and a low battery should look alarming. */
  section.battery-hero.bat-good .bat-fill,
  section.battery-hero.bat-good .bat-cap  { fill: var(--brand); }
  section.battery-hero.bat-good .bat-body { stroke: var(--brand); }
  section.battery-hero .hero-num     { display: flex;
                                       flex-direction: column;
                                       gap: 0.1rem; }
  section.battery-hero .hero-pct     { font-size: clamp(2.6rem, 7cqi, 4.25rem);
                                       font-weight: 700;
                                       line-height: 1.1;
                                       color: var(--brand-text);
                                       font-variant-numeric: tabular-nums; }
  section.battery-hero .hero-label   { font-size: var(--text-xs);
                                       color: var(--fg-soft);
                                       text-transform: uppercase;
                                       letter-spacing: 0.05em;
                                       font-weight: 600; }
  section.battery-hero .hero-band    { font-size: var(--text-xs);
                                       margin-top: 0.35rem;
                                       display: inline-block;
                                       padding: 0.1rem 0.55rem;
                                       border-radius: 999px;
                                       font-weight: 600;
                                       width: fit-content; }
  section.battery-hero.bat-good .hero-band { background: var(--bat-good-tint); color: var(--bat-good); }
  section.battery-hero.bat-warn .hero-band { background: var(--bat-warn-tint); color: var(--bat-warn); }
  section.battery-hero.bat-low  .hero-band { background: var(--bat-low-tint);  color: var(--bat-low); }
  @media (max-width: 480px) {
    section.battery-hero { grid-template-columns: 1fr; gap: 0.6rem; }
    section.battery-hero .bat-icon svg { width: 96px; height: 43px; }
  }
  /* Polyline draw-in: pathLength="1" normalizes the path so a
     dasharray/dashoffset of 1 covers the entire stroke regardless
     of underlying length. CSS-only, fires on first render --
     idiomorph preserves the element instance so subsequent SSE
     morphs do NOT re-draw (correct: first impression matters,
     mid-session re-draws would be noise). */
  svg.drive-track polyline.path-draw,
  svg.battery-soc polyline.path-draw,
  svg.battery-deg polyline.path-draw,
  svg.drive-series polyline.path-draw {
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
    animation: poly-draw 1200ms cubic-bezier(.2,.7,.2,1) 80ms forwards; }
  /* The series area wash fades in just behind the line draw so the
     chart composes rather than popping; the endpoint dot lands last
     with a small overshoot pop -- the line literally arrives at the
     latest reading. */
  svg.drive-series .series-area, svg.battery-soc .soc-area {
    animation: chart-fade 700ms ease 600ms backwards; }
  .series-end { transform-origin: center; transform-box: fill-box;
                animation: dot-pop 550ms cubic-bezier(.2,.9,.3,1.35) 1050ms backwards; }
  @keyframes chart-fade { from { opacity: 0; } to { opacity: 1; } }
  @keyframes dot-pop {
    0%   { transform: scale(0); opacity: 0; }
    65%  { transform: scale(1.6); opacity: 1; }
    100% { transform: scale(1); opacity: 1; }
  }
  @keyframes poly-draw { to { stroke-dashoffset: 0; } }
  /* Drive-track start/end markers gentle ping on mount. */
  svg.drive-track circle.marker { transform-origin: center;
                                  transform-box: fill-box;
                                  animation: marker-pop 600ms cubic-bezier(.2,.9,.2,1.2) 1100ms backwards; }
  @keyframes marker-pop {
    0%   { transform: scale(0); opacity: 0; }
    60%  { transform: scale(1.4); opacity: 1; }
    100% { transform: scale(1); opacity: 1; }
  }
  /* Severity-band background tints on the SoC trend chart's plot
     area.  Subtle (low-alpha) so the polyline still reads as the
     primary signal.  Renders as <rect> children inside the SVG --
     these classes just style the fills. */
  svg.battery-soc .band-low  { fill: var(--bat-low);  opacity: 0.06; }
  svg.battery-soc .band-warn { fill: var(--bat-warn); opacity: 0.05; }
  svg.battery-soc .band-good { fill: var(--bat-good); opacity: 0.04; }
  /* .soc-area fill is the inline url(#bsoc-grad) gradient set by the
     renderer -- no CSS fill here or it would override the gradient. */
  /* Refined nav-card hover -- still no shadows, no transform.
     Adds a subtle border tone shift + a brand-bright accent bar
     along the left edge that grows on hover.  Reads as a
     directional hint without breaking the flat-card spec. */
  nav.dashboard-cards a.card { position: relative; overflow: hidden;
                               transition: background 160ms ease,
                                           border-color 160ms ease; }
  nav.dashboard-cards a.card::before {
    content: ''; position: absolute; left: 0; top: 0; bottom: 0;
    width: 3px; background: var(--brand);
    transform: scaleY(0); transform-origin: bottom center;
    transition: transform var(--dur-medium) var(--ease-out); }
  nav.dashboard-cards a.card:hover { border-color: var(--brand-bright); }
  nav.dashboard-cards a.card:hover::before { transform: scaleY(1); transform-origin: top center; }
  /* Stat tiles ride their section's entry rise (main#app-content > *
     rise-in stagger).  They intentionally carry NO per-tile entry
     animation of their own: measured (Playwright)
     that idiomorph does NOT preserve these inner .stat nodes across a
     same-view morph -- it recreates them on every window change / SSE
     tick -- so a per-tile stat-rise replayed on UNCHANGED tiles every
     update, which reads as jitter, not flair.  The @property count-up
     tweens carry the live value changes; the section's rise carries
     the entry.  (stat-rise keyframe removed with this note.) */
  /* Sparkline cell in drives + charging tables.  SVG sized via
     CSS so the inline markup stays minimal. */
  td.spark-cell { width: 80px; padding-top: 0.4rem; padding-bottom: 0.4rem; }
  td.spark-cell svg { width: 72px; height: 22px; display: block; }
  td.spark-cell .spark-line { fill: none; stroke: var(--brand);
                              stroke-width: 1.5; stroke-linejoin: round;
                              stroke-linecap: round; }
  td.spark-cell .spark-area { fill: var(--brand); opacity: 0.10; }
  /* Live telemetry indicator -- the second sanctioned use of
     --tesla-red per the design system.  Small dot in the
     site-header right cluster that pulses while the SSE stream
     is open.  Static color when prefers-reduced-motion is set. */
  .live-dot { display: inline-flex; align-items: center; gap: 0.35rem;
              font-size: var(--text-xs); color: var(--fg-muted);
              text-transform: uppercase; letter-spacing: 0.05em;
              font-weight: 600; }
  .live-dot::before { content: ''; width: 8px; height: 8px;
                      border-radius: 999px;
                      background: var(--tesla-red);
                      box-shadow: 0 0 0 0 var(--tesla-red);
                      animation: live-pulse 1800ms cubic-bezier(.2,.7,.2,1) infinite; }
  @keyframes live-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(227, 25, 55, 0.55); }
    70%  { box-shadow: 0 0 0 6px rgba(227, 25, 55, 0); }
    100% { box-shadow: 0 0 0 0 rgba(227, 25, 55, 0); }
  }

  /* Hero numerics -- the battery percent + dashboard stat values
     read as digital-instrument numbers, not body text.  System
     mono stack (already in --font-mono) with tnum + ss01 where
     supported; tightened letter-spacing; preserve tabular widths
     so the count-up animation never reflows. */
  section.battery-hero .hero-pct,
  section.summary-stats .stat-value {
    font-family: var(--font-mono);
    font-feature-settings: 'tnum' 1, 'ss01' 1, 'zero' 1;
    letter-spacing: -0.04em;
  }
  /* The .hero-pct gets a slight color depth -- brand-text for the
     digits, brand-bright on the trailing '%' rendered via the
     ::before counter content.  We can't split the counter content,
     so instead we accent the whole number on the battery-hero
     severity ramp (good=success-dark, warn=warning-dark,
     low=danger-dark) when the severity class is set on the hero
     section. Falls back to brand-text otherwise. */
  section.battery-hero.bat-good .hero-pct { color: var(--brand); }
  section.battery-hero.bat-warn .hero-pct { color: var(--bat-warn); }
  section.battery-hero.bat-low  .hero-pct { color: var(--bat-low); }

  /* Section heading accent -- 2.6rem brand-color rule under each
     h1 in JVM-served pages (auth, /app, admin, waitlist).
     Echoes the marketing callout's left-border treatment but
     turned horizontal so it doesn't fight the page edge.  Scoped
     to in-app shells (main#app-content + main.prose-shell) so the
     marketing site stays untouched. */
  main#app-content h1,
  main.prose-shell h1 {
    position: relative;
    padding-bottom: 0.55rem;
    font-size: var(--text-2xl);
    line-height: 1.1;
  }
  /* Section + sub headings on the fluid scale.  Kept token-driven so a new
     view's headings inherit the system instead of picking a magic size. */
  main#app-content h2 { font-size: var(--text-xl); line-height: 1.2; }
  main#app-content h3 { font-size: var(--text-lg); line-height: 1.25; }
  /* Container context for the in-app content column.  Components
     inside query THIS element's inline size via `@container app-main', so
     they reflow on the width they actually have -- not the viewport.  That
     makes layout robust to anything that changes the column width without
     changing the viewport (nav wrap, a future sidebar, i18n text growth),
     and demotes @media to a structural backstop rather than the driver.
     inline-size containment only constrains the inline axis, so
     block height + the SVG/map aspect-ratios inside are unaffected. */
  main#app-content, main.prose-shell { container: app-main / inline-size; }
  main#app-content h1::after,
  main.prose-shell h1::after {
    content: ''; position: absolute; left: 0; bottom: 0;
    width: 2.6rem; height: 2px;
    background: var(--brand);
    border-radius: 1px;
  }

  /* Stat tile gets a 3px brand-tint left border -- gives the row
     a branded vertical rhythm without breaking the flat-card
     spec.  Battery stat carries the severity color so the LEFT
     edge + the icon agree.  Default tint for non-battery stats. */
  section.summary-stats .stat {
    border-left: 3px solid var(--brand-tint);
    padding-left: 0.85rem;
  }
  section.summary-stats .stat.stat-battery.bat-good { border-left-color: var(--bat-good); }
  section.summary-stats .stat.stat-battery.bat-warn { border-left-color: var(--bat-warn); }
  section.summary-stats .stat.stat-battery.bat-low  { border-left-color: var(--bat-low); }

  /* Battery hero gets a faint severity wash -- ultra-low-opacity
     color-mix radial that's barely visible on a quality monitor
     but anchors the block as the page's primary visual.  Brand
     spec says no gradients; this is a tonal wash, not a
     decorative one -- the strongest stop is < 8% saturation. */
  section.battery-hero.bat-good {
    background: radial-gradient(ellipse at top left,
      color-mix(in srgb, var(--bat-good) 6%, var(--bg-elev)),
      var(--bg-elev) 65%);
  }
  section.battery-hero.bat-warn {
    background: radial-gradient(ellipse at top left,
      color-mix(in srgb, var(--bat-warn) 7%, var(--bg-elev)),
      var(--bg-elev) 65%);
  }
  section.battery-hero.bat-low {
    background: radial-gradient(ellipse at top left,
      color-mix(in srgb, var(--bat-low) 8%, var(--bg-elev)),
      var(--bg-elev) 65%);
  }

  /* Delta bar -- tiny horizontal bar inside Δ SoC cells.  Inline
     `--mag` (0..1) scales the fill horizontally; direction class
     picks the color (drive=brand purple = energy spent, charge=
     bat-good green = energy gained).  Bar + value share the cell
     with a small gap; tabular-nums on the value keeps row heights
     aligned. */
  td.delta-cell { color: inherit; font-weight: 600;
                  white-space: nowrap; }
  td.delta-cell .delta-row { display: inline-flex; align-items: center;
                             gap: 0.5rem; }
  td.delta-cell .delta-bar { position: relative; flex: 0 0 36px;
                             height: 6px; border-radius: 3px;
                             background: var(--rule);
                             overflow: hidden; }
  td.delta-cell .delta-bar::after { content: '';
                                    position: absolute; inset: 0;
                                    transform-origin: left center;
                                    transform: scaleX(var(--mag, 0));
                                    transition: transform 700ms cubic-bezier(.2,.7,.2,1);
                                    background: var(--brand); }
  td.delta-cell.delta-up   { color: var(--bat-good); }
  td.delta-cell.delta-up   .delta-bar::after { background: var(--bat-good); }
  td.delta-cell.delta-down { color: var(--brand-text); }
  td.delta-cell.delta-down .delta-bar::after { background: var(--brand); }
  td.delta-cell .delta-val { font-variant-numeric: tabular-nums; }

  /* Severity dot -- inline 8px circle for the row 'End' cell so
     each row carries the iOS-battery mental model.  Sits before
     the percent value. */
  .soc-dot { display: inline-block; width: 8px; height: 8px;
             border-radius: 999px; vertical-align: middle;
             margin-right: 0.45rem;
             transition: background-color 600ms ease; }
  .soc-dot.bat-good { background: var(--bat-good); }
  .soc-dot.bat-warn { background: var(--bat-warn); }
  .soc-dot.bat-low  { background: var(--bat-low); }

  /* Ghost battery -- low-opacity outlined-only variant for empty
     states.  Holds layout when no SoC is available yet, so the
     dashboard slot doesn't collapse and the user knows where the
     gauge will appear. */
  .bat-icon.bat-ghost { opacity: 0.42; }
  .bat-icon.bat-ghost .bat-body { stroke: var(--fg-soft); }
  .bat-icon.bat-ghost .bat-cap  { fill: var(--fg-soft); }
  .bat-icon.bat-ghost .bat-fill { fill: none; }

  /* Empty stat tile -- ghost battery + dash placeholder so the
     dashboard layout doesn't reflow when SoC arrives mid-session.
     Quieter typography than the live tile. */
  section.summary-stats .stat.stat-battery.bat-ghost-tile {
    border-left-color: var(--rule-strong); }
  section.summary-stats .stat.stat-battery.bat-ghost-tile .stat-value {
    color: var(--fg-soft); }

  /* ---- Sparkle: ultra-fine paper grain on the page background.
     SVG noise turbulence at 2.2% opacity -- imperceptible per
     pixel, visible as faint authenticity over large surfaces.
     Brand spec says no textures, but the user opted in for 'a
     tiny bit' of spice; opacity is dialed so it never competes
     with content.

     Scoped to ALL JVM-served pages (auth, /app, admin, waitlist)
     -- the marketing site is served by Cloudflare Pages and does
     not load this stylesheet, so it stays grain-free.  Comment
     out to revert. */
  body {
    background-color: var(--bg);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.08 0 0 0 0 0.04 0 0 0 0 0.07 0 0 0 0.6 0'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.022'/></svg>");
  }

  /* Brand-mark hover -- the header dot quietly brightens on
     hover of the brand link.  No transform, no scale; just a
     subtle color step + a 1px shadow ring in brand-bright at
     low opacity.  The first dab of 'sparkle' that fits the
     hairline aesthetic. */
  .site-header__brand:hover .site-header__brand-mark {
    background: var(--brand-bright);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-bright) 18%, transparent);
    transition: background 220ms ease, box-shadow 220ms ease;
  }

  /* Mini live-pulse caption -- attaches under each widget that
     receives streamed data, mirroring the header live-dot.  Same
     keyframes; smaller dot (6px), smaller caption.  Sits with
     0.55rem top margin so it reads as 'this widget is live'
     rather than a separate row. */
  .live-mini { display: inline-flex; align-items: center; gap: 0.4rem;
               margin-top: 0.55rem;
               font-size: var(--text-2xs); color: var(--fg-soft);
               text-transform: uppercase; letter-spacing: 0.08em;
               font-weight: 600; }
  .live-mini::before { content: ''; width: 6px; height: 6px;
                       border-radius: 999px;
                       background: var(--tesla-red);
                       box-shadow: 0 0 0 0 var(--tesla-red);
                       animation: live-pulse 1800ms cubic-bezier(.2,.7,.2,1) infinite; }
  /* Inside a stat tile the caption hugs the bottom; the tile
     becomes a flex column so the caption nests neatly below the
     value+label pair. */
  section.summary-stats .stat { display: flex; flex-direction: column; }
  section.summary-stats .stat .live-mini { margin-top: auto;
                                            padding-top: 0.5rem; }

  /* ---- Bolder stat typography -- bumps the instrument-panel
     feel without breaking layout.  The battery icon scales in
     proportion (98px wide) so the row reads as one composed
     unit instead of icon + small number.  The former fixed
     1.75rem is now folded into --display-sm (the clamp peaks at
     1.8rem on a wide viewport, ~1.74rem at 1280); only the
     tighter tracking remains here. */
  section.summary-stats .stat-value { letter-spacing: -0.05em; }
  .stat-battery .bat-icon svg { width: 98px; height: 44px; }
  /* The .hero-pct already uses font-mono; its size now scales with the
     hero panel width (clamp(2.6rem, 7cqi, 4.25rem)); only the
     deeper tracking for the Bloomberg-terminal feel remains here. */
  section.battery-hero .hero-pct { letter-spacing: -0.055em; }

  /* ---- Nav-card icon glyphs.  Drives/Charging/Battery/Vehicles
     each carry a small Lucide-style outlined SVG (24x24, 1.5px
     stroke) in the top-right of the card.  Brand-color stroke at
     65% opacity -- icons read as decorative anchors, not
     primary affordances.  On hover the icon brightens + nudges
     1px right, picking up the left-edge accent's directional
     hint. */
  nav.dashboard-cards a.card { padding-right: 3rem; min-height: 4.2rem; }
  nav.dashboard-cards a.card .card-icon {
    position: absolute; top: 0.85rem; right: 0.95rem;
    width: 24px; height: 24px;
    color: var(--brand);
    opacity: 0.55;
    transition: opacity var(--dur-medium) ease,
                transform var(--dur-medium) var(--ease-out),
                color var(--dur-medium) ease;
  }
  nav.dashboard-cards a.card:hover .card-icon {
    opacity: 1;
    color: var(--brand-bright);
    transform: translateX(2px);
  }
  nav.dashboard-cards a.card .card-icon svg {
    width: 24px; height: 24px;
    stroke: currentColor; fill: none;
    stroke-width: 1.5;
    stroke-linecap: round; stroke-linejoin: round;
  }

  /* ---- Auth perimeter polish: signup, login, signup-verify,
     waitlist confirmation/error pages.  These all use
     `main.prose-shell' as their container with bare form +
     table children.  The forms get a tinted-card treatment with
     a brand left-edge accent (echoing the marketing callout);
     the admin tables get the polished thead + hairline row
     dividers from .charge-log without needing each handler to
     opt in by class. */
  main.prose-shell form {
    background: var(--bg-elev);
    border: 1px solid var(--rule);
    border-left: 3px solid var(--brand);
    border-radius: var(--r-md);
    padding: 1.25rem 1.5rem 1.4rem;
    margin: 1.25rem 0 1.5rem;
    max-width: 520px;
  }
  main.prose-shell form label { margin: 0.75rem 0 0.5rem; }
  main.prose-shell form label:first-child { margin-top: 0; }
  main.prose-shell form input[type="file"] {
    padding: 0.45rem 0.55rem;
  }
  /* Buttons inside auth/admin forms get a touch more presence --
     full-width on narrow inputs, comfortable padding.  Stays
     within the existing brand button styling. */
  main.prose-shell form button[type="submit"] {
    margin-top: 1rem;
  }
  /* Cross-link paragraphs ('I already have an account', 'Create
     an account') get muted typography. */
  main.prose-shell > p > a[href^="/login"],
  main.prose-shell > p > a[href^="/signup"] {
    font-size: var(--text-sm);
  }
  /* Admin + waitlist-admin tables -- match the in-app
     charge-log/vehicles-list polish so operator views feel like
     they're part of the same product, not a phpMyAdmin escape
     hatch.  Scoped to bare tables inside .prose-shell (auth
     pages don't render tables; admin pages do).  Tabular nums on
     every cell so signup timestamps + counts align. */
  main.prose-shell table {
    width: 100%;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
    background: var(--bg-elev);
    border: 1px solid var(--rule);
    border-radius: var(--r-md);
    overflow: hidden;
    margin: 1.25rem 0;
    font-size: var(--text-sm);
  }
  main.prose-shell table th,
  main.prose-shell table td {
    padding: 0.55rem 0.8rem;
    text-align: left;
    border-bottom: 1px solid var(--rule);
  }
  main.prose-shell table th {
    background: var(--brand-tint);
    color: var(--brand-text);
    font-weight: 600;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-bottom-color: var(--brand-tint-hover);
  }
  main.prose-shell table tbody tr:last-child td { border-bottom: 0; }
  main.prose-shell table tbody tr:hover { background: var(--bg-soft); }
  main.prose-shell table td em {
    color: var(--fg-soft);
    font-style: normal;
    font-size: var(--text-xs);
  }

  /* H1 typography in auth + admin -- bumped slightly with tighter
     tracking so they read as instrument-style page titles, not
     marketing-blog headings.  Same vibe as the /app stat values. */
  main.prose-shell h1 {
    letter-spacing: -0.025em;
    font-weight: 600;
  }
  main.prose-shell h2 {
    font-size: var(--text-lg);
    letter-spacing: -0.01em;
    color: var(--fg-muted);
    margin-top: 2.25rem;
  }

  /* prefers-reduced-motion -- the spatial-motion-free degradation
     (option (a) of the old design-pass note, per Apple HIG / WCAG SC
     2.3.3).  Under OS reduce:
       - entrance rises collapse to a quick opacity fade (no translate,
         and nothing can be stranded invisible by a paused
         backwards-fill animation);
       - draw-ins + pops complete instantly (duration ~0, fill still
         applies, so lines and dots land in their final state);
       - the looping pulses (live dot, low-battery, charging breath)
         follow the marketing stylesheet's `--decorative-motion' knob,
         which defaults to `running' per the brand decision that the
         live pulse shows for everyone -- flip the knob in style.css
         to `paused' to honour the OS setting fully.
     INFORMATIONAL motion (count-up @property transitions, battery
     fill scale, severity colour cross-fades) always runs: it reports
     a value change rather than decorating one. */
  @media (prefers-reduced-motion: reduce) {
    main#app-content > * {
      animation-name: fade-only !important;
      animation-duration: var(--dur-fast) !important;
      animation-delay: 0ms !important; }
    .series-end,
    svg.drive-track circle.marker,
    table.charge-log tbody tr,
    .tween-int, .tween-pct, .tween-km, .tween-num, .tween-dec {
      animation: none !important; }
    svg.drive-track polyline.path-draw,
    svg.battery-soc polyline.path-draw,
    svg.battery-deg polyline.path-draw,
    svg.drive-series polyline.path-draw,
    svg.drive-series .series-area, svg.battery-soc .soc-area {
      animation-duration: 0.01ms !important;
      animation-delay: 0ms !important; }
    .live-dot::before, .live-mini::before,
    .bat-icon.bat-low .bat-body,
    .bat-charging .bat-fill {
      animation-play-state: var(--decorative-motion, running); }
  }
  @keyframes fade-only { from { opacity: 0; } to { opacity: 1; } }


/* The /app header carries MORE in its right cluster than the marketing
   site (lang toggle + LIVE badge + Account cog + optional Operator menu)
   alongside a full in-app nav.  As a single `nowrap' flex row that cluster
   plus the nav exceeds the viewport well before the marketing site's
   640px mobile breakpoint: measured at 745px the cta cluster's right edge
   reached 829px -> the page horizontally scrolled AND the Account cog +
   LIVE badge bled off-screen unreachable.  The
   marketing stylesheet only hides .site-header__nav at <=640px and has no
   medium step, so nothing collapsed the app cluster in the 641-900px band.

   Fix: drop the app nav to its own full-width row 2 at a MEDIUM
   breakpoint -- before the single-row cluster can overflow --

   i18n calibration: the breakpoint must clear the
   LONGEST language, not English.  Measured single-row need on prod: ~833px
   (Polish, operator account); modelling operator + the longest nav labels
   (German/Finnish, ~1.4x) lands ~980px.  So the threshold is 1000px (was
   900, English-tuned -- a German/Finnish operator header needs ~924-986px
   and would overflow in the 900-986 band).  >1000px keeps the single-row
   desktop layout on every standard screen; <=1000 is the clean two-tier.
   The nav's own flex-wrap (below) is the last-resort fallback if a label
   set ever exceeds even a full row.
   reusing the same wrap mechanism the phone breakpoint already used,
   just earlier.  Brand + the full cta cluster stay on row 1 (they fit
   comfortably at >=641px); the nav wraps below them with its own rule.
   app.css only loads on /app, so the marketing header is untouched.  The
   change is a viewport-driven reflow at the breakpoint, not an
   interaction shift, so it introduces no click-time jump. */
@media (max-width: 1000px) {
  .site-header__inner { flex-wrap: wrap; justify-content: space-between;
                        row-gap: 0.5rem; }
  .site-header__cta   { order: 2; }
  .site-header__nav   { display: flex; order: 3; width: 100%;
                        flex-wrap: wrap; align-items: center;
                        gap: 0.15rem 0.4rem;
                        border-top: 1px solid var(--rule);
                        padding-top: 0.5rem; }
  .site-header__nav > a,
  .site-header__nav .nav-group__summary { padding: 0.3rem 0.45rem; }
}

@media (max-width: 640px) {
  /* On a phone the summary-stat tiles narrow to ~131px; a wide value
     (e.g. a multi-thousand-kWh charging total) nowrap overflows its
     tile by a few px and pushes the page into horizontal scroll.
     --display-sm already floors to
     1.4rem at this width, so no font-size override is needed -- but
     KEEP the shrink-to-fit guards (max-width / overflow-wrap) and the
     phone-tier tracking so a pathologically wide value still sits
     inside the box. */
  section.summary-stats .stat-value { letter-spacing: -0.04em;
                                      max-width: 100%; overflow-wrap: anywhere; }
}


/* Dashboard "Now" hierarchy: charge + range lead in a prominent pair; the
   rest are a quieter, denser secondary grid below. */
.now-primary { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-sm);
               margin: var(--space-md) 0 var(--space-sm); }
.now-lead { display: flex; align-items: center; gap: var(--space-md);
            background: var(--bg-elev); border: 1px solid var(--rule);
            border-radius: 6px; padding: var(--space-md); }
.now-lead .bat-icon svg { width: 84px; height: 38px; }
.now-lead__value { font-size: var(--display-md); font-weight: 700; line-height: 1.05;
                   color: var(--brand-text); white-space: nowrap;
                   font-variant-numeric: tabular-nums; }
.now-lead__label { margin-top: 0.25rem; font-size: var(--text-xs); color: var(--fg-soft);
                   text-transform: uppercase; letter-spacing: 0.05em;
                   font-weight: 600; }
/* Brand glyph at healthy in the lead card (matches the battery hero). */
.now-lead.stat-battery.bat-good .bat-fill,
.now-lead.stat-battery.bat-good .bat-cap  { fill: var(--brand); }
.now-lead.stat-battery.bat-good .bat-body { stroke: var(--brand); }
.now-secondary .stat { padding: 0.7rem 0.95rem; }
.now-secondary .stat-value { font-size: 1.15rem; }
/* The charge+range lead pair drops to a single column once the content
   column itself is too narrow for two readable lead cards (~30rem),
   regardless of viewport -- a container query, not a viewport one.
   The old `@media (max-width: 560px)' fired at roughly the
   same point today but broke the moment the column stopped tracking the
   viewport; querying `app-main' keeps the reflow tied to real space. */
@container app-main (max-width: 30rem) {
  .now-primary { grid-template-columns: 1fr; }
}

/* ---- Activity panel: segmented proportion bar + legend row. ---- */
.activity-panel { background: var(--bg-elev); border: 1px solid var(--rule);
                  border-radius: var(--r-lg); padding: var(--space-md) var(--space-md) var(--space-sm);
                  margin: var(--space-sm) 0 var(--space-md); }
.activity-bar { display: flex; height: 12px; border-radius: 6px;
                overflow: hidden; gap: 2px; background: var(--bg-soft); }
.activity-seg { min-width: 3px; }
/* The activity bar + its segments carry NO entry animation of their
   own: the .activity-panel that wraps them is a rise-in child of
   #app-content, so the whole panel eases in as one unit.  An earlier
   per-segment seg-grow replayed on every same-view morph (idiomorph
   recreates the inner segs; measured 8x on a window change) -- removed.
   Segment widths are set inline from
   the data and are never transitioned (they are flex widths; animating
   them would animate layout on an SSE-live element). */
.act-driving  { background: var(--act-driving); }
.act-charging { background: var(--act-charging); }
.act-idle     { background: var(--act-idle); }
.act-offline  { background: var(--act-offline); }
.activity-legend { display: grid;
                   grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
                   gap: var(--space-xs) var(--space-md); margin-top: var(--space-sm); }
.activity-item { display: flex; align-items: baseline; gap: var(--space-xs);
                 min-width: 0; }
.act-dot { display: inline-block; width: 9px; height: 9px;
           border-radius: 3px; flex: 0 0 auto;
           transform: translateY(0.5px); }
.activity-pct { font-family: var(--font-mono); font-weight: 600;
                font-size: 1.05rem; font-variant-numeric: tabular-nums;
                color: var(--fg); }
.activity-meta { font-size: var(--text-xs); color: var(--fg-soft);
                 text-transform: uppercase; letter-spacing: 0.04em;
                 font-weight: 600; white-space: nowrap;
                 overflow: hidden; text-overflow: ellipsis; }

/* ---- Account & data / service tools: collapsed <details>.  Quiet by
   default -- a hairline row with a one-line hint; expands into the
   familiar sections.  Replaces the dashboard's old wall of forms. ---- */
details.account-tools { margin: 2.25rem 0 1rem;
                        border: 1px solid var(--rule);
                        border-radius: var(--r-md);
                        background: var(--bg-elev); }
.account-tools__summary { display: flex; align-items: baseline; gap: 0.75rem;
                          padding: 0.8rem 1.1rem;
                          cursor: pointer; list-style: none; user-select: none;
                          transition: background-color 120ms ease; }
.account-tools__summary::-webkit-details-marker { display: none; }
.account-tools__summary::after {
  content: ""; margin-left: auto; align-self: center;
  width: 0.4rem; height: 0.4rem;
  border-right: 1.5px solid var(--fg-soft);
  border-bottom: 1.5px solid var(--fg-soft);
  transform: rotate(45deg);
  transition: transform 120ms ease; }
details.account-tools[open] .account-tools__summary::after {
  transform: rotate(-135deg); }
.account-tools__summary:hover { background: var(--bg-soft); }
.account-tools__title { font-weight: 600; font-size: var(--text-sm);
                        color: var(--fg-muted); }
.account-tools__hint { font-size: var(--text-xs); color: var(--fg-soft); }
.account-tools__body { padding: 0.25rem 1.1rem 1.1rem;
                       border-top: 1px solid var(--rule); }
.account-tools__body h2 { font-size: 1.05rem; margin: 1.25rem 0 0.4rem;
                          color: var(--fg-muted); }
.account-tools__body .danger-zone { margin-top: 1.5rem; }

/* ---- Operator dropdown in the header cta cluster.  Deliberately the
   quietest element in the header: small caps, soft colour, the shared
   nav-group dropdown panel.  Operator chrome must never compete with
   the member-facing nav. ---- */
.operator-menu .nav-group__summary {
  font-size: var(--text-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--fg-soft);
  padding: 0.3rem 0.5rem;
  border: 1px dashed var(--rule-strong);
  border-radius: var(--r-sm); }
.operator-menu .nav-group__summary:hover,
.operator-menu[open] .nav-group__summary {
  color: var(--brand); border-color: var(--brand); }
.operator-menu .nav-group__menu { left: auto; right: 0; }

/* Row-level destructive buttons (vehicle Remove) read as quiet
   outlines; the filled red treatment stays reserved for the page-level
   destructive actions (Delete account, admin Purge). */
table.vehicles-list button.btn-danger {
  background: transparent; color: var(--danger);
  border: 1px solid var(--danger-border);
  padding: 0.3rem 0.7rem; font-size: var(--text-xs); }
table.vehicles-list button.btn-danger:hover {
  background: var(--danger-tint); }

/* ---- Mobile degradation ---- */
@media (max-width: 720px) {
  /* Wide telemetry tables become swipeable panels instead of
     overflowing the page.  display:block keeps the internal table
     layout (anonymous table box) while the element itself scrolls. */
  table.charge-log, table.vehicles-list, table.admin-table,
  main.prose-shell table {
    display: block; overflow-x: auto;
    -webkit-overflow-scrolling: touch; }
  table.charge-log th, table.charge-log td,
  table.vehicles-list th, table.vehicles-list td,
  table.admin-table th, table.admin-table td {
    white-space: nowrap; }
  /* Chart panels keep their padding modest on phones. */
  figure.drive-chart, section.battery-chart { padding: 0.7rem 0.75rem 0.55rem; }
  .activity-legend { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---- Route thumbnails in the drive log.  The user's own GPS
   silhouette as a tiny framed map -- the row becomes recognizable at
   a glance ("the Saki run") without sending a single coordinate to a
   tile server. ---- */
td.route-cell { width: 96px; padding: 0.35rem 0.4rem 0.35rem 0.8rem; }
svg.route-thumb { width: 84px; height: 44px; display: block;
                  background: var(--bg-soft);
                  border: 1px solid var(--rule); border-radius: 6px; }
table.charge-log tbody tr:hover svg.route-thumb {
  border-color: var(--brand-tint-hover); background: var(--brand-tint); }

/* ---- Nav choreography.  Each view swap mounts fresh top-level
   sections inside #app-content (idiomorph only preserves elements on
   SAME-view live re-renders), so a staggered rise on direct children
   fires exactly once per navigation and never on a telemetry tick.
   The rise is small + quick: the page should feel composed, not
   theatrical. ---- */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: translateY(0); }
}
main#app-content > * {
  animation: rise-in var(--dur-entry) var(--ease-out) backwards; }
main#app-content > *:nth-child(1) { animation-delay: 20ms; }
main#app-content > *:nth-child(2) { animation-delay: 60ms; }
main#app-content > *:nth-child(3) { animation-delay: 100ms; }
main#app-content > *:nth-child(4) { animation-delay: 140ms; }
main#app-content > *:nth-child(5) { animation-delay: 180ms; }
main#app-content > *:nth-child(6) { animation-delay: 220ms; }
main#app-content > *:nth-child(7) { animation-delay: 260ms; }
main#app-content > *:nth-child(n+8) { animation-delay: 300ms; }

/* ---- Paginated-list row swap.  Page flips + sort
   toggles re-render the table inside the .table-reserve box (which holds
   its height across the morph).  The rows that land carry a soft
   fade-up so a flip reads as a deliberate swap, not a flash-cut.  Pure
   opacity + transform (compositor-only) so the held box never reflows.
   A tiny per-row stagger gives the page a cascade; capped so a full page
   of 25 still settles quickly.  Whether idiomorph reuses or replaces a
   given row, this only paints when the row actually (re)mounts -- an
   unchanged row that morphs in place keeps its element and does not
   replay.  Gated off under reduced motion (rows snap in). ---- */
@keyframes row-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
table.charge-log tbody tr {
  animation: row-in var(--dur-medium) var(--ease-out) backwards; }
table.charge-log tbody tr:nth-child(1)  { animation-delay: 0ms; }
table.charge-log tbody tr:nth-child(2)  { animation-delay: 18ms; }
table.charge-log tbody tr:nth-child(3)  { animation-delay: 36ms; }
table.charge-log tbody tr:nth-child(4)  { animation-delay: 54ms; }
table.charge-log tbody tr:nth-child(5)  { animation-delay: 72ms; }
table.charge-log tbody tr:nth-child(6)  { animation-delay: 90ms; }
table.charge-log tbody tr:nth-child(7)  { animation-delay: 108ms; }
table.charge-log tbody tr:nth-child(8)  { animation-delay: 126ms; }
table.charge-log tbody tr:nth-child(n+9) { animation-delay: 140ms; }

/* ---- Drive scrubber: a real instrument control instead of the
   stock range input.  Hairline track, brand thumb with a soft halo
   that blooms on hover and presses on drag. ---- */
.drive-scrub__range { -webkit-appearance: none; appearance: none;
                      height: 22px; background: transparent; }
.drive-scrub__range::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px; background: var(--rule); }
.drive-scrub__range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 16px; height: 16px; border-radius: 999px;
  background: var(--brand); border: 2.5px solid #fff;
  box-shadow: 0 0 0 1px var(--rule-strong),
              0 0 0 0 color-mix(in srgb, var(--brand) 20%, transparent);
  margin-top: -6px;
  transition: box-shadow 160ms ease, background 160ms ease; }
.drive-scrub__range:hover::-webkit-slider-thumb {
  background: var(--brand-bright);
  box-shadow: 0 0 0 1px var(--rule-strong),
              0 0 0 6px color-mix(in srgb, var(--brand) 16%, transparent); }
.drive-scrub__range:active::-webkit-slider-thumb {
  box-shadow: 0 0 0 1px var(--rule-strong),
              0 0 0 9px color-mix(in srgb, var(--brand) 22%, transparent); }
.drive-scrub__range::-moz-range-track {
  height: 4px; border-radius: 2px; background: var(--rule); }
.drive-scrub__range::-moz-range-thumb {
  width: 13px; height: 13px; border-radius: 999px;
  background: var(--brand); border: 2.5px solid #fff;
  box-shadow: 0 0 0 1px var(--rule-strong);
  transition: box-shadow 160ms ease, background 160ms ease; }
.drive-scrub__range:hover::-moz-range-thumb {
  background: var(--brand-bright);
  box-shadow: 0 0 0 1px var(--rule-strong),
              0 0 0 6px color-mix(in srgb, var(--brand) 16%, transparent); }

/* ---- Charging pulse: while a charge session is live the battery
   glyph's fill breathes -- energy visibly flowing in.  The only
   ambient loop besides the live dot, and only while charging. ---- */
.bat-charging .bat-fill { animation: bat-charge-pulse 2200ms ease-in-out infinite; }
@keyframes bat-charge-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}

/* ---- Table row hover: a 2px brand inset on the left edge picks up
   the nav-card accent language; background warms a touch. ---- */
table.charge-log tbody tr,
table.vehicles-list tbody tr {
  transition: background-color 140ms ease, box-shadow 140ms ease; }
table.charge-log tbody tr:hover,
table.vehicles-list tbody tr:hover {
  box-shadow: inset 2px 0 0 var(--brand-bright); }

/* ---- Header nav links: hairline underline grows from the left on
   hover -- a directional affordance that matches the nav-card edge
   accent.  App pages only (this stylesheet never loads on the
   marketing site). ---- */
.site-header__nav > a,
.site-header__nav .nav-group__summary {
  position: relative; }
.site-header__nav > a::after,
.site-header__nav .nav-group__summary::before {
  content: ''; position: absolute; left: 0; right: auto;
  bottom: -3px; height: 1.5px; width: 100%;
  background: var(--brand);
  transform: scaleX(0); transform-origin: left center;
  transition: transform var(--dur-medium) var(--ease-out); }
.site-header__nav > a:hover::after,
.site-header__nav .nav-group__summary:hover::before {
  transform: scaleX(1); }
