/* =========================================================================
   ORBEVA · BASE — universal reset + a11y primitives shared by every page.
   Link AFTER tokens.css and BEFORE the page's own <style>:
     <link rel="stylesheet" href="/styles/tokens.css">
     <link rel="stylesheet" href="/styles/base.css">
   Page-specific rules (body padding, .wrap width, components) stay in the page.
   These rules are intentionally identical to what was previously inlined on
   each page, so centralising them changes nothing visually.
   ========================================================================= */

*{box-sizing:border-box;min-width:0}

/* Horizontal containment (in-app feedback #6 — "screen wiggles around the width").
   `overflow-x:clip` on <html> kills the iOS Safari horizontal rubber-band WITHOUT
   creating a scroll container the way `overflow-x:hidden` on <body> does (that
   container is what lets the page jiggle 1–2px sideways and can't clip the fixed
   FAB/toast). `scrollbar-gutter:stable` reserves the desktop scrollbar so content
   doesn't shift when it appears. Media is capped so no image/video overruns.

   iOS installed-PWA chrome (feedback #80 + #83 + #50 — 2026-06-26):
   • `background:var(--bg)` on <html> paints the notch/home-indicator cutouts to match the
     app background, so users on installed PWAs no longer see a white system gradient bleed
     through where the body's bg would otherwise stop (#80 "no margin… content cut off at
     edges" — they were seeing the system background bleeding into the cutout area, not the
     content itself being cut off).
   • `overscroll-behavior:none` on both <html> and <body> kills the Y-axis rubber-band the
     tester called "bad UX, should feel like native mobile" (#83) — without it, scrolling
     past the top pulls the whole page down with a 200-300ms bounce that's especially jarring
     on installed PWAs. `none` (not `contain`) also blocks pull-to-refresh, which here would
     conflict with our SDUI auto-refresh contract.
   • `-webkit-tap-highlight-color:transparent` removes the default grey flash on tap that iOS
     PWAs inherit (part of the "doesn't feel native" cluster #50). Real :active styles remain. */
html{overflow-x:clip;scrollbar-gutter:stable;background:var(--bg);overscroll-behavior:none;-webkit-tap-highlight-color:transparent}
body{overscroll-behavior:none}
html,body{max-width:100%}
img,video,canvas{max-width:100%;height:auto}

a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;cursor:pointer}

/* Scroll-driven sticky header (progressive enhancement) */
.top{
  position:sticky;
  top:0;
  z-index:50;
  background:rgba(11,13,16,0.85);
  backdrop-filter:blur(12px);
  -webkit-backdrop-filter:blur(12px);
  margin-top:calc(max(16px, env(safe-area-inset-top)) * -1);
  padding-top:max(16px, env(safe-area-inset-top));
  padding-bottom:10px;
  margin-left:-16px;
  margin-right:-16px;
  padding-left:16px;
  padding-right:16px;
  border-bottom:1px solid transparent;
}

/* ── Canonical header chrome — single source of truth (de-duplicated from per-page inline).
   Pages previously each inlined these identical rules; centralising them here makes the
   header impossible to drift. Page-specific exceptions (invest's accent dot, connections'
   bespoke dot+wordmark+pill) stay inline and override these by source order. ── */
.top{display:flex;justify-content:space-between;align-items:center;gap:10px}
.brand{display:flex;align-items:baseline;gap:8px;min-width:0}
.brand h1{font-size:19px;font-weight:850;letter-spacing:-.02em;margin:0}
.brand .dot{width:7px;height:7px;border-radius:50%;background:var(--green);flex:none;align-self:center;box-shadow:0 0 8px var(--green)}

@supports (animation-timeline: scroll()) {
  @keyframes shrink-header {
    to {
      border-bottom-color: var(--line);
      box-shadow: 0 4px 12px rgba(0,0,0,0.5);
    }
  }
  .top {
    animation: shrink-header linear both;
    animation-timeline: scroll();
    animation-range: 0 60px;
  }
}

