/* Dark mode only, deliberately not prefers-color-scheme-driven — the app
   used to follow OS theme, but the values below (formerly the dark-mode
   override block) are now the only theme there is. color-scheme: dark
   below is what makes browser-native chrome (scrollbars, form controls)
   render dark instead of defaulting to a white scrollbar against a dark
   page. */
:root {
  color-scheme: dark;
  /* 220px used to be the default, but at that width the widest of the
     sidebar's import/export/download button labels ("Import simplified
     budget (.csv)" / "Export simplified budget (.zip)") truncated with an
     ellipsis while shorter ones on the same list didn't — visually
     inconsistent (some buttons full text, some cut off) even though every
     one of them shares the same font-size/weight. Widened instead of
     shrinking that font size, so every label renders in full at the
     default width; still user-resizable down to SIDEBAR_MIN_WIDTH via the
     existing drag handle for anyone who wants it narrower. */
  --sidebar-width: 300px;
  --bg: #16171c;
  --panel: #1e2028;
  --border: #2d2f3a;
  --text: #eaeaef;
  --text-dim: #9797a3;
  --accent: #2563eb;
  --positive: #4ade80;
  --negative: #f87171;
  --zero: #7d7d88;
  /* Net Worth chart's Assets bar only (buildNetWorthTrendChart's assetsColor)
     — a month whose net worth dropped from the previous month colors that
     month's Assets bar gold rather than reusing --negative red, which stays
     reserved for the Debts bar. A red Assets bar would read as "this is
     bad/wrong" the way an overspent category does, but one down month in a
     net worth trend is just a cautionary data point, not the same alert. */
  --gold: #eab308;
  --sidebar-bg: #0f1017;
  --sidebar-text: #c7c9d6;
  --sidebar-active: #262a3b;
}

/* Alternate palettes, switchable live via the sidebar's Theme select
   (setTheme()/applyStoredTheme() in app.js set data-theme on <html> and
   persist the choice in localStorage). Evaluation options alongside the
   base palette above, not a replacement for it — this is deliberately a
   same-dark-mode accent/hue swap, not the light/dark toggle CLAUDE.md
   already ruled out. Every value in every block below keeps roughly the
   same lightness as its base-palette counterpart (only the hue shifts), so
   existing text/background contrast ratios carry over rather than needing
   a fresh audit. Semantic colors that already mean something specific
   regardless of theme (--positive/--negative/--gold) are left unchanged in
   all of them. (An earlier version of "blue" blended toward teal/blue-
   green — replaced per user request with a purer blue, no green
   component. A "green" option and an "amber" option each existed for a
   time — both removed per user request ("did not like that one" / "not
   good"); don't reintroduce either without being asked again. "purple" was
   later removed too, per explicit request, and replaced with "sharepoint"
   below — same removal-is-safe mechanism, see THEME_OPTIONS's own comment
   in app.js.) */
:root[data-theme="blue"] {
  --bg: #0f1420;
  --panel: #172136;
  --border: #2a3b57;
  --text-dim: #93a3bd;
  --accent: #3b82f6;
  --zero: #7d8798;
  --sidebar-bg: #0a0f1c;
  --sidebar-text: #c2cbe0;
  --sidebar-active: #1b2c4a;
}

/* Deliberately neutral gray chrome (not a saturated navy tint the way
   "blue" above is) with the accent carrying all the color — matching
   Fluent UI/Microsoft 365's own dark-mode look (neutral surfaces, blue
   used only for accents/selection), and giving this theme a genuinely
   different feel from "blue" rather than just another shade of navy.
   --accent is #2564CF, Microsoft's current Fluent-era SharePoint blue
   (the older, more famous "Office blue" is #0078D4 — considered and not
   used here, per explicit request for the newer shade); --sidebar-active
   gets its own distinct blue-tinted highlight since Fluent's own selected-
   state treatment is a blue tint against otherwise-neutral chrome. */
:root[data-theme="sharepoint"] {
  --bg: #1b1b1f;
  --panel: #242429;
  --border: #38383f;
  --text-dim: #a0a0aa;
  --accent: #2564cf;
  --zero: #85858f;
  --sidebar-bg: #141417;
  --sidebar-text: #c9c9d2;
  --sidebar-active: #23324d;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
}

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

/* Full-page sign-in/sign-up gate — see js/auth-view.js. Sits alongside
   .app-shell as a sibling in budgetdork.html, not nested inside it, since
   exactly one of the two is ever shown at a time (toggled via .hidden). */
#auth-screen {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* #auth-screen (an ID selector) otherwise beats the plain .hidden class on
   specificity alone regardless of source order, the same trap CLAUDE.md
   already documents for .modal-panel label — .hidden's display: none was
   silently losing here, leaving #auth-screen rendered (and taking up a
   full extra 100vh) even after JS added the class, which is exactly what
   showed up as the auth screen sitting scrollable below the real app. */
#auth-screen.hidden { display: none; }

.auth-card {
  width: 320px;
  max-width: calc(100vw - 40px);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 28px 24px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
}

.auth-title { margin: 0 0 4px; font-size: 20px; }
.auth-subtitle { margin: 0 0 20px; font-size: 13px; color: var(--text-dim); }

.auth-form { display: flex; flex-direction: column; gap: 14px; }
.auth-form label { display: block; font-size: 12px; color: var(--text-dim); }
.auth-form input,
.auth-form textarea {
  display: block;
  width: 100%;
  margin-top: 4px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 14px;
  font-family: inherit;
  resize: vertical;
  box-sizing: border-box;
}

.auth-submit {
  background: var(--accent);
  color: white;
  border: none;
  border-radius: 6px;
  padding: 9px 10px;
  font-size: 14px;
  cursor: pointer;
}
.auth-submit:disabled { opacity: 0.6; cursor: default; }

.auth-error { margin: 14px 0 0; font-size: 13px; color: var(--negative); }

.auth-consent {
  margin: 14px 0 0;
  font-size: 12px;
  color: var(--text-dim);
  text-align: center;
}
.auth-consent a { color: var(--accent); text-decoration: none; }
.auth-consent a:hover { text-decoration: underline; }

.auth-toggle {
  display: block;
  width: 100%;
  margin-top: 16px;
  background: none;
  border: none;
  color: var(--accent);
  font-size: 13px;
  cursor: pointer;
  text-align: center;
  padding: 0;
  text-decoration: none;
}
.auth-toggle:hover { text-decoration: underline; }

/* Sidebar's own account row — see buildAccountFooter() in js/app.js. */
.sidebar-account {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  padding: 4px 2px 0;
}
.sidebar-account-email {
  font-size: 11px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sidebar-account-actions { display: flex; gap: 4px; flex-shrink: 0; }
.sidebar-logout-btn {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 12px;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 4px;
}
.sidebar-logout-btn:hover { background: var(--sidebar-active); color: var(--sidebar-text); }

/* Two-pane split — .sidebar-fixed (non-scrolling) + .sidebar-scroll (its own
   overflow-y: auto) — same fixed-frame + scroll-pane shape the Budget/
   Register/Reports views already use (see CLAUDE.md's conventions note),
   applied to the sidebar per user request so the Budget/Theme switchers and
   the Plan/Insights/Reports/Recurring-transactions nav stay visible instead
   of scrolling away once the account list below them grows long. .sidebar
   itself no longer scrolls directly (no overflow-y here) — it just lays out
   its two children top to bottom and lets .sidebar-scroll's own min-height: 0
   let it shrink to whatever's left below .sidebar-fixed's natural height. */
.sidebar {
  width: var(--sidebar-width);
  flex-shrink: 0;
  background: var(--sidebar-bg);
  color: var(--sidebar-text);
  display: flex;
  flex-direction: column;
  padding: 16px 10px;
}

.sidebar-fixed {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: 10px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border);
}

.sidebar-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Draggable divider between .sidebar and .main — see wireSidebarResizer() in
   js/app.js. A few px wide (not 1px) so it's actually easy to grab, with the
   visible line centered inside via the pseudo-element rather than the whole
   hit area being colored, which would look like a fat, out-of-place bar. */
.sidebar-resizer {
  width: 6px;
  flex-shrink: 0;
  cursor: col-resize;
  background: transparent;
  position: relative;
}
.sidebar-resizer::after {
  content: '';
  position: absolute;
  left: 2px;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--border);
}
.sidebar-resizer:hover::after,
.sidebar-resizer.dragging::after {
  background: var(--accent);
}

/* The top-left Budget switcher — visually separated from the Theme switcher
   right below it via margin-bottom (the sidebar's own flex `gap` alone reads
   as "one more nav item" otherwise). See buildBudgetSwitcher() in app.js. */
.budget-switcher {
  margin-bottom: 6px;
  padding: 0 2px;
}

/* Label sits inside .budget-switcher-row/.theme-switcher-row now (label and
   dropdown side by side in one row, per user request — not the label
   stacked above it) — shared min-width so the two rows' dropdowns line up
   into a clean two-row form despite "Budget"/"Theme" being different
   lengths. flex-shrink: 0 keeps it from being squeezed by the select next
   to it. */
.budget-switcher-label,
.theme-switcher-label {
  flex-shrink: 0;
  min-width: 46px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
}

.budget-switcher-row {
  position: relative; /* anchors .account-menu's position: absolute when the rename/delete menu is open */
  display: flex;
  align-items: center;
  gap: 6px;
}

.budget-select {
  flex: 1;
  min-width: 0;
  background: var(--panel);
  color: var(--sidebar-text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 7px 8px;
  font-size: 13px;
  cursor: pointer;
}

/* Sits right under the Budget switcher in the sidebar's fixed top section
   now (see renderSidebar()'s own comment in app.js), not trailing at the
   very bottom of the sidebar — same "label and dropdown side by side" idiom
   as .budget-switcher-row above, reused here instead of duplicated from
   scratch. */
.theme-switcher {
  padding: 0 2px;
  display: flex;
  align-items: center;
  gap: 6px;
}

.theme-select {
  flex: 1;
  min-width: 0;
  background: var(--panel);
  color: var(--sidebar-text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 7px 8px;
  font-size: 13px;
  cursor: pointer;
}

.budget-menu-btn {
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 14px;
  padding: 4px 8px;
  flex-shrink: 0;
}
.budget-menu-btn:hover { color: var(--sidebar-text); }

.menu-item:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.menu-item:disabled:hover { background: none; }

/* Separates the Budget/Theme switchers above from the Plan/Insights/Reports/
   Recurring-transactions nav below — .sidebar-fixed's own flex `gap` alone
   reads as "one more row" between them, same reasoning as .budget-switcher's
   margin-bottom above. Margin on both sides (not just one) so the line reads
   as its own breathing room rather than sitting flush against either group. */
.sidebar-nav-divider {
  border-top: 1px solid var(--border);
  margin: 8px 2px;
}

.nav-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: none;
  border: none;
  color: inherit;
  font-size: 14px;
  padding: 9px 12px;
  border-radius: 6px;
  cursor: pointer;
  text-align: left;
  width: 100%;
}

.nav-item:hover { background: var(--sidebar-active); }
.nav-item.active { background: var(--sidebar-active); font-weight: 600; }

/* Plan/Insights/Reports/Feedback/Recurring transactions — tiled per user
   request, mimicking .underfunded-btn's bordered-tile look (below) rather
   than the plain single-line .nav-item rows the rest of the sidebar still
   uses. grid-template-areas lays out the exact requested shape: Plan/
   Insights/Reports across row 1, Feedback under Plan and Recurring
   transactions double-wide under Insights+Reports in row 2 — rather than
   relying on CSS Grid's auto-placement algorithm to land 5 items in that
   specific, asymmetric arrangement. */
.nav-tile-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-areas:
    'plan insights reports'
    'feedback recurring recurring';
  gap: 6px;
}
.nav-tile-plan { grid-area: plan; }
.nav-tile-insights { grid-area: insights; }
.nav-tile-reports { grid-area: reports; }
.nav-tile-feedback { grid-area: feedback; }
.nav-tile-recurring { grid-area: recurring; }

