/* Fonts are self-hosted in css/fonts.css. There used to be an @import to
   fonts.googleapis.com here as well as the <link> in index.html — two
   independent references to the same CDN, so removing only the visible one left
   the request intact. It also pulled Noto Serif JP for .kanji-sub, a rule no
   markup uses. Keep font loading in fonts.css; do not reintroduce an @import. */

/* ==========================================================================
   Design Tokens & Theme Variables (Midori Leafy-Green Style)
   ========================================================================== */
:root {
  /* Common Brand Colors */
  --green-forest: #1e381b;     /* Deep stone forest */
  --green-matcha: #4d9c4e;     /* Vibrant tea green */
  --green-sage: #8ba88f;       /* Calming soft sage */
  --green-mint: #a2d2a4;       /* Neon leaf accents */
  --autumn-terracotta: #e07a5f;/* Outgoings accent */
  --bamboo-gold: #cfa87b;      /* Warm wood/gold highlights */
  --stone-charcoal: #181d18;   /* Dark base stone */
  --paper-soft: #fcfdfa;       /* Light base paper */

  /* Dark Theme Variables (Default) */
  --bg-primary: #0e120e;
  --bg-secondary: #161e16;
  --bg-card: rgba(26, 38, 27, 0.7);
  --border-color: rgba(139, 168, 143, 0.15);
  --border-glow: rgba(77, 156, 78, 0.3);
  --text-main: #e6edd5;
  --text-muted: #8ba88f;
  --text-dark: #0e120e;
  
  /* Dialogs get their own OPAQUE surface instead of --bg-card.
     --bg-card is translucent, which is right on the page — it tints itself with
     whatever it sits on. A dialog does not sit on the page: it sits on a dark
     scrim. In the light theme that made 85% white over a 78% black scrim land
     at #e2e3e2 — never white, and carrying the scrim's green-black cast, which
     is the grey-green this token exists to remove. It also pulled the body text
     down to 4.07:1, under the 4.5:1 needed to read comfortably; on white the
     same colour is 5.2:1, with no change to the text itself.
     Dark theme takes a shade LIGHTER than the scrim, so the panel still reads
     as something raised above the page rather than a hole cut into it. */
  --surface-raised: #1e2a1e;

  /* One red, so a hover, a border and a label cannot drift apart. */
  --danger: #ff6b6b;
  --danger-soft: rgba(255, 107, 107, 0.12);

  /* Outline buttons need a border you can actually find. The old hairline was
     --border-color at 12-15% alpha, about 1.1:1 against a dialog surface —
     invisible, which is why Cancel read as a floating word. WCAG asks 3:1 for
     the boundary of a control. */
  /* 0.65, not 0.45: measured against --surface-raised, 0.45 came out at 2.29:1
     and 0.6 lands exactly on 3.0, which is no margin at all for a value someone
     will later nudge. */
  --btn-outline: rgba(139, 168, 143, 0.65);
  --btn-outline-bg: rgba(139, 168, 143, 0.07);

  --glass-bg: rgba(22, 30, 22, 0.65);
  --glass-border: rgba(139, 168, 143, 0.12);
  /* The same colour as --glass-bg with the transparency taken out. The mobile
     drawer drops its backdrop blur — a blurred layer that slides has to re-blur
     the whole panel on every frame — and a translucent panel with nothing
     blurring behind it just shows the page through it. */
  --sidebar-solid: #161e16;
  --shadow-color: rgba(0, 0, 0, 0.4);
  
  --card-gradient-1: linear-gradient(135deg, #1b381a 0%, #3a6839 100%);
  --card-gradient-2: linear-gradient(135deg, #2b453a 0%, #588775 100%);
  --card-gradient-3: linear-gradient(135deg, #515437 0%, #878c58 100%);
  
  --transition-speed: 0.3s;

  /* Height of the mobile bottom bar, excluding the phone's safe-area inset.
     Declared here rather than inline because two unrelated things have to agree
     on it: the bar's own height, and main's bottom padding that keeps content
     from hiding underneath it. Theme-independent, so it is not repeated in the
     light block. */
  --bottom-nav-height: 62px;
  /* The bottom bar needs its own green and its own edge, because it is the one
     surface whose background flips from near-black to pure white between
     themes. --green-matcha reads at 5.02:1 on the dark bar and 3.4:1 on the
     white one, and the labels are 10px — under the 4.5:1 that size needs. */
  --nav-active: #4d9c4e;
  /* The bar's top edge is the only thing dividing it from the content
     scrolling underneath. --border-color is 12% alpha, which is ~1.1:1 here and
     effectively invisible on white. */
  --nav-border: #5d775e;
}

/* Light Theme Variables */
body.light-theme {
  --bg-primary: #f2f5f1;
  --bg-secondary: #e5ebe4;
  --bg-card: rgba(255, 255, 255, 0.85);
  --border-color: rgba(45, 90, 47, 0.12);
  --border-glow: rgba(77, 156, 78, 0.25);
  --text-main: #1c2a1b;
  /* Was #5a735c, 5.2:1 on white. A shade deeper buys 6.6:1 without turning
     secondary text into primary text — it still sits back from --text-main's
     14:1, which is the whole job of a muted colour. */
  --text-muted: #4f6851;
  --text-dark: #ffffff;

  /* Plain white. A dialog is the one surface in the app with a dark scrim
     behind it, so it is the one place a tint has nothing to come from. */
  --surface-raised: #ffffff;

  /* #ff6b6b is a dark-theme red: on white it is 2.17:1, which fails even the
     3:1 asked of a plain UI border, let alone the 4.5:1 for text. This one is
     5.4:1 on white and still unmistakably a warning. */
  --danger: #c0392b;
  --danger-soft: rgba(192, 57, 43, 0.09);

  --btn-outline: #7f8c80;
  --btn-outline-bg: #f1f4f0;

  --glass-bg: rgba(255, 255, 255, 0.75);
  --glass-border: rgba(45, 90, 47, 0.08);
  --sidebar-solid: #ffffff;
  /* Darker than --green-matcha: 5.32:1 on the white bar, where the shared green
     managed only 3.4:1. Also carries the create button, so its white + sits at
     5.32:1 rather than 3.4:1. */
  --nav-active: #2f7a31;
  --nav-border: #7d907c;
  --shadow-color: rgba(45, 90, 47, 0.08);
  
  --card-gradient-1: linear-gradient(135deg, #3d6a3c 0%, #5e955d 100%);
  --card-gradient-2: linear-gradient(135deg, #4b7465 0%, #76a895 100%);
  --card-gradient-3: linear-gradient(135deg, #747954 0%, #a4aa7e 100%);
}

/* ==========================================================================
   Base Styles & Reset
   ========================================================================== */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Hold the scrollbar's 8px whether or not the current section is tall enough to
   need one. Without this, moving from a long section (Ledger) to a short one
   (Settings) drops the scrollbar, the content box widens by 8px, and every
   card, chart and header control slides right — a visible lurch on what should
   be a clean swap. Declared on both boxes on purpose: body's overflow is what
   propagates to the viewport here, and browsers differ on which element they
   read the gutter from. */
html {
  scrollbar-gutter: stable;

  /* Chrome and Android WebView paint a translucent blue rectangle over the
     border box of whatever you touch. It ignores border-radius, so on a 50%
     circle (the FAB) or a 12px pill (every button here) it flashes a hard
     square that does not match the shape being pressed — the janky part.
     The property is inherited, so declaring it once here covers buttons,
     links and the [data-action] elements that behave as buttons, without a
     selector list that would fall out of date the moment a button is added.
     This was the app's only touch feedback on everything but the FAB, so it
     is replaced below rather than simply removed. */
  -webkit-tap-highlight-color: transparent;
}

body {
  font-family: 'Inter', sans-serif;
  background-color: var(--bg-primary);
  color: var(--text-main);
  min-height: 100vh;
  display: flex;
  overflow-x: hidden;
  scrollbar-gutter: stable;
  transition: background-color var(--transition-speed), color var(--transition-speed);
}

h1, h2, h3, h4, .font-title {
  font-family: 'Outfit', sans-serif;
  font-weight: 600;
}

.kanji-sub {
  /* No markup uses this class — it is kept only because it is harmless and may
     be wanted again. 'Noto Serif JP' was declared here and really was being
     downloaded, via the @import that used to head this file, so every page load
     fetched a CJK family for a rule that never rendered. Dropped rather than
     self-hosted; if the kanji subtitle is ever added back, subset the font to
     the glyphs actually used rather than shipping the whole family. */
  font-size: 0.85em;
  opacity: 0.6;
  margin-left: 4px;
}

button, input, select, textarea {
  font-family: inherit;
  color: inherit;
}

/* What replaces the tap highlight removed on html above. A press has to
   acknowledge itself: several buttons here (Save, Cancel, a subtab) do not
   change anything you can see for a moment or two, and with the highlight
   gone and nothing in its place they read as broken rather than merely
   quiet.
   Unlike the highlight this follows the button's own border-radius, because
   it is the button being drawn. Deliberately both scale and fade: scale
   alone is imperceptible on a 22px icon button, fade alone is easy to miss
   on a large one.
   Left untransitioned on purpose — an instant response is what makes a
   touch target feel connected to the finger, and most of these buttons
   already carry `transition: all 0.2s`, which eases the release. */
button:not(:disabled):active {
  transform: scale(0.97);
  opacity: 0.8;
}

/* Custom Scrollbar */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-track {
  background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
  background: var(--border-color);
  border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--green-sage);
}

/* ==========================================================================
   Layout Structure
   ========================================================================== */
#appContainer {
  display: flex;
  width: 100%;
  max-width: 1600px;
  margin: 0 auto;
  min-height: 100vh;
}

/* Sidebar Styling */
aside {
  width: 280px;
  background: var(--glass-bg);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-right: 1px solid var(--glass-border);
  padding: 30px 24px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  position: sticky;
  top: 0;
  height: 100vh;
  z-index: 10;
  transition: background var(--transition-speed), border var(--transition-speed);
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 40px;
}

.brand-logo-container {
  background: linear-gradient(135deg, var(--green-matcha), var(--green-forest));
  width: 42px;
  height: 42px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  box-shadow: 0 4px 12px var(--border-glow);
}

.brand-name {
  font-size: 24px;
  font-weight: 800;
  letter-spacing: -0.5px;
  background: linear-gradient(to right, var(--green-mint), var(--green-sage));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

body.light-theme .brand-name {
  background: linear-gradient(to right, var(--green-forest), var(--green-matcha));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

nav ul {
  list-style: none;
}

nav li {
  margin-bottom: 8px;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 18px;
  border-radius: 12px;
  text-decoration: none;
  font-weight: 500;
  color: var(--text-muted);
  cursor: pointer;
  transition: all 0.25s ease;
}

.nav-link:hover, .nav-link.active {
  color: var(--text-main);
  background: rgba(77, 156, 78, 0.12);
  border-left: 4px solid var(--green-matcha);
  padding-left: 14px;
}

.nav-link svg {
  width: 20px;
  height: 20px;
  stroke-width: 2;
  transition: stroke var(--transition-speed);
}

.nav-link.active svg {
  stroke: var(--green-matcha);
}

.sidebar-footer {
  margin-top: auto;
  border-top: 1px solid var(--border-color);
  padding-top: 20px;
}

/* Theme Switcher Button */
.theme-toggle-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  padding: 12px;
  border-radius: 12px;
  border: 1px solid var(--border-color);
  background: transparent;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.25s ease;
}

.theme-toggle-btn:hover {
  background: rgba(139, 168, 143, 0.08);
  border-color: var(--green-matcha);
}

/* Main Content Area */
main {
  flex: 1;
  padding: 40px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 30px;
}

/* Header Area */
header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--border-color);
  padding-bottom: 24px;
}

.header-title-section h1 {
  font-size: 32px;
  letter-spacing: -0.5px;
}

.header-title-section p {
  color: var(--text-muted);
  font-size: 14px;
  margin-top: 4px;
}

.header-controls {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* The slot the pill sits in. Hidden by default and switched to flex by
   updateSyncUI() in js/sync.js once a signed-in account is known.

   Fixed width, because the header row is saturated at the sizes this app is
   actually used at — 834px of content in 840px at a 1200px window — so there is
   no slack for a growing pill to expand into. Left to itself it dragged the
   date badge and currency selector 27px back and forth on every sync. Holding
   the slot instead of the pill is what lets the visible box stay honest about
   its own text. 104px clears the widest state ("Sync error", 95.7px at 1280px
   with Inter loaded) with room for a fallback font; re-measure if a longer
   status word is ever added. */
.sync-status-slot {
  display: none;
  align-items: center;
  flex-shrink: 0;
  width: 104px;
}

/* The pill itself: sized by its own content, inside the slot above. */
.sync-status-widget {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--border-color);
  border-radius: 20px;
  font-size: 11px;
  cursor: pointer;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.sync-icon-pulse {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

/* No width reservation. The label cycles Synced → Syncing… → Sync error, which
   is 40.1px / 53.8px / 55.7px at 1280px with Inter loaded, and the pill sizes
   to whichever it is showing.

   That used to shove the date badge and currency selector sideways on every
   sync, which is why a 64px floor was held here — at the cost of 24px of dead
   space inside the pill for the ~99% of the time it reads "Synced". The pill
   now sits at the end of the title block instead, with the header's
   space-between gap ahead of the controls absorbing the growth, so the box can
   breathe without anything moving. Keep it that way: put a sibling after the
   pill on this row and the jitter comes straight back. */
#zensync-header-text {
  font-weight: 600;
  opacity: 0.8;
}

/* Simulated Date Display Widget */
.time-travel-widget {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  padding: 10px 18px;
  border-radius: 16px;
  box-shadow: 0 4px 10px var(--shadow-color);
}

/* Anchors the native calendar under the badge. The input has to stay a sibling
   rather than a child, because a <button> may not contain interactive content. */
.date-badge-wrap {
  position: relative;
  display: flex;
  flex-shrink: 0;
}

/* Rendered, but only just. showPicker() throws InvalidStateError on an element
   that is not being rendered, so this cannot be display:none or visibility:
   hidden — 1px at the bottom-left corner is what anchors the popup under the
   badge. pointer-events:none keeps it from ever swallowing a click meant for
   the button on top of it. */
.date-badge-input {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

.date-badge {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  gap: 8px;
  font-weight: 700;
  font-size: 15px;
  color: var(--green-mint);
  font-family: 'Outfit', sans-serif;
  letter-spacing: 0.5px;
  white-space: nowrap;
  position: relative;
  cursor: pointer;
  transition: opacity 0.2s ease;
  /* It is a <button> now, for the keyboard activation that comes free with one.
     These undo the UA chrome that arrives with the element. */
  background: none;
  border: 0;
  padding: 0;
}
.date-badge:hover {
  opacity: 0.85;
}
/* The old badge was a span behind a transparent input, so a keyboard user
   landed on a control with no visible focus at all. */
.date-badge:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
  border-radius: 4px;
}
body.light-theme .date-badge {
  color: var(--green-forest);
}

/* One flex item, so the three steps travel together. Loose in the widget they
   wrapped wherever the width ran out, which on a half-width window left
   -1 Month marooned on the date's line. */
.time-travel-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}

.time-travel-btn {
  background: rgba(77, 156, 78, 0.15);
  border: 1px solid rgba(77, 156, 78, 0.3);
  padding: 6px 12px;
  font-size: 12px;
  font-weight: 600;
  border-radius: 8px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
  transition: all 0.2s ease;
}

.time-travel-btn:hover {
  background: var(--green-matcha);
  color: #fff;
  box-shadow: 0 0 10px var(--border-glow);
}

.time-travel-btn.current-date-btn {
  background: rgba(207, 168, 123, 0.15);
  border: 1px solid rgba(207, 168, 123, 0.4);
  color: var(--bamboo-gold);
}

.time-travel-btn.current-date-btn:hover {
  background: var(--bamboo-gold);
  color: var(--text-dark);
  border-color: var(--bamboo-gold);
  box-shadow: 0 0 10px rgba(207, 168, 123, 0.5);
}

/* Dashboard Base Currency Selector */
.currency-selector-widget {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  padding: 10px 14px;
  border-radius: 16px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.currency-selector-widget label {
  font-size: 12px;
  color: var(--text-muted);
  font-weight: 500;
}

.currency-select {
  background: transparent;
  border: none;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
  outline: none;
  color: var(--text-main);
}

/* ==========================================================================
   Dashboard Metrics
   ========================================================================== */
.dashboard-summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 20px;
}

.metric-card {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 20px;
  padding: 24px;
  position: relative;
  overflow: hidden;
  box-shadow: 0 8px 24px var(--shadow-color);
  transition: transform 0.25s, border-color 0.25s;
}

.metric-card:hover {
  transform: translateY(-4px);
  border-color: var(--green-matcha);
}

/* The base card clips to its rounded corners, which would swallow a popover
   anchored below it. Only the two cards that actually have one opt out, so
   every other card keeps the clip its decoration relies on. */
.metric-card.has-breakdown {
  overflow: visible;
  cursor: pointer;
}

/* Raised above the sibling cards only while open, so the popover is not
   competing for stacking order with cards it is not attached to. */
.metric-card.has-breakdown.breakdown-open {
  z-index: 20;
}

.metric-breakdown-popover {
  position: absolute;
  top: calc(100% + 10px);
  left: 0;
  right: 0;
  z-index: 21;
  padding: 14px 16px 12px;
  /* Opaque, for the same reason dialogs are — see --surface-raised. --bg-card
     is translucent by design: a card tints itself with the page it sits ON.
     This panel does not sit on the page, it hangs OVER it, so the chart, the
     numbers and the card behind it all showed through its own numbers. */
  background: var(--surface-raised);
  border: 1px solid var(--border-color);
  border-radius: 16px;
  box-shadow: 0 12px 32px var(--shadow-color);
  /* The card lifts 4px on hover; the popover must not travel with it, or the
     pointer chases a moving target on its way down into the panel. */
  transform: translateY(4px);
}

/* The hidden attribute alone is enough in every current browser, but this
   element is also positioned by the rule above — an explicit display:none
   keeps it out of the flow regardless of what the UA stylesheet does. */
.metric-breakdown-popover[hidden] {
  display: none;
}

.metric-breakdown-title {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
  margin-bottom: 10px;
}

/* Fixed height because the canvas is responsive with maintainAspectRatio off:
   without a sized parent, Chart.js measures zero and draws nothing. */
.metric-breakdown-canvas-wrap {
  position: relative;
  height: 150px;
}

.metric-breakdown-empty {
  margin: 0;
  padding-top: 52px;
  text-align: center;
  font-size: 13px;
  color: var(--text-muted);
}

/* The savings card's panel. Text, not a chart: the four figures ARE the
   explanation, and a donut of two slices would say less than the numbers do. */
.savings-detail {
  margin: 0;
  font-size: 13px;
}

.savings-detail-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 4px 0;
}

.savings-detail-row dt {
  color: var(--text-muted);
}

/* Tabular figures so the four amounts line up on the decimal point rather than
   on whatever width each glyph happens to be. */
.savings-detail-row dd {
  margin: 0;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.savings-detail-net {
  border-top: 1px solid var(--border-color);
  margin-top: 4px;
  padding-top: 8px;
}

/* Signed figures colour themselves. Applied by renderSavingsRateDetail rather
   than fixed per row, because the same row is good news or bad news depending
   only on its sign — the rate row was painted green unconditionally, which is
   how -595% ended up in the colour reserved for a good month. */
.savings-detail dd.amount-positive {
  color: var(--green-matcha);
}

.savings-detail dd.amount-negative {
  color: var(--danger);
}

.savings-detail-note {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-muted);
}

/* Donut legend. Replaces Chart.js's, which truncated names to fit its box and
   had nowhere to put a number — see renderDonutLegend in js/charts.js. */
.donut-legend {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  font-size: 12px;
}

.donut-legend-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 3px 0;
}

/* The row is a button so that clicking a category switches it off. All the
   chrome a button brings is reset; only the pointer and the focus ring are
   wanted. Padding moves here from the row so the whole strip is the target
   rather than the text alone. */
.donut-legend-toggle {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  padding: 3px 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

.donut-legend-row-interactive {
  padding: 0;
}

.donut-legend-toggle:hover .donut-legend-name {
  color: var(--green-matcha);
}

.donut-legend-toggle:focus-visible {
  outline: 2px solid var(--green-matcha);
  outline-offset: 2px;
  border-radius: 6px;
}

/* 21px was fine to click and not to tap. These rows only became a touch target
   when they became buttons, so the size question arrives with them. The legend
   gets taller on a phone; a row you cannot reliably hit is worse. */
@media (max-width: 768px) {
  .donut-legend-toggle {
    min-height: 44px;
    align-items: center;
  }
}

/* Switched off: struck through and faded, the same signal Chart.js's own legend
   gave. It stays in the list because it is the only way back. */
.donut-legend-toggle.is-excluded {
  opacity: 0.45;
}

.donut-legend-toggle.is-excluded .donut-legend-name {
  text-decoration: line-through;
}

.donut-legend-dot {
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  /* Baseline-aligned text would sit the dot on the text baseline and leave it
     looking dropped; this lifts it back to the middle of the line. */
  transform: translateY(1px);
}

/* min-width:0 so a long category name ellipsises instead of pushing the share
   and the amount off the end of the row — the two things the legend is for. */
.donut-legend-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-main);
}

