/* ==========================================================================
   BudgetPro - mobile-first stylesheet
   Built for a phone held in one hand; scales up to a desktop browser.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Palette definitions.

   Every colour is declared once, as an -l (light) / -d (dark) pair. The three
   short blocks under "which half is live" do nothing but pick a side, so
   adding a colour means editing one line rather than three, and an accent
   palette is a single block covering both modes.
   -------------------------------------------------------------------------- */

:root {
    /* --- neutrals --- */
    --l-bg: #f4f6fa;             --d-bg: #0b1120;
    --l-surface: #ffffff;        --d-surface: #151d2e;
    --l-surface-2: #f8fafc;      --d-surface-2: #1a2334;
    --l-surface-3: #eef2f7;      --d-surface-3: #212c40;
    --l-border: #e2e8f0;         --d-border: #263145;
    --l-border-strong: #cbd5e1;  --d-border-strong: #37445d;
    --l-text: #0f172a;           --d-text: #e9eef7;
    --l-text-2: #475569;         --d-text-2: #a3b0c4;
    --l-text-3: #7c8899;         --d-text-3: #78859a;

    /* --- status: fixed across every accent, so "over budget" always reads
           the same whichever theme is chosen --- */
    --l-good: #059669;           --d-good: #34d399;
    --l-good-soft: #ecfdf5;      --d-good-soft: #0d2b22;
    --l-warn: #b45309;           --d-warn: #fbbf24;
    --l-warn-soft: #fffbeb;      --d-warn-soft: #2e2411;
    --l-bad: #dc2626;            --d-bad: #f87171;
    --l-bad-soft: #fef2f2;       --d-bad-soft: #34191c;
    --l-info: #0284c7;           --d-info: #38bdf8;
    --l-info-soft: #f0f9ff;      --d-info-soft: #0d2436;

    /* --- elevation --- */
    --l-shadow-sm: 0 1px 2px rgba(15, 23, 42, .06), 0 1px 3px rgba(15, 23, 42, .04);
    --d-shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);
    --l-shadow: 0 4px 14px rgba(15, 23, 42, .08);
    --d-shadow: 0 4px 14px rgba(0, 0, 0, .35);
    --l-shadow-lg: 0 12px 40px rgba(15, 23, 42, .18);
    --d-shadow-lg: 0 12px 40px rgba(0, 0, 0, .55);

    /* --- accent: Indigo, the default.
           --brand        buttons, the FAB, progress fill, hero gradient start
           --brand-strong hero gradient end - carries white text, so it stays
                          mid-dark in dark mode rather than brightening
           --brand-soft   tint behind chips and the active nav pill
           --brand-text   accent text on a soft or light background --- */
    --l-brand: #4f46e5;          --d-brand: #6366f1;
    --l-brand-strong: #7c3aed;   --d-brand-strong: #6d28d9;
    --l-brand-soft: #eef2ff;     --d-brand-soft: #1e223f;
    --l-brand-text: #4338ca;     --d-brand-text: #a5b4fc;

    /* --- mode-independent --- */
    /* How far the hero's gradient travels between --brand and --brand-strong.
       100% is the full distance between the two tokens, which on the bolder
       palettes (Plum is purple into magenta on purpose) reads as a graphic
       rather than as a panel. 0% is a flat accent. One place, so every hero
       in the app moves together and none of them can drift. */
    --hero-sweep: 38%;

    /* How much of a category's own colour its banner carries, at the leading
       and trailing edge. The gap between the two IS the gradient, so keeping
       them close is what makes it slight - the band reads as the category's
       colour with a shade across it rather than as a fade into the page. */
    --banner-tint: 50%;
    --banner-tint-end: 43%;
    /* The muted text on a tinted banner. --text-3 is mixed for the page
       surface; a band carrying half a category's colour is a different
       ground, and the grey that reads on a card disappears on it. Written as
       a share of --text so it follows light and dark without naming either. */
    --banner-muted: color-mix(in srgb, var(--text) 68%, transparent);
    /* The filled style is already the colour; its gradient only has to give
       the band a little depth, so it barely darkens. */
    --banner-bold-end: 90%;

    --radius: 6px;
    --radius-sm: 4px;
    --radius-lg: 9px;

    --nav-h: 60px;
    --header-h: 56px;
    --safe-b: env(safe-area-inset-bottom, 0px);
    --safe-t: env(safe-area-inset-top, 0px);

    --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* ==========================================================================
   Accent palettes

   Set with data-theme on <html> (More -> Appearance), remembered per device.
   No attribute = Indigo.
   ========================================================================== */

/* --- Ocean: teal into cyan. Calm, and clearly not "money green". --------- */
:root[data-theme="ocean"] {
    --l-brand: #0d9488;          --d-brand: #14b8a6;
    --l-brand-strong: #0891b2;   --d-brand-strong: #0e7490;
    --l-brand-soft: #ecfeff;     --d-brand-soft: #0c2b2c;
    --l-brand-text: #0f766e;     --d-brand-text: #5eead4;
}
/* --- Evergreen: the classic finance green. ------------------------------- */
:root[data-theme="evergreen"] {
    --l-brand: #15803d;          --d-brand: #16a34a;
    --l-brand-strong: #047857;   --d-brand-strong: #047857;
    --l-brand-soft: #ecfdf5;     --d-brand-soft: #0d2a1c;
    --l-brand-text: #166534;     --d-brand-text: #6ee7b7;
}
/* --- Midnight: deep, sober blue. ---------------------------------------- */
:root[data-theme="midnight"] {
    --l-brand: #1d4ed8;          --d-brand: #3b82f6;
    --l-brand-strong: #1e3a8a;   --d-brand-strong: #1e40af;
    --l-brand-soft: #eff6ff;     --d-brand-soft: #10203c;
    --l-brand-text: #1d4ed8;     --d-brand-text: #93c5fd;
}
/* --- Plum: purple into magenta. The boldest of the set. ------------------ */
:root[data-theme="plum"] {
    --l-brand: #9333ea;          --d-brand: #a855f7;
    --l-brand-strong: #c026d3;   --d-brand-strong: #a21caf;
    --l-brand-soft: #faf5ff;     --d-brand-soft: #2a1436;
    --l-brand-text: #7e22ce;     --d-brand-text: #d8b4fe;
}
/* --- Graphite: no accent hue at all. The dark values are lifted off the
       page background, or the hero card would vanish into it. ------------- */
:root[data-theme="graphite"] {
    --l-brand: #334155;          --d-brand: #64748b;
    --l-brand-strong: #0f172a;   --d-brand-strong: #334155;
    --l-brand-soft: #f1f5f9;     --d-brand-soft: #1e293b;
    --l-brand-text: #1e293b;     --d-brand-text: #cbd5e1;
}

/* ==========================================================================
   Which half is live

   Light is the default. Dark applies when the device asks for it, unless the
   user has forced light; forcing dark works the other way. The two dark
   blocks are deliberately identical - plain CSS has no way to share a
   declaration list between a media query and a plain selector.
   ========================================================================== */

:root,
:root[data-mode="light"] {
    color-scheme: light;

    --bg: var(--l-bg);
    --surface: var(--l-surface);
    --surface-2: var(--l-surface-2);
    --surface-3: var(--l-surface-3);
    --border: var(--l-border);
    --border-strong: var(--l-border-strong);
    --text: var(--l-text);
    --text-2: var(--l-text-2);
    --text-3: var(--l-text-3);
    --good: var(--l-good);
    --good-soft: var(--l-good-soft);
    --warn: var(--l-warn);
    --warn-soft: var(--l-warn-soft);
    --bad: var(--l-bad);
    --bad-soft: var(--l-bad-soft);
    --info: var(--l-info);
    --info-soft: var(--l-info-soft);
    --shadow-sm: var(--l-shadow-sm);
    --shadow: var(--l-shadow);
    --shadow-lg: var(--l-shadow-lg);
    --brand: var(--l-brand);
    --brand-strong: var(--l-brand-strong);
    --brand-soft: var(--l-brand-soft);
    --brand-text: var(--l-brand-text);
}

@media (prefers-color-scheme: dark) {
    :root:not([data-mode="light"]) {
        color-scheme: dark;

        --bg: var(--d-bg);
        --surface: var(--d-surface);
        --surface-2: var(--d-surface-2);
        --surface-3: var(--d-surface-3);
        --border: var(--d-border);
        --border-strong: var(--d-border-strong);
        --text: var(--d-text);
        --text-2: var(--d-text-2);
        --text-3: var(--d-text-3);
        --good: var(--d-good);
        --good-soft: var(--d-good-soft);
        --warn: var(--d-warn);
        --warn-soft: var(--d-warn-soft);
        --bad: var(--d-bad);
        --bad-soft: var(--d-bad-soft);
        --info: var(--d-info);
        --info-soft: var(--d-info-soft);
        --shadow-sm: var(--d-shadow-sm);
        --shadow: var(--d-shadow);
        --shadow-lg: var(--d-shadow-lg);
        --brand: var(--d-brand);
        --brand-strong: var(--d-brand-strong);
        --brand-soft: var(--d-brand-soft);
        --brand-text: var(--d-brand-text);
    }
}

:root[data-mode="dark"] {
    color-scheme: dark;

    --bg: var(--d-bg);
    --surface: var(--d-surface);
    --surface-2: var(--d-surface-2);
    --surface-3: var(--d-surface-3);
    --border: var(--d-border);
    --border-strong: var(--d-border-strong);
    --text: var(--d-text);
    --text-2: var(--d-text-2);
    --text-3: var(--d-text-3);
    --good: var(--d-good);
    --good-soft: var(--d-good-soft);
    --warn: var(--d-warn);
    --warn-soft: var(--d-warn-soft);
    --bad: var(--d-bad);
    --bad-soft: var(--d-bad-soft);
    --info: var(--d-info);
    --info-soft: var(--d-info-soft);
    --shadow-sm: var(--d-shadow-sm);
    --shadow: var(--d-shadow);
    --shadow-lg: var(--d-shadow-lg);
    --brand: var(--d-brand);
    --brand-strong: var(--d-brand-strong);
    --brand-soft: var(--d-brand-soft);
    --brand-text: var(--d-brand-text);
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    font-family: var(--font);
    font-size: 15px;
    line-height: 1.45;
    color: var(--text);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    overscroll-behavior-y: none;
}

h1, h2, h3, h4 { margin: 0; font-weight: 650; letter-spacing: -.01em; }
h1 { font-size: 1.35rem; }
h2 { font-size: 1.1rem; }
h3 { font-size: .98rem; }
p { margin: 0 0 .6rem; }
a { color: var(--brand-text); text-decoration: none; }
a:hover { text-decoration: underline; }
small { font-size: .8rem; }

/* --------------------------------------------------------------- boot ---- */

