/* Portfolio Tracker — design system
   ─────────────────────────────────────────────────────────────────────
   Single source of truth for the entire UI. Tokens at the top; primitives
   (.card, .btn, .tag, .table, .form-control) next; page-level layouts at
   the bottom. Status uses one vocabulary throughout: tone-{green,amber,red}
   on text and .tag-{green,amber,red,indigo,neutral} on labels. */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/InterVariable.woff2') format('woff2-variations'),
       url('../fonts/InterVariable.woff2') format('woff2');
}

/* ═══ Status / state vocabulary ═════════════════════════════════════════
   The app has accumulated five parallel ways to colour-code status:
   `tone-*`, `tag-*`, `badge-*`, `pill-*`, `state-pill is-*`, and the
   one-off `status-*` set on the city-rent table. They all map to the
   same three semantic colours (green = ok, amber = watch, red = bad).
   Pick the right one by surface, not by colour:

       Use this           When                            Visual
       ────────────────── ─────────────────────────────── ──────────────
       state-pill is-on   live on/off/warn header chip    coloured dot
       is-off is-warn     (settings AI toggle, etc.)      + capsule
       ────────────────── ─────────────────────────────── ──────────────
       tag-{red,amber,    inline chip on row meta,        bg + border
       green,neutral}     log level, city status, etc.    + text
       ────────────────── ─────────────────────────────── ──────────────
       tone-{red,amber,   coloured surface (action card,  left rule
       green,gray}        risk callout, milestone row)    + bg tint
       ────────────────── ─────────────────────────────── ──────────────

   `badge-*` and `pill-*` predate this guidance and still work; treat
   them as deprecated aliases of `tag-*` / `state-pill` and migrate
   when you next touch the file. The same applies to `status-fresh /
   stale / missing` — those are city-rent-table-specific shorthands.

   Severity-tier mapping (recommendations risk flags, milestones, etc.)
   always reads `high → red, medium → amber, low → gray`. Green is
   reserved for "good"; using it for "low severity" muddles the
   semantic ("least bad" ≠ "good").
   ──────────────────────────────────────────────────────────────────── */

/* ═══ Tokens ════════════════════════════════════════════════════════════ */
:root {
  /* Brand — indigo is reserved for primary action, active nav, one chart
     accent. Slate carries everything else. */
  --color-primary:        #4f46e5;
  --color-primary-hover:  #4338ca;
  --color-primary-light:  #818cf8;
  --color-primary-bg:     #eef2ff;
  --color-primary-border: #e0e7ff;

  /* Semantic — each tone has fg (text), bg (subtle fill), border. */
  --color-green:          #16a34a;
  --color-green-fg:       #15803d;
  --color-green-bg:       #dcfce7;
  --color-green-border:   #bbf7d0;
  /* Amber: Tailwind amber-500 (#f59e0b) is the canonical "warning"
     shade — bright enough to read on white-card surfaces. The earlier
     amber-600 (#d97706) read too dim against the elevated bg and
     drove ~10 sites to inline #f59e0b directly. Token bumped so all
     surfaces share one amber. fg/bg/border ramp kept; the fg pair
     remains the darker amber-800 for legible text on bg fills. */
  --color-amber:          #f59e0b;
  --color-amber-fg:       #92400e;
  --color-amber-bg:       #fef3c7;
  --color-amber-border:   #fde68a;
  --color-red:            #dc2626;
  --color-red-fg:         #991b1b;
  --color-red-bg:         #fee2e2;
  --color-red-border:     #fca5a5;

  /* Neutrals (slate ramp) */
  --color-bg:             #f1f5f9;   /* page background */
  --color-bg-elevated:    #f8fafc;   /* faint elevation behind table headers */
  --color-surface:        #ffffff;   /* cards, inputs */
  --color-border:         #e2e8f0;
  --color-border-light:   #f1f5f9;
  --color-text:           #0f172a;
  --color-text-muted:     #475569;
  --color-text-faint:     #64748b;

  /* Sidebar */
  --sidebar-bg:           #0f172a;
  --sidebar-text:         #94a3b8;
  --sidebar-active:       #f1f5f9;
  --sidebar-active-bg:    rgba(255, 255, 255, 0.07);
  --sidebar-width:        248px;

  /* Layout */
  --content-max:          1200px;

  /* Spacing */
  --space-1:  4px;
  --space-2:  8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-5:  20px;
  --space-6:  24px;
  --space-8:  32px;
  --space-10: 40px;
  --space-12: 48px;

  /* Radii */
  --radius-sm: 6px;
  --radius:    10px;
  --radius-lg: 14px;
  --radius-xl: 18px;

  /* Shadows — borders do most of the work; shadows are a hint, not a wall. */
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, .04);
  --shadow:    0 4px 16px -6px rgba(15, 23, 42, .08), 0 2px 4px -2px rgba(15, 23, 42, .04);
  --shadow-lg: 0 16px 40px -12px rgba(15, 23, 42, .14);

  /* Focus ring — accessible against light surfaces. */
  --ring-focus: 0 0 0 2px var(--color-surface), 0 0 0 4px rgba(79, 70, 229, .42);

  /* Motion */
  --ease:    cubic-bezier(.4, 0, .2, 1);
  --t-fast:  120ms;
  --t-base:  200ms;
  --t-slow:  340ms;

  /* Breakpoints (reference only — CSS @media can't read vars):
     --bp-sm: 600px   phones / narrow tablets
     --bp-md: 900px   tablets / collapsed sidebar
     --bp-lg: 1100px  full dashboard grid */
}

/* ═══ Reset & base ══════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  font-size: 16px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scroll-padding-top: 80px;
  /* Prevent horizontal scroll chaining from inner scrollers (wide
     tables, horizontal chip strips) from being interpreted as a
     browser back/forward navigation by Chrome on Android. iOS Safari
     ignores this for true edge-swipe-back, so it's complementary to
     the page-header padding bump that keeps the hamburger out of the
     edge-swipe zone. */
  overscroll-behavior-x: contain;
}

body {
  font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.55;
  min-height: 100vh;
  font-feature-settings: 'cv11' 1, 'ss01' 1;
}

a { color: inherit; text-decoration: none; }
button, input, select, textarea { font-family: inherit; }
img, svg { display: block; max-width: 100%; }
ul, ol { list-style: none; }

/* Skip link */
.skip-link {
  position: absolute;
  left: -9999px;
  top: var(--space-3);
  padding: var(--space-2) var(--space-4);
  background: var(--color-primary);
  color: #fff;
  border-radius: var(--radius-sm);
  font-size: .875rem;
  z-index: 200;
}
.skip-link:focus { left: var(--space-3); }

/* ═══ Layout shell ══════════════════════════════════════════════════════ */
.app-layout { display: flex; min-height: 100vh; }

.sidebar {
  width: var(--sidebar-width);
  background: var(--sidebar-bg);
  display: flex;
  flex-direction: column;
  padding: calc(var(--space-4) + env(safe-area-inset-top, 0px)) 0
           calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  position: fixed;
  top: 0; left: 0;
  height: 100vh;
  height: 100dvh;
  z-index: 100;
  transition: transform var(--t-base) var(--ease);
}

.sidebar-logo {
  padding: 0 var(--space-5) var(--space-4);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.sidebar-logo svg { color: var(--color-primary-light); }
.sidebar-logo-text { font-weight: 700; font-size: .95rem; color: #fff; letter-spacing: -.01em; }
.sidebar-logo-sub  { font-size: .7rem; color: var(--sidebar-text); }

.sidebar-nav { flex: 1; padding: 0 var(--space-3); overflow-y: auto; }

.nav-label {
  font-size: .65rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: rgba(148, 163, 184, .55);
  padding: var(--space-3) var(--space-3) var(--space-1);
}
.nav-label:first-child { padding-top: 0; }

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 8px var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--sidebar-text);
  font-size: .875rem;
  font-weight: 500;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
  cursor: pointer;
  margin-bottom: 1px;
}
.nav-item:hover { background: rgba(255, 255, 255, .04); color: var(--sidebar-active); }
.nav-item.active { background: var(--sidebar-active-bg); color: var(--sidebar-active); }
.nav-item svg { flex-shrink: 0; opacity: .75; }
.nav-item.active svg { opacity: 1; }
.nav-item:focus-visible { outline: 2px solid var(--color-primary-light); outline-offset: 2px; }

.sidebar-footer {
  padding: var(--space-3) var(--space-3) 0;
  border-top: 1px solid rgba(255, 255, 255, .06);
}
.nav-item-button { width: 100%; background: none; border: 0; text-align: left; font: inherit; }
.logout-form { margin: 0; }

/* Logo as a link to dashboard. The anchor keeps the row layout from
   the original div; only difference is hover affordance + cursor. */
a.sidebar-logo {
  text-decoration: none;
  color: inherit;
}
a.sidebar-logo:hover .sidebar-logo-text,
a.sidebar-logo:focus-visible .sidebar-logo-text { color: #fff; }
a.sidebar-logo:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Nav-item attention badges. The Loans item picks up `nav-item-badge
   nav-item-badge-danger` when there are overdue loans. Right-aligned
   via margin-left:auto on the label so the badge floats to the
   trailing edge of the row. */
.nav-item-label { flex: 1 1 auto; }
.nav-item-badge {
  flex-shrink: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  font-size: .65rem;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  background: rgba(255, 255, 255, .12);
  color: #fff;
}
.nav-item-badge-danger {
  background: var(--color-red);
  color: #fff;
}

/* Admin group collapsible header. Mirrors the existing .nav-label
   look but adds a chevron + cursor + full-width click target. */
.nav-label-toggle {
  appearance: none;
  background: none;
  border: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font: inherit;
  font-size: .65rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: rgba(148, 163, 184, .55);
  padding: var(--space-3) var(--space-3) var(--space-1);
  cursor: pointer;
}
.nav-label-toggle:hover { color: rgba(148, 163, 184, .85); }
.nav-label-chevron { transition: transform var(--t-fast) var(--ease); }
html.is-admin-group-collapsed .nav-label-chevron { transform: rotate(-90deg); }
html.is-admin-group-collapsed .sidebar-admin-group { display: none; }

/* Desktop sidebar collapse to icon-only. The font-size: 0 trick hides
   the bare text node next to each SVG (every nav-item is "<svg>label"
   with no wrapper), without needing to wrap every label in a span.
   SVGs are sized by their width/height attributes, not font-size, so
   they survive intact. Restored on .nav-item-badge and any child that
   still needs to render. */
.sidebar-collapse-toggle {
  appearance: none;
  background: none;
  border: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
  color: var(--sidebar-text);
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
  border-radius: var(--radius-sm);
}
.sidebar-collapse-toggle:hover { color: var(--sidebar-active); background: rgba(255, 255, 255, .04); }
.sidebar-collapse-toggle:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
.sidebar-collapse-icon { transition: transform var(--t-fast) var(--ease); }
html.is-sidebar-collapsed .sidebar-collapse-icon { transform: rotate(180deg); }

/* The collapse-toggle's collapsed-state CSS (further below) is gated
   behind @media (min-width: 901px) — the desktop in-line column
   pattern. Below that breakpoint the off-canvas drawer is the active
   pattern, and a click on this button would flip the `is-sidebar-
   collapsed` class with no visible effect, reading as broken. Hide
   the control entirely outside its working range. */
@media (max-width: 900px) {
  .sidebar-collapse-toggle { display: none; }
}

/* Apply the collapsed state only on desktop — mobile uses the existing
   off-canvas drawer pattern, which is a different UX. The width
   override drives the sidebar AND the main-content margin via the
   single --sidebar-width variable. */
@media (min-width: 901px) {
  html.is-sidebar-collapsed { --sidebar-width: 64px; }
  html.is-sidebar-collapsed .sidebar-logo-text,
  html.is-sidebar-collapsed .sidebar-logo-sub,
  html.is-sidebar-collapsed .nav-label,
  html.is-sidebar-collapsed .nav-label-toggle,
  html.is-sidebar-collapsed .nav-item-badge,
  html.is-sidebar-collapsed .sidebar-admin-group { display: none; }
  /* Hide the text node next to each icon without wrapping labels. */
  html.is-sidebar-collapsed .nav-item { font-size: 0; gap: 0; justify-content: center; padding: 10px 8px; }
  html.is-sidebar-collapsed .nav-item svg { width: 18px; height: 18px; }
  html.is-sidebar-collapsed .sidebar-logo { justify-content: center; padding: 0 var(--space-3) var(--space-4); }
  html.is-sidebar-collapsed .sidebar-collapse-toggle { justify-content: center; }
}

/* Main content */
.main-content {
  flex: 1;
  min-width: 0;
  margin-left: var(--sidebar-width);
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.page-header {
  background: rgba(255, 255, 255, .85);
  backdrop-filter: saturate(140%) blur(8px);
  -webkit-backdrop-filter: saturate(140%) blur(8px);
  border-bottom: 1px solid var(--color-border);
  padding: var(--space-5) var(--space-8);
  padding-left: max(var(--space-8), env(safe-area-inset-left, 0px));
  padding-right: max(var(--space-8), env(safe-area-inset-right, 0px));
  display: flex;
  align-items: center;
  gap: var(--space-3);
  position: sticky;
  top: 0;
  /* Sits above the off-canvas sidebar (z-index 100) so the hamburger
     remains visible and tappable when the mobile drawer is open. On
     desktop the sidebar is an in-line column not an overlay, so this
     extra stacking is irrelevant there. */
  z-index: 101;
  transition: box-shadow var(--t-base) var(--ease);
}
.page-header.is-scrolled {
  box-shadow: 0 1px 0 rgba(15, 23, 42, .04), 0 6px 16px -10px rgba(15, 23, 42, .08);
}
.page-header-titles { flex: 1; min-width: 0; }
.page-header-actions {
  display: flex; align-items: center; gap: var(--space-3);
  flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end;
}

/* Notification bell in the page header. Subtle by default; the badge
   pulls the eye only when there's something to read. The icon itself
   stays soft-grey so it doesn't compete with the page title for
   attention on every page. */
.header-bell {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  border-radius: 999px;
  color: var(--color-text-muted);
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.header-bell:hover { background: var(--color-bg-elevated); color: var(--color-text); }
.header-bell:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.header-bell.is-active { color: var(--color-primary); }
.header-bell-badge {
  position: absolute;
  top: 2px; right: 2px;
  min-width: 16px; height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--color-red-fg);
  color: #fff;
  font-size: .65rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  border: 2px solid var(--color-surface);
  /* Sit on top of the bell stroke without misaligning the icon centre. */
  box-sizing: content-box;
}
@media (pointer: coarse) {
  .header-bell { width: 44px; height: 44px; }
}
.page-title {
  font-size: clamp(1.05rem, 2.2vw, 1.25rem);
  font-weight: 700;
  letter-spacing: -.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.page-subtitle {
  font-size: .8125rem;
  color: var(--color-text-muted);
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.35;
}

.page-body {
  padding: var(--space-8);
  padding-bottom: calc(var(--space-10) + env(safe-area-inset-bottom, 0px));
  flex: 1;
}
.page-shell {
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
}
.page-content { width: 100%; display: flex; flex-direction: column; gap: var(--space-6); }

.hamburger {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  border: none;
  background: none;
  cursor: pointer;
  color: var(--color-text-muted);
  border-radius: var(--radius);
  flex-shrink: 0;
  margin-right: var(--space-1);
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.hamburger:hover  { background: var(--color-bg); color: var(--color-text); }
.hamburger:focus-visible { box-shadow: var(--ring-focus); outline: none; }
/* Icon swap: the button contains both glyphs; CSS shows the open
   (burger) by default and swaps to the close (X) glyph when the
   drawer is open. Body class flip happens in toggleSidebar(). */
.hamburger-icon-close { display: none; }
body.sidebar-open .hamburger-icon-open  { display: none; }
body.sidebar-open .hamburger-icon-close { display: inline-block; }
@media (min-width: 901px) { .hamburger { display: none; } .mobile-overlay { display: none; } }

.mobile-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, .48);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  z-index: 99;
  display: none;
}
.mobile-overlay.active { display: block; }

/* ═══ Section header (used inside cards) ═══════════════════════════════ */
.section-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
  flex-wrap: wrap;
}
.section-title {
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -.015em;
  color: var(--color-text);
}
.eyebrow {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--color-text-faint);
}

/* ═══ Cards ════════════════════════════════════════════════════════════ */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  box-shadow: var(--shadow-sm);
}
@media (min-width: 601px) { .card { padding: var(--space-6); } }
.card-quiet  { box-shadow: none; }
.card-bg     { background: var(--color-bg-elevated); }
.card-danger { border-color: var(--color-red-border); }
.card-danger .section-title { color: var(--color-red-fg); }

/* Card that's also a link — keeps text colour, lifts border on hover. */
a.card,
a.card-link {
  display: block;
  text-decoration: none;
  color: inherit;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
a.card-link:hover { border-color: #cbd5e1; box-shadow: var(--shadow); }

/* ═══ Tags — single label/badge/pill/chip vocabulary ═══════════════════ */
.tag,
.badge,
.pill,
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border-radius: 999px;
  font-size: .72rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: var(--color-bg);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  white-space: nowrap;
  letter-spacing: .01em;
}
.tag-sm { padding: 2px 6px; font-size: .68rem; }

.tag-green,  .badge-green,  .pill-green,
.tag-amber,  .badge-amber,  .pill-amber,
.tag-red,    .badge-red,    .pill-red,
.tag-indigo, .badge-purple,
.tag-neutral, .badge-gray { /* shared shape inherited above */ }

.tag-green,  .badge-green,  .pill-green  { background: var(--color-green-bg); color: var(--color-green-fg); border-color: var(--color-green-border); }
.tag-amber,  .badge-amber,  .pill-amber  { background: var(--color-amber-bg); color: var(--color-amber-fg); border-color: var(--color-amber-border); }
.tag-red,    .badge-red,    .pill-red    { background: var(--color-red-bg);   color: var(--color-red-fg);   border-color: var(--color-red-border); }
.tag-indigo, .badge-purple { background: var(--color-primary-bg); color: var(--color-primary-hover); border-color: var(--color-primary-border); }
.tag-neutral, .badge-gray { background: var(--color-bg); color: var(--color-text-muted); border-color: var(--color-border); }

.dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.dot-green { background: var(--color-green); }
.dot-amber { background: var(--color-amber); }
.dot-red   { background: var(--color-red); }

/* ═══ Buttons ══════════════════════════════════════════════════════════ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-sm);
  font-size: .875rem;
  font-weight: 500;
  cursor: pointer;
  border: 1px solid transparent;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
  white-space: nowrap;
  outline: none;
  min-height: 40px;
  text-decoration: none;
}
.btn:focus-visible { box-shadow: var(--ring-focus); }
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn:active:not(:disabled) { transform: translateY(1px); }

.btn-primary {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
}
.btn-primary:hover { background: var(--color-primary-hover); border-color: var(--color-primary-hover); }

.btn-secondary {
  background: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-border);
}
.btn-secondary:hover { background: var(--color-bg-elevated); border-color: #cbd5e1; }

.btn-ghost {
  background: transparent;
  color: var(--color-text-muted);
  padding: var(--space-2) var(--space-3);
  min-height: 32px;
}
.btn-ghost:hover { background: var(--color-bg); color: var(--color-text); }

/* Subtle-red ghost: signals a destructive action without shouting.
   Pairs the quiet `btn-ghost` chrome with red text + red-tint hover so
   buttons like "Unlink" or "Disconnect" read as "this will remove
   something" while staying visually secondary to the primary CTA. */
.btn-ghost-danger        { color: var(--color-red-fg); }
.btn-ghost-danger:hover  { background: var(--color-red-bg); color: var(--color-red-fg); border-color: var(--color-red-border); }
.btn-ghost-danger:focus-visible { box-shadow: 0 0 0 3px rgba(220, 38, 38, .25); outline: none; }

.btn-danger {
  background: var(--color-red);
  color: #fff;
  border-color: var(--color-red);
}
.btn-danger:hover { background: var(--color-red-fg); border-color: var(--color-red-fg); }

.btn-danger-soft {
  background: var(--color-red-bg);
  color: var(--color-red-fg);
  border-color: var(--color-red-border);
}
.btn-danger-soft:hover { background: var(--color-red-border); }

.btn-sm    { padding: var(--space-2) var(--space-3); font-size: .8rem; min-height: 34px; }
/* Icon-only square button. Pairs with a colour variant (btn-secondary
   / btn-ghost / btn-danger-soft) and an aria-label + title for screen
   readers and hover tooltips. Sized to match btn-sm height; the mobile
   rule under .row-actions further bumps it to the 44px tap-target. */
.btn-icon {
  width: 34px;
  min-width: 34px;
  height: 34px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.btn-lg    { padding: var(--space-4) var(--space-6); font-size: .95rem; min-height: 48px; }
.btn-block { width: 100%; justify-content: center; }

/* Buttons rendered on a dark surface (hero, login). */
.btn-on-hero {
  background: rgba(255, 255, 255, .07);
  color: #f1f5f9;
  border-color: rgba(255, 255, 255, .14);
}
.btn-on-hero:hover { background: rgba(255, 255, 255, .12); border-color: rgba(255, 255, 255, .22); }
.btn-on-hero-primary {
  background: #f8fafc;
  color: #0f172a;
  border-color: #f8fafc;
  font-weight: 600;
}
.btn-on-hero-primary:hover { background: #fff; border-color: #fff; }
.btn-on-hero-ghost { background: transparent; }
.btn-on-hero-ghost:hover { background: rgba(255, 255, 255, .06); }

.btn.is-success { background: var(--color-green); border-color: var(--color-green); color: #fff; }
.btn.is-error   { background: var(--color-red);   border-color: var(--color-red);   color: #fff; }

/* ═══ Forms ════════════════════════════════════════════════════════════ */
.form-group { margin-bottom: var(--space-5); }
.form-label {
  display: block;
  font-size: .85rem;
  font-weight: 600;
  margin-bottom: var(--space-2);
  color: var(--color-text);
}
.form-label .hint {
  font-weight: 400;
  color: var(--color-text-muted);
  font-size: .75rem;
  margin-left: 4px;
}

.form-control {
  width: 100%;
  padding: 10px var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-size: .9rem;
  color: var(--color-text);
  background: var(--color-surface);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
  outline: none;
  min-height: 42px;
}
.form-control:hover  { border-color: #cbd5e1; }
.form-control:focus,
.form-control:focus-visible {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(79, 70, 229, .18);
}
.form-control::placeholder { color: var(--color-text-faint); }
textarea.form-control { min-height: 80px; resize: vertical; line-height: 1.5; }

select.form-control {
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-4) center;
  background-size: 12px;
  padding-right: var(--space-8);
}

input[type="file"].form-control { padding: 6px; cursor: pointer; }
input[type="file"].form-control::file-selector-button {
  font-family: inherit;
  font-size: .85rem;
  font-weight: 500;
  padding: 8px var(--space-4);
  margin-right: var(--space-3);
  border-radius: var(--radius-sm);
  border: none;
  background: var(--color-bg);
  color: var(--color-text);
  cursor: pointer;
}
input[type="file"].form-control::file-selector-button:hover { background: var(--color-border-light); }

.form-row   { display: grid; gap: var(--space-4); }
/* minmax(0, 1fr) (not plain 1fr) so columns can shrink below their
   content's intrinsic min-width. Inputs with a fixed intrinsic width,
   notably <input type="date"> and its native picker UI, otherwise push
   the track past the form edge and overflow on narrow screens. */
.form-row-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.form-row-3 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); }

.form-check { display: flex; align-items: center; gap: var(--space-3); cursor: pointer; min-height: 32px; }
.form-check input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--color-primary); cursor: pointer; flex-shrink: 0; }

/* Toggle switch: same semantics as a checkbox, different visual.
   The native input stays in the DOM (focusable + announced by screen
   readers); the visible track+thumb is rendered with sibling spans. */
.form-toggle {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  cursor: pointer;
  user-select: none;
  min-height: 32px;
}
.form-toggle input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  width: 1px;
  height: 1px;
  margin: -1px;
}
.form-toggle-track {
  position: relative;
  flex-shrink: 0;
  width: 40px;
  height: 22px;
  border-radius: 999px;
  background: var(--color-border);
  transition: background var(--t-fast) var(--ease);
  margin-top: 2px;
}
.form-toggle-track::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
  transition: transform var(--t-fast) var(--ease);
}
.form-toggle input:checked ~ .form-toggle-track {
  background: var(--color-primary);
}
.form-toggle input:checked ~ .form-toggle-track::after {
  transform: translateX(18px);
}
.form-toggle input:focus-visible ~ .form-toggle-track {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
.form-toggle input:disabled ~ .form-toggle-track {
  opacity: .55;
  cursor: not-allowed;
}
.form-toggle-label {
  font-weight: 600;
  font-size: .875rem;
  line-height: 1.4;
}
.form-toggle-label .hint {
  display: block;
  font-weight: 400;
  font-size: .78rem;
  color: var(--color-text-muted);
  margin-top: 2px;
}
@media (pointer: coarse) {
  .form-toggle { min-height: 44px; }
  .form-toggle-track { width: 44px; height: 24px; margin-top: 4px; }
  .form-toggle-track::after { width: 20px; height: 20px; }
  .form-toggle input:checked ~ .form-toggle-track::after { transform: translateX(20px); }
}
@media (pointer: coarse) {
  .form-check { min-height: 44px; gap: var(--space-4); padding: var(--space-1) 0; }
  .form-check input[type="checkbox"] { width: 20px; height: 20px; }
}

fieldset.form-group {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-4);
}
fieldset.form-group > legend {
  padding: 0 var(--space-2);
  font-size: .85rem;
  font-weight: 600;
  color: var(--color-text);
}

/* Inline radio group ("Lump sum" / "Monthly instalments"). */
.radio-group { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.radio-group label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: .9rem;
  font-weight: 400;
  cursor: pointer;
  color: var(--color-text);
}
.radio-group input[type="radio"] { accent-color: var(--color-primary); cursor: pointer; }

/* Block checkbox: multi-line label with bold title + hint. */
.checkbox-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  cursor: pointer;
}
.checkbox-row > input[type="checkbox"] { margin-top: 3px; flex-shrink: 0; accent-color: var(--color-primary); cursor: pointer; }
.checkbox-row-title { display: block; font-weight: 600; font-size: .85rem; }
.checkbox-row-hint  { display: block; margin-top: 2px; color: var(--color-text-muted); font-size: .75rem; }

/* Inline checkbox: single line ("Account is active"). */
.checkbox-inline {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  font-size: .9rem;
  color: var(--color-text);
}
.checkbox-inline input[type="checkbox"] { accent-color: var(--color-primary); cursor: pointer; }
.checkbox-inline .hint { color: var(--color-text-muted); font-size: .75rem; }
/* When a checkbox needs to align vertically with labelled inputs in a form-row. */
.form-group-align-input { padding-top: 28px; }

/* Form action footer: cancel + primary. */
.form-actions {
  display: flex;
  gap: var(--space-3);
  justify-content: flex-end;
  flex-wrap: wrap;
}
.form-actions-split { justify-content: space-between; }

/* Hidden by JS state. */
.is-hidden { display: none !important; }

/* Page-access checkbox list (admin/users/edit). */
.page-access-list { display: flex; flex-direction: column; gap: var(--space-2); }
.page-access-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: .9rem;
  transition: background var(--t-fast) var(--ease);
}
.page-access-row:hover { background: var(--color-bg); }
.page-access-row input[type="checkbox"] { accent-color: var(--color-primary); cursor: pointer; }
.page-access-row .hint { color: var(--color-text-faint); margin-left: auto; }
.page-access-row.is-locked { cursor: not-allowed; opacity: .7; }
.page-access-row.is-locked input[type="checkbox"] { opacity: .55; cursor: not-allowed; }

