/* 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;
  }
  /* 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. */
  body.app-body { padding: 0; }
  main#app-content { display: block; }
  /* 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;
    transition: background 120ms ease; }
  a.cta-primary:hover, button[type="submit"]:hover { background: var(--brand-deep); }
  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); }
  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: 0.9rem; color: var(--fg-muted); }
  /* Charging log v0 */
  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: 0.95rem; 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: 0.78rem; 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: 0.92rem; 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: 0.9rem; }
  .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: 0.72rem;
                 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: 0.86rem; 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: 0.72rem;
                      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: 0.78rem; }
  /* 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: 0.9rem;
                       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; }
  td.admin-access button, button.admin-resend {
                       padding: 0.2rem 0.5rem; font-size: 0.74rem;
                       margin: 0.1rem 0.15rem 0.1rem 0; }
  .sub-badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 999px;
               font-size: 0.7rem; 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: 0.74rem; 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: 0.85rem; }
  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; }
  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: 0.92rem; }
  table.charge-log th, table.vehicles-list th {
                        background: var(--brand-tint); color: var(--brand-text);
                        font-weight: 600; font-size: 0.78rem;
                        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; }
  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; }
  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: 0.78rem;
              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 v0 - SVG track + stats + pager */
  section.drive-detail { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
                         gap: 1.5rem; align-items: start; margin: 1rem 0; }
  svg.drive-track { width: 100%; height: auto; max-width: 100%;
                    border: 1px solid var(--rule); border-radius: 6px;
                    background: var(--bg-soft); display: block; }
  /* <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); }
  .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); }
  .trip-route { margin: 0 0 0.5rem; font-weight: 600; color: var(--fg); }
  /* Vehicle telemetry/pairing status badges + the add-virtual-key warning. */
  .tel-badge { display: inline-block; padding: 2px 8px; border-radius: 999px;
               font-size: 0.78rem; 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; }
  dl.drive-stats { margin: 0;
                   display: grid; grid-template-columns: auto 1fr;
                   gap: 0.4rem 1rem; font-variant-numeric: tabular-nums;
                   background: var(--bg-elev); border: 1px solid var(--rule);
                   border-radius: 6px; padding: 1rem; }
  dl.drive-stats dt { color: var(--fg-muted); font-size: 0.85rem; }
  dl.drive-stats dd { margin: 0; font-weight: 600; }
  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; }
  @media (max-width: 720px) {
    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: 0.8rem; color: var(--fg-muted);
               font-weight: 400; }
  abbr.est-mark { margin-left: 0.25rem; color: var(--fg-muted); font-size: 0.8rem;
                  text-decoration: none; cursor: help; }
  section.drive-soundtrack { margin: 1.25rem 0; }
  section.drive-soundtrack h2 { font-size: 1rem; 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 v0 - /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: 0.75rem; margin: 1rem 0 1.5rem; }
  section.summary-stats .stat { background: var(--bg-elev);
                                border: 1px solid var(--rule);
                                border-radius: 6px; padding: 0.9rem 1rem; }
  section.summary-stats .stat-value { font-size: 1.4rem; 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'; }
  /* 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 {
    --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: 0.78rem;
                                      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: 0.75rem; margin: 1rem 0 1.5rem; }
  nav.dashboard-cards a.card { display: block; padding: 1rem 1.1rem;
                               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: 0.85rem;
                                 color: var(--fg-muted); }
  p.signed-in-as { color: var(--fg-muted); font-size: 0.85rem; }
  /* ---- 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: 0.78rem; 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: 0.82rem;
                        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: 1.15rem; }
  section.battery-chart > p.muted { margin: 0 0 0.9rem; font-size: 0.88rem;
                                    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: 0.85rem;
                     color: var(--fg-muted); }
  span.legend-pill { display: inline-block; padding: 0.1rem 0.5rem;
                     border-radius: 999px; font-size: 0.75rem;
                     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: 1.2rem;
                         background: var(--bg-elev);
                         border: 1px solid var(--rule);
                         border-radius: 6px; padding: 1.1rem 1.25rem;
                         margin: 1rem 0 1.25rem; }
  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: 2.4rem;
                                       font-weight: 700;
                                       line-height: 1.1;
                                       color: var(--brand-text);
                                       font-variant-numeric: tabular-nums; }
  section.battery-hero .hero-label   { font-size: 0.85rem;
                                       color: var(--fg-soft);
                                       text-transform: uppercase;
                                       letter-spacing: 0.05em;
                                       font-weight: 600; }
  section.battery-hero .hero-band    { font-size: 0.8rem;
                                       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 220ms cubic-bezier(.2,.7,.2,1); }
  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 fade + slide in on first paint. Idiomorph keeps
     the element across SSE morphs so this only fires on real
     mounts (page load + nav swap), not on every value update --
     the @property counter animations carry the live updates. */
  section.summary-stats .stat { animation: stat-rise 480ms cubic-bezier(.2,.7,.2,1) backwards; }
  section.summary-stats .stat:nth-child(1) { animation-delay: 40ms; }
  section.summary-stats .stat:nth-child(2) { animation-delay: 120ms; }
  section.summary-stats .stat:nth-child(3) { animation-delay: 200ms; }
  section.summary-stats .stat:nth-child(4) { animation-delay: 280ms; }
  @keyframes stat-rise {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  /* 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: 0.78rem; 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); }
  }
  /* ---- Pass 2 polish: typography + table affordances + sparkle ---- */

  /* 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;
  }
  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;
  }

  /* ---- Pass 3: live-pulse captions + nav-card icons + bolder stats ---- */

  /* 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: 0.68rem; 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. */
  section.summary-stats .stat-value { font-size: 1.75rem;
                                       letter-spacing: -0.05em; }
  .stat-battery .bat-icon svg { width: 98px; height: 44px; }
  /* The .hero-pct already uses font-mono; bump from 2.4rem to
     2.8rem with deeper tracking for the Bloomberg-terminal feel
     the user is asking for. */
  section.battery-hero .hero-pct { font-size: 2.8rem;
                                    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 220ms ease, transform 220ms cubic-bezier(.2,.7,.2,1),
                color 220ms 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: 0.95rem;
  }
  /* 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: 0.92rem;
  }
  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: 0.78rem;
    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: 0.85rem;
  }

  /* 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 {
    font-size: 2.2rem;
    letter-spacing: -0.025em;
    font-weight: 600;
  }
  main.prose-shell h2 {
    font-size: 1.2rem;
    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 > *,
    section.summary-stats .stat {
      animation-name: fade-only !important;
      animation-duration: 160ms !important;
      animation-delay: 0ms !important; }
    .activity-seg,
    .series-end,
    svg.drive-track circle.marker,
    .tween-int, .tween-pct, .tween-km {
      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; } }


@media (max-width: 640px) {
  /* The shared marketing stylesheet hides .site-header__nav at <=640px (the
     marketing site has no in-page nav on mobile).  The /app primary nav uses
     that SAME class, so without this it vanishes on phones.  app.css only
     loads on /app, so re-showing it here never touches the marketing header.
     Keep brand + cta on row 1; drop the nav to a full-width wrapped row 2. */
  .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; }
}