/* Same bordered-tile look as .underfunded-btn, shrunk to fit three across a
   sidebar's width and center-aligned per user request (a plain <button>'s
   own default text-align is already center, but this is set explicitly
   rather than relied on implicitly). */
.nav-tile {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 8px;
  padding: 8px 6px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  text-align: center;
  line-height: 1.25;
}
.nav-tile:hover { background: var(--sidebar-active); color: var(--sidebar-text); }
.nav-tile.active { background: var(--sidebar-active); color: var(--sidebar-text); }

/* The import/export/download buttons are plain single-line text with no
   icon or badge (unlike other .nav-item uses that legitimately rely on
   display:flex's justify-content:space-between for multiple children), so
   a longer label (e.g. "Export simplified budget (.zip)") would
   otherwise wrap to two lines while a shorter one on the same list (e.g.
   "Import data (.csv)") stays on one — different button heights sitting
   right next to each other in the sidebar. Same overflow: hidden;
   text-overflow: ellipsis; white-space: nowrap; treatment .account-name
   already uses for the same "keep the row a single, consistent height"
   reason — kept as a safety net for anyone who drags the sidebar narrower
   than its new default (see --sidebar-width's own comment: that default
   was widened specifically so none of these five need to actually
   truncate at rest). Scoped to these five classes specifically, not the
   shared .nav-item base rule, so a different .nav-item elsewhere that does
   need to wrap or hold multiple children is unaffected.

   Also spaced out from each other and from "+ Add account" above —
   .sidebar-scroll's own `gap: 2px` (shared by every child, including the
   account-group list) reads as cramped once this group's rows are all
   forced to a single consistent line height; margin-top adds on top of
   that shared gap rather than replacing it, so the account list above
   keeps its own original spacing untouched. */
.simple-import-item,
.simple-export-item,
.example-files-item,
.import-item,
.export-item {
  margin-top: 8px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-balance { color: var(--text-dim); font-variant-numeric: tabular-nums; }

.add-account-item {
  color: var(--text-dim);
  font-size: 13px;
  margin-top: 6px;
}

.account-group { margin-top: 6px; }

.group-header {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 6px 12px 4px;
  cursor: pointer;
  text-align: left;
}
.group-header:hover { color: var(--sidebar-text); }

.chevron { font-size: 9px; width: 9px; display: inline-block; }

.group-header-total { margin-left: auto; font-variant-numeric: tabular-nums; }
.group-header-utilization { font-variant-numeric: tabular-nums; white-space: nowrap; }

.account-row {
  position: relative;
  display: flex;
  align-items: center;
  border-radius: 6px;
}
.account-row:hover { background: var(--sidebar-active); }
.account-row.active { background: var(--sidebar-active); }
.account-row.dragging { opacity: 0.4; }
.account-row.drag-over-top { box-shadow: inset 0 2px 0 var(--accent); }
.account-row.drag-over-bottom { box-shadow: inset 0 -2px 0 var(--accent); }

/* Sits outside .account-row-main (a click-to-navigate button covering the
   rest of the row) rather than inside it, so grabbing the grip doesn't also
   register as a click-to-navigate the moment a drag starts. */
.account-drag-handle { margin: 0 2px 0 12px; flex-shrink: 0; }

.account-row-main {
  flex: 1;
  min-width: 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 6px;
  background: none;
  border: none;
  color: inherit;
  font-size: 14px;
  padding: 9px 4px;
  cursor: pointer;
  text-align: left;
}
.account-row.active .account-row-main { font-weight: 600; }

.account-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.account-row.closed .account-name { color: var(--text-dim); }

/* Sidebar reminder that a Credit Card/Line of Credit/Mortgage balance is
   currently owed (see isRedBalanceType in app.js) — both the name and the
   balance figure, not just the number. A Mortgage is red essentially always,
   which is the point (a deliberate, constant reminder), not a bug. Matches
   .account-row.closed .account-name's specificity and is declared after it,
   so a (real-world-shouldn't-happen, but handled) closed+negative account
   still shows red rather than the dimmed closed color. */
.account-row .account-name.debt-negative,
.account-row .account-balance.debt-negative {
  color: var(--negative);
}

.account-menu-btn {
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 14px;
  padding: 4px 10px;
  flex-shrink: 0;
}
.account-menu-btn:hover { color: var(--sidebar-text); }

.account-menu {
  position: absolute;
  right: 6px;
  top: 100%;
  z-index: 20;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
  display: flex;
  flex-direction: column;
  min-width: 140px;
  overflow: hidden;
}

.menu-item {
  background: none;
  border: none;
  color: var(--text);
  text-align: left;
  padding: 8px 12px;
  font-size: 13px;
  cursor: pointer;
}
.menu-item:hover { background: var(--bg); }

/* Separates Close account from everything above it (Edit/Update balance/
   Redeem cash back/Log a payment) on a debt-type account's kebab menu, per
   explicit request — Close is the one destructive action in this menu, the
   rest are routine. */
.account-menu-divider { border-top: 1px solid var(--border); margin: 4px 0; }

/* The five import/export/download buttons (grouped Import-then-Export per
   format, simplified then full, plus the example-files download tucked in
   with the simplified pair — see renderSidebar() in app.js) are one group
   of secondary data-management actions, all the same dimmed/small
   treatment. margin-top: auto on simple-import-item (the first of the
   five) is what pushes the whole group down to the bottom of the sidebar —
   only the first one needs it, the rest just follow normally after it. */
.simple-import-item {
  margin-top: auto;
  color: var(--text-dim);
  font-size: 12px;
}
.simple-export-item,
.example-files-item,
.import-item,
.export-item {
  color: var(--text-dim);
  font-size: 12px;
}

.reset-item {
  color: var(--text-dim);
  font-size: 12px;
}

.main {
  flex: 1;
  padding: 24px 32px;
  overflow-x: auto;
  overflow-y: auto;
}

.view-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 20px;
}

.view-header h2 { margin: 0; font-size: 20px; }

.month-nav {
  display: flex;
  align-items: center;
  gap: 10px;
}

.month-label { font-size: 20px; font-weight: 600; min-width: 180px; text-align: center; }

/* The label is purely visual; the real <input type="month"> sits on top,
   fully transparent but still hit-testable, so clicking/tapping anywhere on
   the label opens the browser's native month picker instead of requiring
   the arrow buttons one click at a time. */
.month-picker-wrap {
  position: relative;
  display: inline-flex;
}

.month-picker-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  border: none;
  padding: 0;
}

.current-month-btn {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 12px;
  font-size: 13px;
  cursor: pointer;
  /* A direct child of .month-nav-col now (see budget-view.js's own comment
     on why it moved out of .month-nav) — that's a flex column with no
     align-items override, so it defaults to stretch and would otherwise
     grow to the column's full width (as wide as .month-scrub-row) instead
     of sizing to its own text. */
  align-self: flex-start;
}
.current-month-btn:hover { background: var(--sidebar-active); color: var(--sidebar-text); }

.month-nav-col {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.month-scrub-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  max-width: 420px;
}

.month-scrub-label { font-size: 11px; color: var(--text-dim); white-space: nowrap; }

.month-scrub-slider {
  flex: 1;
  accent-color: var(--accent);
  cursor: pointer;
}

.icon-btn {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 6px;
  width: 30px;
  height: 30px;
  cursor: pointer;
  font-size: 14px;
}
.icon-btn:hover { background: var(--sidebar-active); color: var(--sidebar-text); }
/* First real use of a disabled .icon-btn: the Insights year-nav arrows
   (.report-year-nav), which — unlike the Budget month-nav's arrows, where
   every month is a valid destination — hit a real boundary at the oldest/
   newest available year. */
.icon-btn:disabled { opacity: 0.4; cursor: default; }
.icon-btn:disabled:hover { background: var(--panel); color: var(--text); }

.header-stats {
  display: flex;
  gap: 12px;
}