/* Alert with a bulleted list inside. */
.alert-list { margin: 0; padding-left: 1.2em; list-style: disc; }
.alert-list li + li { margin-top: 4px; }

/* ═══ Tables ═══════════════════════════════════════════════════════════ */
.table-wrap {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.table-wrap.table-wrap-bare { border: none; background: transparent; border-radius: 0; }

.table { width: 100%; border-collapse: collapse; }
.table th {
  background: var(--color-bg-elevated);
  padding: var(--space-3) var(--space-5);
  text-align: left;
  font-size: .68rem;
  font-weight: 700;
  color: var(--color-text-muted);
  letter-spacing: .07em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--color-border);
  white-space: nowrap;
}
.table td {
  padding: var(--space-4) var(--space-5);
  font-size: .875rem;
  border-bottom: 1px solid var(--color-border-light);
  vertical-align: middle;
}
.table td.cell-nowrap { white-space: nowrap; }
.table tr:last-child td { border-bottom: none; }
.table tbody tr { transition: background var(--t-fast) var(--ease); }
/* background-color, not the `background` shorthand: the loans table draws its
   progress bar via background-image, which the shorthand would reset to none. */
.table tbody tr:hover { background-color: var(--color-bg-elevated); }
.table tr.row-overdue { background-color: #fffaf0; }
.table tr.row-overdue:hover { background-color: #fff4e0; }
.table .num { text-align: right; font-variant-numeric: tabular-nums; }

@media (min-width: 601px) and (max-width: 900px) {
  .loans-table, .users-table, .trends-yearly-table { min-width: 720px; }
}

/* ═══ Account category palette ═════════════════════════════════════════
   Shared by the accounts page (group swatches, "Your mix" bar) and the
   category chips. Names follow the schema's `accounts.category` slugs so
   the markup can interpolate the class directly. */
:root {
  --cat-equities:        #6366f1;  /* indigo */
  --cat-bonds:           #0ea5e9;  /* sky    */
  --cat-cash:            #22c55e;  /* green  */
  --cat-crypto:          #f59e0b;  /* amber  */
  --cat-real_estate:     #8b5cf6;  /* violet */
  --cat-commodities:     #eab308;  /* yellow */
  --cat-loans_receivable:#14b8a6;  /* teal   */
  --cat-loans_payable:   #ef4444;  /* red    */
  --cat-other:           #94a3b8;  /* slate  */
}
.cat-equities         { background: var(--cat-equities); }
.cat-bonds            { background: var(--cat-bonds); }
.cat-cash             { background: var(--cat-cash); }
.cat-crypto           { background: var(--cat-crypto); }
.cat-real_estate      { background: var(--cat-real_estate); }
.cat-commodities      { background: var(--cat-commodities); }
.cat-loans_receivable { background: var(--cat-loans_receivable); }
.cat-loans_payable    { background: var(--cat-loans_payable); }
.cat-other            { background: var(--cat-other); }
/* Chips reuse the same colour-by-category vocabulary, with a tinted
   background + dark text so they stay legible inside a table cell.
   rgba() values mirror the --cat-* hex variables above at 18% alpha,
   matching the rgba-tint pattern used elsewhere in this file. */
.chip.cat-equities, .chip.cat-bonds, .chip.cat-cash, .chip.cat-crypto,
.chip.cat-real_estate, .chip.cat-commodities,
.chip.cat-loans_receivable, .chip.cat-loans_payable, .chip.cat-other {
  color: #1e293b;
}
.chip.cat-equities         { background: rgba(99, 102, 241, .18); }
.chip.cat-bonds            { background: rgba(14, 165, 233, .18); }
.chip.cat-cash             { background: rgba(34, 197, 94, .18); }
.chip.cat-crypto           { background: rgba(245, 158, 11, .18); }
.chip.cat-real_estate      { background: rgba(139,  92, 246, .18); }
.chip.cat-commodities      { background: rgba(234, 179,   8, .18); }
.chip.cat-loans_receivable { background: rgba( 20, 184, 166, .18); }
.chip.cat-loans_payable    { background: rgba(239,  68,  68, .18); }
.chip.cat-other            { background: rgba(148, 163, 184, .18); }


/* Drift pill — single inline chip that picks up the snapshot's status
   tone. Aligns right inside the table; tabular-nums keeps the number
   from shifting between rows. */
.drift-pill {
  display: inline-flex; align-items: baseline; gap: 4px;
  font-size: 12px; font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--color-bg-elevated);
}
.drift-pill.tone-green { color: var(--color-green-fg); background: rgba(22, 163, 74, .08); }
.drift-pill.tone-amber { color: var(--color-amber-fg); background: rgba(245, 158, 11, .10); }
.drift-pill.tone-red   { color: var(--color-red-fg);   background: rgba(220, 38, 38, .10); }


/* ═══ Accounts edit form ═══════════════════════════════════════════════
   Section eyebrows + the "Currently $X" chip above the form give the
   editor structure and context. Form-section-header sits with a thin
   top divider and a touch of vertical breathing room so the eye can
   parse "Identity → Pricing → Allocation" at a glance without making
   the page feel chopped up. */
.account-value-strip {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 8px 14px;
  margin-bottom: var(--space-4);
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  font-size: 13px;
}
.account-value-label {
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 10px;
  font-weight: 600;
  color: var(--color-text-muted);
}
.account-value-figure {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}
.account-value-pct {
  color: var(--color-text-muted);
  font-weight: 500;
  margin-left: 2px;
}

.form-section-header {
  margin: var(--space-5) 0 var(--space-3);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border-light);
}
.form-section-header:first-of-type {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* Live target-sum indicator under the Target/Cap row on the edit form.
   Single inline line; the tone-* class on the wrapper drives colour. */
.target-sum-indicator {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: -8px 0 var(--space-3);
  padding: 6px 10px;
  background: var(--color-bg-elevated);
  border-radius: 6px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.target-sum-label {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 10px;
  font-weight: 600;
  color: var(--color-text-muted);
}
.target-sum-value { font-weight: 700; }
.target-sum-note  { color: var(--color-text-muted); font-weight: 500; }
.target-sum-indicator.tone-green .target-sum-value { color: var(--color-green-fg); }
.target-sum-indicator.tone-amber .target-sum-value { color: var(--color-amber-fg); }
.target-sum-indicator.tone-red   .target-sum-value { color: var(--color-red-fg); }

/* Loan repayment progress: 3px gradient at the bottom edge of each row. */
/* Tiny legend for the bottom progress bar: "{pct}% repaid" for repayment
   progress, "{pct}% elapsed" for a bullet loan's time-to-due. Tinted to match
   the bar (--loan-progress-color, set per row) so the two read as one unit.
   Sits in the first cell's bottom padding, just above the bar's left edge — no
   extra row height, no overlap with content. Hidden in the mobile card view. */
.loans-table td.col-primary { position: relative; }
.loan-bar-tag {
  position: absolute;
  left: var(--space-5);
  bottom: 5px;
  font-size: .56rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--loan-progress-color, var(--color-text-faint));
  pointer-events: none;
}
@media (max-width: 600px) { .loan-bar-tag { display: none; } }

/* Explicit progress bar for the mobile loan card (the desktop row-background
   bar can't survive the card layout). Hidden on desktop; shown in the phone
   card-stack media query below. Fill width/colour come from the row's
   --loan-progress-* vars. */
.loan-mbar { display: none; }

.loans-table tbody tr {
  background-image: linear-gradient(
    to right,
    var(--loan-progress-color, var(--color-primary)) 0%,
    var(--loan-progress-color, var(--color-primary)) var(--loan-progress-pct, 0%),
    transparent var(--loan-progress-pct, 0%),
    transparent 100%
  );
  background-repeat: no-repeat;
  background-size: 100% 3px;
  background-position: 0 100%;
}

/* ═══ Alerts ═══════════════════════════════════════════════════════════ */
.alert {
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius);
  font-size: .875rem;
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}
.alert-success { background: var(--color-green-bg); color: #14532d; border: 1px solid var(--color-green-border); }
.alert-error   { background: var(--color-red-bg);   color: #7f1d1d; border: 1px solid var(--color-red-border); }
.alert-warning { background: var(--color-amber-bg); color: #78350f; border: 1px solid var(--color-amber-border); }
.alert-info    { background: var(--color-primary-bg); color: var(--color-primary-hover); border: 1px solid var(--color-primary-border); }
.alert-dismiss {
  background: none;
  border: none;
  cursor: pointer;
  margin-left: auto;
  font-size: 1.25rem;
  line-height: 1;
  opacity: .6;
  color: inherit;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  padding: 0;
  flex-shrink: 0;
}
.alert-dismiss:hover  { opacity: 1; }
.alert-dismiss:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

/* ═══ Empty state ══════════════════════════════════════════════════════ */
.empty-state {
  text-align: center;
  padding: var(--space-8) var(--space-4);
  color: var(--color-text-muted);
}
.empty-state-icon {
  width: 52px; height: 52px;
  margin: 0 auto var(--space-4);
  border-radius: 50%;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
}
.empty-state-icon svg { width: 24px; height: 24px; opacity: .7; }
.empty-state-title { font-size: 1rem; font-weight: 600; color: var(--color-text); margin-bottom: var(--space-2); }
.empty-state p { max-width: 340px; margin: 0 auto; font-size: .875rem; }
@media (max-width: 360px) {
  .empty-state { padding: var(--space-6) var(--space-3); }
}

/* ═══ Utilities ════════════════════════════════════════════════════════ */
.text-muted   { color: var(--color-text-muted); }
.text-faint   { color: var(--color-text-faint); }
.text-red     { color: var(--color-red-fg); }
.text-sm      { font-size: .8rem; }
.text-xs      { font-size: .72rem; }
.text-right   { text-align: right; }
.font-mono    { font-family: ui-monospace, 'SF Mono', 'Fira Code', monospace; }
.font-semibold{ font-weight: 600; }
.tabular      { font-variant-numeric: tabular-nums; }
.num-positive { color: var(--color-green-fg); font-variant-numeric: tabular-nums; }
.num-negative { color: var(--color-red-fg);   font-variant-numeric: tabular-nums; }
.tone-green   { color: var(--color-green-fg); }
.tone-amber   { color: var(--color-amber-fg); }
.tone-red     { color: var(--color-red-fg); }
.tone-neutral { color: var(--color-text-muted); }
.tone-primary { color: var(--color-primary-hover); }

.divider  { border: none; border-top: 1px solid var(--color-border); margin: var(--space-6) 0; }
.spacer-4 { height: var(--space-4); }

.grid-2    { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-5); }
.grid-auto { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 360px), 1fr)); gap: var(--space-5); }
.flex-end  { display: flex; justify-content: flex-end; }
.row-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.row-actions form { margin: 0; }

/* Current-user marker in the admin users table. A profile glyph replaces
   the old "(you)" text; the colour distinguishes an admin self (indigo,
   matching the Admin role tag) from a regular user (neutral). Sized to
   match .btn-icon so the row stays aligned with the action buttons. */
.self-badge {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  background: var(--color-bg);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
}
.self-badge-admin {
  background: var(--color-primary-bg);
  color: var(--color-primary-hover);
  border-color: var(--color-primary-border);
}

/* ═══ Disclosure (<details>) ═══════════════════════════════════════════ */
details.disclosure {
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
}
details.disclosure > summary {
  list-style: none;
  cursor: pointer;
  padding: var(--space-4) var(--space-5);
  font-size: .875rem;
  font-weight: 600;
  color: var(--color-text);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
details.disclosure > summary::-webkit-details-marker { display: none; }
details.disclosure > summary::after {
  content: '';
  width: 8px; height: 8px;
  border-right: 2px solid var(--color-text-muted);
  border-bottom: 2px solid var(--color-text-muted);
  transform: rotate(45deg);
  transition: transform var(--t-base) var(--ease);
  margin-left: auto;
}
details.disclosure[open] > summary::after { transform: rotate(-135deg); }
details.disclosure > .disclosure-body {
  padding: var(--space-5);
  border-top: 1px solid var(--color-border-light);
}

/* ═══ Modal (custom confirm dialog) ════════════════════════════════════ */
/* display:flex below would otherwise override the `hidden` attribute, so
   markup-driven modals (e.g. the multi-balance add-entry sheet) need this
   explicit rule to stay closed until shown. */
.modal-backdrop[hidden] { display: none; }
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, .55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: var(--space-4);
  animation: fadeIn .18s var(--ease) both;
}
.modal-card {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: var(--space-6);
  width: 100%;
  max-width: 420px;
}
.modal-title { font-size: 1.05rem; font-weight: 700; margin-bottom: var(--space-3); }
.modal-body  { font-size: .9rem; color: var(--color-text-muted); margin-bottom: var(--space-6); line-height: 1.5; }
.modal-actions { display: flex; gap: var(--space-3); justify-content: flex-end; flex-wrap: wrap; }
@media (max-width: 480px) {
  .modal-actions { flex-direction: column-reverse; }
  .modal-actions .btn { width: 100%; min-height: 48px; }
}

/* ═══ Skeleton ═════════════════════════════════════════════════════════ */
@keyframes shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position:  200% 0; }
}
.skeleton {
  background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
  border-radius: var(--radius-sm);
}

/* ═══ Animations ═══════════════════════════════════════════════════════ */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.fade-in { animation: fadeIn .28s var(--ease) both; }
.fade-in-delay-1 { animation-delay: .04s; }
.fade-in-delay-2 { animation-delay: .08s; }
.fade-in-delay-3 { animation-delay: .12s; }

@media (prefers-reduced-motion: reduce) {
  .fade-in, .fade-in-delay-1, .fade-in-delay-2, .fade-in-delay-3 { animation: none !important; }
}

/* ═══ Idle lock overlay ═══════════════════════════════════════════════ */
body.idle-locked > *:not(.idle-lock-overlay) {
  filter: blur(20px);
  pointer-events: none;
  user-select: none;
}
.idle-lock-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, .78);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 99999;
  animation: fadeIn .2s ease-out;
}
.idle-lock-card {
  background: #fff;
  padding: var(--space-6) var(--space-8);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  text-align: center;
  max-width: 360px;
}
.idle-lock-title { font-size: 1.1rem; font-weight: 700; margin-bottom: 6px; }
.idle-lock-body  { font-size: .9rem; color: var(--color-text-muted); }
.idle-lock-countdown { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--color-text); }
.idle-lock-stay { margin-top: var(--space-4); }

/* ═══ Login page ═══════════════════════════════════════════════════════ */
.login-page {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Anchor the absolutely-positioned theme control to the page box. */
  position: relative;
  background: var(--sidebar-bg);
  padding:
    max(var(--space-6), env(safe-area-inset-top, 0px))
    max(var(--space-6), env(safe-area-inset-right, 0px))
    max(var(--space-6), env(safe-area-inset-bottom, 0px))
    max(var(--space-6), env(safe-area-inset-left, 0px));
}
.login-card {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  padding: var(--space-10) var(--space-8);
  width: 100%;
  max-width: 400px;
  box-shadow: 0 24px 48px -16px rgba(0, 0, 0, .35);
}
.login-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-8);
}
.login-brand-mark {
  width: 36px; height: 36px;
  background: var(--color-primary);
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: #fff;
}
.login-brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.login-brand-name { font-size: .95rem; font-weight: 600; letter-spacing: -.01em; }
.login-brand-tag  { font-size: .75rem; color: var(--color-text-faint); }
.login-heading {
  font-size: 1.55rem;
  font-weight: 700;
  letter-spacing: -.025em;
  margin-bottom: var(--space-2);
}
.login-sub { font-size: .9rem; color: var(--color-text-muted); margin-bottom: var(--space-6); }

.login-password-wrap { position: relative; }
.login-password-wrap .form-control { padding-right: 44px; }
.login-toggle-pw {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  width: 32px; height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--color-text-faint);
  cursor: pointer;
}
.login-toggle-pw:hover { color: var(--color-text-muted); background: var(--color-bg); }
.login-toggle-pw:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; color: var(--color-text-muted); }
.login-remember { font-size: .875rem; color: var(--color-text-muted); }
.login-remember .form-check { user-select: none; }

/* ═══ Dashboard hero ═══════════════════════════════════════════════════ */
.dash-hero {
  background: var(--sidebar-bg);
  color: #e2e8f0;
  border-radius: var(--radius-xl);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.dash-hero-inner {
  padding: var(--space-8);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}
.dash-hero-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-6);
  flex-wrap: wrap;
}
/* flex: 0 1 auto so the NW block stays at its content width rather than
   growing into the gap; the milestone slot beside it absorbs the slack
   (and is the anchor the milestone hugs). */
.dash-hero-main { flex: 0 1 auto; min-width: 0; }

.dash-hero-actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  align-self: flex-end;
}

/* Milestone visual: FluentUI 3D PNG units arranged by HeroStackLayout into
   one of four shapes (fan / pile / heap / column), quantity from
   MilestoneVisual::for(). Each unit's placement, depth shading and z-order
   are computed in PHP and applied inline.

   Flex slot that sits immediately right of the NW column and soaks up the
   gap before the actions. It stretches to the row height (so the absolute
   milestone inside can centre vertically) but holds no in-flow content, so
   it never adds to the row height. Because flex items can't overlap, the
   milestone anchored to this slot's left edge can never run into the NW
   block at any width, which is the "as far left as safe" anchor. */
.dash-hero-milestone-slot {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  align-self: stretch;
}
.dash-hero-milestone {
  /* Absolute so the visual NEVER drives the hero row's height. Anchored to
     the slot's LEFT edge (a fixed gap right of the NW column) rather than
     the row centre, so it hugs the net-worth figure. */
  position: absolute;
  left: 0;
  top: 50%;
  width: 320px;
  height: 220px;
  transform: translateY(-50%);
  /* Hover-tooltip needs pointer events; bills underneath have their
     own pointer-events: none. */
}
/* Soft ground shadow so the pile sits on a surface instead of floating.
   Sits behind the units (z-index 0; units are z-index >= 1). Anchored a
   little below the slot centre since the layouts are centred there. */
.dash-hero-milestone::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 210px;
  height: 26px;
  transform: translate(-50%, 62px);
  background: radial-gradient(50% 50% at 50% 50%, rgba(0, 0, 0, .4) 0%, rgba(0, 0, 0, 0) 72%);
  filter: blur(2px);
  pointer-events: none;
  z-index: 0;
}
.dash-hero-milestone--coin::before { width: 170px; }
.dash-hero-milestone--moneybag::before,
.dash-hero-milestone--wad::before { width: 240px; height: 30px; }
/* Below ~960px the hero gets cramped horizontally and the milestone
   would start overlapping the headline or the action buttons. Hide
   it there rather than scaling — the milestone KPI card below still
   carries the milestone info numerically. */
@media (max-width: 960px) {
  /* Hide the whole slot, not just the milestone: an empty flex slot would
     still add a `gap` band between the NW figure and the actions when the
     hero stacks into a column. */
  .dash-hero-milestone-slot { display: none; }
}
.dash-hero-milestone-unit {
  position: absolute;
  /* Centre-origin: HeroStackLayout gives each unit an x/y offset from the
     slot centre, so every layout (fan/pile/heap/column) shares one render
     path. Per-unit transform, filter (depth shading) and z-index are set
     inline from the view. */
  left: 50%;
  top: 50%;
  width: 96px;
  height: 96px;
  transform-origin: center;
  pointer-events: none;
}

/* Concept-specific unit sizing (the art's natural scale). */
.dash-hero-milestone--coin .dash-hero-milestone-unit {
  width: 72px;
  height: 72px;
}
.dash-hero-milestone--moneybag .dash-hero-milestone-unit,
.dash-hero-milestone--wad .dash-hero-milestone-unit {
  width: 112px;
  height: 112px;
}