.donut-legend-share {
  flex: 0 0 auto;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.donut-legend-amount {
  flex: 0 0 auto;
  min-width: 76px;
  text-align: right;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.donut-legend-total {
  margin-top: 4px;
  padding-top: 8px;
  border-top: 1px solid var(--border-color);
  font-weight: 600;
}

.donut-legend-total .donut-legend-amount {
  color: var(--text-main);
}

/* Below the grid's minmax(260px) breakpoint the cards go full width, where a
   150px canvas with a right-hand legend gets cramped. */
@media (max-width: 600px) {
  .metric-breakdown-canvas-wrap {
    height: 190px;
  }
}

.metric-header {
  display: flex;
  justify-content: space-between;
  color: var(--text-muted);
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 12px;
}

.metric-icon-circle {
  width: 36px;
  height: 36px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(139, 168, 143, 0.1);
  color: var(--green-sage);
}

.metric-card.net-worth .metric-icon-circle {
  background: rgba(207, 168, 123, 0.15);
  color: var(--bamboo-gold);
}

.metric-card.income .metric-icon-circle {
  background: rgba(77, 156, 78, 0.15);
  color: var(--green-matcha);
}

.metric-card.expense .metric-icon-circle {
  background: rgba(224, 122, 95, 0.15);
  color: var(--autumn-terracotta);
}

.metric-value {
  font-size: 28px;
  font-weight: 700;
  letter-spacing: -0.5px;
  font-family: 'Outfit', sans-serif;
  margin-bottom: 6px;
}

.metric-desc {
  font-size: 12px;
  color: var(--text-muted);
}

.metric-progress-container {
  margin-top: 14px;
  width: 100%;
}

.metric-progress-bar {
  height: 6px;
  background: var(--border-color);
  border-radius: 3px;
  overflow: hidden;
  position: relative;
}

.metric-progress-fill {
  height: 100%;
  border-radius: 3px;
  transition: width 0.5s ease-out;
}

/* ==========================================================================
   Visual Dashboard Analytics Section
   ========================================================================== */
.dashboard-charts-section {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 24px;
}

@media (max-width: 1100px) {
  .dashboard-charts-section {
    grid-template-columns: 1fr;
  }
}

.dashboard-bottom-section {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}

@media (max-width: 768px) {
  .dashboard-bottom-section {
    grid-template-columns: 1fr;
  }
}

.chart-card {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 24px;
  padding: 24px;
  box-shadow: 0 8px 24px var(--shadow-color);
  /* Grid items default to min-width: auto, which keeps them pinned to
     their content's intrinsic width (the chart canvas) — without this,
     the canvas's width forces the whole grid track wider than the
     viewport on mobile, even though grid-template-columns is 1fr. */
  min-width: 0;
}

.chart-container-large canvas,
.chart-container-medium canvas {
  max-width: 100%;
}

.chart-card h3 {
  font-size: 18px;
  margin-bottom: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.chart-container-large {
  height: 320px;
  position: relative;
}

.chart-container-medium {
  height: 240px;
  position: relative;
}

/* ==========================================================================
   Components & Interactive Elements
   ========================================================================== */

/* Leafy Cards Carousel for Wallets */
.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 18px;
}

.section-header h2 {
  font-size: 22px;
}

.add-btn {
  background: var(--green-matcha);
  color: #fff;
  border: none;
  border-radius: 12px;
  padding: 10px 20px;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  box-shadow: 0 4px 12px var(--border-glow);
  transition: all 0.25s ease;
}

.add-btn:hover {
  background: var(--green-forest);
  transform: translateY(-2px);
}

.wallets-carousel {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
  margin-bottom: 10px;
}

/* Premium Wallet Card Design */
.wallet-card {
  background: var(--card-gradient-1);
  border-radius: 20px;
  padding: 24px;
  color: #fff;
  position: relative;
  overflow: hidden;
  height: 180px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25);
  transition: all 0.3s ease;
  cursor: pointer;
}

.wallet-card::before {
  content: '';
  position: absolute;
  top: -30%;
  right: -20%;
  width: 150px;
  height: 150px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.05);
  pointer-events: none;
}