/* Visible keyboard focus everywhere — uses the brand accent. */
:focus-visible{outline:2px solid var(--accent,#8b7cf6);outline-offset:2px;border-radius:6px}

/* Respect users who reduce motion. */
@media (prefers-reduced-motion:reduce){
  *{animation-duration:.01ms !important;transition-duration:.01ms !important;scroll-behavior:auto !important}
}

/* =========================================================================
   POLISH — app-wide visual refinement. One file, every page, no per-page edits.
   The dark "cockpit" identity is unchanged; this is feel, not restyle.
   ========================================================================= */

/* Typeset financial figures: fixed-width digits so money columns align and
   live-updating numbers never shift width as the value changes. */
body{font-variant-numeric:tabular-nums}

/* Native-feeling touch: no grey tap-flash, no 300ms delay / double-tap-zoom. */
a,button,summary,[role=button],input,select,textarea,label{-webkit-tap-highlight-color:transparent}
a,button,summary,[role=button]{touch-action:manipulation}

/* Smooth press / state feedback (auto-disabled by the reduced-motion block above). */
a,button,summary{transition:background-color var(--dur-fast) var(--ease-out),border-color var(--dur-fast) var(--ease-out),color var(--dur-fast) var(--ease-out),opacity var(--dur-fast) var(--ease-out)}

/* Brand-tinted text selection. */
::selection{background:var(--accent-tint);color:var(--ink)}

/* App-like standalone scrolling: no rubber-band reveal of the page background. */
html,body{overscroll-behavior-y:none}

/* ───────────────────────────────────────────────────────────────────────────
   Mobile-native hardening — three precise causes of "jumpy keyboard" and
   "screen-size wiggle" on iOS that look unprofessional vs a native app:

   1. AUTO-ZOOM ON INPUT FOCUS (iOS Safari) — when an input has a computed
      font-size <16px, iOS Safari ZOOMS THE PAGE on focus, then unzooms on
      blur. That's the "page jumps" feeling. Force ≥16px on every form field.
      max(16px, 1em) keeps the per-component sizing the page wanted (e.g.
      a 17px input stays 17px) while raising any <16px input to the safe min.

   2. SCROLL-INTO-VIEW HIDDEN BY BNAV — when the user taps an input near the
      bottom of the page, the browser scrolls it into the viewport, but the
      fixed bottom nav covers it (the browser doesn't know about position:
      fixed siblings). scroll-padding-bottom on the scroll container reserves
      space at the bottom of the SCROLLPORT so focused content lands above
      the nav. (env() handles the iPhone safe-area inset.)

   3. TEXT-SIZE-ADJUST ON ROTATION — iOS Safari may auto-resize text after a
      landscape rotation, which reflows everything ("screen-size wiggle").
      Lock to 100% so rotation doesn't reflow.

   Also: overflow-anchor:none stops the browser from snapping scroll position
   to dynamically-inserted content (the SDUI canvas + assist FAB cause this
   without the rule). scrollbar-gutter:stable (already set above) keeps the
   desktop scrollbar reserved so its appearance never jitters layout width. */
html{
  scroll-padding-bottom:calc(104px + env(safe-area-inset-bottom));
  -webkit-text-size-adjust:100%;
  text-size-adjust:100%;
}
/* Mobile app chrome should not reserve or paint a browser scrollbar. Keep desktop's
   `scrollbar-gutter:stable` above, but on phone widths use overlay/native-feeling
   scrolling while preserving scrollability. */
@media (max-width: 767px){
  html{scrollbar-gutter:auto;scrollbar-width:none}
  html::-webkit-scrollbar,
  body::-webkit-scrollbar{width:0;height:0;display:none}
}
input,select,textarea{
  font-size:max(16px, 1em);
}
body{ overflow-anchor:none }

/* ───────────────────────────────────────────────────────────────────────────
   Native-feel polish — chrome elements (nav, header, FAB, tab triggers)
   shouldn't get the iOS long-press context menu (popping up when a user is
   trying to tap) or be selectable like prose. Body text + inputs are still
   selectable; this only suppresses chrome. Plus a touch of installed-PWA
   polish (display-mode:standalone) so when run from the home screen the
   app reads as more deliberate than the browser version. */
nav.bnav, nav.bnav a, .top, .brand, .atlas-fab, .prefs-fab, .orbeva-install-card{
  -webkit-touch-callout:none;
  -webkit-user-select:none; user-select:none;
}
/* Strip platform-default chrome from form controls — iOS gives <input type=search> a skeumorphic
   round-rect, <select> a dropdown chevron with system styling, etc. We re-style everything to
   match the dark cockpit theme, so the native chrome competes with our look.
   EXCLUDED via :not(): range (slider track+thumb go invisible), checkbox/radio (the check/dot
   IS the native chrome we'd nuke), color (the swatch). These types have no project-wide custom
   skin downstream, so they need the native rendering. */
input:not([type=range]):not([type=checkbox]):not([type=radio]):not([type=color]),
select,textarea,button{
  -webkit-appearance:none; -moz-appearance:none; appearance:none;
}
/* Keep BROWSER-AUTOFILLED fields on-theme app-wide (ticket #30). Chromium/WebKit paint an
   autofilled field with the UA's own light/yellow background + dark text, ignoring our CSS — so on
   the dark theme an autofilled email box looked "not standard". There is no way to set the autofill
   background directly, so we cover it with an inset box-shadow in our surface colour and hold the
   text/caret colour. The 9999s background-color transition keeps Chromium from re-flashing its
   colour back in on interaction. Lives here (not in login.html) so onboarding + connections inputs
   get the same treatment. Scoped to the same themed form-control selector as the rule above. */
input:not([type=range]):not([type=checkbox]):not([type=radio]):not([type=color]):-webkit-autofill,
input:not([type=range]):not([type=checkbox]):not([type=radio]):not([type=color]):-webkit-autofill:hover,
input:not([type=range]):not([type=checkbox]):not([type=radio]):not([type=color]):-webkit-autofill:focus,
textarea:-webkit-autofill,select:-webkit-autofill{
  -webkit-text-fill-color:var(--txt);
  -webkit-box-shadow:0 0 0 1000px var(--panel2) inset;
  caret-color:var(--txt);
  transition:background-color 9999s ease-in-out 0s;
}
/* When we strip native chrome from <select>, we lose the disclosure chevron — looks like a
   plain text input, so users miss that it's tappable. Re-add a tiny SVG chevron on the right
   edge so the "I open a picker" affordance survives. Inline data:URI so no extra request. */
select{
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1l5 5 5-5' fill='none' stroke='%238b93a3' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat:no-repeat;
  background-position:right 12px center;
  padding-right:30px;
}

/* Crisper text on Apple devices — Safari/Chrome default to subpixel anti-aliasing which on a
   dark background reads as slightly thicker/blurrier than native iOS app text. `antialiased`
   is what native iOS uses. optimizeLegibility enables kerning + ligatures (negligible cost
   for body text, real win for the financial figures we display heavily).                     */
body{
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  text-rendering:optimizeLegibility;
}

/* When the app is RUNNING AS A STANDALONE PWA (installed to home screen): */
@media all and (display-mode: standalone){
  /* Mobile-only context — no desktop scrollbar to reserve. (Reduces a tiny right-edge artifact
     some installed contexts otherwise show due to the reserved-gutter rule above.) */
  html{ scrollbar-gutter:auto }

  /* PAINT THE STATUS-BAR / HOME-INDICATOR AREAS BRAND-DARK so a brief overscroll never reveals
     the system surface beneath (iOS would otherwise flash the system wallpaper colour through
     the safe-area on a rubber-band gesture). The body background already paints the content
     region; this paints the *root* element so the OS-reserved insets stay on-brand too.       */
  html{ background-color: var(--bg) }
  body{ background-color: var(--bg) }

  /* Make tap targets slightly more generous in standalone — the brand-chrome surface area is
     limited (no browser chrome), and a wider hit area for the bnav reads as more app-like. */
  nav.bnav a{ min-height:52px }

  /* `--install-card-display: none` — landing.html's install handoff card has no purpose when
     the user is ALREADY in the installed app. Hide it so installed users don't get a nag.    */
  .orbeva-install-card{ display:none !important }

  /* SAFE-AREA RESPECT FOR THE TOP BRAND ROW.
     pages with a status-bar-style of "black-translucent" (every app page) draw UNDER the iOS
     status bar. Pages already pad their <body> with env(safe-area-inset-top), but in standalone
     the brand chip can still feel cramped — give it a tiny extra breathing top margin so it sits
     well clear of the status-bar glyphs and the in-app back chip (#pwa-back) doesn't collide.   */
  .brand{ padding-top: max(0px, env(safe-area-inset-top)) }

  /* The browser chrome is gone in standalone, so user-text selection on chrome rows (the brand,
     the bnav labels) is meaningless and reads as un-app-like. Lock it down for chrome only —
     content text remains selectable. (-webkit-user-select inherited up the chain.) */
  .brand, nav.bnav, .top, .atlas-fab, #pwa-back{
    user-select:none; -webkit-user-select:none;
  }

  /* Slight content lift when standalone — pages that don't already use 100dvh sometimes leave
     a 1-2px slice of the body's background_color visible at the bottom of long-content pages
     during scroll bounce. Force the body to fill the dvh window so that gap can't appear.       */
  html, body{ min-height: 100dvh }
}
/* Note: existing .btn:active{transform:scale(.97)} (components.css) + nav.bnav a:active and the
   page-specific .cta:active translations already cover tactile press feedback — no extra
   :active rule needed here. */

/* =========================================================================
   SIGNATURE — self-hosted display face + card depth, propagated app-wide.
   Works with zero per-page edits because pages set neither font-family on
   their heroes nor box-shadow on .card, so these add cleanly with no conflict.
   Self-hosted (not Google's CDN) → no runtime third-party dependency and no
   user IP sent to Google, which matters for a finance app (privacy/GDPR).
   ========================================================================= */
@font-face{
  font-family:"Space Grotesk";
  font-style:normal;
  font-weight:300 700;
  font-display:swap;
  src:url("/styles/fonts/space-grotesk.woff2") format("woff2");
}

/* Hero numbers + page titles + wordmark wear the display face; body stays system. */
.brand h1,.htitle,.htitle .t,.hero .big,.route .big,.nw .big,.move .h,.move .gr .gn,
.thp .big,.opp,.ovv .tot,.risk .rbig,.goal .gn{
  font-family:"Space Grotesk",-apple-system,BlinkMacSystemFont,system-ui,sans-serif;
  font-variant-numeric:tabular-nums;
}

/* Lit-from-above depth on every card (inset top highlight + soft lift).
   Now reads from the --shadow-2 token (byte-identical value → zero visual change). */
.card{box-shadow:var(--shadow-2)}

/* Ambient accent aura behind the top of every page. `html body` (0,0,2) outranks
   each page's inline `body{background:var(--bg)}` shorthand (0,0,1), so the glow
   survives while the page keeps its own background-color. */
html body{
  /* Signature mesh aura — layered brand-hue radials (accent · info · violet) instead of one flat
     glow, so the top of every page reads premium + alive. Token-built (color-mix on the OKLCH
     accents) → stays on-brand and theme-coherent; static + GPU-cheap; low-alpha so it never fights
     the data. (Richer-UI Phase 2; perf-check only guards the select-chevron data-URI, not the aura.) */
  background-image:
    radial-gradient(38% 34% at 16% 4%, color-mix(in oklch, var(--accent) 20%, transparent), transparent 62%),
    radial-gradient(36% 32% at 86% 0%, color-mix(in oklch, var(--info) 15%, transparent), transparent 64%),
    radial-gradient(46% 40% at 50% -4%, color-mix(in oklch, var(--violet) 13%, transparent), transparent 60%),
    radial-gradient(120% 55% at 50% 0%, rgba(139,124,246,.05), transparent 60%);
  background-repeat:no-repeat;
  background-attachment:fixed;
}

/* ==================================================================   MOTION + INTERACTION UTILITIES — additive, token-driven, reduced-motion-safe.
   Each does nothing until a page opts in (adds the class), except the tap-area
   and tactile-press rules which enhance existing controls in place.
   ========================================================================= */

/* Screen-reader-only: visually hidden but still announced (none existed before;
   needed for live regions + accessible names in the per-screen a11y work). */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}