.hero-label {
  font-size: .68rem;
  font-weight: 600;
  color: #94a3b8;
  letter-spacing: .14em;
  text-transform: uppercase;
  margin-bottom: var(--space-3);
}
.hero-value {
  font-size: clamp(2rem, 4.6vw, 3.4rem);
  font-weight: 800;
  letter-spacing: -.04em;
  line-height: 1;
  font-feature-settings: 'tnum' 1;
  color: #f8fafc;
}
.hero-value .currency-sym {
  font-size: .42em;
  vertical-align: top;
  margin-top: .32em;
  margin-right: .06em;
  font-weight: 600;
  opacity: .5;
}
.hero-meta {
  margin-top: var(--space-4);
  font-size: .82rem;
  color: #94a3b8;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.hero-meta-dot { width: 6px; height: 6px; border-radius: 50%; background: #94a3b8; flex-shrink: 0; }

/* Compact "£980k · €1.1M · ₦1.6B" conversion strip — used on the dashboard
   hero (dark bg) and loans summary (light bg). The .fx-strip-light modifier
   shifts the slate colours up the ramp so the strip stays "supporting
   detail" rather than headline-weight on a white card. */
.fx-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  font-size: .82rem;
  color: #64748b;
  font-variant-numeric: tabular-nums;
  letter-spacing: .005em;
}
.fx-strip-item  { display: inline-flex; align-items: baseline; gap: 1px; }
.fx-strip-glyph { color: #94a3b8; margin-right: 1px; }
.fx-strip-code  { color: #94a3b8; font-size: .72rem; font-weight: 600; letter-spacing: .04em; margin-left: 2px; }
.fx-strip-sep   { color: #475569; }

.fx-strip-light { color: var(--color-text-muted); }
.fx-strip-light .fx-strip-glyph,
.fx-strip-light .fx-strip-code  { color: var(--color-text-faint); }
.fx-strip-light .fx-strip-sep   { color: var(--color-text-faint); opacity: .7; }

.hero-fx { margin-top: 6px; }

/* Per-KPI-card fx strip — sits between the headline and the card's
   footer link (or loan-bar). Smaller than .hero-fx since a KPI card
   is a fraction of the hero's footprint, and a hair tighter on the
   item gap so three currencies still fit on one line on narrow cards.
   Inherits the .fx-strip-light slate palette from the .fx-strip-light
   modifier the markup already applies. */
.kpi-fx {
  margin-top: var(--space-2);
  font-size: .76rem;
  gap: 2px 6px;
}

/* Composition strip — single bar, no chip legend below. */
.hero-composition { display: flex; flex-direction: column; gap: var(--space-2); }
.hero-comp-bar {
  display: flex;
  height: 6px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(255, 255, 255, .05);
}
.hero-comp-seg { height: 100%; transition: width var(--t-slow) var(--ease); }
.hero-comp-seg:not(:last-child) { border-right: 1px solid rgba(15, 23, 42, .8); }
.hero-comp-caption {
  font-size: .72rem;
  color: #94a3b8;
  letter-spacing: .04em;
}

/* Hero "vs last week" delta line. Sits between the big NW figure and
   the meta line so the user reads direction-of-travel without leaving
   the hero. tabular-nums keeps the number stable when it animates in
   on a navigation refresh. */
.hero-delta {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  margin-top: 4px;
  font-size: 1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
}
.hero-delta.tone-green   { color: #4ade80; }
.hero-delta.tone-red     { color: #f87171; }
.hero-delta.tone-neutral { color: #cbd5e1; }
.hero-delta-window {
  font-size: .7rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: #cbd5e1;
  text-transform: uppercase;
}
/* Mobile: the composition strip is too narrow at 6px to feel like a
   real chart and the donut+legend below carries the detail. Bump it
   to 14px so it reads as a substantive scaler not a hairline. */
@media (max-width: 600px) {
  .hero-comp-bar { height: 14px; }
}

/* ═══ Dashboard: allocation card + KPI strip ═══════════════════════════
   Allocation card now spans the full row on its own; the KPI cards
   (drift status + loans receivable + loans payable) live in their
   own grid-kpi row below. Splits a "describe / prescribe" hierarchy
   that read as one cramped column when stacked alongside the donut,
   and lets the legend use the full horizontal width without a
   scroll-trap inside the card. */
.grid-allocation { display: block; }

.grid-kpi {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-5);
  margin-top: var(--space-5);
  /* Each card sizes to its own content rather than stretching to the
     tallest sibling. After the drift card was flattened to the same
     shape as the others (title / headline+tag / caption / footer
     link), the height variance between cards is small enough that
     align-items: start reads cleanly without leaving big gaps. */
  align-items: start;
}

.allocation-body {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: var(--space-6);
  /* Top-align the donut with the first legend row; the legend can
     now grow much taller than the donut on accounts-heavy portfolios,
     so centering would visually orphan the donut. */
  align-items: start;
}
@media (max-width: 720px) { .allocation-body { grid-template-columns: 1fr; } }

.donut-wrap {
  position: relative;
  width: 100%;
  max-width: 260px;
  aspect-ratio: 1 / 1;
  margin: 0 auto;
}
.donut-center {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  text-align: center;
}

/* HTML tooltip for the allocation donut. Lives on <body> so a long label
   ("iShares Core ...") can wrap and overflow the narrow canvas instead of
   being clipped at its edge. Positioned above the hovered wedge. */
.chart-tooltip {
  position: absolute;
  z-index: 1000;
  pointer-events: none;
  transform: translate(-50%, calc(-100% - 10px));
  max-width: 240px;
  padding: 9px 11px;
  border-radius: 8px;
  background: rgba(15, 23, 42, .95);
  color: #fff;
  font-size: .72rem;
  line-height: 1.35;
  white-space: normal;
  opacity: 0;
  transition: opacity .12s ease;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .25);
}
.chart-tooltip-title  { font-weight: 600; margin-bottom: 4px; }
.chart-tooltip-row    { display: flex; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
.chart-tooltip-swatch { width: 9px; height: 9px; border-radius: 2px; flex: 0 0 auto; }
.donut-center-label {
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--color-text-faint);
  margin-bottom: 4px;
}
.donut-center-value {
  font-size: clamp(1.2rem, 2.4vw, 1.6rem);
  font-weight: 800;
  letter-spacing: -.025em;
  font-feature-settings: 'tnum' 1;
}
/* Centre subline shown when the donut centre is in top-category mode
   ("Equities · 77% of NW"). Quieter than the headline so the eye
   still lands on the category name first. */
.donut-center-stat {
  margin-top: 2px;
  font-size: .72rem;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

.allocation-legend {
  /* Block layout (not flex) so CSS multi-column can flow rows into
     two columns at wide widths. The cap-and-scroll trap is gone —
     the card grows to fit every account, and the multicol layout
     keeps the legend wide-but-not-tall on accounts-heavy portfolios. */
  display: block;
}
.allocation-legend-row {
  display: grid;
  grid-template-columns: 10px 1fr auto auto;
  align-items: center;
  gap: var(--space-3);
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  font-size: .85rem;
  transition: background var(--t-fast) var(--ease);
  /* Don't let the browser break a row across two multicol columns. */
  break-inside: avoid;
}
.allocation-legend-row:hover { background: var(--color-bg); }
/* Two-column legend once there's room. The 1100px threshold matches
   the rest of the dashboard's "wide enough for side-by-side" breakpoint. */
@media (min-width: 1100px) {
  .allocation-legend {
    column-count: 2;
    column-gap: var(--space-5);
  }
}
.alloc-swatch { width: 10px; height: 10px; border-radius: 3px; }
.alloc-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.alloc-pct  { font-variant-numeric: tabular-nums; font-weight: 600; font-size: .82rem; }
.alloc-target { font-variant-numeric: tabular-nums; font-size: .78rem; color: var(--color-text-faint); }

/* KPI cards (drift / loans receivable / loans payable on the
   dashboard + variants on the loans page). The dashboard now lays
   them out in a horizontal grid-kpi row, but the same .kpi-card
   styling still applies anywhere these cards land. */
.kpi-card  { padding: var(--space-5); }
@media (min-width: 601px) { .kpi-card { padding: var(--space-6); } }

.kpi-card.accent-green,
.kpi-card.accent-amber,
.kpi-card.accent-red {
  position: relative;
  overflow: hidden;
}
.kpi-card.accent-green::before,
.kpi-card.accent-amber::before,
.kpi-card.accent-red::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
}
.kpi-card.accent-green::before { background: var(--color-green); }
.kpi-card.accent-amber::before { background: var(--color-amber); }
.kpi-card.accent-red::before   { background: var(--color-red); }
/* Neutral accent used by the milestone card when progress is below
   the "almost there" / "halfway" thresholds — keeps the card visually
   in family with its siblings instead of standing out as the only
   one without a coloured left bar. Matches the green/amber/red
   variants' visual weight: just the left bar, no body tint. */
.kpi-card.accent-primary::before { background: var(--color-primary); }

.card-title {
  font-size: .68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.kpi-headline {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  margin-top: var(--space-1);
}
.kpi-value {
  font-size: 1.7rem;
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: 1.05;
  font-feature-settings: 'tnum' 1;
  /* min-width: 0 lets the headline shrink + wrap cleanly when the
     card is narrow, rather than forcing the meta column past the
     card's right edge (the .kpi-card has overflow: hidden so any
     overrun gets clipped, which previously chopped the tail off
     "target $125,000"). */
  min-width: 0;
}
.kpi-unit { font-size: .55em; font-weight: 800; opacity: .6; margin-left: 2px; }
/* min-width: 0 mirrors the .kpi-value rule above so the right column
   stays inside the card. flex-shrink: 0 on .kpi-tag would also work
   for the pill itself, but the caption text below still needs the
   column to be allowed to compress, hence the column-level fix. */
.kpi-meta { text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; min-width: 0; }

/* Status tag in the kpi-meta column. Pill-shaped to match the
   visual vocabulary of the .pill / .tag-* family used elsewhere
   (status pills below the drift count, account badges, etc.).
   Default neutral; .tone-* siblings tint background + text for
   green / amber / red / primary. */
.kpi-tag {
  display: inline-flex;
  align-items: center;
  padding: 3px 8px;
  border-radius: 999px;
  font-size: .72rem;
  font-weight: 600;
  line-height: 1.2;
  background: var(--color-bg);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.kpi-tag.tone-green   { background: var(--color-green-bg);   color: var(--color-green-fg);   border-color: var(--color-green-border); }
.kpi-tag.tone-amber   { background: var(--color-amber-bg);   color: var(--color-amber-fg);   border-color: var(--color-amber-border); }
.kpi-tag.tone-red     { background: var(--color-red-bg);     color: var(--color-red-fg);     border-color: var(--color-red-border); }
.kpi-tag.tone-primary { background: var(--color-primary-bg); color: var(--color-primary-hover); border-color: var(--color-primary-border); }


.kpi-card-link {
  display: block;
  text-align: right;
  margin-top: var(--space-3);
  font-size: .82rem;
  color: var(--color-primary);
  text-decoration: none;
  font-weight: 500;
}
.kpi-card-link:hover { text-decoration: underline; }

.loan-bar { margin-top: var(--space-4); }
.loan-bar-meta {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-3);
  font-size: .75rem;
  color: var(--color-text-muted);
  flex-wrap: wrap;
}
.loan-bar-link { margin-left: auto; color: var(--color-primary); font-weight: 600; font-size: .78rem; }
.loan-bar-link:hover { text-decoration: underline; }

/* ── Loans page summary card ────────────────────────────────────────────
   One card carries the meaningful figure ("$50,000 outstanding") with
   active count + % of net worth as supporting context; the policy
   thresholds sit to the right. The status row beneath only renders when
   there's something past due or written off — no point colouring an
   empty state. */
.loan-summary { display: flex; flex-direction: column; gap: var(--space-4); }
.loan-summary-headline {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.loan-summary-main { min-width: 0; flex: 1 1 auto; }
.loan-summary-value {
  font-size: clamp(1.6rem, 3.2vw, 2.1rem);
  font-weight: 800;
  letter-spacing: -.03em;
  line-height: 1.05;
  font-feature-settings: 'tnum' 1;
  margin-top: 4px;
}
.loan-summary-sym {
  font-size: .55em;
  vertical-align: top;
  margin-top: .35em;
  margin-right: .04em;
  /* `color: inherit` so the +/− directional sign picks up the
     same green/red as the digits when the parent has
     num-positive / num-negative. On the (legacy) untinted variant
     the parent's default colour cascades through. */
  color: inherit;
  font-weight: 700;
  display: inline-block;
}
.loan-summary-sub { margin-top: 6px; font-size: .85rem; color: var(--color-text-muted); }
.loan-summary-thresholds { text-align: right; flex-shrink: 0; }
.loan-summary-thresholds .eyebrow { display: block; margin-bottom: 2px; }

.loan-summary-status {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border-light);
}
.loan-summary-status .tag { padding: 4px 10px; font-size: .76rem; }

/* Conversion strip slot inside the summary card: slightly tighter
   margin-top than on the dashboard hero because it sits between the
   value (large) and the "1 active · 3.3% of NW" sub line, and we want
   them to feel like one stack of related context. */
.loan-summary .fx-strip { margin-top: 4px; }

/* Borrower cell: name + repayment-type badges on one line. */
.loan-borrower-line {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

/* ═══ Dashboard: account cards ═════════════════════════════════════════ */
.account-cards-section { margin: 0; }
/* Filter-chip row above the accounts grid. Three chips: All / Needs
   attention / Compact. Mode lives on .account-grid's data-mode attr;
   CSS rules below handle the visual transformations. */
.account-filter {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
  flex-wrap: wrap;
}
.account-filter-chip {
  padding: 6px 12px;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease);
}
.account-filter-chip:hover { background: var(--color-bg-elevated); }
.account-filter-chip[data-active="1"] {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.account-filter-chip[data-active="1"] .text-faint { color: rgba(255, 255, 255, .8); }

.account-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--space-4);
}
/* Needs-attention mode: hide green-status cards so the user sees only
   the rows that need action. Counts in the filter chip already show
   how many will remain (red + amber). */
.account-grid[data-mode="attention"] .account-card.status-green { display: none; }
/* In the attention view, lead with the most off-target cards (highest drift
   first) via CSS order; the default "All" view keeps DOM/USD order. */
.account-grid[data-mode="attention"] .account-card { order: var(--drift-rank, 0); }
.account-card {
  position: relative;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  overflow: hidden;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.account-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 3px;
  background: var(--accent, var(--color-primary));
  opacity: .9;
}
.account-card:hover { box-shadow: var(--shadow-sm); }
.account-card.status-amber { border-color: var(--color-amber-border); }
.account-card.status-red   { border-color: var(--color-red-border); }

.ac-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-3); }
.ac-head-main { min-width: 0; flex: 1; }
.ac-name {
  font-size: .95rem;
  font-weight: 700;
  letter-spacing: -.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 6px;
}
/* Account title links to its edit page; keep it looking like the heading
   until hover so it doesn't read as a generic link. */
.ac-name-link { color: inherit; text-decoration: none; }
.ac-name-link:hover { text-decoration: underline; text-decoration-thickness: 1px; }
.ac-meta { display: flex; gap: 6px; flex-wrap: wrap; }
.ac-status { flex-shrink: 0; text-transform: capitalize; }

.ac-values {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-bottom: var(--space-3);
  border-bottom: 1px dashed var(--color-border-light);
}
.ac-usd {
  font-size: 1.45rem;
  font-weight: 800;
  letter-spacing: -.025em;
  font-feature-settings: 'tnum' 1;
  line-height: 1.05;
}
.ac-usd-sym {
  font-size: .62em;
  vertical-align: top;
  margin-top: .2em;
  margin-right: .04em;
  color: var(--color-text-faint);
  font-weight: 700;
}
.ac-local {
  font-size: .78rem;
  color: var(--color-text-muted);
  display: flex;
  align-items: baseline;
  gap: 4px;
}

.ac-allocation { display: flex; flex-direction: column; gap: var(--space-3); }
.ac-pct-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--space-2); }
.ac-pct-block-right { text-align: right; }
.ac-pct-label {
  font-size: .62rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--color-text-faint);
  margin-bottom: 3px;
}
.ac-pct-value {
  font-size: .88rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.ac-progress { position: relative; }
.ac-progress-track {
  position: relative;
  height: 6px;
  background: var(--color-border-light);
  border-radius: 999px;
  overflow: hidden;
}
.ac-progress-fill {
  position: absolute;
  inset: 0 auto 0 0;
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  transition: width .9s var(--ease);
}
/* Fixed target reference at 70% of the track (matches the $fillPct scale in
   the view). Lives on .ac-progress, NOT inside the overflow:hidden track, so
   it's never clipped, and it paints above the fill so it stays visible even
   when the bar reaches/passes it. */
.ac-progress-target {
  position: absolute;
  left: 70%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 2px; height: 11px;
  background: var(--color-text-muted);
  border-radius: 1px;
  z-index: 1;
}

.ac-action {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: .72rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.ac-action-buy   { background: var(--color-green-bg); color: var(--color-green-fg); }
.ac-action-sell,
.ac-action-trim  { background: var(--color-amber-bg); color: var(--color-amber-fg); }
.ac-action-hold  { background: var(--color-bg); color: var(--color-text-muted); border: 1px solid var(--color-border); }
/* On a red-status card the reduce pill escalates from amber to red so it
   matches the card's urgency; amber-status cards keep the amber pill. */
.account-card.status-red .ac-action-sell,
.account-card.status-red .ac-action-trim { background: var(--color-red-bg); color: var(--color-red-fg); }

/* On-target card: the "On target" pill plus a quiet caption showing the
   buy/trim amount (USD + local) needed to sit exactly on target. Grouped so
   the caption tucks just under the pill rather than a full card-gap below. */
.ac-ontarget { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
.ac-tune { padding-left: 2px; }

/* Off-NW card: in place of the allocation visuals, a quiet "why it's
   excluded" block plus the user's note (or a hint) so the freed space
   carries context instead of sitting empty. */
.ac-offnw { display: flex; flex-direction: column; gap: 4px; }
.ac-offnw-reason {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: .8rem; font-weight: 600; color: var(--color-text-muted);
}
.ac-offnw-reason svg { color: var(--color-text-faint); flex-shrink: 0; }
.ac-offnw-notes { white-space: pre-line; }

@media (max-width: 600px) {
  .account-grid { grid-template-columns: 1fr; }
  .ac-usd { font-size: 1.3rem; }
}

/* ═══ Charts ═══════════════════════════════════════════════════════════ */
.chart-wrap { position: relative; width: 100%; height: 300px; }
.trend-card { /* nothing — uses .card */ }
.chart-skeleton {
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
  background: linear-gradient(90deg, var(--color-bg) 0%, var(--color-bg-elevated) 50%, var(--color-bg) 100%);
  background-size: 200% 100%;
  animation: chartSkeleton 1.4s linear infinite;
  pointer-events: none;
}
.chart-skeleton.is-hidden { display: none; }
@keyframes chartSkeleton {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .chart-skeleton { animation: none; }
}

/* ═══ Trends: lifetime totals + per-metric FX strip ═══════════════════
   Sits inside the yearly-summary card as a footer. Three rows
   (Income / Expenses / Net), each: muted label, headline USD value
   tinted green/red, then the compact FX strip. Grid columns keep the
   labels and figures aligned across rows. */
.trends-lifetime {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.trends-lifetime-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.trends-lifetime-row {
  display: grid;
  grid-template-columns: 88px minmax(96px, auto) 1fr;
  align-items: baseline;
  gap: var(--space-3) var(--space-4);
  padding: var(--space-2) 0;
  border-bottom: 1px dashed var(--color-border-light);
}
.trends-lifetime-row:last-child { border-bottom: none; }
.trends-lifetime-label {
  font-size: .8rem;
  color: var(--color-text-muted);
  font-weight: 600;
}
.trends-lifetime-value {
  font-size: 1.05rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.trends-lifetime-row .fx-strip { font-size: .78rem; }
@media (max-width: 600px) {
  .trends-lifetime-row {
    grid-template-columns: 1fr auto;
    gap: 2px var(--space-3);
  }
  .trends-lifetime-row .fx-strip { grid-column: 1 / -1; }
}

/* ═══ Trends: by-category breakdown ════════════════════════════════════
   Horizontal bar list — one row per category, proportional bar showing
   share of the section total. Matches the calm/restrained aesthetic
   (no donut, no rainbow), reuses the existing eyebrow / num-tone
   vocabulary. Two stacked sections per year (expenses, income).      */
.category-year-chips {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.category-year-chip {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  font-size: .78rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: var(--color-bg);
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.category-year-chip:hover { background: var(--color-bg-elevated); color: var(--color-text); }
.category-year-chip.is-active {
  background: var(--color-primary-bg);
  color: var(--color-primary-hover);
  border-color: var(--color-primary-border);
}
@media (max-width: 360px) {
  .category-year-chip { padding: 3px 8px; font-size: .72rem; }
}

.category-year-panel + .category-year-panel { margin-top: var(--space-5); }
.category-section + .category-section { margin-top: var(--space-5); }

.category-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.category-section-total {
  font-size: .95rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.category-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.category-row {
  display: grid;
  /* name | bar | sparkline | $amount | %+change */
  grid-template-columns: minmax(110px, 1.2fr) minmax(0, 2.4fr) 56px 78px 76px;
  align-items: center;
  /* Explicit axes: column gap as before; small row gap so the optional FX
     line (see .category-row-fx) tucks just under the amount, not a full
     space-3 below. */
  row-gap: 2px;
  column-gap: var(--space-3);
  /* Horizontal padding + matching negative margin: the hover highlight
     bleeds to the card edges so the row reads as one band linking the
     category name to its far-right amount/%, without shifting the grid. */
  padding: 6px 8px;
  margin: 0 -8px;
  border-radius: var(--radius-sm);
  transition: background var(--t-fast) var(--ease);
}
.category-row:hover { background: var(--color-bg); }
/* Foreign-currency breakdown beneath a category row: spans all columns on
   its own grid line and right-aligns under the amount/%. Visibility is
   governed by the shared [data-trends-fx-state] rule (hidden when "off"). */
.category-row .category-row-fx {
  grid-column: 1 / -1;
  justify-content: flex-end;
  margin-top: 0;
}

/* Trends footer: accepted import currencies — a labelled, wrapping chip
   group instead of a cramped run-on sentence of badges. */
.import-currencies-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}
.import-currencies-label {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.import-currencies-action { font-size: .78rem; }
.import-currencies-note { font-size: .78rem; color: var(--color-text-faint); }
.import-currencies-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 5px;
}

/* Collapsible category groups (e.g. Housing → rent, repairs, …). The group
   and its children stack like list rows; the header is a full-row button. */
.category-group { display: flex; flex-direction: column; gap: 6px; }
.category-group-children {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* Header reuses the .category-row grid; reset the button chrome. */
.category-group-head {
  width: 100%;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: left;
}
.category-group-head .category-name { font-weight: 600; display: inline-flex; align-items: center; gap: 4px; min-width: 0; }
.category-group-head .category-amount { font-weight: 700; }
.category-group-head:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; border-radius: var(--radius-sm); }
.category-group-caret {
  flex-shrink: 0;
  color: var(--color-text-faint);
  transition: transform var(--t-fast) var(--ease);
}
.category-group.is-collapsed .category-group-caret { transform: rotate(-90deg); }
.category-group.is-collapsed .category-group-children { display: none; }
/* Sub-items: indented under the header, lighter than a top-level row. */
.category-child .category-name { padding-left: 20px; color: var(--color-text-muted); }
.category-child .category-amount { font-weight: 500; }

.category-name {
  font-size: .82rem;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.category-bar-wrap {
  position: relative;
  height: 6px;
  background: var(--color-border-light);
  border-radius: 999px;
  overflow: hidden;
  min-width: 0;
}
.category-bar {
  position: relative;
  z-index: 1;
  height: 100%;
  border-radius: 999px;
  transition: width var(--t-slow) var(--ease);
}
.category-bar-expense { background: var(--color-red); opacity: .7; }
.category-bar-income  { background: var(--color-green); opacity: .8; }
/* Ghost bar: last year's level, sat behind the live bar. Where it peeks past
   the live bar the category shrank; where the live bar runs past it, it grew. */
.category-bar-ghost {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 0;
  height: 100%;
  border-radius: 999px;
  background: var(--color-text-faint);
  opacity: .28;
}
/* Marker line at last year's level, drawn on top of the live bar so the
   comparison is visible in both directions: a notch inside the bar when this
   year has grown past last year, or out beyond the bar's end when it's below. */
.category-bar-marker {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 2;
  width: 2px;
  margin-left: -1px;
  background: var(--color-text);
  opacity: .65;
  border-radius: 1px;
}

/* Within-year monthly sparkline. Fixed-width column; muted, tinted by sign. */
.category-spark-cell {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-width: 0;
  height: 16px;
}
.category-spark { display: block; width: 56px; height: 16px; opacity: .8; }
.category-spark.spark-expense { color: var(--color-red); }
.category-spark.spark-income  { color: var(--color-green); }

.category-amount {
  font-size: .82rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
/* Last grid cell: % on top, the year-over-year change chip beneath it. */
.category-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  min-width: 0;
}
.category-pct {
  font-size: .75rem;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.category-delta {
  font-size: .68rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  line-height: 1;
}
.category-delta.is-good { color: var(--color-green); }
.category-delta.is-bad  { color: var(--color-red); }
.category-delta.is-flat,
.category-delta.is-new  { color: var(--color-text-faint); font-weight: 500; }

/* Per-year KPI strip: the punchline (income / spend / net / savings) above
   the lists. */
.category-kpis {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.category-kpi {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-3);
  background: var(--color-bg);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius-sm);
}
.category-kpi-label {
  font-size: .64rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.category-kpi-val {
  font-size: 1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* KPI comparison line (under each KPI value) + the partial-year period note. */
.category-kpi-cmp {
  font-size: .68rem;
  line-height: 1;
  margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
.category-kpi-cmp.is-muted { color: var(--color-text-faint); }
.category-period {
  font-size: .72rem;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}
.category-period.is-partial { color: var(--color-amber, var(--color-text-muted)); font-weight: 600; }

/* Toolbar: partial-year/period note on the left, sort control on the right. */
.category-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.category-sort { display: inline-flex; gap: 2px; }
.category-sort button {
  background: none;
  border: 1px solid transparent;
  font: inherit;
  font-size: .72rem;
  color: var(--color-text-muted);
  padding: 3px 9px;
  border-radius: 999px;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.category-sort button:hover { background: var(--color-bg-elevated); color: var(--color-text); }
.category-sort button.is-active {
  background: var(--color-primary-soft, var(--color-bg-elevated));
  color: var(--color-text);
  font-weight: 600;
}

/* "Other" rollup row: same grid as a category row but a quieter, tappable
   summary of the folded tail. */
.cat-other-row {
  cursor: pointer;
  color: var(--color-text-muted);
}
.cat-other-row .category-name { font-style: italic; }
.cat-other-row .cat-other-bar { opacity: .4; }
.cat-other-row:hover { background: var(--color-bg); }
.cat-other-row:focus-visible { outline: 2px solid var(--color-primary); outline-offset: -2px; border-radius: var(--radius-sm); }

@media (max-width: 600px) {
  /* Stack: name on top, bar / amount / (% + change) on a second row. The
     sparkline is dropped on narrow viewports to keep the row uncluttered. */
  .category-row {
    grid-template-columns: minmax(0, 1fr) 64px 60px;
    grid-template-areas:
      "name    name    name"
      "bar     amount  meta";
    gap: 4px var(--space-3);
  }
  .category-name      { grid-area: name; }
  .category-bar-wrap  { grid-area: bar; }
  .category-spark-cell { display: none; }
  .category-amount    { grid-area: amount; }
  .category-meta      { grid-area: meta; }

  .category-kpis { grid-template-columns: repeat(2, 1fr); }
}

/* In-cell FX strip for the per-year trends table. Tighter than the
   default .fx-strip so three currencies fit comfortably under the USD
   value without forcing an extra line wrap. The cell aligns USD
   primary value left/right per its existing class; the strip flows
   underneath. */
.trends-cell-fx { vertical-align: top; }
.trends-cell-year { vertical-align: top; }
.fx-strip-cell {
  margin-top: 3px;
  font-size: .72rem;
  font-weight: 500;
  gap: 2px 6px;
}
.fx-strip-cell .fx-strip-code { font-size: .66rem; }
/* Override the green/red tint inherited from the parent num-positive
   /num-negative cell — the FX strip should stay neutral so the year's
   USD figure remains the only colour-coded value. */
.num-positive .fx-strip-cell,
.num-negative .fx-strip-cell { color: var(--color-text-muted); }

/* Trends summary card actions + FX visibility toggle. The card carries
   data-trends-fx-state so a single attribute hides every FX strip inside
   (per-row + lifetime) without per-element traversal. */
.trends-summary-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.trends-fx-toggle svg { opacity: .7; }
.trends-fx-toggle[aria-pressed="true"] {
  background: var(--color-primary-bg);
  color: var(--color-primary-hover);
}
.trends-fx-toggle[aria-pressed="true"] svg { opacity: 1; color: var(--color-primary); }

[data-trends-fx-state="off"] .fx-strip-cell,
[data-trends-fx-state="off"] .trends-lifetime-row .fx-strip { display: none; }

/* ═══ Holdings (balances/update.php) ═══════════════════════════════════ */
.balance-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  /* Query container so the multi-balance row can restack on CARD width,
     not viewport width. These cards live in an auto-fill grid, so two or
     three share a row on desktop and each card gets narrow even though
     the viewport is wide; a viewport media query can't see that. */
  container-type: inline-size;
  /* Own stacking context + clip so the watermark index (::after, below)
     can sit behind the content without spilling past the card edge. */
  position: relative;
  z-index: 0;
  overflow: hidden;
}

/* Card numbering: a faint watermark index behind each card's content.
   The number is injected per card via data-card-num (set in the balances
   view's loop) rather than a CSS counter: .balance-card has
   container-type: inline-size, which imposes style containment and scopes
   CSS counters to each card's own subtree, so counter() resolved to 0 on
   mobile Safari. attr() sidesteps that entirely. */
.balance-card::after {
  content: attr(data-card-num);
  position: absolute;
  right: var(--space-4);
  bottom: var(--space-2);
  z-index: -1;
  font-size: 6rem;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--color-text);
  opacity: 0.05;
  pointer-events: none;
  user-select: none;
}

/* Unsaved-edit cue. Each balance card saves on its own button; once an
   input differs from the last saved value the card flags itself amber so
   a typed-but-not-saved edit can't masquerade as a saved one. Card border
   + an "Unsaved changes" note make it spottable while scrolling; the ring
   points at the specific Save button still waiting to be pressed. */
.balance-card.is-unsaved { border-color: var(--color-amber-border); }
.balance-card.is-unsaved .balance-updated::after {
  content: " · Unsaved changes";
  color: var(--color-amber-fg);
  font-weight: 600;
}
.balance-card form.is-unsaved button[type="submit"] {
  box-shadow: 0 0 0 3px rgba(245, 158, 11, .35);
}
.balance-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}
/* The left (name/updated) column must be allowed to shrink so the name
   truncates; without min-width:0 it holds its full width and pushes the
   right-aligned amount stack past the card's padding. */
.balance-header > :first-child { min-width: 0; }
/* ───────────────────────────────────────────────────────────────────────
   Balances page type scale (consistent roles for every value/indicator):
     amount/hero      1.5rem  / 800 / -.02em   tabular   (portfolio total)
     amount/primary   1.25rem / 700 / -.015em  tabular   (per-account header USD)
     amount/secondary 1.05rem / 600 / -.01em   tabular   (editable balance fields: slot + mb amount; read-only onchain holdings; loans figure)
     card title       1rem    / 700 / -.01em             (account name, section title)
     item label       .78rem  / 600            muted     (onchain label, mb caption)
     field eyebrow    .68rem  / 600 / +.05em UPPER muted  (money-slot caption)
     percentage       .82rem  / 600            muted tabular
     meta/conversion  .72rem  / —              faint tabular (≈$, local total, timestamps)
     control text     .75rem  / —              (toggles, disclosure summaries)
     error            .72rem  / 500            red
   ─────────────────────────────────────────────────────────────────────── */
.balance-name { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 1rem; font-weight: 700; letter-spacing: -.01em; }
/* The title links to the account's edit page; keep it looking like the
   heading until hover so it doesn't read as a generic blue link. */
.balance-name a { color: inherit; text-decoration: none; }
.balance-name a:hover { text-decoration: underline; text-decoration-thickness: 1px; }
.balance-updated { font-size: .72rem; color: var(--color-text-faint); margin-top: 2px; }
.balance-usd-stack { text-align: right; }
.balance-usd { font-size: 1.25rem; font-weight: 700; letter-spacing: -.015em; font-variant-numeric: tabular-nums; }
/* Header secondary line (local total / shares / BTC) drops to the quiet
   conversion tier: faint, not muted, matching the ≈$ lines. */
.balance-usd-stack .text-muted { color: var(--color-text-faint); }
.balance-card.is-readonly { background: var(--color-bg-elevated); }

/* Off-NW account: a dashed border (vs the solid default) flags at a glance
   that the card's value isn't counted, plus a small badge under the name. */
.balance-card.is-off-nw { border-style: dashed; }
.off-nw-badge {
  display: inline-block;
  margin-top: 3px;
  padding: 1px 7px;
  font-size: .6rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: 999px;
}

.balance-form-full { display: flex; flex-direction: column; gap: var(--space-3); }

/* Captioned "money slot": a filled, rounded panel that makes the value
   being entered the focal point of the card. A small uppercase caption
   sits above a large tabular value; the currency glyph is a faint inline
   prefix and Save lives inside the slot, right-aligned, so the number you
   type reads as money rather than a generic form box. The inner input is
   borderless/transparent -- the slot carries the border, focus ring, and
   invalid state. The multi-balance .mb-amount-field mirrors this style. */
.money-slot {
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: 4px var(--space-3) 6px;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.money-slot:focus-within {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(79, 70, 229, .18);
  background: var(--color-surface);
}
.money-slot:has(.amount-input.is-invalid) {
  border-color: var(--color-red);
  box-shadow: 0 0 0 3px rgba(220, 38, 38, .12);
}
/* Generic invalid state for a plain .form-control (e.g. the inline
   payment-edit amount, which has no money-slot wrapper to colour). */
.form-control.is-invalid {
  border-color: var(--color-red);
  box-shadow: 0 0 0 3px rgba(220, 38, 38, .12);
}

/* Loan instalment ledger (loans/edit.php): statement-style year grouping. */
.loan-year + .loan-year { margin-top: var(--space-2); }
.loan-year-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-2) 0;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--color-border);
  font: inherit;
  font-weight: 700;
  font-size: .9rem;
  color: var(--color-text);
  text-align: left;
  cursor: pointer;
}
.loan-year-caret {
  display: inline-block;
  width: 1em;
  color: var(--color-text-faint);
}
.loan-pmt-num {
  min-width: 1.75rem;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
  font-size: .8rem;
}
.loan-pmt-amt {
  min-width: 104px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* Match the money inputs to the 42px height of the form controls (currency
   select, dates) they sit beside, so a form row lines up. Scoped to loans so
   the balances page's own money-slot sizing is untouched. */
.loan-money-slot {
  min-height: 42px;
  padding-top: 0;
  padding-bottom: 0;
  display: flex;
  align-items: center;
}
.loan-money-slot .money-slot-body { flex: 1 1 auto; min-width: 0; }
/* Bare .hint has no global rule, so mute it explicitly inside the statement
   (year subtotal, payment notes) instead of inheriting the bold year colour. */
.loan-year-head .hint { font-weight: 500; color: var(--color-text-faint); }
.loan-payment .hint { color: var(--color-text-faint); }
.money-slot-caption {
  font-size: .68rem;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: 1px;
}
.money-slot-caption .hint {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}
.money-slot-body { display: flex; align-items: center; gap: 6px; }
.money-slot-prefix {
  flex: 0 0 auto;
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.money-slot-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  padding: 0;
  margin: 0;
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  outline: none;
}
.money-slot-input::placeholder { color: var(--color-text-faint); font-weight: 400; }
.money-slot-btn { flex: 0 0 auto; white-space: nowrap; }

.amount-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  min-height: 1.1em;
  margin-top: 4px;
  font-size: .72rem;
}
.amount-preview { color: var(--color-text-faint); font-variant-numeric: tabular-nums; }
.amount-error   { color: var(--color-red-fg); font-weight: 500; }

/* Portfolio-total card sits at the bottom of the balances page. The label
   stays left, the figure (with optional FX strip beneath) right-aligned so
   numbers don't drift away from the eye line as the user finishes scanning
   down the balances grid. */
.portfolio-total-card { padding: var(--space-5); }
.portfolio-total-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.portfolio-total-meta { min-width: 0; flex: 1 1 auto; }
.portfolio-total-value { text-align: right; flex-shrink: 0; }
.portfolio-total-amount {
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: -.02em;
  font-feature-settings: 'tnum' 1;
  white-space: nowrap;
}
.portfolio-total-value .fx-strip { margin-top: 4px; justify-content: flex-end; }

/* Legacy alias used by balance cards: small flex row with two ends. */
.rc-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: .78rem;
  color: var(--color-text-muted);
}

/* On-chain BTC accounts on the balances page. Each address is a row
   showing label (optional), the address (truncated), and the per-
   address BTC balance. The block sits inside .balance-card so it
   inherits the surrounding spacing. */
.onchain-list {
  list-style: none;
  /* No bottom margin — sibling blocks (`.onchain-more`, `.onchain-actions`)
     set their own margin-top so the gap can be tightened when the
     disclosure follows directly without affecting the list-to-actions
     spacing when there's no disclosure. */
  margin: var(--space-3) 0 0;
  padding: 0;
  /* Auto-fit grid: at standard card widths (≥360px content) the inline
     "first 2" addresses sit side-by-side, halving the card height.
     auto-fit collapses to one column when the cell can't reach the
     minimum width, so narrow viewports degrade cleanly. The disclosure
     list (`.onchain-list-rest`) overrides this to stay single-column for
     easier scanning when the user has many addresses. */
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-2);
}
.onchain-row {
  /* In the compact 2-up cell, content stacks vertically (label →
     address → amount) so it fits a narrow track. The disclosure list
     restores a horizontal layout below. */
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: var(--space-3);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius-sm);
  background: var(--color-bg-elevated);
}
.onchain-list-rest { grid-template-columns: 1fr; }
.onchain-list-rest .onchain-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-3);
  align-items: baseline;
}
.onchain-meta { min-width: 0; }
.onchain-label {
  font-size: .78rem;
  font-weight: 600;
  margin-bottom: 2px;
  color: var(--color-text-muted);
}
.onchain-address {
  display: block;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .76rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.onchain-amount {
  /* The per-holding value (shares / BTC / cash) is the figure that
     matters on these read-only cards, so it reads at a comfortable size
     rather than the small .82rem caption scale it used to share with the
     label. Still below the editable money-slot hero, keeping hierarchy. */
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The "@ price ccy" unit-price sub-line is a conversion-tier value: faint. */
.onchain-amount .text-muted { color: var(--color-text-faint); }
.onchain-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
  /* margin-top picks up the spacing the address list used to contribute
     via its own margin-bottom. Keeps list-to-actions distance unchanged
     in the no-disclosure case. */
  margin-top: var(--space-4);
}

/* "Show N more" disclosure for accounts with >2 on-chain addresses.
   Styled as a quiet text link rather than a button — it's UI
   navigation, not data. Two span labels swap visibility via the
   [open] attribute on <details> so the summary reads "Show N more"
   when collapsed and "Show fewer" once expanded. */
.onchain-more { margin-top: var(--space-2); }
.onchain-more-toggle {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 0;
  font-size: .75rem;
  font-weight: 600;
  color: var(--color-primary);
  user-select: none;
}
.onchain-more-toggle::-webkit-details-marker { display: none; }
.onchain-more-toggle::after {
  content: '';
  width: 7px; height: 7px;
  border-right: 2px solid var(--color-primary);
  border-bottom: 2px solid var(--color-primary);
  transform: rotate(45deg);
  transition: transform var(--t-fast) var(--ease);
  margin-left: 2px;
  margin-bottom: 2px;
}
.onchain-more[open] .onchain-more-toggle::after { transform: rotate(-135deg); margin-bottom: -2px; }
.onchain-more-toggle:hover { color: var(--color-primary-hover); }

.onchain-more [data-when-open]   { display: none; }
.onchain-more[open] [data-when-open]   { display: inline; }
.onchain-more[open] [data-when-closed] { display: none; }

.onchain-list-rest { margin-top: var(--space-2); }

/* ═══ Trends ═══════════════════════════════════════════════════════════ */
.sample-link {
  display: block;
  text-decoration: none;
  color: inherit;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-4);
  transition: border-color var(--t-fast) var(--ease);
}
.sample-link:hover { border-color: var(--color-primary); }
.sample-link-title { font-weight: 600; font-size: .95rem; margin-bottom: 4px; }
.sample-pre {
  font-size: .72rem;
  line-height: 1.45;
  margin: 0 0 var(--space-3);
  background: var(--color-bg);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  overflow-x: auto;
}

/* ═══ Settings: key-value list ═════════════════════════════════════════ */
.kv-list { display: flex; flex-direction: column; }
.kv-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border-light);
}
.kv-row:last-child { border-bottom: none; }
.kv-label { font-weight: 600; font-size: .875rem; }
.kv-sub   { font-size: .72rem; color: var(--color-text-faint); margin-top: 2px; }
.kv-value { text-align: right; }
.kv-value-primary { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; font-variant-numeric: tabular-nums; }
.kv-value-secondary { font-size: .72rem; color: var(--color-text-faint); margin-top: 2px; }