.app-loading, .boot {
    min-height: 100vh;
    display: grid;
    place-items: center;
    text-align: center;
    color: var(--text-2);
}
.boot-logo {
    width: 54px; height: 54px;
    margin: 0 auto .8rem;
    border-radius: 8px;
    background: var(--brand);
    color: #fff;
    font-size: 1.6rem; font-weight: 700;
    display: grid; place-items: center;
    animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
.boot-error { color: var(--bad); }

/* ------------------------------------------------------------- layout ---- */

/* #app holds the shell, the nav and the toast stack. Making it a column flex
   container lets the nav move above the content on wide screens, where a
   bottom tab bar would be the wrong idiom. */
#app { display: flex; flex-direction: column; min-height: 100vh; }

.shell { flex: 1; display: flex; flex-direction: column; }

.topbar {
    position: sticky; top: 0; z-index: 40;
    padding: calc(var(--safe-t) + 8px) 14px 8px;
    background: color-mix(in srgb, var(--surface) 88%, transparent);
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    border-bottom: 1px solid var(--border);
    display: flex; align-items: center; gap: 10px;
    min-height: var(--header-h);
}
.topbar .title { flex: 1; min-width: 0; }
.topbar .title h1 {
    font-size: 1.1rem;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.topbar .title .sub { font-size: .76rem; color: var(--text-3); }

main {
    flex: 1;
    padding: 12px 12px calc(var(--nav-h) + var(--safe-b) + 24px);
    max-width: 880px;
    width: 100%;
    margin: 0 auto;
}

.bottom-nav {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 45;
    display: grid; grid-template-columns: repeat(5, 1fr);
    padding-bottom: var(--safe-b);
    background: color-mix(in srgb, var(--surface) 92%, transparent);
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    border-top: 1px solid var(--border);
}
.bottom-nav button {
    appearance: none; border: 0; background: none;
    padding: 8px 2px 10px;
    color: var(--text-3);
    font: inherit; font-size: .68rem;
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    cursor: pointer;
    position: relative;
    min-height: var(--nav-h);
    justify-content: center;
}
.bottom-nav button svg { width: 22px; height: 22px; }
.bottom-nav button.active { color: var(--brand-text); }
.bottom-nav button .dot {
    position: absolute; top: 6px; right: calc(50% - 20px);
    min-width: 16px; height: 16px; padding: 0 4px;
    border-radius: 999px;
    background: var(--bad); color: #fff;
    font-size: .62rem; font-weight: 700;
    display: grid; place-items: center;
}
.bottom-nav .fab-slot button {
    color: #fff;
}
.bottom-nav .fab-slot button .fab {
    width: 46px; height: 46px; border-radius: 50%;
    background: var(--brand); color: #fff;
    display: grid; place-items: center;
    box-shadow: var(--shadow);
    margin-top: -14px;
}
.bottom-nav .fab-slot button span.label { color: var(--text-3); }

/*
 * Only where there is room. On a phone the nav is five slots across the foot
 * of the screen and a sixth would squeeze the labels; the item is still in the
 * markup, so nothing has to be re-rendered when the window is resized.
 */
.bottom-nav .nav-wide { display: none; }

@media (min-width: 780px) {
    :root { --nav-h: 0px; }
    .bottom-nav {
        /* Above the content, and static rather than sticky so it cannot fight
           the sticky page header for the same top edge. */
        position: static;
        order: -1;
        /* Sized by content rather than counted, so an item appearing or
           leaving does not need this number changed to match. */
        grid-auto-flow: column;
        grid-template-columns: none;
        grid-auto-columns: auto;
        justify-content: center; gap: 4px;
        border-top: 0; border-bottom: 1px solid var(--border);
        padding: 6px;
        padding-bottom: 6px;
    }
    .bottom-nav .nav-wide { display: flex; }
    .bottom-nav button { flex-direction: row; font-size: .85rem; padding: 8px 14px; border-radius: 999px; min-height: 0; }
    .bottom-nav button.active { background: var(--brand-soft); }
    .bottom-nav .fab-slot button .fab { width: 26px; height: 26px; margin: 0; box-shadow: none; }
    .bottom-nav button .dot { position: static; margin-left: 4px; }
    main { padding-bottom: 40px; }
}

/* -------------------------------------------------------------- cards ---- */

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    margin-bottom: 12px;
    overflow: hidden;
}
.card-head {
    display: flex; align-items: center; gap: 8px;
    padding: 12px 14px;
    border-bottom: 1px solid var(--border);
}
.card-head h2, .card-head h3 { flex: 1; min-width: 0; }
.card-body { padding: 14px; }
.card-body.tight { padding: 8px 14px; }
.card.flush { padding: 0; }

.section-title {
    font-size: .74rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-3);
    margin: 18px 4px 8px;
}

/* ==========================================================================
   Budget dashboard layout

   On a phone the sections stack in reading order: hero, flags, the headline
   numbers, then the categories, then the month controls. `display: contents`
   on the sidebar wrapper lets its three children take part in that single
   column so `order` can interleave them.

   From 900px the wrapper becomes a real sidebar and the categories move into
   a multi-column grid, which is what fills a desktop window properly.
   ========================================================================== */

.dash { display: flex; flex-direction: column; }
.dash-main, .dash-side { display: contents; }

.d-hero    { order: 1; }
.d-flags   { order: 2; }
.d-stats   { order: 3; }
.d-cats    { order: 4; }
.d-actions { order: 5; }
.d-jump    { order: 6; }

.d-flags:empty { display: none; }
.d-flags .chips { margin: 0 2px 12px; }
.d-stats .stat-row { margin-bottom: 14px; }
.d-jump .month-jump { margin-bottom: 10px; }

/* The group blocks sit in a grid so wide screens get two or three columns
   instead of one very long ribbon. */
.group-grid { display: grid; gap: 12px; align-items: start; }
.group-grid > .group-block { margin-bottom: 0; }

@media (min-width: 900px) {
    /* Only the dashboard widens - a transaction list reads better narrow. */
    main.wide { max-width: 1240px; }

    main.wide .dash {
        display: grid;
        grid-template-columns: minmax(0, 1fr) 300px;
        column-gap: 18px;
        align-items: start;
    }
    /* Two items, one row. The sidebar is content-height thanks to
       align-items: start, which is what gives `sticky` room to travel. */
    main.wide .dash-main { display: block; min-width: 0; }
    main.wide .dash-side {
        display: block;
        position: sticky;
        top: calc(var(--header-h) + 14px);
    }

    /* The sidebar is one column wide, so the stat tiles stack in it. */
    main.wide .d-stats .stat-row { grid-template-columns: 1fr; margin-bottom: 14px; }
    main.wide .d-actions .btn-row .btn { width: 100%; }
    main.wide .d-jump .month-jump { flex-wrap: wrap; overflow: visible; }
    main.wide .d-cats .section-title { margin-top: 4px; }
}

@media (min-width: 1180px) {
    /* Multi-column rather than a 2-track grid: category blocks differ a lot in
       height (Income has 3 items, Other has 8), and grid rows are as tall as
       their tallest cell, which leaves ragged gaps. Columns pack vertically and
       balance themselves, so the blocks flow down one column and into the next
       with no holes. */
    main.wide .group-grid {
        display: block;
        columns: 2;
        column-gap: 14px;
        orphans: 1;
        widows: 1;
    }
    main.wide .group-grid > .group-block {
        break-inside: avoid;
        -webkit-column-break-inside: avoid;
        margin-bottom: 14px;
    }
}

@media (min-width: 1560px) {
    main.wide { max-width: 1600px; }
    main.wide .group-grid { columns: 3; }
}

/* ------------------------------------------------------ summary blocks --- */

.hero {
    position: relative;
    /* A faint dark wash over the accent gradient, so white text stays legible
       whichever palette is chosen - the lighter accents (Ocean, Plum) would
       otherwise be close to the text.

       The sweep stops short of --brand-strong rather than reaching it: the
       panel still has depth, but it is one colour with a shade on it instead
       of two colours meeting. --hero-sweep is the dial. */
    /* The plain accent first, so a browser without color-mix() drops only the
       layered declaration below and still has a background. White text on a
       dropped background is invisible text. */
    background: var(--brand);
    background:
        radial-gradient(120% 140% at 100% 0%, rgba(255, 255, 255, .08) 0%, rgba(255, 255, 255, 0) 55%),
        linear-gradient(180deg, rgba(0, 0, 0, .08) 0%, rgba(0, 0, 0, .02) 100%),
        linear-gradient(150deg,
            var(--brand) 0%,
            color-mix(in srgb, var(--brand-strong) var(--hero-sweep), var(--brand)) 100%);
    color: #fff;
    border: 0;
    border-radius: var(--radius-lg);
    padding: 16px;
    box-shadow: var(--shadow);
    overflow: hidden;
}
/* A hairline of light along the top edge: enough to lift the panel off the
   page without a border that would fight the rounded corner. */
.hero::before {
    content: '';
    position: absolute; left: 0; right: 0; top: 0; height: 1px;
    background: rgba(255, 255, 255, .28);
}

/* Over budget is a state, not a minus sign. The accent stays underneath so
   the app still looks like itself, with red laid over it - which also keeps
   this working across all six palettes without naming any of them. */
.hero.over {
    background:
        radial-gradient(120% 140% at 100% 0%, rgba(255, 255, 255, .07) 0%, rgba(255, 255, 255, 0) 55%),
        linear-gradient(150deg, rgba(190, 18, 60, .62) 0%, rgba(150, 14, 48, .72) 100%),
        linear-gradient(150deg,
            var(--brand) 0%,
            color-mix(in srgb, var(--brand-strong) var(--hero-sweep), var(--brand)) 100%);
}

.hero .label {
    font-size: .72rem; opacity: .92;
    text-transform: uppercase; letter-spacing: .07em; font-weight: 600;
}
.hero .big {
    font-size: 2.15rem; font-weight: 720; letter-spacing: -.025em;
    margin: 1px 0 3px;
    font-variant-numeric: tabular-nums;
    /* Keeps the figure crisp against the gradient behind it. */
    text-shadow: 0 1px 2px rgba(0, 0, 0, .16);
}
.hero .meta { font-size: .8rem; opacity: .9; font-variant-numeric: tabular-nums; }
/* The percentage carries the headline reading, so it is not just more prose. */
.hero .meta b { font-weight: 700; opacity: 1; }

/* ---- how much of the budget is gone -------------------------------------
   Under budget the fill is the percentage. Over budget the track is the whole
   budget and `spill` is the overshoot, so 140% reads as a bar that has run
   past its end rather than one sitting innocently at full.
   ------------------------------------------------------------------------ */
.hero-bar {
    position: relative;
    display: flex; align-items: center;
    height: 8px; margin: 13px 0 2px;
    background: rgba(0, 0, 0, .22);
    border-radius: 999px;
    overflow: hidden;
}
.hero-bar .fill,
.hero-bar .spill {
    flex: none;                 /* honour the computed width exactly */
    display: block; height: 100%;
    transition: width .45s cubic-bezier(.4, 0, .2, 1);
}
.hero-bar .fill { background: rgba(255, 255, 255, .92); }
/* Hard edge between the two, so the boundary reads as "budget ends here". */
.hero-bar .spill {
    background: repeating-linear-gradient(135deg,
        var(--bad) 0 5px, rgba(255, 255, 255, .5) 5px 7px);
}
/* A notch where the budget ends, so the boundary is readable even when the
   overshoot is small enough that the two segments look continuous. */