/* Skeleton loading — a shaped placeholder that shimmers. The shimmer is gated
   behind no-preference, so reduced-motion users see a calm static block. */
.skeleton{
  background:linear-gradient(90deg,var(--surface-2) 25%,var(--line) 37%,var(--surface-2) 63%);
  background-size:200% 100%;
  border-radius:var(--r-md);
}
@media (prefers-reduced-motion:no-preference){
  @keyframes shimmer{to{background-position:-200% 0}}
  .skeleton{animation:shimmer 1.4s linear infinite}
}

/* Long-list performance — skip layout/paint for offscreen cards. Graceful
   no-op where unsupported. intrinsic-size tuned to a typical month-card. */
.cv-card{content-visibility:auto;contain-intrinsic-size:auto 720px}

/* Comfortable tap targets — grow small +/- steppers to >=44px hit area WITHOUT
   changing their visual size (transparent ::before, out of layout flow so it
   can never add horizontal overflow). Mirrors the in-repo .pchk precedent. */
.stp button{position:relative}
.stp button::before{content:"";position:absolute;inset:-8px}

/* Tactile press — a micro-depress on commit controls. Scoped to interactive
   instances (button.pill, not badge spans). The declared transform transition
   is what lets the reduced-motion block above neutralise the depress. */
.stp button,.pchk,button.pill,.move .acts button{
  transition:transform var(--dur-fast) var(--ease-out);
}
.stp button:active,.pchk:active,button.pill:active,.move .acts button:active{
  transform:scale(.94);
}