.currency-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: var(--space-2);
}
.currency-pick {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 8px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
  font-size: .85rem;
}
.currency-pick:hover { border-color: #cbd5e1; background: var(--color-bg-elevated); }
.currency-pick:has(input:checked) { border-color: var(--color-primary); background: var(--color-primary-bg); }
.currency-pick.is-locked {
  cursor: not-allowed;
  /* Spans two grid cells so the "Always on" pill has room without
     overflowing into the neighbouring chip on narrow viewports. */
  grid-column: span 2;
}
.currency-pick.is-locked input[type="checkbox"] { cursor: not-allowed; opacity: .55; }
.currency-pick code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.currency-pick .currency-flag { font-size: 1.05em; line-height: 1; }
.currency-pick .currency-always-on {
  font-size: .58rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-left: auto;
  padding: 1px 6px;
  background: var(--color-bg-elevated);
  border-radius: 999px;
  white-space: nowrap;
  flex-shrink: 0;
}
.currency-group {
  margin-bottom: var(--space-4);
}
.currency-group:last-of-type { margin-bottom: 0; }
.currency-group-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  text-transform: uppercase;
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .06em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
}
.currency-group-label .currency-group-count {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--color-text-faint);
}

/* ═══ Profile / preferences key-value rows ═════════════════════════════ */
.profile-list { display: flex; flex-direction: column; gap: var(--space-2); font-size: .9rem; }
.profile-list > div { display: flex; align-items: baseline; gap: var(--space-2); }
.profile-list .text-muted { min-width: 96px; }

/* ═══ Recommendations ══════════════════════════════════════════════════ */

.rec-profile-form { display: flex; flex-direction: column; gap: var(--space-3); }

/* Snapshot list inside disclosure */
.rec-snapshot-list { display: flex; flex-direction: column; gap: var(--space-2); }
.rec-snapshot-row {
  display: grid;
  grid-template-columns: minmax(140px, 1.2fr) minmax(0, 2fr) auto;
  gap: var(--space-4);
  align-items: center;
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border-light);
  background: var(--color-surface);
}
.rec-snapshot-row.tone-amber { border-color: var(--color-amber-border); }
.rec-snapshot-row.tone-red   { border-color: var(--color-red-border); }
.rec-snapshot-name { min-width: 0; }
.rec-snapshot-bars { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.rec-snapshot-drift { text-align: right; min-width: 60px; }

/* Bar pattern (snapshot + allocation comparison) */
.rec-bar-row { display: flex; align-items: center; gap: var(--space-2); font-size: .75rem; }
.rec-bar-label { width: 84px; color: var(--color-text-muted); flex-shrink: 0; }
.rec-bar-track {
  flex: 1;
  height: 6px;
  background: var(--color-border-light);
  border-radius: 999px;
  overflow: hidden;
  min-width: 0;
}
.rec-bar-fill { height: 100%; border-radius: 999px; transition: width var(--t-slow) var(--ease); }
.rec-bar-actual      { background: var(--color-primary); }
.rec-bar-target      { background: #94a3b8; }
.rec-bar-value {
  width: 56px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  flex-shrink: 0;
}

/* Cash-flow inline cards inside the snapshot disclosure */
.rec-cashflow-section {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.rec-cashflow {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--space-2);
}
.rec-cashflow-card {
  padding: var(--space-3);
  background: var(--color-bg-elevated);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border-light);
}
.rec-cashflow-net {
  font-size: 1.1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  margin: 2px 0;
}

/* Result block */
.rec-result { padding: var(--space-6); }

/* The Read tab's summary is rendered as a standard .rec-block now,
   not a callout box. Only .rec-summary-text remains as a styling hook
   for the body paragraph; the eyebrow/callout/quote glyph rules
   moved with the markup. */
.rec-summary-text { font-size: .95rem; line-height: 1.6; color: var(--color-text); }

.rec-block { margin-bottom: var(--space-6); }
.rec-block-title {
  font-size: .76rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-3);
}
.rec-twocol {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-5);
  margin-bottom: var(--space-6);
}
@media (max-width: 800px) { .rec-twocol { grid-template-columns: 1fr; } }

/* Action cards */
.rec-action-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
  gap: var(--space-3);
}
.rec-action-card {
  position: relative;
  padding: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  border-left: 3px solid var(--color-border);
}
.rec-action-card.tone-red    { border-left-color: var(--color-red); }
/* Mute the medium tier's left rule to neutral gray so only genuinely
   critical (high → red) action rows visually pop. The .rec-action-
   priority label is still tinted amber for that tier, so the tier
   distinction is preserved without flooding the card grid with
   colour. */
.rec-action-card.tone-amber  { border-left-color: var(--color-text-faint); }
/* Green is kept saturated because it signals "good news, no action
   needed" rather than severity; a muted border would let it blend
   indistinguishably with the gray (low-priority) rows. */
.rec-action-card.tone-green  { border-left-color: var(--color-green); }
.rec-action-card.tone-gray   { border-left-color: var(--color-text-faint); }
.rec-action-head { display: flex; align-items: center; gap: 6px; margin-bottom: var(--space-2); }
.rec-action-priority {
  font-size: .68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--color-text-muted);
}
.rec-action-card.tone-red   .rec-action-priority { color: var(--color-red-fg); }
.rec-action-card.tone-amber .rec-action-priority { color: var(--color-amber-fg); }
.rec-action-card.tone-green .rec-action-priority { color: var(--color-green-fg); }
.rec-action-card.tone-gray  .rec-action-priority { color: var(--color-text-muted); }
.rec-action-title { font-size: .95rem; font-weight: 600; margin-bottom: 4px; }
.rec-action-detail { font-size: .85rem; line-height: 1.55; color: var(--color-text-muted); }

/* Rebalance list */
.rec-rebal-list { display: flex; flex-direction: column; gap: var(--space-2); }
.rec-rebal-row {
  display: grid;
  grid-template-columns: 90px minmax(0, 1fr) auto;
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}
.rec-rebal-action {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  font-weight: 600;
  font-size: .8rem;
  justify-self: start;
}
.rec-rebal-row.tone-green  .rec-rebal-action { background: var(--color-green-bg); color: var(--color-green-fg); }
.rec-rebal-row.tone-amber  .rec-rebal-action { background: var(--color-amber-bg); color: var(--color-amber-fg); }
.rec-rebal-row.tone-gray   .rec-rebal-action { background: var(--color-bg);       color: var(--color-text-muted); }
.rec-rebal-arrow { font-size: 1rem; line-height: 1; }
.rec-rebal-amount { text-align: right; }

.rec-growth-list {
  margin: 0;
  padding-left: var(--space-4);
  font-size: .9rem;
  line-height: 1.6;
}
.rec-growth-list li { margin-bottom: 6px; }
.rec-growth-list li::marker { color: var(--color-text-faint); }

/* Risk cards */
.rec-risk-list { display: flex; flex-direction: column; gap: var(--space-2); }
.rec-risk-card {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  border-left: 3px solid var(--color-border);
}
.rec-risk-card.tone-red   { border-left-color: var(--color-red); }
.rec-risk-card.tone-amber { border-left-color: var(--color-amber); }
.rec-risk-card.tone-gray  { border-left-color: var(--color-text-faint); }
.rec-risk-icon { flex-shrink: 0; margin-top: 2px; color: var(--color-text-muted); }
.rec-risk-card.tone-red   .rec-risk-icon { color: var(--color-red); }
.rec-risk-card.tone-amber .rec-risk-icon { color: var(--color-amber); }
.rec-risk-body { flex: 1; min-width: 0; }
.rec-risk-head { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 2px; flex-wrap: wrap; }
.rec-risk-head .text-xs { margin-left: auto; }
.rec-risk-sev {
  font-size: .66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--color-bg);
  color: var(--color-text-muted);
}
.rec-risk-card.tone-red   .rec-risk-sev { background: var(--color-red-bg);   color: var(--color-red-fg); }
.rec-risk-card.tone-amber .rec-risk-sev { background: var(--color-amber-bg); color: var(--color-amber-fg); }
.rec-risk-category { font-size: .72rem; color: var(--color-text-faint); text-transform: capitalize; }
.rec-risk-message { font-size: .88rem; line-height: 1.5; }
.rec-risk-top { padding: var(--space-4) var(--space-5); margin-bottom: var(--space-4); }

.rec-assumptions {
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.rec-assumptions-title {
  font-size: .66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--color-text-faint);
  margin-bottom: var(--space-2);
}
.rec-assumptions ul {
  margin: 0;
  padding-left: var(--space-4);
  font-size: .82rem;
  color: var(--color-text-muted);
  line-height: 1.6;
}

/* History timeline */
.rec-history { display: flex; flex-direction: column; margin: 0; padding: 0; }
.rec-history-row {
  display: grid;
  grid-template-columns: 12px minmax(0, 1fr) auto;
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border-light);
}
.rec-history-main { min-width: 0; }
.rec-history-action { white-space: nowrap; }
.rec-history-row:last-child { border-bottom: none; }
.rec-history-row.is-current { background: var(--color-primary-bg); padding-left: var(--space-3); padding-right: var(--space-3); border-radius: var(--radius-sm); border-bottom: none; }
.rec-history-marker {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--color-text-faint);
  justify-self: center;
}
.rec-history-marker.tone-green { background: var(--color-green); }
.rec-history-marker.tone-red   { background: var(--color-red); }
.rec-history-when { font-weight: 600; font-size: .88rem; }

/* History pager */
.rec-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border-light);
  flex-wrap: wrap;
}
.rec-pager-pages { display: flex; align-items: center; gap: 4px; flex: 1; justify-content: center; flex-wrap: wrap; }
.rec-pager-page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  height: 30px;
  padding: 0 8px;
  font-size: .8rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
  border-radius: var(--radius-sm);
}
a.rec-pager-page:hover { background: var(--color-bg); color: var(--color-text); }
.rec-pager-page.is-active { background: var(--color-primary); color: #fff; font-weight: 600; }
.rec-pager-gap { color: var(--color-text-faint); padding: 0 4px; }
.rec-pager-disabled { opacity: .4; pointer-events: none; }

[data-rec-history] { transition: opacity var(--t-fast) var(--ease); }
[data-rec-history].is-loading { opacity: .55; pointer-events: none; }

/* Generating overlay */
.rec-generating-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, .55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9000;
  animation: fadeIn .15s ease-out;
}
.rec-generating-overlay[hidden] { display: none; }
.rec-generating-card {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  background: #fff;
  padding: var(--space-5) var(--space-6);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  max-width: 420px;
}
.rec-generating-title { font-size: 1rem; font-weight: 600; margin-bottom: 4px; }
.rec-generating-body  { font-size: .85rem; color: var(--color-text-muted); line-height: 1.4; }

.rec-spinner {
  width: 28px; height: 28px;
  border-radius: 50%;
  border: 3px solid var(--color-border);
  border-top-color: var(--color-primary);
  animation: spin .8s linear infinite;
  flex-shrink: 0;
}
button[data-generate-btn] .rec-spinner-sm {
  display: inline-block;
  width: 12px; height: 12px;
  margin-right: 6px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .4);
  border-top-color: #fff;
  vertical-align: -2px;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

@media (max-width: 640px) {
  .rec-snapshot-row,
  .rec-rebal-row { grid-template-columns: 1fr; gap: var(--space-2); }
  .rec-snapshot-drift, .rec-rebal-amount { text-align: left; }
  .rec-bar-label { width: 70px; }
  .rec-summary-text { font-size: .9rem; }
  .rec-result { padding: var(--space-5) var(--space-4); }
  .rec-chapters-nav { padding: var(--space-2) var(--space-4); margin: 0 calc(-1 * var(--space-4)) var(--space-4); }
}

/* ═══ Chat ═════════════════════════════════════════════════════════════ */
/* The chat page is viewport-locked at every width and in every mode (with
   or without the demo banner): the page itself never scrolls — only the
   history sidebar and the message thread do. main-content is pinned to the
   viewport, and page-body/page-shell become flex columns so the shell (and
   the demo banner above it, when present) fill exactly the available
   height. No magic header-height number that can drift and leave a few
   stray pixels for the window to scroll. */
body:has(.chat-shell) .main-content {
  height: 100dvh;
  min-height: 0;
  overflow: hidden;
}
body:has(.chat-shell) .page-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  /* A small uniform gutter around the shell at all widths (replaces the
     old negative-margin full-bleed trick, which fought the flex sizing). */
  padding: var(--space-3);
  padding-left: max(var(--space-3), env(safe-area-inset-left, 0px));
  padding-right: max(var(--space-3), env(safe-area-inset-right, 0px));
  padding-bottom: max(var(--space-3), env(safe-area-inset-bottom, 0px));
}
body:has(.chat-shell) .page-shell {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.chat-shell {
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr);
  flex: 1;
  min-height: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
@media (max-width: 900px) {
  .chat-shell { grid-template-columns: 1fr; }
}

.chat-sidebar {
  border-right: 1px solid var(--color-border);
  background: var(--color-bg-elevated);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.chat-newchat-form {
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border-light);
}
.chat-newchat-form .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }

.chat-history {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.chat-history[hidden] { display: none; }
.chat-history-empty {
  padding: var(--space-4);
  color: var(--color-text-faint);
  font-size: .85rem;
  text-align: center;
}
.chat-history-row {
  position: relative;
  border-radius: var(--radius-sm);
  transition: background var(--t-fast) var(--ease);
}
.chat-history-row:hover { background: var(--color-bg); }
.chat-history-row.is-active {
  background: var(--color-surface);
  box-shadow: 0 0 0 1px var(--color-border);
}
.chat-history-link { display: block; padding: var(--space-3); padding-right: 32px; color: var(--color-text); }
.chat-history-title { display: flex; align-items: center; gap: 5px; font-size: .88rem; font-weight: 600; min-width: 0; }
.chat-history-pin-icon { color: var(--color-amber); flex-shrink: 0; }
.chat-history-title-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
.chat-history-title-input {
  font: inherit;
  font-weight: 600;
  width: 100%;
  padding: 2px 6px;
  border: 1px solid var(--color-primary);
  border-radius: 4px;
  outline: none;
}
.chat-history-preview {
  font-size: .75rem;
  color: var(--color-text-muted);
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chat-history-time {
  font-size: .68rem;
  color: var(--color-text-faint);
  margin-top: 4px;
}

.chat-history-menu { position: absolute; top: 6px; right: 6px; }
.chat-history-menu-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 6px;
  cursor: pointer;
  color: var(--color-text-muted);
  background: transparent;
  border: 0;
  list-style: none;
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.chat-history-menu-trigger::-webkit-details-marker { display: none; }
.chat-history-row:hover .chat-history-menu-trigger,
.chat-history-row.is-active .chat-history-menu-trigger,
.chat-history-menu[open] .chat-history-menu-trigger { opacity: 1; }
.chat-history-menu-trigger:hover { background: var(--color-bg); color: var(--color-text); }

.chat-history-menu-pop {
  position: absolute;
  top: 30px;
  right: 0;
  z-index: 10;
  min-width: 160px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
  padding: 4px;
  display: flex;
  flex-direction: column;
}
.chat-history-menu-form { margin: 0; }
.chat-history-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 10px;
  font-size: .82rem;
  font-family: inherit;
  text-align: left;
  background: transparent;
  border: 0;
  border-radius: 4px;
  color: var(--color-text);
  cursor: pointer;
}
.chat-history-menu-item:hover { background: var(--color-bg); }
.chat-history-menu-item.is-danger { color: var(--color-red-fg); }
.chat-history-menu-item.is-danger:hover { background: var(--color-red-bg); }
.chat-history-menu-item svg { color: var(--color-text-faint); }
.chat-history-menu-item.is-danger svg { color: var(--color-red); }

.chat-shell.is-sidebar-collapsed { grid-template-columns: 0 minmax(0, 1fr); }
.chat-shell.is-sidebar-collapsed .chat-sidebar { visibility: hidden; pointer-events: none; }
@media (max-width: 900px) {
  .chat-shell.is-sidebar-collapsed { grid-template-columns: 1fr; }
  .chat-shell.is-sidebar-collapsed .chat-sidebar { visibility: visible; }
  .chat-sidebar {
    position: absolute;
    z-index: 4;
    top: 0; left: 0;
    height: 100%;
    /* 280px is ~75% of a 375px phone — too wide for the drawer to
       feel like a panel and not "the whole page minus a sliver".
       min(85vw, 320px) scales with the device so the visible
       backdrop strip stays a real tap-out target on phones while
       capping the absolute width on tablets. */
    width: min(85vw, 320px);
    transform: translateX(-100%);
    transition: transform var(--t-base) var(--ease);
    box-shadow: var(--shadow-lg);
  }
  .chat-sidebar.is-open { transform: translateX(0); }

  /* Mobile drawer rows need real tap targets. Desktop's 12px padding
     gave a ~44px row in practice but the kebab menu trigger at 26x26
     was below iOS's 44px minimum and easy to miss with a thumb.
     Bump the link padding to put the row at ~56px and force the
     kebab trigger to 44x44 on touch. */
  .chat-history-link { padding: var(--space-4); padding-right: 52px; min-height: 56px; }
  /* Always-visible kebab on touch — hover-reveal doesn't fire without
     a mouse, so the desktop "opacity 0 until row hover" rule would
     leave the trigger invisible until the user already tapped into
     the row. Trigger sized to iOS's 44px minimum tap target. */
  .chat-history-menu-trigger { width: 44px; height: 44px; opacity: 1; }
  /* The desktop popover sits at top: 30px to hang below a 26x26
     trigger. With the trigger bumped to 44x44 the original offset
     lands inside the trigger; push the popover down so it clears
     the larger button cleanly. */
  .chat-history-menu-pop { top: 50px; }
}

.chat-history-search { padding: var(--space-2) var(--space-3) 0; }
.chat-history-search-input.form-control {
  height: 32px;
  font-size: .82rem;
  padding: 0 10px;
  background: var(--color-bg);
  border-radius: 8px;
}
.chat-history-snippet {
  font-size: .76rem;
  line-height: 1.35;
  color: var(--color-text-muted);
  margin-top: 4px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.chat-history-snippet mark,
.chat-history-title-text mark {
  background: var(--color-amber-bg, rgba(255, 191, 0, 0.22));
  color: inherit;
  padding: 0 1px;
  border-radius: 2px;
}

.chat-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.chat-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  border-bottom: 1px solid var(--color-border-light);
  background: var(--color-surface);
}
.chat-header-titles { flex: 1; min-width: 0; }
.chat-header-title {
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chat-header-sub {
  font-size: .72rem;
  color: var(--color-text-faint);
  margin-top: 1px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.chat-header-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--color-green); }
.chat-header-cost { font-variant-numeric: tabular-nums; color: var(--color-text-muted); cursor: help; }

.chat-thread {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  scroll-behavior: smooth;
}

.chat-msg { display: flex; flex-direction: column; max-width: min(720px, 88%); }
.chat-msg-user { align-self: flex-end; align-items: flex-end; }
.chat-msg-assistant { align-self: flex-start; align-items: flex-start; max-width: min(820px, 94%); }

.chat-msg-bubble {
  padding: 12px 16px;
  border-radius: 14px;
  line-height: 1.6;
  word-wrap: break-word;
  overflow-wrap: anywhere;
  font-size: .925rem;
}
.chat-msg-bubble > *:first-child { margin-top: 0; }
.chat-msg-bubble > *:last-child  { margin-bottom: 0; }
.chat-msg-bubble p { margin: 0; }
.chat-msg-bubble p + p,
.chat-msg-bubble ul + p,
.chat-msg-bubble ol + p,
.chat-msg-bubble p + ul,
.chat-msg-bubble p + ol,
.chat-msg-bubble pre + p,
.chat-msg-bubble p + pre,
.chat-msg-bubble blockquote + p,
.chat-msg-bubble p + blockquote,
.chat-msg-bubble p + .md-table-wrap,
.chat-msg-bubble .md-table-wrap + p,
.chat-msg-bubble blockquote { margin-top: 12px; }

.chat-msg-bubble h3,
.chat-msg-bubble h4,
.chat-msg-bubble h5 {
  font-weight: 700;
  letter-spacing: -.01em;
  margin-top: 14px;
  margin-bottom: 4px;
  line-height: 1.3;
}
.chat-msg-bubble h3 { font-size: 1.1rem; }
.chat-msg-bubble h4 { font-size: 1rem; }
.chat-msg-bubble h5 { font-size: .82rem; text-transform: uppercase; letter-spacing: .06em; color: var(--color-text-muted); }

.chat-msg-bubble ul,
.chat-msg-bubble ol { padding-left: 22px; margin: 0; }
.chat-msg-bubble li + li { margin-top: 4px; }
.chat-msg-bubble li::marker { color: var(--color-text-faint); }
.chat-msg-bubble strong { font-weight: 700; font-variant-numeric: tabular-nums; }

.chat-msg-bubble code {
  background: var(--color-bg);
  padding: 1px 5px;
  border-radius: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85em;
}
.chat-msg-bubble pre {
  background: var(--sidebar-bg);
  color: #e2e8f0;
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  font-size: .82rem;
  margin: 0;
}
.chat-msg-bubble pre code { background: transparent; padding: 0; color: inherit; }
.chat-msg-bubble a { color: var(--color-primary-hover); text-decoration: underline; }

.chat-msg-bubble blockquote {
  margin: 0;
  padding: 8px 12px;
  border-left: 3px solid var(--color-border);
  background: var(--color-bg-elevated);
  border-radius: 0 6px 6px 0;
  font-size: .9rem;
}

.chat-msg-bubble .md-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--color-border);
  border-radius: 8px;
}
.chat-msg-bubble table { width: 100%; border-collapse: collapse; font-size: .85rem; font-variant-numeric: tabular-nums; }
.chat-msg-bubble th,
.chat-msg-bubble td { padding: 7px 10px; border-bottom: 1px solid var(--color-border-light); text-align: left; vertical-align: top; }
.chat-msg-bubble th {
  font-weight: 600;
  background: var(--color-bg);
  color: var(--color-text-muted);
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .04em;
}
.chat-msg-bubble tr:last-child td { border-bottom: 0; }