.rta-badge {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  padding: 8px 16px;
  border-radius: 8px;
  background: var(--panel);
  border: 1px solid var(--border);
}
.rta-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
.rta-amount { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.rta-badge.positive .rta-amount { color: var(--positive); }
.rta-badge.negative .rta-amount { color: var(--negative); }
.rta-badge.zero .rta-amount { color: var(--zero); }

.register-stats {
  display: flex;
  gap: 12px;
}

.register-stat-box {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 7px 14px;
}

.budget-grid, .register-grid {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  min-width: 640px;
}

/* .budget-grid and .register-grid are each the bottom (scrolling) pane of
   their view's two-pane layout — see .budget-viewport/.register-viewport
   below. Each is its own scroll container now (not #main), so vertical
   scroll-position math (register-view.js's row virtualization) reads
   table.scrollTop/table.clientHeight directly, with nothing cached that
   could go stale on a window/sidebar resize (an earlier position:sticky
   version had exactly that bug). flex + min-height: 0 is what actually lets
   each *shrink* to the remaining space in its viewport and scroll its own
   (potentially huge) content instead of pushing the container to grow —
   without min-height: 0 a flex item's default min-height: auto refuses to
   shrink below its content's natural height, which for a multi-thousand-row
   account (or, for .budget-grid, a large category list) defeats the entire
   point. overflow-x is explicitly hidden (not left to default) rather than
   auto: nothing inside either box is ever actually wider than the box itself
   (rows are sized via CSS grid to fit exactly), so neither would ever show
   its own horizontal scrollbar regardless — explicit here just to be
   unambiguous. Horizontal overflow (a narrow window/sidebar) is still
   #main's job, scrolling the whole viewport so the fixed frame and the grid
   move together and stay column-aligned. */
.budget-grid, .register-grid {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  /* Reserve the vertical scrollbar's width even when the grid isn't
     overflowing, and reserve the same strip on the non-scrolling fixed frame
     above (see .budget-fixed-frame / .register-fixed-frame) — otherwise every
     column right of the flexible Memo/Available track sits a scrollbar-width
     (~15px on Windows/classic scrollbars) off between the frozen header row
     and the data rows scrolling underneath it. No effect with overlay
     scrollbars (macOS), where there's no misalignment to fix. */
  scrollbar-gutter: stable;
}

.budget-row {
  display: grid;
  grid-template-columns: 1fr 130px 130px 130px;
  align-items: center;
  border-bottom: 1px solid var(--border);
  padding: 0 16px;
  min-height: 40px;
}
.budget-row:last-child { border-bottom: none; }

.budget-col-header {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  background: var(--bg);
  min-height: 34px;
}

/* Budget view's top-level two-pane layout — the same split as the register
   view (see .register-viewport's comment for the full rationale): a static,
   non-scrolling frame (month nav/stats + column header) above a fully
   independent scrolling pane (.budget-grid) for just the category rows.
   min-width matches .budget-grid's own effective min-width so #main's
   horizontal scroll, if the window is narrower than the grid needs, moves
   the fixed frame and the grid together and keeps their columns aligned. */
.budget-viewport {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-width: 640px;
}

/* Wraps .budget-scroll-outer and .budget-side-panel side by side. height:
   100% passes #main's own definite height through unchanged so
   .budget-viewport's height: 100% (above) still resolves the same as when it
   was #main's direct child. */
.budget-layout {
  height: 100%;
  display: flex;
}

/* .budget-viewport's own horizontal scroll container — see budget-view.js's
   comment on why this exists as a separate box from #main's own overflow-x:
   without it, a long category name or a narrow window would scroll
   .budget-side-panel off to the right along with the grid, defeating the
   point of pinning it there. min-width: 0 lets it shrink below
   .budget-viewport's own min-width (640px) when the side panel's width
   doesn't leave that much room, deferring to its own scrollbar instead of
   forcing #main itself to scroll. overflow-y is explicitly hidden (not left
   to auto) since vertical scrolling is already .budget-grid's job, one level
   in — this box should never show its own vertical scrollbar. */
.budget-scroll-outer {
  flex: 1;
  min-width: 0;
  height: 100%;
  overflow-x: auto;
  overflow-y: hidden;
}

/* The Underfunded button + totals panel + trend charts, to the right of the
   grid — pinned there regardless of the grid's own horizontal scroll (see
   .budget-scroll-outer above). Width is a runtime inline style
   (budget-view.js's loadBudgetSideWidth()/wireBudgetSideResizer()), not a
   fixed value here, so .budget-side-resizer can drag it between
   BUDGET_SIDE_MIN_WIDTH/MAX_WIDTH and have it persist across reloads — same
   small-UI-preference-in-localStorage treatment as the sidebar's own width
   (see wireSidebarResizer() in app.js). flex-shrink: 0 keeps it from being
   squeezed by .budget-scroll-outer's flex: 1. Its own overflow-y: auto so a
   tall stack of charts degrades to an internal scrollbar rather than pushing
   #main's own scroll out further. */
.budget-side-panel {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
  overflow-y: auto;
}

/* Mirrors .sidebar-resizer (see its own comment) but for the boundary
   between the grid and .budget-side-panel — dragging it changes
   .budget-side-panel's width instead of --sidebar-width. Re-wired on every
   render rather than once at startup, since this element (unlike
   #sidebar-resizer) lives inside the Budget view's own torn-down-and-rebuilt
   DOM — see wireBudgetSideResizer()'s own comment in budget-view.js. */
.budget-side-resizer {
  width: 6px;
  margin: 0 10px;
  flex-shrink: 0;
  cursor: col-resize;
  background: transparent;
  position: relative;
}
.budget-side-resizer::after {
  content: '';
  position: absolute;
  left: 2px;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--border);
}
.budget-side-resizer:hover::after,
.budget-side-resizer.dragging::after {
  background: var(--accent);
}

.underfunded-btn {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 8px;
  padding: 10px 14px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.underfunded-btn:hover { background: var(--sidebar-active); color: var(--sidebar-text); }

.budget-fund-btn-row {
  display: flex;
  gap: 10px;
}
.budget-fund-btn-row .underfunded-btn { flex: 1; }

/* The bulk "prior months" pair — deliberately smaller/dimmer than the
   single-month pair above (own label, smaller text) so it doesn't read as
   equally routine; it rewrites however many past months precede the one on
   screen, not just the one currently visible. */
.budget-fund-prior-label {
  font-size: 11px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-top: 14px;
  margin-bottom: 6px;
}
.budget-fund-btn-row-prior .underfunded-btn { font-size: 12px; padding: 8px 10px; }

/* A single full-width .underfunded-btn that isn't sharing a row with
   others — the "Upcoming Expenses" button (per explicit request), and any
   future single full-width side-panel action. A <button>'s own block-level
   default already fills its container's width; margin-top is the only
   thing actually needed here, kept as its own class rather than inline so
   a second such button doesn't have to repeat it. */
.budget-full-width-btn { display: block; margin-top: 10px; }

/* The Big Purchase Planner launch link (budget-view.js) reuses .underfunded-btn's
   bordered-tile look on a plain <a>, not a <button> — needs its own
   display/text-align/text-decoration since an anchor's browser defaults
   don't match a button's (inline instead of inline-block, underlined blue
   text instead of the tile's own color). */
.budget-purchase-planner-link {
  display: block;
  text-align: center;
  text-decoration: none;
}

.budget-totals-panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
}

.budget-totals-header {
  font-size: 13px;
  font-weight: 700;
  margin-bottom: 8px;
}

.budget-totals-row {
  display: grid;
  grid-template-columns: 1fr 70px 70px;
  gap: 6px;
  align-items: center;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
.budget-totals-row:last-child { border-bottom: none; }

.budget-totals-col-header {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
}

/* Styled as its own complete panel (same recipe as .budget-grid below it)
   rather than plain page content with a line under it — margin-bottom is
   the gap to the scrolling grid box below. flex-shrink: 0 keeps it at its
   full natural content height regardless of how little space is left for
   .budget-grid to shrink into. */
.budget-fixed-frame {
  flex-shrink: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  /* Matches .budget-grid's reserved scrollbar gutter so the frozen column
     header lines up with the scrolling rows below — see that rule's comment. */
  scrollbar-gutter: stable;
  margin-bottom: 16px;
}

/* Padded wrapper for the month nav/stats — colHeader deliberately sits
   outside this, flush against the frame's own edges with only its own
   .budget-row padding, so its columns stay aligned with the data rows
   scrolling in .budget-grid underneath. */
.budget-fixed-frame-body {
  padding: 16px 16px 8px;
}

.group-row {
  font-weight: 700;
  background: var(--bg);
  font-size: 13px;
}

.col-num { text-align: right; font-variant-numeric: tabular-nums; }

.assigned-cell { cursor: text; border-radius: 4px; }
.assigned-cell:hover { background: var(--sidebar-active); color: var(--sidebar-text); }

/* Assigned falls short of this category's still-pending recurring spend for
   the month on screen (see pendingRecurringAmountForCategory() in db.js —
   the same shortfall math the Underfunded button already acts on, just
   surfaced here as a standing visual cue instead of only after clicking
   it). font-weight matches .available-cell's own negative/positive states
   so it reads as the same severity level, not a different kind of alert. */
.assigned-cell.underfunded { color: var(--negative); font-weight: 600; }

.clickable-cell { cursor: pointer; border-radius: 4px; }
.clickable-cell:hover { background: var(--sidebar-active); color: var(--sidebar-text); }

/* Drag-and-drop reordering (Budget grid group/category rows) — the handle
   is the only element that's draggable="true" (see budget-view.js's
   comment on why: it keeps drag gestures from fighting with the
   assigned-cell's click-to-edit input for text selection/focus). box-shadow
   (not border) for the drop-position indicator so it doesn't shift row
   height/layout by a pixel while dragging.
   The 6-dot grip icon is drawn entirely in CSS (one 2px dot plus 5 more via
   box-shadow, in a 2-column x 3-row grid) rather than relying on the ⠿
   braille glyph a `<span>`'s text content used to hold — that glyph's
   spacing/alignment is inconsistent across fonts (the actual bug report:
   "the 6 dots aren't lining up"), where a fixed-offset CSS grid always
   renders pixel-identical regardless of the browser's font. */
.drag-handle {
  display: inline-block;
  position: relative;
  width: 8px;
  height: 12px;
  vertical-align: middle;
  cursor: grab;
  margin-right: 10px;
  user-select: none;
}
.drag-handle::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: var(--text-dim);
  box-shadow:
    6px 0 0 var(--text-dim),
    0 5px 0 var(--text-dim),
    6px 5px 0 var(--text-dim),
    0 10px 0 var(--text-dim),
    6px 10px 0 var(--text-dim);
}
.drag-handle:active { cursor: grabbing; }
.budget-row.dragging { opacity: 0.4; }
.budget-row.drag-over-top { box-shadow: inset 0 2px 0 var(--accent); }
.budget-row.drag-over-bottom { box-shadow: inset 0 -2px 0 var(--accent); }

.assigned-input {
  width: 100%;
  text-align: right;
  border: 1px solid var(--accent);
  border-radius: 4px;
  padding: 4px 6px;
  font-variant-numeric: tabular-nums;
}

.available-cell.positive { color: var(--positive); font-weight: 600; }
.available-cell.negative { color: var(--negative); font-weight: 600; }
.available-cell.zero { color: var(--zero); }

/* Budget grid's per-row selection checkbox, right after the drag-handle grip
   in .col-category — feeds the side panel's "Underfunded (Selected)"/
   "Overfunded (Selected)" buttons (see budget-view.js). Sized/spaced to sit
   inline with the drag-handle and category name rather than stretching to
   the row's default line-height. */
.row-select-checkbox {
  margin: 0 8px 0 0;
  vertical-align: middle;
  cursor: pointer;
}

/* Disabled state for the side panel's fund/unfund buttons — used by the
   "Selected categories" pair while nothing is checked, so it reads as
   unavailable at a glance rather than just quietly no-op-ing on click. */
.underfunded-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.underfunded-btn:disabled:hover { background: var(--panel); color: var(--text); }

.register-row {
  display: grid;
  grid-template-columns: 28px 110px 150px 150px 1fr 130px 95px 95px 105px 60px 150px;
  align-items: center;
  gap: 6px;
  border-bottom: 1px solid var(--border);
  padding: 6px 16px;
  font-size: 13px;
}
.register-row:last-child { border-bottom: none; }