/* First-load entrance — a gentle fade-up (opacity + translateY only, never
   width/translateX, so it stays horizontally inert). JS adds .in on first
   paint; media-gated so reduced-motion users get no entrance animation. */
@media (prefers-reduced-motion:no-preference){
  @keyframes reveal-rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
  .reveal.in{animation:reveal-rise var(--dur-slow) var(--ease-out) both;animation-delay:calc(var(--i,0)*60ms)}
}

/* =========================================================================
   PAGE TRANSITIONS — cross-document View Transitions. Turns the MPA's hard
   reloads (every <a href> + the ~44 location.href navigations) into smooth,
   native-style animated page changes — across every page, since all link this
   file. Progressive enhancement: ZERO JS, fully inert where unsupported
   (Safari/WebKit support is rolling out; others just get the normal reload).
   The bottom nav is named so it STAYS PUT across navigation instead of
   flashing — the detail that reads "app", not "web page". Reduced-motion is
   honoured on both the OS @media path and the in-app html.reduce-motion path.
   ========================================================================= */
@view-transition{ navigation: auto; }
.bnav{ view-transition-name: orbeva-nav; }
/* Persist app chrome across nav — naming an element gives it its own VT snapshot. Identical-
   shape names on both pages → element STAYS PUT (no fade), which is the "feels like an app"
   detail vs the "feels like a web page" cross-fade. Where shape DIFFERS (e.g. .brand is 19px on
   authed pages but 20-26px on login/terms/privacy/onboarding) the engine MORPHS (slide+resize)
   instead of cross-fading — still smoother than a flash, but not pixel-stays-put.
   PERSISTENCE COVERAGE (the common paths users actually take):
     .brand          — pixel-identical across the 8 authed-app pages (cockpit, spending,
                       networth, goals, invest, insights, transactions, tax) → stays put.
                       Morphs to the bigger login/terms/privacy/onboarding brand. Absent on
                       connections.html (bespoke header) → fades.
     .atlas-fab      — singleton mounted by assist.js on every authed page → stays put on every
                       authed↔authed nav. Tucked-by-scroll state captured at nav start, so
                       scroll-down + nav animates a clean slide-up reveal (pleasant, not a flash).
     .prefs-fab      — singleton mounted by prefs.js on 6 pages (cockpit/goals/invest/networth/
                       spending/transactions). Cross-fades on nav to/from the other 7 authed
                       pages (tax/cards/deals/insights/plan/billing/connections). Adding prefs.js
                       to those pages would extend coverage if needed.
   Spec note: view-transition-name MUST be unique per snapshot. All three named elements are
   singletons in their respective surfaces — uniqueness verified by perf-check + runtime probe.
   Reduced-motion: the html.reduce-motion + @media (prefers-reduced-motion) wildcard overrides
   above neutralise the animation for these groups too — no extra rules needed.                  */