/* User bubble: indigo. Assistant bubble: plain surface with hairline. */
.chat-msg-user .chat-msg-bubble { background: var(--color-primary); color: #fff; }
.chat-msg-user .chat-msg-bubble a { color: #fff; }
.chat-msg-user .chat-msg-bubble code { background: rgba(255, 255, 255, .15); color: #fff; }
.chat-msg-user .chat-msg-bubble blockquote {
  border-left-color: rgba(255, 255, 255, .6);
  background: rgba(255, 255, 255, .08);
  color: #fff;
}
.chat-msg-user .chat-msg-bubble th { background: rgba(255, 255, 255, .12); color: #fff; }
.chat-msg-user .chat-msg-bubble th,
.chat-msg-user .chat-msg-bubble td { border-color: rgba(255, 255, 255, .18); }
.chat-msg-user .chat-msg-bubble .md-table-wrap { border-color: rgba(255, 255, 255, .25); }

.chat-msg-assistant .chat-msg-bubble {
  background: var(--color-bg-elevated);
  color: var(--color-text);
  border: 1px solid var(--color-border-light);
}

.chat-msg-foot { display: flex; align-items: center; gap: var(--space-3); margin-top: 4px; padding: 0 4px; }
.chat-msg-meta { font-size: .68rem; color: var(--color-text-faint); }
.chat-msg-cost { font-variant-numeric: tabular-nums; color: var(--color-text-faint); }
.chat-msg-cost-dot { margin: 0 2px; opacity: .75; }

.chat-msg-actions { display: flex; gap: 6px; opacity: 0; transition: opacity var(--t-fast) var(--ease); }
.chat-msg:hover .chat-msg-actions,
.chat-msg:focus-within .chat-msg-actions,
/* Error replies always reveal their actions — the Try again button is
   the primary affordance, and waiting on hover would hide it behind a
   gesture the user might not try. */
.chat-msg-error .chat-msg-actions { opacity: 1; }
@media (max-width: 900px) {
  .chat-msg-actions { opacity: 1; gap: var(--space-2); }
  .chat-msg-action {
    padding: 8px 12px;
    min-height: 40px;
    font-size: .8rem;
  }
}
.chat-msg-action {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  font-size: .72rem;
  font-weight: 500;
  font-family: inherit;
  color: var(--color-text-muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.chat-msg-action:hover { background: var(--color-bg); color: var(--color-text); border-color: var(--color-border-light); }
.chat-msg-action:disabled { opacity: .55; cursor: progress; }
.chat-msg-action.is-success {
  color: var(--color-green-fg);
  border-color: var(--color-green-border);
  background: var(--color-green-bg);
}
.chat-msg-action svg { color: inherit; }

.chat-msg-thinking { display: none; }
.chat-msg-thinking.is-active { display: flex; }

.chat-typing { display: inline-flex; gap: 4px; }
.chat-typing span {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--color-text-faint);
  animation: chatTyping 1.2s infinite ease-in-out;
}
.chat-typing span:nth-child(2) { animation-delay: .15s; }
.chat-typing span:nth-child(3) { animation-delay: .3s; }
@keyframes chatTyping {
  0%, 80%, 100% { transform: scale(.7); opacity: .4; }
  40%           { transform: scale(1);  opacity: 1;  }
}

.chat-empty {
  margin: auto;
  text-align: center;
  max-width: 520px;
  padding: var(--space-8) var(--space-4);
}
.chat-empty-icon {
  width: 52px; height: 52px;
  border-radius: 16px;
  background: var(--color-primary-bg);
  color: var(--color-primary);
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto var(--space-4);
}
.chat-empty-title { font-size: 1.2rem; font-weight: 700; letter-spacing: -.01em; margin-bottom: 6px; }
.chat-empty-sub { color: var(--color-text-muted); font-size: .9rem; line-height: 1.55; margin-bottom: var(--space-6); }
.chat-quick-prompts { display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: center; }
.chat-quick-prompt {
  padding: 7px 12px;
  font-size: .8rem;
  font-weight: 500;
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.chat-quick-prompt:hover { background: var(--color-bg); border-color: #cbd5e1; }
.chat-quick-prompt-sm { padding: 4px 10px; font-size: .76rem; }

.chat-composer {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--color-border-light);
  background: var(--color-surface);
}
.chat-composer-input-wrap { position: relative; flex: 1; display: flex; }
.chat-composer-input.form-control {
  resize: none;
  min-height: 44px;
  padding: 11px 14px;
  font-size: .925rem;
  line-height: 1.45;
  flex: 1;
  /* Hidden by default so a growing single/few-line input never shows a
     scrollbar; the autosize JS flips this to auto only once the textarea
     reaches its height cap. */
  overflow-y: hidden;
}
.chat-composer-counter {
  position: absolute;
  right: 8px;
  bottom: 4px;
  font-size: .68rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-faint);
  background: var(--color-surface);
  padding: 0 4px;
  border-radius: 4px;
  pointer-events: none;
  user-select: none;
  /* Counter stays out of sight until the user crosses ~80% of the
     character cap (JS adds .is-visible). Short messages shouldn't be
     visually accompanied by an N/8000 chip — the cap matters mostly
     when approaching it. aria-live on the wrapper still announces the
     value to assistive tech regardless of opacity. */
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease);
}
.chat-composer-counter.is-visible { opacity: 1; }
.chat-composer-counter.is-warning { color: var(--color-amber-fg); }
.chat-composer-counter.is-danger  { color: var(--color-red-fg); font-weight: 600; }
.chat-composer-send {
  padding: 0 14px;
  height: 44px;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: opacity var(--t-fast) var(--ease), filter var(--t-fast) var(--ease);
}
/* Visual affordance: when the textarea is empty (placeholder still
   visible) the send button reads as "not yet ready" — dimmed +
   desaturated. As soon as the user types a character the placeholder
   vanishes, this rule no longer matches, and the button goes back
   to full primary-blue, giving the "tap me to send" cue ChatGPT
   surfaces. Pure CSS via :has(), no JS state to manage. */
.chat-composer:has(.chat-composer-input:placeholder-shown) .chat-composer-send:not(.is-sending) {
  opacity: 0.45;
  filter: saturate(0.5);
}
.chat-composer-send .sr-only {
  position: absolute;
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

.chat-msg-tools,
.chat-msg-request { margin-top: 4px; font-size: .72rem; color: var(--color-text-muted); }
.chat-msg-tools-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 6px;
  list-style: none;
  user-select: none;
  transition: background var(--t-fast) var(--ease);
}
.chat-msg-tools-summary::-webkit-details-marker { display: none; }
.chat-msg-tools-summary:hover { background: var(--color-bg); }
.chat-msg-tools-summary > svg { color: var(--color-text-faint); flex-shrink: 0; }
.chat-msg-tool-chip {
  display: inline-block;
  padding: 1px 7px;
  font-size: .7rem;
  background: var(--color-bg);
  border: 1px solid var(--color-border-light);
  border-radius: 999px;
  color: var(--color-text);
}
.chat-msg-tools[open] .chat-msg-tools-summary,
.chat-msg-request[open] .chat-msg-tools-summary { background: var(--color-bg); }
.chat-msg-tools-body {
  margin-top: 6px;
  padding: 8px 10px;
  background: var(--color-bg);
  border: 1px solid var(--color-border-light);
  border-radius: 8px;
}
.chat-msg-tool-block + .chat-msg-tool-block { margin-top: 8px; }
.chat-msg-tool-name { font-weight: 600; font-size: .72rem; margin-bottom: 4px; }
.chat-msg-tool-json {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .72rem;
  line-height: 1.45;
  margin: 0;
  padding: 8px 10px;
  background: var(--color-surface);
  border: 1px solid var(--color-border-light);
  border-radius: 6px;
  white-space: pre;
  overflow-x: auto;
  max-height: 320px;
}

.chat-msg-followups {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px dashed var(--color-border-light);
}
.chat-msg-followups-label {
  font-size: .68rem;
  color: var(--color-text-faint);
  letter-spacing: .03em;
  text-transform: uppercase;
  margin-right: 2px;
}

.chat-msg-error .chat-msg-bubble {
  background: var(--color-red-bg);
  border: 1px solid var(--color-red-border);
  color: var(--color-red-fg);
}

/* Mobile composer + thread tuning. The big one is font-size: 16px on the
   textarea — anything smaller triggers iOS Safari's focus-zoom, which is
   the usual cause of "hard to type" on a phone. The rest is breathing
   room: a taller input, a proper round 48×48 send button, tighter thread
   padding so more vertical space goes to the conversation, and a header
   that doesn't crowd the title on a narrow screen. */
@media (max-width: 900px) {
  .chat-header { padding: var(--space-2) var(--space-3); gap: var(--space-2); }
  .chat-header-title { font-size: .95rem; }
  /* Strip the "Grounded in live data . model . cost" sub-line from the
     mobile header. On a 375px phone it crowds the conversation title
     out of view, and ChatGPT mobile sets the precedent: the header is
     just the title + drawer toggle. The cost / model figures are still
     reachable on desktop where there's room. */
  .chat-header-sub { display: none; }

  .chat-thread {
    padding: var(--space-3);
    gap: var(--space-3);
  }
  .chat-msg { max-width: 94%; }
  .chat-msg-assistant { max-width: 100%; }

  .chat-empty { padding: var(--space-6) var(--space-3); }

  /* Empty-state quick prompts as a 2-column grid on phones. The
     wrap-flow row that works on desktop collapses on a 375px screen
     into a ragged list where the rightmost chip is half-clipped and
     the visual rhythm is broken. A 2x2 grid feels finished and
     matches the ChatGPT mobile suggestion-tiles pattern. Each tile
     also gets a 56px min-height so it's a comfortable tap target
     without inventing new styling. */
  .chat-quick-prompts {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-2);
    width: 100%;
  }
  .chat-quick-prompt {
    min-height: 56px;
    justify-content: center;
    text-align: center;
    white-space: normal;
    line-height: 1.3;
  }

  /* Tool-call chip summary on the assistant bubble gets a real tap
     target. 4px padding was fine for hover-to-expand on desktop;
     on touch it's barely 24px tall and easy to miss. */
  .chat-msg-tools-summary,
  .chat-msg-request > summary { padding: 10px 12px; }

  .chat-composer {
    padding: var(--space-2) var(--space-2) calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
    gap: var(--space-2);
  }
  .chat-composer-input.form-control {
    font-size: 16px;
    min-height: 48px;
    padding: 12px 14px;
    line-height: 1.4;
    border-radius: 12px;
  }
  .chat-composer-send {
    width: 48px;
    height: 48px;
    padding: 0;
    border-radius: 50%;
    justify-content: center;
  }
  .chat-composer-send svg { width: 18px; height: 18px; }

  /* The counter sits at the textarea's bottom-right; on a narrow screen
     typed text wraps right into it. Hide it on mobile until the user is
     within ~10% of the cap, at which point the warning/danger class makes
     it reappear and the brief overlap is acceptable feedback. */
  .chat-composer-counter { display: none; }
  .chat-composer-counter.is-warning,
  .chat-composer-counter.is-danger { display: inline-block; }
}

/* ═══ Touch-friendly nav (coarse pointers) ═════════════════════════════ */
@media (pointer: coarse) {
  .sidebar .nav-item { min-height: 44px; }
  /* Row actions (Edit/Deactivate) and ghost icon buttons need real tap
     targets on touch devices, not the dense 32–34px sizing used at the
     desktop hover density. */
  .row-actions .btn,
  .row-actions .btn-sm,
  .row-actions .btn-ghost { min-height: 44px; }
  /* Icon-only buttons need width AND height bumped to the tap target
     so they stay square on touch devices. */
  .row-actions .btn-icon { width: 44px; min-width: 44px; height: 44px; }
}

/* ═══ Responsive overrides ═════════════════════════════════════════════ */
@media (max-width: 900px) {
  .sidebar { transform: translateX(-100%); }
  .sidebar.open { transform: translateX(0); }
  .main-content { margin-left: 0; }
  .grid-3, .grid-4 { grid-template-columns: 1fr 1fr; }
  .grid-2 { grid-template-columns: 1fr; }
  /* minmax(0, 1fr), not 1fr: a lone column still can't shrink below a
     date input's intrinsic width, so it would overflow on mobile. */
  .form-row-2, .form-row-3 { grid-template-columns: minmax(0, 1fr); }
  /* Inputs at 16px+ stop iOS Safari from zooming the page on focus. */
  .form-control { font-size: 16px; min-height: 44px; }
  .page-body {
    padding: var(--space-5);
    padding-bottom: calc(var(--space-10) + env(safe-area-inset-bottom, 0px));
    padding-left: max(var(--space-5), env(safe-area-inset-left, 0px));
    padding-right: max(var(--space-5), env(safe-area-inset-right, 0px));
  }
  .page-header {
    padding: var(--space-4) var(--space-5);
    padding-left: max(var(--space-5), env(safe-area-inset-left, 0px));
    padding-right: max(var(--space-5), env(safe-area-inset-right, 0px));
  }
  body.sidebar-open { overflow: hidden; }

  .page-header-actions .btn-sm { padding: var(--space-2) var(--space-4); min-height: 40px; }
}

@media (max-width: 600px) {
  .grid-3, .grid-4 { grid-template-columns: 1fr; }
  .page-header { padding: var(--space-3) var(--space-4); gap: var(--space-2); }
  /* Bigger hamburger hit target on mobile (48×48 vs the base 44×44).
     The iOS Safari edge-swipe-back gesture is OS-controlled and not
     preventable from web code, so pushing the button further from the
     edge has limited effect. A slightly larger button makes it easier
     to land the tap in the centre — away from both the screen edge
     and the button's own edge where edge-back triggers most often. */
  .hamburger { width: 48px; height: 48px; }
  .page-body {
    padding: var(--space-4);
    padding-bottom: calc(var(--space-10) + env(safe-area-inset-bottom, 0px));
    padding-left: max(var(--space-4), env(safe-area-inset-left, 0px));
    padding-right: max(var(--space-4), env(safe-area-inset-right, 0px));
  }
  .dash-hero { border-radius: var(--radius-lg); }
  .dash-hero-inner { padding: var(--space-5); gap: var(--space-5); }
  .hero-card { padding: var(--space-5); }
  .dash-hero-top { flex-direction: column; align-items: stretch; gap: var(--space-5); }
  .dash-hero-main { flex-basis: auto; }
  /* Milestone visual is already hidden below 960px (see desktop rule)
     so no special mobile handling needed here — the actions stack
     directly under the main column. */
  .dash-hero-actions { width: 100%; flex-direction: column; align-self: stretch; gap: var(--space-3); }
  .dash-hero-actions .btn { width: 100%; min-height: 48px; font-size: .9375rem; border-radius: var(--radius); }
  .page-title {
    white-space: normal;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
  }
  .page-subtitle {
    white-space: normal;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    text-overflow: ellipsis;
  }

  /* Convert wide data tables to card-list layout on phones. Templates mark
     the primary cell with .col-primary, action cells with .col-actions, and
     each <td data-label="..."> for the floating label. */
  .accounts-table, .accounts-table thead, .accounts-table tbody, .accounts-table tr, .accounts-table th, .accounts-table td,
  .loans-table,    .loans-table    thead, .loans-table    tbody, .loans-table    tr, .loans-table    th, .loans-table    td,
  .users-table,    .users-table    thead, .users-table    tbody, .users-table    tr, .users-table    th, .users-table    td,
  .trends-yearly-table, .trends-yearly-table thead, .trends-yearly-table tbody, .trends-yearly-table tr, .trends-yearly-table th, .trends-yearly-table td {
    display: block;
    border: none;
    white-space: normal;
    min-width: 0;
  }
  .accounts-table thead, .loans-table thead, .users-table thead, .trends-yearly-table thead { display: none; }
  .accounts-table, .loans-table, .users-table, .trends-yearly-table { width: 100%; }
  .table-wrap:has(.accounts-table),
  .table-wrap:has(.loans-table),
  .table-wrap:has(.users-table),
  .table-wrap:has(.trends-yearly-table) {
    border: none;
    background: transparent;
    overflow: visible;
  }
  .accounts-table tr,
  .loans-table tr,
  .users-table tr,
  .trends-yearly-table tr {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-4) var(--space-5);
    margin-bottom: var(--space-3);
    box-shadow: var(--shadow-sm);
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-1) var(--space-3);
  }
  .accounts-table td,
  .loans-table td,
  .users-table td,
  .trends-yearly-table td {
    padding: 5px 0;
    font-size: .875rem;
    text-align: right;
  }
  .accounts-table td::before,
  .loans-table td::before,
  .users-table td::before,
  .trends-yearly-table td::before {
    content: attr(data-label);
    text-transform: uppercase;
    font-size: .68rem;
    font-weight: 600;
    color: var(--color-text-muted);
    letter-spacing: .06em;
    float: left;
    margin-right: var(--space-3);
    line-height: 1.5;
    padding-top: 1px;
  }
  .accounts-table td.col-primary,
  .loans-table td.col-primary,
  .users-table td.col-primary,
  .trends-yearly-table td.col-primary {
    grid-column: 1 / -1;
    text-align: left;
    border-bottom: 1px solid var(--color-border-light);
    margin-bottom: var(--space-2);
    padding-bottom: var(--space-3);
  }
  .accounts-table td.col-primary::before,
  .loans-table td.col-primary::before,
  .users-table td.col-primary::before,
  .trends-yearly-table td.col-primary::before { content: none; }
  .accounts-table td.col-actions,
  .loans-table td.col-actions,
  .users-table td.col-actions {
    grid-column: 1 / -1;
    text-align: left;
    border-top: 1px solid var(--color-border-light);
    margin-top: var(--space-2);
    padding-top: var(--space-3);
  }
  .accounts-table td.col-actions::before,
  .loans-table td.col-actions::before,
  .users-table td.col-actions::before { content: none; }
  .accounts-table tbody tr:hover,
  .loans-table tbody tr:hover,
  .users-table tbody tr:hover,
  .trends-yearly-table tbody tr:hover { background-color: var(--color-surface); }

  /* Loan card, phone layout: restore the progress bar, lead with Remaining as
     a headline, fold away the exact Repaid amount (the bar carries progress),
     and keep the multi-line Remaining/Schedule cells left-aligned label-above
     so long figures stop colliding with the floated label. */
  .loans-table .loan-mbar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-2);
  }
  .loan-mbar-track {
    flex: 1 1 auto;
    height: 6px;
    border-radius: 999px;
    background: var(--color-border-light);
    overflow: hidden;
  }
  .loan-mbar-fill {
    display: block;
    height: 100%;
    width: var(--loan-progress-pct, 0%);
    background: var(--loan-progress-color, var(--color-primary));
    border-radius: 999px;
  }
  .loan-mbar-pct {
    flex: 0 0 auto;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    white-space: nowrap;
  }
  .loans-table td.col-repaid { display: none; }
  .loans-table td.col-remaining,
  .loans-table td.col-schedule {
    grid-column: 1 / -1;
    text-align: left;
  }
  .loans-table td.col-remaining::before,
  .loans-table td.col-schedule::before {
    float: none;
    display: block;
    margin: 0 0 2px;
  }
  .loans-table td.col-remaining { font-size: 1.05rem; font-weight: 600; }

  /* /admin/cron table: same card-stack pattern as accounts/loans, but the
     Schedule cell holds a full form (input + toggle + save) so it gets a
     wider treatment than the standard data-label-on-the-left layout. */
  .cron-table, .cron-table thead, .cron-table tbody, .cron-table tr, .cron-table th, .cron-table td {
    display: block;
    border: none;
    white-space: normal;
    min-width: 0;
  }
  .cron-table thead { display: none; }
  .cron-table { width: 100%; }
  .table-wrap:has(.cron-table) { border: none; background: transparent; overflow: visible; }
  .cron-table tr {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-4) var(--space-5);
    margin-bottom: var(--space-3);
    box-shadow: var(--shadow-sm);
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-1) var(--space-3);
  }
  .cron-table td { padding: 5px 0; font-size: .875rem; text-align: right; }
  .cron-table td::before {
    content: attr(data-label);
    text-transform: uppercase;
    font-size: .68rem;
    font-weight: 600;
    color: var(--color-text-muted);
    letter-spacing: .06em;
    float: left;
    margin-right: var(--space-3);
    line-height: 1.5;
    padding-top: 1px;
  }
  .cron-table td.col-primary {
    grid-column: 1 / -1;
    text-align: left;
    border-bottom: 1px solid var(--color-border-light);
    margin-bottom: var(--space-2);
    padding-bottom: var(--space-3);
  }
  .cron-table td.col-primary::before { content: none; }
  /* Schedule cell hosts the schedule input + Enabled + Save form. Span the
     row so the input gets full width; suppress the data-label because the
     form has its own visible "Cron expression" label. */
  .cron-table td.col-form {
    grid-column: 1 / -1;
    text-align: left;
    padding: var(--space-2) 0;
  }
  .cron-table td.col-form::before { content: none; }
  .cron-table td.col-actions {
    grid-column: 1 / -1;
    text-align: left;
    border-top: 1px solid var(--color-border-light);
    margin-top: var(--space-2);
    padding-top: var(--space-3);
  }
  .cron-table td.col-actions::before { content: none; }
  .cron-table tbody tr:hover { background: var(--color-surface); }

  .login-card { padding: var(--space-6); }
}

/* Recommendation overhaul additions ─────────────────────────────────── */

.form-hint {
  display: block;
  margin-top: 4px;
  color: var(--color-text-muted);
  font-size: 0.78rem;
  line-height: 1.4;
}

.rec-profile-section {
  border: none;
  padding: 0;
  margin: 0 0 var(--space-5) 0;
}
.rec-profile-section:last-of-type { margin-bottom: var(--space-3); }
.rec-profile-legend {
  display: block;
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  margin: 0 0 var(--space-4) 0;
  padding: 0 0 var(--space-3) 0;
  border-bottom: 1px solid var(--color-border-light);
  line-height: 1.4;
}

.rec-profile-derived {
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-alt, var(--color-surface));
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-md);
}
.rec-profile-derived-title {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--color-text);
  margin-bottom: var(--space-2);
}
.rec-profile-derived-list {
  list-style: disc;
  padding-left: 1.1rem;
  margin: 0;
  color: var(--color-text-muted);
  font-size: 0.82rem;
  line-height: 1.55;
}
.rec-profile-derived-list li + li { margin-top: 4px; }

.rec-scenario-chips {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin: 6px 0 var(--space-2) 0;
}
.rec-scenario-chip {
  font-size: 0.78rem;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  cursor: pointer;
  font-family: inherit;
}
.rec-scenario-chip:hover {
  border-color: var(--color-text);
  background: var(--color-surface);
}
.rec-scenario-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-3);
  margin-top: 4px;
}
.rec-scenario-counter {
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--color-text-muted);
}

.rec-preflight-hint {
  padding: var(--space-2) var(--space-3);
  border-left: 3px solid var(--color-amber-border);
  background: var(--color-surface);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

.rec-targets-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.rec-target-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.rec-target-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.rec-target-name-block {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  min-width: 0;
}
.rec-target-name { font-weight: 600; font-size: 0.95rem; }
.rec-target-delta-block {
  text-align: right;
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: flex-end;
}
.rec-target-delta {
  font-variant-numeric: tabular-nums;
  font-size: 0.88rem;
}
.rec-target-delta-num { color: var(--color-text-muted); margin-left: 4px; }
.rec-target-delta.tone-green   { color: var(--color-green-fg); }
.rec-target-delta.tone-amber   { color: var(--color-amber-fg); }
.rec-target-delta.tone-neutral { color: var(--color-text-muted); }
.rec-target-impact {
  font-size: 0.88rem;
  color: var(--color-text);
  line-height: 1.5;
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg);
  border-left: 3px solid var(--color-border);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.rec-target-evidence {
  list-style: disc;
  padding-left: 1.1rem;
  margin: 0;
  font-size: 0.82rem;
  color: var(--color-text-muted);
  line-height: 1.55;
}
.rec-target-evidence li + li { margin-top: 3px; }
.rec-target-rationale {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  line-height: 1.5;
}
.rec-target-apply { margin: 0; }
.rec-target-applied {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 5px 12px;
  border-radius: 999px;
  color: var(--color-green-fg);
  background: var(--color-green-bg);
  border: 1px solid var(--color-green-border);
  cursor: default;
}
.rec-target-applied svg { flex-shrink: 0; }

.rec-stale-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-amber-border);
  background: var(--color-amber-bg);
  color: var(--color-amber-fg);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-3);
}
.rec-stale-icon { flex-shrink: 0; margin-top: 2px; color: var(--color-amber-fg); }
.rec-stale-body { flex: 1 1 auto; min-width: 0; }
.rec-stale-title {
  font-weight: 600;
  font-size: 0.92rem;
  margin-bottom: 2px;
}
.rec-stale-reasons {
  list-style: disc;
  padding-left: 1.1rem;
  margin: 0;
  font-size: 0.85rem;
  line-height: 1.5;
}
.rec-stale-reasons li + li { margin-top: 2px; }
.rec-stale-cta { flex-shrink: 0; align-self: center; }
.rec-stale-cta-form { flex-shrink: 0; align-self: center; margin: 0; }
@media (max-width: 560px) {
  .rec-stale-banner { flex-direction: column; }
  .rec-stale-cta-form { align-self: stretch; }
  .rec-stale-cta { width: 100%; }
}

/* Thin-profile banner: same shape as the stale banner but in the
   neutral/info palette rather than amber, so a thin profile doesn't
   read as "your recommendation is broken" — it reads as "fill these
   and the next run will be sharper". */
.rec-thin-profile-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  background: var(--color-bg-elevated);
  color: var(--color-text);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-3);
}
/* Muted icon, not indigo — on a page that explicitly mentions "AI"
   repeatedly, a brand-color accent reads as "AI product" branding.
   The banner's left-aligned title already draws the eye; the icon's
   job is to mark the role (status/info), not to add brand weight. */
.rec-thin-profile-icon { flex-shrink: 0; margin-top: 2px; color: var(--color-text-muted); }
.rec-thin-profile-body { flex: 1 1 auto; min-width: 0; }
.rec-thin-profile-title {
  font-weight: 600;
  font-size: 0.92rem;
  margin-bottom: 2px;
}
.rec-thin-profile-sub {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  line-height: 1.5;
}
.rec-thin-profile-cta { flex-shrink: 0; align-self: center; }
@media (max-width: 560px) {
  .rec-thin-profile-banner { flex-direction: column; }
  .rec-thin-profile-cta { align-self: stretch; text-align: center; }
}

/* Sum-of-targets warning slot, populated by JS on each AJAX apply.
   Self-contained styling (NOT piggybacking on .alert / .alert-warning)
   so the empty slot doesn't render an empty yellow box on page load:
   the `hidden` attribute is respected because no other rule forces a
   display value. JS clears `hidden` when there's a message to surface,
   and resets it when the warning clears. */
.rec-targets-warning {
  display: block;
  padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-3);
  border: 1px solid var(--color-amber-border);
  background: var(--color-amber-bg);
  color: var(--color-amber-fg);
  border-radius: var(--radius-md);
  font-size: 0.88rem;
  line-height: 1.45;
}
.rec-targets-warning[hidden] { display: none; }

/* Mobile floating Generate button. Hidden on desktop because the
   Ask-next panel is sticky in the right rail and always within reach;
   shown on narrow viewports where the panel collapses below the main
   column and would otherwise require a long scroll to reach. */