.wallet-card::after {
  content: '🍃';
  position: absolute;
  bottom: 10%;
  right: 8%;
  font-size: 60px;
  opacity: 0.12;
  pointer-events: none;
}

.wallet-card:hover {
  transform: translateY(-6px) scale(1.02);
  box-shadow: 0 15px 30px var(--border-glow);
}

.wallet-card-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
}

.wallet-name-label {
  font-size: 14px;
  opacity: 0.8;
  font-weight: 500;
}

.wallet-type-badge {
  font-size: 11px;
  background: rgba(255, 255, 255, 0.15);
  padding: 4px 8px;
  border-radius: 6px;
  font-weight: 600;
  letter-spacing: 0.5px;
}

.wallet-balance-display {
  font-size: 32px;
  font-weight: 700;
  font-family: 'Outfit', sans-serif;
  letter-spacing: -0.5px;
}

.wallet-card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 13px;
  opacity: 0.8;
}

.wallet-delete-btn {
  background: transparent;
  border: none;
  color: rgba(255, 255, 255, 0.6);
  cursor: pointer;
  padding: 4px;
  border-radius: 4px;
  transition: all 0.2s;
}

.wallet-delete-btn:hover {
  color: var(--danger);
  background: rgba(255, 255, 255, 0.1);
}

/* ==========================================================================
   Planning > Categories
   ========================================================================== */

/* 220px, not 320px. The old minimum was set when a card carried the name, the
   type, the spend, the limit, a full-width bar, a caption and two icon buttons;
   at 320px a 375px phone could only ever fit one column, so nine categories were
   nine screenfuls. The card lost everything that was not the answer to "which of
   these is close to its limit" — the rest is one tap away in the detail dialog
   — and what is left fits two abreast with room to spare. */
.budgets-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
}

/* A <button>, so the reset comes first: a button inherits neither the page font
   nor its colour, and centres its own text. */
.budget-card {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  /* The token, NOT inherit. <body> is given its colour once from the dark
     palette and does not re-take it from --text-main when .light-theme goes on,
     so inheriting picked up #e6edd5 — pale green text on a white card. Every
     other element here is coloured from a token and never noticed; a <button>
     has to state one, so this is where it showed up. */
  color: var(--text-main);
  text-align: left;
  cursor: pointer;
  width: 100%;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 16px;
  padding: 14px;
  box-shadow: 0 4px 15px var(--shadow-color);
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: transform 0.15s ease, border-color 0.15s ease;
}

.budget-card:hover {
  border-color: var(--green-matcha);
}

/* Pressed feedback the card needs precisely because it is now the tap target:
   without it there is nothing between the tap and the dialog opening. */
.budget-card:active {
  transform: scale(0.985);
}

.budget-card:focus-visible {
  outline: 2px solid var(--green-matcha);
  outline-offset: 2px;
  border-color: var(--green-matcha);
}

.budget-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.tag-icon-wrap {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  /* The name beside it wraps; without this the icon is what gets squeezed. */
  flex: 0 0 auto;
}

.tag-icon-wrap svg {
  width: 15px;
  height: 15px;
}

/* Two lines, then an ellipsis. Truncating at one line loses the half of a name
   that distinguishes "Food" from "Food (work)", and letting it run to three
   makes the cards in a row different heights. */
.budget-card-name {
  font-weight: 700;
  font-size: 13px;
  line-height: 1.25;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.budget-card-figures {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 6px;
}

.budget-spent {
  font-size: 15px;
  font-weight: 700;
}

/* "of ฿5,000", not "Monthly limit: ฿5,000". Which period is selected is
   already stated once, in the select at the top of the list; repeating it on
   every card spent a line each time to say something that cannot vary between
   them. */
.budget-max {
  font-size: 11px;
  color: var(--text-muted);
}

.budget-progress-bar {
  display: block;
  height: 6px;
  background: var(--border-color);
  border-radius: 3px;
  overflow: hidden;
}

.budget-progress-fill {
  display: block;
  height: 100%;
  border-radius: 3px;
  transition: width 0.4s ease, background-color 0.4s ease;
}

.budget-card-pct {
  font-size: 10px;
  color: var(--text-muted);
}

/* The one line a category with no budget gets. Its type is the only thing left
   worth saying about it, and a budgeted card does not repeat it because a
   budget already implies an expense — isBudgetedCategory requires it. */
.budget-card-kind {
  font-size: 10px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Two columns on a phone. The gap shrinks with them: 20px between cards that
   are 165px wide is an eighth of the screen spent on nothing. */
@media (max-width: 768px) {
  .budgets-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }

  .budget-card {
    padding: 12px;
    border-radius: 14px;
    gap: 6px;
  }
}

/* The detail dialog's own header — the category has an icon and a colour, and
   those are how it is recognised in every list it appears in. Body rows below
   it are the shared .tx-detail-* set. */
.cat-detail-headline {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 18px;
}

.cat-detail-icon {
  width: 40px;
  height: 40px;
  border-radius: 12px;
}

.cat-detail-icon svg {
  width: 20px;
  height: 20px;
}

/* Edit takes the room; Delete is sized to be deliberate rather than convenient.
   It is the destructive one, and it used to sit 4px from Edit on the card. */
.cat-detail-actions {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

.cat-detail-actions .btn-secondary {
  flex: 1 1 auto;
}

.cat-detail-actions .btn-danger {
  flex: 0 0 auto;
}

/* Status colors for budgets */
.status-safe {
  background-color: var(--green-matcha);
  color: var(--green-matcha);
}
.status-warn {
  background-color: var(--bamboo-gold);
  color: var(--bamboo-gold);
}
.status-danger {
  background-color: var(--autumn-terracotta);
  color: var(--autumn-terracotta);
}

/* A month with nothing recorded in it. Not a status — the absence of one.
   The savings bar used to land on .status-danger here, because "no income" and
   "saved 0% of your income" were the same number to it, so an empty ledger
   opened on a red bar. */
.status-idle {
  background-color: var(--border-color);
  color: var(--text-muted);
}

/* The .status-* classes above set background AND text colour to the same value,
   so one class can both fill a progress bar and tint a label. On a text label
   that combination paints coloured text on an identically-coloured block, i.e.
   an invisible label — this modifier drops the background for that use. Declared
   after them so it wins at equal specificity. */
.status-text {
  background-color: transparent;
}

/* ==========================================================================
   Ledger Transaction History List
   ========================================================================== */
/* Four labelled fields in a 2fr 1fr 1fr 1fr grid, which collapsed to one field
   per row on a phone: four labels, four controls, a two-line hint and 20px of
   padding came to roughly 460px — more than half a phone screen, above a list
   of transactions, on the tab whose only job is showing transactions.

   Stacked flex now, with the three selects sharing one row. The labels are gone
   from the markup (each control's default option already names what it filters)
   so this is two rows and a toggle, about 150px. */
.ledger-filter-bar {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 16px;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  box-shadow: 0 4px 15px var(--shadow-color);
}

/* minmax(0, 1fr), not 1fr: a grid track's default minimum is its content, and a
   <select> holding a long wallet name would push the row wider than the card. */
.ledger-filter-selects {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

.ledger-filter-selects .form-control {
  padding: 9px 10px;
  font-size: 13px;
}

/* Wide enough to have the room: the search goes back beside the selects rather
   than above them, and the toggle keeps its own line under both. */
@media (min-width: 769px) {
  .ledger-filter-bar {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    padding: 16px;
  }

  .ledger-search {
    flex: 1 1 240px;
  }

  .ledger-filter-selects {
    flex: 2 1 420px;
  }

  .ledger-future-toggle {
    flex: 1 0 100%;
  }
}

/* Full width under the four fields. A switch is not a field, and the grid above
   is already tight at 2fr 1fr 1fr 1fr — a fifth column squeezed all of them. */
/* ---- Planning > Categories -------------------------------------------- */

/* The toolbar above the merged list. Budgets and Tags were two tabs, and the
   Budgets one existed to show ONLY what had a limit; this filter is what gives
   that view back now they are one list. Same shape as the ledger's filter bar,
   because it does the same job one screen over. */
.category-filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 16px;
  margin-bottom: 16px;
}

.category-filter-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  font-size: 13px;
  color: var(--text-main);
}

.category-filter-input {
  width: 18px;
  height: 18px;
  accent-color: var(--green-matcha);
  cursor: pointer;
}

.category-budgeted-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--green-matcha);
  color: var(--text-dark);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.category-budgeted-count[hidden] {
  display: none;
}

.category-period-select {
  margin: 0;
  padding: 6px 12px;
  height: auto;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background: var(--glass-bg);
}

/* Was an inline style on the div this replaced. */
.budget-progress-caption {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  color: var(--text-muted);
  margin-top: 2px;
}

/* A category with no budget has no bar and no figures, so its card is shorter.
   The grid rows stay ragged rather than padding it out to match: reserving the
   height of a progress bar under a category that has none draws attention to an
   absence, and a phantom empty bar reads as a limit at 0%. */