/* A transaction dated after today — still listed, but greyed and deliberately
   left out of every balance/budget total until its date arrives (see
   isFutureTxn() in db.js). The Balance cell shows "—" rather than a figure,
   since it hasn't moved the running balance. Selection/edit/delete still work
   normally on these rows. */
.register-row.future-txn { color: var(--text-dim); }
.register-row.future-txn .col-num.outflow,
.register-row.future-txn .col-num.inflow { opacity: 0.6; }

/* Memo is the row's one flexible (1fr) track, so it's the column that
   absorbs whatever space every fixed-width column doesn't need — but with
   no floor, that meant it could shrink to a near-unreadable sliver (~35px,
   text wrapping mid-word) well before the window felt "narrow" at all.
   Confirmed via headless-browser testing at real laptop resolutions
   (1366/1440/1536px, default 300px sidebar): the fixed columns alone
   already consume nearly the full row width at those completely ordinary
   sizes, starving Memo down to its bare min-content floor — this is very
   likely what actually read as "columns get messed up," independent of
   deliberately narrowing the window. min-width here guarantees Memo never
   gets smaller than a couple of short words before wrapping; every
   min-width below (this rule and the two @media blocks) accounts for it. */
.col-memo { min-width: 90px; }

.col-select { text-align: center; }

/* The register view's top-level two-pane layout: a static, non-scrolling
   frame (.register-fixed-frame) above a fully independent scrolling pane
   (.register-grid). A flex column, not position: sticky — an earlier sticky
   version worked at rest but had a real bug: its scroll-position math was
   computed once via getBoundingClientRect() and cached, so resizing the
   window (or dragging the sidebar divider) without a full rerender left
   that cached geometry stale, which showed up as things moving oddly at
   small sizes. A true split pane has no such cache to go stale — .register-
   grid below reads its own live scrollTop/clientHeight every time. height:
   100% fills #main's available height (#main has a definite height via its
   own flex: 1 in .app-shell, so this resolves correctly); min-width matches
   .register-grid's own effective min-width (see below) so #main's
   horizontal scroll, if the window is narrower than the grid needs, moves
   the fixed frame and the grid together and keeps their columns aligned.
   1255px (up from a stale 1078px) — .register-row's own fixed-width
   columns plus gaps/padding plus .col-memo's own min-width above already
   add up to just about this much; 1078px undersold it by ~180px, which is
   exactly what let Memo silently squeeze to its bare min-content floor at
   completely ordinary screen widths instead of #main's horizontal scroll
   ever kicking in the way this comment already claimed it would. */
.register-viewport {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-width: 1255px;
}

/* Progressive column hiding, so a too-narrow window loses the two
   least-essential columns (Tags, then Memo) instead of Memo silently
   shrinking to its min-width floor above (or #main's horizontal
   scrollbar being the only fallback). This is the app's first real
   desktop-width CSS @media query — deliberately a different mechanism
   from the mobile shell's JS-driven matchMedia/isMobileLayout() switch
   (js/mobile-shell.js), which swaps in a wholesale different UI at
   <=700px; these two breakpoints stay entirely within the existing
   desktop register DOM/JS, just hiding one column div at a time via the
   shared .col-tags/.col-memo class every row variant (header, plain row,
   add-row, edit-row) already renders with. Hiding a column only removes a
   grid-template-columns track and a min-width allowance — row height (and
   therefore the virtualization/scroll-spacer math in js/register-view.js)
   is untouched either way.

   Breakpoints are viewport-width-based (not #main's own width — CSS has
   no container query hook wired up here, and adding one would be a much
   bigger change than this warrants), calibrated against the *default*
   300px --sidebar-width plus .main's own padding (~370px of chrome
   between the browser viewport and the register's actual available
   width, confirmed empirically). A much wider dragged sidebar eats into
   that assumption and can make the real squeeze point arrive a bit
   earlier than these thresholds predict — an accepted imprecision, not a
   promise these numbers are exact for every sidebar width.

   Placed deliberately AFTER the base .register-viewport rule above, not
   before it — a real bug caught in testing: an earlier draft had these
   min-width overrides declared *before* the unconditional base rule, and
   since both are the same selector/specificity, source order alone
   decides the winner — the later, unconditional 1255px rule silently won
   at every width, making these two @media blocks' min-width completely
   inert regardless of viewport. */
@media (max-width: 1600px) {
  .register-row {
    grid-template-columns: 28px 110px 150px 150px 1fr 95px 95px 105px 60px 150px;
  }
  .col-tags { display: none; }
  .register-viewport { min-width: 1119px; }
}

@media (max-width: 1480px) {
  .register-row {
    grid-template-columns: 28px 110px 150px 150px 95px 95px 105px 60px 150px;
  }
  .col-memo { display: none; }
  .register-viewport { min-width: 1023px; }
}

/* Styled as its own complete panel (same background/border/radius recipe as
   .register-grid below it) rather than plain page content with a line under
   it — margin-bottom is the gap to the scrolling grid box below, standing
   in for what used to be one continuous bordered box when the header lived
   inside .register-grid. flex-shrink: 0 keeps it at its full natural
   content height regardless of how little space is left for .register-grid
   to shrink into. */
.register-fixed-frame {
  flex-shrink: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  /* Unlike the earlier position:sticky version, nothing in here is sticky
     anymore, so there's no conflict clipping to the rounded corners this
     way (see .register-grid's own comment for what that conflict was) —
     this clips whichever row (colHeaderRow, or addRow when the account is
     open) happens to render last against the bottom corners automatically,
     without needing to chase down which one that is with a selector. */
  overflow: hidden;
  /* Matches .register-grid's reserved scrollbar gutter so the frozen column
     header + add-row line up with the scrolling data rows below — see that
     rule's comment for the full rationale. */
  scrollbar-gutter: stable;
  margin-bottom: 16px;
}

/* Padded wrapper for the non-grid-aligned content (account name/stats,
   credit line, closed banner, filter bar) — colHeaderRow/addRow deliberately
   sit outside this, flush against the frame's own edges with only their own
   .register-row padding, so their columns stay aligned with the data rows
   scrolling in .register-grid underneath (see the JS comment where this is
   built). */
.register-fixed-frame-body {
  padding: 12px 16px 2px;
}

/* .view-header's 20px margin-bottom is tuned for Budget/Reports; the
   register's fixed frame is denser and doesn't need that much air below
   the name/stats row, so it's overridden here rather than changed
   globally. */
.register-fixed-frame .view-header {
  margin-bottom: 10px;
}

.register-col-header {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  background: var(--bg);
}

.add-row { background: var(--bg); }

.register-row input, .register-row select {
  width: 100%;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 4px;
  padding: 4px 6px;
  font-size: 13px;
}

/* The register's add-row/edit-row .col-category cell can hold two stacked
   selects — the primary category-or-transfer picker, plus .f-transfer-category
   (shown only for a Cash -> non-Cash transfer, see canCategorizeTransferLeg in
   db.js) for optionally categorizing that transfer. A single-child cell
   (column header, or a plain data row's category label) is unaffected by the
   column layout. Scoped to .register-row specifically — .col-category is
   also used by the Budget grid's category cell (drag-handle + name, meant to
   stay inline on one line), and this flex-column rule leaking into that
   unrelated reuse of the same class name was a real bug: it turned the
   drag-handle span and the category name into separate stacked flex items,
   rendering the handle above the name instead of beside it. */
.register-row .col-category { display: flex; flex-direction: column; gap: 4px; }

/* A plain (non-editing) register row's Payee/Category cell gets this when
   that field is missing but expected — a non-transfer transaction with no
   payee typed, or no category chosen (transfers are exempt: no payee is
   normal for one, and an uncategorized transfer is a deliberate, common
   choice — see canCategorizeTransferLeg in db.js — so neither is flagged).
   Gold, not --negative red, matching this app's existing "worth a second
   look, not a financial error" use of gold elsewhere (the Net Worth chart's
   down-month bar) — this is a reminder to go fill something in, nothing is
   actually wrong with the transaction as saved. */
.register-row .cell-flag {
  /* .register-row uses align-items: center, so a cell is sized to its own
     content, not stretched to the row's height — and an empty payee/category
     cell (blank payee, or no label text at all for an uncategorized txn) has
     no text content and therefore no line box, collapsing to 0px tall. That
     left the background/box-shadow below painting onto a zero-height box —
     applied, but genuinely invisible. min-height is what actually makes this
     visible for the common case (nothing typed at all), not just for a
     non-empty-but-still-"missing" value. */
  min-height: 1.3em;
  background: rgba(234, 179, 8, 0.28);
  box-shadow: inset 0 0 0 1px rgba(234, 179, 8, 0.75);
  border-radius: 4px;
}

.col-num.outflow { color: var(--negative); }
.col-num.inflow { color: var(--positive); }

.col-check { text-align: center; }
.col-actions { text-align: center; display: flex; gap: 4px; justify-content: center; }

.btn-add, .btn-save {
  background: var(--accent);
  color: white;
  border: none;
  border-radius: 4px;
  padding: 5px 10px;
  cursor: pointer;
  font-size: 12px;
}

.btn-cancel {
  background: none;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 4px;
  padding: 5px 10px;
  cursor: pointer;
  font-size: 12px;
}

.btn-delete {
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 16px;
}
.btn-delete:hover { color: var(--negative); }

.edit-row { background: var(--bg); }

.closed-tag {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 6px;
  vertical-align: middle;
}

.closed-banner {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 14px;
  font-size: 13px;
  color: var(--text-dim);
  margin-bottom: 16px;
}

.register-search-filter {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.register-search-filter .rf-column {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 6px;
  padding: 5px 8px;
  font-size: 13px;
}
.register-search-filter .rf-search-wrap {
  position: relative;
  flex: 0 1 260px;
  display: flex;
}
.register-search-filter .rf-search {
  width: 100%;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 6px;
  padding: 5px 24px 5px 8px;
  font-size: 13px;
}
.register-search-filter .rf-clear {
  position: absolute;
  right: 2px;
  top: 50%;
  transform: translateY(-50%);
  border: none;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  padding: 4px 6px;
}
.register-search-filter .rf-clear:hover { color: var(--text); }

/* The credit-card spending-target summary (js/register-view.js), on the
   right side of the same row as the search box — margin-left: auto is what
   pushes it there, since .register-search-filter itself is a plain flex row
   with no justify-content: space-between (adding that would also spread the
   column select away from the search box, which isn't wanted). */
.rf-spend-target {
  margin-left: auto;
  font-size: 13px;
  color: var(--text-dim);
  white-space: nowrap;
  cursor: pointer;
}
.rf-spend-target:hover { color: var(--text); }

/* The register's bulk Category/Cleared/Memo/Tags toolbar (js/register-view.js)
   — only rendered when at least one row is selected. A compact wrap-capable
   row of four independent mini-controls (each its own input/select + its own
   apply button) rather than one shared "value + apply" pair, since each
   field applies via its own distinct db.js call. */
.register-bulk-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin-bottom: 8px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  font-size: 12.5px;
}
.register-bulk-bar .bulk-count {
  font-weight: 600;
  white-space: nowrap;
}
.register-bulk-bar .bulk-clear {
  background: none;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 4px 8px;
  cursor: pointer;
  font-size: 12px;
}
.register-bulk-bar .bulk-group {
  display: flex;
  align-items: center;
  gap: 6px;
}
.register-bulk-bar select,
.register-bulk-bar input[type="text"] {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 6px;
  padding: 4px 7px;
  font-size: 12.5px;
}
.register-bulk-bar input[type="text"] {
  width: 150px;
}
.register-bulk-bar button.bulk-apply {
  background: var(--accent);
  color: white;
  border: none;
  border-radius: 6px;
  padding: 4px 9px;
  cursor: pointer;
  font-size: 12px;
}
.register-bulk-bar .bulk-note {
  color: var(--gold);
  font-size: 12px;
}

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.modal-panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
  width: 340px;
  max-width: calc(100vw - 40px);
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
}