.rec-fab-form { display: none; }
.rec-fab {
  position: fixed;
  right: var(--space-4);
  bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--color-primary);
  color: #fff;
  border: 0;
  cursor: pointer;
  display: grid;
  place-items: center;
  box-shadow: 0 8px 24px -8px rgba(15, 23, 42, 0.45),
              0 2px 6px rgba(15, 23, 42, 0.10);
  transition: transform var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
  z-index: 50;
}
.rec-fab:hover  { background: var(--color-primary-fg, var(--color-primary)); }
.rec-fab:active { transform: scale(0.96); }
.rec-fab:focus-visible {
  outline: 3px solid rgba(99, 102, 241, .45);
  outline-offset: 2px;
}
@media (max-width: 900px) {
  .rec-fab-form { display: block; }
}

.rec-target-empty {
  padding: var(--space-4) var(--space-5);
  background: var(--color-bg);
  border: 1px dashed var(--color-border);
  border-radius: var(--radius-md);
}
.rec-target-empty-title {
  font-weight: 600;
  font-size: 0.95rem;
  margin-bottom: 4px;
  color: var(--color-text);
}
.rec-target-empty-body { line-height: 1.55; }

/* Transparency footer at the bottom of the Targets tab. Tells the user
   which accounts the AI considered but didn't flag, and which were
   excluded by the eligibility filter and why. Collapsed by default so
   it doesn't compete with the actionable suggestions above. */
.rec-target-transparency {
  margin-top: var(--space-4);
  border-top: 1px solid var(--color-border-light);
  padding-top: var(--space-3);
}
.rec-target-transparency > summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  cursor: pointer;
  list-style: none;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--color-text-muted);
  padding: var(--space-2) 0;
}
.rec-target-transparency > summary::-webkit-details-marker { display: none; }
.rec-target-transparency-toggle { transition: transform .15s ease; }
.rec-target-transparency[open] > summary .rec-target-transparency-toggle { transform: rotate(45deg); }
.rec-target-transparency-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding-top: var(--space-2);
}
.rec-target-transparency-group { display: flex; flex-direction: column; gap: 6px; }
.rec-target-transparency-title {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--color-text);
}
.rec-target-transparency-sub  { line-height: 1.5; }
.rec-target-transparency-chips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.rec-target-transparency-chip {
  font-size: 0.74rem;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  white-space: nowrap;
}
.rec-target-transparency-chip-muted {
  color: var(--color-text-muted);
  background: var(--color-bg);
}
.rec-target-transparency-reason {
  margin-left: 6px;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}

@media (max-width: 600px) {
  .rec-target-delta-block { align-items: flex-start; text-align: left; }
}

/* ════════════════════════════════════════════════════════════════════
   Recommendation page UI overhaul: slim hero, 2-col layout with sticky
   rail, tabbed result chapters, history dropdown, mobile reflow.
   ════════════════════════════════════════════════════════════════════ */

/* ── Page wrapper: tighter outer padding so the rail has breathing room */
.rec-page { display: flex; flex-direction: column; gap: var(--space-5); }

/* ── Slim hero: single horizontal row on desktop, stack on mobile ─────── */
.rec-hero-slim {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: var(--space-3) var(--space-5);
  padding: var(--space-4) var(--space-5);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  /* No box-shadow: financial reports tend to read flat (border carries
     information, shadow is decorative noise). Removing it pulls the
     hero closer to "section header" and away from "elevated card". */
}
.rec-hero-value-wrap { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rec-hero-eyebrow {
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.rec-hero-amount {
  font-size: 1.55rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  color: var(--color-text);
}
.rec-hero-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 0.82rem;
  color: var(--color-text-muted);
  min-width: 0;
}
.rec-hero-dot, .rec-meta-dot { color: var(--color-border); }
.rec-hero-pills { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.rec-pill {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-muted);
  white-space: nowrap;
}
.rec-pill-green   { border-color: var(--color-green-border); color: var(--color-green-fg); }
.rec-pill-amber   { border-color: var(--color-amber-border); color: var(--color-amber-fg); }
.rec-pill-red     { border-color: var(--color-red-border);   color: var(--color-red-fg); }
.rec-pill-neutral { color: var(--color-text-muted); }
.rec-hero-cta { white-space: nowrap; }

/* ── Main 2-col layout: result on left, sticky rail on right ──────────── */
.rec-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 320px;
  gap: var(--space-5);
  /* No align-items: start. The rail's .rec-ask-next is position:sticky;
     for it to actually stick as the user scrolls the long result on the
     left, its containing block (the rail grid cell) must extend down to
     match the result's height. align-items: start was capping the rail
     cell at the rail's natural content height, so the sticky panel had
     nowhere to stick once the user scrolled past the cards. Default
     stretch lets the cell match the row's height, giving sticky the
     room it needs. */
}
.rec-main { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; }
.rec-rail { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }

/* ── Result card: header + history button + tabs + panels ─────────────── */
.rec-result-card { padding: var(--space-5) var(--space-6); }
.rec-result-header { margin-bottom: var(--space-4); }
.rec-result-title-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}
.rec-result-title {
  font-size: 1.1rem;
  font-weight: 600;
  margin: 0;
  color: var(--color-text);
}
.rec-result-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 0.78rem;
  color: var(--color-text-muted);
}

/* History dropdown trigger + lazy-loaded panel */
.rec-history-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.78rem;
  font-weight: 500;
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  cursor: pointer;
  font-family: inherit;
}
.rec-history-btn:hover { border-color: var(--color-text-muted); }
.rec-history-btn[aria-expanded="true"] .rec-caret { transform: rotate(180deg); }
.rec-caret { transition: transform .15s ease; display: inline-block; }
.rec-history-count {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  background: var(--color-border-light);
  color: var(--color-text-muted);
  border-radius: 999px;
  padding: 0 6px;
  font-size: 0.72rem;
}
.rec-history-panel {
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}
.rec-history-panel.is-loading { opacity: 0.6; pointer-events: none; }
.rec-history-loading { padding: var(--space-2) 0; }

/* ── Tabs: replaces the old anchored chapter nav. One panel visible at
   a time, focus-ring + ARIA-driven. Horizontal scroll on small screens. */
/* Underline tabs (newspaper style) rather than a segmented pill
   control. The pill treatment reads as "product UI"; an underline-
   on-active reads as a section break in a written report. The parent
   carries the baseline border; each tab's bottom-border sits flush
   with it and changes colour on the active state. */
.rec-tabs {
  display: flex;
  gap: var(--space-5);
  border-bottom: 1px solid var(--color-border);
  margin-bottom: var(--space-4);
  overflow-x: auto;
  /* Pin the cross axis: the tabs' -1px baseline overlap makes content a
     hair taller than the box, and `overflow-x: auto` alone would let the
     other axis compute to `auto` too, painting a phantom vertical scrollbar
     on wide screens. Horizontal scroll on narrow screens is preserved. */
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
}
.rec-tab {
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 8px 0;
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--color-text-muted);
  cursor: pointer;
  font-family: inherit;
  text-align: left;
  white-space: nowrap;
  /* Overlap the parent's 1px bottom-border so the active tab's 2px
     underline sits cleanly on top of the baseline instead of below it. */
  margin-bottom: -1px;
  transition: color .12s ease, border-color .12s ease;
}
.rec-tab:hover { color: var(--color-text); }
.rec-tab.is-active {
  color: var(--color-text);
  border-bottom-color: var(--color-text);
}
.rec-tab:focus-visible {
  outline: 2px solid var(--color-text-muted);
  outline-offset: 1px;
}
.rec-tab-panel { display: none; }
.rec-tab-panel.is-active { display: block; }

/* ── Right rail: sticky on desktop so generate is always reachable ───── */
.rec-rail-card {
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}
.rec-rail-eyebrow {
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
  display: block;
}

.rec-context-summary { padding: var(--space-3) var(--space-4); }
.rec-context-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.rec-context-list li {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.82rem;
  color: var(--color-text);
}
.rec-context-dot {
  width: 8px; height: 8px; border-radius: 999px;
  background: var(--color-border);
  flex-shrink: 0;
}
.rec-context-list li.is-complete .rec-context-dot { background: var(--color-green); }
.rec-context-list li.is-partial  .rec-context-dot { background: var(--color-amber); }
.rec-context-list li.is-empty    .rec-context-dot { background: var(--color-border); }
.rec-context-detail {
  margin-left: auto;
  font-size: 0.74rem;
  color: var(--color-text-muted);
}

.rec-verify-card { padding: 0; }
.rec-verify-card > .rec-rail-summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-3) var(--space-4);
  cursor: pointer;
  list-style: none;
}
.rec-verify-card > .rec-rail-summary::-webkit-details-marker { display: none; }
.rec-verify-card > .rec-rail-summary > .rec-rail-eyebrow { margin: 0; }
.rec-verify-toggle {
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--color-text-muted);
  transition: transform .15s ease;
}
.rec-verify-card[open] > .rec-rail-summary > .rec-verify-toggle { transform: rotate(45deg); }
.rec-verify-body {
  padding: 0 var(--space-4) var(--space-3);
  border-top: 1px solid var(--color-border-light);
  overflow: hidden;
}
.rec-verify-body .rec-snapshot-list { gap: var(--space-2); margin-top: var(--space-3); }
/* Override the wide-card grid: the rail is only ~280px wide. Stack
   name + bars + drift vertically and shrink each line so nothing
   collides or gets clipped. */
.rec-verify-body .rec-snapshot-row {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  padding: var(--space-2) var(--space-3);
  min-width: 0;
}
.rec-verify-body .rec-snapshot-name { width: 100%; min-width: 0; }
.rec-verify-body .rec-snapshot-name .font-semibold {
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rec-verify-body .rec-snapshot-bars {
  width: 100%;
  min-width: 0;
  gap: 3px;
}
.rec-verify-body .rec-bar-row { gap: 6px; }
.rec-verify-body .rec-bar-label { width: 48px; font-size: 0.72rem; }
.rec-verify-body .rec-bar-value { font-size: 0.74rem; min-width: 44px; text-align: right; }
.rec-verify-body .rec-snapshot-drift {
  align-self: stretch;
  text-align: left;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.rec-verify-body .rec-snapshot-drift .font-mono {
  font-size: 0.85rem;
}
.rec-verify-body .rec-snapshot-drift .text-xs {
  font-size: 0.7rem;
}
.rec-verify-body .rec-cashflow-card { padding: var(--space-2) var(--space-3); }
.rec-verify-body .rec-cashflow-net  { font-size: 0.95rem; }

/* Rail card order. The rail is a flex column; using `order` lets us
   put the primary action (Ask-next) at the top without restructuring
   the markup. Markup order stays Context summary → Verify → Ask-next
   so screen readers and no-CSS clients still read sensibly, but
   visually the rail leads with the action the user came to take. */
.rec-context-summary { order: 1; }
.rec-verify-card     { order: 2; }
.rec-ask-next        { order: 0; }

/* Ask-next sticky panel (desktop). On mobile, reflows below the result.
   top:5rem clears the page-header (also position:sticky with top:0; its
   rendered height is ~64-80px depending on title + subtitle); a smaller
   value put the rail card behind the translucent header. */
.rec-ask-next {
  position: sticky;
  top: 5rem;
}
.rec-ask-form { display: flex; flex-direction: column; gap: var(--space-3); }
.rec-ask-submit { width: 100%; }

/* Hide the visually-hidden labels but keep them for screen readers. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* The default .form-row-* grid uses 1fr 1fr, which lets content with
   wider intrinsic min-widths (long input + helper text) push the column
   past its allocated space. Inside .rec-main the column is narrower than
   the viewport because the rail eats 320px, so we explicitly use
   minmax(0, 1fr) here and stack to a single column when the main column
   itself gets cramped. */
.rec-main { overflow: hidden; }
.rec-main .form-row-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.rec-main .form-row-3 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); }
@media (max-width: 1180px) {
  .rec-main .form-row-3 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
@media (max-width: 1024px) {
  .rec-main .form-row-2,
  .rec-main .form-row-3 { grid-template-columns: minmax(0, 1fr); }
}

/* ── Edit-context disclosure under the main column ───────────────────── */
.rec-edit-context { padding: 0; }
.rec-edit-context-summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-4) var(--space-5);
  cursor: pointer;
  list-style: none;
}
.rec-edit-context-summary::-webkit-details-marker { display: none; }
.rec-edit-context-title { font-weight: 600; font-size: 0.95rem; }
.rec-edit-context-sub { margin-top: 2px; }
.rec-edit-context-toggle {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--color-text-muted);
  padding: 4px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}
.rec-edit-context-open-label  { display: inline; }
.rec-edit-context-close-label { display: none; }
.rec-edit-context[open] .rec-edit-context-open-label  { display: none; }
.rec-edit-context[open] .rec-edit-context-close-label { display: inline; }
.rec-edit-context-body {
  padding: var(--space-5);
  border-top: 1px solid var(--color-border-light);
}

/* ── Empty-state intro card ──────────────────────────────────────────── */
.rec-intro-card { padding: var(--space-6); }
.rec-intro-title {
  font-size: 1.2rem;
  font-weight: 600;
  margin: 0 0 var(--space-2);
  color: var(--color-text);
}
.rec-intro-lead {
  margin: 0 0 var(--space-5);
  color: var(--color-text-muted);
  line-height: 1.55;
  max-width: 64ch;
}
.rec-intro-steps {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.rec-intro-steps > li {
  display: grid;
  grid-template-columns: 32px 1fr;
  gap: var(--space-3);
  align-items: start;
}
.rec-intro-step-num {
  width: 32px; height: 32px;
  border-radius: 999px;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  color: var(--color-text-muted);
}
.rec-intro-step-title { font-weight: 600; margin-bottom: 2px; }
.rec-intro-step-body  { line-height: 1.5; }

/* ── Mobile / tablet reflow ──────────────────────────────────────────── */
@media (max-width: 960px) {
  .rec-layout {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-4);
  }
  .rec-rail { order: 2; }
  /* Sticky doesn't help in a single column — release it so the panel
     scrolls naturally with the rest of the page. */
  .rec-ask-next { position: static; }
  /* The "Verify what the AI received" card is transparency chrome that
     duplicates the dashboard's per-account allocation view. Worth its
     space on a desktop rail; pure scroll-overhead on a phone where the
     primary action (Ask-next) is what the user actually needs to
     reach. Hide it — the same info is one tap away on the dashboard. */
  .rec-verify-card { display: none; }

  /* Compact context summary on mobile/tablet: flip the vertical list
     to a horizontal one-line strip ("Profile 4/6 · 5 accounts · 3y
     cashflow") so the rail card takes one row of height instead of
     four. Status dots stay visible because they're the at-a-glance
     completeness signal; only the layout changes. */
  .rec-context-summary { padding: var(--space-2) var(--space-3); }
  .rec-context-list {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 4px var(--space-3);
    font-size: 0.78rem;
  }
  .rec-context-list li { gap: 4px; }
  /* The desktop layout pushes the detail (e.g. "4/6 fields") to the
     right edge of each row via margin-left: auto. In a horizontal
     wrap that ruins the rhythm — clear it so label + detail sit
     together as a single unit. */
  .rec-context-detail { margin-left: 0; }

  .rec-hero-slim {
    grid-template-columns: 1fr;
    align-items: stretch;
  }
  .rec-hero-pills { gap: 6px; }
  .rec-hero-cta { align-self: flex-start; }

  .rec-result-card  { padding: var(--space-4) var(--space-4); }
  .rec-intro-card   { padding: var(--space-5) var(--space-4); }
  .rec-tab          { font-size: 0.78rem; padding: 7px 12px; }
}

@media (max-width: 560px) {
  .rec-hero-amount { font-size: 1.35rem; }
  .rec-result-title-row { align-items: flex-start; }
  .rec-history-btn { font-size: 0.74rem; padding: 3px 8px; }

  /* On narrow phones the meta strip's dot-separated row wraps at
     awkward points (e.g. mid-date), and the dots end up dangling at
     line ends. Switch to a column stack so each fact gets its own
     line and the dots disappear — easier to scan at a glance. */
  .rec-hero-meta {
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
  }
  .rec-hero-dot { display: none; }
  /* Full-width CTA on the smallest viewports so the primary action
     is obvious and easy to tap, rather than left-aligned at default
     button width. */
  .rec-hero-cta { align-self: stretch; justify-content: center; }
}

/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 1ms !important; animation-duration: 1ms !important; }
}

/* ═══ UX pass: balances multi-balance rows ═════════════════════════════
   The previous markup used inline flex with fixed pixel widths on the
   amount + currency + save controls. On a 375px viewport those widths
   summed past the card width and the delete-form often dropped onto
   its own line below Save. Switching to a grid lets the cells share
   space without overflow, with a dedicated mobile rule below so the
   controls stack cleanly on phones. */
.balance-name { min-width: 0; }
/* Ellipsis the name on the icon+name flex row: text-overflow only works on
   the inline-text element, so truncate the link, not the flex container. */
.balance-name a {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.balance-usd-stack { flex-shrink: 0; }

.mb-row {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--color-border-light);
}
.mb-row-inner {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: nowrap;
}
.mb-grid {
  flex: 1;
  display: grid;
  /* Label is optional and secondary, so it gets a small share; the
     amount takes the bulk of the row. Currency + Save stay at their
     tightened auto widths. Focus-expand below still hands the amount the
     whole row while you type. */
  grid-template-columns: minmax(46px, 0.5fr) minmax(0, 1.5fr) auto auto;
  gap: 6px;
  align-items: center;
  margin: 0;
  /* Animate the column resize when the amount field gains focus.
     Transitions on grid-template-columns work in modern browsers
     between explicit fr / px values; older browsers just snap, which
     is still a functional improvement over the cramped default. */
  transition: grid-template-columns 180ms var(--ease, ease);
}
.mb-grid .mb-label   {
  min-width: 0;
  /* Trim the base form-control side padding so a short label still fits
     in the narrowed column at rest (focus-expand reveals long ones). */
  padding-left: var(--space-3);
  padding-right: var(--space-3);
}
/* The amount input is borderless inside .mb-amount-field (which carries the
   border + focus/invalid states). Same size as the single-balance money
   slot so the two editable fields read identically. */
.mb-grid .mb-amount  {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  padding: 0;
  outline: none;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -.01em;
}
.mb-grid .mb-currency {
  width: auto;
  min-width: 58px;
  padding-left: var(--space-3);
  padding-right: 22px;
  background-position: right 6px center;
}

/* ── Edit details: label + currency behind a toggle ──────────────────────
   Label and currency are edited rarely and competed with the amount for
   width, so by default both editors are hidden and the amount spans the
   row (mirroring the single-balance money slot). The label shows as a
   read-only caption above the amount; the currency as a read-only code
   beside it. "Edit details" adds .mb-editing to reveal the inputs. */
.mb-row-cap {
  font-size: .78rem;
  font-weight: 600;
  color: var(--color-text-muted);
  margin-bottom: 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mb-row-cap[hidden] { display: none; }

/* The amount is a glyph-prefixed field mirroring the single-balance money
   slot: a bordered box holding a faint currency glyph + a borderless input,
   so the symbol reads as part of the value. The glyph reflects the entry's
   currency (updated live when the currency picker changes in edit mode). */
.mb-amount-field {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
  padding: 0 var(--space-3);
  min-height: 42px;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.mb-amount-field:focus-within {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(79, 70, 229, .18);
  background: var(--color-surface);
}
.mb-amount-field:has(.mb-amount.is-invalid) {
  border-color: var(--color-red);
  box-shadow: 0 0 0 3px rgba(220, 38, 38, .12);
}
.mb-amount-prefix {
  flex: 0 0 auto;
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* Collapsed (default): hide the label + currency picker; the amount field
   (with its glyph) spans the row. */
[data-mb-card]:not(.mb-editing) .mb-save-form .mb-label,
[data-mb-card]:not(.mb-editing) .mb-save-form .mb-currency { display: none; }
[data-mb-card]:not(.mb-editing) .mb-save-form.mb-grid {
  grid-template-columns: minmax(0, 1fr) auto;
}
/* Editing: reveal label + currency inputs, hide the read-only caption. */
[data-mb-card].mb-editing .mb-row-cap { display: none; }

/* Tighter vertical rhythm for the multi-balance card: smaller gap between
   its sections (header / controls / list / add / footer) than the default
   balance-card spacing. */
[data-mb-card] { gap: var(--space-2); }
.mb-controls { display: flex; justify-content: flex-end; margin-top: 0; }
.mb-edit-toggle {
  background: none;
  border: 0;
  cursor: pointer;
  font-size: .75rem;
  color: var(--color-text-muted);
  padding: 2px 4px;
  border-radius: var(--radius-sm);
}
.mb-edit-toggle:hover { color: var(--color-text); }
.mb-edit-toggle .mb-edit-exit { display: none; }
[data-mb-card].mb-editing .mb-edit-toggle .mb-edit-enter { display: none; }
[data-mb-card].mb-editing .mb-edit-toggle .mb-edit-exit { display: inline; }

/* Focus-expand: whichever of label / amount the user is typing in
   takes the full flex pool while the other fades to 0. The currency
   select + Save button stay visible at their auto widths. The label
   fades on opacity instead of swapping to display:none so the rest
   of the row doesn't flicker. :has() handles the parent-from-child
   match; the .mb-grid transition above carries the resize over ~180ms.
   Applies at any usable card width since the row no longer stacks. */
@container (min-width: 200px) {
  .mb-editing .mb-grid .mb-label,
  .mb-editing .mb-grid .mb-amount-field { transition: opacity 120ms var(--ease, ease); }

  .mb-editing .mb-grid:has(.mb-amount:focus) {
    grid-template-columns: 0 minmax(0, 1fr) auto auto;
  }
  .mb-editing .mb-grid:has(.mb-amount:focus) .mb-label {
    opacity: 0;
    pointer-events: none;
  }

  .mb-editing .mb-grid:has(.mb-label:focus) {
    grid-template-columns: minmax(0, 1fr) 0 auto auto;
  }
  .mb-editing .mb-grid:has(.mb-label:focus) .mb-amount-field {
    opacity: 0;
    pointer-events: none;
  }
}
.mb-save-btn { min-width: 56px; }
.mb-delete-form { margin: 0; }
.mb-delete-btn {
  color: var(--color-text-muted);
  padding: var(--space-2);
  line-height: 0;
  border-radius: var(--radius);
}
.mb-row-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  margin-top: 4px;
  min-height: 16px;
  font-size: .78rem;
}
.mb-row-error { color: var(--color-red-fg); }
.mb-row-usd   { margin-left: auto; color: var(--color-text-faint); }

/* Per-entry "exclude from net worth". The checkbox shows in edit mode
   (full width under the inputs); when set, the bucket is marked Off-NW:
   its amount field dims (collapsed) and an "Off net worth" tag shows in
   the meta line. The bucket still displays its value; it just isn't summed
   into the account total or net worth. */
.mb-row-exclude {
  display: none;
  align-items: center;
  gap: 6px;
  grid-column: 1 / -1;
  font-size: .75rem;
  color: var(--color-text-muted);
  cursor: pointer;
}
.mb-row-exclude input { margin: 0; flex: 0 0 auto; }
[data-mb-card].mb-editing .mb-row-exclude { display: flex; }
[data-mb-card]:not(.mb-editing) .mb-row.is-off-nw .mb-amount-field {
  opacity: .55;
  border-style: dashed;
}
.mb-row-offnw {
  display: none;
  font-size: .6rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.mb-row.is-off-nw .mb-row-offnw { display: inline-block; }
.mb-add-exclude {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: .85rem;
  color: var(--color-text-muted);
  cursor: pointer;
}
.mb-add-exclude input { margin: 0; flex: 0 0 auto; }
/* Control text (disclosure summary) shares the .75rem scale with the
   labels toggle. */
.mb-disclosure-toggle { font-size: .75rem; }
/* Add-entry is a small button that opens a modal, so it takes minimal
   space: no full-width bar, no heavy separator rule above it. */
.mb-add-launch { margin-top: var(--space-1); }
.mb-add-launch .mb-add-open { padding: 4px 10px; }
.mb-add-form { display: flex; flex-direction: column; gap: var(--space-2); margin: 0; }
.mb-add-field-label { margin-bottom: 0; }
.mb-add-amount-row { display: flex; gap: var(--space-2); }
.mb-add-amount-row .mb-amount { flex: 1 1 auto; min-width: 0; font-variant-numeric: tabular-nums; }
.mb-add-amount-row .mb-amount.is-invalid {
  border-color: var(--color-red);
  box-shadow: 0 0 0 3px rgba(220, 38, 38, .12);
}
.mb-add-amount-row .mb-currency { width: auto; min-width: 84px; }
.mb-add-error {
  display: block;
  margin-top: 4px;
  color: var(--color-red-fg);
}

@container (max-width: 360px) {
  /* Very narrow card: keep the single inline row, just shrink the value
     and controls a touch so nothing clips or wraps, and stack the meta
     (preview/error) line. */
  .mb-grid .mb-amount,
  .mb-amount-prefix     { font-size: .95rem; }
  .mb-grid .mb-currency { min-width: 50px; padding-right: 20px; }
  .mb-save-btn          { min-width: 50px; }
  .mb-delete-btn        { min-width: 40px; min-height: 40px; }
  .mb-row-meta { flex-direction: column; align-items: flex-start; min-height: 0; }
  .mb-row-meta .mb-row-usd { margin-left: 0; }
}

/* Stack the .amount-meta row's preview + error vertically on phones so
   a long FX preview ("≈ $1,234.56 · 1,234,500.00 NGN at 800.00 NGN/unit")
   doesn't collide with the inline error text. */
@media (max-width: 600px) {
  .amount-meta {
    flex-direction: column;
    align-items: stretch;
    min-height: 0;
  }
  .amount-meta .amount-preview,
  .amount-meta .amount-error { white-space: normal; }
}

/* ═══ UX pass: chart canvas height on phones ═══════════════════════════
   300px is fine on desktop but eats ~45% of an iPhone 8-class viewport.
   Drop to 220px on small screens so the chart shares vertical space
   with the cards above and below. */
@media (max-width: 600px) {
  .chart-wrap { height: 220px; }
}

/* ═══ UX pass: trends category-year chips overflow ════════════════════
   Long-time users with 10+ years of categorical data overflowed the
   chip row. Switch to a horizontally-scrollable strip with snap points
   + a faded right edge so scrollability is visible. */
.category-year-chips {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  padding-bottom: 4px;
}
.category-year-chips .category-year-chip {
  flex-shrink: 0;
  scroll-snap-align: start;
}
/* On phones a faded right edge hints at overflow. We can't tell from CSS
   alone whether the strip is actually overflowing, so keep the mask
   small enough that it doesn't visibly clip the last chip when the
   strip is short. */
@media (max-width: 600px) {
  .category-year-chips {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 16px), transparent);
            mask-image: linear-gradient(to right, #000 calc(100% - 16px), transparent);
  }
}

/* ═══ UX pass: chat ═════════════════════════════════════════════════════ */
/* Backdrop revealed when the mobile sidebar slides open. Tapping it
   closes the drawer; clicking on desktop is a no-op (it stays hidden). */
.chat-sidebar-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, .35);
  z-index: 8;
  display: none;
}
@media (max-width: 900px) {
  .chat-sidebar-backdrop:not([hidden]) { display: block; }
  .chat-sidebar.is-open { z-index: 9; }
}

/* "Jump to latest" pill — surfaced when the user has scrolled the
   thread up and a new reply lands while they're reading. Anchors
   against the chat-composer-region wrapper so it always sits 8px above
   the composer's top edge regardless of how tall the textarea has
   auto-grown. */