.budget-card-plain {
  align-self: start;
}

/* The limits appear with the tick that gives them something to apply to. */
.budget-limit-fields[hidden] {
  display: none;
}

/* Same tap-target reasoning as the ledger toggle below. */
/* nowrap, so the checkbox and the period select share one row instead of taking
   one each. With the subtab bar and the Add button above them, two rows here put
   the first category below the fold on a 375px screen. */
@media (max-width: 768px) {
  .category-filter-label {
    min-height: 44px;
    font-size: 12px;
  }

  .category-filter-bar {
    flex-wrap: nowrap;
    gap: 10px;
    margin-bottom: 12px;
  }

  .category-period-select {
    font-size: 12px;
    padding: 6px 8px;
    flex: 0 0 auto;
  }
}

/* The grey hint under this is gone — two lines explaining a control whose own
   label reads "Show upcoming transactions". The rule that styled it went with
   it; the border above is what still separates the switch from the fields. */
.ledger-future-toggle {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  padding-top: 2px;
  border-top: 1px solid var(--border-color);
}

.ledger-future-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  font-size: 13px;
  font-weight: 500;
  margin: 0;
  /* The label wraps the input, so a tap anywhere on the text toggles it — which
     on a phone is the difference between a 16px target and a whole line. */
  padding: 6px 0;
}

.ledger-future-input {
  width: 16px;
  height: 16px;
  accent-color: var(--green-matcha);
  cursor: pointer;
  /* Flex would otherwise shrink the box when the label wraps. */
  flex: 0 0 auto;
}

/* The count of rows currently being held back. Hidden at zero rather than shown
   as "(0)", which invites the reader to work out whether it is broken. */
.ledger-upcoming-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--green-matcha);
  color: var(--text-dark);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.ledger-upcoming-count[hidden] {
  display: none;
}

/* The label is the tap target on a phone, and a 16px checkbox on a 28px line is
   a miss waiting to happen. Height rather than padding so the hint below it
   does not get pushed away from the control it explains. */
@media (max-width: 768px) {
  .ledger-future-label {
    min-height: 44px;
  }
}

/* An upcoming row has not happened. It is in no wallet balance —
   recalculateWalletBalances replays only up to the simulated today — so it is
   dimmed to sit visibly apart from settled money rather than among it. */
.tx-row-upcoming {
  opacity: 0.72;
}

.tx-upcoming-pill {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--border-color);
  background: var(--bg-secondary);
  color: var(--text-muted);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
  /* The date cell is the row's anchor on desktop and a labelled line on mobile;
     the pill must not stretch the line height in either. */
  vertical-align: middle;
  white-space: nowrap;
}

/* Ledger pagination bar. display is toggled between none and flex by
   renderLedgerPagination, so the value here is only the starting point. */
.ledger-pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 4px 4px;
  border-top: 1px solid var(--border-color);
  margin-top: 8px;
}

.ledger-pagination-size,
.ledger-pagination-nav {
  display: flex;
  align-items: center;
  gap: 10px;
}

.ledger-pagination-size label {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
}

/* Sized to its content so the rows-per-page control does not stretch to the
   full-width .form-control default it inherits. */
.ledger-pagination-size .form-control {
  width: auto;
  padding: 6px 10px;
}

#ledgerPageStatus {
  font-size: 12px;
  color: var(--text-muted);
  font-family: 'Outfit', sans-serif;
  white-space: nowrap;
  /* The text swings between "1–20 of 7" and "981–1000 of 1000" as pages
     change; without a floor, the buttons on either side jump around under the
     cursor between clicks. */
  min-width: 170px;
  text-align: center;
}

.btn-page {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  color: var(--text-main);
  border-radius: 10px;
  padding: 7px 14px;
  font-size: 12px;
  font-weight: 600;
  font-family: 'Outfit', sans-serif;
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease;
}

.btn-page:hover:not(:disabled) {
  border-color: var(--green-mint);
  color: var(--green-mint);
}

/* Disabled at the first and last page. Kept visible rather than hidden so the
   bar does not reflow and slide the other button under the user's cursor. */
.btn-page:disabled {
  opacity: 0.4;
  cursor: default;
}

@media (max-width: 600px) {
  .ledger-pagination {
    justify-content: center;
  }
}

.input-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* Grid items (e.g. inside .form-row) default to min-width: auto, which
     pins them to their content's intrinsic width — a <select> with long
     option text (like "Income Deposit") would otherwise force the row
     wider than its track and get clipped on narrow viewports. */
  min-width: 0;
}

/* .input-label is the icon picker's group heading. It cannot be a <label>: the
   control is a grid of buttons whose only form field is hidden, so there is
   nothing for `for` to point at. Same styling, named via aria-labelledby. */
.input-group label,
.input-group .input-label {
  font-size: 12px;
  color: var(--text-muted);
  font-weight: 600;
}

/* The icon picker replaces a <select>, which could only ever list emoji —
   an <option> renders text, never markup. Tiles are real buttons so they stay
   Tab-reachable and usable without a pointer.

   Deliberately shaped after .color-picker-grid below, which solves the same
   problem in the same dialog: a fixed column count of small square swatches,
   no per-swatch caption. The first version used minmax(74px) tiles with a text
   label under each glyph, which on a 375px phone fell to three columns and four
   rows — about 236px, roughly three times the colour picker for fewer items,
   and enough to push the submit button off screen. */
.icon-picker {
  display: grid;
  /* Fixed 5, not auto-fill: the count must not drift with viewport width, or
     ten icons reflow between two and three rows and the dialog changes height
     as the on-screen keyboard opens. Five columns gives exactly two rows. */
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
}

/* The heading row. The chosen icon's NAME lives here rather than under every
   tile — it is the one thing the per-tile labels genuinely carried, and stating
   it once costs a single line instead of ten. */
.icon-picker-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.icon-picker-current {
  font-size: 12px;
  font-weight: 600;
  color: var(--green-matcha);
}

.icon-choice {
  display: flex;
  align-items: center;
  justify-content: center;
  /* 40px, a little taller than the 36px colour swatch: a glyph needs more
     breathing room inside its box than a flat fill does, and it keeps the tile
     nearer a comfortable touch target. */
  height: 40px;
  padding: 0;
  background: var(--bg-primary);
  border: 1px solid var(--border-color);
  border-radius: 8px;
  color: var(--text-muted);
  font-family: inherit;
  cursor: pointer;
  transition: border-color var(--transition-speed), color var(--transition-speed),
              background-color var(--transition-speed);
}

.icon-choice:hover {
  border-color: var(--green-matcha);
  color: var(--text-main);
}

.icon-choice.selected {
  border-color: var(--green-matcha);
  background: var(--border-glow);
  color: var(--green-matcha);
}

.icon-choice:focus-visible {
  outline: 2px solid var(--green-matcha);
  outline-offset: 2px;
}

.icon-choice-glyph {
  display: inline-flex;
  width: 19px;
  height: 19px;
}

/* SVG_ICONS markup carries class="icon", which .icon pins to 18x18 — the same
   mismatch that made the category chip's glyph paint outside its pill. Sizing
   to the wrapper keeps every tile's glyph identical. */
.icon-choice-glyph .icon {
  width: 100%;
  height: 100%;
}

.form-control {
  background: var(--bg-primary);
  border: 1px solid var(--border-color);
  border-radius: 10px;
  padding: 10px 14px;
  font-size: 14px;
  outline: none;
  transition: border-color 0.2s;
  width: 100%;
}

.form-control:focus {
  border-color: var(--green-matcha);
}

.ledger-table-container {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 24px;
  /* overflow: hidden clipped the table's columns entirely on narrow
     viewports instead of letting users scroll to see them. Below 768px
     the table reflows into cards (see media query below) where scrolling
     isn't needed; above that, horizontal scroll remains the fallback. */
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  box-shadow: 0 8px 24px var(--shadow-color);
}

.ledger-table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

.ledger-table th {
  padding: 18px 24px;
  font-family: 'Outfit', sans-serif;
  font-size: 14px;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-color);
  background: rgba(139, 168, 143, 0.05);
}

.ledger-table td {
  padding: 18px 24px;
  border-bottom: 1px solid var(--border-color);
  font-size: 14px;
}

.ledger-table tr:last-child td {
  border-bottom: none;
}

.ledger-table tr:hover td {
  background: rgba(139, 168, 143, 0.03);
}

.tx-title-cell {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
}

/* Title and note share the first line; the chip gets the second to itself.
   Chip and note previously sat side by side with a 6px gap and nothing
   between them, so "Entertainment April 2027" read as one run-on phrase. */
.tx-title-line {
  display: flex;
  align-items: baseline;
  gap: 8px;
  /* stretch, not the parent's flex-start, so the note has a real width to be
     truncated against rather than sizing to its own content. */
  align-self: stretch;
  min-width: 0;
}

.tx-title-main {
  font-weight: 600;
  /* The title anchors the row — the note yields space, never this. */
  flex: 0 0 auto;
}

/* The title is a real <button>, so the detail dialog is reachable by keyboard
   and announced as an action rather than as text. Every scrap of button chrome
   is stripped here: it has to render identically to the <span> it replaced. */
.tx-detail-btn {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  font-weight: 600;
  color: inherit;
  text-align: left;
  cursor: pointer;
}

/* Was three inline declarations on the cell. Moved to a class so the mobile
   card can override the weight and size — an inline style cannot be. */
.tx-date-cell {
  font-weight: 600;
  font-family: 'Outfit', sans-serif;
  white-space: nowrap;
}

/* The detail dialog. It is what makes the mobile card safe to strip down: the
   card shows four fields, this shows all of them, so nothing is unreachable. */
.tx-detail-headline {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 18px;
}

.tx-detail-headline .tx-detail-amount {
  font-family: 'Outfit', sans-serif;
  font-size: 28px;
  font-weight: 700;
}

.tx-detail-headline .tx-detail-name {
  font-size: 15px;
  font-weight: 600;
  /* A long title wraps here rather than truncating. This is the one place the
     full text is guaranteed to be readable, so it must not be cut off. */
  overflow-wrap: anywhere;
}

.tx-detail-list {
  display: flex;
  flex-direction: column;
  margin: 0;
}

.tx-detail-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border-color);
}

.tx-detail-row:last-child {
  border-bottom: none;
}

.tx-detail-row dt {
  font-size: 12px;
  color: var(--text-muted);
  flex: 0 0 auto;
}

/* flex, not text-align: the wallet badge and category chip are inline-flex
   boxes, and text-align would leave them pinned left while the plain-text
   values beside them sat right. As flex items they all end at the same edge,
   and wrapping keeps a two-badge transfer from overflowing the row. */
.tx-detail-row dd {
  margin: 0;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  text-align: right;
  overflow-wrap: anywhere;
}

/* The category chip and the scheduled note share one wrap-friendly row.
   The chip used to own a whole column, which measured 174px and — unlike
   every other column — never shrank at any viewport width, so it starved
   Amount until the sign broke onto its own line. Pairing chip with note on a
   single row also keeps a scheduled entry two lines tall rather than three. */
.tx-title-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

