  /* Self-hosted, not loaded from Google Fonts -- the service worker
     (sw.js) explicitly skips cross-origin requests, so a CDN-hosted font
     is never cached and never loads offline before its first successful
     online fetch. Both are the exact variable-font files Google Fonts
     itself already serves for these families (one file per script,
     covering the whole 400-800 weight range this app actually uses --
     see the h1-h4/badge/etc. font-weight rules below), just fetched once
     and committed here instead of requested from fonts.gstatic.com.
     Inter has NO Thai glyphs at all (verified against Google Fonts' own
     served subsets -- only latin/latin-ext/cyrillic/cyrillic-ext/greek/
     greek-ext/vietnamese exist for it, no "thai"), so Thai text was
     always falling back to whatever font the OS happened to provide,
     online or offline alike -- self-hosting Inter alone would not have
     fixed that. Noto Sans Thai's "thai" subset is paired in as a second
     family specifically to make Thai text (the app's primary language,
     and the Baht sign "฿" used everywhere) render consistently from a
     cached, self-hosted file too, not just Latin text. Combined weight:
     ~47KB (Inter, latin subset) + ~26KB (Noto Sans Thai, thai subset) =
     ~74KB for the full weight range in both scripts -- far lighter than
     "the full Inter Thai set" implied, precisely because that set
     doesn't exist. */
  @font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url("./fonts/inter-latin.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  }
  @font-face {
    font-family: "Noto Sans Thai";
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url("./fonts/notosansthai-thai.woff2") format("woff2");
    unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC;
  }
  /* Self-hosted, same reasoning as Inter/Noto Sans Thai above -- sw.js
     never caches cross-origin requests, so a Google Fonts CDN load would
     never work offline. Weight 700 only, latin subset only: this is
     docs/specs/coral-rebrand-and-logo.md's sidebar wordmark lockup, the
     one spot in the app that uses Poppins -- "whereisit" is the same
     Latin brand name in both languages (i18n.js's appTitle), so no other
     subset is ever needed. Not the app's general UI font -- that stays
     Inter/Noto Sans Thai everywhere else. */
  @font-face {
    font-family: "Poppins";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("./fonts/poppins-latin-700.woff2") format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  }
  /* This block is the ONE owner of every color token in the app: light mode
     with the Coral accent, the defaults. Dark mode and the Purple accent are
     two override blocks right after it, each redeclaring only the tokens that
     actually differ. theme.js picks between them by setting data-theme and
     data-accent on <html> -- it writes no colors itself. See docs/UX.md's
     "Design-token ownership". */
  :root {
    color-scheme: light;
    /* neutrals */
    --gray-900:#15161a;
    /* Brand accent. Coral is the default (docs/specs/coral-rebrand-and-logo.md,
       docs/specs/color-palette-refresh.md -- those hold the full provenance;
       both accents' values are pixel-sampled from the maintainer's reference
       images via canvas getImageData, never eyeballed or hand-converted).
       The Purple block below overrides all three.

       Contrast, because these numbers are load-bearing and easy to undo by
       "tidying" a hex:
       - The base #D97757 is the un-darkened Claude brand hue and measures
         **3.12:1 against white text -- below the 4.5:1 floor, deliberately.**
         The maintainer chose the truer hue over a darker AA-safe one, with a
         text-shadow fix deferred. Tracked in docs/UX.md's Known UI debt; do
         not "fix" it by darkening this value.
       - c600/c700 were NOT relaxed the same way -- they render as colored
         text on --color-card / --color-accent-tint (badge-brand,
         .shortcut-btn.active, .kind-toggle button.active, .btn-ghost), never
         under white text, so they clear their own floor: c700 8.47:1 on the
         tint, c600 4.53:1 on the tint and 5.11:1 on --color-card in light.
       - Neither c600 clears --color-card in *dark* mode (~2.2-2.5:1), since
         --color-accent* deliberately doesn't invert per theme the way
         income/expense do. Pre-existing, affects .btn-ghost's dark-mode text. */
    --color-accent: #D97757;
    --color-accent-600: #B74D2A;
    --color-accent-700: #74311A;
    --color-accent-tint: color-mix(in srgb, var(--color-accent) 12%, white);
    --shadow-accent: 0 12px 28px color-mix(in srgb, var(--color-accent) 30%, transparent);
    /* Home hero card's gradient stops, shared with the tab bar's raised Add
       button -- both render the identical two-stop 135deg gradient. Accent-
       dependent, so the Purple block overrides both. Start is always the
       accent base; the end stop rotates hue ~23deg toward a brighter adjacent
       hue at constant saturation (terracotta -> warm amber). Coral's end is a
       dedicated color, NOT c700 and not indigo's 42deg rotation transposed --
       at this hue that lands in mustard, verified by rendering swatches.
       White text on Coral's gradient tapers 3.12:1 (start) -> ~2.26:1 (end),
       Indigo's 5.75:1 -> 2.94:1; both below the floor toward the far corner,
       same deferred text-shadow fix as the base above. The mitigation is
       positional: large balance text stays over the start corner. */
    --hero-gradient-start: #D97757;
    --hero-gradient-end: #D6A44C;
    /* Sidebar lockup icon only (see .sidebar-logo below) -- deliberately
       not promoted to a general system color, same reasoning as when the
       coral rebrand spec first considered and rejected that. --logo-ring-color
       follows the Coral/Purple preference in light mode via var(), and goes
       solid white in dark mode (see the dark block). --color-logo-amber, the
       handle+dot, never changes with theme or preference. */
    --color-logo-amber: #f2a618;
    --logo-ring-color: var(--color-accent);
    /* semantic */
    /* -700 pair is the AA-contrast-safe variant for small/bold text on
       --color-card. Both are per-theme; the dark block overrides them. */
    --color-income: #1fae71; --color-income-700: #15803D; --color-income-tint: color-mix(in srgb, var(--color-income) 12%, white);
    /* --color-income-tint always mixes toward white regardless of theme
       (measured: light and dark tint end up within ~2 RGB points of each
       other, since a 12% mix is dominated by the white it's mixed into) --
       unlike --color-card/--color-surface, it never inverts for dark mode.
       So an icon foreground sitting on it needs a color that's ALSO
       theme-invariant, not --color-income-700 (which deliberately equals
       the bright base --color-income in dark mode, correct for its own
       "text on --color-card" job but wrong here: paired with a tint that
       stays light, that would measure ~1.9:1). Reuses -700's light-mode
       hex, fixed, for exactly this "icon glyph on its own permanently-light
       tint circle" pattern -- categories.js's rowTone()/GOAL_TONES. */
    --color-income-tint-fg: #147a54;
    --color-expense: #ef4b3a; --color-expense-700: #CC2020; --color-expense-tint: color-mix(in srgb, var(--color-expense) 12%, white);
    --color-chart-5-tint: color-mix(in srgb, var(--color-chart-5) 12%, white);
    /* This tint stays near-white in both themes, so its foreground must
       stay dark instead of following the chart color's dark-mode brightening.
       #17665c measures 5.78:1 in light and 6.32:1 in dark mode. */
    --color-chart-5-tint-fg: #17665c;
    --color-warning: #ec9f2e; --color-warning-text: #93630f; --color-warning-tint: color-mix(in srgb, var(--color-warning) 15%, white);
    /* .hero-card-negative's own gradient, deliberately NOT --color-expense/
       -700 -- those measure only 3.66:1 (light mode's lighter gradient
       stop) / 2.55:1 (dark mode, where -700 collapses to the same hex as
       the base) for the white text laid on top, both below the 4.5:1
       small-text AA floor. Same hue as --color-expense (~6 deg), just
       darkened until white clears AA (4.77:1 / 6.89:1) -- and unlike every
       other themed token here, this ONE pair is theme-invariant (verified:
       white-on-gradient contrast doesn't depend on the app's light/dark
       surroundings, only on these two stops), so the dark block leaves it
       alone. */
    --hero-negative-gradient-start: #cf3f2f; --hero-negative-gradient-end: #a82c1f;
    /* Breakdown chart's 5th/6th rotating colors (derived.js's CHART_COLORS).
       Chosen far apart in hue from both accent options (Coral ~20deg,
       Purple ~250deg -- #a190f7, the value this replaced, sat right on top
       of Purple at ~255deg) so neither can be misread as "the accent" once
       assigned to a category. Both are per-theme; the dark block overrides
       them, same as --color-income/-expense/-warning above. */
    --color-chart-5: #1f7d70; --color-chart-6: #b23a7a;
    /* surfaces (WI-018) */
    --color-bg: #eef0f4; --color-card: #ffffff; --color-surface: #e5e8ee;
    --color-text: #15161a; --color-muted: #71747f; --color-tertiary: #9497a3; --color-divider: #dde0e7; --color-border: #cfd3dc;
    /* Own token rather than reusing --color-tertiary: that token is shared
       by 11+ other (mostly static-text) spots, but the tab bar is an
       interactive UI component subject to WCAG's stricter 3:1 minimum --
       #9497a3 on this bg only computes to ~2.55:1 (verified via a real
       relative-luminance calc, not eyeballed; re-measured for WI-018's new
       bg). #7d808c clears 3:1 with margin (~3.45:1); dark mode's tertiary
       already passes (~3.86:1) so it's reused as-is below. */
    --color-tabbar-inactive: #7d808c;
    /* type + shape */
    /* "Noto Sans Thai" comes right after "Inter" so Thai-block characters
       (which the self-hosted Inter file above has no glyphs for at all)
       render from the self-hosted, cached Noto Sans Thai file instead of
       skipping straight to whatever Thai font the OS happens to ship --
       or none, on some platforms/older Android especially. */
    --font-sans: "Inter", "Noto Sans Thai", -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
    --radius-sm: 8px; --radius-md: 12px; --radius-lg: 18px; --radius-xl: 24px; --radius-pill: 999px;
    /* Linear (4px-step), not the more common doubling scale -- chosen
       deliberately per docs/specs/home-spacing-scale.md's design-skill
       review so the existing, audit-confirmed-fine 12px card rhythm can be
       tokenized as-is instead of being forced to round to 8px or 16px.
       Home-only for now (see that spec for the full remap + rationale);
       other screens keep their literal px values until a future pass. */
    --space-xs: 8px; --space-sm: 12px; --space-md: 16px; --space-xl: 24px;
    --shadow-xs: 0 1px 2px rgba(15,15,20,0.04);
    --shadow-sm: 0 2px 6px rgba(15,15,20,0.06);
    --shadow-lg: 0 16px 40px rgba(15,15,20,0.14);
    --ease-standard: cubic-bezier(0.4,0,0.2,1);
    --duration-fast: 120ms; --duration-normal: 200ms;
    /* Referenced by .screen's bottom padding and .toast's bottom offset so
       both stay correctly clear of the fixed tab bar -- see the tab bar
       section below for why plain env(safe-area-inset-bottom) alone isn't
       enough. Must match .tabbar's own height. */
    --tabbar-h: 64px;
  }
  /* Dark mode. Set by theme.js from state.dark -- an explicit, persisted user
     preference, deliberately NOT prefers-color-scheme. Redeclares only the
     tokens that actually differ; everything else inherits from :root above.
     The *-tint tokens are absent on purpose: they're color-mix() over a base
     declared on this same element, so they re-resolve against the overridden
     base for free. Their *-tint-fg partners are absent for the opposite
     reason -- tints mix toward white in both themes, so their foregrounds
     must stay theme-invariant (see --color-income-tint-fg's own note). */
  :root[data-theme="dark"] {
    color-scheme: dark;
    --color-bg: #111216; --color-card: #1f2027; --color-surface: #282a31;
    --color-text: #f5f5f7; --color-muted: rgba(245,245,247,0.62);
    --color-tertiary: rgba(245,245,247,0.42);
    --color-divider: rgba(255,255,255,0.10); --color-border: rgba(255,255,255,0.16);
    /* Dark mode's tertiary already clears the tab bar's 3:1 minimum (~3.86:1),
       so unlike light mode this reuses it rather than needing its own value. */
    --color-tabbar-inactive: rgba(245,245,247,0.42);
    --color-income: #34c98a; --color-income-700: #4ADE80;
    --color-expense: #ff7a68; --color-expense-700: #F87171;
    --color-warning: #f5b95a;
    --color-chart-5: #4fd6c4; --color-chart-6: #e768ab;
    /* Solid white regardless of the Coral/Purple preference -- matching the
       design's own dark lockup reference, which uses white for the ring
       independent of hue. */
    --logo-ring-color: #ffffff;
  }
  /* Purple accent (the app's original, kept as an opt-in preference). Coral is
     the default and lives in :root -- there is deliberately no
     [data-accent="coral"] block. Theme-independent: both accents look the
     same in light and dark, so this sits alongside the dark block, not inside
     it. */
  :root[data-accent="purple"] {
    --color-accent: #6247ea;
    --color-accent-600: #4f34d6;
    --color-accent-700: #3f28ab;
    --hero-gradient-start: #6149ea;
    --hero-gradient-end: #409ce9;
  }
  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
  html, body { margin: 0; min-height: 100%; }
  body {
    background: var(--color-bg); color: var(--color-text); font-family: var(--font-sans);
    font-size: 15px; line-height: 1.45; -webkit-font-smoothing: antialiased;
  }
  h1, h2, h3, h4 { font-family: var(--font-sans); font-weight: 800; margin: 0; letter-spacing: -0.02em; }
  a { color: var(--color-accent); }
  svg { display: block; }
  .icon { width: 18px; height: 18px; flex-shrink: 0; }

  .app { display: flex; flex-direction: column; min-height: 100vh; }

  /* Every screen (Home/Transactions/Add/Insights/Settings) renders into
     this one shared element -- there's no per-screen scroll container, the
     whole document scrolls, so a single fix here covers all of them. The
     old flat 100px bottom padding happened to clear the 64px tab bar with
     ~36px to spare, but didn't account for env(safe-area-inset-bottom) at
     all -- on a notched/gesture-nav phone (and PWAs installed to the home
     screen, where the safe area is largest) that inset can eat into or
     exceed that slack, letting the last row render partly behind the tab
     bar. Bottom padding is now the tab bar's real height plus the actual
     safe-area inset plus a fixed breathing-room gap, so it's always
     exactly enough regardless of device. */
  .screen { flex: 1; max-width: 480px; margin: 0 auto; width: 100%; padding: 20px 20px calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 20px); box-sizing: border-box; }
  /* Settings sub-pages are a full-screen context: their back arrow is the
     only exit, so the tab bar and its reserved vertical space disappear. */
  body.settings-subpage-open .tabbar-wrap { display: none; }
  body.settings-subpage-open .screen { padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 20px); }
  .screen-title { font-size: 22px; margin: 4px 0 var(--space-md); }
  /* Fires only on a genuine tab switch (screens/router.js's setTab, which
     re-adds this class after a forced reflow) -- NOT on every renderScreen()
     call, since that also runs on sync pulls and local saves where a fade
     would feel like a glitch rather than polish. */
  @keyframes screen-fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
  .screen.screen-enter { animation: screen-fade-in var(--duration-normal) var(--ease-standard); }
  @media (prefers-reduced-motion: reduce) { .screen.screen-enter { animation: none; } }

  /* — shared card base (list/insight/goal/stat/etc. cards all sit on this) — */
  .card, .list-card, .insight-card, .insight-rollup, .trend-chart-card, .goal-card,
  .stat-card, .today-spend-card, .inline-form {
    background: var(--color-card); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);
  }

  /* — buttons — */
  .btn {
    display: inline-flex; align-items: center; justify-content: center; gap: 7px;
    cursor: pointer; text-decoration: none;
    font-family: var(--font-sans); font-weight: 600; font-size: 14px; line-height: 1.2;
    color: var(--color-text); background: transparent; border: none;
    height: 44px; padding: 0 18px; border-radius: var(--radius-pill);
    transition: transform var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard);
  }
  .btn:active { transform: scale(0.97); }
  .btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
  .btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  .btn-primary { background: var(--color-accent); color: #ffffff; box-shadow: var(--shadow-accent); }
  .btn-primary:hover { background: var(--color-accent-600); }
  .btn-secondary { background: var(--color-card); color: var(--color-text); border: 1px solid var(--color-border); box-shadow: none; }
  .btn-secondary:hover { background: var(--color-surface); }
  .btn-ghost { color: var(--color-accent-600); padding: 0 4px; height: auto; font-size: 13px; font-weight: 600; }
  .btn-ghost:hover { opacity: 0.75; }
  .btn-danger { background: var(--color-expense); color: #fff; box-shadow: 0 8px 18px color-mix(in srgb, var(--color-expense) 35%, transparent); }
  .btn-danger:hover { background: var(--color-expense-700); }
  .btn-icon { width: 30px; height: 30px; padding: 0; border-radius: 50%; background: var(--color-surface); }
  .btn-icon:hover { background: var(--color-border); }
  .btn-block { width: 100%; }
  .btn-sm { height: 36px; padding: 0 14px; font-size: 13px; }

  /* — forms — */
  .field { display: flex; flex-direction: column; gap: 6px; }
  /* Add form's Category/Transfer-To fields (stage 2 of
     docs/specs/account-transfers.md) toggle visibility based on the
     Expense/Income/Transfer tab -- a dedicated class rather than an
     inline style so both the initial server-rendered state and the later
     JS toggle (on tab change) go through the same rule. */
  .form-field-hidden { display: none; }
  .field > label { font-size: 13px; font-weight: 500; color: var(--color-muted); }
  .input-wrap {
    display: flex; align-items: center; gap: 8px; height: 48px; padding: 0 14px;
    border-radius: var(--radius-md); border: 1px solid var(--color-border); background: var(--color-card);
  }
  .input-wrap:focus-within { outline: 2px solid var(--color-accent); outline-offset: 1px; }
  .input-wrap.has-error { border-color: var(--color-expense); }
  .input-wrap .prefix { color: var(--color-tertiary); font-size: 15px; flex-shrink: 0; }
  .input {
    width: 100%; min-height: 46px; padding: 0 14px; font: inherit; font-size: 15px;
    color: var(--color-text); background: var(--color-card);
    border: 1px solid var(--color-border); border-radius: var(--radius-md);
  }
  .input:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
  .input-wrap input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font: inherit; font-size: 15px; color: var(--color-text); height: 100%; padding: 0; }

  .tabs { display: inline-flex; background: var(--color-surface); border-radius: var(--radius-pill); padding: 4px; gap: 2px; max-width: 100%; }
  .tabs.block { display: flex; width: 100%; justify-content: space-between; }
  /* flex:0 1 auto (not flex:1) -- equal-width cells made a short label
     (e.g. "All") sit in a cell sized for the row's longest label (e.g.
     "Transfer"), so the two ended up with wildly different amounts of
     whitespace around them. Content-sized cells give every label the same
     8px/16px padding regardless of its own length; .tabs.block's own
     justify-content:space-between (above) spreads the now-content-sized
     cells back out to fill the row. flex-grow stays 0 (don't force cells
     wider than their own content), but flex-shrink stays 1 (not 0) --
     on a genuinely narrow phone, four content-sized cells (esp. with
     "Transfer" the longest of the four) don't all fit their natural width,
     and flex-shrink:0 would let the row overflow its container with the
     last cell silently clipped instead of the row compressing to fit;
     min-width:0 + the existing ellipsis below is what makes shrinking
     degrade gracefully instead of just squashing text unreadably. */
  .tab-opt {
    display: inline-flex; align-items: center; justify-content: center; flex: 0 1 auto; min-width: 0;
    padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; border-radius: var(--radius-pill);
    color: var(--color-muted); white-space: nowrap; text-align: center; overflow: hidden; text-overflow: ellipsis;
    transition: background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard);
  }
  .tab-opt input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
  .tab-opt:has(input:checked) { background: var(--color-card); color: var(--color-text); box-shadow: var(--shadow-xs); }
  /* Type has three similarly sized fixed choices, unlike the shared tabs'
     potentially uneven labels, so equal cells close its wide desktop gaps. */
  .tabs.block:has(.type-tab-opt) { justify-content: initial; }
  .type-tab-opt { flex: 1 1 0; gap: 5px; padding: 8px 6px; }
  .type-tab-opt .icon { flex-shrink: 0; }
  .type-tab-opt span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .type-tab-opt:has(input:checked) { background: var(--tab-active-bg); color: var(--tab-active-color); }

  /* docs/specs/category-icon-chips.md -- the chip row sits above the
     original <select>, which stays in the DOM (not removed) as the
     "more" overflow, visually hidden by .category-select-collapsed
     until it's the one actually holding the true selection. */
  /* scrollbar-width/-ms-overflow-style + the ::-webkit-scrollbar rule below
     hide the row's scrollbar chrome (a full OS-style scrollbar with arrow
     buttons on some platforms) while leaving the row itself still scrollable
     by touch/drag/wheel -- the row's own horizontal overflow is the intended
     interaction, just without a visible scrollbar advertising it. */
  .category-chip-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; scrollbar-width: none; -ms-overflow-style: none; }
  .category-chip-row::-webkit-scrollbar { display: none; }
  .category-chip {
    display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0;
    padding: 8px 14px; border-radius: var(--radius-pill); border: 1px solid var(--color-border);
    background: var(--color-card); color: var(--color-text); font-family: var(--font-sans); font-size: 13px; font-weight: 600;
    white-space: nowrap; cursor: pointer;
    transition: background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
  }
  .category-chip .icon { width: 16px; height: 16px; }
  .category-chip:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  .category-chip.active { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }
  .category-chip-more { color: var(--color-muted); }
  .category-chip-more.active { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }
  .account-chip {
    display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0;
    padding: 8px 14px; border-radius: var(--radius-pill); border: 1px solid var(--color-border);
    background: var(--color-card); color: var(--color-text); font-family: var(--font-sans); font-size: 13px; font-weight: 600;
    white-space: nowrap; cursor: pointer;
    transition: background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
  }
  .account-chip .icon { width: 16px; height: 16px; }
  .account-chip:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  .account-chip.active { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }
  /* Mouse-only: the chip already declares a background/border transition
     above but had no hover rule to trigger it, so a desktop pointer got no
     feedback at all before a click landed. (hover: hover) keeps this off
     touch, where it would otherwise stick after a tap. */
  @media (hover: hover) { .account-chip:not(.active):not(:disabled):not(.account-chip-disabled):hover { background: var(--color-surface); } }
  /* Add form's Transfer tab (requested after the Transfer tab shipped):
     whichever account the other picker (From/To) currently holds renders
     disabled here, so picking the same account for both is caught at the
     chip itself instead of only after a submit attempt. */
  .account-chip:disabled, .account-chip.account-chip-disabled { opacity: 0.4; cursor: not-allowed; }
  /* Add/Edit form's account picker (option requested after the initial
     multi-account-support pass shipped) -- same chip shape as the switcher
     above, but its own row class since this one lives inside a .field like
     the category chip row, not styled as a screen-level filter row. Always
     collapses the underlying <select> (.account-select-collapsed), unlike
     .category-select-collapsed's conditional toggle -- there's no "more"
     overflow case here since every account always gets its own chip. */
  .account-chip-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; scrollbar-width: none; -ms-overflow-style: none; }
  .account-chip-row::-webkit-scrollbar { display: none; }
  .account-select-collapsed { display: none; }
  .category-select-collapsed { display: none; }

  /* — badges & switches — */
  .badge { display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border-radius: var(--radius-pill); font-size: 12px; font-weight: 600; white-space: nowrap; }
  .badge-brand { background: var(--color-accent-tint); color: var(--color-accent-700); }
  .badge-income { background: var(--color-income-tint); color: var(--color-income-tint-fg); }
  .badge-expense { background: var(--color-expense-tint); color: var(--color-expense-700); }
  .badge-warn { background: var(--color-warning-tint); color: var(--color-warning-text); }
  .switch { width: 48px; height: 28px; background: var(--color-border); border: none; border-radius: var(--radius-pill); padding: 2px; cursor: pointer; position: relative; box-sizing: border-box; display: inline-flex; transition: background var(--duration-normal) var(--ease-standard); }
  .switch.on { background: var(--color-accent); justify-content: flex-end; }
  .switch .thumb { display: block; width: 24px; height: 24px; border-radius: 50%; background: #fff; box-shadow: var(--shadow-xs); transition: transform var(--duration-normal) var(--ease-standard); }

  /* — icon avatar + list rows — */
  .icon-avatar { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
  .icon-avatar.sm { width: 30px; height: 30px; }
  .list-card { padding: 4px 14px; }

  /* — home — */
  .today-label { font-size: 13px; color: var(--color-muted); }
  .home-header-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
  /* Shortcut into Settings' own profile row (sign-in/out lives there, not
     here) -- same avatar-photo-or-initials fallback logic as that row, just
     a smaller fixed size for a header context. */
  .home-profile-btn {
    width: 46px; height: 46px; border-radius: 50%; flex-shrink: 0; margin-top: 2px;
    border: none; cursor: pointer; padding: 0; background: var(--color-surface); color: var(--color-tertiary);
    font-family: var(--font-sans); font-weight: 700; font-size: 17px;
    display: flex; align-items: center; justify-content: center; overflow: hidden;
    transition: transform var(--duration-fast) var(--ease-standard);
  }
  .home-profile-btn:active { transform: scale(0.92); }
  .home-profile-btn .icon { width: 21px; height: 21px; }
  .home-profile-btn img { width: 100%; height: 100%; object-fit: cover; }
  .home-columns { display: flex; flex-direction: column; gap: 20px; }
  .hero-card { position: relative; overflow: hidden; touch-action: pan-y; user-select: none; border-radius: var(--radius-lg); padding: 22px; color: #ffffff; background: linear-gradient(135deg, var(--hero-gradient-start) 0%, var(--hero-gradient-end) 100%); box-shadow: var(--shadow-accent); margin-top: var(--space-md); }
  .hero-card.hero-card-negative { background: linear-gradient(135deg, var(--hero-negative-gradient-start) 0%, var(--hero-negative-gradient-end) 100%); box-shadow: 0 12px 28px color-mix(in srgb, var(--color-expense) 30%, transparent); }
  .hero-carousel-viewport { overflow: hidden; }
  .hero-carousel-track { display: flex; transition: transform 240ms cubic-bezier(.22,1,.36,1); }
  .hero-carousel-track.dragging { transition: none; }
  .hero-page { flex: 0 0 100%; min-width: 0; padding-bottom: 10px; }
  .hero-card .kicker { font-size: 13px; opacity: 0.85; font-weight: 500; }
  /* "Hide financial status" quick toggle, top corner of the hero card next
     to the Balance label -- kicker moves into a row alongside it rather
     than the row being a new sibling, so the button stays vertically
     centered with the label regardless of font metrics. */
  .hero-card .kicker-row { display: flex; align-items: center; justify-content: space-between; }
  /* 32x32px hit area (the 16px icon plus generous padding), not just the
     glyph -- WCAG 2.2 AA's Target Size (Minimum) requires 24x24 CSS px for
     a pointer target; the original 16px icon + 2px padding was ~20x20,
     under that floor. Matches this app's other icon-only buttons
     (.btn-icon is 30x30) rather than introducing a third icon-button size. */
  .hero-hide-btn { background: none; border: none; color: inherit; opacity: 0.85; cursor: pointer; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; }
  .hero-hide-btn:hover { opacity: 1; }
  .hero-hide-btn:focus-visible { outline: 2px solid rgba(255,255,255,0.8); outline-offset: 2px; border-radius: 4px; }
  .hero-card .amount { font-size: 34px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.15; margin-top: 4px; }
  .hero-card .foot-row { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; }
  .hero-card .delta-pill { font-size: 13px; font-weight: 600; background: rgba(255,255,255,0.18); border-radius: var(--radius-pill); padding: 6px 12px; white-space: nowrap; }
  .hero-dots { position: absolute; z-index: 2; left: 50%; bottom: 4px; transform: translateX(-50%); display: flex; opacity: 0; }
  .hero-dots.visible { opacity: 1; }
  .hero-dot { width: 18px; height: 24px; padding: 0; border: none; background: transparent; cursor: pointer; display: grid; place-items: center; }
  .hero-dot::before { content: ""; width: 8px; height: 8px; background: rgba(255,255,255,0.35); border-radius: 50%; }
  .hero-dot-indicator { position: absolute; z-index: 1; top: 50%; left: 5px; width: 8px; height: 8px; border-radius: 50%; background: #fff; pointer-events: none; transform: translateX(calc(var(--hero-dot-index) * 18px)) translateY(-50%); }
  .hero-dot:focus-visible { outline: 2px solid #fff; outline-offset: -2px; border-radius: 50%; }
  .hero-arrow { display: none; position: absolute; z-index: 2; top: 50%; transform: translateY(-50%); background: rgba(255,255,255,0.18); color: #fff; }
  .hero-arrow:hover { background: rgba(255,255,255,0.28); }
  .hero-arrow:active { transform: translateY(-50%) scale(0.97); }
  .hero-arrow:disabled { transform: translateY(-50%); }
  .hero-arrow-prev { left: 8px; }
  .hero-arrow-next { right: 8px; }
  @media (prefers-reduced-motion: no-preference) {
    .hero-dots { transition: opacity var(--duration-normal) var(--ease-standard); }
    .hero-dot-indicator { transition: transform var(--duration-fast) var(--ease-standard); }
  }

  .stat-row { display: flex; gap: 12px; margin-top: var(--space-sm); }
  .stat-card { flex: 1; padding: 14px 16px; min-width: 0; }
  .stat-card .head { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--color-muted); }
  .stat-card .head .icon { width: 15px; height: 15px; }
  .stat-card .head-income .icon { color: var(--color-income-700); }
  .stat-card .head-expense .icon { color: var(--color-expense-700); }
  .stat-card .value { font-size: 22px; font-weight: 700; margin-top: 6px; letter-spacing: -0.02em; }
  .stat-card .delta { font-size: 12px; font-weight: 600; margin-top: 4px; }

  /* One-shot stagger for Home's top cards on a genuine tab entry (piggybacks
     on .screen-enter, same trigger/comment as screen-fade-in above -- not on
     every re-render). Opt-in via (no-preference) rather than an opt-out
     override, so a reduced-motion user gets no animation at all here. */
  @media (prefers-reduced-motion: no-preference) {
    @keyframes home-card-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
    .screen.screen-enter .hero-card { animation: home-card-in var(--duration-normal) var(--ease-standard) both; }
    .screen.screen-enter .stat-row .stat-card:nth-child(1) { animation: home-card-in var(--duration-normal) var(--ease-standard) both 40ms; }
    .screen.screen-enter .stat-row .stat-card:nth-child(2) { animation: home-card-in var(--duration-normal) var(--ease-standard) both 80ms; }
    .screen.screen-enter .today-spend-card { animation: home-card-in var(--duration-normal) var(--ease-standard) both 120ms; }
  }
  .today-spend-card { display: flex; align-items: center; gap: 12px; padding: 12px 16px; margin-top: var(--space-sm); }
  .today-spend-card .label { font-size: 14px; font-weight: 500; color: var(--color-text); }
  .today-spend-card .value { margin-left: auto; font-size: 16px; font-weight: 700; flex-shrink: 0; }

  .section-head { margin: var(--space-xl) 0 var(--space-xs); display: flex; align-items: center; justify-content: space-between; gap: 10px; }
  .section-head h3 { font-size: 15px; font-weight: 700; }
  .section-head:first-of-type { margin-top: 0; }

  .tx-date-group { padding: 14px 4px 4px; font-size: 12px; font-weight: 700; color: var(--color-muted); text-transform: uppercase; letter-spacing: 0.02em; }
  .tx-date-group:first-child { padding-top: 4px; }
  .tx-row-wrap { position: relative; overflow: hidden; border-bottom: 1px solid var(--color-divider); touch-action: pan-y; user-select: none; }
  .tx-row-wrap:last-child { border-bottom: none; }
  .tx-row-wrap .info { min-width: 0; flex: 1; }
  .tx-row-wrap .cat { font-size: 15px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .tx-row-wrap .meta { font-size: 12px; color: var(--color-muted); margin-top: 2px; }
  .tx-row-wrap .amt { font-size: 16px; font-weight: 700; flex-shrink: 0; }
  .tx-row-inner { display: flex; align-items: stretch; width: 100%; position: relative; z-index: 1; background: var(--color-card); transition: transform 240ms cubic-bezier(.22,1,.36,1); }
  .tx-row-inner.dragging { transition: none; }
  .tx-lead { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; padding: 12px 4px 12px 4px; }
  /* The amount stays in the same rigid block as the icon/category/note.
     .tx-row-inner translates as one unit; the wrapper clips its leading
     edge instead of shrinking text to reveal the actions behind it. */
  .tx-trail-group { display: flex; align-items: stretch; flex-shrink: 0; }
  .tx-trail { display: flex; align-items: center; flex-shrink: 0; padding: 12px 0 12px 8px; cursor: grab; }
  .tx-row-actions { position: absolute; z-index: 0; top: 0; right: 0; bottom: 0; display: flex; align-items: center; width: 96px; gap: 4px; padding: 0 0 0 12px; }
  .tx-row-actions .tx-swipe-action {
    width: 40px; height: 40px; min-width: 40px; padding: 0;
    border-radius: 20px; box-shadow: none; transform: scale(0); transform-origin: right center;
    transition: transform 240ms cubic-bezier(.22,1,.36,1);
  }
  .tx-row-actions.dragging .tx-swipe-action { transition: none; }
  .tx-row-actions .tx-swipe-edit { background: var(--color-border); color: var(--color-text); }
  .tx-row-actions .tx-swipe-delete { background: var(--color-expense); color: #fff; }
  .tx-row-actions.full-swipe .tx-swipe-edit { opacity: 0; pointer-events: none; }
  .tx-row-actions.full-swipe { inset: 0; width: auto; gap: 0; padding: 0; }
  .tx-row-actions.full-swipe .tx-swipe-delete { position: absolute; z-index: 1; }
  /* Per-row date column + table header row -- both exist in markup at
     every width (tx-row.js always renders .tx-date-cell; transactions.js
     always renders .tx-table-head) but stay hidden except in the
     Transactions screen's own desktop table mode (.tx-list-card, see the
     1024px breakpoint below) -- the mobile swipe-to-reveal row, and every
     other consumer of txRowHtml (Home's recent-activity list), never sees
     either at any width. */
  .tx-date-cell, .tx-table-head { display: none; }
  .empty-note { padding: 36px 0; text-align: center; color: var(--color-muted); font-size: 13px; }
  /* Distinguishes "your search/filter found nothing" (this) from the
     app's other, plain .empty-note first-use states ("you haven't added
     any budgets yet", etc.) -- those stay untouched, generic, and
     intentionally minimal per this repo's earlier onboarding pass. */
  .empty-note-search { display: flex; flex-direction: column; align-items: center; gap: 10px; }
  .empty-note-search .icon { width: 28px; height: 28px; color: var(--color-tertiary); }

  .budget-item .row1 { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; }
  .budget-item .row1 .right { color: var(--color-muted); }
  .bar-track { height: 8px; border-radius: var(--radius-pill); background: var(--color-surface); overflow: hidden; }
  .bar-fill { height: 100%; border-radius: var(--radius-pill); }
  .budgets-list { display: flex; flex-direction: column; gap: var(--space-sm); padding: 16px; }

  /* — transactions filters — */
  .filter-row { display: flex; gap: 8px; margin-bottom: 10px; }
  .filter-row select.input { flex: 1; min-width: 0; }
  /* The pill's .picker-anchor is content-sized by default (see the
     shared-popover comment below) -- fine for Insights' .toolbar-row,
     which explicitly stretches it, but left unstretched here the pill
     hugs the left edge of the field and its centered popover can overflow
     past the sheet's own left edge. Match Insights' treatment so the pill
     spans the full field width and the popover centers correctly. */
  .filter-row .picker-anchor { flex: 1; min-width: 0; }
  .filter-row .period-pill.wide { width: 100%; }

  /* docs/specs/transactions-filters-rework.md -- Type, Date, Category, and
     Amount all collapse behind this button/sheet; only Search stays
     permanently visible, sharing one row with the Filters button (see
     .tx-toolbar-row below). Insights' Breakdown tab shares its own
     Filters button with its pill instead, via .toolbar-row (see the
     insights section further down). */
  .tx-toolbar-row { display: flex; gap: 8px; align-items: center; margin-bottom: var(--space-sm); }
  .tx-search-field { position: relative; flex: 1; min-width: 0; }
  .tx-search-field .input { width: 100%; padding-left: 42px; padding-right: 42px; }
  .tx-search-icon {
    position: absolute; left: 14px; top: 50%; transform: translateY(-50%); z-index: 1;
    display: flex; color: var(--color-muted); pointer-events: none;
  }
  .tx-search-icon .icon { width: 18px; height: 18px; }
  .tx-search-clear {
    position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
    width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
    padding: 0; border: none; border-radius: 50%; background: none;
    color: var(--color-muted); font-size: 22px; line-height: 1; cursor: pointer;
  }
  .tx-search-clear[hidden] { display: none; }
  .tx-search-clear:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }
  .tx-toolbar-row .filters-btn {
    position: relative; flex: 0 0 44px; width: 44px; height: 44px;
    display: flex; align-items: center; justify-content: center;
    padding: 0; border: 1px solid var(--color-border); border-radius: 50%;
    background: var(--color-card); color: var(--color-text); cursor: pointer;
  }
  .tx-toolbar-row .filters-btn:hover { background: var(--color-surface); }
  .tx-toolbar-row .filters-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  .filter-badge {
    position: absolute; top: -6px; right: -6px; min-width: 16px; height: 16px;
    display: flex; align-items: center; justify-content: center; padding: 0 4px;
    background: var(--color-accent); color: #fff; border-radius: 999px; font-size: 10px; font-weight: 700;
  }
  /* This class's own `display: flex` above has the same specificity as the
     browser's default `[hidden] { display: none }` UA rule, and (being an
     author-stylesheet rule loaded after the UA sheet) wins the tie on
     source order -- so a badge set to `hidden` would otherwise still
     render. This rule out-specifies both to make hidden actually hide it. */
  .filter-badge[hidden] { display: none; }
  .active-filter-chips { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-bottom: var(--space-sm); }
  .active-filter-chips:empty { margin-bottom: 0; }
  .filter-chip {
    display: inline-flex; align-items: center; gap: 4px; border: none; cursor: pointer;
    padding: 5px 10px; border-radius: var(--radius-pill);
    background: var(--color-accent-tint); color: var(--color-accent-600); font-size: 12px; font-weight: 600;
  }
  .filter-chip:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  .filter-sheet-backdrop {
    position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 50; display: flex; align-items: flex-end;
    animation: backdrop-fade-in var(--duration-normal) var(--ease-standard);
  }
  .filter-sheet-backdrop[hidden] { display: none; }
  @keyframes backdrop-fade-in { from { opacity: 0; } to { opacity: 1; } }
  .filter-sheet {
    width: 100%; max-width: 480px; margin: 0 auto; max-height: 80vh;
    background: var(--color-card); border-radius: 20px 20px 0 0; padding: 20px;
    padding-top: 8px;
    padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
    box-shadow: var(--shadow-lg); animation: sheet-slide-up var(--duration-normal) var(--ease-standard);
    display: flex; flex-direction: column; gap: 16px;
  }
  /* The sheet shell keeps the viewport-sized max-height that
     syncSheetToViewport() updates and the transform that wireSheetDrag()
     updates. Its inner body is the only scrollport, so Chrome's native
     scroll-into-view no longer scrolls the same box being resized while the
     keyboard opens. Matched negative margins and padding extend that
     scrollport's clip box to the shell edges without moving its content,
     leaving room for child outlines and hiding top-edge clipping directly
     below the header divider. min-height:0 is required for this flex child
     to shrink below its content height and actually scroll. */
  .sheet-body {
    margin: -16px -20px calc(-20px - env(safe-area-inset-bottom, 0px));
    padding: 16px 20px calc(20px + env(safe-area-inset-bottom, 0px));
    flex: 1 1 auto; min-height: 0; overflow-y: auto;
    display: flex; flex-direction: column; gap: 16px;
  }
  @keyframes sheet-slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } }
  /* A drag that doesn't clear wireSheetDrag()'s dismiss threshold animates
     back to translateY(0) via this class (added right before the reset,
     removed on transitionend) -- deliberately not a permanent transition on
     .filter-sheet itself, which would fight the live 1:1 tracking during an
     active drag (utils.js sets .style.transform directly, no transition,
     while dragging is true). */
  .filter-sheet.snap-back { transition: transform var(--duration-fast) var(--ease-standard); }
  /* The header is a non-scrolling sibling before .sheet-body, so its title
     and actions stay reachable without sticky positioning. padding-top
     (paired with .filter-sheet's reduced padding-top above) makes room for
     .sheet-grabber; position:relative establishes the containing block for
     that absolutely positioned first child without making the header part
     of the body's scrolling or stacking behavior. */
  .filter-sheet-header {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    position: relative; flex-shrink: 0; background: var(--color-card);
    padding-top: 16px; padding-bottom: 10px; border-bottom: 1px solid var(--color-border);
  }
  /* The drag handle every sheet renders as its header's first child
     (sheetGrabberHtml()/wireSheetDrag(), utils.js) -- position:absolute so
     it overlays the header's own padding-top space without affecting the
     title/buttons row's flex layout at all. A generous invisible hit area
     (64x16) around the small visible bar (36x4), matching this app's other
     touch targets. */
  .sheet-grabber {
    position: absolute; top: 0; left: 50%; transform: translateX(-50%);
    width: 64px; height: 16px; display: flex; align-items: center; justify-content: center;
    cursor: grab; touch-action: none;
  }
  .sheet-grabber::after { content: ""; width: 36px; height: 4px; border-radius: 999px; background: var(--color-border); }
  .filter-sheet-header h3 { font-size: 16px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .filter-sheet-header h3.sheet-title-center { text-align: center; }
  .filter-sheet-close-btn { background: none; border: none; font-size: 22px; line-height: 1; color: var(--color-muted); cursor: pointer; padding: 4px 8px; }
  .filter-sheet-close-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  /* The Add sheet's header Cancel/Save pair (add.js's renderAddSheet) --
     text buttons, not .btn, matching the Cancel/Save convention from
     Apple's own Sheets HIG rather than this app's usual full-width .btn. */
  .sheet-header-btn { background: none; border: none; font-size: 15px; cursor: pointer; padding: 6px 4px; color: var(--color-muted); flex-shrink: 0; border-radius: 6px; }
  .sheet-header-btn-primary { color: var(--color-accent); font-weight: 700; }
  .sheet-header-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  /* docs/specs/app-lock.md stage 3. Deliberately its own class, not a
     reskinned .filter-sheet-backdrop/.filter-sheet: those are bottom
     sheets (slide up, rounded top corners only, implicitly swipeable),
     and a lock screen reading as "swipeable" would be actively misleading
     -- it has no wireSheetDrag() wired at all, on purpose (see the spec's
     own note on why). What IS reused is the underlying mechanics: fixed
     full-viewport positioning, createFocusTrap()'s focus-trap + piggybacked
     scroll-lock, and a centered dialog card. z-index 60 -- deliberately
     above .filter-sheet-backdrop's 50 and .toast's 40, so nothing (an
     open sheet from before backgrounding, a background-sync toast) can
     ever render on top of the lock screen while it's up. */
  .app-lock-backdrop {
    position: fixed; inset: 0; background: var(--color-bg); z-index: 60;
    display: flex; align-items: center; justify-content: center; padding: 24px;
    animation: backdrop-fade-in var(--duration-normal) var(--ease-standard);
  }
  .app-lock-panel {
    width: 100%; max-width: 340px; background: var(--color-card); border-radius: var(--radius-xl);
    box-shadow: var(--shadow-lg); padding: 32px 24px; display: flex; flex-direction: column;
    align-items: center; gap: 14px; text-align: center;
  }
  .app-lock-panel h2 { font-size: 18px; margin: 0; }
  .app-lock-pin-input {
    width: 160px; text-align: center; font-size: 24px; letter-spacing: 12px; padding: 10px 0 10px 12px;
  }
  .app-lock-forgot-btn { background: none; border: none; color: var(--color-accent); font-size: 13px; font-weight: 600; cursor: pointer; padding: 6px; }
  .app-lock-forgot-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  .app-lock-forgot-note { font-size: 11px; color: var(--color-muted); margin: -6px 0 0; max-width: 260px; }
  /* Wrong-PIN feedback -- re-triggered on each failed attempt via a
     remove/reflow/add cycle (see applock.js), since re-adding the same
     class name alone wouldn't restart an already-finished CSS animation. */
  @keyframes app-lock-shake {
    10%, 90% { transform: translateX(-1px); } 20%, 80% { transform: translateX(2px); }
    30%, 50%, 70% { transform: translateX(-4px); } 40%, 60% { transform: translateX(4px); }
  }
  .app-lock-shake { animation: app-lock-shake 400ms ease-in-out; }
  /* Backgrounded-app content cover (decision 2 of the spec): targets the
     actual chrome regions directly, NOT a filter on their shared .app
     ancestor -- a filter on an ancestor blurs every descendant, which
     would also blur the lock screen itself once rendered into
     #appLockContainer (also inside .app). Targeting #screen/.sidebar/
     .tabbar-wrap directly leaves #appLockContainer (and #toast) untouched
     regardless of how long .app-blurred stays applied. */
  .app-blurred #screen, .app-blurred .sidebar, .app-blurred .tabbar-wrap { filter: blur(20px); }
  .filter-checkbox-list { display: flex; flex-direction: column; gap: 2px; max-height: 240px; overflow-y: auto; }
  .filter-checkbox-row { display: flex; align-items: center; gap: 10px; padding: 8px 4px; font-size: 14px; cursor: pointer; }
  .amount-range-row { display: flex; align-items: center; gap: 8px; }
  .amount-range-row .input { flex: 1; min-width: 0; }

  /* — add form — */
  .add-form { display: flex; flex-direction: column; gap: 16px; }
  .transfer-swap-row:not(.form-field-hidden) { display: flex; justify-content: center; margin: -8px 0; }
  .transfer-swap-row .btn-icon { transform: rotate(90deg); }
  /* Standard visually-hidden-but-still-announced pattern -- keeps a real
     <label> in the DOM (for txAmount on the mobile Add sheet, see add.js)
     for screen readers without rendering visible text where the large
     amount input + its ฿ prefix already make the field's purpose obvious. */
  .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
  /* Mobile Add-sheet hierarchy pass (docs/specs/add-transaction-bottom-sheet.md
     phase 1): #txAmount gets a large, centered, borderless-except-underline
     treatment here instead of the standard .input-wrap box desktop keeps.
     .amount-hero-row still carries the base .input-wrap class (see add.js)
     so the existing has-error/focus-within plumbing keeps working -- these
     rules only override what needs to look different, at higher specificity
     via the extra class rather than fighting .input-wrap's own rules. */
  .field-amount-hero { align-items: center; }
  .amount-hero-row {
    border: none; border-bottom: 2px solid var(--color-border); border-radius: 0; background: transparent;
    height: auto; padding: 8px 0 12px; justify-content: center; gap: 6px;
  }
  .amount-hero-row:focus-within { outline: none; border-bottom-color: var(--color-accent); }
  .amount-hero-row.has-error { border-bottom-color: var(--color-expense); }
  .amount-hero-row .prefix { font-size: 22px; font-weight: 700; color: var(--color-muted); flex-shrink: 0; }
  .amount-hero-row .amount-hero-input {
    border: none; background: transparent; outline: none; padding: 0; height: auto;
    font-family: var(--font-sans); font-size: 40px; font-weight: 800; letter-spacing: -0.01em;
    color: var(--color-text); text-align: left; width: auto; max-width: 220px; min-width: 60px;
    font-variant-numeric: tabular-nums;
  }
  .amount-hero-row .amount-hero-input::placeholder { color: var(--color-tertiary); }
  /* Same #txDateText/#txDateNative/.date-icon markup and interaction as the
     full-size field (tap the icon corner for the native picker via
     .date-native-overlay, type directly anywhere else) -- only shrunk to a
     pill. .date-compact needs an explicit width (not the default block
     auto-fill-parent) so .date-native-overlay's absolute right:0 tracks the
     pill's own right edge instead of the field's full column width. */
  .date-compact { width: fit-content; }
  .date-compact .input-wrap {
    width: fit-content; height: 34px; padding: 0 30px 0 12px; border: none; border-radius: var(--radius-pill);
    background: var(--color-surface);
  }
  /* 104px, not a tighter guess: dateLabel() renders a 10-char dd/mm/YYYY
     string in both languages (Thai's Buddhist-Era year is 4 digits too,
     e.g. "04/09/2569") -- measured via scrollWidth against clientWidth in
     a real browser, a narrower width clipped the last 1-2 digits. */
  .date-compact .input-wrap input { flex: none; width: 104px; font-size: 13px; font-weight: 600; }
  /* Phase 2's live commit-preview row (mobile sheet only, see
     renderCommitPreview()/add.js) -- .info/.cat/.amt deliberately reuse
     tx-row.js's own class names (scoped under this different ancestor, so
     no collision) rather than inventing a parallel vocabulary for what is
     visually a miniature transaction row. */
  .commit-preview { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius-lg); background: var(--color-surface); }
  .commit-preview:empty { display: none; }
  .commit-preview .info { min-width: 0; flex: 1; }
  .commit-preview .cat { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .commit-preview .acc { font-size: 12px; color: var(--color-muted); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .commit-preview .amt { font-size: 15px; font-weight: 700; flex-shrink: 0; }

  /* — CSV import sheet (docs/specs/csv-import.md) — */
  .import-step { display: flex; flex-direction: column; gap: 16px; }
  .import-step-actions { display: flex; gap: 10px; }
  .import-step-actions .btn-primary { flex: 1; }
  .import-preview-row { padding: 6px 10px; border-radius: 8px; background: var(--color-bg); font-size: 13px; margin-bottom: 4px; }
  .import-preview-row-bad { color: var(--color-expense); }
  .date-input-wrap { position: relative; }
  .date-input-wrap .input-wrap input { padding-right: 24px; }
  .date-icon { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--color-tertiary); }
  .date-native-overlay { position: absolute; right: 0; top: 0; width: 40px; height: 100%; opacity: 0; cursor: pointer; border: none; padding: 0; margin: 0; }

  /* — insights — */
  /* docs/specs/insights-period-picker-redesign.md: the mode-switch tabs
     (Budgets/Categories/Trend) sit directly under the screen title; each
     tab then renders its own toolbar (the same pill component, see
     period-picker.js's pillPickerHtml) inside its own body -- there's no
     longer one shared period-picker row above both tabs the way there
     used to be, since Budgets and Breakdown don't share one
     period-picker instance (though they do share its component/shape). */
  #insightsModeTabs { margin-bottom: 14px; }
  .toolbar { display: flex; flex-direction: column; gap: 10px; margin-bottom: 18px; }
  /* Breakdown tab only: its pill shares a row with the Filters button,
     unlike Budgets' pill which sits alone -- .picker-anchor flexes to
     fill the remaining space so Filters stays a fixed, compact button
     alongside it (the pill renders nothing at all while a custom date is
     active, per pillPickerHtml's hasCustomRange check in insights.js --
     Filters then sits alone in this row). */
  .toolbar-row { display: flex; gap: 8px; align-items: stretch; }
  .toolbar-row .picker-anchor { flex: 1; min-width: 0; }
  .toolbar-row .period-pill.wide { width: 100%; }
  .toolbar-row .filters-btn { position: relative; flex-shrink: 0; }
  /* One-tap shortcut(s) inside the popover (opts.shortcuts in
     pillPickerHtml) -- Budgets passes none, Breakdown passes one
     ("today"), Transactions passes two ("all", "today"). A single
     shortcut fills the row (flex:1 on one child = 100%); two sit side by
     side at equal width. Dashed border distinguishes these from the
     month-grid cells below as a different kind of action (a shortcut,
     not one of the 12 equal choices). */
  .shortcut-row { display: flex; gap: 8px; margin-bottom: 10px; }
  .shortcut-btn {
    flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 7px;
    height: 38px; border-radius: var(--radius-md); border: 1px dashed var(--color-border);
    background: var(--color-surface); color: var(--color-text); font-family: var(--font-sans);
    font-weight: 600; font-size: 13px; cursor: pointer; white-space: nowrap;
  }
  .shortcut-btn:hover { border-color: var(--color-accent); color: var(--color-accent); }
  .shortcut-btn.active { background: var(--color-accent-tint); border-style: solid; border-color: var(--color-accent); color: var(--color-accent-700); }
  .shortcut-btn .icon { width: 15px; height: 15px; color: var(--color-accent); flex-shrink: 0; }

  /* — Budgets tab: standalone pill (prev/next steppers flanking a
     centered calendar icon + label). The label zone is its own flex:1
     with justify-content:center so the icon+text stay centered
     regardless of what ends up beside the pill -- the two step buttons
     are equal width (40px) so they cancel out and never pull the
     centered content off-axis. */
  .period-pill-row { display: flex; }
  .period-pill {
    display: flex; align-items: stretch; background: var(--color-card); border: 1px solid var(--color-border);
    border-radius: var(--radius-pill); height: 44px; overflow: hidden; box-shadow: var(--shadow-xs);
  }
  .period-pill.wide { flex: 1; min-width: 0; }
  .period-pill button { background: none; border: none; cursor: pointer; color: var(--color-muted); font-family: var(--font-sans); }
  .period-pill .step {
    width: 40px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
    transition: background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
  }
  .period-pill .step:hover { background: var(--color-surface); color: var(--color-text); }
  .period-pill .step:active { transform: scale(0.92); }
  .period-pill .step .icon { width: 17px; height: 17px; }
  .period-pill .trigger {
    flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 7px;
    font-size: 14.5px; font-weight: 700; color: var(--color-text); padding: 0 2px;
  }
  .period-pill .trigger .icon { width: 16px; height: 16px; color: var(--color-accent); flex-shrink: 0; }
  .period-pill .trigger span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  .period-pill .trigger:hover { color: var(--color-accent); }
  .period-pill .trigger[aria-expanded="true"] { color: var(--color-accent); }

  /* — shared popover: the month grid (Budgets + Breakdown's Month mode)
     and the year grid (Breakdown's Year mode) both use this same shell,
     anchored under whichever control opened it. — */
  .picker-anchor { position: relative; }
  .picker-popover {
    position: absolute; top: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(-6px) scale(0.97);
    width: min(300px, 88vw); background: var(--color-card); border-radius: var(--radius-xl);
    box-shadow: var(--shadow-lg); border: 1px solid var(--color-border); padding: 14px;
    z-index: 40; opacity: 0; pointer-events: none;
    transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard);
  }
  .picker-popover.open { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0) scale(1); }
  .picker-backdrop { position: fixed; inset: 0; z-index: 35; display: none; }
  .picker-backdrop.open { display: block; }
  .picker-year-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
  .picker-year-row .step {
    width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
    background: var(--color-surface); border: none; cursor: pointer; color: var(--color-text);
    transition: background var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard);
  }
  .picker-year-row .step:hover { background: var(--color-border); }
  .picker-year-row .step:active { transform: scale(0.9); }
  .picker-year-row .step .icon { width: 16px; height: 16px; }
  /* Tapping this heading switches to a whole-year view, in both Budgets'
     and Breakdown's pill -- see pillPickerHtml's doc comment for why
     both share this one component. */
  .picker-year-heading {
    border: none; background: none; cursor: pointer; font-family: var(--font-sans);
    font-size: 16px; font-weight: 800; color: var(--color-text); padding: 6px 14px; border-radius: var(--radius-pill);
    transition: background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
  }
  .picker-year-heading:hover { background: var(--color-accent-tint); color: var(--color-accent-700); }
  .picker-year-heading.selected { background: var(--color-accent); color: #fff; }
  .picker-month-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
  .picker-month-cell {
    border: none; background: var(--color-surface); color: var(--color-text); cursor: pointer;
    font-family: var(--font-sans); font-size: 13px; font-weight: 600; padding: 10px 2px; border-radius: var(--radius-md);
    transition: background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard);
  }
  .picker-month-cell:hover { background: var(--color-border); }
  .picker-month-cell:active { transform: scale(0.94); }
  .picker-month-cell.selected { background: var(--color-accent); color: #fff; box-shadow: var(--shadow-accent); }
  .picker-hint { text-align: center; font-size: 11px; color: var(--color-tertiary); margin-top: 10px; }

  /* — Filters sheet additions: the custom single-day/range date field
     (docs/specs/insights-period-picker-redesign.md) sits above the
     existing category checkbox list, reusing .field/.input-wrap/
     .amount-range-row/.filter-checkbox-list rather than introducing
     parallel form styling. */
  .filter-field-label { display: flex; align-items: center; justify-content: space-between; font-size: 12px; font-weight: 700; color: var(--color-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 8px; }
  .filter-field-label button { border: none; background: none; color: var(--color-accent); font-family: var(--font-sans); font-size: 12px; font-weight: 700; cursor: pointer; text-transform: none; letter-spacing: 0; }
  .kind-toggle { display: flex; gap: 8px; margin-bottom: 10px; }
  .kind-toggle button {
    flex: 1; height: 36px; border-radius: var(--radius-pill); border: 1px solid var(--color-border);
    background: var(--color-surface); color: var(--color-muted); font-family: var(--font-sans); font-weight: 600; font-size: 12.5px; cursor: pointer;
    transition: background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
  }
  .kind-toggle button.active { background: var(--color-accent-tint); border-color: var(--color-accent); color: var(--color-accent-700); }
  .field-hint { font-size: 11.5px; color: var(--color-tertiary); margin-top: 6px; line-height: 1.5; }

  .insight-rollup { display: flex; align-items: baseline; gap: 14px; padding: 18px 16px; margin-bottom: 14px; }
  .insight-rollup .pct { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; flex-shrink: 0; }
  .insight-rollup .detail { min-width: 0; }
  .insight-rollup .label { font-size: 13px; color: var(--color-muted); }
  .insight-rollup .figures { font-size: 13px; color: var(--color-muted); margin-top: 2px; }
  .insight-cards { display: flex; flex-direction: column; gap: 14px; padding-bottom: 16px; }
  .insight-card { padding: 16px; }
  .insight-card .head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; gap: 8px; }
  .insight-card .head .cat { font-weight: 600; font-size: 14px; }
  .insight-card .foot { display: flex; justify-content: space-between; font-size: 12px; color: var(--color-muted); margin-top: 8px; }

  /* Was an inline style; now a class so the desktop breakpoint below can
     enlarge the chart itself (the svg has explicit width/height attributes
     that CSS width/height override, scaling proportionally via its
     viewBox) without touching derived.js's pieChartSvg(). */
  .breakdown-chart { display: flex; justify-content: center; margin-bottom: 16px; }
  .breakdown-row { margin-bottom: 14px; }
  .breakdown-row:last-child { margin-bottom: 0; }
  .breakdown-row .row1 { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; }
  .breakdown-row .row1 .right { color: var(--color-muted); }
  .legend-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 7px; vertical-align: middle; }

  .trend-legend { display: flex; gap: 16px; font-size: 12px; color: var(--color-muted); margin-bottom: 14px; }
  .trend-legend .swatch { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 6px; vertical-align: middle; }
  .trend-chart-card { padding: 20px 16px 8px; }
  .trend-chart { display: flex; align-items: flex-end; gap: 12px; height: 140px; }
  /* Now a real interactive target (tap/Enter reveals a toast with the
     month's exact income/expense figures, since the bars themselves are
     too narrow to print currency text on) -- cursor + focus ring make
     that discoverable rather than silently interactive. */
  .trend-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; height: 100%; justify-content: flex-end; cursor: pointer; border-radius: var(--radius-sm); }
  .trend-col:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  .trend-bars { display: flex; gap: 4px; align-items: flex-end; height: 100%; }
  .trend-bars .bar { width: 10px; border-radius: 4px 4px 0 0; }
  .trend-labels { display: flex; gap: 12px; margin-top: 8px; padding-bottom: 12px; }
  .trend-labels div { flex: 1; text-align: center; font-size: 11px; color: var(--color-tertiary); }

  /* — settings — */
  .settings-block { display: flex; flex-direction: column; gap: var(--space-xl); padding-bottom: var(--space-xl); max-width: 480px; }
  /* .settings-layout becomes list-left/detail-right at 1024px+. Below
     that breakpoint, data-mobile-subpage selects the root or one drill-in
     section while preserving the existing cards and rows. */
  .settings-nav { display: none; }
  .settings-panels { display: flex; flex-direction: column; gap: var(--space-xl); }
  .settings-section-label { font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: color-mix(in srgb, var(--color-muted) 92%, var(--color-text)); margin-bottom: var(--space-xs); }
  .settings-profile-header { display: flex; flex-direction: column; align-items: center; gap: var(--space-xs); text-align: center; }
  .avatar { width: 72px; height: 72px; border-radius: 50%; background: var(--color-surface); display: flex; align-items: center; justify-content: center; font-weight: 700; font-family: var(--font-sans); font-size: 18px; color: var(--color-muted); flex-shrink: 0; object-fit: cover; }
  .profile-name { font-weight: 600; font-size: 15px; }

  .toggle-row { display: flex; align-items: center; gap: 12px; padding: 12px 4px; border-bottom: 1px solid var(--color-divider); }
  .toggle-row:last-child { border-bottom: none; }
  .toggle-row .label { flex: 1; font-size: 15px; font-weight: 500; }
  /* export rows are real <button>s (keyboard/a11y) styled to look like a
     .toggle-row -- reset button chrome, then reapply the row layout. */
  button.toggle-row { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 12px; padding: 12px 4px; width: 100%; cursor: pointer; font: inherit; color: inherit; text-align: left; border-bottom: 1px solid var(--color-divider); }
  button.toggle-row:last-child { border-bottom: none; }

  .settings-disclosure { border-bottom: 1px solid var(--color-divider); }
  .settings-disclosure-trigger, button.settings-disclosure-trigger { border-bottom: none; }
  .settings-disclosure-copy { display: flex; flex: 1; min-width: 0; flex-direction: column; gap: 2px; }
  .settings-disclosure-label { font-size: 15px; font-weight: 500; }
  .settings-disclosure-value { display: flex; align-items: center; gap: 6px; color: var(--color-muted); font-size: 12px; }
  .settings-disclosure-trigger > .icon:last-child { flex-shrink: 0; transform: rotate(90deg); transition: transform var(--duration-fast) var(--ease-standard); }
  .settings-disclosure.expanded .settings-disclosure-trigger > .icon:last-child { transform: rotate(-90deg); }
  .settings-disclosure-control { padding: 0 4px 12px 40px; }
  .settings-disclosure-control .tabs { max-width: 100%; }
  .settings-disclosure-trigger:focus-visible, .settings-disclosure-control .tab-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  .settings-accent-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--color-accent); }

  .settings-screen .toggle-row > .icon, .settings-nav-item > .icon, .settings-manage-header > .icon { color: var(--color-muted); }
  .settings-drill-row > .icon:last-child, .settings-external-row > .icon:last-child { flex-shrink: 0; }
  .settings-item-count { font-size: 11px; font-weight: 700; color: var(--color-muted); background: var(--color-surface); border-radius: var(--radius-pill); padding: 0 var(--space-xs); }
  .settings-external-row { color: var(--color-text); text-decoration: none; }
  .settings-logout-row, button.settings-logout-row { color: var(--color-expense-700); }
  .settings-logout-row > .icon { color: var(--color-expense-700) !important; }
  .settings-manage-section { display: none; }
  .settings-manage-header, .settings-security-subpage-header {
    display: flex; align-items: center; gap: 12px; padding: 12px 4px;
  }
  .settings-manage-header .label, .settings-security-subpage-header h2 { flex: 1; min-width: 0; font-size: 15px; font-weight: 500; margin: 0; }
  .settings-manage-header > .icon, .settings-manage-header > .settings-item-count { display: none; }
  .settings-manage-body { padding: 0 4px var(--space-sm); }
  /* All five Manage sub-pages render their rows inside a .manage-rows-card
     (a plain .list-card, plus overflow:hidden -- see below) nested INSIDE
     .settings-manage-body -- not applied to .settings-manage-body itself
     -- specifically so the FAB-clearance bottom padding two lines down
     stays outside the visual card, in the page background, instead of
     showing as blank space inside the rounded card (a real defect a
     maintainer screenshot caught: the first version of this put .list-card
     directly on .settings-manage-body, so that padding rendered as a
     large empty area at the card's own bottom, before its rounded
     corner). docs/specs/settings-manage-rows-card-styling.md. overflow:hidden
     clips a full-swipe row's square corners to .manage-rows-card's own
     radius; scoped to this class rather than the shared .list-card rule
     so every other card in the app (Home, Insights, etc.) is unaffected. */
  .manage-rows-card { overflow: hidden; }
  /* Populated Goals is the one exception: it keeps its pre-existing
     individually-carded .goal-card look (bare .insight-cards, no
     .manage-rows-card) rather than the other four sections' unified
     divided list -- a flattened version was tried and reverted per
     maintainer feedback that separate cards looked better. Only Goals'
     *empty* state uses .manage-rows-card, so "no goals yet" isn't left
     floating bare on the page background. See
     docs/specs/settings-manage-rows-card-styling.md. */
  body.settings-subpage-open .settings-manage-body { padding-bottom: calc(var(--space-xl) + var(--space-xl) + var(--space-sm) + var(--space-xs)); }
  .settings-add-fab { position: fixed; right: var(--space-md); bottom: calc(env(safe-area-inset-bottom, 0px) + var(--space-xl)); aspect-ratio: 1; padding: 0; z-index: 20; }
  .settings-layout [data-settings-panel="security"] { display: none; }
  .settings-layout:not([data-mobile-subpage="root"]) [data-settings-panel="display"],
  .settings-layout:not([data-mobile-subpage="root"]) [data-settings-panel="sync"] { display: none; }
  .settings-layout:not([data-mobile-subpage="root"]) .settings-mobile-manage-nav { display: none; }
  .settings-layout[data-mobile-subpage="budgets"] .settings-manage-section[data-settings-section-content="budgets"],
  .settings-layout[data-mobile-subpage="bills"] .settings-manage-section[data-settings-section-content="bills"],
  .settings-layout[data-mobile-subpage="goals"] .settings-manage-section[data-settings-section-content="goals"],
  .settings-layout[data-mobile-subpage="categories"] .settings-manage-section[data-settings-section-content="categories"],
  .settings-layout[data-mobile-subpage="accounts"] .settings-manage-section[data-settings-section-content="accounts"] { display: block; }
  .settings-layout[data-mobile-subpage="security"] [data-settings-panel="manage"] { display: none; }
  .settings-layout[data-mobile-subpage="security"] [data-settings-panel="security"] { display: block; }
  .settings-layout[data-mobile-subpage="security"] [data-settings-panel="security"] > .settings-section-label { display: none; }

  #syncStatus { font-size: 12.5px; color: var(--color-muted); display: flex; align-items: center; gap: 6px; }
  #syncStatus.ok { color: var(--color-income-700); }
  #syncStatus.err { color: var(--color-expense-700); }
  .sync-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--color-tertiary); flex-shrink: 0; }
  #syncStatus.ok .sync-dot { background: var(--color-income-700); }
  #syncStatus.err .sync-dot { background: var(--color-expense-700); }

  .manage-row { display: flex; align-items: center; gap: 12px; padding: 12px 4px; border-bottom: 1px solid var(--color-divider); }
  .manage-row:last-child { border-bottom: none; }
  .manage-row .info { min-width: 0; flex: 1; }
  .manage-row .name { font-size: 15px; font-weight: 500; }
  .manage-row .sub { font-size: 12px; color: var(--color-muted); margin-top: 2px; }
  .manage-row .amt { font-size: 14px; font-weight: 600; flex-shrink: 0; }
  /* Mobile-only (<1024px) swipe-to-reveal for Manage rows
     (docs/specs/settings-manage-swipe-and-sheet.md), a sibling of
     tx-row.js's .tx-row-wrap/.tx-trail-group pattern -- see
     manage-row-swipe.js's own doc comment for why .manage-row-content
     needs no fixed natural-width baseline the way .tx-lead does. The
     wrapper owns the row's border-bottom (not .manage-row-content
     itself, which reuses the plain .manage-row rules above and would
     otherwise double up the divider line). */
  .manage-row-wrap { position: relative; overflow: hidden; border-bottom: 1px solid var(--color-divider); touch-action: pan-y; user-select: none; }
  .manage-row-wrap:last-child { border-bottom: none; }
  .manage-row-wrap .manage-row-content,
  .goal-card-top-content { position: relative; z-index: 1; width: 100%; box-sizing: border-box; background: var(--color-card); transition: transform 240ms cubic-bezier(.22,1,.36,1); }
  .manage-row-wrap .manage-row-content { border-bottom: none; }
  .manage-row-wrap .manage-row-content.dragging,
  .goal-card-top-content.dragging { transition: none; }
  .manage-row-actions-group { position: absolute; z-index: 0; inset-block: 0; right: 0; display: flex; align-items: center; }
  .manage-row-actions-group .row-actions { width: 100%; height: 100%; align-items: center; gap: 4px; padding: 0 0 0 12px; }
  .manage-row-actions-group .manage-swipe-action {
    width: 40px; height: 40px; min-width: 40px; padding: 0;
    border-radius: 20px; box-shadow: none; transform: scale(0); transform-origin: right center;
    transition: transform 240ms cubic-bezier(.22,1,.36,1);
  }
  .manage-row-actions-group.dragging .manage-swipe-action { transition: none; }
  .manage-row-actions-group .manage-swipe-edit,
  .manage-row-actions-group .manage-swipe-archive { background: var(--color-border); color: var(--color-text); }
  .manage-row-actions-group .manage-swipe-delete { background: var(--color-expense); color: #fff; }
  .manage-row-actions-group.full-swipe { position: absolute; inset: 0; width: auto; gap: 0; padding: 0; }
  .manage-row-actions-group.full-swipe .row-actions { gap: 0; padding: 0; }
  .manage-row-actions-group.full-swipe .manage-swipe-action:not(.manage-swipe-delete) { opacity: 0; pointer-events: none; }
  .manage-row-actions-group.full-swipe .manage-swipe-delete { position: absolute; z-index: 1; }
  /* Goal cards (mobile-only swipe for Edit/Delete, Contribute stays
     always-visible outside the swipe wrapper -- docs/specs/
     settings-manage-swipe-and-sheet.md) are their own self-contained box,
     not a list row -- no divider needed between the swiped area and the
     Contribute button beside it. */
  .manage-row-wrap.goal-card-swipe-wrap { border-bottom: none; flex: 1; min-width: 0; }
  .goal-card-top-content { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; }
  /* Category add/edit form's icon picker (docs/specs/custom-categories.md
     stage 3) -- selection is tracked purely by the .selected class
     (settings.js reads it straight from the DOM at save time), so this is
     the only place that class needs a visual treatment. */
  .icon-picker { display: flex; flex-wrap: wrap; gap: 8px; }
  .icon-picker-option {
    width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0;
    border: 2px solid transparent; background: var(--color-surface); color: var(--color-tertiary);
    display: flex; align-items: center; justify-content: center; cursor: pointer;
  }
  .icon-picker-option:hover { background: var(--color-border); }
  .icon-picker-option.selected { border-color: var(--color-accent); background: var(--color-accent-tint); color: var(--color-accent); }
  .icon-picker-option:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
  /* Overdue bill rows (Home's upcoming-bills list and Settings' bill rows) --
     reuses the expense color rather than inventing a new one, distinct from
     the plain --color-muted "due soon" text every other row uses. */
  .manage-row-overdue .sub { color: var(--color-expense-700); font-weight: 600; }
  /* Archived accounts (Settings' Accounts section) -- a neutral, dimmed
     treatment, deliberately not reusing manage-row-overdue's warning/expense
     color, since "archived" isn't a problem state the way an overdue bill
     is. */
  .manage-row-archived { opacity: 0.6; }
  /* Subcategory rows in Settings' Categories list (docs/specs/
     category-nesting.md stage 3) -- indented so a parent immediately
     followed by its children reads visually as a tree, without an actual
     collapsible widget (see that spec's decision on why). */
  .manage-row-child { padding-left: 28px; }
  .row-actions { display: flex; gap: 4px; flex-shrink: 0; }
  .section-add-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
  .inline-form { display: flex; flex-direction: column; gap: 12px; padding: 16px; margin-bottom: 12px; }
  .inline-form .actions-row { display: flex; gap: 8px; }
  .inline-form .actions-row .btn { flex: 1; }

  .goal-card { padding: 16px; }
  .goal-card .top { display: flex; align-items: center; gap: 12px; }
  .goal-card .name { font-weight: 700; font-size: 15px; }
  .goal-card .progress-label { font-size: 13px; color: var(--color-muted); margin-top: 2px; }
  .goal-card .bar-track { margin-top: 12px; }
  .goal-card-actions { display: flex; gap: 4px; flex-shrink: 0; align-items: center; }

  .footer-note { font-size: 11px; line-height: 1.6; color: var(--color-tertiary); margin: 4px 0 0; }

  /* — tab bar — */
  /* padding-bottom (not just the fixed 64px .tabbar height below) is what
     actually pushes the buttons up clear of a notched/gesture-nav
     device's home indicator -- .screen's padding and .toast's bottom
     offset both add this same env() value on top of --tabbar-h so every
     fixed-position element anchored to "above the tab bar" agrees on the
     tab bar's true rendered height. */
  .nav-btn:focus-visible,
  .tab-opt:has(input:focus-visible),
  .switch:focus-visible,
  button.toggle-row:focus-visible,
  .home-profile-btn:focus-visible,
  .toast-undo-btn:focus-visible,
  .shortcut-btn:focus-visible,
  .period-pill button:focus-visible,
  .picker-year-row .step:focus-visible,
  .picker-year-heading:focus-visible,
  .picker-month-cell:focus-visible,
  .filter-field-label button:focus-visible,
  .kind-toggle button:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

  .tabbar-wrap { position: fixed; left: 0; right: 0; bottom: 0; display: flex; justify-content: center; background: color-mix(in srgb, var(--color-bg) 82%, transparent); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-top: 1px solid var(--color-divider); z-index: 30; padding-bottom: env(safe-area-inset-bottom, 0px); }
  .tabbar { display: flex; width: 100%; max-width: 480px; height: var(--tabbar-h); }
  /* Icon-only bar -- no <span> label in the markup any more (removed along
     with #sidebar's equivalent text, which DOES keep its labels: this is a
     mobile-bar-only change). min-width:0 is a holdover from when label
     width could force a button wider than its siblings; harmless to keep
     now that flex:1 alone already divides the bar evenly, but doesn't need
     removing either. */
  .tabbar button {
    flex: 1; min-width: 0; display: flex; align-items: center; justify-content: center;
    background: none; border: none; padding: 0; cursor: pointer; color: var(--color-tabbar-inactive);
    font-family: var(--font-sans);
    transition: color var(--duration-fast) var(--ease-standard);
  }
  /* Active state is color-only now that the bolder-label second cue (the
     label itself) is gone -- a deliberate tradeoff of the icon-only
     redesign, not an oversight. */
  .tabbar button.active { color: var(--color-accent); }
  /* 22px unchanged from before the labels were removed -- just re-centered
     in the bar with nothing below it now. The add tab's own [data-tab="add"]
     rule below is already more specific and is untouched by this. */
  .tabbar button:not([data-tab="add"]) .icon {
    width: 22px; height: 22px;
    transition: transform var(--duration-fast) var(--ease-standard);
  }
  .tabbar button:active .icon { transform: scale(0.88); }
  /* The "add" tab is the app's primary action, so it's broken out of the
     bar visually (raised circle, accent gradient) rather than sitting as
     a 5th equal-weight icon -- literally the same --hero-gradient-start/
     -end stops and 135deg angle .hero-card uses (previously this comment
     claimed that consistency but the rule below actually used
     --color-accent/-700, a flatter, less saturated pair -- fixed as part
     of the post-WI-018 gradient-system pass, see the --hero-gradient-*
     comment in :root above). Padding shrinks the icon's own rendered content within
     the circle (SVG content box = width/height minus padding under the
     app's global box-sizing:border-box), independent of .icon's own
     18px default, which this selector out-specifies. 60px/16px padding/
     -23px raise all scaled up from the pre-redesign 52px/14px/-20px,
     keeping the same glyph-to-circle ratio and the same amount of "poke"
     above the bar's top edge. */
  .tabbar button[data-tab="add"] .icon {
    width: 60px; height: 60px; padding: 16px; flex-shrink: 0;
    border-radius: 50%; color: #ffffff;
    background: linear-gradient(135deg, var(--hero-gradient-start) 0%, var(--hero-gradient-end) 100%);
    box-shadow: var(--shadow-accent);
    margin-top: -23px;
  }

  /* — desktop sidebar (>=1024px, see the breakpoint below) —
     Hidden by default; only shown once the 1024px breakpoint flips .app to
     a row layout and hides .tabbar-wrap. Uses .nav-btn (shared with
     .tabbar's buttons) for router.js/main.js to wire active-state/clicks
     identically -- only one of the two navs is ever visible at a time.
     height:100vh (not "however tall .screen's content happens to be", the
     original approach) is what makes .sidebar-footer's margin-top:auto
     actually pin it to the bottom of the *viewport* -- with a shorter,
     content-sized sidebar, "the bottom" could be anywhere on a tall
     Transactions/Insights page, defeating the point of an at-a-glance
     footer. position:sticky;top:0 on a box already exactly viewport-tall
     behaves like position:fixed for this single-instance case, but without
     needing to take the element out of the row's normal flow. */
  .sidebar {
    display: none; flex-direction: column; gap: 2px;
    width: 248px; flex-shrink: 0; height: 100vh;
    position: sticky; top: 0;
    background: var(--color-card); border-right: 1px solid var(--color-border);
    padding: 20px 12px; box-sizing: border-box;
  }
  .sidebar-header { display: flex; align-items: center; gap: 10px; padding: 4px 10px 18px; }
  /* Two-tone lockup icon (docs/specs/coral-rebrand-and-logo.md's follow-up):
     ring uses --logo-ring-color (var(--color-accent) in light, so it still
     follows the Coral/Purple preference; solid white in dark via the
     [data-theme="dark"] block, matching the design's own "dark lockup"
     reference exactly, which uses white regardless of hue). Handle+dot use --color-logo-amber always,
     in both themes, also matching the reference. No background box here --
     unlike the standalone app icon PNG, the lockup's icon is bare. */
  .sidebar-logo { width: 26px; height: 26px; flex-shrink: 0; }
  .sidebar-logo .ring { fill: none; stroke: var(--logo-ring-color); stroke-width: 8; }
  .sidebar-logo .handle { stroke: var(--color-logo-amber); stroke-width: 8; stroke-linecap: round; }
  .sidebar-logo .dot { fill: var(--color-logo-amber); }
  .sidebar-title { font-family: "Poppins", var(--font-sans); font-weight: 700; font-size: 18px; letter-spacing: -0.01em; color: var(--color-text); }
  .sidebar .nav-btn {
    display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: 12px;
    width: 100%; padding: 10px 12px; border-radius: var(--radius-md);
    background: none; border: none; cursor: pointer; color: var(--color-tabbar-inactive);
    font-family: var(--font-sans); text-align: left;
    transition: color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard);
  }
  .sidebar .nav-btn .icon { width: 20px; height: 20px; }
  .sidebar .nav-btn span { font-size: 14px; font-weight: 600; }
  .sidebar .nav-btn:hover { background: var(--color-surface); }
  .sidebar .nav-btn.active { color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
  .sidebar .nav-btn.active span { font-weight: 700; }
  /* Pushed to the bottom of the sidebar's own flex column via margin-top:
     auto -- status only, not a nav target (Settings' own profile row
     already owns sign-in/out), so it's a plain div, not a .nav-btn. */
  .sidebar-footer {
    display: flex; align-items: center; gap: 10px;
    margin-top: auto; padding: 12px 12px 4px; border-top: 1px solid var(--color-divider);
  }
  .sidebar-account-avatar {
    display: flex; align-items: center; justify-content: center; flex-shrink: 0;
    width: 28px; height: 28px; border-radius: 50%; background: var(--color-surface); color: var(--color-tertiary);
  }
  .sidebar-account-avatar .icon { width: 15px; height: 15px; }
  .sidebar-account-name {
    font-size: 13px; font-weight: 600; color: var(--color-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  }

  .toast {
    /* Same 18px gap above the tab bar as before (82px was 64px + 18px),
       now tracking --tabbar-h and the real safe-area inset instead of a
       flat number, so the toast can't end up rendering under a tab bar
       that's grown taller for its own safe-area padding. */
    position: fixed; left: 50%; bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + 18px); transform: translateX(-50%);
    background: var(--gray-900); color: #fff; padding: 12px 18px; font-size: 13px; font-weight: 500;
    border-radius: var(--radius-pill); box-shadow: var(--shadow-lg);
    z-index: 40; max-width: 420px; width: calc(100% - 40px);
    animation: toast-fade-in var(--duration-normal) var(--ease-standard);
    display: flex; align-items: center; justify-content: center; gap: 14px;
  }
  body.settings-subpage-open .toast { bottom: calc(env(safe-area-inset-bottom, 0px) + 18px); }
  .toast[hidden] { display: none; }
  @keyframes toast-fade-in { from { opacity: 0; transform: translateX(-50%) translateY(8px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
  .toast-undo-btn {
    background: none; border: none; padding: 0; min-height: 0; flex-shrink: 0;
    color: #fff; font-family: var(--font-sans); font-weight: 700;
    font-size: 12.5px; text-decoration: underline; cursor: pointer;
  }

  @media (min-width: 560px) {
    .screen, .tabbar { max-width: 480px; }
    .app { background: var(--color-surface); }
    .screen { background: var(--color-bg); }
  }

  /* — desktop: same bottom bar, just a wider stage with multi-column screens —
     grid tracks use minmax(0, Nfr) rather than bare Nfr: a bare fr track's
     minimum defaults to auto (based on its content's min-content size), so
     a wide child anywhere inside -- long unwrapped text, or a
     JS-driven width like the swipeable transaction row's reveal --
     pushes the whole track (and everything up to the page) wider instead
     of shrinking to fit, a classic CSS Grid "blowout". minmax(0, Nfr)
     caps each track's minimum at 0 so the fr distribution actually holds. */
  @media (min-width: 880px) {
    /* Home/Insights/Transactions actually use the extra width via their own
       internal grids (.home-columns/.insight-cards/.breakdown-columns below,
       plus Transactions' desktop table at 1024px further down); Add and
       Settings stay a comfortable centered column instead of stretching
       into a narrow strip against a big gray field -- the previous
       unconditional 1120px cap here did exactly that. .screen-wide is
       toggled by router.js's renderChrome() based on state.tab, alongside
       the calmer surrounding background already established by the 560px
       breakpoint above (.app surface / .screen card), which keeps applying
       since it's untouched here. */
    .screen { max-width: 640px; }
    .screen.screen-wide { max-width: 1120px; }
    .home-columns { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 32px; align-items: start; }
    .home-columns .hero-card { margin-top: 0; }
    /* The old compact one-row "tab-switch + period-picker" treatment this
       breakpoint used to apply doesn't carry over cleanly to the
       redesigned per-tab toolbars (docs/specs/insights-period-picker-redesign.md)
       -- Budgets' pill and Breakdown's tab-row+Filters button are different
       enough shapes that cramming them beside the outer mode tabs would
       look worse, not better, so both stay stacked at this width too.
       Flagged as a possible follow-up polish pass, not solved here. */
    .breakdown-columns { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 32px; align-items: start; }
    .insight-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
  }

  /* — desktop shell: persistent left sidebar replaces the bottom tab bar —
     Everything below 1024px stays exactly as above (tabbar-wrap fixed to
     the bottom, .app column layout) -- this block only adds overrides, it
     never touches an already-set property outside this query. .screen's
     own flex:1 + max-width + margin:0 auto combo (set above) already
     centers it within whatever space it's given -- that didn't need to
     change: flex-basis:0 (from flex:1) governs the *main* axis, which
     becomes horizontal once .app switches to row below, so .screen fills
     the remaining width beside the sidebar and its max-width/auto-margins
     center it in that space exactly the way they already centered it in
     the full viewport width before. */
  @media (min-width: 1024px) {
    .tabbar-wrap { display: none; }
    .sidebar { display: flex; }
    .app { flex-direction: row; }
    /* No fixed bottom bar in this mode -- the --tabbar-h + safe-area
       contribution to .screen's bottom padding and .toast's bottom offset
       (set above, for the mobile fixed tab bar) is now dead space with
       nothing to clear. */
    .screen { padding-bottom: 20px; }
    .toast { bottom: 24px; }
    .hero-arrow { display: inline-flex; }
    .hero-page { padding-left: 24px; padding-right: 24px; }

    /* — Add form: wrap the category chips instead of horizontal-scrolling
       them. Mobile's overflow-x:auto (with its scrollbar intentionally
       hidden, see the comment above .category-chip-row) relies on touch
       swipe as the discovery mechanism -- there's no such affordance with a
       mouse, so at this width the row wrapped instead, using the space a
       centered form column otherwise leaves idle on either side. */
    .category-chip-row { flex-wrap: wrap; overflow-x: visible; }

    /* — Insights: take further advantage of the space beside the sidebar,
       building on (not replacing) the 880px rules above -- 3 budget cards
       per row instead of 2, and a bigger pie chart since the breakdown
       list next to it has room to spare at this width. */
    .insight-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .breakdown-chart svg { width: 180px; height: 180px; }

    /* — Transactions: dense table-like rows instead of the mobile stacked
       swipe-reveal card — scoped to .tx-list-card (transactions.js's own
       wrapper) so Home's recent-activity list, which reuses the exact same
       txRowHtml/.tx-row-wrap markup, is completely unaffected at any width.
       Each row is its own single-row grid rather than one big table grid,
       matching the header's grid-template-columns exactly so columns line
       up -- .tx-row-inner and .tx-trail-group (mobile's sliding-content
       wrappers) are collapsed with display:contents so their children
       (.tx-lead, .tx-trail, .tx-row-actions) become direct grid items
       alongside .tx-date-cell, with no DOM or JS changes needed. */
    .tx-list-card .tx-table-head {
      display: grid; grid-template-columns: 100px 1fr 110px 108px; column-gap: 12px;
      padding: 10px 4px 8px; font-size: 12px; font-weight: 700; color: var(--color-muted);
      text-transform: uppercase; letter-spacing: 0.02em; border-bottom: 1px solid var(--color-divider);
    }
    .tx-list-card .tx-table-head span:nth-child(3) { text-align: right; }
    .tx-list-card .tx-date-group { display: none; }
    .tx-list-card .tx-date-cell { display: block; align-self: center; padding: 12px 4px; font-size: 13px; color: var(--color-muted); }
    .tx-list-card .tx-row-wrap {
      display: grid; grid-template-columns: 100px 1fr 110px 108px; column-gap: 12px;
      overflow: visible;
    }
    .tx-list-card .tx-row-inner, .tx-list-card .tx-trail-group { display: contents; }
    /* Actions are always visible as their own column here, not gated
       behind the mobile hover/swipe reveal. The mobile transform is inert
       once .tx-row-inner is display:contents, so the shared JS can run
       without a desktop-specific branch. */
    .tx-list-card .tx-trail { justify-content: flex-end; padding: 12px 0; cursor: default; }
    .tx-list-card .tx-row-actions { position: static; justify-content: center; padding: 0 12px; }
    .tx-list-card .tx-row-actions .tx-swipe-action { transform: none; }
    .tx-list-card .tx-row-actions.full-swipe { inset: auto; width: auto; gap: 0; padding: 0; }

    /* — Settings: list-left/detail-right instead of one long stacked page —
       .settings-block's own 480px cap (meant for a single narrow mobile
       column) is lifted so the nav + panel combination has room; router.js
       gives Settings .screen-wide at this width for the same reason
       Transactions did (see that comment). Only one of the three
       [data-settings-panel] blocks is shown at a time, selected by
       .settings-layout's own data-active attribute (kept in sync with
       state.settingsSubPage -- see settings.js) rather than a full
       re-render, so switching sections never disturbs an open inline form
       elsewhere on the page. */
    .settings-block { max-width: 100%; }
    .settings-layout { display: flex; align-items: flex-start; gap: 32px; }
    .settings-nav { display: flex; flex-direction: column; gap: var(--space-xs); width: 220px; flex-shrink: 0; padding: var(--space-xs); background: var(--color-card); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
    .settings-nav-item {
      display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px; border-radius: var(--radius-md);
      background: none; border: none; cursor: pointer; color: var(--color-text); font-family: var(--font-sans); text-align: left;
    }
    .settings-nav-item span { font-size: 14px; font-weight: 500; }
    .settings-nav-item:hover { background: var(--color-surface); }
    .settings-nav-item:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
    .settings-nav-item.active { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
    .settings-nav-item.active span { color: var(--color-accent); font-weight: 700; }
    .settings-panels { flex: 1; min-width: 0; }
    .settings-panels [data-settings-panel] { display: none; }
    .settings-mobile-manage-nav, .settings-back-btn, .settings-security-subpage-header { display: none; }
    .settings-layout[data-active="display"] [data-settings-panel="display"] { display: block; }
    .settings-layout[data-active="sync"] [data-settings-panel="sync"] { display: block; }
    .settings-layout[data-active="security"] [data-settings-panel="security"] { display: block; }
    .settings-layout[data-active="budgets"] [data-settings-panel="manage"],
    .settings-layout[data-active="bills"] [data-settings-panel="manage"],
    .settings-layout[data-active="goals"] [data-settings-panel="manage"],
    .settings-layout[data-active="categories"] [data-settings-panel="manage"],
    .settings-layout[data-active="accounts"] [data-settings-panel="manage"] { display: block; }
    .settings-layout[data-active="budgets"] .settings-manage-section[data-settings-section-content="budgets"],
    .settings-layout[data-active="bills"] .settings-manage-section[data-settings-section-content="bills"],
    .settings-layout[data-active="goals"] .settings-manage-section[data-settings-section-content="goals"],
    .settings-layout[data-active="categories"] .settings-manage-section[data-settings-section-content="categories"],
    .settings-layout[data-active="accounts"] .settings-manage-section[data-settings-section-content="accounts"] { display: block; }
    .settings-manage-header > .icon { display: flex; }
    .settings-manage-header > .settings-item-count { display: inline-block; }
  }