.chat-composer-region { position: relative; }
.chat-jump-latest {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 8px);
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 6px 12px;
  font-size: .78rem;
  font-weight: 500;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 999px;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  z-index: 5;
}
.chat-jump-latest:hover { background: var(--color-bg); }
/* Honour the [hidden] attribute. The display: inline-flex rule above
   has the same specificity as the UA's [hidden] { display: none }
   sheet, and author styles win at equal specificity, so without this
   override the pill stayed rendered even when jumpPill.hidden = true
   was set from JS — leading it to overlap the followup chips. */
.chat-jump-latest[hidden] { display: none; }

/* Cost popover. Native <details> chip whose summary is the cost label;
   the body slides out below the row on tap/click so the token detail
   reaches mobile (a `title` attribute did not). */
.chat-msg-cost-details { display: inline; }
.chat-msg-cost-details > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
}
.chat-msg-cost-details > summary::-webkit-details-marker { display: none; }
.chat-msg-cost-details[open] .chat-msg-cost { color: var(--color-text); }
.chat-msg-cost-detail {
  display: block;
  margin-top: 3px;
  font-size: .68rem;
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
}

/* Per-code-block copy buttons. The shell wraps the <pre> so we can
   absolutely position the button inside it without nudging the code
   layout. */
.chat-code-shell { position: relative; }
.chat-code-shell > pre { margin: 0; }
.chat-code-copy {
  position: absolute;
  top: 6px;
  right: 6px;
  padding: 3px 8px;
  font-size: .68rem;
  font-weight: 500;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 4px;
  color: var(--color-text-muted);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease);
}
.chat-code-shell:hover .chat-code-copy,
.chat-code-shell:focus-within .chat-code-copy,
.chat-code-copy.is-success { opacity: 1; }
@media (pointer: coarse) {
  .chat-code-copy { opacity: 1; }
}
.chat-code-copy.is-success {
  color: var(--color-green-fg);
  border-color: var(--color-green-border);
  background: var(--color-green-bg);
}

/* Send button gets a Stop affordance during an in-flight request.
   The icon swap is purely CSS — the JS only toggles the class. */
.chat-composer-send .chat-composer-stop-icon { display: none; }
.chat-composer-send.is-sending .chat-composer-send-icon { display: none; }
.chat-composer-send.is-sending .chat-composer-stop-icon { display: inline-block; }
.chat-composer-send.is-sending {
  background: var(--color-red);
  border-color: var(--color-red);
}

/* Touch-only: the kebab menu in the conversation list was opacity:0
   except on :hover or [open] or .is-active. On a phone :hover doesn't
   fire, which made rename/pin/delete unreachable for every non-active
   conversation. Force it visible on coarse pointers. */
@media (pointer: coarse) {
  .chat-history-menu-trigger { opacity: 1; }
}

/* Pinned conversation rows get a subtle amber accent so they read as
   "promoted" even before the pin icon registers. The pin icon alone
   was easy to miss against a busy list. */
.chat-history-row.is-pinned {
  box-shadow: inset 2px 0 0 var(--color-amber);
}

/* Tool-call chip overflow. Server may emit 5-7 chips for a complex
   reply; on mobile that stacked under the "Grounded with" label and
   ate vertical space before the user could read the bubble. Hide
   chips past the second when the <details> is closed; opening it
   shows the full set inline with the JSON body. */
.chat-msg-tools:not([open]) .chat-msg-tools-summary .chat-msg-tool-chip:nth-of-type(n+3) {
  display: none;
}

/* Hide the action labels under tight bubbles. The buttons keep their
   aria-label and tooltip so screen readers + desktop hovers still get
   the text. */
@media (max-width: 480px) {
  .chat-msg-action .chat-msg-action-label { display: none; }
  .chat-msg-action { padding: 8px 10px; min-width: 44px; justify-content: center; }
}

/* Mobile meta + counter polish. Meta at .68rem was below comfortable
   reading on a phone; the counter only appeared near the cap, which
   left long admin-set limits invisible to the user. */
@media (max-width: 900px) {
  .chat-msg-meta { font-size: .78rem; }
}
@media (max-width: 900px) {
  /* Always show the counter on mobile — a faint readout that snaps
     into warning/danger color as the user approaches the cap. */
  .chat-composer-counter {
    display: inline-block;
    opacity: .55;
  }
  .chat-composer-counter.is-warning,
  .chat-composer-counter.is-danger { opacity: 1; }
}

/* ═══ UX pass: recommendations form on phones ══════════════════════════
   Three-column form rows were collapsing only at 900px; between 600 and
   900px the long labels ("Time horizon (yrs)") still crowded the inputs.
   Collapse earlier and bump the active tab indicator so the user can
   tell which page they're on at .78rem. */
@media (max-width: 700px) {
  .rec-main .form-row-3 { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
  .rec-tab.is-active,
  .rec-tab[aria-selected="true"] {
    font-weight: 600;
    border-bottom-width: 3px;
  }
}

/* Sticky save bar for the long Edit-your-context disclosure on mobile
   so the Save button reaches the thumb without scrolling back up
   through six form sections. */
@media (max-width: 700px) {
  .rec-profile-form .rec-profile-save {
    position: sticky;
    bottom: env(safe-area-inset-bottom, 0px);
    background: var(--color-surface);
    padding: var(--space-3) 0;
    margin-top: var(--space-4);
    border-top: 1px solid var(--color-border-light);
    z-index: 2;
  }
}

/* ───────────────────────────────────────────────────────────────────
   Milestones strip — sits inside the net-worth chart card under the
   canvas. The "next" row is always-visible; the achievements list is
   tucked behind a <details> disclosure so the card stays tight on a
   user with a long history. */
.milestone-strip {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid rgba(0,0,0,0.05);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.milestone-next-row {
  display: flex; justify-content: space-between; align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.milestone-next-label  { color: var(--color-text); }
.milestone-next-figure { color: var(--color-text-muted); font-size: 12px; }
.milestone-next-remaining { color: var(--color-text); font-weight: 500; }
.milestone-next-bar {
  height: 6px;
  background: var(--color-bg-elevated);
  border-radius: 999px;
  overflow: hidden;
}
.milestone-next-fill {
  height: 100%;
  background: var(--color-primary);
  border-radius: 999px;
  transition: width 240ms ease;
}
.milestone-achievements summary {
  cursor: pointer;
  font-size: 12px;
  color: var(--color-text-muted);
  padding: 2px 0;
  list-style: none;
}
.milestone-achievements summary::-webkit-details-marker { display: none; }
.milestone-achievements summary::before {
  content: '▸ ';
  display: inline-block;
  transition: transform 120ms ease;
  color: var(--color-text-muted);
}
.milestone-achievements[open] summary::before { content: '▾ '; }
.milestone-achieved-list {
  list-style: none;
  padding: 0;
  margin: 8px 0 0;
  display: flex; flex-direction: column; gap: 4px;
  font-size: 13px;
}
.milestone-achieved-list li { display: flex; align-items: baseline; gap: 8px; }
.milestone-tick {
  color: var(--color-green);
  font-weight: 700;
  font-size: 14px;
}
.milestone-manage {
  align-self: flex-end;
  font-size: 12px;
  color: var(--color-primary);
  text-decoration: none;
}
.milestone-manage:hover { text-decoration: underline; }
.milestone-manage-inline { margin-top: -4px; }
.milestone-all-done {
  font-size: 13px;
  color: var(--color-text-muted);
  text-align: center;
  padding: 8px 0;
}
.milestone-all-done a {
  color: var(--color-primary);
  text-decoration: none;
  margin-left: 6px;
}

/* ───────────────────────────────────────────────────────────────────
   Privacy blur for net-worth figures.

   Any element marked [data-blur="net-worth"] becomes a tap target;
   the JS in app.js mirrors a single on/off bit into
   body.is-net-worth-blurred (persisted to localStorage and synced
   across tabs), so one rule below covers every annotated figure
   without per-element bookkeeping.
   ─────────────────────────────────────────────────────────────────── */
[data-blur="net-worth"] {
  cursor: pointer;
  transition: filter 200ms ease;
  border-radius: 6px;
}
[data-blur="net-worth"]:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 4px;
}
.is-net-worth-blurred [data-blur="net-worth"],
.is-net-worth-blurred [data-account-local],
.is-net-worth-blurred .onchain-amount,
.is-net-worth-blurred .balance-usd-stack .text-muted {
  /* Heavy enough that an over-the-shoulder viewer can't shape-match
     digits, light enough that the user knows what's behind it.
     [data-account-local] is the per-account local total sitting next to
     the blurred USD; .onchain-amount covers read-only holding values
     (equity shares, per-wallet BTC, cash) and .balance-usd-stack
     .text-muted their header sub-lines -- all reveal position size, so
     they hide alongside the USD figures. */
  filter: blur(12px);
  user-select: none;
}
/* Editable balance inputs blur at rest as well, but reveal on focus so
   they stay usable while the privacy toggle is on. */
.is-net-worth-blurred .money-slot-input,
.is-net-worth-blurred .mb-amount {
  filter: blur(8px);
}
.is-net-worth-blurred .money-slot-input:focus,
.is-net-worth-blurred .mb-amount:focus {
  filter: none;
}

/* ═══ Settings page: shared header/state pills ═════════════════════════ */
.section-header-meta {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
  min-width: 0;
}
.section-header-meta .section-title { flex: 0 0 auto; }
.section-header-meta .header-stamp {
  font-size: .78rem;
  color: var(--color-text-muted);
}
.section-header-meta .header-stamp strong { color: var(--color-text); font-weight: 600; }

.state-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .03em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 999px;
}
.state-pill::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: currentColor;
}
.state-pill.is-on  { color: var(--color-green); background: var(--color-green-bg); }
.state-pill.is-off { color: var(--color-red); background: var(--color-red-bg); }
.state-pill.is-warn{ color: var(--color-amber); background: rgba(245,158,11,.1); }

/* Per-card flash slot: lives inside a save form so AJAX responses
   appear next to the action that triggered them, not at the top of
   the page. Slides in then auto-fades after a few seconds. */
.card-flash {
  display: none;
  align-items: center;
  gap: var(--space-2);
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  font-size: .85rem;
  margin: var(--space-3) 0;
  transition: opacity var(--t-fast) var(--ease);
}
.card-flash.is-visible { display: flex; }
.card-flash.is-ok   { background: var(--color-green-bg); color: #14532d; border: 1px solid var(--color-green-border); }
.card-flash.is-err  { background: var(--color-red-bg);   color: #7f1d1d; border: 1px solid var(--color-red-border); }
.card-flash.is-fade { opacity: 0; }

/* AI chat card: caps sub-group + dim-when-off treatment */
.chat-caps {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border-light);
}
.chat-caps-header {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.chat-caps-label {
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.chat-caps-note {
  font-size: .78rem;
  color: var(--color-amber);
}
/* Fade the cap inputs when the chat kill-switch is off. Inputs stay
   focusable/editable so an admin can pre-configure values while
   chat is disabled. */
form[data-ajax-save="chat"]:has(#chat_enabled:not(:checked)) .chat-caps .form-row {
  opacity: .55;
  transition: opacity var(--t-fast) var(--ease);
}

/* See-also footer (per-user drift link, etc.) */
.see-also {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding-top: var(--space-3);
  margin-top: var(--space-3);
  border-top: 1px solid var(--color-border-light);
  font-size: .82rem;
  color: var(--color-text-muted);
}
.see-also a { color: var(--color-primary); text-decoration: none; font-weight: 500; }
.see-also a:hover { text-decoration: underline; }

/* ═══ Settings: market data refresh row + alerts ═══════════════════════ */
.market-not-fetched {
  color: var(--color-amber) !important;
  font-weight: 600;
}
.kv-row.is-warn .kv-label::before {
  content: '⚠ ';
  color: var(--color-amber);
}

/* FX rates disclosure: header row mirrors city-rent layout */
.fx-rates-status,
.city-rent-status {
  border-top: 1px solid var(--color-border-light);
  padding-top: var(--space-3);
  margin-top: var(--space-4);
}
.fx-rates-status > summary,
.city-rent-status > summary {
  cursor: pointer;
  user-select: none;
  font-size: .85rem;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.fx-rates-status > summary::-webkit-details-marker,
.city-rent-status > summary::-webkit-details-marker { display: none; }
.fx-rates-status > summary::before,
.city-rent-status > summary::before {
  content: '▸';
  font-size: .7rem;
  color: var(--color-text-faint);
  transition: transform var(--t-fast) var(--ease);
  display: inline-block;
}
.fx-rates-status[open] > summary::before,
.city-rent-status[open] > summary::before { transform: rotate(90deg); }

.fx-rates-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: var(--space-1) var(--space-4);
  margin-top: var(--space-3);
  font-size: 13px;
}
.fx-rates-grid .fx-pair {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 4px 0;
  border-bottom: 1px solid var(--color-border-light);
}
.fx-rates-grid .fx-pair-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--color-text-muted);
}
.fx-rates-grid .fx-pair-rate {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
}

/* ═══ Settings: city rent table (lifted from inline styles) ════════════ */
.city-rent-filter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-3);
  font-size: 12px;
}
.city-rent-filter-btn {
  padding: 4px 10px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: 999px;
  cursor: pointer;
  font-weight: 500;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.city-rent-filter-btn:hover { border-color: var(--color-primary); }
.city-rent-filter-btn[data-active="1"] {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.city-rent-filter-btn .text-muted { color: inherit; opacity: .75; }

.city-rent-table-wrap {
  overflow-x: auto;
  margin-top: var(--space-3);
  -webkit-overflow-scrolling: touch;
}
.city-rent-table {
  width: 100%;
  min-width: 540px;
  font-size: 13px;
  border-collapse: collapse;
}
.city-rent-table thead th {
  text-align: left;
  color: var(--color-text-muted);
  font-weight: 600;
  padding: 6px 8px;
  border-bottom: 1px solid var(--color-border-light);
}
.city-rent-table thead th.is-num { text-align: right; }
.city-rent-table tbody td {
  padding: 6px 8px;
  vertical-align: top;
}
.city-rent-table tbody tr[data-row-status] {
  border-bottom: 1px solid var(--color-border-light);
}
.city-rent-table td.is-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.city-rent-table .country-header td {
  padding: 8px 8px 4px;
  font-size: 12px;
  color: var(--color-text-muted);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  background: var(--color-bg);
  border-top: 1px solid var(--color-border-light);
}
.city-rent-table .country-header .country-flag {
  font-size: 14px;
  margin-right: 6px;
}
.city-rent-table .status-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  vertical-align: middle;
  margin-right: 6px;
}
.city-rent-table .status-label {
  font-weight: 500;
  text-transform: capitalize;
}
.city-rent-table .status-fresh   { color: var(--color-green); }
.city-rent-table .status-stale   { color: var(--color-amber); }
.city-rent-table .status-missing { color: var(--color-red); }
.city-rent-table .bg-fresh   { background: var(--color-green); }
.city-rent-table .bg-stale   { background: var(--color-amber); }
.city-rent-table .bg-missing { background: var(--color-red); }
.city-rent-table .city-flag { margin-right: 6px; }
.city-rent-table .suspect-warning {
  color: var(--color-amber);
  margin-left: 8px;
  font-weight: 500;
  cursor: help;
  font-size: .72rem;
}
.city-rent-table .partial-warning {
  color: var(--color-amber);
  margin-left: 6px;
  font-weight: 500;
  font-size: .72rem;
}
.city-rent-table .rent-secondary {
  color: var(--color-text-muted);
  font-weight: 400;
  font-size: .72rem;
}
.city-rent-table .age-cell {
  color: var(--color-text-muted);
}
.city-rent-table .age-cell .snapshot-date {
  font-size: .72rem;
  color: var(--color-text-faint);
}
.city-rent-table .placeholder { color: var(--color-text-muted); }

/* Inline alert used for "refresh appears stalled" */
.inline-alert {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  font-size: .85rem;
  margin: var(--space-3) 0;
}
.inline-alert.is-warn {
  background: rgba(245,158,11,.08);
  border: 1px solid rgba(245,158,11,.25);
  color: #92400e;
}
.inline-alert.is-info {
  background: var(--color-primary-bg);
  border: 1px solid var(--color-border);
  color: var(--color-text);
}

/* Mobile polish: shrink heavy table on narrow screens */
@media (max-width: 600px) {
  .currency-grid {
    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  }
  .city-rent-table { font-size: 12px; }
  .city-rent-table thead th,
  .city-rent-table tbody td { padding: 5px 6px; }
}

/* ═══ Account page: "More" list + Trading212 security disclosure ══════ */
.account-more-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.account-more-link {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.account-more-link:hover {
  border-color: var(--color-primary);
  background: var(--color-bg-elevated);
}
.account-more-text { flex: 1; min-width: 0; }
.account-more-title {
  font-weight: 600;
  font-size: .9rem;
  color: var(--color-text);
}
.account-more-sub {
  font-size: .8rem;
  color: var(--color-text-muted);
  margin-top: 2px;
}
.account-more-chevron {
  flex-shrink: 0;
  color: var(--color-text-faint);
  transition: transform var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.account-more-link:hover .account-more-chevron {
  color: var(--color-primary);
  transform: translateX(2px);
}

/* Trading212 read-only-key warning: closed by default once a key is
   configured, open by default while the user is still setting up. */
.t212-security-note {
  border: 1px solid var(--color-amber);
  background: rgba(245, 158, 11, .06);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
}
.t212-security-note > summary {
  cursor: pointer;
  user-select: none;
  list-style: none;
  font-weight: 600;
  font-size: .9rem;
  color: #92400e;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.t212-security-note > summary::-webkit-details-marker { display: none; }
.t212-security-note > summary::before {
  content: '⚠';
  font-size: 1rem;
}
.t212-security-note > summary::after {
  content: '▸';
  margin-left: auto;
  font-size: .7rem;
  color: var(--color-text-muted);
  transition: transform var(--t-fast) var(--ease);
}
.t212-security-note[open] > summary::after { transform: rotate(90deg); }
.t212-security-body {
  margin-top: var(--space-3);
  font-size: .85rem;
  line-height: 1.5;
  color: var(--color-text);
}
.t212-security-footnote {
  margin-top: var(--space-2);
  font-size: .8rem;
  color: var(--color-text-muted);
}

/* ═══ Admin logs filter form (errors page) ═════════════════════════════
   Three-column grid on desktop (fixed-width level select, flexible
   search, button row); stacks to one column on phones so the search
   field isn't crushed and the buttons get their own row. */
.log-filter-form {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr) auto;
  gap: var(--space-3);
  align-items: end;
}
.log-filter-form .form-group { margin: 0; }
.log-filter-form .log-filter-actions {
  display: flex;
  gap: var(--space-2);
}
@media (max-width: 700px) {
  .log-filter-form {
    grid-template-columns: 1fr;
    gap: var(--space-3);
  }
  .log-filter-form .log-filter-actions {
    /* Buttons get equal share of the row at narrow widths so primary
       and secondary are easy thumb targets. */
    flex-wrap: wrap;
  }
  .log-filter-form .log-filter-actions .btn { flex: 1 1 auto; min-width: 0; }
}

/* ═══ PWA install banner ══════════════════════════════════════════════
   Bottom-sticky banner shown to mobile-web users who could benefit
   from installing the app to their home screen. Stays hidden until
   app.js decides this browser is eligible (not already standalone,
   enough visits, not recently dismissed) and adds `.is-visible`.
   Two text variants live in the same shell:
     - Android / Chromium: shows the [Install] CTA which fires
       beforeinstallprompt's deferred prompt
     - iOS Safari: hides the CTA and switches the sub-text to
       instructional copy pointing at the Share button */
.pwa-install-banner {
  position: fixed;
  left: var(--space-3);
  right: var(--space-3);
  bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: 0 10px 30px -10px rgba(15, 23, 42, .25), 0 2px 6px rgba(15, 23, 42, .06);
  transform: translateY(140%);
  transition: transform .25s var(--ease);
}
.pwa-install-banner[hidden] { display: none; }
.pwa-install-banner.is-visible { transform: translateY(0); }

.pwa-install-icon {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  background: var(--color-primary-bg);
  color: var(--color-primary);
}
.pwa-install-body {
  flex: 1;
  min-width: 0;
}
.pwa-install-title {
  font-weight: 600;
  font-size: .9rem;
  color: var(--color-text);
}
.pwa-install-sub {
  font-size: .78rem;
  color: var(--color-text-muted);
  margin-top: 2px;
  line-height: 1.4;
}
/* iOS share-icon glyph embedded inline-block in the sub copy. JS
   injects it after the words "Tap" so the icon sits next to its
   meaning without us templating the position twice. */
.pwa-install-sub .ios-share-glyph {
  display: inline-block;
  vertical-align: -2px;
  margin: 0 2px;
  color: var(--color-primary);
}
.pwa-install-cta { flex-shrink: 0; }
.pwa-install-cta[hidden] { display: none; }
.pwa-install-close {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.pwa-install-close:hover  { background: var(--color-bg); color: var(--color-text); }
.pwa-install-close:focus-visible { box-shadow: var(--ring-focus); outline: none; }

/* Desktop polish: when shown on a wider viewport (Chrome desktop also
   supports beforeinstallprompt) anchor the banner to the bottom-right
   instead of stretching across the screen. */
@media (min-width: 720px) {
  .pwa-install-banner {
    left: auto;
    right: var(--space-5);
    bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
    max-width: 380px;
  }
}

/* When the page is already running in standalone mode (PWA installed)
   the banner is fully suppressed via JS, but this is defence-in-depth
   in case the JS doesn't run. */
@media (display-mode: standalone) {
  .pwa-install-banner { display: none !important; }
}

/* ═══ Password page: reveal toggle + match indicator ══════════════════
   Wraps each password input with a positioned eye-icon toggle. Pure
   progressive enhancement — without JS the inputs still work and the
   button just does nothing visible. */
.password-input { position: relative; }
.password-input .form-control { padding-right: 44px; }
.password-reveal-btn {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  cursor: pointer;
}
.password-reveal-btn:hover  { color: var(--color-text); background: var(--color-bg); }
.password-reveal-btn:focus-visible { box-shadow: var(--ring-focus); outline: none; }
/* Eye-shut shown when revealed; default shows eye-open. */
.password-reveal-btn .eye-shut { display: none; }
.password-reveal-btn[data-shown="1"] .eye-open { display: none; }
.password-reveal-btn[data-shown="1"] .eye-shut { display: inline; }

.password-match-hint {
  display: block;
  margin-top: 6px;
  font-size: .78rem;
}
.password-match-hint.is-match    { color: var(--color-green); }
.password-match-hint.is-mismatch { color: var(--color-text-muted); }

/* ═══ Milestones page ═════════════════════════════════════════════════
   List of net-worth milestone rows with inline edit forms. Achieved
   rows tint green; in-progress rows show a small progress bar so the
   distance to goal reads at a glance. Mobile collapses each row's
   form into two columns with the save button below. */
.milestone-row {
  display: flex; align-items: center; gap: var(--space-3);
  padding: 12px 14px;
  border: 1px solid var(--color-border);
  border-radius: 10px;
  background: var(--color-surface);
}
.milestone-row + .milestone-row { margin-top: var(--space-2); }
.milestone-row.is-achieved {
  border-color: rgba(22, 163, 74, 0.35);
  background: rgba(22, 163, 74, 0.04);
}
.milestone-row-status {
  flex: 0 0 auto;
  width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: var(--color-bg-elevated);
  color: var(--color-text-muted);
  font-size: 14px;
}
.milestone-row.is-achieved .milestone-row-status {
  background: rgba(22, 163, 74, 0.15);
  color: var(--color-green);
}
.milestone-row-main { flex: 1 1 auto; min-width: 0; }
.milestone-row-form {
  display: grid;
  /* Label : Target : Button. Label gets twice the share of the target
     column so long descriptions ("Down payment for the apartment") have
     room, but the target field still gets enough width to display
     "1,000,000" without the user needing to scroll the input. */
  grid-template-columns: minmax(0, 2fr) minmax(160px, 1fr) auto;
  gap: var(--space-2);
  align-items: center;
  margin: 0;
}
.milestone-row-form input[type="text"],
.milestone-row-form input[type="number"] {
  width: 100%;
}
.milestone-row-meta {
  font-size: 12px;
  color: var(--color-text-muted);
  margin-top: 4px;
  font-variant-numeric: tabular-nums;
}
.milestone-row-meta strong { color: var(--color-green); }

/* Thin progress bar under each in-progress milestone row. */
.milestone-progress {
  height: 4px;
  margin-top: 6px;
  background: var(--color-bg-elevated);
  border-radius: 999px;
  overflow: hidden;
}
.milestone-progress-fill {
  height: 100%;
  background: var(--color-primary);
  border-radius: 999px;
  transition: width var(--t-base) var(--ease);
}

.milestone-delete {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  border: 0; background: transparent; cursor: pointer;
  color: var(--color-text-muted);
  border-radius: 6px;
}
.milestone-delete:hover { color: var(--color-red); background: rgba(220, 38, 38, 0.06); }

@media (max-width: 600px) {
  .milestone-row-form { grid-template-columns: 1fr 1fr; }
  .milestone-row-form button[type="submit"] { grid-column: 1 / -1; }
}

.milestone-add-section {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border-light);
}

/* Upcoming / Achieved section headers on /milestones. The Achieved
   section is a <details> element so long histories collapse without
   pushing the actionable Upcoming list off-screen. */
.milestone-section + .milestone-section { margin-top: var(--space-4); }
.milestone-section-title {
  margin: 0 0 var(--space-2);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.milestone-section-toggle {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
}
.milestone-section-toggle::-webkit-details-marker { display: none; }
.milestone-section-toggle::before {
  content: '▸';
  display: inline-block;
  font-size: 9px;
  transition: transform 120ms ease;
}
details.milestone-section[open] > .milestone-section-toggle::before { content: '▾'; }

/* "Next" chip: marks the closest unachieved milestone above current
   net worth (same row the dashboard chart strip surfaces). */
.milestone-next-badge {
  display: inline-block;
  padding: 1px 6px;
  margin-right: 6px;
  border-radius: 999px;
  background: rgba(99, 102, 241, 0.12);
  color: var(--color-primary);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: middle;
}
.milestone-row.is-next { border-color: rgba(99, 102, 241, 0.4); }

/* Inline error shown by the add-form client-side pre-flight before
   the server gets a chance to reject + redirect. */
.milestone-add-error {
  margin-top: var(--space-2);
  padding: 6px 10px;
  border: 1px solid rgba(220, 38, 38, 0.25);
  background: rgba(220, 38, 38, 0.07);
  border-radius: 6px;
  color: var(--color-red);
  font-size: 13px;
}
.milestone-add-error[hidden] { display: none; }

/* ═══ Notifications page ══════════════════════════════════════════════
   The card uses the shared .form-toggle for the on/off switch and the
   shared .state-pill in the section-header for the live status. The
   secondary block below the toggle (send time, test push) hides when
   push is off so the card collapses to a clean "off" state instead of
   showing half-relevant controls. */
.notif-secondary { margin-top: var(--space-3); }
.notif-secondary[hidden] { display: none; }
.notif-time-label {
  display: block;
  font-size: 13px;
  color: var(--color-text-muted);
  margin-bottom: 6px;
}
.notif-time-select {
  width: 100%;
  min-height: 38px;
  font-size: 14px;
}
/* iOS Safari zooms in on focus when the font-size is under 16px on a
   form control. Bump up on the touch breakpoint specifically. */
@media (max-width: 480px) {
  .notif-time-select { font-size: 16px; }
}
.notif-save-flag {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 12px;
  color: var(--color-green);
  margin-left: 8px;
  opacity: 0;
  transition: opacity 180ms ease;
}
.notif-save-flag[data-shown="1"] { opacity: 1; }
.notif-tz-hint {
  margin-top: 6px;
  font-size: 12px;
  color: var(--color-text-muted);
}
.notif-tz-hint strong { font-weight: 600; color: var(--color-text); }
.notif-test-row {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border-light);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
.notif-test-row-hint {
  font-size: 12px;
  color: var(--color-text-muted);
}
@media (max-width: 480px) {
  .notif-test-row { flex-direction: column; align-items: stretch; }
  .notif-test-row form { width: 100%; }
  .notif-test-row .btn { width: 100%; }
}

/* ═══ BudgetBakers bank-account mapping (/balances/budgetbakers) ════════ */
.bb-link-list { display: flex; flex-direction: column; gap: var(--space-2); }
.bb-link-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-bg);
}
.bb-link-bank { flex: 1 1 200px; min-width: 0; }
.bb-link-name { font-weight: 600; letter-spacing: -.01em; }
.bb-link-bal  { font-size: .82rem; font-variant-numeric: tabular-nums; }
.bb-link-target { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.bb-link-form   { display: flex; align-items: center; gap: var(--space-2); margin: 0; flex-wrap: wrap; }
.bb-link-form .form-control { width: auto; min-width: 220px; }
/* Each row renders both states; .is-linked (toggled by JS on link/unlink)
   decides which one shows, so the AJAX path never rebuilds markup. */
.bb-link-row:not(.is-linked) .bb-link-target { display: none; }
.bb-link-row.is-linked .bb-link-form { display: none; }
.bb-link-error { color: var(--color-red-fg); flex: 0 0 100%; }
/* The .tag display:inline-flex rule has the same origin/specificity edge
   over the UA's [hidden]{display:none}, so the hidden attribute alone
   won't hide this pill. Restore it (matches the pattern used elsewhere,
   e.g. .pwa-install-cta[hidden]). Also covers the JS toggle of .hidden. */
.bb-ccy-warn[hidden] { display: none; }
@media (max-width: 600px) {
  .bb-link-form .form-control { width: 100%; min-width: 0; }
  .bb-link-form { width: 100%; }
}

/* Synced-from-BudgetBakers indicators (read-only balance fields + buckets). */
.bb-synced-tag {
  flex: 0 0 auto;
  display: inline-block;
  font-size: .6rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--color-primary-hover);
  background: var(--color-primary-bg);
  border: 1px solid var(--color-primary-border);
  border-radius: 999px;
  padding: 1px 7px;
}
.money-slot.is-synced { background: var(--color-bg-elevated); }
.money-slot.is-synced .money-slot-input,
.mb-row.is-bb-synced .mb-amount { cursor: default; }
/* Keeps the bucket's synced tag + sync button on one line even when the
   meta row stacks into a column on very narrow cards. */
.bb-sync-meta { display: inline-flex; align-items: center; gap: 5px; }
/* Per-card "sync now" button sitting beside the synced tag. */
.bb-sync-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 2px;
  margin: 0;
  border: 0;
  background: transparent;
  color: var(--color-text-faint);
  cursor: pointer;
  border-radius: 6px;
  line-height: 0;
}
.bb-sync-btn:hover { color: var(--color-primary-hover); background: var(--color-primary-bg); }
.bb-sync-btn:disabled { cursor: default; opacity: .7; }
.bb-sync-btn.is-spinning svg { animation: spin .8s linear infinite; }

/* ═══ Demo account showcase ════════════════════════════════════════════ */
/* Slim sandbox notice shown on every page while signed in as the demo
   user. Amber to read as "not your real data" against the indigo app. */
.demo-banner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-5);
  background: #fffbeb;
  border: 1px solid #fde68a;
  border-radius: var(--radius);
  color: #854d0e;
  font-size: .875rem;
  line-height: 1.45;
}
.demo-banner-badge {
  flex: none;
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: 2px 9px;
  border-radius: 999px;
  background: #f59e0b;
  color: #fff;
}