.brand{ view-transition-name: orbeva-brand; }
.atlas-fab{ view-transition-name: orbeva-atlas-fab; }
.prefs-fab{ view-transition-name: orbeva-prefs-fab; }

@media (prefers-reduced-motion:no-preference){
  ::view-transition-old(root){ animation:orbeva-page-out var(--dur-fast,.16s) var(--ease-out,ease) both; }
  ::view-transition-new(root){ animation:orbeva-page-in var(--dur-slow,.32s) var(--ease-out,ease) both; }
  @keyframes orbeva-page-out{ to{ opacity:0 } }
  @keyframes orbeva-page-in{ from{ opacity:0; transform:translateY(10px) } to{ opacity:1; transform:none } }
}
/* In-app "Reduce motion" toggle also neutralises the page transition. */
html.reduce-motion::view-transition-group(*),
html.reduce-motion::view-transition-old(*),
html.reduce-motion::view-transition-new(*){ animation:none !important; }
@media (prefers-reduced-motion:reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation:none !important; }
}

/* =========================================================================
   USER PREFERENCES (prefs.js) — in-app Display controls, applied as classes on
   <html> over the existing tokens. Reduce-motion + High-contrast (density deferred
   until spacing is fully tokenised).
   ========================================================================= */

/* In-app "Reduce motion" — MUST live OUTSIDE the @media(prefers-reduced-motion)
   block so the toggle works even when the OS flag is off (the key correctness point). */