.modal-panel h3 { margin: 0 0 12px; font-size: 16px; }

/* Pinned (non-blocking, movable) popups — see js/modal.js's openModal opts.pinned.
   No dimmed backdrop, and pointer-events: none lets clicks/scroll reach the
   main view underneath; the panel itself opts back into pointer-events so
   it stays interactive. */
.modal-overlay-pinned {
  background: transparent;
  pointer-events: none;
}
.modal-panel-pinned {
  position: fixed;
  /* Opens centered on screen, same as a blocking modal — top/left 50% plus
     a transform (rather than a hardcoded corner) since the panel's own
     height is content-dependent, not knowable in plain CSS. modal.js's
     wirePinnedModalDrag() swaps this for explicit pixel left/top (and
     clears the transform) the moment a drag starts or a remembered
     position is restored, so this rule only ever governs the very first,
     never-yet-moved render of a pinned popup. */
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: auto;
  /* No dimmed backdrop behind it anymore, so it needs a stronger shadow of
     its own to still read as "floating above" the main view. */
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.55);
}
/* Higher specificity than the plain ".modal-panel h3" rule above (element +
   class beats class + element), so this isn't at the mercy of source order. */
.modal-panel h3.modal-drag-handle {
  cursor: move;
  user-select: none;
  margin: -20px -20px 12px;
  padding: 12px 20px;
  background: var(--sidebar-bg);
  border-bottom: 1px solid var(--border);
  border-radius: 9px 9px 0 0;
}
.modal-panel p { margin: 0 0 14px; font-size: 13px; color: var(--text-dim); line-height: 1.5; }

.form-hint { margin-top: -4px; }
.form-error { color: var(--negative) !important; }
.form-success { color: var(--positive) !important; font-size: 13px; margin: 0 0 14px; }
.hidden { display: none; }

/* Account Settings modal (js/account-modal.js) — three stacked sections
   (password / email / delete) in one modal, each visually separated with
   its own top border rather than three separate modals, since they're all
   small and closely related. */
.account-modal-email { font-size: 13px; }
.account-modal-section {
  padding-top: 16px;
  margin-top: 16px;
  border-top: 1px solid var(--border);
}
.account-modal-section h4 { margin: 0 0 10px; font-size: 14px; }
.account-modal-danger p { color: var(--text-dim); }
.account-modal-danger .form-hint { color: var(--text-dim); font-size: 12px; }

.modal-panel label {
  display: block;
  font-size: 12px;
  color: var(--text-dim);
  margin-bottom: 10px;
}
/* .modal-panel label's specificity otherwise beats the generic .hidden
   utility, so a hidden <label> (e.g. the credit-limit field, or the "New
   group name" field in the inline category modal) would stay visible. */
.modal-panel label.hidden { display: none; }

.modal-panel input,
.modal-panel select,
.modal-panel textarea {
  display: block;
  width: 100%;
  margin-top: 4px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 4px;
  padding: 6px 8px;
  font-size: 13px;
  font-family: inherit;
  box-sizing: border-box;
}
.modal-panel textarea { resize: vertical; }

/* The Feedback modal's live character count under its textarea — right-
   aligned rather than left like every other .form-hint, so it reads as a
   counter sitting under the field's own right edge rather than body text. */
.fb-char-count { text-align: right; }

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 6px;
}

.btn-danger {
  background: var(--negative);
  color: white;
  border: none;
  border-radius: 4px;
  padding: 6px 12px;
  cursor: pointer;
  font-size: 13px;
}

.manage-categories-btn {
  align-self: center;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 6px;
  padding: 0 14px;
  height: 30px;
  cursor: pointer;
  font-size: 13px;
}
.manage-categories-btn:hover { background: var(--sidebar-active); color: var(--sidebar-text); }

.modal-panel:has(.manage-categories) { width: 560px; }

.category-manage-list { max-height: 50vh; overflow-y: auto; margin-bottom: 14px; }

.category-manage-group { margin-bottom: 16px; }
.category-manage-group-header .cmg-name { font-weight: 600; margin-top: 0; }

.category-manage-cats { margin-top: 6px; }

.category-manage-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
}
.category-manage-row input[type="text"] { flex: 1; width: auto; margin-top: 0; }
.category-manage-row select { width: 140px; flex-shrink: 0; margin-top: 0; }
.category-manage-row.is-hidden .cmc-name { color: var(--text-dim); }

.category-manage-row button,
.category-manage-add-row button,
.category-manage-add-group-row button {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 4px;
  padding: 4px 10px;
  cursor: pointer;
  font-size: 12px;
  white-space: nowrap;
}

.category-manage-add-row,
.category-manage-add-group-row {
  display: flex;
  gap: 8px;
  margin-top: 6px;
}
.category-manage-add-row input,
.category-manage-add-group-row input { flex: 1; width: auto; margin-top: 0; }
.category-manage-add-group-row { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }

.register-grid { min-width: 1050px; }

.credit-utilization { font-size: 12px; color: var(--text-dim); margin: -6px 0 8px; }
/* Utilization thresholds: <10% healthy, 10-30% worth watching, >30% pulses
   red rather than just sitting there statically — a static color alone is
   easy to miss on a header line you're not directly looking at. --gold gets
   a font-weight bump here since it's rendered small (12px) against a dark
   background and needs to read clearly as "caution," not blend into the
   surrounding --text-dim text. */
.util-value.util-low { color: var(--positive); font-weight: 600; }
.util-value.util-medium { color: var(--gold); font-weight: 700; }
.util-value.util-high { color: var(--negative); font-weight: 700; animation: util-pulse 1.6s ease-in-out infinite; }

@keyframes util-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

/* Clickable — see openUpcomingRegisterPopup() in js/register-view.js — since
   the inline preview below only ever spells out a handful of items before
   collapsing the rest into "+N more" text with no way to reach them
   otherwise. The whole line is the click target (not just "+N more") so a
   short list is just as reachable as a long one, and overflow: hidden +
   text-overflow: ellipsis on one line keeps a run of long payee names from
   wrapping the fixed, non-scrolling frame taller than it should be. */
.upcoming-recurring {
  font-size: 12px;
  color: var(--text-dim);
  margin: -6px 0 8px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.upcoming-recurring:hover { color: var(--text); }
.upcoming-recurring:hover .upcoming-recurring-label { text-decoration: underline; }
.upcoming-recurring-label { color: var(--text-dim); }
.upcoming-recurring-item { color: var(--text); }
.upcoming-recurring-item .negative { color: var(--negative); }
.upcoming-recurring-item .positive { color: var(--positive); }
.upcoming-recurring-more { color: var(--accent); }
.upcoming-recurring-sep { margin: 0 6px; color: var(--border); }

.btn-split {
  background: none;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 4px;
  padding: 5px 10px;
  cursor: pointer;
  font-size: 12px;
}
.btn-split:hover { background: var(--sidebar-active); color: var(--sidebar-text); }

/* 480px clipped long category names in the per-line <select> (real-world
   category names run longer than the seed/test data) — widened to match the
   other "wider" modals (.manage-categories/.manage-recurring are also 560px)
   rather than inventing a one-off value. */
.modal-panel:has(.split-lines) { width: 560px; }

.split-lines { margin-top: 4px; }

.split-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}
.split-line select { flex: 1; width: auto; margin-top: 0; }
.split-line input[type="number"] { width: 110px; flex-shrink: 0; margin-top: 0; }
.split-line .sl-remove {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text-dim);
  border-radius: 4px;
  width: 26px;
  height: 26px;
  cursor: pointer;
  flex-shrink: 0;
}

.sp-add-line {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 4px;
  padding: 4px 10px;
  cursor: pointer;
  font-size: 12px;
  margin-bottom: 10px;
}

.split-total { font-weight: 600; font-size: 13px; margin: 0 0 10px; }

.sp-cleared-label { display: flex; align-items: center; gap: 6px; }
.sp-cleared-label input { width: auto; margin-top: 0; }

.modal-panel:has(.manage-recurring) { width: 560px; }
.modal-panel:has(.recurring-template-form) { width: 480px; }
.modal-panel:has(.manage-spend-targets) { width: 560px; }
.modal-panel:has(.spend-target-form) { width: 480px; }

/* The spending-target form's category checklist (js/spend-target-modal.js)
   — a plain scrollable box of checkboxes grouped by category group, same
   idea as .category-manage-list above but checkboxes instead of editable
   rows, since this is a one-time "which categories count" pick, not a
   management UI. */
.st-field-label { font-size: 12px; color: var(--text-dim); margin-bottom: 4px; }
.st-category-list {
  max-height: 200px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  margin-bottom: 14px;
}
.st-category-group-label { font-weight: 600; font-size: 12px; margin: 8px 0 4px; }
.st-category-group-label:first-child { margin-top: 0; }
/* .modal-panel label's own display:block/margin-bottom:10px (higher
   specificity than a bare .st-category-row class rule — same trap
   .modal-panel label.hidden documents above) needs an equally-qualified
   override here, not a plain class selector. */
.modal-panel label.st-category-row { display: flex; align-items: center; gap: 6px; font-size: 13px; padding: 2px 0; margin-bottom: 0; }
.st-category-row input { width: auto; margin-top: 0; }