.hero-bar .spill {
    border-left: 2px solid rgba(255, 255, 255, .85);
}

/* ---- the ring, and the headline beside it -------------------------------
   The chart goes first in the source as well as on screen, so a screen
   reader hears the shape of the month before the summary of it, and so the
   two never disagree about which side they are on.
   ------------------------------------------------------------------------ */
.hero-top {
    display: flex; align-items: center; gap: 15px;
}
.hero-main { flex: 1 1 auto; min-width: 0; }

.hero-chart {
    flex: 0 0 auto;
    width: 108px;
    /* The bar under the headline already reaches the bottom of the text
       block; nudging the ring up keeps the two centred on each other. */
    margin: 2px 0 4px;
}
.donut { display: block; width: 100%; height: auto; overflow: visible; }

/* One shared stroke width, so a slice cannot look thicker than its
   neighbour, and butt caps so touching arcs meet without overlapping. */
.donut circle {
    fill: none;
    stroke-width: 5.4;
    stroke-linecap: butt;
}
/* The same wash the bar sits on, so an unplanned month still reads as a
   ring rather than as nothing at all. */
.donut .dtrack { stroke: rgba(0, 0, 0, .22); }

.donut .dpct,
.donut .dsub {
    fill: #fff;
    text-anchor: middle;
    font-variant-numeric: tabular-nums;
}
.donut .dpct { font-size: 7.4px; font-weight: 700; letter-spacing: -.03em; }
.donut .dsub { font-size: 3.5px; opacity: .72; text-transform: uppercase; letter-spacing: .08em; }

.dslice { cursor: pointer; }
.dslice circle { transition: opacity .18s ease; }
/* Hovering one slice dims the others rather than growing the one under the
   pointer: a ring that changes size while you read it is hard to compare. */
.donut:hover .dslice { opacity: .45; }
.donut:hover .dslice:hover { opacity: 1; }

@media (max-width: 400px) {
    .hero-top { gap: 12px; }
    .hero-chart { width: 88px; }
}

/* Where a tapped slice lands. Brief, and on the card rather than the band,
   so it reads as "this one" without recolouring the category. */
.group-block.flash {
    animation: flash-block 1.2s ease-out;
}
@keyframes flash-block {
    0%, 40% { box-shadow: 0 0 0 2px var(--brand); }
    100% { box-shadow: 0 0 0 2px transparent; }
}

.hero-grid {
    display: grid; grid-template-columns: repeat(3, 1fr);
    gap: 8px; margin-top: 12px;
}
.hero-stat {
    background: rgba(255, 255, 255, .14);
    border: 1px solid rgba(255, 255, 255, .1);
    border-radius: var(--radius-sm);
    padding: 8px 9px;
    min-width: 0;
}
/* The net tile is the one whose sign changes meaning, so it gets a tint as
   well as a minus. Kept low-saturation: it sits on a coloured ground, and
   full-strength green or red on top of the accent is a mess. */
.hero-stat.pos { background: rgba(74, 222, 128, .2); border-color: rgba(134, 239, 172, .34); }
.hero-stat.neg { background: rgba(248, 113, 113, .24); border-color: rgba(252, 165, 165, .4); }