html.reduce-motion *,
html.reduce-motion *::before,
html.reduce-motion *::after{
  animation-duration:.01ms !important;
  animation-iteration-count:1 !important;
  animation-delay:0s !important;        /* else staggered .reveal still holds cards hidden then late-pops */
  transition-duration:.01ms !important;
  scroll-behavior:auto !important;
}

/* In-app "High contrast" — lift dim/faint text + hairlines toward AA on the dark bg. */
html.hc{
  --ink-dim:#c5cdd9;
  --ink-faint:#9da6b4;
  --line:#3b4250;
  --line-strong:#7c828e;   /* high-contrast structural border — ~4.6:1 on --surface, comfortably above the AA non-text floor and above the default --line-strong */
}

/* Floating launcher — stacked ABOVE the atlas command-bar FAB (which sits at
   bottom:80px/92px, 40-50px tall, on the same right edge). Sit clearly above it
   so the 40px gear and the atlas circle never overlap or steal each other's taps.
   Safe-area aware, out of flow (no overflow). */
.prefs-fab{
  position:fixed;
  right:max(14px,env(safe-area-inset-right));
  bottom:calc(132px + env(safe-area-inset-bottom));
  width:40px;height:40px;border-radius:var(--r-pill);
  display:flex;align-items:center;justify-content:center;
  background:var(--surface-2);border:1px solid var(--line);color:var(--ink-dim);
  font-size:17px;line-height:1;box-shadow:var(--shadow-1);
  transition:transform var(--dur-fast) var(--ease-out);
  z-index:calc(var(--z-nav) + 1);   /* one intentional layer above the bottom nav, not a DOM-order tie */
}
.prefs-fab:active{transform:scale(.94)}
.prefs-fab::before{content:"";position:absolute;inset:-3px}   /* >=44px hit target (visual stays 40px) */