.tx-title-note {
  font-size: 11px;
  color: var(--text-muted);
  /* A long note truncates instead of wrapping: wrapping would grow the row and
     undo the two-line height the meta row was restructured to protect. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Muted like the old inline style, but WITHOUT its text-align:right — that
   made the transfer amount the only one in the column not sharing the
   left edge that .ledger-table's text-align:left gives income and expense. */
.amount-transfer {
  color: var(--text-muted);
  font-weight: 500;
}

/* No width here on purpose. The column is content-sized — 54px of buttons plus
   the cell's 24px padding either side — and measures 102px regardless of what
   the <th> width hint says, so pinning it changes nothing. These two rules
   exist only to lift the button group's layout out of inline styles. */
.tx-actions-cell {
  white-space: nowrap;
}

.tx-actions-group {
  display: flex;
  gap: 6px;
  justify-content: center;
}

.tx-amount-cell {
  font-weight: 700;
  font-family: 'Outfit', sans-serif;
  font-size: 15px;
  /* "-" and "+" are line-break opportunities (UAX #14 classes HY and PR), so
     the sign printed alone on its own line and pushed the digits beneath it
     once the column got tight — the reported "6+ digits look weird". nowrap
     is the entire fix; tabular-nums keeps digits aligned down the money
     column now that every amount is guaranteed to be one line. */
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.amount-income {
  color: var(--green-matcha);
}

.amount-expense {
  color: var(--autumn-terracotta);
}

@media (max-width: 768px) {
  /* No `main` padding rule here on purpose. The obvious place to reclaim side
     margin, main's 40px, is already cut to 24px 16px by the max-width:1080px
     query near the end of this file — which, being later in source order, would
     win over anything written here anyway. Measured at 360px the row gets 326px
     to work with, so the space was never the problem; the wrapping was. */

  .ledger-table-container {
    overflow-x: visible;
    /* 24px of corner reads as inset on a list this dense. */
    border-radius: 16px;
  }

  .ledger-table,
  .ledger-table thead,
  .ledger-table tbody,
  .ledger-table tr,
  .ledger-table td {
    display: block;
    width: 100%;
  }

  /* A five-column key for a layout that no longer has columns. */
  .ledger-table thead {
    display: none;
  }

  .ledger-table td {
    border-bottom: none;
    padding: 0;
    width: auto;
  }

  /* Two lines, stated rather than negotiated.
   *
   * The previous version was a wrapping flex row, so the card's shape depended
   * on how long each title happened to be: five cells flowed until they ran out
   * of width and broke wherever they landed. Measured at 360px that gave three
   * lines and 263px per transaction — about three rows per screen — the last
   * line being an actions row with an empty left half.
   *
   * A grid fixes the shape, so every card is the same height whatever is in it:
   *
   *     title   date    .       amount  actions
   *     wallet  wallet  wallet  amount  actions
   *
   * Amount and actions span both rows down the right. The empty cell is what
   * absorbs the slack — without it the 1fr would land between title and date
   * and push the date away from the title it belongs to.
   *
   * The title track is a percentage, not `auto`, and that is the whole point of
   * it: sized to its content the column was as wide as each title happened to
   * be, so the date started at a different x on every card — measured 91px on
   * one row and 175px on the next — and the column of dates came out ragged.
   * A percentage resolves against the row's content box alone, so it does not
   * move when the title, the amount or the wallet changes width. The date lands
   * in the same place on every card, near the middle.
   *
   * minmax(0, …) rather than a bare 34%: it lets the track give way if an
   * unusually long amount would otherwise push the row wider than the screen.
   * The title truncates instead, which is the one field that can afford to.
   *
   * 34% is chosen from a measurement, not by eye. At 360px the row has 302px of
   * content box; the date (59px), the buttons (24px) and four 8px gaps are all
   * fixed, leaving 187px to share between the title, the empty cell and the
   * amount. A title track of 34% takes 103px of that and leaves 84px of slack —
   * which is what the widest amount these currencies produce actually measures,
   * "-฿450,000.00" at 84px. Anything roomier and the amount eats into the title
   * track and drags the date with it: 45% left only 43px of slack and a
   * five-figure total pulled the date 40px out of line, 38% left 72px and the
   * same total still moved it 12px. */
  .ledger-table tbody tr {
    display: grid;
    grid-template-columns: minmax(0, 34%) auto 1fr auto auto;
    grid-template-areas:
      "title  date   .      amount actions"
      "wallet wallet wallet amount actions";
    align-items: center;
    /* 8px, not 10: four gaps sit between these five tracks, so the two pixels
       are worth 8px of slack against the amount — see the budget above. */
    column-gap: 8px;
    row-gap: 3px;
    padding: 10px 12px;
    /* The per-cell borders are gone above, so without this the cards run
       together into one block with no visible boundary between transactions. */
    border-bottom: 1px solid var(--border-color);
    /* Anchors the stretched hit area on .tx-detail-btn below. */
    position: relative;
  }

  .ledger-table tbody tr:last-child {
    border-bottom: none;
  }

  .ledger-table td[data-label="Transaction"] { grid-area: title; min-width: 0; }
  .ledger-table td[data-label="Wallet"]      { grid-area: wallet; min-width: 0; }
  /* 13px rather than the desktop 14px. This is the track competing with the
     title for what slack the row has, so a point of size here is several pixels
     of headroom before a large total starts pushing the date out of line. */
  .ledger-table td.tx-amount-cell            { grid-area: amount; text-align: right; font-size: 13px; }

  .ledger-table td.tx-date-cell {
    grid-area: date;
    font-size: 11px;
    font-weight: 500;
    color: var(--text-muted);
  }

  .ledger-table td.tx-actions-cell {
    grid-area: actions;
    /* Lifted above the stretched hit area, or Edit and Delete stop responding. */
    position: relative;
    z-index: 1;
  }

  /* block, not the flex row it is at desktop width: the note is the only other
     thing in this line and it is hidden below, so there is nothing left to lay
     out, and `flex: 0 0 auto` on the title would stop it shrinking. */
  .tx-title-line {
    display: block;
    min-width: 0;
  }

  /* The only field allowed to truncate. Everything else in the card is short
     and fixed-width, so the title is the one that absorbs the overflow.

     max-width is what actually makes that happen, and it is easy to leave out:
     a <button> shrink-wraps its content even at display:block, so overflow and
     text-overflow have nothing to clip against. Measured before adding it, a
     long title rendered 328px wide inside a 136px cell and printed straight
     across the date and the amount. */
  .tx-title-main {
    display: block;
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Dropped, not shrunk. The category chip is the widest element in the card
     for the least information per pixel, and the wallet takes its place on line
     two; the note is usually a restatement of the title. Neither is lost —
     tapping the card opens the detail dialog, which shows every field. */
  .tx-title-note,
  .tx-title-meta {
    display: none;
  }

  /* Side by side these two cost a full-width row of their own. Stacked they fit
     inside the height the card already has for its two lines of text. */
  .tx-actions-group {
    flex-direction: column;
    gap: 0;
  }

  /* Makes the whole card the tap target for the detail dialog. The button is
     real and focusable at every width; this only grows its hit area. A click
     handler on the <tr> would have been simpler and unreachable by keyboard. */
  .tx-detail-btn::after {
    content: '';
    position: absolute;
    inset: 0;
  }
}

.badge-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 8px;
  font-size: 12px;
  font-weight: 600;
  background: rgba(139, 168, 143, 0.1);
}

/* Compact variant for the chip now that it rides inside the Transaction cell
   as secondary detail rather than heading its own column. Same colours, so a
   tag stays recognisable; smaller box, so it reads as a label on the title
   instead of competing with it. */
/* SVG_ICONS markup carries class="icon", which .icon sizes at 18x18 — bigger
   than the 12x12 box this chip gives it. As a flex item the glyph's WIDTH
   shrank to fit but its HEIGHT did not, so it painted 3px below the chip's
   background (which is overflow:visible) and the icon sat visibly low, hanging
   off the bottom of the pill. Filling the wrapper is the fix; the global .icon
   is left alone so nothing else on the page resizes. */
.badge-tag-sm .icon {
  width: 100%;
  height: 100%;
}

.badge-tag-sm {
  gap: 4px;
  padding: 2px 8px;
  border-radius: 6px;
  font-size: 11px;
  white-space: nowrap;
}

.badge-wallet {
  display: inline-flex;
  /* Was the initial `stretch`: on a wallet whose name wrapped to two lines,
     the currency code stretched the full badge height and its glyphs sat
     against the top edge, which is what made a wrapped badge look broken
     rather than merely tall. Centring aligns it to the name on either
     line count. */
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 500;
  line-height: 1.35;
  max-width: 100%;
  border: 1px solid var(--border-color);
  background: var(--bg-primary);
}

/* Never let the currency code be what wraps: it is three characters of
   context, so orphaning it on its own line reads as a second wallet. */
.badge-wallet-currency {
  font-size: 9px;
  opacity: 0.6;
  white-space: nowrap;
}

.action-btn-row {
  display: flex;
  gap: 8px;
}

/* The frameless twin of .btn-icon-danger. js/transactions.js already emitted
   this class with the same declarations written inline; the schedule card emits
   it clean. Frameless is the choice made across both lists: the schedule card's
   Edit button was the only framed one, which made two buttons sitting side by
   side look like different kinds of control. */
.btn-icon-secondary {
  background: transparent;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 8px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s;
}

.btn-icon-secondary:hover {
  color: var(--text-main);
  background: rgba(139, 168, 143, 0.12);
}

.btn-icon-danger {
  background: transparent;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 6px;
  border-radius: 6px;
  transition: all 0.2s;
}

.btn-icon-danger:hover {
  color: var(--danger);
  background: var(--danger-soft);
}

/* Empty State Ledger Placeholder */
.empty-placeholder {
  padding: 60px;
  text-align: center;
  color: var(--text-muted);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.empty-placeholder svg {
  width: 48px;
  height: 48px;
  opacity: 0.4;
}

/* ==========================================================================
   Scheduled Recurrent Tasks Lists
   ========================================================================== */
.schedules-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.schedule-item {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 18px;
  padding: 20px 24px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-shadow: 0 4px 15px var(--shadow-color);
  transition: transform 0.2s;
}

.schedule-item:hover {
  transform: translateX(4px);
  border-color: var(--green-matcha);
}

.schedule-meta {
  display: flex;
  align-items: center;
  gap: 20px;
}

/* Holds the frequency pill and, when a schedule is off, the Paused pill. */
.sched-badge-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.sched-frequency-badge {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  background: rgba(207, 168, 123, 0.2);
  color: var(--bamboo-gold);
  padding: 4px 8px;
  border-radius: 6px;
}

/* The word the Pause/Resume button used to carry. Without it a paused schedule
   is distinguishable only by the colour of a 4px border. Same shape as the
   frequency pill, in the terracotta already used for outgoings (#e07a5f). */
.sched-paused-badge {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  background: rgba(224, 122, 95, 0.18);
  color: var(--autumn-terracotta);
  padding: 4px 8px;
  border-radius: 6px;
  white-space: nowrap;
}

.sched-details {
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* Flex and grid items default to min-width:auto, so a long wallet name in the
     labels row would push this column wider than the card. */
  min-width: 0;
}

.sched-title {
  font-weight: 600;
  font-size: 16px;
}

/* The title is a real <button>, so the detail dialog is reachable by keyboard
   and announced as an action rather than as text. Every scrap of button chrome
   is stripped here: it has to render identically to the <span> it replaced.
   Kept separate from .tx-detail-btn rather than shared, because the ledger's
   copy is pinned by a test to the exact class pair that row emits. */
.sched-detail-btn {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  font-weight: 600;
  font-size: 16px;
  color: inherit;
  text-align: left;
  cursor: pointer;
  overflow-wrap: anywhere;
}

/* The wallet badge and category chip, in the same two shapes the ledger row
   draws. Wrapping so a long pair drops to a second line instead of overflowing. */
.sched-labels {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.sched-sub {
  font-size: 12px;
  color: var(--text-muted);
}

.sched-finances {
  display: flex;
  align-items: center;
  gap: 24px;
}

.sched-amount {
  font-size: 18px;
  font-weight: 700;
  font-family: 'Outfit', sans-serif;
}

/* Was three inline declarations on the wrapper div. Moved to a class so the
   mobile card can turn the row into a column — an inline style cannot be. */
.sched-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* .btn-icon-danger carries 6px padding and no inline-flex of its own, so left
   alone it measured 28x31 beside the 32x32 Edit button — a mismatch that reads
   as a misalignment now that neither has a frame to hide behind. Scoped rather
   than fixed on .btn-icon-danger itself, which the ledger row also uses. */
.sched-actions .btn-icon-danger {
  padding: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* --------------------------------------------------------------------------
   The schedule card on a phone.

   This block sits AFTER every .sched-* rule it overrides on purpose. Media
   queries add no specificity, so a base rule written later in this file beats
   an identically-specific one inside an earlier @media — the same trap that
   silently disabled half the ledger card block.

   The desktop card is one flex row: badge, details, amount, buttons,
   space-between. At 360px that row has five things wanting roughly 500px, and
   nothing in it may shrink, so the buttons were pushed past the right edge and
   could only be reached by scrolling sideways. The fix is to stop negotiating
   and state the shape, one thing per line:

       badge                    edit  delete
       title
       AMOUNT
       [wallet] [tag]
       Next Auto-Occurrence: ...

   The three wrappers become display:contents so their children join the card's
   own grid — they exist for the desktop row, and removing them from the markup
   would mean rebuilding that layout too.
   -------------------------------------------------------------------------- */
@media (max-width: 768px) {
  .schedule-item {
    display: grid;
    /* minmax(0, 1fr) rather than 1fr: a grid item's default min-width is auto,
       so a long wallet or tag name would push the track wider than the card
       and put the sideways scroll straight back. The second track holds only
       the two icon buttons, so it never varies with the amount. */
    grid-template-columns: minmax(0, 1fr) auto;
    /* One thing per line, in reading order. The amount used to sit in the
       top-right corner, which is also where the controls live — it was
       competing with them for the eye. On its own full-width row nothing is
       near it. */
    grid-template-areas:
      "badge   actions"
      "title   title"
      "amount  amount"
      "labels  labels"
      "next    next"
      "notice  notice";
    align-items: start;
    column-gap: 12px;
    row-gap: 6px;
    padding: 14px 16px;
    border-radius: 16px;
    /* Anchors the stretched tap area below. */
    position: relative;
  }

  /* Hover translate is a desktop affordance; on a phone it only ever fires on
     tap, nudging the card sideways under the finger. */
  .schedule-item:hover {
    transform: none;
  }

  /* Both wrappers dissolve so their children become items of the card's own
     grid. They exist for the desktop row; removing them from the markup would
     mean rebuilding that layout too. */
  .schedule-meta,
  .sched-details,
  .sched-finances {
    display: contents;
  }

  .sched-badge-row { grid-area: badge; }
  .sched-title     { grid-area: title; }
  .sched-labels    { grid-area: labels; }
  .sched-next      { grid-area: next; }
  .sched-deactivated { grid-area: notice; }

  /* Dropped, not shrunk. The detail dialog carries the first and last
     occurrence dates; the card keeps only the date that is actually about to
     happen. */
  .sched-started {
    display: none;
  }

  /* The point of the whole rearrangement: big, alone on its line, and nowhere
     near a button. */
  .sched-amount {
    grid-area: amount;
    font-size: 22px;
    line-height: 1.1;
  }

  /* Back in the corner as a row of two — the column was for three buttons, one
     of which (Pause) has moved into the edit form. Two 32px icons take less
     room across than down and leave the description the full card width.
     z-index lifts them out from under the card-wide tap area. */
  .sched-actions {
    grid-area: actions;
    align-self: start;
    gap: 4px;
    position: relative;
    z-index: 1;
  }

  /* Anywhere on the card opens the detail dialog. */
  .sched-detail-btn::after {
    content: '';
    position: absolute;
    inset: 0;
  }
}

/* ==========================================================================
   Modals & Popup Forms
   ========================================================================== */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(14, 18, 14, 0.7);
  backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  opacity: 0;
  /* pointer-events blocks the mouse but NOT the keyboard: with opacity alone,
     the 80 form controls inside these 10 closed modals stayed in the tab order,
     so tabbing through the app walked invisibly into every hidden form.
     visibility:hidden removes them from both the tab order and the a11y tree,
     and unlike display:none it can still be transitioned — delaying it by the
     fade duration keeps the close animation intact. */
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.3s ease, visibility 0s linear 0.3s;
}

.modal-overlay.active {
  opacity: 1;
  visibility: visible;
  pointer-events: all;
  transition: opacity 0.3s ease, visibility 0s;
}

.modal-window {
  /* Opaque, not --bg-card: see --surface-raised. This one line is what stops
     every dialog in the app — delete confirmations, and the transaction and
     schedule detail sheets on a phone — from painting itself grey-green. */
  background: var(--surface-raised);
  border: 1px solid var(--border-color);
  border-radius: 24px;
  width: 90%;
  max-width: 500px;
  padding: 30px;
  box-shadow: 0 15px 40px rgba(0, 0, 0, 0.5);
  transform: scale(0.9);
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  max-height: 90vh;
  overflow-y: auto;
}

.modal-overlay.active .modal-window {
  transform: scale(1);
}

/* A confirm carries one sentence, and 500px of it reads as a form with the
   fields missing. */
.modal-window-narrow {
  max-width: 420px;
}

.modal-confirm-message {
  color: var(--text-muted);
  line-height: 1.55;
  /* Some callers pass '\n's to separate a summary from its caveat. textContent
     preserves them; only this makes them visible. */
  white-space: pre-line;
}

.modal-confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 24px;
}

/* On a phone the buttons stack with the safe one underneath: a full-width
   destructive button sitting directly below the message is too easy to hit on
   the way to reading it. */
@media (max-width: 480px) {
  .modal-confirm-actions {
    flex-direction: column-reverse;
  }

  .modal-confirm-actions button {
    width: 100%;
  }
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
  border-bottom: 1px solid var(--border-color);
  padding-bottom: 12px;
}

.modal-header h3 {
  font-size: 20px;
}

.modal-close-btn {
  background: transparent;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: 4px;
  border-radius: 4px;
  transition: color 0.2s;
}

.modal-close-btn:hover {
  color: var(--danger);
}

.modal-form {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.form-error {
  display: none;
  margin: 0;
  padding: 10px 14px;
  border-radius: 8px;
  background: rgba(220, 53, 69, 0.12);
  color: #dc3545;
  font-size: 0.9rem;
  font-weight: 500;
}

.midori-toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translate(-50%, 16px);
  max-width: 90vw;
  padding: 12px 20px;
  border-radius: 10px;
  background: #dc3545;
  color: #fff;
  font-size: 0.9rem;
  font-weight: 500;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
  z-index: 9999;
}

.midori-toast.visible {
  opacity: 1;
  transform: translate(-50%, 0);
  pointer-events: auto;
}

.fx-rates-label {
  display: block;
  margin-top: 4px;
  font-size: 0.7rem;
  opacity: 0.6;
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

.form-checkbox-row {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  font-size: 13px;
  font-weight: 500;
  user-select: none;
}

.form-checkbox-row input {
  width: 16px;
  height: 16px;
  accent-color: var(--green-matcha);
}

.form-submit-btn {
  background: var(--green-matcha);
  color: #fff;
  border: none;
  border-radius: 12px;
  padding: 14px;
  font-weight: 700;
  cursor: pointer;
  transition: all 0.2s;
  box-shadow: 0 4px 10px var(--border-glow);
  margin-top: 10px;
}

.form-submit-btn:hover {
  background: var(--green-forest);
}

/* Color pickers */
.color-picker-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 10px;
}

.color-option {
  height: 36px;
  border-radius: 8px;
  cursor: pointer;
  border: 2px solid transparent;
  transition: transform 0.2s;
}

.color-option:hover {
  transform: scale(1.1);
}

.color-option.selected {
  border-color: #fff;
  box-shadow: 0 0 10px rgba(255,255,255,0.4);
}

/* ==========================================================================
   Utility classes & Settings
   ========================================================================== */
.settings-group {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: 20px;
  padding: 24px;
  box-shadow: 0 4px 15px var(--shadow-color);
  margin-bottom: 24px;
}

.settings-group h3 {
  font-size: 18px;
  margin-bottom: 12px;
}

.settings-group p {
  color: var(--text-muted);
  font-size: 14px;
  margin-bottom: 20px;
}

.settings-action-row {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}

.btn-secondary {
  /* A faint fill as well as the border. On a dialog the border alone left
     Cancel looking like a word someone had typed rather than a button you could
     press — and a barely-there Cancel next to a solid red Delete quietly pushes
     people towards the destructive one. */
  background: var(--btn-outline-bg);
  border: 1px solid var(--btn-outline);
  color: var(--text-main);
  padding: 12px 20px;
  border-radius: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-secondary:hover {
  background: rgba(139, 168, 143, 0.08);
  border-color: var(--green-sage);
}

.btn-danger {
  background: transparent;
  border: 1px solid var(--danger);
  color: var(--danger);
  padding: 12px 20px;
  border-radius: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-danger:hover {
  background: var(--danger-soft);
}
/* The affirmative button in a dialog action row. The row already had the two
   ways of saying no -- outline Cancel, outline destructive -- and nothing that
   reads as the thing you came here to do. Reusing .btn-secondary for it made
   Save and Cancel identical twins, which on a passphrase dialog is worse than
   plain: the two buttons have opposite consequences and looked the same.

   Filled rather than outlined, matching .form-submit-btn's green so an
   affirmative action looks the same whether it sits in a form or a dialog, and
   sharing the exact geometry of .btn-secondary/.btn-danger so the row's buttons
   line up on their edges rather than nearly doing so. */
.btn-primary {
  background: var(--green-matcha);
  border: 1px solid var(--green-matcha);
  color: #fff;
  padding: 12px 20px;
  border-radius: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-primary:hover {
  background: var(--green-forest);
  border-color: var(--green-forest);
}

/* Shown while the key is being derived. PBKDF2 at 310,000 iterations is not
   instant on a phone, and a button that looks pressable during it invites a
   second press. */
.btn-primary:disabled {
  opacity: 0.55;
  cursor: progress;
}
/* Automatic snapshot list (tier 1). Rows rather than cards: this is a list you
   scan for a date and leave, not one you browse. */
.snapshot-heading {
  margin: 22px 0 4px;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text-main);
}

.snapshot-note {
  margin: 0 0 12px;
  font-size: 12px;
  opacity: 0.65;
}

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

.snapshot-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border: 1px solid var(--btn-outline);
  border-radius: 12px;
  background: var(--btn-outline-bg);
}

.snapshot-when {
  flex: 1 1 auto;
  font-size: 13px;
  font-weight: 600;
  /* Dates line up in a column, so the digits should too. */
  font-variant-numeric: tabular-nums;
}

.snapshot-reason {
  flex: 0 0 auto;
  font-size: 11px;
  opacity: 0.6;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.snapshot-restore-btn {
  flex: 0 0 auto;
  padding: 7px 14px;
  font-size: 12px;
}

.snapshot-empty {
  margin: 0;
  font-size: 13px;
  opacity: 0.6;
}

/* The reason label is the first thing to go: on a narrow screen the date and
   the action are what the row is for. */
@media (max-width: 420px) {
  .snapshot-reason { display: none; }
}

/* Text utility icons */
.icon {
  width: 18px;
  height: 18px;
  display: inline-block;
  vertical-align: middle;
}

/* Tab display toggles */
.tab-content {
  display: none;
  animation: fadeIn 0.4s ease;
}

.tab-content.active {
  display: flex;
  flex-direction: column;
  gap: 30px;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ==========================================================================
   Subtabs — the segmented control inside Overview and Category
   ========================================================================== */

/* Title and control sit closer together than the section's 30px rhythm, so
   they read as one header rather than two stacked blocks. */
.section-head {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* Tablist on the left, the active view's own control on the right. */
.subtab-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}

.subtabs {
  position: relative;
  display: inline-flex;
  padding: 4px;
  background: var(--glass-bg);
  border: 1px solid var(--border-color);
  border-radius: 12px;
}

/* The indicator moves by whole segment widths, so every segment has to be the
   same width — that is what flex plus a shared min-width buys. It means the
   slide needs no measurement in JS: no resize listener, and no misalignment
   when the webfont swaps in and the label widths change. --subtab-count and
   --subtab-index are set by selectSubtab() in js/ui-core.js. */
.subtab-indicator {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 4px;
  width: calc((100% - 8px) / var(--subtab-count, 2));
  border-radius: 9px;
  background: rgba(77, 156, 78, 0.18);
  border: 1px solid var(--border-glow);
  transform: translateX(calc(var(--subtab-index, 0) * 100%));
  transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
  pointer-events: none;
}

.subtab {
  position: relative;
  z-index: 1;
  /* flex-basis MUST stay 0, not auto. With `auto` each segment starts from its
     own label width, so "Budgets" and "Tags" end up different sizes and the
     indicator — which is one fraction of the track — no longer lines up with
     either. Desktop hid this because both labels clamped to min-width; it only
     showed once the track stretched on a phone. */
  flex: 1 1 0;
  min-width: 124px;
  padding: 9px 14px;
  background: none;
  border: 0;
  border-radius: 9px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--transition-speed);
}

.subtab:hover {
  color: var(--text-main);
}

/* Neutral, not green. The selected pill is already green, so mint text on it
   read as one green smudge. Contrast against the composited pill, per theme:

     dark   mint 8.14:1  ->  body text 11.55:1   (unselected labels: 6.83:1)
     light  mint 1.39:1  ->  body text 12.26:1   (unselected labels: 5.10:1)

   Light theme was the real damage: 1.39:1 is far under the 4.5:1 floor and well
   under its own unselected neighbours, so selecting a tab made its label the
   least readable thing in the control. Body text clears AA in both themes and
   is brighter than its neighbours, which is the direction selection should run.
   The weight bump adds a second, non-colour signal for anyone who cannot
   separate the two greens. */
.subtab[aria-selected="true"] {
  color: var(--text-main);
  font-weight: 600;
}

/* Keyboard focus has to stay visible: the arrow keys move focus between
   segments, and with no ring nothing on screen says which one holds it. */
.subtab:focus-visible {
  outline: 2px solid var(--green-matcha);
  outline-offset: 2px;
}

.subtab-action {
  display: none;
}

.subtab-action.active {
  display: flex;
  align-items: center;
  gap: 12px;
}

.tab-pane {
  display: none;
}

.tab-pane.active {
  display: flex;
  flex-direction: column;
  gap: 30px;
  animation: fadeIn 0.3s ease;
}

/* Narrow screens: the control stretches to full width and the action wraps
   onto its own line beneath it, rather than squeezing both onto one row. */
@media (max-width: 620px) {
  .subtabs {
    display: flex;
    width: 100%;
  }

  .subtab {
    min-width: 0;
  }

  .subtab-action.active {
    width: 100%;
  }
}

/* ==========================================================================
   Quick User Guide (Settings) — collapsible card
   ========================================================================== */
.guide-card {
  border-left: 4px solid var(--green-matcha);
  background: rgba(77, 156, 78, 0.04);
  margin-bottom: 24px;
  padding: 24px;
}

/* A real <button> wrapping the heading: it is the control that opens and closes
   the card, so it has to be focusable and reachable by Enter/Space without any
   extra keyboard handling of our own. */
.guide-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  text-align: left;
}

.guide-toggle-title {
  font-family: 'Outfit', sans-serif;
  font-size: 18px;
  font-weight: 600;
  color: var(--green-sage);
}

/* Pushed to the right by margin-left:auto so the chevron sits at the far edge
   whatever the title's length. */
.guide-toggle-state {
  margin-left: auto;
  font-size: 12px;
  color: var(--text-muted);
}

.guide-chevron {
  flex-shrink: 0;
  color: var(--green-sage);
  transition: transform var(--transition-speed);
}

.guide-toggle[aria-expanded="false"] .guide-chevron {
  transform: rotate(-90deg);
}

.guide-toggle:focus-visible {
  outline: 2px solid var(--green-matcha);
  outline-offset: 4px;
  border-radius: 6px;
}

/* The body is hidden with the `hidden` attribute rather than a class, so it is
   removed from the accessibility tree too — a collapsed card must not leave
   eight headings for a screen reader to walk through. */
.guide-body[hidden] {
  display: none;
}

.guide-intro {
  font-size: 13px;
  opacity: 0.9;
  margin: 14px 0 16px;
}

.guide-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 16px;
}

.guide-item {
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  padding: 14px;
  border-radius: 12px;
}

.guide-item h4 {
  font-size: 14px;
  color: var(--green-mint);
  margin-bottom: 6px;
}

.guide-item p {
  font-size: 12px;
  line-height: 1.5;
  opacity: 0.8;
}

.guide-item b {
  font-weight: 600;
  opacity: 0.95;
}

body.light-theme .guide-item h4 {
  color: var(--green-forest);
}

/* The indicator is the one moving part here; anyone who has asked the OS to
   stop animating things should get an instant swap instead. */
@media (prefers-reduced-motion: reduce) {
  .subtab-indicator {
    transition: none;
  }

  .tab-pane.active {
    animation: none;
  }

  .guide-chevron {
    transition: none;
  }
}

/* ==========================================================================
   Favicon & Responsive Mobile Sidebar Overlay & Collapsible Controls
   ========================================================================== */
/* ==========================================================================
   Bottom Navigation (mobile / tablet)

   Replaces the slide-out drawer below 1080px. Both are display:none until
   their own media query claims them, so exactly one nav is ever on screen.
   ========================================================================== */

.header-settings-btn {
  display: none;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  color: var(--text-muted);
  padding: 9px;
  border-radius: 12px;
  cursor: pointer;
  align-items: center;
  justify-content: center;
  /* A 22px icon in 9px of padding measured 42x42 — two pixels under the 44px
     a thumb can hit reliably, which is exactly the size of miss that makes a
     control feel unreliable rather than broken. Set as a floor so the icon,
     the padding and the target stay independent of each other. */
  min-width: 44px;
  min-height: 44px;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}

.header-settings-btn:hover,
.header-settings-btn:focus-visible {
  color: var(--green-matcha);
  border-color: var(--green-matcha);
  background: rgba(77, 156, 78, 0.1);
}

.bottom-nav {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 900;
  align-items: center;
  justify-content: space-around;
  /* The bar's own height, plus the strip the phone reserves for its home
     indicator or gesture pill. On hardware without one the inset is 0 and this
     collapses to the plain height. */
  height: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
}

/* The bar's chrome lives on a pseudo-element so that opening a dialog can fade
   it out on the compositor instead of repainting it.

   It used to sit on .bottom-nav itself and animate via
   `transition: background, border-color, box-shadow`. All three are paint
   properties, and box-shadow is the worst of them: a 20px blur spread across
   the full width of the bar, re-rasterised every frame for a quarter of a
   second — on the exact gesture that also fades a full-viewport scrim and runs
   a spring on the dialog. Fading a layer's opacity is one GPU operation and
   costs the same whatever is drawn into it.

   Opaque, not glass. This bar sits over scrolling content all day; a
   translucent one would pick up whatever passed beneath it and its labels
   would change contrast row by row — the same alpha-compositing trap the
   dialogs were in. */
/* No border-top. The bar is opaque and carries an upward shadow, so it already
   reads as a separate plane; the hairline on top of that was a second, harder
   answer to a question the shadow had already answered, and on a phone it drew
   a grey rule across the full width of the screen. */
.bottom-nav::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--sidebar-solid);
  box-shadow: 0 -4px 20px var(--shadow-color);
  /* Nothing here is interactive; without this the layer would swallow taps
     meant for the tabs painted on top of it. */
  pointer-events: none;
  transition: opacity 0.2s ease;
}

/* While a dialog is open the bar gets out of the way but the + stays, because
   the + is now the close button.

   This is a correctness fix as much as a visual one. The bar is z-index 900 and
   the modal overlay is 100, so the bar renders ABOVE an open dialog and stayed
   fully clickable through the scrim — elementFromPoint at the bar's centre
   returned the nav button, not the overlay. You could tap Wallets mid-form and
   navigate underneath it. pointer-events is what actually closes that hole; the
   fading is so the bar does not just sit there looking pressable. */
body.modal-open .bottom-nav {
  pointer-events: none;
}

/* The whole of the bar's chrome, gone in one compositor-only property. */
body.modal-open .bottom-nav::before {
  opacity: 0;
}

body.modal-open .bottom-nav .nav-link {
  opacity: 0;
  pointer-events: none;
}

/* Re-armed explicitly: the bar above set pointer-events: none for its whole
   subtree, and this is the one child that must stay hittable. */
body.modal-open .bottom-nav-fab {
  pointer-events: auto;
}

.bottom-nav-fab svg {
  transition: transform 0.2s ease;
}

/* 45° turns the + into an X. Rotating rather than swapping the icon keeps the
   two states visibly the same object, so it reads as the button changing its
   mind rather than a different button appearing where the last one was. */
body.modal-open .bottom-nav-fab svg {
  transform: rotate(45deg);
}

.bottom-nav .nav-link {
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  /* 44px is the smallest target most people can hit reliably with a thumb;
     the whole column is the target, not just the icon. */
  min-height: 44px;
  padding: 6px 2px;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  font-family: inherit;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.2px;
  /* opacity is here because body.modal-open fades these out. Without it in the
     list the labels vanished on the same frame the class landed, while the bar
     beneath them took 0.2s to go — one gesture, two different speeds. */
  transition: color 0.2s ease, opacity 0.2s ease;
}

.bottom-nav .nav-link svg {
  width: 22px;
  height: 22px;
}

.bottom-nav .nav-link span {
  /* "Transactions" is too long for a fifth of a small phone; it ellipsises
     rather than wrapping to a second line and shoving the row taller. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bottom-nav .nav-link.active {
  color: var(--nav-active);
}

/* Colour alone must not be the only thing marking the current tab — a
   red-green colour vision deficiency would erase it against these greens. The
   dot is a second, non-colour channel saying the same thing. */
.bottom-nav .nav-link.active::before {
  content: '';
  position: absolute;
  top: 0;
  width: 18px;
  height: 3px;
  border-radius: 0 0 3px 3px;
  background: var(--nav-active);
}

.bottom-nav .nav-link {
  position: relative;
}

.bottom-nav-fab {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  /* Positioned so it paints above .bottom-nav::before, which is itself
     positioned and would otherwise cover every unpositioned sibling. The tabs
     are already position:relative for their active dot; this is the same need
     for a different reason. */
  position: relative;
  width: 56px;
  height: 56px;
  /* Lifted out of the bar so it reads as the primary action rather than a
     fifth tab. The bar is z-index 900 and this rides above its own border. */
  margin-top: calc(var(--bottom-nav-height) * -0.45);
  border: 3px solid var(--sidebar-solid);
  border-radius: 50%;
  background: var(--nav-active);
  color: #ffffff;
  cursor: pointer;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.28);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.bottom-nav-fab:hover,
.bottom-nav-fab:focus-visible {
  transform: scale(1.06);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.34);
}

/* No :active rule here. The general `button:not(:disabled):active` near the top
   of this file already presses the FAB, and it wins anyway: :not(:disabled) and
   :active are two pseudo-classes, so it scores (0,2,1) against this selector's
   (0,2,0). A rule here would be dead weight that looks live. */

.header-title-section {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* Responsive Media Queries for Mobile/Tablet Screens */
@media (max-width: 1080px) {
  /* The sidebar is simply not present below this width — no drawer, no
     backdrop, no swipe. Every one of those existed to make a desktop nav
     reachable on a phone, and .bottom-nav makes it reachable by being there
     already. */
  aside {
    display: none;
  }

  /* The scrim loses its blur: the cost is reading and re-compositing the whole
     viewport, which a smaller radius barely reduces, and behind a 78%-opaque
     dim the blur was close to invisible anyway. A little more opacity replaces
     it.

     It also runs shorter here. Opening a dialog moves four things at once — the
     scrim, the dialog's spring, the bar's chrome and the + rotating into an X —
     and at 0.3s the scrim and the window were still going 100ms after the bar
     and the icon had stopped. Landing them together reads as one movement
     rather than a thing that keeps twitching, and 100ms less of a
     full-viewport layer is 100ms the phone spends drawing the form instead.

     The visibility delay has to track the fade exactly: it is what keeps a
     closed dialog's fields out of the tab order, and any excess leaves them
     reachable after the dialog is invisible. */
  .modal-overlay {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(14, 18, 14, 0.78);
    transition: opacity 0.2s ease, visibility 0s linear 0.2s;
  }

  .modal-overlay.active {
    transition: opacity 0.2s ease, visibility 0s;
  }

  .modal-window {
    transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  .bottom-nav {
    display: flex;
  }

  /* The + in the middle of that bar opens the same dialog, is twice the size and
     is already under the thumb. Two buttons for one action is one button's worth
     of wasted height on the screen that most needs it. Above 1080px the bar does
     not exist and this is the only way in, so it is hidden here rather than
     deleted. */
  .ledger-add-btn {
    display: none;
  }

  /* The FAB stays put while a dialog is open, so it overhangs the dialog's
     bottom edge — at full height a dialog runs to 95vh and the FAB's top edge
     is above that. Without this the Save button can end up under the FAB, which
     is the one place a close button must not sit. Padding rather than a shorter
     max-height: the dialog already scrolls, so this just guarantees the last
     control can be scrolled clear. */
  body.modal-open .modal-window {
    padding-bottom: 76px;
  }

  .header-settings-btn {
    display: inline-flex;
    /* Pushed to the right end of the title row. margin rather than a place in
       .header-controls: that row already carries the date widget and the
       currency select, and a third item wrapped it onto a line of its own. */
    margin-left: auto;
  }

  /* Full width so the auto margin above has something to push against — with
     header switched to a column and align-items:flex-start, this row would
     otherwise shrink to its content and park the gear beside the title. */
  .header-title-section {
    width: 100%;
  }

  main {
    /* Bottom padding clears the fixed bar, or the last row of every list sits
       under it unreachable — the ledger's pagination controls being the case
       that actually bites. --bottom-nav-height plus the phone's own home
       indicator, plus a little air. */
    padding: 16px 16px calc(var(--bottom-nav-height) + env(safe-area-inset-bottom) + 20px);
    gap: 20px;
  }

  /* 30px between the toolbar and the list it filters is desktop spacing on a
     phone: it read as two unrelated blocks and cost most of a thumb's worth of
     scroll before the first row of content. */
  .tab-pane.active {
    gap: 16px;
  }

  .section-head {
    gap: 10px;
  }

  header {
    flex-direction: column;
    align-items: flex-start;
    gap: 16px;
  }

  /* The bar already names the section it is highlighting, so repeating it in a
     26px heading spends the most valuable strip on a phone — the top of the
     first screenful — saying what the reader just tapped. The subtab bar keeps
     its labels, since those name the view rather than the section.

     Clipped rather than display:none. The heading is what gives the document
     its outline, and a screen reader user navigating by heading has no bottom
     bar to read the section name off — removing it from the tree would take
     away the one label they had. */
  .section-head > h2,
  .section-header > h2 {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }
  
  .header-controls {
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
  }
  
  .time-travel-widget {
    width: 100%;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
    padding: 10px 12px;
  }

  .currency-selector-widget {
    width: 100%;
    justify-content: center;
    padding: 10px 12px;
  }
  
  .header-title-section h1 {
    font-size: 26px;
  }
  
  .header-title-section p {
    font-size: 12px;
  }
}

/* ZenSync Pulsing Keyframe Animations */
@keyframes sync-pulse {
  0% {
    transform: scale(0.95);
    box-shadow: 0 0 0 0 rgba(162, 210, 164, 0.6);
    opacity: 0.6;
  }
  70% {
    transform: scale(1.1);
    box-shadow: 0 0 0 6px rgba(162, 210, 164, 0);
    opacity: 1;
  }
  100% {
    transform: scale(0.95);
    box-shadow: 0 0 0 0 rgba(162, 210, 164, 0);
    opacity: 0.6;
  }
}

.sync-icon-pulse {
  animation: sync-pulse 2s infinite ease-in-out;
}

/*
  Respect the OS "reduce motion" setting. Users who enable it often do so
  because vestibular disorders make transform and scale animations physically
  unpleasant, and this stylesheet has ~35 transitions plus several infinite
  loops (the sync pulse, the breathing loader) that ran regardless.

  Motion is reduced rather than removed: durations collapse to ~1ms so
  transitionend/animationend listeners still fire and any state that depends
  on them cannot stall. Infinite decorative loops are stopped outright, since
  a 1ms infinite animation is worse than none.
*/
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    /* Without this the modal's visibility transition keeps its 0.3s delay while
       the fade it was hiding is already over, leaving a closed dialog sitting
       invisible-but-focusable for a third of a second. */
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }

  .sync-icon-pulse {
    animation: none !important;
  }
}


/* ==========================================================================
   Bottom-bar subtab bubbles (js/nav-gestures.js)

   Long-press a bar button that has subtabs and they rise out of it; slide onto
   one and lift. Built and positioned in JS because the stack has to be anchored
   to whichever button was pressed.

   Not the only route to any of these views — the subtab bar inside each section
   still does the same job, and does it for keyboards and screen readers, which
   is why no aria-hidden or role plumbing is needed here: these are plain buttons
   that happen to be reachable by a gesture.
   ========================================================================== */
.nav-bubbles {
  position: fixed;
  z-index: 950;
  display: flex;
  /* column-reverse so the FIRST subtab ends up nearest the bar. The stack grows
     upward, and the end nearest the thumb is the one a short slide reaches. */
  flex-direction: column-reverse;
  gap: 8px;
}

.nav-bubbles[hidden] {
  display: none;
}

/* Opaque, like the dialogs and the bar itself: this floats over whatever the
   reader was looking at, and a translucent pill would take its contrast from
   the rows passing underneath it. */
.nav-bubble {
  min-width: 136px;
  min-height: 44px;
  padding: 10px 18px;
  border-radius: 999px;
  border: 1px solid var(--border-color);
  background: var(--surface-raised);
  color: var(--text-main);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  text-align: center;
  box-shadow: 0 8px 24px var(--shadow-color);
  cursor: pointer;
  /* The entrance animates SCALE ONLY, and the resting state is the visible one.
     Opacity was in here too, and it is exactly the wrong property to put an
     entrance on for this control: if the animation does not run — a paused
     compositor, a backgrounded tab, an engine that treats a display:none parent
     as never having started it — a fill mode holds whichever end frame it is
     given, and the losing case leaves the bubbles present, hit-testable and
     completely invisible. That was reproduced here, not theorised. Frozen at the
     from-frame the worst this can now look is 12% small.

     `backwards` so the stagger delay shows the from-frame rather than snapping
     the later bubbles to full size and then back down to animate. */
  transform: none;
  animation: nav-bubble-pop 0.16s ease backwards;
}

/* The subtab the section is already on, so a long-press also answers "where am
   I" without having to open the section to find out. */
.nav-bubble.is-current {
  border-color: var(--green-matcha);
}

/* Under the finger right now. This is the whole feedback loop of the gesture:
   there is no cursor, so without it you are dragging blind. */
.nav-bubble.is-active {
  background: var(--green-matcha);
  color: var(--text-dark);
  border-color: var(--green-matcha);
}

.nav-bubble:nth-child(2) { animation-delay: 0.03s; }
.nav-bubble:nth-child(3) { animation-delay: 0.06s; }

@keyframes nav-bubble-pop {
  from {
    transform: scale(0.88);
  }
}

/* The button the press came from, held lit while its bubbles are up. */
.bottom-nav .nav-link-pressed {
  color: var(--green-matcha);
}

/* The slide from the button onto a bubble is a horizontal drag across a fixed
   bar. Without this the browser claims it as a pan and the pointermove events
   stop arriving mid-gesture. The bar does not scroll, so nothing is lost. */
.bottom-nav {
  touch-action: none;
}

@media (prefers-reduced-motion: reduce) {
  .nav-bubble {
    animation: none;
  }

  .budget-card {
    transition: none;
  }
}

/* ---------------------------------------------------------------------------
   Settings > Appearance: the theme switch.

   The sidebar's own theme button lives in .sidebar-footer, which stops
   rendering at 1080px, so on a phone this is the only theme control there is.
   A round icon button rather than a labelled one: it sits at the end of a row
   whose text already says what it does, and 52px clears the 44px touch
   minimum with room for a thumb that is not aimed carefully.
   --------------------------------------------------------------------------- */
.theme-switch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.theme-switch-copy {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.theme-switch-label {
  font-weight: 600;
  font-size: 14px;
  /* The token, not inherit: <body> keeps the dark palette's colour when
     .light-theme is added, so inherit renders pale green on a white card. */
  color: var(--text-main);
}

.theme-switch-hint {
  font-size: 12px;
  color: var(--text-muted);
}

.theme-switch-btn {
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-raised);
  border: 1px solid var(--border-color);
  color: var(--green-matcha);
  cursor: pointer;
  box-shadow: 0 4px 14px var(--shadow-color);
  transition: transform 0.18s ease, border-color 0.18s ease;
}

.theme-switch-btn:hover {
  border-color: var(--green-matcha);
}

.theme-switch-btn:active {
  transform: scale(0.92);
}

.theme-switch-btn:focus-visible {
  outline: 2px solid var(--green-matcha);
  outline-offset: 3px;
}

.theme-switch-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Overrides the width/height attributes on the shared 18px SVG strings, so the
   same markup applyTheme() writes into the sidebar reads correctly at 52px. */
.theme-switch-btn svg {
  width: 22px;
  height: 22px;
}

@media (prefers-reduced-motion: reduce) {
  .theme-switch-btn { transition: none; }
}