.hero-stat .k {
    font-size: .66rem; opacity: .85;
    text-transform: uppercase; letter-spacing: .04em;
}
.hero-stat .v {
    font-size: 1.02rem; font-weight: 680; letter-spacing: -.01em;
    font-variant-numeric: tabular-nums;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.hero-stat .n {
    font-size: .64rem; opacity: .72;
    font-variant-numeric: tabular-nums;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.stat-row {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 10px;
}
.stat {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 11px 12px;
    box-shadow: var(--shadow-sm);
}
.stat .k { font-size: .72rem; color: var(--text-3); text-transform: uppercase; letter-spacing: .05em; }
.stat .v { font-size: 1.15rem; font-weight: 680; letter-spacing: -.01em; }
.stat .n { font-size: .74rem; color: var(--text-3); }

/* ---------------------------------------------------- budget item rows --- */

.group-block { margin-bottom: 12px; }

/* ---- category heading banner -------------------------------------------
   Three styles, chosen under Appearance and set as data-banner on <html>.
   All three carry the same reading - the category colour, the money left,
   and a bar along the bottom edge for how far through the budget it is - so
   switching changes the volume, not the information.

   The colour arrives as --gc (and --gc-wash, a pre-mixed fallback for
   browsers without color-mix) on the banner itself, set inline per category.
   ------------------------------------------------------------------------ */

.group-block > .group-head {
    position: relative;
    display: flex; align-items: center; gap: 10px;
    padding: 11px 13px 13px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius) var(--radius) 0 0;
    cursor: pointer;
    user-select: none;
    /* Keeps the spend bar inside the rounded top corners. */
    overflow: hidden;
    transition: background .18s ease, border-color .18s ease;
}
.group-block.collapsed > .group-head { border-radius: var(--radius); }

.group-block > .group-head .gname {
    flex: 1; min-width: 0;
    font-weight: 650; font-size: .95rem; line-height: 1.25;
}
/* The item count: context that costs no vertical space, since the figure on
   the right already makes the banner two lines tall. */
.group-block > .group-head .gname em {
    display: block;
    font-style: normal; font-weight: 500;
    font-size: .7rem; letter-spacing: .02em;
    color: var(--text-3);
}
.group-block > .group-head .gnums {
    text-align: right; line-height: 1.25;
}
.group-block > .group-head .gnums b {
    display: block;
    font-size: .98rem; font-weight: 680; letter-spacing: -.01em;
    font-variant-numeric: tabular-nums;
}
.group-block > .group-head .gnums b i {
    font-style: normal; font-weight: 500;
    font-size: .72rem; color: var(--text-3);
}
.group-block > .group-head .gnums small {
    display: block;
    font-size: .72rem; color: var(--text-3);
    font-variant-numeric: tabular-nums;
}
/* Readable against the tint, which --text-3 is not once the band carries real
   colour. Scoped to the tinted style so the other two keep their own answer.
   Measured, not guessed: at --banner-tint: 50% this takes the worst category
   from 1.45 to above 4.5 in both modes. */
:root[data-banner="tinted"] .group-block > .group-head .gname em,
:root[data-banner="tinted"] .group-block > .group-head .gnums small,
:root[data-banner="tinted"] .group-block > .group-head .gnums b i {
    color: var(--text-3);
    color: var(--banner-muted);
}

.group-block > .group-head .chev {
    flex: none;
    color: var(--text-3); transition: transform .18s ease;
}
.group-block.collapsed > .group-head .chev { transform: rotate(-90deg); }

/* How much of the category has been used, on the bottom edge. */
.group-head .gbar {
    position: absolute; left: 0; right: 0; bottom: 0;
    height: 3px;
    background: var(--border);
}
.group-head .gbar::before {
    content: ''; position: absolute; top: 0; bottom: 0; left: 0;
    width: var(--pct, 0%);
    background: var(--gc, var(--brand));
    transition: width .3s ease;
}
/* Status wins over the category colour: over-budget is always red, and
   exactly-on-budget always green, whatever the category is tinted. */
.group-head.over .gbar::before    { background: var(--bad); }
.group-head.settled .gbar::before { background: var(--good); }
.group-head.over .gnums b         { color: var(--bad); }

/* The category badge: an auto-matched icon stroked in the category colour on
   a wash of the same colour, so the colour coding survives while the shape
   carries the meaning. */
.gicon {
    flex: none;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 5px;
    color: var(--gc, var(--brand));
    background: var(--gc-wash, transparent);
    background: color-mix(in srgb, var(--gc, #6366f1) 16%, transparent);
}
.group-block > .group-head .gicon { width: 30px; height: 30px; }
.item-summary .head .gicon { width: 24px; height: 24px; }
.gi-row { display: flex; align-items: center; gap: 8px; }
.gi-row .gicon { width: 24px; height: 24px; }

/* A row that is in the list but not in the total: present, and visibly
   not part of the sum above it. */
.txn.not-counted .desc,
.txn.not-counted .amt { opacity: .55; }
.txn.not-counted .amt { text-decoration: line-through; }

/* ---- style 1: tinted (the default) ----
   The band is the category's own colour, lightened into the surface, with a
   slight shade across it. It used to fade to bare surface by the right-hand
   edge, which left most of the banner uncoloured and the category readable
   only from the badge; now the colour carries the whole width and the
   gradient is just enough to stop the band looking like a flat swatch.

   Still a wash, not a fill - the numbers on it are the thing being read, and
   --banner-tint is the dial if it wants to be quieter or stronger. */
:root[data-banner="tinted"] .group-block > .group-head {
    /* Plain first, so a browser without color-mix() still gets the surface
       rather than losing the background declaration altogether. */
    background: var(--gc-wash, var(--surface));
    background: linear-gradient(100deg,
        color-mix(in srgb, var(--gc, #6366f1) var(--banner-tint), var(--surface)) 0%,
        color-mix(in srgb, var(--gc, #6366f1) var(--banner-tint-end), var(--surface)) 100%);
    border-color: var(--border);
    border-color: color-mix(in srgb, var(--gc, #6366f1) 28%, var(--border));
}

/* ---- style 2: accent ----
   The quietest: the surface left alone, with the colour as an edge. */
:root[data-banner="accent"] .group-block > .group-head {
    padding-left: 11px;
    border-left: 3px solid var(--gc, var(--brand));
}

/* ---- style 3: bold ----
   The band filled with the category colour. Everything on it goes white,
   including the badge, which is why the badge takes its colour from the
   stylesheet rather than an inline declaration. */
:root[data-banner="bold"] .group-block > .group-head {
    background: var(--gc, var(--brand));
    background: linear-gradient(100deg,
        var(--gc, #6366f1) 0%,
        color-mix(in srgb, var(--gc, #6366f1) var(--banner-bold-end), #000) 100%);
    border-color: transparent;
}
:root[data-banner="bold"] .group-block > .group-head,
:root[data-banner="bold"] .group-block > .group-head .gnums b,
:root[data-banner="bold"] .group-block > .group-head .gname { color: #fff; }
:root[data-banner="bold"] .group-block > .group-head .gname em,
:root[data-banner="bold"] .group-block > .group-head .gnums small,
:root[data-banner="bold"] .group-block > .group-head .gnums b i,
:root[data-banner="bold"] .group-block > .group-head .chev { color: rgba(255, 255, 255, .78); }
:root[data-banner="bold"] .group-block > .group-head .gicon {
    color: #fff;
    background: rgba(255, 255, 255, .22);
}
:root[data-banner="bold"] .group-head .gbar { background: rgba(0, 0, 0, .22); }
:root[data-banner="bold"] .group-head .gbar::before { background: rgba(255, 255, 255, .9); }
/* Over-budget still has to shout louder than the category colour. */
:root[data-banner="bold"] .group-head.over .gnums b { color: #fff; }
:root[data-banner="bold"] .group-head.over .gbar::before { background: var(--bad); }

/* A miniature of each banner style for the picker, so the choice is made by
   looking rather than by reading three descriptions. --gc is the app accent
   here, since the preview belongs to no particular category. */
.banner-demo {
    --gc: var(--brand);
    position: relative; flex: none;
    width: 46px; height: 30px;
    border-radius: 5px; overflow: hidden;
    border: 1px solid var(--border);
    background: var(--surface);
}
.banner-demo i {
    position: absolute; left: 5px; top: 6px;
    width: 9px; height: 9px; border-radius: 2px;
    background: var(--gc);
}
/* Stands in for the row of text. */
.banner-demo > span {
    position: absolute; left: 18px; top: 9px; right: 6px; height: 3px;
    border-radius: 2px; background: var(--border-strong, var(--border));
}
.banner-demo::after {
    content: ''; position: absolute; left: 0; bottom: 0; height: 3px;
    width: 62%; background: var(--gc);
}
/* The previews use the same dials as the real thing, so the choice made by
   looking is the choice that gets applied. */
.banner-demo-tinted {
    background: linear-gradient(100deg,
        color-mix(in srgb, var(--gc) var(--banner-tint), var(--surface)) 0%,
        color-mix(in srgb, var(--gc) var(--banner-tint-end), var(--surface)) 100%);
}
.banner-demo-accent { border-left: 3px solid var(--gc); }
.banner-demo-bold {
    background: linear-gradient(100deg, var(--gc),
        color-mix(in srgb, var(--gc) var(--banner-bold-end), #000));
    border-color: transparent;
}
.banner-demo-bold i { background: rgba(255, 255, 255, .85); }
.banner-demo-bold > span { background: rgba(255, 255, 255, .55); }
.banner-demo-bold::after { background: rgba(255, 255, 255, .9); }

/* ---- the planned amount, edited in place ---------------------------------
   A button that looks like the text around it until you go near it: the row
   is dense and mostly read, so an always-visible control on every line would
   be noise. The dotted underline is the only standing hint that it is live.
   ------------------------------------------------------------------------ */
.planned {
    appearance: none;
    background: none;
    border: 0;
    padding: 0 1px;
    margin: 0;
    font: inherit;
    color: inherit;
    font-variant-numeric: tabular-nums;
    cursor: text;
    border-radius: 3px;
    border-bottom: 1px dotted var(--text-3);
}
.planned:hover,
.planned:focus-visible {
    background: var(--surface-2);
    border-bottom-color: var(--brand);
    color: var(--text);
    outline: none;
}
/* A hit area that a thumb can actually find, without changing the layout. */
@media (hover: none) {
    .planned { padding: 3px 5px; margin: -3px -4px; }
}

.planned-input {
    /* Sized to the text it replaces, so the line does not jump. */
    width: 8.5em;
    padding: 1px 5px;
    font: inherit;
    font-variant-numeric: tabular-nums;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--brand);
    border-radius: 4px;
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 18%, transparent);
}
.planned-input:disabled { opacity: .6; }

.item-list {
    border: 1px solid var(--border);
    border-top: 0;
    border-radius: 0 0 var(--radius) var(--radius);
    background: var(--surface);
    overflow: hidden;
}
.group-block.collapsed .item-list { display: none; }

.item {
    padding: 10px 13px;
    border-top: 1px solid var(--border);
    cursor: pointer;
}
.item:first-child { border-top: 0; }
.item:active { background: var(--surface-2); }
.item .line1 { display: flex; align-items: baseline; gap: 8px; }
.item .iname { flex: 1; font-weight: 550; min-width: 0; }
.item .iname .cf {
    font-size: .62rem; font-weight: 700; color: var(--info);
    background: var(--info-soft); border-radius: 3px; padding: 1px 4px;
    margin-left: 5px; vertical-align: 1px;
}
.item .iremain { font-weight: 650; font-variant-numeric: tabular-nums; white-space: nowrap; }
.item .line2 {
    display: flex; gap: 8px; align-items: center;
    font-size: .76rem; color: var(--text-3);
    margin-top: 2px;
}
.item .line2 .spread { flex: 1; }

.bar {
    height: 5px; border-radius: 999px;
    background: var(--surface-3);
    overflow: hidden;
    margin-top: 7px;
}
.bar > i {
    display: block; height: 100%; border-radius: 999px;
    background: var(--brand);
    transition: width .3s ease;
}
.bar.ok > i { background: var(--good); }
.bar.warn > i { background: var(--warn); }
.bar.over > i { background: var(--bad); }
/* Exactly on budget: a full green bar, distinct from the amber "almost out"
   band that 100% would otherwise land in. */
.bar.done > i { background: var(--good); width: 100% !important; }
.bar.done { background: var(--good-soft); }

/* The tick against an item that has been spent exactly to budget. */
.settled-tick {
    display: inline-grid;
    place-items: center;
    width: 15px; height: 15px;
    margin-left: 6px;
    vertical-align: 1px;
    border-radius: 3px;
    background: var(--good);
    color: #fff;
    flex: none;
}
.settled-tick svg { width: 11px; height: 11px; stroke-width: 3; }

.amount-settled { color: var(--good); }

.amount-pos { color: var(--good); }
.amount-neg { color: var(--bad); }
.muted { color: var(--text-3); }
.tabular { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ buttons ---- */

.btn {
    appearance: none;
    font: inherit; font-weight: 550; font-size: .9rem;
    padding: 9px 14px;
    min-height: 40px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border-strong);
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    transition: filter .12s ease, background .12s ease;
    -webkit-tap-highlight-color: transparent;
}
.btn:hover { filter: brightness(.97); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn.primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn.danger { background: var(--bad); border-color: var(--bad); color: #fff; }
.btn.ghost { background: transparent; border-color: transparent; color: var(--brand-text); }
.btn.subtle { background: var(--surface-3); border-color: transparent; }
.btn.sm { min-height: 32px; padding: 5px 10px; font-size: .82rem; }
.btn.block { width: 100%; }
.btn.icon { padding: 8px; min-width: 40px; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-row.end { justify-content: flex-end; }

/* -------------------------------------------------------------- forms ---- */

.field { margin-bottom: 12px; }
.field > label {
    display: block;
    font-size: .78rem; font-weight: 600;
    color: var(--text-2);
    margin-bottom: 5px;
}
/* Not scoped to .field - both are also used on their own, under a form or
   next to a button. */
.hint { font-size: .74rem; color: var(--text-3); margin-top: 4px; line-height: 1.35; }
.err { font-size: .76rem; color: var(--bad); margin-top: 4px; }

/* `input` with no type attribute is still a text field, and an attribute
   selector like input[type="text"] would not match it - hence the :not() list
   rather than an allow-list of types. */
input:not([type]),
input[type="text"], input[type="email"], input[type="password"], input[type="number"],
input[type="date"], input[type="month"], input[type="search"], input[type="tel"],
input[type="url"], input[type="datetime-local"],
select, textarea {
    width: 100%;
    font: inherit;
    /* 16px stops iOS Safari zooming when a field takes focus. */
    font-size: 16px;
    padding: 10px 11px;
    min-height: 44px;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    appearance: none;
}
textarea { min-height: 72px; resize: vertical; }
select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='%2378859a' d='M6 8.5 2 4.5h8z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 12px;
    padding-right: 30px;
}
input:focus, select:focus, textarea:focus {
    outline: 2px solid color-mix(in srgb, var(--brand) 45%, transparent);
    outline-offset: 1px;
    border-color: var(--brand);
}
input[type="checkbox"], input[type="radio"] {
    width: 20px; height: 20px; min-height: 0; accent-color: var(--brand);
}

/* File pickers: the native button never matches the theme, so it gets the
   same treatment as .btn.subtle. */
input[type="file"] {
    width: 100%;
    font: inherit; font-size: .88rem;
    color: var(--text-2);
    background: var(--surface);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 10px 11px;
}
input[type="file"]::file-selector-button {
    font: inherit; font-size: .85rem; font-weight: 550;
    color: var(--text);
    background: var(--surface-3);
    border: 1px solid var(--border-strong);
    border-radius: 4px;
    padding: 7px 11px;
    margin-right: 10px;
    cursor: pointer;
}
input[type="file"]::file-selector-button:hover { filter: brightness(1.08); }

.row { display: grid; gap: 10px; grid-template-columns: 1fr 1fr; }
.row.three { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 520px) { .row.three { grid-template-columns: 1fr 1fr; } }

.check {
    display: flex; align-items: center; gap: 9px;
    padding: 9px 0;
    font-size: .9rem;
    cursor: pointer;
}
.check input { flex: none; }
/* A checkbox that carries an explanation: line the tick up with the first line
   of text instead of floating it in the middle of the block. */
.check:has(.hint) { align-items: flex-start; }
.check:has(.hint) input { margin-top: 3px; }
.check .hint { display: block; font-style: normal; }

/* A disabled field is showing a value the app is maintaining, not inviting an
   edit - it should read as information. */
input:disabled, select:disabled, textarea:disabled {
    background: var(--surface-2); color: var(--text-2); cursor: not-allowed;
}
.money-input:has(input:disabled) .cur { opacity: .6; }

.money-input { position: relative; }
.money-input .cur {
    position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
    color: var(--text-3); font-size: .9rem; pointer-events: none;
}
.money-input input { padding-left: 38px; font-variant-numeric: tabular-nums; }
.money-input.big input { font-size: 26px; font-weight: 650; min-height: 62px; padding-left: 46px; }
.money-input.big .cur { font-size: 1.1rem; left: 14px; }

/* -------------------------------------------------------- net worth --- */

/* Fixed height, full width: the shape of the line is the message, and the
   viewBox is stretched to fit rather than kept square. */
/*
 * Scales uniformly now - the height comes from the aspect ratio rather than
 * being pinned - because the canvas carries text and dots, and a stretched
 * viewBox smears both.
 */
/* The quick ranges, above the canvas. */
.nw-ranges {
    display: flex; gap: 4px; flex-wrap: wrap;
    margin: 0 0 10px;
}
.nw-range {
    flex: 1 1 auto; min-width: 0;
    padding: 5px 4px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-3);
    font: inherit; font-size: .74rem; font-weight: 600;
    cursor: pointer;
    text-align: center;
}
.nw-range:hover { color: var(--text); }
.nw-range.on {
    background: var(--brand-soft);
    border-color: var(--brand);
    color: var(--brand-text);
}
/*
 * A range longer than the record. Still pressable - it simply shows
 * everything - but dimmed, so it reads as "there is no more" rather than as a
 * button that did nothing.
 */
.nw-range[data-over] { opacity: .45; }
.nw-range[data-over].on { opacity: 1; }

/*
 * The fold that hides a category's closed accounts. Quiet by design - it is a
 * footnote to the group above it, not a heading of its own.
 */
.nw-closed { margin-top: 6px; }
.nw-closed > summary {
    cursor: pointer;
    list-style: none;
    padding: 7px 2px;
    font-size: .78rem;
    color: var(--text-3);
    display: flex; align-items: center; gap: 6px;
}
.nw-closed > summary::-webkit-details-marker { display: none; }
.nw-closed > summary::before {
    content: "";
    width: 0; height: 0; flex: none;
    border-left: 4px solid currentColor;
    border-top: 3.5px solid transparent;
    border-bottom: 3.5px solid transparent;
    transition: transform .15s ease;
}
.nw-closed[open] > summary::before { transform: rotate(90deg); }
.nw-closed > summary:hover { color: var(--text-2); }
.nw-closed .card { margin-top: 2px; }
/* Closed means gone: the rows read as history rather than as holdings. */
.nw-closed .txn .desc { color: var(--text-2); }

/* The uploaded picture, beside the merchant it belongs to. */
/*
 * A part-fill, tucked under the full tank whose window counts it. The rule is
 * the join: it reads as "this belongs to the row above" rather than as a row
 * that has simply been nudged.
 */
.txn.fill-part {
    padding-left: 26px;
    position: relative;
}
.txn.fill-part::before {
    content: "";
    position: absolute;
    left: 13px; top: 0; bottom: 0;
    border-left: 2px solid var(--border-strong);
    opacity: .55;
}
/* The last of a run closes the rule off rather than letting it run into the
   full fill of the next window down. */
.txn.fill-part:not(:has(+ .txn.fill-part))::before { bottom: 50%; }
.txn.fill-part .desc { font-weight: 500; }

/*
 * The car on its own panel.
 *
 * Absolute, top right, so it never pushes the figure the panel exists for.
 * The hero is a gradient, so the mark carries a soft white ring rather than a
 * border colour - a border token would disappear into one end of the gradient
 * and shout at the other.
 */
.veh-mark {
    position: absolute; top: 14px; right: 14px;
    width: 46px; height: 46px;
    border-radius: 12px;
    overflow: hidden;
    display: grid; place-items: center;
    background: rgba(255, 255, 255, .14);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .28), var(--shadow-sm);
    backdrop-filter: blur(2px);
}

/*
 * On the consumption panel the car is a column of its own rather than a badge
 * in the corner: the figure and its explanation to the left, the picture to
 * the right, both ending where the tiles begin.
 *
 * `align-self: stretch` is what makes it full height - the row's height comes
 * from the text beside it, so the picture follows whatever that turns out to
 * be rather than guessing at a number that only holds for one line of "meta".
 */
.hero-top.fh-top { align-items: stretch; }
.veh-mark.fh-pic {
    position: static;
    flex: none;
    align-self: stretch;
    /* Wide enough to read a car in, never so wide that the figure wraps. */
    width: clamp(96px, 32%, 170px);
    height: auto;
    min-height: 88px;
    border-radius: 14px;
    gap: 6px;
}
.veh-mark.fh-pic .veh-name {
    font-size: .72rem; font-weight: 600;
    opacity: .85; text-align: center;
    padding: 0 8px; line-height: 1.25;
    overflow-wrap: anywhere;
}
.veh-mark img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The same mark before there is a figure to put it beside. On a plain
   card rather than the gradient, so it takes the card colours. */
.veh-empty { position: relative; padding-right: 72px; }
.veh-empty .veh-mark {
    background: var(--surface-2); color: var(--text-3);
    box-shadow: 0 0 0 1px var(--border);
}
/* No picture: the glyph, quieter than a photograph would be. */
.veh-mark.empty { color: rgba(255, 255, 255, .72); }

/* The same mark, big, in the sheet that uploads it. */
.veh-mark.lg {
    position: static;
    width: 76px; height: 76px; border-radius: 16px;
    background: var(--surface-2);
    box-shadow: 0 0 0 1px var(--border);
    color: var(--text-3);
}
.veh-pick { display: flex; align-items: center; gap: 14px; margin: 8px 0 4px; }
.veh-pick-actions { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0; }

/* On a tab, and beside the name in the list. */
.veh-chip img {
    width: 16px; height: 16px; border-radius: 4px;
    object-fit: cover; vertical-align: -3px; margin-right: 6px;
}
.veh-chip-mark {
    display: inline-grid; place-items: center;
    width: 20px; height: 20px; margin-right: 8px; vertical-align: -5px;
    border-radius: 5px; overflow: hidden;
    background: var(--surface-2); color: var(--text-3);
}
.veh-chip-mark img { width: 100%; height: 100%; object-fit: cover; }

/* The receipt on the edit sheet: the photo, and what can be done to it. */
.rc-pick { display: flex; align-items: center; gap: 12px; margin: 6px 0 4px; }
.rc-shot {
    display: block; flex: none;
    width: 84px; height: 84px;
    border-radius: var(--radius-sm); overflow: hidden;
    background: var(--surface-2);
    box-shadow: 0 0 0 1px var(--border);
}
.rc-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A PDF has no thumbnail worth drawing at this size, so it says what it is. */
.rc-shot.is-pdf {
    display: grid; place-items: center; gap: 2px;
    color: var(--text-2); text-decoration: none;
}
.rc-shot.is-pdf span { font-size: .66rem; font-weight: 700; letter-spacing: .06em; }
.rc-actions { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0; }
/* No receipt yet: the same footprint, so attaching one does not reflow
   the sheet under the pointer. */
.rc-empty {
    display: flex; align-items: center; gap: 8px; flex: 1;
    color: var(--text-3); font-size: .82rem;
}

.lg-chip {
    display: inline-grid; place-items: center;
    width: 22px; height: 22px; margin-right: 8px;
    vertical-align: -6px;
    border-radius: 5px; overflow: hidden;
    background: var(--surface-2); flex: none;
}
.lg-chip img { width: 100%; height: 100%; object-fit: contain; }

.nw-wrap { position: relative; }
/* The account chart sits at the top of a sheet, above its own readings. */
.nw-wrap-sm { margin: 0 0 14px; }
.nw-chart { display: block; width: 100%; height: auto; touch-action: pan-y; }

/*
 * One rule for every line on the chart, and a colour per series.
 *
 * The total is the brand colour and twice the weight of the rest, because the
 * four category lines are there to explain it rather than to compete with it.
 */
.nw-s {
    fill: none;
    stroke-width: 1.4;
    stroke-linejoin: round;
    stroke-linecap: round;
    opacity: .85;
    vector-effect: non-scaling-stroke;
}
.nw-s-total      { stroke: var(--brand); stroke-width: 2; opacity: 1; }
.nw-s-current    { stroke: var(--info); }
.nw-s-investment { stroke: var(--good); }
.nw-s-asset      { stroke: var(--warn); }
.nw-s-liability  { stroke: var(--bad); }

/* On an account's own chart, the days somebody actually looked. */
.nw-read-dot { fill: var(--brand); stroke: var(--surface); stroke-width: 1.5; }

/* Faint enough to read the line through, present enough to read a value off. */
.nw-grid { stroke: var(--border); stroke-width: 1; vector-effect: non-scaling-stroke; }
.nw-grid[data-zero] { stroke: var(--border-strong); stroke-dasharray: 3 3; }
.nw-tick { fill: var(--text-3); font-size: 11px; font-family: inherit; }

/* The crosshair. Nothing here takes pointer events - the SVG owns them. */
.nw-cross line {
    stroke: var(--text-3); stroke-width: 1; stroke-dasharray: 3 3;
    vector-effect: non-scaling-stroke;
}
.nw-cross circle { fill: var(--brand); stroke: var(--surface); stroke-width: 2; }
.nw-cross { pointer-events: none; }
.nw-cross[hidden] { display: none; }

/* Alternating calendar years behind the line, so "roughly where is 2019"
   is answered by counting stripes rather than by measuring against the few
   dates the axis has room to print.

   currentColor at a low alpha rather than a grey of its own: --text is near
   black in light mode and near white in dark, so one rule gives a band that
   is darker than the card in one and lighter in the other. A fixed light grey
   would vanish on the dark theme. */
.nw-year {
    fill: currentColor;
    opacity: .045;
    pointer-events: none;
}

/* A milestone, hanging from the top down to the line it marks. */
.nw-ev { cursor: pointer; }
.nw-ev line {
    stroke: var(--text-3); stroke-width: 1; stroke-dasharray: 2 3; opacity: .8;
    vector-effect: non-scaling-stroke;
}
.nw-ev circle { fill: var(--surface); stroke: var(--text-2); stroke-width: 2; }
.nw-ev:hover circle { r: 4.5; }

/*
 * The milestone's name, on the canvas beside its mark.
 *
 * paint-order draws the stroke first, so the surface-coloured outline sits
 * *behind* the glyphs rather than through them - that is what keeps the words
 * readable where they cross the line or the area fill underneath it.
 */
.nw-ev-label {
    fill: var(--text-2);
    font-size: 10px;
    font-family: inherit;
    font-weight: 600;
    paint-order: stroke;
    stroke: var(--surface);
    stroke-width: 3px;
    stroke-linejoin: round;
}
.nw-ev:hover .nw-ev-label { fill: var(--text); }

/* The hover readout: HTML over the canvas, so it can wrap and use app type. */
.nw-read {
    position: absolute; top: 6px;
    pointer-events: none;
    background: var(--surface-2);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    padding: 7px 9px;
    font-size: .76rem;
    line-height: 1.4;
    box-shadow: var(--shadow-sm);
    display: grid; gap: 1px;
    white-space: nowrap;
    z-index: 2;
}
.nw-read[hidden] { display: none; }
.nw-read strong { font-size: .92rem; font-variant-numeric: tabular-nums; }
.nw-read span { color: var(--text-3); }
.nw-read .ev { color: var(--text); font-weight: 600; }

/* One category per row: a swatch, its name, and its figure right-aligned. */
.nw-read .kv {
    display: grid; grid-template-columns: 8px 1fr auto;
    align-items: center; gap: 6px;
    color: var(--text-2);
}
.nw-read .kv b { width: 8px; height: 2px; border-radius: 1px; background: currentColor; }
.nw-read .kv b.nw-s-current    { background: var(--info); }
.nw-read .kv b.nw-s-investment { background: var(--good); }
.nw-read .kv b.nw-s-asset      { background: var(--warn); }
.nw-read .kv b.nw-s-liability  { background: var(--bad); }
.nw-read .kv em {
    font-style: normal; font-variant-numeric: tabular-nums;
    color: var(--text); padding-left: 10px;
}

/* The key under the chart: which lines are drawn, and what the dots mean. */
.nw-legend {
    display: flex; flex-wrap: wrap; gap: 6px 12px;
    margin-top: 10px;
    font-size: .74rem; color: var(--text-3);
}

.nw-key {
    display: inline-flex; align-items: center; gap: 6px;
    cursor: pointer;
    background: none; border: 0; padding: 2px 0;
    font: inherit; color: inherit;
}
.nw-key i {
    width: 12px; height: 3px; border-radius: 2px;
    background: currentColor; flex: none;
}
.nw-key.nw-s-total i      { background: var(--brand); }
.nw-key.nw-s-current i    { background: var(--info); }
.nw-key.nw-s-investment i { background: var(--good); }
.nw-key.nw-s-asset i      { background: var(--warn); }
.nw-key.nw-s-liability i  { background: var(--bad); }


.nw-key:hover { color: var(--text); }
/* A line that is switched off: greyed, and the swatch hollowed out. */
.nw-key[aria-pressed="false"] { opacity: .45; }
.nw-key[aria-pressed="false"] i { background: var(--text-3) !important; }

/* The two ends of the line, one at each end of the row. */
.nw-ends {
    display: flex; justify-content: space-between; gap: 12px;
    margin-top: 8px;
    font-size: .76rem; color: var(--text-3); line-height: 1.35;
}

/* ----------------------------------------------------- merchant logos --- */

/*
 * One mark per row, the same size whether it is a fetched logo or the
 * category badge standing in for one - otherwise the descriptions no longer
 * line up and the list reads as broken.
 *
 * object-fit: contain because there is no GD on this host to crop or resize
 * with: the cache holds whatever each site publishes, from 25px to 256px and
 * square or not, and the browser does the fitting.
 */
.mlogo {
    flex: none;
    width: 22px; height: 22px;
    border-radius: 5px;
    object-fit: contain;
    background: var(--surface-2);
    align-self: center;
    margin-right: 9px;
}
.mlogo.as-badge {
    display: grid; place-items: center;
    background: none;
}
.mlogo.as-badge .gicon { width: 22px; height: 22px; }
.mlogo img { width: 100%; height: 100%; object-fit: contain; border-radius: 5px; }

/*
 * Account, arrow, merchant. The marks keep their own margin off, so the pair
 * sits as one block against the description.
 */
.tflow {
    display: inline-flex; align-items: center; gap: 3px;
    flex: none; margin-right: 9px;
}
.tflow .mlogo { margin-right: 0; }
/* The card glyph standing in for a bank with no picture. */
.tflow .mlogo.as-badge { color: var(--text-3); }
.tf-arrow {
    width: 13px; height: 10px; flex: none;
    fill: none; stroke: var(--text-3); stroke-width: 1.6;
    stroke-linecap: round; stroke-linejoin: round;
}

/* On a phone the pair is the first thing squeezed, so it loses the gaps. */
@media (max-width: 420px) {
    .tflow { gap: 1px; margin-right: 7px; }
    .tf-arrow { width: 10px; }
}

/* ------------------------------------------------- capture form actions --- */

/* Its own card, sitting under the one with the fields in it. */
.cap-actions { margin-bottom: 0; }

/*
 * On a phone, Save stays in reach.
 *
 * Measured at 375x812 with an amount already filled in: the button landed at
 * y=784, behind the bottom nav, and getting to it meant scrolling the whole
 * form - for a screen whose entire job is "pick a category, save".
 *
 * Sticky, not fixed, so at the end of the form it settles into its natural
 * place rather than hovering over the last field for ever. Pinned to the top
 * of the nav; the "+" circle overhangs the nav by 5px and sits slightly over
 * this bar, but its tappable area never leaves the nav, so it cannot take a
 * tap meant for Save. z-index stays under the nav's 45 for the same reason.
 *
 * Phones only: from 780px up --nav-h is 0, the nav is at the top, and the form
 * is a centred card that does not need its footer pinned.
 */
@media (max-width: 779px) {
    .cap-actions {
        position: sticky;
        /*
         * The nav's measured height, set on the outlet when the form is built.
         * --nav-h is 60px and the nav renders 69: the row is stretched by the
         * "+" circle, so trusting the variable left the button's last 9px -
         * and its label - behind the bar. The fallback is the old sum, for the
         * moment before the script runs.
         */
        bottom: var(--cap-bar-bottom, calc(var(--nav-h) + var(--safe-b)));
        z-index: 40;
        background: color-mix(in srgb, var(--surface) 94%, transparent);
        backdrop-filter: saturate(180%) blur(14px);
        -webkit-backdrop-filter: saturate(180%) blur(14px);
    }
    /* 40px is under the 44px a thumb wants, and this is the one button that
       matters on the screen. */
    .cap-actions .btn { min-height: 46px; }
}

.seg {
    display: flex; gap: 3px; padding: 3px;
    background: var(--surface-3);
    border-radius: var(--radius-sm);
}
.seg button {
    flex: 1; appearance: none; border: 0; background: none;
    font: inherit; font-size: .85rem; font-weight: 550;
    color: var(--text-2);
    padding: 8px 6px; border-radius: 4px;
    cursor: pointer;
}
.seg button.on { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); }

/* -------------------------------------------------------------- chips ---- */

.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
    font-size: .76rem; font-weight: 600;
    padding: 4px 9px;
    border-radius: 999px;
    background: var(--surface-3);
    color: var(--text-2);
    border: 1px solid transparent;
    cursor: pointer;
    white-space: nowrap;
}
.chip.on { background: var(--brand-soft); color: var(--brand-text); border-color: color-mix(in srgb, var(--brand) 35%, transparent); }
.chip.static { cursor: default; }
.chip.warn { background: var(--warn-soft); color: var(--warn); }
.chip.bad { background: var(--bad-soft); color: var(--bad); }
.chip.good { background: var(--good-soft); color: var(--good); }
.chip.info { background: var(--info-soft); color: var(--info); }

.scroll-x {
    display: flex; gap: 6px;
    overflow-x: auto;
    padding-bottom: 4px;
    scrollbar-width: none;
    -ms-overflow-style: none;
    scroll-snap-type: x proximity;
}
.scroll-x::-webkit-scrollbar { display: none; }
.scroll-x > * { scroll-snap-align: start; flex: none; }

/* ------------------------------------------------------- transactions --- */

.txn {
    display: flex; gap: 10px; align-items: flex-start;
    padding: 11px 13px;
    border-top: 1px solid var(--border);
    background: var(--surface);
}
.txn:first-child { border-top: 0; }
.txn.attention { background: color-mix(in srgb, var(--warn-soft) 60%, var(--surface)); }
.txn.selected { background: var(--brand-soft); }
.txn .pick { flex: none; padding-top: 2px; }
.txn .body { flex: 1; min-width: 0; }
.txn .t1 { display: flex; gap: 8px; align-items: baseline; }
.txn .desc {
    flex: 1; min-width: 0; font-weight: 550;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.txn .amt { font-weight: 650; white-space: nowrap; font-variant-numeric: tabular-nums; }
.txn .t2 {
    font-size: .76rem; color: var(--text-3);
    margin-top: 2px;
    display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
}
.txn .cat {
    font-size: .74rem; font-weight: 600;
    padding: 2px 7px; border-radius: 999px;
    background: var(--surface-3); color: var(--text-2);
}
.txn .cat.none { background: var(--warn-soft); color: var(--warn); }
.txn .cat.alloc { background: var(--info-soft); color: var(--info); }

/* ---- the two controls that live on the row ------------------------------
   Both are meant to read as the labels they replaced until you reach for
   them: the list is for scanning first and editing second, and a row full of
   obvious form controls would make it neither.
   ------------------------------------------------------------------------ */

/* The picker wears .cat as well, so it keeps that chip's size, radius and
   the three state colours. Only the caret and the hit area are new. */
.txn .cat-pick {
    position: relative;
    display: inline-flex; align-items: center;
    padding-right: 3px;
    max-width: min(240px, 52vw);
}
.txn .cat-pick select {
    appearance: none; -webkit-appearance: none;
    font: inherit; color: inherit;
    background: transparent; border: 0; border-radius: 999px;
    /* The right padding runs under the caret, so a tap on the caret is a tap
       on the select rather than on the span behind it. */
    margin: 0; padding: 0 16px 0 0;
    max-width: 100%;
    text-overflow: ellipsis;
    cursor: pointer;
}
.txn .cat-pick select:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}
.txn .cat-pick::after {
    content: '';
    position: absolute; right: 8px; top: 50%;
    width: 5px; height: 5px; margin-top: -4px;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(45deg);
    opacity: .5;
    pointer-events: none;
}

.txn .okmark {
    display: inline-flex; align-items: center; gap: 3px;
    flex: none;
    padding: 2px 7px 2px 5px;
    border-radius: 999px;
    border: 1px solid var(--border-strong);
    background: var(--surface-3); color: var(--text-3);
    font-size: .68rem; font-weight: 650;
    text-transform: uppercase; letter-spacing: .04em;
    cursor: pointer;
    -webkit-user-select: none; user-select: none;
}
/* The input still exists and still gets focus and keyboard - it is only the
   browser's own box that is out of the way. */
.txn .okmark input {
    position: absolute;
    width: 1px; height: 1px;
    opacity: 0; margin: 0;
}
.txn .okmark svg { opacity: .35; }
.txn .okmark:has(input:focus-visible) {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}
/* Confirmed is the quiet state, because nearly every row is confirmed and a
   list of green badges would be a list of nothing. The colour is spent on
   the rows that still want you. */
.txn .okmark.on {
    border-color: transparent;
    background: transparent; color: var(--text-3);
    padding: 2px 4px;
}
.txn .okmark.on svg { opacity: .8; }
.txn .okmark.on:hover { background: var(--surface-3); color: var(--good); }

.txn .okmark:not(.on) {
    border-color: var(--warn);
    color: var(--warn);
    background: var(--warn-soft);
}
/* The row behind it is already tinted amber, so the badge takes the plain
   surface instead - two amber washes on top of each other read as neither. */
.txn.attention .okmark:not(.on) { background: var(--surface); }
.txn .okmark:not(.on) svg { opacity: .9; }
.txn .splits {
    margin-top: 7px; padding-left: 10px;
    border-left: 2px dashed var(--border-strong);
    display: grid; gap: 5px;
}
.txn .splits .sp {
    display: flex; gap: 8px; font-size: .78rem; color: var(--text-2);
}
.txn .splits .sp .n { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A category row in the notification sheet: the whole row is the label, so
   the tap target is the row rather than a 16px box at the end of it. */
.txn.push-cat { cursor: pointer; align-items: center; }
.txn.push-cat .t1 { align-items: center; gap: 10px; }
.txn.push-cat .desc { display: flex; align-items: center; gap: 8px; font-weight: 550; }
.txn.push-cat input { flex: none; width: 20px; height: 20px; }

.inline-edit {
    padding: 11px 13px;
    background: var(--surface-2);
    border-top: 1px solid var(--border);
}

.empty {
    text-align: center;
    padding: 30px 18px;
    color: var(--text-3);
}
.empty .big { font-size: 2rem; margin-bottom: 6px; }

/* -------------------------------------------------------------- sheet ---- */

.backdrop {
    position: fixed; inset: 0; z-index: 90;
    background: rgba(8, 12, 22, .5);
    backdrop-filter: blur(2px);
    display: flex; align-items: flex-end; justify-content: center;
    animation: dim .16s ease;
}
/* The dim fades in via background-colour rather than opacity: opacity would
   apply to the sheet as well, and a throttled animation in a background tab
   can park an element on its first keyframe - an invisible dialog. */
@keyframes dim { from { background: rgba(8, 12, 22, 0); } }
.sheet {
    background: var(--surface);
    width: 100%;
    max-width: 620px;
    max-height: 92vh;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: var(--shadow-lg);
    display: flex; flex-direction: column;
    animation: slideup .22s cubic-bezier(.22, 1, .36, 1);
    padding-bottom: var(--safe-b);
}
/* Transform only, deliberately: a browser that throttles animations in a
   background tab can leave an element parked on the `from` keyframe, and a
   half-transparent sheet would be unreadable. Sliding alone is always legible. */
@keyframes slideup { from { transform: translateY(22px); } }
.sheet-head {
    display: flex; align-items: center; gap: 10px;
    padding: 14px 16px 10px;
    border-bottom: 1px solid var(--border);
}
.sheet-head h2 { flex: 1; font-size: 1.05rem; }
.sheet-body { padding: 14px 16px; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.sheet-foot {
    padding: 10px 16px 14px;
    border-top: 1px solid var(--border);
    display: flex; gap: 8px;
}
.sheet-foot .btn { flex: 1; }
.grab {
    width: 38px; height: 4px; border-radius: 999px;
    background: var(--border-strong);
    margin: 8px auto 0;
}
@media (min-width: 700px) {
    .backdrop { align-items: center; }
    .sheet { border-radius: var(--radius-lg); max-height: 86vh; }
    .grab { display: none; }
}

/* ---------------------------------------------------- confirmation card -- */

/*
 * The Shortcut's confirmation takes the screen.
 *
 * It is the end of a thirty-second errand rather than a step in a sitting, so
 * it gets read at a glance and dismissed - and a bottom sheet puts that glance
 * in the bottom third with the finished form still visible behind it.
 */
.sheet.full {
    max-width: 620px;
    height: 100dvh;
    max-height: 100dvh;
    border-radius: 0;
    /*
     * No slide. The shared keyframe starts 22px lower and, exactly as the note
     * on it warns, a browser can park an element on the `from` frame - which
     * on a sheet that is already the full height of the screen leaves it 22px
     * down for good, with the buttons clipped off the bottom. Measured doing
     * precisely that: transform stuck at translateY(22px), Done ending at 820
     * in an 812 viewport.
     */
    animation: none;
}
/* flex: 1 so the body takes the leftover height - without it the content
   and the buttons bunch at the top with dead screen beneath them. */
.sheet.full .sheet-body {
    flex: 1;
    display: flex; flex-direction: column; justify-content: center;
}

/* Big enough to read without looking. */
.confirm.big .tick { width: 76px; height: 76px; margin-bottom: 16px; }
.confirm.big .amount { font-size: 2.6rem; }
.confirm.big .what { font-size: 1.05rem; margin-bottom: 18px; }

/*
 * The budget figures too. This screen is read at arm's length with a card
 * machine still printing, and "R 1 088.00 remaining" at 0.92rem is the one
 * number on it somebody is actually deciding something with.
 */
.confirm.big .item-summary { padding: 14px; }
.confirm.big .item-summary .head .n { font-size: 1.02rem; }
/*
 * Two on top, and what is left on a line of its own underneath.
 *
 * Budget and Spent are context; Remaining is the answer, and it is the only
 * figure on this screen anybody decides anything with - whether to put the
 * second thing in the basket back. Sharing a row with the other two capped it
 * at a third of the width and therefore at a third of the type size.
 */
.confirm.big .item-summary .nums {
    grid-template-columns: repeat(2, 1fr);
    gap: 10px; margin-top: 12px;
}
.confirm.big .item-summary .nums > div { padding: 11px 6px; }
.confirm.big .item-summary .nums .k { font-size: .74rem; }
.confirm.big .item-summary .nums .v { font-size: 1.28rem; }

.confirm.big .item-summary .nums > div:last-child {
    grid-column: 1 / -1;
    padding: 14px 10px;
}
.confirm.big .item-summary .nums > div:last-child .k { font-size: .82rem; }
.confirm.big .item-summary .nums > div:last-child .v { font-size: 2.1rem; letter-spacing: -.02em; }
.confirm.big .item-summary .foot { font-size: .84rem; margin-top: 11px; }

/* Card, arrow, shop. */
.confirm .pay-flow {
    display: flex; align-items: flex-start; justify-content: center;
    gap: 10px; margin: 0 auto 14px;
}
.confirm .pay-flow .leg {
    margin: 0; display: grid; justify-items: center; gap: 7px;
    width: 92px; min-width: 0;
}
.confirm .pay-flow .mark { margin: 0; }
/* An account with no bank behind it: the glyph, quietly. */
.confirm .pay-flow .mark.card { background: var(--surface-2); color: var(--text-3); }
.confirm .pay-flow figcaption {
    font-size: .72rem; line-height: 1.3; color: var(--text-3);
    overflow-wrap: anywhere;
}
.confirm .flow-arrow {
    width: 38px; height: 22px; flex: none; margin-top: 21px;
    fill: none; stroke: var(--text-3); stroke-width: 2;
    stroke-linecap: round; stroke-linejoin: round;
}
.confirm.big .pay-flow { gap: 14px; margin-bottom: 18px; }
.confirm.big .pay-flow .leg { width: 112px; }
.confirm.big .pay-flow .mark { width: 80px; height: 80px; border-radius: 18px; }
.confirm.big .pay-flow .mark img { border-radius: 18px; }
.confirm.big .pay-flow .mark .gicon { width: 54px; height: 54px; }
.confirm.big .pay-flow figcaption { font-size: .82rem; }
.confirm.big .flow-arrow { width: 48px; height: 28px; margin-top: 26px; }

/* The merchant's own mark, when one is cached - see LogoService. */
.confirm .mark {
    width: 64px; height: 64px; margin: 0 auto 12px;
    border-radius: 14px;
    object-fit: contain;
    background: var(--surface-2);
    display: grid; place-items: center;
}
.confirm .mark img { width: 100%; height: 100%; object-fit: contain; border-radius: 14px; }
.confirm .mark .gicon { width: 44px; height: 44px; }

.confirm { text-align: center; padding: 6px 0 2px; }
.confirm .tick {
    width: 58px; height: 58px; margin: 0 auto 10px;
    border-radius: 50%;
    background: var(--good-soft); color: var(--good);
    display: grid; place-items: center;
    animation: pop .28s cubic-bezier(.2, 1.4, .4, 1);
}
@keyframes pop { from { transform: scale(.6); opacity: 0; } }
.confirm .amount { font-size: 1.7rem; font-weight: 700; letter-spacing: -.02em; }
.confirm .what { color: var(--text-2); margin-bottom: 12px; }

.item-summary {
    text-align: left;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 13px;
}
.item-summary .head { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }
.item-summary .head .swatch { width: 10px; height: 10px; border-radius: 3px; }
.item-summary .head .n { flex: 1; font-weight: 650; }
.item-summary .nums {
    display: grid; grid-template-columns: repeat(3, 1fr);
    gap: 8px; text-align: center;
    margin-top: 10px;
}
.item-summary .nums > div { background: var(--surface); border-radius: var(--radius-sm); padding: 8px 4px; min-width: 0; }
.item-summary .nums .k { font-size: .68rem; color: var(--text-3); text-transform: uppercase; letter-spacing: .04em; }
.item-summary .nums .v {
    font-weight: 680;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
    font-size: .92rem;
}
.item-summary .foot { font-size: .78rem; color: var(--text-3); margin-top: 9px; }

/* --------------------------------------------------- theme picker ------- */

.theme-list { display: grid; gap: 8px; margin: 12px 0; }
.theme-row {
    appearance: none; font: inherit; text-align: left; cursor: pointer;
    display: flex; align-items: center; gap: 11px;
    padding: 10px 12px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
}
.theme-row:hover { border-color: var(--border-strong); }
.theme-row.on { border-color: var(--brand); background: var(--brand-soft); }
.theme-swatch {
    width: 34px; height: 34px; flex: none;
    border-radius: var(--radius-sm);
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
}
.theme-meta { flex: 1; min-width: 0; display: block; }
.theme-meta strong { display: block; font-size: .92rem; font-weight: 600; }
.theme-meta small { color: var(--text-3); font-size: .76rem; line-height: 1.3; display: block; }
.theme-tick { color: var(--brand-text); flex: none; }

/* -------------------------------------------------------------- toast ---- */

.toasts {
    position: fixed;
    left: 12px; right: 12px;
    bottom: calc(var(--nav-h) + var(--safe-b) + 14px);
    z-index: 120;
    display: grid; gap: 8px;
    pointer-events: none;
}
.toast {
    background: #111827;
    color: #f9fafb;
    border-radius: var(--radius-sm);
    padding: 11px 13px;
    font-size: .88rem;
    box-shadow: var(--shadow-lg);
    display: flex; gap: 8px; align-items: flex-start;
    animation: slideup .2s ease;
    pointer-events: auto;
}
.toast.good { background: #065f46; }
.toast.bad { background: #991b1b; }
.toast .x { margin-left: auto; opacity: .7; cursor: pointer; }
@media (min-width: 700px) {
    .toasts { left: auto; right: 18px; width: 380px; bottom: 18px; }
}

/* ------------------------------------------------------------- tables ---- */

.tbl-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.tbl { width: 100%; border-collapse: collapse; font-size: .88rem; }
table.tbl th, table.tbl td {
    padding: 9px 11px;
    text-align: left;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}
table.tbl th {
    font-size: .72rem; text-transform: uppercase; letter-spacing: .05em;
    color: var(--text-3); font-weight: 700;
    background: var(--surface-2);
}
table.tbl td.num, table.tbl th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.tbl tr:last-child td { border-bottom: 0; }

/* ------------------------------------------------------------ helpers ---- */

.stack { display: grid; gap: 10px; }
.flex { display: flex; gap: 8px; align-items: center; }
.flex.wrap { flex-wrap: wrap; }
.spacer { flex: 1; }
.center { text-align: center; }
.nowrap { white-space: nowrap; }
.hide { display: none !important; }
.pill-count {
    background: var(--bad); color: #fff;
    font-size: .68rem; font-weight: 700;
    padding: 1px 6px; border-radius: 999px;
}
.divider { height: 1px; background: var(--border); margin: 12px 0; }
.skeleton {
    background: linear-gradient(90deg, var(--surface-3) 25%, var(--surface-2) 50%, var(--surface-3) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.2s infinite;
    border-radius: var(--radius-sm);
    height: 58px;
    margin-bottom: 10px;
}
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }

.callout {
    border: 1px solid var(--border);
    border-left: 3px solid var(--info);
    background: var(--info-soft);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
    font-size: .85rem;
    color: var(--text-2);
    margin-bottom: 12px;
}
.callout.warn { border-left-color: var(--warn); background: var(--warn-soft); }
.callout.bad { border-left-color: var(--bad); background: var(--bad-soft); }
.callout.good { border-left-color: var(--good); background: var(--good-soft); }
code {
    font-family: var(--mono); font-size: .82em;
    background: var(--surface-3); padding: 1px 5px; border-radius: 3px;
    /* Break long URLs, but keep short paths like bin/sync.php in one piece. */
    word-break: normal;
    overflow-wrap: anywhere;
}
pre.code {
    font-family: var(--mono); font-size: .78rem;
    background: var(--surface-3);
    padding: 11px; border-radius: var(--radius-sm);
    overflow-x: auto;
    margin: 0 0 10px;
}

/* ---------------------------------------------------------- auth page ---- */

.auth-wrap {
    min-height: 100vh;
    display: grid; place-items: center;
    padding: 24px 16px calc(24px + var(--safe-b));
}
.auth-card {
    width: 100%; max-width: 380px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow);
    padding: 22px;
}
.auth-card .logo {
    width: 48px; height: 48px; border-radius: 8px;
    background: var(--brand); color: #fff;
    display: grid; place-items: center;
    font-size: 1.4rem; font-weight: 700;
    margin: 0 auto 12px;
}

/* The marker on a budget line that belongs to a savings pot. "from pot" is the
   one that matters: that row is on the page and in none of the totals. */
.pot-tag {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 5px;
    border-radius: var(--radius-sm);
    font-size: .6rem; font-weight: 700;
    letter-spacing: .03em; text-transform: uppercase;
    background: var(--surface-3); color: var(--text-3);
    vertical-align: 1px;
}
.pot-tag.in { background: var(--good-soft); color: var(--good); }

.hero-aside { opacity: .82; font-size: .94em; }
/* --------------------------------------------------- savings pots ------- */

.sv-fund { padding: 0; overflow: hidden; }

.sv-fund-head {
    display: flex; align-items: flex-start; gap: 10px;
    padding: 13px 14px 11px;
    border-bottom: 1px solid var(--border);
}
.sv-fund-id { flex: 1; min-width: 0; }
.sv-fund-head h3 { margin: 0 0 2px; font-size: .95rem; font-weight: 650; }
.sv-fund-head .muted { font-size: .76rem; }

/* What the pots claim against what the bank says. Quiet when they agree -
   a green bar on every fund every day trains people to stop reading it. */
.sv-rec {
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
    padding: 8px 14px;
    font-size: .76rem;
    border-bottom: 1px solid var(--border);
}
.sv-rec.ok   { color: var(--text-3); }
.sv-rec.ok svg { color: var(--good); }
.sv-rec.warn { background: var(--warn-soft); color: var(--text-2); }
.sv-rec.bad  { background: var(--bad-soft); color: var(--text-2); }
.sv-rec-fig {
    margin-left: auto;
    font-variant-numeric: tabular-nums;
    opacity: .78; white-space: nowrap;
}

.sv-pots { display: block; }
/* The rows are .item like everywhere else, so they only need to say they are
   tappable - keyboard focus included, since they are divs with a role. */
.sv-pot { cursor: pointer; }
.sv-pot:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.sv-none { padding: 14px; font-size: .8rem; margin: 0; }

.sv-fund-foot {
    padding: 9px 14px 11px;
    border-top: 1px solid var(--border);
}

/* ---- one pot, inside its sheet ---- */

.sv-detail { padding: 2px 0 12px; }
.sv-detail-big {
    font-size: 1.7rem; font-weight: 700; letter-spacing: -.02em;
    font-variant-numeric: tabular-nums;
}
.sv-detail .muted { font-size: .78rem; line-height: 1.5; }

.sv-ledger {
    border-top: 1px solid var(--border);
    max-height: 46vh; overflow-y: auto;
}
.sv-move {
    display: flex; align-items: center; gap: 10px;
    padding: 9px 2px;
    border-bottom: 1px solid var(--border);
}
.sv-move:last-child { border-bottom: 0; }
.sv-move-main { flex: 1; min-width: 0; }
.sv-move-what {
    display: block; font-size: .83rem; font-weight: 550;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sv-move-when { display: block; font-size: .72rem; color: var(--text-3); }
.sv-move-amt {
    font-variant-numeric: tabular-nums; font-weight: 640;
    white-space: nowrap;
}
.sv-move-amt.in  { color: var(--good); }
.sv-move-amt.out { color: var(--text-2); }
/* A row that came from a transaction is not deletable here - undoing it means
   re-categorising the transaction. The icon stands where the X would be so the
   rows still line up. */
.sv-move-locked {
    display: inline-grid; place-items: center;
    width: 30px; height: 30px;
    color: var(--text-3); opacity: .55;
}
/* ------------------------------------------- category name, in place --- */

/* Same idea as .planned on the budget: it looks like the text around it until
   you go near it. A list of thirteen categories is mostly read, and thirteen
   visible text boxes would turn a list into a form. */
input.gp-name {
    flex: 1; min-width: 0;
    appearance: none; -webkit-appearance: none;
    background: none;
    border: 0;
    border-bottom: 1px dotted transparent;
    border-radius: 3px;
    padding: 1px 2px;
    margin: 0;
    font: inherit; color: inherit;
    cursor: text;
}
input.gp-name:hover { border-bottom-color: var(--text-3); }
input.gp-name:focus {
    background: var(--surface-2);
    border-bottom-color: var(--brand);
    color: var(--text);
    outline: none;
}
/* A hit area a thumb can find, without moving the row. */
@media (hover: none) {
    input.gp-name { padding: 3px 5px; margin: -3px -4px; border-bottom-color: var(--text-3); }
}

/* --------------------------------------------- category colour swatch --- */

/* A native colour input, cut down to a swatch. The browser chrome around one
   is a chunky bevelled box that would dominate a dense row; the picker it
   opens is the one worth keeping. */
.gp-swatch {
    flex: none;
    width: 22px; height: 22px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: none;
    cursor: pointer;
    appearance: none; -webkit-appearance: none;
}
.gp-swatch::-webkit-color-swatch-wrapper { padding: 2px; }
.gp-swatch::-webkit-color-swatch { border: 0; border-radius: 2px; }
.gp-swatch::-moz-color-swatch { border: 0; border-radius: 2px; }
.gp-swatch:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ------------------------------------------------- working spinner ------ */

/* Shown in a button while its handler runs. The button keeps the width it
   had, so a row of them does not shuffle sideways when one is swapped for a
   spinner and back. */
.spin {
    display: inline-block;
    width: 1em; height: 1em;
    vertical-align: -.14em;
    border: 2px solid currentColor;
    /* One transparent edge is what makes the rotation visible at all. */
    border-right-color: transparent;
    border-radius: 50%;
    animation: spin .62s linear infinite;
    opacity: .85;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* A button that is working is not a button that can be pressed, and it should
   look like it. The browser's own disabled styling is inconsistent across the
   ghost and primary variants, so it is stated here. */
.btn[aria-busy="true"] { opacity: .68; cursor: progress; }

/* Text for a screen reader and nothing else - the spinner is a picture, and a
   picture of activity says nothing out loud. */
.vis-hidden {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
/* ------------------------------------------------- reduced motion ------- */

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* ------------------------------------------------------------ printing -- */

@media print {
    .bottom-nav, .topbar, .toasts { display: none !important; }
    body { background: #fff; }
    .card { box-shadow: none; break-inside: avoid; }
}

/* ------------------------------------------ budget item statement ------ */
/* Three columns, and the third is the reason the page exists: what the
   balance was after each movement. Tabular figures throughout, because a
   column of amounts that does not line up cannot be scanned. */
.st-month { padding: 0; overflow: hidden; margin-bottom: 10px; }
.st-head {
    display: flex; align-items: center; gap: 8px;
    padding: 10px 12px;
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
}
.st-head h3 { margin: 0; flex: 1; font-size: .9rem; font-weight: 650; }

.st-rows { display: block; }
.st-row {
    display: grid;
    grid-template-columns: 1fr auto auto;
    align-items: baseline; gap: 10px;
    padding: 8px 12px;
    border-bottom: 1px solid var(--border);
}
.st-row:last-child { border-bottom: 0; }
.st-what { min-width: 0; }
.st-label {
    display: block; font-size: .83rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.st-sub { display: block; font-size: .72rem; color: var(--text-3); }
.st-delta, .st-bal {
    font-variant-numeric: tabular-nums;
    white-space: nowrap; font-size: .83rem;
}
.st-delta { min-width: 5.5rem; text-align: right; color: var(--text-2); }
.st-delta.pos { color: var(--good); }
.st-delta.neg { color: var(--text-2); }
.st-bal { min-width: 6rem; text-align: right; font-weight: 640; }

/* The two balances are the anchors - the movements between them are the
   working. */
.st-row.open, .st-row.close { background: var(--surface-2); }
.st-row.open .st-label, .st-row.close .st-label { font-weight: 600; }
.st-row.budget .st-label { color: var(--text-2); }

/* A transfer is listed because it is filed against this line, and greyed
   because no budget total counts it. Its balance column is unchanged from
   the row above, which is the point. */
.st-row.uncounted .st-label,
.st-row.uncounted .st-bal { opacity: .5; }

.st-break {
    margin: 0; padding: 9px 12px;
    border-bottom: 1px solid var(--border);
    background: var(--warn-soft);
    font-size: .76rem; line-height: 1.5; color: var(--text-2);
}

/* ------------------------------------------------- sync check --------- */
/* Two kinds of row, and the difference is the point: a discrepancy reads
   normally, a "cannot be checked yet" is dimmed and folded away. */
.si-acct { padding: 0; overflow: hidden; margin-bottom: 10px; }
.si-head {
    display: flex; align-items: center; gap: 8px;
    padding: 10px 12px;
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
}
.si-head h3 { margin: 0; flex: 1; font-size: .9rem; font-weight: 650; }

.si-health {
    padding: 9px 12px;
    border-bottom: 1px solid var(--border);
    background: var(--warn-soft);
    font-size: .78rem; line-height: 1.5; color: var(--text-2);
}

.si-row {
    padding: 9px 12px;
    border-bottom: 1px solid var(--border);
    cursor: pointer;
}
.si-row:last-child { border-bottom: 0; }
.si-row:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.si-row-top { display: flex; align-items: baseline; gap: 10px; }
.si-desc {
    flex: 1; min-width: 0; font-size: .85rem; font-weight: 550;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.si-amt { font-variant-numeric: tabular-nums; white-space: nowrap; font-size: .85rem; }
.si-row-sub { font-size: .72rem; color: var(--text-3); margin-top: 2px; }
.si-why { font-size: .75rem; line-height: 1.5; color: var(--text-2); margin-top: 5px; }

/* Nothing is known about these, so they must not read as findings. */
.si-row.quiet { opacity: .62; }

.si-more { border-top: 1px solid var(--border); }
.si-more > summary {
    padding: 9px 12px; cursor: pointer;
    font-size: .78rem; color: var(--text-2);
}
.si-more > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.si-more[open] > summary { border-bottom: 1px solid var(--border); }
.si-none { padding: 12px; font-size: .8rem; margin: 0; }