.recurring-sort-row { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.recurring-sort-label { font-size: 12px; color: var(--text-dim); }
.recurring-sort-btn {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 4px;
  padding: 4px 10px;
  cursor: pointer;
  font-size: 12px;
}
.recurring-sort-btn.active { background: var(--sidebar-active); color: var(--sidebar-text); border-color: var(--accent); font-weight: 600; }

/* Divider between account (or calendar-month) groups in the sorted/grouped
   recurring list — same uppercase/dim/letter-spaced idiom as the sidebar's
   own .group-header, with a bottom border standing in for that version's
   collapse chevron since this one is never interactive. Label on the left,
   that group's expected total (sum of its own templates' signed amounts,
   sign-colored the same red/green convention as everywhere else in this
   app) on the right, per user request. */
.recurring-group-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  font-size: 11px;
  color: var(--text-dim);
  padding: 10px 0 4px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 2px;
}
.recurring-group-header:first-child { padding-top: 0; }
.recurring-group-header-label { text-transform: uppercase; letter-spacing: 0.05em; }
.recurring-group-header-total { font-variant-numeric: tabular-nums; font-weight: 600; }
.recurring-group-header-total.positive { color: var(--positive); }
.recurring-group-header-total.negative { color: var(--negative); }

.recurring-list { max-height: 50vh; overflow-y: auto; margin-bottom: 14px; }

.recurring-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.recurring-row:last-child { border-bottom: none; }
.recurring-row.is-paused, .recurring-row.is-ended { opacity: 0.55; }

.recurring-row-title { font-size: 13px; font-weight: 600; }
.recurring-row-detail { font-size: 12px; color: var(--text-dim); margin-top: 2px; }

.recurring-row-actions { display: flex; gap: 6px; flex-shrink: 0; }
.recurring-row-actions button,
.recurring-add-btn {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 4px;
  padding: 4px 10px;
  cursor: pointer;
  font-size: 12px;
  white-space: nowrap;
}

.recurring-add-btn { margin-top: 4px; }

.rt-frequency-row { display: flex; gap: 8px; margin-top: 4px; }
.rt-frequency-row input { width: 70px; flex-shrink: 0; margin-top: 0; }
.rt-frequency-row select { flex: 1; margin-top: 0; }

.rt-amount-row { display: flex; gap: 8px; }
.rt-amount-row label { flex: 1; margin-bottom: 10px; }

.rt-checkbox-label { display: flex; align-items: center; gap: 6px; }
.rt-checkbox-label input { width: auto; margin-top: 0; }

.autocomplete-dropdown {
  position: absolute;
  z-index: 200;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  max-height: 220px;
  overflow-y: auto;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
}

.autocomplete-item {
  padding: 6px 10px;
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.autocomplete-item:hover,
.autocomplete-item.active {
  background: var(--sidebar-active);
  color: var(--sidebar-text);
}

.modal-panel:has(.debt-payment-form) { width: 480px; }

.dp-field-label { font-size: 12px; color: var(--text-dim); margin-bottom: 4px; }

.dp-expense-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.dp-expense-line input { width: 110px; flex-shrink: 0; margin-top: 0; }
.dp-expense-line select { flex: 1; margin-top: 0; }

/* Principal's amount is computed (Total payment minus everything else),
   never typed in directly — dimmed so it visually reads as "result," not
   "field you filled in," the same distinction cursor:default communicates. */
.dp-expense-line input[readonly] { color: var(--text-dim); cursor: default; }

/* ---- Budget grid's Activity-cell popup ------------------------------ */

/* User-resizable (native CSS resize, bottom-right handle) — overrides the
   base .modal-panel's overflow-y: auto with overflow: auto on both axes,
   since resize requires the axis being resized to not compute to visible.
   Explicit width/height (rather than the base rule's content-driven sizing)
   gives the resize handle somewhere to start from; the min/max width/height
   bounds keep it legible on the small end and never past the viewport on
   the large end, same ceiling the base .modal-panel rule already uses.
   display: flex here (not on .activity-popup, the content div one level
   in) is what lets .activity-popup-list's flex: 1 below actually expand to
   fill whatever height the user drags the panel to. */
.modal-panel:has(.activity-popup) {
  width: 560px;
  height: 460px;
  min-width: 420px;
  min-height: 260px;
  max-width: calc(100vw - 40px);
  max-height: calc(100vh - 40px);
  resize: both;
  overflow: auto;
  display: flex;
  flex-direction: column;
}

.activity-popup {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}
.activity-popup > h3,
.activity-popup-header,
.activity-popup-empty,
.activity-popup-total,
.activity-popup-footer {
  flex-shrink: 0;
}

.activity-popup-row {
  display: grid;
  grid-template-columns: 90px 1fr 1fr 90px;
  gap: 8px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
.activity-popup-row:last-child { border-bottom: none; }

/* A still-pending recurring occurrence, not yet a real posted transaction
   (see openActivityPopup()'s own comment in budget-view.js) — dimmed so it
   visually reads as "expected," the same distinction .dp-expense-line
   input[readonly] draws for a computed-not-typed value elsewhere in this
   file. */
.activity-popup-row-future { color: var(--text-dim); }

/* openUpcomingExpensesPopup() (budget-view.js) reuses .activity-popup-row's
   whole 4-column shape when only one category is checked (Date/Account/
   Payee/Amount, identical to the single-category Activity popup above) —
   this modifier widens it to 5 for the multi-category case, which needs a
   Category column the single-category popup never does. */
.activity-popup-row.upcoming-expenses-row-5col { grid-template-columns: 90px 1fr 1fr 1fr 90px; }

/* openUpcomingRegisterPopup() (js/register-view.js) — the register header's
   "Upcoming this month" popup. No Account column (every row is necessarily
   this one account), so 3 columns instead of the base rule's 4. */
.activity-popup-row.upcoming-register-row-3col { grid-template-columns: 90px 1fr 90px; }

.activity-popup-header {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  background: var(--bg);
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: 6px 6px 0 0;
}

/* flex: 1 (not a fixed pixel cap) so dragging the panel taller (see the
   resize rule above) actually reveals more rows instead of just adding
   blank space below a capped list; still its own overflow-y: auto scroll
   container for whatever doesn't fit. activity-popup-header (above) stays
   outside this so it doesn't scroll away with the rows. */
.activity-popup-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-top: none;
  border-radius: 0 0 6px 6px;
}

.activity-popup-empty {
  color: var(--text-dim);
  font-size: 13px;
  margin: 4px 0 0;
}

/* Bottom-right summary of just the dimmed future rows — see
   openActivityPopup()'s own comment on why this is a running total rather
   than making the reader add up the greyed-out rows by hand. Sign-colored
   the same restrained "only the summary figure gets tone" convention as
   .available-cell/.networth-table-row elsewhere in this file. */
.activity-popup-total {
  text-align: right;
  font-size: 13px;
  margin-top: 10px;
}
.activity-popup-total .positive { color: var(--positive); font-weight: 600; }
.activity-popup-total .negative { color: var(--negative); font-weight: 600; }
.activity-popup-total .zero { color: var(--zero); }

.activity-popup-footer {
  display: flex;
  justify-content: flex-start;
  gap: 8px;
  margin-top: 14px;
}

.activity-popup-close-btn {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 14px;
  font-size: 13px;
  cursor: pointer;
}
.activity-popup-close-btn:hover { background: var(--sidebar-active); color: var(--sidebar-text); }

/* ---- Reports screen ------------------------------------------------ */

.report-filter-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 20px;
}

.report-year-nav { display: flex; gap: 4px; }

/* Insights' top-level two-pane layout — the same split as Budget/Register
   (see .register-viewport's comment for the full rationale): a static,
   non-scrolling frame (title + filter row) above a fully independent
   scrolling pane (.reports-scroll-pane) for the report sections. Unlike
   .budget-fixed-frame/.register-fixed-frame, this frame is NOT overflow:
   hidden with an inner padded body — there's no grid-column alignment
   between this frame and the content below it to preserve (report sections
   aren't a column grid the way budget/register rows are), and one of the
   filter row's popovers (.filter-popover-panel) is position: absolute and
   expects to be able to render past the frame's own bottom edge. Padding is
   applied directly on the frame instead. */
.reports-viewport {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.reports-fixed-frame {
  flex-shrink: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px 16px 12px;
  margin-bottom: 16px;
}

.reports-fixed-frame .view-header {
  margin-bottom: 12px;
}

/* The filter row's own 20px margin-bottom (above) is meant for when it sits
   directly above page content; here that gap is already the fixed frame's
   own bottom padding, so it's zeroed out to avoid doubling up. */
.reports-fixed-frame .report-filter-row {
  margin-bottom: 0;
}

.reports-scroll-pane {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.report-preset-select,
.report-month-input {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
}

.report-range-sep { color: var(--text-dim); font-size: 13px; }

.filter-popover-wrap { position: relative; }

.filter-popover-btn {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 13px;
  cursor: pointer;
}
.filter-popover-btn:hover { background: var(--sidebar-active); color: var(--sidebar-text); }

.filter-popover-panel {
  position: absolute;
  left: 0;
  top: 100%;
  z-index: 30;
  margin-top: 4px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
  min-width: 220px;
  max-width: 320px;
  padding: 8px;
}

.filter-popover-actions {
  display: flex;
  gap: 6px;
  margin-bottom: 6px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}
.filter-popover-actions button {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 4px;
  padding: 3px 8px;
  font-size: 11px;
  cursor: pointer;
}

.filter-popover-list { max-height: 260px; overflow-y: auto; }

.filter-popover-row,
.filter-popover-group-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 2px;
  font-size: 13px;
  cursor: pointer;
}
.filter-popover-row input,
.filter-popover-group-row input { width: auto; margin: 0; }
.filter-popover-group-row { font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-dim); margin-top: 6px; }
.filter-popover-group-row:first-child { margin-top: 0; }
.filter-popover-row-nested { padding-left: 18px; }

.report-sections {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.report-section {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px 20px;
}
.report-section h3 { margin: 0 0 12px; font-size: 15px; }

/* Title + a corner action button (e.g. Income vs Expense's "Export table
   (.csv)") on one row — the h3's own margin-bottom above is zeroed out here
   so it doesn't also stack with this row's, since the row itself now owns
   that spacing. */
.report-section-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}
.report-section-header-row h3 { margin: 0; }

/* A nested sub-panel inside a .report-section — same recipe (border/
   radius/padding) but on --bg instead of --panel, so it reads as visibly
   distinct from its parent rather than just more content in the same box.
   First use: the Net Worth section's Monte Carlo projection, directly
   below the month-to-month chart per explicit request for a "separate
   sub-window." */
.report-subsection {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px 16px;
}
.report-subsection h4 { margin: 0 0 10px; font-size: 13px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; }

/* Label-left/control-right pairs in one row, same idiom as the Budget
   switcher/Theme switcher — used by the Monte Carlo projection's Months
   ahead/Method/Confidence range controls. */
.report-subsection-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-bottom: 12px;
}
.report-subsection-control { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-dim); }
.report-subsection-control input,
.report-subsection-control select {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 8px;
  font-size: 12px;
}
.report-subsection-control input[type="number"] { width: 64px; }