/* Dialog (native <dialog>; ::backdrop + focus-trap come for free with showModal). */
.prefs-dlg{border:0;padding:0;background:transparent;color:var(--ink);max-width:360px;width:calc(100% - 32px);margin:auto;opacity:0;transform:translateY(10px) scale(.98);transition:opacity var(--dur-fast) var(--ease-out),transform var(--dur-fast) var(--ease-out),display var(--dur-fast) allow-discrete,overlay var(--dur-fast) allow-discrete}
.prefs-dlg[open]{opacity:1;transform:translateY(0) scale(1)}
.prefs-dlg::backdrop{background:rgba(0,0,0,.55);opacity:0;transition:opacity var(--dur-fast) var(--ease-out),display var(--dur-fast) allow-discrete,overlay var(--dur-fast) allow-discrete}
.prefs-dlg[open]::backdrop{opacity:1}
@starting-style{
  .prefs-dlg[open]{opacity:0;transform:translateY(10px) scale(.98)}
  .prefs-dlg[open]::backdrop{opacity:0}
}
.prefs-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-xl);box-shadow:var(--shadow-3);padding:18px 16px 14px}
.prefs-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.prefs-top h2{margin:0;font-size:var(--text-lg);font-weight:800}
.prefs-x{position:relative;width:32px;height:32px;border:0;background:transparent;color:var(--ink-dim);font-size:22px;line-height:1;border-radius:var(--r-sm)}
.prefs-x::before{content:"";position:absolute;inset:-7px}   /* >=44px hit target (visual stays 32px) */
.prefs-rows{display:flex;flex-direction:column;gap:8px}
.pref-row{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-md);padding:12px 13px;text-align:left}
.pref-l{display:flex;flex-direction:column;gap:2px;min-width:0}
.pref-t{font-size:var(--text-md);font-weight:700;color:var(--ink)}
.pref-h{font-size:var(--text-xs);color:var(--ink-faint)}
/* --line-strong: the OFF-state switch track conveys state → AA non-text 3:1 (was --line ≈1.3:1). */
.pref-sw{flex:none;width:40px;height:24px;border-radius:var(--r-pill);background:var(--line-strong);position:relative;transition:background var(--dur-fast) var(--ease-out)}
.pref-sw::after{content:"";position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:var(--ink);transition:transform var(--dur-fast) var(--ease-out)}
.pref-row[aria-checked="true"] .pref-sw{background:var(--accent)}
.pref-row[aria-checked="true"] .pref-sw::after{transform:translateX(16px);background:var(--accent-ink)}
.prefs-note{margin:12px 2px 2px;font-size:var(--text-2xs);color:var(--ink-faint);line-height:1.45}
/* "All settings →" link out of the small Display dialog into the full /settings.html page.
   Tap-sized; visually a secondary action (subdued color, no fill) — the gear stays Display-only. */
.prefs-more{display:block;margin-top:14px;padding:11px 12px;text-align:center;color:var(--ink);font-size:var(--text-sm);font-weight:700;text-decoration:none;border:1px solid var(--line);border-radius:var(--r-md);background:var(--surface-2,var(--surface));min-height:44px;line-height:1.5}
.prefs-more:hover,.prefs-more:focus-visible{border-color:var(--accent);outline:none}
@media (prefers-reduced-motion:reduce){.prefs-dlg,.prefs-dlg::backdrop{transition:none;transform:none}}
html.reduce-motion .prefs-dlg,html.reduce-motion .prefs-dlg::backdrop{transition:none;transform:none}
/* Bottom tab bar — more depth + a clearer active state (in-app feedback #3). `nav.bnav…` (0,0,1,1)
   outranks each page's inline `.bnav…` (0,0,1,0), so this polish lands on every page without
   touching per-page markup. */
nav.bnav{box-shadow:0 -12px 32px rgba(0,0,0,.55);background:linear-gradient(to top,rgba(11,11,16,.98),rgba(11,11,16,.9))}
nav.bnav a{font-size:21px;gap:3px;border-radius:13px;margin:2px;padding-top:7px;transition:background .16s,transform .12s,opacity .16s;opacity:.82}
nav.bnav a span{font-size:10px;font-weight:800}
/* Recede the chrome (Linear doctrine): inactive tabs dim a notch so the active tab + the data lead;
   the current tab and a press stay full-strength. Opacity-only + transition → reduced-motion-safe. */
nav.bnav a.on{background:var(--greent);opacity:1}
nav.bnav a.on>span{color:var(--violet)}
nav.bnav a:active{transform:scale(.93);opacity:1}
nav.bnav a.on::before{width:30px;height:3px;box-shadow:0 0 12px var(--violet),0 0 5px var(--violet)}

/* Mobile tab bar dock — feedback #103/#104.
   The tab bar should read as fixed app chrome, not a floating row of pills. Keep
   desktop's sidebar pill state below, but on phone widths make the bar edge-to-edge
   and express the current tab with accent text + a small rail. */