/* Feature highlights — dashboard-only landing card for the demo. */
.demo-features {
  margin-bottom: var(--space-6);
  padding: var(--space-6);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(130% 150% at 0% 0%, var(--color-primary-bg), transparent 58%),
    var(--color-surface);
  box-shadow: var(--shadow-sm);
}
/* Collapsible header: a native <summary> so it folds to a single row to
   reclaim height, no JS needed. */
.demo-features-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  cursor: pointer;
  list-style: none;
}
.demo-features-summary::-webkit-details-marker { display: none; }
.demo-features-summary::marker { content: ''; }
.demo-features-summary:focus-visible { outline: 2px solid var(--color-primary-light); outline-offset: 3px; border-radius: var(--radius-sm); }
.demo-features-headings { display: flex; flex-direction: column; min-width: 0; }
.demo-features-title { font-size: 1.1rem; font-weight: 700; color: var(--color-text); }
.demo-features-sub { font-size: .85rem; color: var(--color-text-muted); margin-top: 2px; }

/* Quiet "Hide / Show" affordance + rotating chevron on the collapse header. */
.demo-features-toggle-hint {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: .8rem;
  font-weight: 600;
  color: var(--color-text-muted);
}
.demo-features-toggle-label::after { content: 'Hide'; }
.demo-features:not([open]) .demo-features-toggle-label::after { content: 'Show'; }
.demo-features-summary:hover .demo-features-toggle-hint { color: var(--color-primary-hover); }
.demo-features-chevron {
  flex: none;
  transition: transform var(--t-base) var(--ease);
}
.demo-features[open] .demo-features-chevron { transform: rotate(180deg); }
.demo-features[open] .demo-features-grid { margin-top: var(--space-4); }
/* Collapsed state nudges the user to expand: the card picks up the accent
   colour and the chevron gently bounces so it reads as "more below". */
.demo-features:not([open]) { border-color: var(--color-primary-border); }
.demo-features:not([open]) .demo-features-toggle-hint { color: var(--color-primary-hover); }
@keyframes demo-features-nudge { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(3px); } }
.demo-features:not([open]) .demo-features-chevron { animation: demo-features-nudge 1.6s var(--ease) infinite; }
@media (prefers-reduced-motion: reduce) {
  .demo-features:not([open]) .demo-features-chevron { animation: none; }
}
.demo-features-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(248px, 1fr));
  gap: var(--space-3);
}

/* Each feature is a launchpad link into that area of the app. */
.demo-feature {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  text-decoration: none;
  color: inherit;
  transition: border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}
.demo-feature:hover {
  border-color: var(--color-primary-light);
  box-shadow: var(--shadow-sm);
  transform: translateY(-1px);
}
.demo-feature:focus-visible {
  outline: 2px solid var(--color-primary-light);
  outline-offset: 2px;
}
.demo-feature-icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-sm);
  background: var(--color-primary-bg);
  color: var(--color-primary-hover);
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.demo-feature:hover .demo-feature-icon {
  background: var(--color-primary);
  color: #fff;
}
.demo-feature-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.demo-feature-name { font-weight: 600; font-size: .9rem; color: var(--color-text); }
.demo-feature-desc { font-size: .8rem; color: var(--color-text-muted); line-height: 1.4; margin-top: 2px; }
.demo-feature-arrow {
  flex: none;
  color: var(--color-text-faint);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.demo-feature:hover .demo-feature-arrow,
.demo-feature:focus-visible .demo-feature-arrow {
  opacity: 1;
  transform: translateX(0);
  color: var(--color-primary);
}

/* One-click demo entry on the login page. */
.login-demo { margin-top: var(--space-5); }
.login-demo-divider {
  display: flex;
  align-items: center;
  text-align: center;
  color: var(--color-text-muted);
  font-size: .8rem;
  margin-bottom: var(--space-4);
}
.login-demo-divider::before,
.login-demo-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--color-border);
}
.login-demo-divider span { padding: 0 var(--space-3); }
.login-demo-note {
  margin-top: var(--space-3);
  font-size: .8rem;
  color: var(--color-text-muted);
  text-align: center;
  line-height: 1.45;
}

/* ═══ Accounts page (grouped layout) ═══════════════════════════════════ */
.acc-summary { padding: 18px 20px; }
.acc-sum-top { display: flex; align-items: flex-start; gap: 28px; flex-wrap: wrap; margin-bottom: 14px; }
.acc-sum-lbl { display: block; font-size: .64rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--color-text-faint); }
.acc-sum-val { display: flex; align-items: center; gap: 8px; font-size: 1.15rem; font-weight: 800; letter-spacing: -.02em; margin-top: 3px; }
.acc-sum-delta { font-size: .78rem; font-weight: 600; color: var(--color-amber-fg); }
.acc-chip { font-size: .72rem; font-weight: 700; padding: 3px 10px; border-radius: 999px; }
.acc-chip.tone-green { color: var(--color-green-fg); background: rgba(22,163,74,.10); }
.acc-chip.tone-amber { color: var(--color-amber-fg); background: rgba(245,158,11,.12); }
.acc-chip.tone-red   { color: var(--color-red-fg);   background: rgba(220,38,38,.10); }

.acc-sec-title { font-size: .64rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--color-text-faint); margin: 16px 0 8px; }
.acc-sec-hint { font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--color-text-faint); }

/* Your mix: one composition bar + a quiet legend */
.acc-mixbar { display: flex; height: 14px; border-radius: 7px; overflow: hidden; background: var(--color-bg-elevated); }
.acc-mix-seg { height: 100%; }
.acc-mix-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 9px; font-size: .74rem; color: var(--color-text-muted); }
.acc-mix-key { display: inline-flex; align-items: center; gap: 6px; }
.acc-sw { width: 9px; height: 9px; border-radius: 2px; display: inline-block; flex: none; }

/* On target: per-category gauge on an absolute 0–100% of NW scale */
.acc-health { display: flex; flex-direction: column; gap: 10px; border-top: 1px solid var(--color-border-light); padding-top: 12px; }
.acc-hrow { display: grid; grid-template-columns: 116px 1fr 66px 132px; align-items: center; gap: 14px; }
.acc-hname { display: flex; align-items: center; gap: 7px; font-size: .82rem; font-weight: 600; }
.acc-gauge { position: relative; height: 9px; border-radius: 5px; background: var(--color-bg-elevated); border: 1px solid var(--color-border-light); }
.acc-gauge-fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 5px 0 0 5px; }
.acc-gauge-fill.tone-green { background: var(--color-green-fg); }
.acc-gauge-fill.tone-amber { background: var(--color-amber-fg); }
.acc-gauge-fill.tone-red   { background: var(--color-red-fg); }
.acc-gauge-tick { position: absolute; top: -3px; bottom: -3px; width: 2px; background: #334155; border-radius: 1px; }
.acc-gauge-tick::after { content: ''; position: absolute; top: -8px; left: 50%; transform: translateX(-50%); border: 3px solid transparent; border-top-color: #334155; }
.acc-hnums { font-size: .74rem; color: var(--color-text-muted); font-variant-numeric: tabular-nums; text-align: right; }
.acc-hchip { justify-self: end; }

/* Grouped table */
.acc-groups { padding: 0; overflow: visible; }
.acc-group + .acc-group .acc-ghead { border-top: 1px solid var(--color-border); }
.acc-ghead { display: flex; align-items: center; gap: 14px; padding: 12px 18px; background: var(--color-bg-elevated); border-bottom: 1px solid var(--color-border-light); }
.acc-group:first-child .acc-ghead { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
.acc-gname { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: .9rem; }
.acc-gta { font-size: .74rem; color: var(--color-text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.acc-gsub { margin-left: auto; text-align: right; white-space: nowrap; }
.acc-gsub-v { font-weight: 700; font-size: .9rem; font-variant-numeric: tabular-nums; }
.acc-gsub-c { font-size: .7rem; color: var(--color-text-faint); }

.acc-row { display: grid; grid-template-columns: minmax(0,1fr) 140px 184px 44px; align-items: center; gap: 14px; padding: 13px 18px; border-bottom: 1px solid var(--color-border-light); }
.acc-group .acc-row:last-child { border-bottom: none; }
.acc-row:hover { background: #fafbff; }
.acc-row-name { font-weight: 600; font-size: .92rem; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.acc-row-name a { color: inherit; text-decoration: none; }
.acc-row-name a:hover { color: var(--color-primary-hover); }
.acc-tag { font-size: .6rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; padding: 2px 6px; border-radius: 5px; background: #f1f5f9; color: var(--color-text-muted); border: 1px solid var(--color-border); }
.acc-row-sub { font-size: .74rem; color: var(--color-text-faint); margin-top: 3px; }
.acc-row-main { display: flex; align-items: center; gap: 10px; min-width: 0; }
.acc-row-icon { flex: none; display: inline-flex; }
.acc-row-text { min-width: 0; }
.acc-row-val { text-align: right; font-variant-numeric: tabular-nums; display: flex; flex-direction: column; align-items: flex-end; }
.acc-row-usd { font-weight: 700; font-size: .92rem; }
.acc-row-loc { font-size: .72rem; color: var(--color-text-faint); }
.acc-row-drift { display: flex; justify-content: flex-end; }
.acc-row-actions { display: flex; justify-content: flex-end; }

/* Kebab action menu — native <details> popover (no JS) */
.row-menu { position: relative; }
.row-menu-btn { list-style: none; cursor: pointer; width: 30px; height: 30px; border-radius: var(--radius-sm); display: inline-flex; align-items: center; justify-content: center; color: var(--color-text-faint); }
.row-menu-btn::-webkit-details-marker { display: none; }
.row-menu-btn:hover, .row-menu[open] .row-menu-btn { background: var(--color-bg-elevated); color: var(--color-text); }
.row-menu-pop { position: absolute; top: 34px; right: 0; z-index: 30; min-width: 150px; padding: 6px; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); box-shadow: var(--shadow-lg); display: flex; flex-direction: column; }
.row-menu-form { margin: 0; }
.row-menu-item { display: block; width: 100%; text-align: left; font: inherit; font-size: .84rem; padding: 7px 10px; border: 0; background: transparent; color: var(--color-text); border-radius: var(--radius-sm); cursor: pointer; text-decoration: none; }
.row-menu-item:hover { background: var(--color-bg-elevated); }

/* Inactive */
.acc-inactive { padding: 0; overflow: visible; }
.acc-inactive-summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 10px; padding: 14px 18px; }
.acc-inactive-summary::-webkit-details-marker { display: none; }
.acc-inactive-count { margin-left: auto; font-size: .72rem; color: var(--color-text-faint); }
.acc-inactive-chev { transition: transform var(--t-base, .2s) ease; color: var(--color-text-faint); }
.acc-inactive[open] .acc-inactive-chev { transform: rotate(180deg); }
.acc-groups-inactive .acc-row { grid-template-columns: minmax(0,1fr) auto 44px; border-bottom: none; border-top: 1px solid var(--color-border-light); opacity: .8; }

@media (max-width: 680px) {
  .acc-hrow { grid-template-columns: 1fr auto; grid-template-areas: "name chip" "gauge gauge" "nums nums"; row-gap: 5px; }
  .acc-hname { grid-area: name; } .acc-gauge { grid-area: gauge; } .acc-hnums { grid-area: nums; text-align: left; } .acc-hchip { grid-area: chip; }
  .acc-row, .acc-groups-inactive .acc-row { grid-template-columns: 1fr auto; grid-template-areas: "main actions" "val val" "drift drift"; row-gap: 8px; }
  .acc-row-main { grid-area: main; } .acc-row-val { grid-area: val; align-items: flex-start; text-align: left; flex-direction: row; gap: 8px; } .acc-row-drift { grid-area: drift; justify-content: flex-start; } .acc-row-actions { grid-area: actions; }
  .acc-ghead { flex-wrap: wrap; } .acc-gsub { margin-left: auto; }
}

/* ═══ Numbered pager (notifications inbox; reusable) ═══════════════════ */
.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: var(--space-5);
}
.pager-nums { display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap; justify-content: center; }
.pager-num, .pager-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  height: 32px;
  padding: 0 9px;
  font-size: .82rem;
  font-weight: 600;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text-muted);
  text-decoration: none;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.pager-num:hover, .pager-step:hover { background: var(--color-bg-elevated); color: var(--color-text); border-color: #cbd5e1; }
.pager-num.is-current {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
  cursor: default;
}
.pager-step.is-disabled { opacity: .45; pointer-events: none; }
.pager-gap { padding: 0 2px; color: var(--color-text-faint); user-select: none; }
.pager-total { text-align: center; margin-top: var(--space-2); font-size: .72rem; color: var(--color-text-faint); }

/* ═══ Account type icon tile (balances cards + accounts list) ══════════ */
.acct-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 8px;
  flex: none;
}
.acct-icon svg { width: 16px; height: 16px; }
.acct-icon-link { display: inline-flex; text-decoration: none; border-radius: 8px; }
.acct-icon-link:focus-visible { outline: 2px solid var(--color-primary-light); outline-offset: 2px; }
.acct-icon-link:hover .acct-icon { filter: brightness(.96); }

.acct-icon.tone-bb               { background: rgba(13, 148, 136, .14); color: #0f766e; }
.acct-icon.tone-t212             { background: rgba(37, 99, 235, .12);  color: #1e40af; }
.acct-icon.tone-btc              { background: rgba(247, 147, 26, .16); color: #b45309; }
.acct-icon.tone-equities         { background: rgba(99, 102, 241, .14); color: #4f46e5; }
.acct-icon.tone-bonds            { background: rgba(14, 165, 233, .14); color: #0284c7; }
.acct-icon.tone-cash             { background: rgba(34, 197, 94, .14);  color: #16a34a; }
.acct-icon.tone-crypto           { background: rgba(245, 158, 11, .16); color: #d97706; }
.acct-icon.tone-real_estate      { background: rgba(139, 92, 246, .14); color: #7c3aed; }
.acct-icon.tone-commodities      { background: rgba(234, 179, 8, .16);  color: #ca8a04; }
.acct-icon.tone-loans_receivable { background: rgba(20, 184, 166, .14); color: #0d9488; }
.acct-icon.tone-loans_payable    { background: rgba(239, 68, 68, .14);  color: #dc2626; }
.acct-icon.tone-other            { background: rgba(148, 163, 184, .18); color: #64748b; }

/* ═══ Login page enhancements ═════════════════════════════════════════ */
/* Honeypot: kept in the layout (not display:none) but pushed off-screen so
   bots fill it while people never see it. */
.login-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.login-caps-hint {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 8px;
  font-size: .78rem;
  font-weight: 600;
  color: var(--color-amber-fg);
}
/* An author `display` value overrides the UA's [hidden]{display:none}, so
   restore it explicitly — otherwise the hint shows even when hidden. */
.login-caps-hint[hidden] { display: none; }
.login-help {
  margin-top: var(--space-5);
  text-align: center;
  font-size: .8rem;
  color: var(--color-text-muted);
}

/* ═══ Dark theme ══════════════════════════════════════════════════════
   The front end sets data-theme on <html> before paint (system preference
   on first visit, the user's stored choice after they use the toggle), so
   these overrides drive the whole token system. The sidebar is dark in
   both modes by design; here we flip the main content surfaces + text and
   lift the semantic/brand tints for legible contrast on a dark background. */
:root { color-scheme: light; }
:root[data-theme="dark"] {
  color-scheme: dark;

  --color-primary:        #6366f1;
  --color-primary-hover:  #818cf8;
  --color-primary-light:  #a5b4fc;
  --color-primary-bg:     rgba(99, 102, 241, .18);
  --color-primary-border: rgba(99, 102, 241, .40);

  --color-green:          #22c55e;
  --color-green-fg:       #4ade80;
  --color-green-bg:       rgba(34, 197, 94, .16);
  --color-green-border:   rgba(34, 197, 94, .34);
  --color-amber:          #f59e0b;
  --color-amber-fg:       #fbbf24;
  --color-amber-bg:       rgba(245, 158, 11, .16);
  --color-amber-border:   rgba(245, 158, 11, .34);
  --color-red:            #ef4444;
  --color-red-fg:         #f87171;
  --color-red-bg:         rgba(239, 68, 68, .16);
  --color-red-border:     rgba(239, 68, 68, .34);

  --color-bg:             #0b1220;
  --color-bg-elevated:    #1e293b;
  --color-surface:        #161f2e;
  --color-border:         #2c3a4f;
  --color-border-light:   #1f2a3a;
  --color-text:           #e6e9ef;
  --color-text-muted:     #aab4c2;
  --color-text-faint:     #8593a4;

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow:    0 4px 16px -6px rgba(0, 0, 0, .55), 0 2px 4px -2px rgba(0, 0, 0, .4);
  --shadow-lg: 0 16px 40px -12px rgba(0, 0, 0, .6);
  --ring-focus: 0 0 0 2px var(--color-surface), 0 0 0 4px rgba(129, 140, 248, .5);
}

/* Targeted overrides for rules carrying hardcoded light colours. */
:root[data-theme="dark"] a.card-link:hover { border-color: var(--color-border); }
:root[data-theme="dark"] .table tr.row-overdue { background-color: rgba(245, 158, 11, .10); }
:root[data-theme="dark"] .table tr.row-overdue:hover { background-color: rgba(245, 158, 11, .16); }
:root[data-theme="dark"] .idle-lock-card,
:root[data-theme="dark"] .rec-generating-card { background: var(--color-surface); }
:root[data-theme="dark"] .acc-row:hover { background: rgba(255, 255, 255, .03); }
:root[data-theme="dark"] .acc-tag { background: var(--color-bg-elevated); }
/* The default .btn-secondary:hover border is a hardcoded light grey (#cbd5e1)
   that reads as a bright outline on dark secondary buttons (Edit, Cancel...).
   Use the theme border on hover in dark mode instead. */
:root[data-theme="dark"] .btn-secondary:hover { border-color: var(--color-border); }
:root[data-theme="dark"] .acc-gauge-tick { background: #94a3b8; }
:root[data-theme="dark"] .acc-gauge-tick::after { border-top-color: #94a3b8; }
:root[data-theme="dark"] .demo-banner {
  background: rgba(245, 158, 11, .12);
  border-color: rgba(245, 158, 11, .34);
  color: var(--color-amber-fg);
}

/* Account icon tiles: lift the glyph colour for contrast on the dark tile. */
:root[data-theme="dark"] .acct-icon.tone-bb               { color: #2dd4bf; }
:root[data-theme="dark"] .acct-icon.tone-t212             { color: #60a5fa; }
:root[data-theme="dark"] .acct-icon.tone-btc              { color: #fbbf24; }
:root[data-theme="dark"] .acct-icon.tone-equities         { color: #a5b4fc; }
:root[data-theme="dark"] .acct-icon.tone-bonds            { color: #38bdf8; }
:root[data-theme="dark"] .acct-icon.tone-cash             { color: #4ade80; }
:root[data-theme="dark"] .acct-icon.tone-crypto           { color: #fcd34d; }
:root[data-theme="dark"] .acct-icon.tone-real_estate      { color: #c4b5fd; }
:root[data-theme="dark"] .acct-icon.tone-commodities      { color: #facc15; }
:root[data-theme="dark"] .acct-icon.tone-loans_receivable { color: #2dd4bf; }
:root[data-theme="dark"] .acct-icon.tone-loans_payable    { color: #f87171; }
:root[data-theme="dark"] .acct-icon.tone-other            { color: #cbd5e1; }

/* Theme: 3-segment control (Light / Dark / System). All three are always
   visible so the current mode is self-evident; the active segment is
   highlighted off data-theme-pref (the user's choice), which the no-FOUC
   pre-paint sets before render. Base styling is token-based so the same
   control works on the light login card; the sidebar variant overrides it
   for the dark rail. */
.theme-seg {
  display: flex;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg);
}
.theme-seg-opt {
  flex: 1 1 0;
  min-width: 0;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 6px 12px;
  border: 0;
  border-radius: calc(var(--radius-sm) - 2px);
  background: none;
  color: var(--color-text-muted);
  font: inherit;
  font-size: .68rem;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.theme-seg-opt svg { width: 16px; height: 16px; opacity: .85; }
.theme-seg-opt:hover { color: var(--color-text); background: var(--color-surface); }
.theme-seg-opt:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 1px; }
/* Active segment, keyed on the chosen preference (null → System). */
:root[data-theme-pref="light"]  .theme-seg-opt[data-theme-value="light"],
:root[data-theme-pref="dark"]   .theme-seg-opt[data-theme-value="dark"],
:root[data-theme-pref="system"] .theme-seg-opt[data-theme-value="system"],
:root:not([data-theme-pref])    .theme-seg-opt[data-theme-value="system"] {
  background: var(--color-primary);
  color: #fff;
}
:root[data-theme-pref="light"]  .theme-seg-opt[data-theme-value="light"] svg,
:root[data-theme-pref="dark"]   .theme-seg-opt[data-theme-value="dark"] svg,
:root[data-theme-pref="system"] .theme-seg-opt[data-theme-value="system"] svg,
:root:not([data-theme-pref])    .theme-seg-opt[data-theme-value="system"] svg { opacity: 1; }

/* Sidebar rail (dark): blend the control in and keep it to a single slim
   icon-only row so it reads as one nav row (like Account / Sign Out) rather
   than a chunky block. Labels are dropped here; aria-label + title carry the
   names, and the highlighted segment shows the active mode at a glance. */
.sidebar .theme-seg { background: rgba(255, 255, 255, .05); border-color: transparent; margin-bottom: 1px; }
.sidebar .theme-seg-opt { flex-direction: row; gap: 0; padding: 8px 2px; color: var(--sidebar-text); }
.sidebar .theme-seg-text { display: none; }
.sidebar .theme-seg-opt:hover { color: var(--sidebar-active); background: rgba(255, 255, 255, .06); }
/* Collapsed rail is narrow: stack the three icon segments vertically. */
html.is-sidebar-collapsed .sidebar .theme-seg { flex-direction: column; }

/* Login: the page background is the dark brand rail, so the corner control
   uses the same light-on-dark treatment. Pinned top-right, clear of notches. */
.login-theme-seg {
  position: absolute;
  top: max(var(--space-4), env(safe-area-inset-top, 0px));
  right: max(var(--space-4), env(safe-area-inset-right, 0px));
  background: rgba(255, 255, 255, .06);
  border-color: rgba(255, 255, 255, .1);
}
.login-theme-seg .theme-seg-opt { color: var(--sidebar-text); }
.login-theme-seg .theme-seg-opt:hover { color: #fff; background: rgba(255, 255, 255, .08); }
/* Drop the floating corner control into normal flow above the card whenever
   the card can't guarantee clearance from the corner: narrow viewports (the
   near-full-width card reaches the top-right corner — e.g. Safari mobile
   portrait, where the later-painted card otherwise hides the toggle) and
   short / landscape viewports (the centred card leaves no top gap). Stacked,
   right-aligned, so they never collide on any phone. */
@media (max-width: 640px), (max-height: 700px) {
  .login-page { flex-direction: column; justify-content: flex-start; gap: var(--space-5); }
  .login-theme-seg { position: static; align-self: flex-end; }
}

/* Dark: rules with hardcoded light-mode text on fills → readable light tokens. */
:root[data-theme="dark"] .alert-success,
:root[data-theme="dark"] .card-flash.is-ok { color: var(--color-green-fg); }
:root[data-theme="dark"] .alert-error,
:root[data-theme="dark"] .card-flash.is-err { color: var(--color-red-fg); }
:root[data-theme="dark"] .alert-warning,
:root[data-theme="dark"] .inline-alert.is-warn,
:root[data-theme="dark"] .t212-security-note > summary { color: var(--color-amber-fg); }
:root[data-theme="dark"] .chip[class*="cat-"] { color: var(--color-text); }
:root[data-theme="dark"] .fx-strip { color: var(--color-text-muted); }
:root[data-theme="dark"] .fx-strip-sep { color: var(--color-text-faint); }

/* Sticky page header uses a translucent-white fill in light mode; give it a
   translucent-dark fill in dark mode so the (now light) title text reads. */
:root[data-theme="dark"] .page-header { background: rgba(11, 18, 32, .85); }