.report-section-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.stat-tile { display: flex; flex-direction: column; gap: 2px; }
.stat-tile-row { display: flex; flex-wrap: wrap; gap: 24px; }
.stat-tile-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); }
.stat-tile-value { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-tile-value.tone-positive { color: var(--positive); }
.stat-tile-value.tone-negative { color: var(--negative); }
.stat-tile-sub { font-size: 12px; color: var(--text-dim); }

.chart-empty {
  color: var(--text-dim);
  font-size: 13px;
  padding: 20px 0;
}

.bar-chart { display: flex; flex-direction: column; gap: 8px; }
.bar-row {
  display: grid;
  grid-template-columns: 200px 1fr 90px;
  align-items: center;
  gap: 10px;
  font-size: 13px;
}
.bar-row-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-row-track { background: var(--bg); border-radius: 4px; height: 14px; overflow: hidden; }
.bar-row-fill { background: var(--accent); height: 100%; border-radius: 4px; }
.bar-row-value { text-align: right; font-variant-numeric: tabular-nums; }
.bar-row:hover .bar-row-fill { filter: brightness(1.15); }
.bar-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bar-row-clickable { cursor: pointer; border-radius: 4px; }
.bar-row-clickable:hover { background: var(--sidebar-active); }
.bar-row-clickable.selected { background: var(--sidebar-active); }
.bar-row-clickable.selected .bar-row-label { font-weight: 700; }

.chart-subheading-label { margin-top: 4px; }

.line-chart-wrap { position: relative; }
.line-chart-svg { width: 100%; height: auto; display: block; }

.chart-gridline { stroke: var(--border); stroke-width: 1; }
.chart-baseline { stroke: var(--text-dim); stroke-width: 1; }
.chart-tick { fill: var(--text-dim); font-size: 10px; }
.chart-line { stroke-width: 2; }
/* Monte Carlo projection band (opts.bands, buildLineChart) — a filled
   region, deliberately low-opacity so the gridlines/other series lines
   stay legible underneath it, and no stroke of its own (the two series it
   sits between already draw their own lines on top). */
.chart-band { opacity: 0.15; stroke: none; }
.chart-dot { pointer-events: none; }
.chart-crosshair { stroke: var(--text-dim); stroke-width: 1; stroke-dasharray: 3 3; }

.networth-chart-wrap { position: relative; }
.networth-chart-svg { width: 100%; height: auto; display: block; }
/* var(--text) rather than a literal white: it reads near-white in dark mode
   (this app's dark --text is #eaeaef) while staying dark-ink/high-contrast
   against a light-mode white panel, where a literal white line would vanish.
   Solid single color, deliberately not colored by month-over-month direction
   — that's the bars' job (see buildNetWorthTrendChart's monthColor). */
.networth-target-line { stroke: var(--text); stroke-width: 2; stroke-dasharray: 6 4; }

.networth-table-controls { display: flex; gap: 8px; }

.networth-table { margin-top: 10px; }

.networth-table-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;
  gap: 8px;
  padding: 6px 10px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  border-bottom: 1px solid var(--border);
}

/* Same header/list split as .activity-popup-header/-list: the header row
   sits outside the scrolling list so it never scrolls away with the rows. */
.networth-table-header {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  font-variant-numeric: normal;
  background: var(--bg);
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: 6px 6px 0 0;
}

.networth-table-list {
  max-height: 280px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-top: none;
  border-radius: 0 0 6px 6px;
}
.networth-table-list .networth-table-row:last-child { border-bottom: none; }
.networth-table-row .tone-positive { color: var(--positive); }
.networth-table-row .tone-negative { color: var(--negative); }

/* The Reports panel's Recurring Bills table (js/income-expense-view.js) —
   same fixed-column-list shape as .networth-table above (a bounded, always-
   5-column list, not a data-dependent month-by-month grid like .ie-table),
   just with its own column count/header/list class names since the two
   tables show unrelated things. */
.recurring-bills-table { margin-top: 10px; }
.recurring-bills-table-row {
  display: grid;
  grid-template-columns: 2.5fr 1fr 1fr 1fr 1fr;
  gap: 8px;
  padding: 6px 10px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  border-bottom: 1px solid var(--border);
}
.recurring-bills-table-header {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  font-variant-numeric: normal;
  background: var(--bg);
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: 6px 6px 0 0;
}
.recurring-bills-table-list {
  max-height: 320px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-top: none;
  border-radius: 0 0 6px 6px;
}
.recurring-bills-table-list .recurring-bills-table-row:last-child { border-bottom: none; }
.recurring-bills-table-row .tone-positive { color: var(--positive); }
.recurring-bills-table-row .tone-negative { color: var(--negative); }

/* The Reports panel's Income vs Expense table (js/income-expense-view.js).
   Column count is data-dependent (one per month in the selected range), so
   each .ie-row gets its grid-template-columns set inline per render
   (ieGridTemplate()) rather than a fixed CSS template like .budget-row's —
   every row still shares that same computed string, which is what keeps
   columns aligned down the table. .ie-table-wrap scrolls horizontally when
   a wide date range (e.g. "All time" on a multi-year budget) makes the
   table wider than the panel; .ie-table's own width: max-content + min-width:
   100% lets it size to its natural (minmax-floor) width while never being
   narrower than the available space, so a short table's 1fr columns still
   stretch to fill the panel instead of hugging the left edge. */
/* Border/rounding/clipping live on .ie-table-wrap (the actual scroll
   container) rather than .ie-table itself — .ie-cell-label below is
   position: sticky, and a sticky element sticks relative to the *nearest*
   ancestor with a scrolling mechanism, which per spec includes any ancestor
   with overflow != visible, not just ones that actually scroll. If .ie-table
   itself carried `overflow: hidden` (as it used to, purely for the rounded
   corners), it would — being the closer ancestor — silently become that
   binding container instead of .ie-table-wrap, and since .ie-table never
   itself scrolls (it's sized to exactly fit its content), the sticky column
   would just never appear to stick. Moving the clip to the real scroll
   container sidesteps this entirely.
   max-height + explicit overflow-y (not just overflow-x) is what makes the
   *header row's* own top: 0 sticky (see .ie-row-header below) work at all —
   the frozen-column fix above already established that a sticky element
   binds to the nearest ancestor with any non-visible overflow, so the
   header row's vertical stickiness binds to .ie-table-wrap too, same as the
   column's horizontal stickiness. Without a real bounded height + overflow-y
   here, .ie-table-wrap never actually scrolls vertically (its height just
   grows to fit content), so a sticky element bound to it would have nothing
   to resist and would simply scroll away with the rest of the page instead
   of sticking — this is the same nested-scroll-box pattern
   .networth-table-list already uses for its own capped, internally-
   scrolling breakdown table, just applied to both axes at once here. */
.ie-table-wrap {
  overflow: auto;
  max-height: 65vh;
  margin-top: 4px;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.ie-table {
  display: flex;
  flex-direction: column;
  width: max-content;
  min-width: 100%;
}
.ie-row {
  display: grid;
  gap: 8px;
  padding: 6px 10px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  border-bottom: 1px solid var(--border);
  align-items: center;
}
.ie-row:last-child { border-bottom: none; }
.ie-cell-num { text-align: right; }
.ie-cell-indent { padding-left: 18px; }
/* Frozen Category column — sticks to the left edge of .ie-table-wrap while
   the month/Average/Total columns scroll underneath it. Needs its own
   opaque background (matching whatever background that row type already
   paints — see the per-row-type overrides below) and a z-index above the
   scrolling cells, or the cells passing underneath as you scroll would show
   through/behind it instead of being hidden by it. border-right doubles as
   a permanent visual seam marking "this column doesn't move," not just a
   scroll-triggered affordance — simpler than toggling a shadow class on
   scroll for a v1. */
.ie-cell-label {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--panel);
  border-right: 1px solid var(--border);
}
.ie-row-header .ie-cell-label,
.ie-row-section .ie-cell-label,
.ie-row-bold .ie-cell-label {
  background: var(--bg);
}
/* Frozen header row — sticks to the top of .ie-table-wrap while the
   category rows scroll underneath it (the vertical counterpart to
   .ie-cell-label's horizontal freeze above; see .ie-table-wrap's own
   comment for why max-height + overflow-y had to be added for this to bind
   to the right container at all). z-index higher than .ie-cell-label's
   (2) so the whole header row — including its own label cell, which is
   *also* sticky left: 0 via the shared .ie-cell-label rule — paints above
   every other row's sticky label cell where the two axes overlap at the
   top-left corner; nested top+left stickiness composes correctly here since
   the row and the cell are two different sticky elements on two different
   axes, not one element trying to do both. */
.ie-row-header {
  position: sticky;
  top: 0;
  z-index: 3;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  font-variant-numeric: normal;
  background: var(--bg);
}
/* Section divider rows ("Income" / "Expense") — only the label cell is
   filled in (see buildIeSectionLabelRow's own comment), so this needs no
   .ie-cell-num rules of its own. */
.ie-row-section {
  font-weight: 700;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  background: var(--bg);
}
.ie-row-bold { font-weight: 700; background: var(--bg); }
.ie-row .tone-positive { color: var(--positive); }
.ie-row .tone-negative { color: var(--negative); }

.column-chart-wrap { position: relative; }
.column-chart-svg { width: 100%; height: auto; display: block; }
/* Same var(--text)-over-literal-white reasoning as .networth-target-line
   (see above) — the dashed cumulative-total overlay in
   buildCumulativeColumnChart needs the same "derived reference line, not a
   primary series" treatment. */
.chart-cumulative-line { stroke: var(--text); stroke-width: 2; stroke-dasharray: 6 4; }

.chart-legend { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 6px; }
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-dim); }
.legend-swatch { width: 14px; height: 2px; display: inline-block; border-radius: 1px; }

.chart-tooltip {
  position: absolute;
  top: 8px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 12px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
  pointer-events: none;
  white-space: nowrap;
}
.chart-tooltip.hidden { display: none; }
.chart-tooltip-x { color: var(--text-dim); margin-bottom: 4px; }
.chart-tooltip-row { display: flex; align-items: center; gap: 6px; }
/* Same cascade trap as .modal-panel label.hidden elsewhere in this file —
   .chart-tooltip-row's own display: flex above is declared after the
   generic .hidden utility, so without this more specific override,
   .hidden would silently lose on a row hidden by buildLineChart's showAt()
   for a gap-carrying series (a Monte Carlo projection series with no value
   at a historical-only index, say). */
.chart-tooltip-row.hidden { display: none; }
.chart-tooltip-key { width: 10px; height: 2px; display: inline-block; }
.chart-tooltip-name { color: var(--text-dim); }

/* buildRecurringForecastChart's (charts.js) two derived reference lines.
   The average line's stroke color is set inline per-render (its sign
   determines --positive/--negative), so only width/dash live here. The
   cumulative line is a literal white, not var(--text) — a deliberate,
   scoped exception to .chart-cumulative-line's own "derived line" var(--text)
   convention right above, per explicit user request for this one chart;
   .chart-cumulative-line itself (used by every other cumulative-overlay
   chart) is untouched. */