@media (max-width: 767px) {
  nav.bnav {
    border-radius: 0;
    padding: 5px max(8px, env(safe-area-inset-right)) max(5px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
    box-shadow: 0 -1px 0 var(--line), 0 -10px 24px rgba(0,0,0,.34);
  }
  nav.bnav a {
    position: relative;
    min-height: 54px;
    margin: 0;
    padding: 8px 2px 6px;
    background: transparent;
    border-radius: 10px;
  }
  nav.bnav a.on {
    background: transparent;
    color: var(--accent,#8b7cf6);
  }
  nav.bnav a.on > span {
    color: var(--accent,#8b7cf6);
  }
  nav.bnav a.on::before {
    content: "";
    position: absolute;
    top: 4px;
    left: 50%;
    width: 22px;
    height: 3px;
    border-radius: 999px;
    background: var(--accent,#8b7cf6);
    box-shadow: 0 0 10px rgba(139,124,246,.5);
    transform: translateX(-50%);
  }
}

/* =========================================================================
   FLUID DISPLAY NUMBERS (in-app feedback #6 — "feel native, dynamically sized").
   The big money figures across the app are hardcoded px in each page's <style>
   (.nw .big=42, .hero .big=40, .thp .big=38, .opp=34, .route .big=32,
   .risk .rbig=30, .ovv .tot=28, .htitle=24). At those fixed sizes a large value
   on a <360px phone can overrun its row; overflow-x:clip only HIDES that — the
   native fix is to let the number scale. Each clamp's MAX equals the current px,
   so on a normal phone and on desktop nothing changes; the number only shrinks
   on narrow screens, exactly where the overflow risk is. The `body ` prefix
   raises specificity above each page's inline rule (loaded after this file),
   so this lands app-wide with zero per-page edits — same trick as `html body`. */
body .nw .big   {font-size:clamp(31px,8.5vw + .4rem,42px)}
body .hero .big {font-size:clamp(30px,8vw + .4rem,40px)}
body .thp .big  {font-size:clamp(29px,7.6vw + .4rem,38px)}
body .opp       {font-size:clamp(27px,7vw + .35rem,34px)}
body .route .big{font-size:clamp(26px,6.6vw + .35rem,32px)}
body .risk .rbig{font-size:clamp(25px,6.2vw + .3rem,30px)}
body .ovv .tot  {font-size:clamp(24px,5.8vw + .3rem,28px)}
body .htitle,
body .htitle .t {font-size:clamp(21px,5vw + .3rem,24px)}
/* Suffixes that ride inside a fluid number keep their own small fixed size. */
body .ovv .tot small{font-size:14px}
body .thp .big .per {font-size:15px}

/* Desktop Sidebar Overhaul */
@media (min-width: 768px) {
  body:has(nav.bnav) {
    padding-left: 280px !important;
    padding-bottom: 32px !important;
    background-size: 100% 600px;
  }
  body:has(nav.bnav) .wrap {
    max-width: 820px;
    margin: 0 auto;
    padding: 24px;
  }
  nav.bnav {
    top: 0;
    left: 0;
    bottom: 0;
    width: 250px;
    right: auto;
    flex-direction: column;
    justify-content: flex-start;
    padding: 32px 16px;
    border-right: 1px solid var(--line);
    border-top: none;
    background: rgba(11,11,16,1);
    box-shadow: 12px 0 32px rgba(0,0,0,0.2);
  }
  nav.bnav a {
    flex: none;
    flex-direction: row;
    min-height: 56px;
    padding: 0 20px;
    justify-content: flex-start;
    gap: 16px;
  }
  nav.bnav a span {
    font-size: 15px;
    font-weight: 700;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  nav.bnav a.on::before {
    width: 3px;
    height: 30px;
    top: 50%;
    left: 0;
    transform: translateY(-50%);
    border-radius: 0 3px 3px 0;
  }
  /* Desktop has a LEFT sidebar instead of a bottom bar, so the FABs no longer
     need the ~76-92px offset that cleared the mobile bottom nav. Drop them to a
     normal bottom inset and keep the prefs gear stacked clearly above the atlas
     FAB so the two never overlap in mid-air bottom-right. */
  .prefs-fab { bottom: calc(72px + env(safe-area-inset-bottom)); }
  /* Desktop label-pill atlas FAB (40px). prefs at 72px clears its 20+40=60px top. */
  .atlas-fab { bottom: max(20px, env(safe-area-inset-bottom)) !important; }
}