/* 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: 0.75rem;
               margin: 1rem 0 0.75rem; }
.now-lead { display: flex; align-items: center; gap: 1rem;
            background: var(--bg-elev); border: 1px solid var(--rule);
            border-radius: 6px; padding: 1.15rem 1.3rem; }
.now-lead .bat-icon svg { width: 84px; height: 38px; }
.now-lead__value { font-size: 2.1rem; 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: 0.78rem; 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; }
@media (max-width: 560px) {
  .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: 1rem 1.15rem 0.9rem;
                  margin: 0.75rem 0 1.25rem; }
.activity-bar { display: flex; height: 12px; border-radius: 6px;
                overflow: hidden; gap: 2px; background: var(--bg-soft); }
.activity-seg { min-width: 3px;
                transform-origin: left center;
                animation: seg-grow 700ms cubic-bezier(.2,.7,.2,1) backwards; }
.activity-seg:nth-child(2) { animation-delay: 80ms; }
.activity-seg:nth-child(3) { animation-delay: 160ms; }
.activity-seg:nth-child(4) { animation-delay: 240ms; }
@keyframes seg-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.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: 0.5rem 1rem; margin-top: 0.85rem; }
.activity-item { display: flex; align-items: baseline; gap: 0.5rem;
                 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: 0.78rem; 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: 0.95rem;
                        color: var(--fg-muted); }
.account-tools__hint { font-size: 0.8rem; 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: 0.78rem; 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: 0.82rem; }
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 420ms cubic-bezier(.2,.7,.2,1) 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; }

/* ---- 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 200ms cubic-bezier(.2,.7,.2,1); }
.site-header__nav > a:hover::after,
.site-header__nav .nav-group__summary:hover::before {
  transform: scaleX(1); }