.recurring-forecast-average-line { stroke-width: 2; stroke-dasharray: 6 4; }
.recurring-forecast-cumulative-line { stroke: #ffffff; stroke-width: 2; stroke-dasharray: 6 4; }

/* The Recurring Forecast chart's per-bar tooltip detail list (up to 5
   transactions, already filtered/sorted/capped by the caller) — sits below
   the usual chart-tooltip-row summary lines, separated by a thin divider. */
.chart-tooltip-divider { border-top: 1px solid var(--border); margin: 6px 0 4px; }
.chart-tooltip-txn-list { display: flex; flex-direction: column; gap: 2px; }
.chart-tooltip-txn { display: flex; align-items: center; gap: 10px; font-size: 11px; }
.chart-tooltip-txn-label { color: var(--text-dim); }
.chart-tooltip-txn-amount { margin-left: auto; font-variant-numeric: tabular-nums; }
.chart-tooltip-txn-amount.positive { color: var(--positive); }
.chart-tooltip-txn-amount.negative { color: var(--negative); }
.chart-tooltip-txn-empty { color: var(--text-dim); font-style: italic; }

/* The Recurring Forecast section's Outflow/Inflow/Both toggle — same
   bordered-tile idiom as .recurring-sort-btn (js/recurring-modal.js's own
   sort toggle), scoped to its own classes since this lives in a different
   file/screen entirely. */
.forecast-direction-row { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.forecast-direction-btn {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  border-radius: 4px;
  padding: 4px 10px;
  cursor: pointer;
  font-size: 12px;
}
.forecast-direction-btn.active { background: var(--sidebar-active); color: var(--sidebar-text); border-color: var(--accent); font-weight: 600; }

/* ---- Mobile shell -----------------------------------------------------
   Entirely separate from the desktop rules above — every selector here is
   scoped under a `.mobile-*`/`#mobile-shell` class that desktop's own DOM
   never produces (see js/app.js's render() guard and js/mobile-shell.js).
   Nothing above this comment was changed to make room for this section. */

#mobile-shell {
  /* 100vh on a real phone browser is taller than what's actually visible —
     it doesn't shrink for the address bar's collapse/expand, unlike
     .app-shell's own 100vh, which is fine since desktop browsers have no
     such dynamic chrome. That extra height pushed the bottom tab bar just
     below the visible fold until the whole page was scrolled down — a
     real, confirmed bug, not a hypothetical one. 100dvh (dynamic viewport
     height) is sized to the actual visible area on every modern mobile
     browser; the 100vh line above it is a fallback for anything that
     doesn't understand dvh yet, silently ignored (as an invalid value) by
     anything that does, so the dvh line below it wins there. */
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* A bare ID selector (#mobile-shell, specificity 1-0-0) always beats a bare
   class selector (.hidden, specificity 0-1-0) regardless of source order —
   so #mobile-shell's own display: flex above was winning over .hidden's
   display: none the entire time, meaning class="hidden" never actually hid
   it. This is exactly the bug #auth-screen.hidden (near the top of this
   file) was already written to avoid for the same reason; same fix here.
   Real, confirmed symptom: the mobile shell's header/tab bar stayed
   visible, stacked above the sign-in form, even while #auth-screen was
   correctly shown underneath it. */
#mobile-shell.hidden { display: none; }

.mobile-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.mobile-header-title { font-weight: 700; font-size: 16px; }
.mobile-budget-select {
  background: none;
  border: none;
  color: var(--text);
  font-weight: 700;
  font-size: 16px;
  max-width: 60%;
}
.mobile-header-logout {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-dim);
  border-radius: 6px;
  padding: 6px 12px;
  font-size: 13px;
  cursor: pointer;
}

.mobile-main {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 12px 16px 90px;
  position: relative;
}

.mobile-tab-bar {
  flex-shrink: 0;
  display: flex;
  border-top: 1px solid var(--border);
  background: var(--panel);
}
.mobile-tab {
  flex: 1;
  background: none;
  border: none;
  color: var(--text-dim);
  padding: 14px 0;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.mobile-tab.active { color: var(--accent); }

/* ---- Plan tab ---- */
.mobile-plan-month { font-size: 13px; color: var(--text-dim); margin-bottom: 10px; }
.mobile-plan-group-header {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin: 18px 0 6px;
}
.mobile-plan-row {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
  margin-bottom: 6px;
  color: var(--text);
  font-size: 15px;
  text-align: left;
  cursor: pointer;
}
.mobile-plan-available { font-variant-numeric: tabular-nums; font-weight: 600; }
.mobile-plan-available.positive { color: var(--positive); }
.mobile-plan-available.negative { color: var(--negative); }
.mobile-plan-available.zero { color: var(--zero); }

/* ---- Accounts tab ---- */
.mobile-account-switcher {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.mobile-account-select {
  flex: 1;
  background: var(--panel);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 15px;
}
.mobile-account-balance { font-weight: 700; font-variant-numeric: tabular-nums; }

.mobile-txn-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
  margin-bottom: 6px;
  cursor: pointer;
}
/* Future-dated: still listed but greyed and excluded from the account
   balance shown above it — mirrors the desktop register (see isFutureTxn()
   in db.js). */
.mobile-txn-card.future-txn { opacity: 0.55; }
.mobile-txn-card-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.mobile-txn-payee { font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mobile-txn-sub { font-size: 12.5px; color: var(--text-dim); }
.mobile-txn-amount { font-variant-numeric: tabular-nums; font-weight: 600; flex-shrink: 0; }
.mobile-txn-amount.positive { color: var(--positive); }
.mobile-txn-amount.negative { color: var(--negative); }

.mobile-empty-note { color: var(--text-dim); font-size: 14px; text-align: center; margin-top: 24px; }

.mobile-fab {
  position: fixed;
  right: 20px;
  bottom: 84px;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  border: none;
  font-size: 28px;
  line-height: 1;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
  cursor: pointer;
}

/* ---- Sheets (add/edit transaction, assign amount) ---- */
.mobile-sheet-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: flex-end;
  z-index: 50;
}
.mobile-sheet {
  width: 100%;
  max-height: 88vh;
  overflow-y: auto;
  background: var(--panel);
  border-radius: 16px 16px 0 0;
  padding: 18px 18px 28px;
}
.mobile-sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.mobile-sheet-header h3 { margin: 0; font-size: 17px; }
.mobile-sheet-close {
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
}
.mobile-txn-sheet label,
.mobile-assign-sheet label {
  display: block;
  font-size: 13px;
  color: var(--text-dim);
  margin-bottom: 12px;
}
.mobile-txn-sheet input,
.mobile-txn-sheet select,
.mobile-assign-sheet input {
  display: block;
  width: 100%;
  margin-top: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 15px;
  box-sizing: border-box;
}
.mobile-amount-row { display: flex; gap: 12px; }
.mobile-amount-row label { flex: 1; }
.mobile-sheet-actions { display: flex; gap: 10px; margin-top: 6px; }
.mobile-sheet-actions .btn-save {
  flex: 1;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 12px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.mobile-sheet-actions .btn-danger {
  background: none;
  border: 1px solid var(--negative);
  color: var(--negative);
  border-radius: 8px;
  padding: 12px 16px;
  font-size: 15px;
  cursor: pointer;
}

/* ---------------------------------------------------------------------
   Admin dashboard — public/admin.html + js/admin.js. A separate,
   self-contained page (own script chain, never linked from the app or
   marketing site), so this section is scoped entirely to admin-prefixed
   class/id names (.admin-..., #admin-...) the rest of the app never
   produces. Reuses
   existing tokens/classes rather than inventing a new visual language:
   .auth-card/.auth-form/.auth-submit/.auth-error (the sign-in gate),
   .btn-save/.btn-cancel (row actions), --bg/--panel/--border/--text/
   --text-dim/--accent/--positive/--negative/--gold (everything else).
   --------------------------------------------------------------------- */
/* position: fixed + inset: 0 rather than height: 100vh — a fixed overlay
   spanning the whole viewport centers its content regardless of body/html's
   own box height (which plain flow content never stretches to fill on its
   own), where a height: 100vh block can still end up not truly centered
   depending on what else is on the page. */
.admin-auth-gate {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.admin-auth-gate.hidden { display: none; }

.admin-shell {
  max-width: 900px;
  margin: 0 auto;
  padding: 24px 20px 60px;
}
.admin-shell.hidden { display: none; }

.admin-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 16px;
  margin-bottom: 20px;
  border-bottom: 1px solid var(--border);
}
.admin-header h1 { margin: 0; font-size: 20px; }
.admin-header-account { display: flex; align-items: center; gap: 12px; }
.admin-signed-in-as { font-size: 13px; color: var(--text-dim); }

.admin-section { margin-bottom: 36px; }
.admin-section h2 { font-size: 16px; margin: 0 0 14px; }
.admin-subsection-title {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  margin: 18px 0 8px;
}
.admin-subsection-title:first-of-type { margin-top: 0; }

.admin-empty { color: var(--text-dim); font-size: 13px; margin: 0 0 8px; }

.admin-list { display: flex; flex-direction: column; gap: 8px; }

/* Pending/History (js/admin.js's buildSignupRequestsHtml()) — capped to
   roughly 6 cards' worth of height before scrolling internally, same
   nested-scroll-box idiom as .ie-table-wrap/.networth-table-list elsewhere
   in this app, rather than letting either subsection grow unbounded as
   requests accumulate. An approximation, not an exact 6-row measurement
   (card height varies with how much name/message text a request has) —
   fine for this purpose, same as those other max-height scroll boxes. */
.admin-list-scroll { max-height: 640px; overflow-y: auto; padding-right: 4px; }

.admin-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 12px 14px;
}
.admin-row-highlighted { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

.admin-row-main { flex: 1; min-width: 0; }
.admin-row-title { font-size: 14px; font-weight: 600; }
.admin-row-detail { font-size: 13px; color: var(--text-dim); margin-top: 2px; }
.admin-row-message { white-space: pre-wrap; }
.admin-row-meta { font-size: 12px; color: var(--text-dim); margin-top: 6px; }

.admin-row-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.admin-feedback-field {
  display: block;
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 10px;
}
.admin-feedback-field select,
.admin-feedback-field textarea {
  display: block;
  width: 100%;
  margin-top: 4px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
  font-family: inherit;
  box-sizing: border-box;
  resize: vertical;
}

.status-pill {
  display: inline-block;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 6px;
  vertical-align: middle;
  margin-left: 4px;
}
.status-pill-positive { color: var(--positive); border-color: var(--positive); }
.status-pill-negative { color: var(--negative); border-color: var(--negative); }
.status-pill-gold { color: var(--gold); border-color: var(--gold); }
.status-pill-dim { color: var(--text-dim); }
.chart-tooltip-value { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
