/* =================================================================================================
   BudgetControl - the whole app, in Concept C "Dashboard".

   Comp: Projects\HomeFin\Designs\concept-c-dashboard.html (+ designs.css for the token set), which
   Johan has at http://budget-designs.netdentity.net/ and picked on 2026-09-23. Task 471.

   Task 449 put Concept C on the SHELL only - the rail, the pill row and the chart board - and left
   the rest of the page on the old `1px solid currentColor` vocabulary. That was visible: the board
   was a designed thing sitting on top of a wireframe. Task 471 is the rest of it. Everything below
   is now drawn from one token set:

       plane -> surface -> border -> shadow

   A panel is a SURFACE card floating on the PLANE. Nothing on this page draws a box with
   currentColor any more, because currentColor is the text colour and a near-black 1px box round
   every section is what made the old page read as a form rather than a product.

   THREE DECISIONS HERE THAT ARE NOT COSMETIC, all carried forward from task 449:

     * THE RAIL IS ALWAYS NAVY; THE BOARD FOLLOWS THE USER'S COLOUR SCHEME. The comp is a light
       board. Hard-coding it white would hand a dark-mode user a white rectangle at midnight, so
       every board token has a dark counterpart below and the rail keeps its one fixed colour
       because it is the concept's anchor and it is readable either way.

     * BELOW 60rem THE RAIL BECOMES A HORIZONTAL SCOPE STRIP, NOT A HAMBURGER DRAWER. Hiding the
       active scope hides why the total on screen is a third of the household's money.

     * PHONE WIDTH (390px = 24.4rem) IS A FIRST-CLASS WIDTH, not a follow-up - task 471 says so
       explicitly. The last block in this file is the 30rem one and it exists to be opened at 390.

   COLOUR IS NEVER THE ONLY SIGNAL. Every delta carries a glyph and a word; every account carries a
   name beside its dot; no value lives in a `title` attribute, because a tooltip never fires on
   touch. Three account hues, because three passed the contrast gate.
   ================================================================================================= */

:root {
    color-scheme: light dark;

    /* Surfaces. --plane is Concept C's own override of the comp set (#eef1f5): the board is a
       cooler grey than the other four concepts so the white cards lift off it. */
    --plane:   #eef1f5;
    --surface: #ffffff;
    --raised:  #ffffff;
    --wash:    #f4f6f9;

    /* Ink. --ink is a navy-black rather than a true black so it agrees with the rail. --muted is
       the floor: #667085 is 5.4:1 on white, which clears AA for body text, and nothing smaller
       than --t-small ever uses it. */
    --ink:   #101828;
    --ink-2: #475467;
    --muted: #667085;

    --border: #dde3ea;
    --grid:   #e6ebf1;
    --shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 8px 24px rgba(16, 24, 40, .05);

    /* One accent for CATEGORIES. They are ranked by LENGTH, not by hue - 21 categories against an
       eight-hue ceiling of which three passed the contrast gate, so a palette here would be a lie. */
    --s1: #2563eb;
    --s1-ink: #ffffff;
    --s1-wash: #e8f0fe;

    /* Task 484. The comp's second slot, `designs.css` line 28, and it is BACK WITH ITS JOB rather
       than as a spare colour: --s2 is THE MONTH BEFORE THIS ONE. Task 471 reduced the page to one
       accent, which is correct for categories and wrong for comparisons - every ghost line and every
       "against last month" on the comp is this orange, and with it deleted the board had nothing to
       compare against. It is never used for a category, never for an account, and never on its own:
       the prior-month line is DASHED as well as orange, so the difference survives a monochrome
       screen and anyone who cannot separate the two hues. */
    --s2: #eb6834;
    --s2-wash: rgba(235, 104, 52, .10);

    /* Semantic. Spending more is --critical, spending less is --delta-good; both are stated in
       words beside the colour. --warning carries the sample stamp and the Other banner. */
    --critical:   #b3261e;
    --delta-good: #1b6b3a;
    --warning:    #b45309;
    --warning-wash: #fdf6ec;

    --navy:     #12243c;
    --navy-ink: #ffffff;
    --navy-ink-2: #c6d2e2;
    --navy-muted: #8fa2ba;
    --navy-line: rgba(255, 255, 255, .14);
    --navy-wash: rgba(255, 255, 255, .16);

    /* Type, spacing and radius, straight off the comp set. --r-3 is Concept C's 12px, not the
       14px the other concepts use. */
    --t-micro: .6875rem; --t-small: .8125rem;  --t-body: .9375rem; --t-lead: 1.0625rem;
    --t-h3: 1.125rem;    --t-h2: 1.375rem;     --t-h1: 1.75rem;
    --sp-1: .25rem; --sp-2: .5rem; --sp-3: .75rem; --sp-4: 1rem; --sp-5: 1.5rem; --sp-6: 2rem;
    --r-1: 4px; --r-2: 8px; --r-3: 12px;
    --nav-w: 15.25rem;  /* 244px - Concept C's narrow rail */
}

@media (prefers-color-scheme: dark) {
    :root {
        --plane:   #0b1220;
        --surface: #141c2b;
        --raised:  #1b2534;
        --wash:    rgba(255, 255, 255, .05);

        --ink:   #f2f5f9;
        --ink-2: #c3ccd9;
        --muted: #94a3b8;

        --border: rgba(255, 255, 255, .13);
        --grid:   rgba(255, 255, 255, .09);
        --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .35);

        --s1: #4b8bf5;
        --s1-ink: #06142b;
        --s1-wash: rgba(75, 139, 245, .16);

        /* Lifted off the comp's own dark block (`designs.css` line 61 is #d95926, which is DARKER
           and disappears on a #0b1220 plane). This is the same hue opened up until it clears the
           plane, the way --s1 is. */
        --s2: #f2895c;
        --s2-wash: rgba(242, 137, 92, .14);

        --critical:   #f28b82;
        --delta-good: #6ee7a0;
        --warning:    #fbbf24;
        --warning-wash: rgba(251, 191, 36, .12);
    }
}

/* The page hides and shows almost everything with the hidden ATTRIBUTE, and the browser implements
   that as display:none in its own stylesheet - which ANY display rule of ours outranks. .action-bar
   and .toast both set display:flex, so both were on screen from the first paint: an empty toast
   with an Undo button, and a floating "0 selected" bar over the panel below it. One rule, here,
   rather than a :not([hidden]) on every component that ever grows a display. */
[hidden] { display: none !important; }

* { box-sizing: border-box; }

body {
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    margin: 0; line-height: 1.5;
    background: var(--plane); color: var(--ink);
    font-size: 1rem;
}

h1, h2, h3 { line-height: 1.25; }

/* ------------------------------------------------------------------------------------------------
   Controls. One button vocabulary for the whole app: a quiet surface button by default, the accent
   for the one primary action in a block, a bare button where it sits inside a list.
   ------------------------------------------------------------------------------------------------ */
button {
    font: inherit; font-size: var(--t-body);
    padding: .45rem 1rem; border-radius: var(--r-2);
    border: 1px solid var(--border); background: var(--surface); color: var(--ink);
    cursor: pointer;
}
button:hover { background: var(--wash); border-color: var(--muted); }
button:focus-visible { outline: 2px solid var(--s1); outline-offset: 2px; }

/* The one primary action per block. Named rather than a class, because these are the buttons that
   already exist and the markup should not have to grow a class to say what it obviously is. */
#upload-form button[type="submit"],
#rulebook-add button[type="submit"],
#account-register,
#account-password-save,
#bulk-apply {
    background: var(--s1); color: var(--s1-ink); border-color: var(--s1); font-weight: 600;
}
#upload-form button[type="submit"]:hover,
#rulebook-add button[type="submit"]:hover,
#account-register:hover,
#account-password-save:hover,
#account-form #account-register:hover {
    background: color-mix(in srgb, var(--s1) 85%, #000); border-color: transparent;
}

select, input[type="search"], input[type="email"], input[type="password"], input[type="file"] {
    font: inherit; font-size: var(--t-body);
    padding: .4rem .55rem; border-radius: var(--r-2);
    border: 1px solid var(--border); background: var(--surface); color: var(--ink);
}
select:focus-visible, input:focus-visible { outline: 2px solid var(--s1); outline-offset: 1px; }
input[type="file"] { padding: .35rem; max-width: 100%; }

a { color: var(--s1); }

/* ------------------------------------------------------------------------------------------------
   The shell
   ------------------------------------------------------------------------------------------------ */
.app { display: grid; grid-template-columns: var(--nav-w) 1fr; min-height: 100vh; }

.bc-nav {
    background: var(--navy); color: var(--navy-ink); padding: 1.25rem 1rem;
    display: flex; flex-direction: column; gap: var(--sp-4);
}
/* Task 498 (Johan, chat 514) - the rail, and the Uncategorised list, scroll in their OWN area and stop
   at their bottom instead of dragging the page with them. One rule for both. The rail is pinned to the
   viewport height; at 60rem and below it lies down as the scope strip and this is undone there. */
.bc-nav { overflow-y: auto; overscroll-behavior: contain; }
.bc-nav { position: sticky; top: 0; height: 100vh; height: 100dvh; align-self: start; }
.bc-nav .brand { display: flex; align-items: center; gap: .6rem; font-weight: 700; }
.bc-nav .brand .dot {
    display: inline-grid; place-items: center; width: 1.9rem; height: 1.9rem;
    border-radius: var(--r-2); background: var(--s1); color: #fff; font-size: .95rem; flex: none;
}
.bc-nav .brand small { display: block; font-weight: 400; color: var(--navy-muted); font-size: var(--t-micro); }

.nav-sections { list-style: none; margin: 0; padding: 0; display: grid; gap: .15rem; }
/* Task 484. Three columns, which is the comp's rail row: an 18px icon gutter, the label, and the
   COUNT on the right. The count is the whole reason the rail is worth a look - without it this is a
   table of contents, and the comp puts a number on every row that has one. */
.nav-item {
    display: grid; grid-template-columns: 18px 1fr auto; gap: var(--sp-2); align-items: center;
    padding: .45rem .6rem; border-radius: var(--r-2);
    color: var(--navy-ink-2); text-decoration: none; font-size: var(--t-body); white-space: nowrap;
}
.nav-item:hover, .nav-item:focus-visible { background: var(--navy-wash); color: var(--navy-ink); }
.nav-item:focus-visible { outline: 2px solid #fff; outline-offset: -2px; }
/* The active section. Filled, and the icon goes to full white - the fill alone is a colour-only
   signal, and aria-current is what a screen reader hears. board.js sets it on scroll and on click. */
.nav-item[aria-current="page"] { background: var(--navy-wash); color: var(--navy-ink); font-weight: 600; }
.nav-item[aria-current="page"] .nav-ico { color: var(--navy-ink); }
.nav-ico { color: var(--navy-muted); text-align: center; font-size: .9em; }
.nav-count {
    font-size: var(--t-micro); color: var(--navy-muted); font-variant-numeric: tabular-nums;
}
.nav-item[aria-current="page"] .nav-count { color: var(--navy-ink-2); }
.nav-label { overflow: hidden; text-overflow: ellipsis; }

.nav-heading {
    font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .08em;
    color: var(--navy-muted); margin: var(--sp-2) 0 0; font-weight: 600;
}
/* Empty until board.js finds rows. A heading over an empty list reads as a fault, so both go. */
.nav-accounts:empty, .nav-accounts:empty + .nav-foot { display: none; }
.nav-heading:has(+ .nav-accounts:empty) { display: none; }
/* Task 728 - the core tools grouped under category headers (the same set and order as maxwealth.co.za's home page). */
.nav-group {
    list-style: none; font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .08em;
    color: var(--navy-muted); font-weight: 600; padding: var(--sp-2) .7rem 0;
}


.nav-accounts { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
/* DIRECT CHILDREN ONLY. The statements added in task 484 are <li> too, and a descendant selector
   here drew a bordered card round every one of them - five nested boxes inside one box. */
.nav-accounts > li {
    border: 1px solid var(--navy-line); border-radius: var(--r-2);
    background: rgba(255, 255, 255, .04); min-width: 0; overflow: hidden;
}
/* Task 484. The head of the card is the grid the dot and the name used to be; the statements hang
   underneath it, which is the comp's shape. */
.acct-head {
    display: grid; grid-template-columns: auto 1fr; gap: 0 var(--sp-2); align-items: center;
    padding: .5rem .6rem; min-width: 0;
}
/* The comp's swatch is a 4px bar down the FULL height of the card, not a dot. At 0.6rem the key was
   small enough to miss, which defeats the point of having one. */
.acct-dot { width: 4px; border-radius: 2px; flex: none; align-self: stretch; min-height: 1.9rem; }
/* Three hues, because three is what passed the contrast gate at dE 13.7 - the fourth was yellow
   against orange. Past the third account the swatch is neutral and the NAME carries the difference;
   nothing on this board is distinguished by colour alone. */
.acct-dot.hue-1 { background: #7cc0ff; }
.acct-dot.hue-2 { background: #6fd6c4; }
.acct-dot.hue-3 { background: #f2b366; }
.acct-dot.hue-none { background: var(--navy-muted); }
.acct-name { font-size: var(--t-body); font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
.acct-sub { grid-column: 2; font-size: var(--t-micro); color: var(--navy-muted); }
/* Task 459 - the rename pencil sits in a third column on the name's row; the sub-line stays in 2. */
.acct-head:has(.acct-rename) { grid-template-columns: auto 1fr auto; }
.acct-rename {
    grid-column: 3; grid-row: 1; background: none; border: 0; padding: .1rem .3rem; cursor: pointer;
    color: var(--navy-muted); font-size: var(--t-body); line-height: 1; border-radius: var(--r-2);
}
.acct-rename:hover, .acct-rename:focus-visible { color: #fff; background: rgba(255, 255, 255, .08); }
.acct-rename-form { display: grid; gap: .2rem; min-width: 0; grid-column: 2 / 4; grid-row: 1; }
.acct-rename-form input { font: inherit; font-size: var(--t-body); min-width: 0; width: 100%; }
.acct-rename-status { font-size: var(--t-micro); color: var(--navy-muted); }
.acct-rename-status:empty { display: none; }
.acct-note {
    display: block !important; border: 0 !important; padding: 0 !important; background: none !important;
    font-size: var(--t-micro); color: var(--navy-muted);
}

/* Task 484 - the statements under an account, straight off the comp. They come from
   model.merge.accounts[].statements[] and .gaps[], both of which the browser already held and
   nothing on this page ever read. */
.stmts { list-style: none; margin: 0; padding: 0 .6rem .5rem 1.1rem; display: grid; gap: .1rem; }
.stmt {
    display: grid; grid-template-columns: 14px 1fr auto; gap: var(--sp-2); align-items: center;
    font-size: var(--t-micro); color: var(--navy-ink-2); padding: .2rem .3rem; border-radius: var(--r-1);
    min-width: 0;
}
.stmt .stmt-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stmt-ico { color: var(--navy-muted); text-align: center; }
.stmt-rows { color: var(--navy-muted); font-variant-numeric: tabular-nums; }
/* A HOLE IN THE COVERAGE IS THE ONE THING IN THE RAIL THAT IS NOT NEUTRAL. The comp's --serious,
   and the same orange family as the prior-month line: a missing month makes every total on the
   board smaller than the truth. The glyph and the word "missing" carry it without the colour. */
.stmt.stmt-gap { color: #f2a488; font-weight: 600; }
.stmt.stmt-gap .stmt-ico { color: #f2a488; }
/* The comp's "add" beside a hole. It is a real link to the upload form, not a label: an affordance
   that looks like an action and does nothing is worse than no affordance. */
.stmt-add {
    color: var(--navy-ink-2); font-size: var(--t-micro); text-decoration: underline;
    text-underline-offset: 2px; font-weight: 400;
}
.stmt-add:hover, .stmt-add:focus-visible { color: var(--navy-ink); }
.nav-foot { margin: auto 0 0; font-size: var(--t-micro); color: var(--navy-muted); }

/* The main column. The board is much wider than a column of copy, so the reading constraint lives
   on the blocks that ARE copy rather than on the container. */
.bc-main { max-width: none; margin: 0; padding: var(--sp-5) var(--sp-6) 4rem; min-width: 0; }

/* A page with no board and nothing to scope - /unsupported-bank. One surface card on the plane,
   the same tokens, no rail: a navy rail down the side of one paragraph would be decoration. */
.standalone {
    max-width: 42rem; margin: var(--sp-6) auto; padding: var(--sp-5);
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow);
}
.standalone > * { margin: 0 0 var(--sp-3); }
.standalone > :last-child { margin-bottom: 0; }
.standalone h1 { font-size: var(--t-h1); letter-spacing: -.01em; }
.standalone h2 { font-size: var(--t-h3); margin-top: var(--sp-5); }
.standalone p { font-size: var(--t-body); color: var(--ink-2); }
.standalone strong { color: var(--ink); }
.standalone ul { font-size: var(--t-body); color: var(--ink-2); padding-left: 1.4rem; }
.standalone li { margin: var(--sp-1) 0; }
@media (max-width: 30rem) {
    .standalone { margin: var(--sp-3); padding: var(--sp-4); }
    .standalone h1 { font-size: var(--t-h2); }
}

/* ------------------------------------------------------------------------------------------------
   Page head - the comp's title block. "Everything below follows the controls" is the promise the
   pill row has to keep, so the sentence is rendered here rather than implied.
   ------------------------------------------------------------------------------------------------ */
.page-head { max-width: 46rem; margin: 0 0 var(--sp-5); }
.page-head h1 { font-size: var(--t-h1); margin: 0 0 .2rem; letter-spacing: -.01em; }
.lede { font-size: var(--t-lead); color: var(--ink-2); margin: 0; }

/* ------------------------------------------------------------------------------------------------
   The panel - one card shape for every block of the app that is not a chart card.

   Upload, the account strip, the privacy notice, the merge summary, the projection, the rules list
   and the table all wear it, which is the whole of what task 471 changed: they used to each draw
   their own currentColor box.
   ------------------------------------------------------------------------------------------------ */
.panel {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow); padding: var(--sp-4) var(--sp-5); margin: 0 0 var(--sp-4);
}
.panel > h2 { font-size: var(--t-h3); margin: 0 0 var(--sp-2); }
.panel > h3 { font-size: var(--t-body); margin: var(--sp-4) 0 var(--sp-1); font-weight: 600; }
.panel-narrow { max-width: 46rem; }

.sub, .note-text { color: var(--ink-2); font-size: var(--t-small); margin: var(--sp-1) 0 0; }

/* The stack of copy blocks above the board keeps a reading measure; the board and the table do not. */
.bc-main > .library-notice, .bc-main > .account-panel,
.bc-main > .privacy-notice, .bc-main > .upload-panel, .bc-main > #result,
.bc-main > .sample-link { max-width: 46rem; }

.library-notice {
    border-left: .25rem solid var(--s1); background: var(--s1-wash); color: var(--ink);
    border-radius: 0 var(--r-2) var(--r-2) 0; padding: .6rem .85rem; margin: 0 0 var(--sp-4);
    font-size: var(--t-body);
}

/* ------------------------------------------------------------------------------------------------
   Upload. The primary action of a first visit, so it is the one card on the page with the accent
   button in it and it sits directly under the page head.
   ------------------------------------------------------------------------------------------------ */
.upload-panel form { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; margin: 0; }
.upload-panel label {
    flex-basis: 100%; font-size: var(--t-small); font-weight: 600; color: var(--ink-2);
}
#result { margin: 0 0 var(--sp-4); font-size: var(--t-body); }
#result:empty { margin: 0; }

.sample-link { margin: 0 0 var(--sp-5); font-size: var(--t-small); color: var(--ink-2); }
.sample-link button { font-size: var(--t-small); padding: .3rem .8rem; margin-left: var(--sp-2); }

/* ------------------------------------------------------------------------------------------------
   The privacy notice. ABOVE THE FOLD and not behind a link - s 18 of POPIA, section 5 of the build
   spec. Quieter than the rest of the page on purpose: it is long, it is read once, and it must not
   compete with the file picker above it. PrivacyNoticeTests finds this block by the literal string
   `class="privacy-notice"` - DO NOT add a second class to that attribute.
   ------------------------------------------------------------------------------------------------ */
.privacy-notice {
    background: var(--wash); border: 1px solid var(--border); border-radius: var(--r-3);
    padding: var(--sp-4) var(--sp-5); margin: 0 0 var(--sp-5);
    font-size: var(--t-small); color: var(--ink-2);
}
.privacy-notice h2 {
    font-size: var(--t-micro); margin: 0 0 var(--sp-2); text-transform: uppercase;
    letter-spacing: .08em; color: var(--muted);
}
.privacy-notice p { margin: 0 0 var(--sp-2); }
.privacy-notice p:last-child { margin-bottom: 0; }
.privacy-notice strong { color: var(--ink); }

/* ------------------------------------------------------------------------------------------------
   The account strip - task 453. One line and a button until somebody presses it: the product works
   with no account at all, and a sign-up form competing with the file picker would say otherwise.
   Nothing here is fixed or sticky, because it must not follow the user down a table.
   ------------------------------------------------------------------------------------------------ */
.account-panel { margin: 0 0 var(--sp-4); }
.account-state {
    display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: 0;
    font-size: var(--t-body); color: var(--ink-2);
}
.account-state button { font-size: var(--t-small); padding: .3rem .8rem; }
/* Task 528 - the strip is two cards on /account now; the data row gets the same one-line shape. */
.account-rights {
    display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); align-items: center; margin: 0;
    font-size: var(--t-body);
}
.account-rights button { font-size: var(--t-small); padding: .3rem .8rem; }
#account-delete-form { margin-top: var(--sp-3); }
/* Task 528 - Account and Sign out, at the foot of the rail on a desktop. */
.nav-account { margin-top: var(--sp-2); }

#account-form, #account-password-form {
    display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2) var(--sp-3); align-items: center;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow); padding: var(--sp-4) var(--sp-5); margin-top: var(--sp-3);
    max-width: 34rem;
}
/* The full-width rows inside that two-column grid: the explanation, the buttons and the message.
   Spelled out rather than done with :nth-child, which would move when a field is added. */
#account-form .account-lede, #account-form button, #account-form .account-message,
#account-password-form .account-lede, #account-password-form button,
#account-password-form .account-message { grid-column: 1 / -1; }
#account-form label, #account-password-form label { font-size: var(--t-small); color: var(--ink-2); }
#account-form button, #account-password-form button { justify-self: start; }

.account-lede { margin: 0 0 var(--sp-1); font-size: var(--t-small); color: var(--ink-2); }
.account-message { margin: var(--sp-1) 0 0; font-size: var(--t-small); }
/* Colour is not the only signal - the message text itself says what went wrong - so this is an
   emphasis, not the information. */
.account-message.is-error { font-weight: 600; color: var(--critical); }

/* ------------------------------------------------------------------------------------------------
   THE BOARD - task 449's layout, on task 471's tokens. One pill row governs every card under it.
   ------------------------------------------------------------------------------------------------ */
.board { max-width: 76rem; margin: 0 0 var(--sp-5); }
.board > h2 { font-size: var(--t-h2); margin: 0 0 var(--sp-3); }

/* The pill row is the concept. It is a floating capsule, not a bordered strip, because it has to
   read as a control that sits ON the board rather than a section of it. */
.filters {
    display: flex; flex-wrap: wrap; gap: var(--sp-1); align-items: center;
    background: var(--surface); border: 1px solid var(--border); border-radius: 999px;
    box-shadow: var(--shadow); padding: 6px 8px;
}
.filters button {
    border: 0; background: none; border-radius: 999px; padding: .3rem .9rem;
    font-size: var(--t-small); color: var(--ink-2);
}
.filters button:hover { background: var(--wash); border-color: transparent; }
.filters button[aria-pressed="true"] {
    background: var(--s1); color: var(--s1-ink); font-weight: 600;
}
.filters button[aria-pressed="true"]:hover { background: var(--s1); }
.filters .sep { width: 1px; height: 20px; background: var(--border); margin: 0 var(--sp-1); flex: none; }
/* Task 570 - Include transfers: a tick box among the pills, on its default per account. */
.filters .filter-tick { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-small); color: var(--ink-2); margin-left: var(--sp-2); cursor: pointer; white-space: nowrap; }
.filters .filter-tick input { margin: 0; width: 16px; height: 16px; accent-color: var(--s1); }

/* Task 486 - the pill row moved OUT of the board and into the shared layout, above whichever page
   you are on, because period and account now have to mean the same thing on three screens. It keeps
   every one of its own styles above; this is only the strip that holds it and the category control
   beside it, which is a second capsule rather than a pill so that it does not read as a fourth
   period. Both wrap together on a narrow board. */
.page-filters {
    display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center;
    max-width: 76rem;
}
.filters-category { gap: var(--sp-2); padding: 6px 12px; }
.filters-category label {
    font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}
.filters-category select { border-radius: 999px; padding: .3rem .7rem; font-size: var(--t-small); }

/* Task 515 - the period capsule, first in the bar at the top of the page. Same shape as the category
   capsule; the date inputs and the Apply button only appear for a custom range. */
.page-filters { margin-bottom: var(--sp-2); }
/* Task 521 - the bar stays on screen while the page scrolls, so the period can be changed from the
   bottom of a long Transactions list (Johan, chat 553). The page scrolls on the window, so sticky
   works from here. Solid plane behind it and a hairline under it, or the rows show through as they
   pass beneath. --stick-top is the height of the phone nav, which is itself sticky below 60rem;
   board.js measures it, because that nav wraps and has no fixed height. */
.page-filters {
    position: sticky; top: var(--stick-top, 0px); z-index: 4;
    background: var(--plane); padding: var(--sp-2) 0; border-bottom: 1px solid var(--border);
}
.filters-period { gap: var(--sp-2); padding: 6px 12px; }
.filters-period label {
    font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}
.filters-period select, .filters-period input[type="date"] {
    border-radius: 999px; padding: .3rem .7rem; font-size: var(--t-small);
}
.filters-period button { background: var(--s1); color: var(--s1-ink); font-weight: 600; }
.filters-period button:hover { background: var(--s1); }
.period-problem { font-size: var(--t-small); color: var(--delta-bad, #b3261e); }
.period-problem:empty { display: none; }


.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-4); margin-bottom: var(--sp-4); }
.tile {
    display: grid; align-content: start; gap: .15rem; min-width: 0;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow); padding: var(--sp-3) var(--sp-4);
}
.tile .lbl { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.tile .val { font-size: 1.6rem; font-weight: 600; line-height: 1.15; letter-spacing: -.02em; overflow-wrap: anywhere; }
.tile .val-small { font-size: var(--t-lead); padding-top: .3rem; }
/* The arrow and the words carry the meaning; the colour only agrees with them. A red 3.6% with no
   glyph would be unreadable to a third of the people who will open this. */
.tile .delta { font-size: var(--t-small); color: var(--ink-2); }
.tile .delta.up { color: var(--critical); }
.tile .delta.down { color: var(--delta-good); }
.tile .delta.flat { color: var(--muted); }
.tile .delta.serious { color: var(--critical); font-weight: 600; }

.board-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-4); }
.card {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow); padding: var(--sp-4) var(--sp-4) var(--sp-3); min-width: 0;
}
.card.span-2 { grid-column: span 2; }
.card header { display: grid; gap: .1rem; margin-bottom: var(--sp-3); }
.card header h3 { font-size: var(--t-body); font-weight: 600; margin: 0; }
.card header .q { font-size: var(--t-micro); color: var(--muted); }
/* Says what it is. A placeholder chart on a page of real money is a lie waiting to be believed. */
.card-empty { margin: 0; font-size: var(--t-small); color: var(--muted); font-style: italic; }

/* ------------------------------------------------------------------------------------------------
   The charts. Inline SVG, hand-rolled, nothing from a CDN, because a third-party script on this
   page can read the whole statement (measure 7).

   ONE HUE. Categories are ranked by length and the colour means nothing, which is the only honest
   option at 21 categories against an eight-hue ceiling.
   ------------------------------------------------------------------------------------------------ */
.charts { margin: var(--sp-5) 0; }
.charts h2 { font-size: var(--t-h3); margin: 0; }
.charts h3 { font-size: var(--t-body); margin: var(--sp-4) 0 var(--sp-1); font-weight: 600; }
.chart-total { margin: 0 0 var(--sp-3); font-size: var(--t-small); color: var(--ink-2); }

.bars { list-style: none; margin: 0; padding: 0; }
.bars li { margin: 0; }

/* The whole row is the button, because the bar is the filter. */
.bar-row {
    display: grid; grid-template-columns: 11rem 1fr 6.5rem 3.5rem; gap: var(--sp-2); align-items: center;
    width: 100%; text-align: left; padding: .3rem .4rem; margin: .1rem 0;
    background: none; border: 1px solid transparent; border-radius: var(--r-1); color: inherit;
    font-size: var(--t-small);
}
.bar-row:hover, .bar-row:focus-visible { background: var(--wash); border-color: var(--border); }
.bar-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar { width: 100%; height: .7rem; }
.bar rect { fill: var(--s1); opacity: .8; }
.bar-row:hover .bar rect, .bar-row:focus-visible .bar rect { opacity: 1; }
.bar-value { text-align: right; font-variant-numeric: tabular-nums; }
.bar-share { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); font-size: var(--t-micro); }

.months { margin-top: var(--sp-1); }
.month-chart { width: 100%; height: 12rem; }
.month-chart rect { fill: var(--s1); opacity: .8; }
.month-chart text { fill: var(--ink-2); font-size: 12px; font-family: inherit; }

/* Task 452 - the projected cap on the month we are standing in. Hatched rather than shaded: a
   lighter version of the same fill reads as the same bar, and it disappears entirely for anyone who
   cannot separate the two tones. Texture survives both, and it survives a black-and-white print.
   The selector carries two classes so it beats `.month-chart rect` above - the fill CANNOT be set
   as an attribute in charts.js, because CSS outranks a presentation attribute and this rule would
   paint the forecast solid. */
.month-chart rect.projected { fill: url(#bc-projected-hatch); opacity: 1; }
.month-chart .hatch-ground { fill: var(--s1); opacity: .12; }
.month-chart .hatch-line { fill: var(--s1); opacity: .85; }

/* ------------------------------------------------------------------------------------------------
   Task 448 - MONTH AGAINST MONTH, and CATEGORY TREND. Rows are .bar-row buttons like the category
   bars, because they are the same filter. --s1 is the month picked, --s2 the month it is set
   against (task 484's meaning, nothing else); the --s2 bar is OUTLINED as well as orange so the two
   survive a monochrome screen.
   ------------------------------------------------------------------------------------------------ */
.compare-controls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
    margin: 0 0 var(--sp-2); font-size: var(--t-small); }
.compare-key { display: inline-block; width: .8rem; height: .8rem; border-radius: 2px; }
.compare-key.a { background: var(--s1); }
.compare-key.b { background: var(--s2-wash); border: 1.5px solid var(--s2); }
.compare-against { color: var(--muted); }
.compare-row { grid-template-columns: 11rem 1fr 6.5rem 6.5rem; }
.bar.pair { height: 1.3rem; }
.bar.pair rect.a { fill: var(--s1); opacity: .85; }
.bar.pair rect.b { fill: var(--s2-wash); stroke: var(--s2); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: 1; }
.compare .bar-share.up { color: var(--critical); }
.spark { width: 100%; height: 1.5rem; }
.spark rect { fill: var(--s1); opacity: .75; }
.spark rect.now { opacity: .35; }
.spark rect.gap { fill: var(--muted); opacity: .5; }
.bar-row:hover .spark rect, .bar-row:focus-visible .spark rect { opacity: 1; }

/* Task 564 - Compare to, on every Overview card. The comparison is --s2 and OUTLINED (bars) or
   DASHED (lines), never a second solid fill, so it reads as "the other period" on a monochrome
   screen too. Two-class selectors so they beat the cards' own `rect` fills. */
.spark rect.was { fill: var(--s2-wash); stroke: var(--s2); stroke-width: 1; vector-effect: non-scaling-stroke; opacity: 1; }
.month-chart rect.was { fill: var(--s2-wash); stroke: var(--s2); stroke-width: 1.5; opacity: 1; }
.balance-chart .month-bar.was { fill: var(--s2-wash); stroke: var(--s2); stroke-width: 1.5; opacity: 1; }
.balance-chart .was-line { fill: none; stroke: var(--s2); stroke-width: 1.75; stroke-dasharray: 6 4; stroke-linejoin: round; }
.balance-chart .was-label { fill: var(--s2); }
.compare-legend .compare-key { vertical-align: -1px; }
.bar-share .share-now, .bar-share .share-was { display: block; }
.bar-share .share-was.up { color: var(--critical); }
.tile .delta.vs { display: block; margin-top: 2px; }

/* ------------------------------------------------------------------------------------------------
   Task 484 - RUNNING BALANCE ACROSS THE MONTH. The comp's largest card, and the one Johan noticed
   was gone. The data reached TransactionRow in task 450 (RunningBalanceCents) and nothing had ever
   drawn it.

   ONE SERIES PER ACCOUNT AND NEVER A SUM. AccountBalanceSeries says why in prose and it is not a
   styling preference: where coverage differs between accounts - the normal case, because people add
   one statement at a time - a summed line is a cliff on every day the other account is missing, and
   a user reads a cliff as a real event.

   This month is solid --s1 over a washed area; LAST month is --s2 and DASHED. Dashed as well as
   orange, because a colour-only difference between "what happened" and "what happened before" is
   the one difference on this board that must survive a monochrome screen.
   ------------------------------------------------------------------------------------------------ */
.balance-account { margin: 0 0 var(--sp-4); }
.balance-account:last-child { margin-bottom: 0; }
.balance-account > h4 {
    margin: 0 0 var(--sp-1); font-size: var(--t-small); font-weight: 600; color: var(--ink-2);
}
/* height:auto, so the viewBox decides the aspect and the axis figures are never stretched. The
   narrow variant carries its own viewBox from charts.js and needs its type scaled with it. */
.balance-chart { width: 100%; height: auto; display: block; }
.balance-chart.narrow .axis-label,
.balance-chart.narrow .series-label,
.balance-chart.narrow .mark-tip { font-size: 17px; }
.balance-chart .grid-line { stroke: var(--grid); stroke-width: 1; }
/* Zero is not a gridline like the others - "how close to zero did I get" is the card's question. */
.balance-chart .zero-line { stroke: var(--critical); stroke-width: 1; stroke-dasharray: 2 3; opacity: .7; }
.balance-chart .axis-label { fill: var(--muted); font-size: 11px; font-family: inherit; }
.balance-chart .series-label { font-size: 11px; font-weight: 600; font-family: inherit; }
.balance-chart .now-area { fill: var(--s1); opacity: .10; }
.balance-chart .now-line { fill: none; stroke: var(--s1); stroke-width: 2; stroke-linejoin: round; }
.balance-chart .now-label { fill: var(--s1); }
/* Task 621 - the forecast on /projection: the same colour as the line it carries on, dotted (the
   comparison is the dashed --s2 one), so a forecast never reads as the bank's record. */
.balance-chart .ahead-line { fill: none; stroke: var(--s1); stroke-width: 2; stroke-dasharray: 2 4; stroke-linecap: round; opacity: .8; }
.balance-chart .ahead-label { fill: var(--s1); font-style: italic; }
/* Task 571 - a high and a low dot per month; amount and date in the tooltip, or a tap label on a
   phone. The comparison line's pair is hollow, in its own muted --s2. */
.balance-chart .month-mark { cursor: pointer; }
.balance-chart .mark-hit { fill: transparent; }
.balance-chart .low-dot { fill: var(--critical); }
.balance-chart .high-dot { fill: var(--s1); }
.balance-chart .month-mark:hover .low-dot,
.balance-chart .month-mark:hover .high-dot { stroke: var(--ink-2); stroke-width: 1.5; }
.balance-chart .was-low-dot,
.balance-chart .was-high-dot { fill: var(--surface, #fff); stroke: var(--s2); stroke-width: 1.5; }
.balance-chart .mark-tip { fill: var(--ink-2); font-size: 11px; font-weight: 600; font-family: inherit; pointer-events: none; }
.balance-chart .mark-tip.was-mark { fill: var(--s2); }
/* A bank that prints no running balance gets a sentence, not a flat line and never a line
   reconstructed by adding amounts to the opening figure - that would be our arithmetic presented as
   the bank's record. AccountBalanceSeries.Note is the same sentence on the server. */
.balance-none { margin: 0; font-size: var(--t-small); color: var(--muted); font-style: italic; }
/* Task 524 - the key is one line of words: the span drawn and the x-axis step picked for it. */
.balance-key { margin: 0 0 var(--sp-3); font-size: var(--t-micro); color: var(--ink-2); }
/* Task 552 - one category's figures on the same card: the Cumulative / Per month toggle, and the bars. */
.balance-mode { margin: 0 0 var(--sp-2); }
.balance-chart .month-bar { fill: var(--s1); opacity: .8; }

/* ------------------------------------------------------------------------------------------------
   The table panel and everything inside it.
   ------------------------------------------------------------------------------------------------ */
.table-panel { max-width: 76rem; margin: 0; padding: 0 0 8rem; }

/* Sample data says so on its face, everywhere, in every concept. Dashed and warning-coloured so it
   cannot be mistaken for a section of the product. */
.sample-notice {
    border: 2px dashed var(--warning); background: var(--warning-wash); color: var(--ink);
    border-radius: var(--r-3); padding: var(--sp-3) var(--sp-4); margin: 0 0 var(--sp-4);
    font-weight: 600; font-size: var(--t-body);
}

/* The merge panel - step 9, and the wedge. It sits ABOVE the charts because it is what makes the
   numbers in them correct: which periods the statements actually cover and where the holes are,
   what was already added and is counted once, and which movements are the household paying itself.
   A gap is marked with a rule down its left edge rather than a colour, so it survives dark mode,
   high contrast and a monochrome print with no palette to keep in step. */
.merge-panel {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow); padding: var(--sp-4) var(--sp-5); margin: 0 0 var(--sp-4);
}
.merge-panel h2 { font-size: var(--t-h3); margin: 0 0 var(--sp-2); }
.merge-panel h3 { font-size: var(--t-body); margin: var(--sp-4) 0 var(--sp-1); font-weight: 600; }
.merge-note { margin: var(--sp-1) 0 var(--sp-2); font-size: var(--t-small); color: var(--ink-2); }
.coverage, .merge-notes { list-style: none; margin: 0; padding: 0; font-size: var(--t-body); }
.coverage li, .merge-notes li { margin: var(--sp-1) 0; }
.coverage li.gap { border-left: 3px solid var(--warning); padding-left: var(--sp-2); font-weight: 600; }
.merge-notes { margin-top: var(--sp-2); color: var(--ink-2); font-size: var(--t-small); }
.transfer-pairs { list-style: none; margin: 0; padding: 0; }
.transfer-pairs li { margin: var(--sp-2) 0; }
.transfer-pairs label { display: flex; gap: var(--sp-2); align-items: baseline; }
.pair-detail { display: block; margin-left: 1.5rem; font-size: var(--t-small); color: var(--muted); }

/* The session went and the table below is less than the user thinks they added. Full weight and
   full colour, unlike the merge notes: it is the one line on this page that says a number is
   smaller than it should be. */
.session-warning {
    border-left: 3px solid var(--critical); padding-left: var(--sp-2);
    font-weight: 600; margin: var(--sp-2) 0;
}

/* Still to come - task 452. Dashed, everywhere, and that is the whole visual language of this
   panel: the page draws facts with solid borders and forecasts with broken ones, so the difference
   is visible before a single word is read. The hatched cap on the month chart is the same idea. */
.projection {
    background: var(--surface); border: 2px dashed var(--border); border-radius: var(--r-3);
    padding: var(--sp-4) var(--sp-5); margin: 0 0 var(--sp-4);
}
.projection h2 { font-size: var(--t-h3); margin: 0 0 var(--sp-2); }
.projection-headline {
    margin: 0; font-size: var(--t-lead); display: flex; flex-wrap: wrap;
    gap: var(--sp-3); align-items: baseline;
}
.projection-headline button { padding: .25rem .7rem; font-size: var(--t-small); }
.projection-actual { margin: var(--sp-1) 0 0; font-size: var(--t-body); color: var(--ink-2); }
.projection-note { margin: var(--sp-1) 0 var(--sp-2); font-size: var(--t-small); color: var(--muted); }

.projection-list { list-style: none; margin: var(--sp-2) 0 0; padding: 0; }
.projection-list li {
    display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: baseline;
    margin: var(--sp-1) 0; padding: var(--sp-2) 0; border-top: 1px dashed var(--border);
    font-size: var(--t-body);
}
.projection-list button { padding: .25rem .7rem; font-size: var(--t-small); margin-left: auto; }

.expected-date { min-width: 6.5rem; font-variant-numeric: tabular-nums; color: var(--ink-2); }
.expected-name { min-width: 11rem; font-weight: 600; }
.expected-where { min-width: 13rem; font-size: var(--t-small); color: var(--muted); }
.expected-amount { min-width: 6.5rem; font-variant-numeric: tabular-nums; text-align: right; }
/* The evidence, and on its own line at every width: it is the reason the user can disagree with
   the row, so it must not be the thing that gets cut off first. */
.expected-why { flex-basis: 100%; font-size: var(--t-small); color: var(--muted); }

/* The day has passed on the calendar but not in the data we hold. Marked, not hidden - the money
   is still coming out of the month, but "we expect this" and "we cannot see whether this already
   happened" are different sentences. */
.projection-list li.unconfirmed .expected-date { font-style: italic; }
.projection-list li.unconfirmed .expected-why { color: var(--warning); font-weight: 600; }
/* Task 603 - one heading per month of the selected period, and money coming in marked as such. */
.projection-list li.projection-month {
    justify-content: space-between; margin-top: var(--sp-3); border-top: 2px solid var(--border);
    font-weight: 700;
}
.projection-month-total { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink-2); }
.projection-list li.income .expected-amount { color: var(--success, #15803d); }

/* Other is a failure, not a category - the banner is meant to be the loudest thing on the page. */
.other-banner {
    background: var(--warning-wash); border: 1px solid var(--warning); border-left: 4px solid var(--warning);
    border-radius: var(--r-3); padding: var(--sp-3) var(--sp-4); margin: 0 0 var(--sp-4);
}
.other-banner p { margin: 0; font-weight: 600; font-size: var(--t-body); }
.other-banner button { margin-left: var(--sp-2); padding: .25rem .7rem; font-size: var(--t-small); }
.other-list { list-style: none; margin: var(--sp-2) 0 0; padding: 0; }
.other-list li { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: var(--sp-1) 0; }
.other-name { min-width: 11rem; }
.other-value { min-width: 11rem; font-variant-numeric: tabular-nums; color: var(--ink-2); font-size: var(--t-small); }

/* ------------------------------------------------------------------------------------------------
   The toolbar. It is the SECOND pill row on the page and it is deliberately the same object as the
   board's: the view tabs, the search and the category filter all govern the table under them the
   way the board's pills govern the cards. One idiom, two places, so the page has one control
   language rather than a dashboard bolted onto a form.
   ------------------------------------------------------------------------------------------------ */
.toolbar {
    display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center;
    background: var(--surface); border: 1px solid var(--border); border-radius: 999px;
    box-shadow: var(--shadow); padding: 6px 10px; margin: 0 0 var(--sp-3);
}
.toolbar label {
    font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}
.toolbar button { border-radius: 999px; padding: .3rem .9rem; font-size: var(--t-small); }
.toolbar select, .toolbar input[type="search"] { border-radius: 999px; padding: .3rem .7rem; font-size: var(--t-small); }

.view-toggle { display: flex; gap: var(--sp-1); }
.view-toggle button { border: 0; background: none; color: var(--ink-2); }
.view-toggle button:hover { background: var(--wash); }
.view-toggle button[aria-pressed="true"] {
    background: var(--s1); color: var(--s1-ink); font-weight: 600; text-decoration: none;
}
.view-toggle button[aria-pressed="true"]:hover { background: var(--s1); }

/* The chip shows WHAT YOU SELECTED BY, so the user can see the rule rather than trust it. */
.chip {
    display: inline-flex; gap: var(--sp-2); align-items: center;
    background: var(--s1-wash); border: 1px solid var(--border); color: var(--ink);
    border-radius: 999px; padding: .2rem .35rem .2rem .85rem; margin: 0 0 var(--sp-3);
    font-size: var(--t-small);
}
.chip button { padding: .2rem .7rem; font-size: var(--t-micro); border-radius: 999px; }

.changes {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow); padding: var(--sp-3) var(--sp-5); margin: 0 0 var(--sp-3);
    font-size: var(--t-body);
}
.changes li { margin: var(--sp-1) 0; }
.changes button { padding: .15rem .6rem; margin-left: var(--sp-2); font-size: var(--t-micro); border-radius: 999px; }

/* The user's own rules (kept in the database with their statements since task 513/534). Same shape as the
   changes list on purpose: it is read the same way and it is a list they can delete from. */
.rules {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow); padding: var(--sp-3) var(--sp-4); margin: 0 0 var(--sp-3);
}
.rules-note { margin: var(--sp-1) 0 var(--sp-2); font-size: var(--t-small); color: var(--ink-2); }
.rules-list { list-style: none; margin: 0 0 var(--sp-2); padding: 0; font-size: var(--t-body); }
.rules-list li { margin: var(--sp-1) 0; }
.rules-list button { padding: .15rem .6rem; margin-left: var(--sp-2); font-size: var(--t-micro); border-radius: 999px; }

/* Task 472 - the add-a-category form. Same row on a monitor, stacked on a phone, because the name
   box needs the width and a 390px viewport has none to give a four-column row. */
.category-add {
    display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: 0 0 var(--sp-2);
}
.category-add label { font-size: var(--t-small); color: var(--ink-2); }
.category-add input { min-width: 14rem; flex: 1 1 14rem; }
@media (max-width: 40rem) {
    .category-add { display: grid; grid-template-columns: 1fr; }
    .category-add input, .category-add select, .category-add button { width: 100%; }
}

.downloads {
    display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: var(--sp-4) 0 0;
}
.downloads span { font-size: var(--t-small); color: var(--ink-2); margin-right: var(--sp-1); }
.downloads button { padding: .3rem .8rem; font-size: var(--t-small); }

/* ------------------------------------------------------------------------------------------------
   The table itself. It lives in a surface card like every other block, because a bare table on the
   plane was the last thing on the page still reading as a wireframe.
   ------------------------------------------------------------------------------------------------ */
#transactions-view, #merchants-view {
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow); padding: var(--sp-2) var(--sp-4) var(--sp-3); overflow-x: auto;
}

.txn-table { width: 100%; border-collapse: collapse; font-size: var(--t-body); }
.txn-table caption {
    text-align: left; padding: var(--sp-2) 0; font-size: var(--t-small); color: var(--ink-2);
}
.txn-table th {
    text-align: left; padding: var(--sp-2) var(--sp-2); border-bottom: 1px solid var(--border);
    font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .06em;
    color: var(--muted); font-weight: 600; white-space: nowrap;
}
.txn-table td {
    text-align: left; padding: .4rem var(--sp-2); border-bottom: 1px solid var(--grid);
    vertical-align: top;
}
.txn-table tbody tr:last-child td { border-bottom: 0; }
.txn-table tbody tr:hover { background: var(--wash); }
.txn-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.txn-table .date { white-space: nowrap; color: var(--ink-2); }
.txn-table .out { font-variant-numeric: tabular-nums; }
.txn-table tr.selected { background: var(--s1-wash); }
/* Task 525 - the ONE row the side panel shows. An outline, so it still reads on a ticked (.selected)
   row and on the phone's card layout, where the row is a block. */
.txn-table tbody tr.active, .txn-table tbody tr.active:hover {
    background: var(--s1-wash); outline: 2px solid var(--s1); outline-offset: -2px;
}
.txn-table select { font-size: var(--t-small); padding: .2rem .4rem; max-width: 100%; }
/* Task 470 - a description the PDF's font would not decode. Italic and dimmed so it cannot be
   mistaken for the bank's own words; NOT hidden, because the date and the amount are still right
   and the row still has to be categorised by hand. */
.txn-table .unreadable { font-style: italic; color: var(--muted); }
.mixed { font-size: var(--t-micro); color: var(--muted); }

/* Sticky, because a button at the bottom of a 360-row table loses the selection on scroll. */
.action-bar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 2;
    display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; justify-content: center;
    padding: var(--sp-3) var(--sp-4); border-top: 1px solid var(--border);
    background: var(--surface); box-shadow: 0 -4px 16px rgba(16, 24, 40, .08);
    font-size: var(--t-body);
}
.action-bar label { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }

.toast {
    position: fixed; left: 50%; transform: translateX(-50%); bottom: 5.5rem; z-index: 3;
    display: flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap;
    background: var(--raised); border: 1px solid var(--border); border-radius: var(--r-3);
    box-shadow: var(--shadow); padding: var(--sp-2) var(--sp-4); font-size: var(--t-body);
    max-width: calc(100vw - 2rem);
}
/* The "always categorise this shop as that" checkbox, on the toast. It wraps under the message on
   a phone rather than pushing Undo off the edge. */
.toast .remember { display: inline-flex; gap: var(--sp-1); align-items: center; font-size: var(--t-small); }

/* Task 508 - the transaction side panel. Fixed to the right edge so the table under it keeps its
   scroll position; full width on a phone, where the right edge is the whole screen. */
.txn-table tr.openable { cursor: pointer; }
.row-panel {
    position: fixed; top: 0; right: 0; bottom: 0; z-index: 6; width: min(24rem, 100vw);
    overflow-y: auto; padding: var(--sp-4); background: var(--surface);
    border-left: 1px solid var(--border); box-shadow: var(--shadow); font-size: var(--t-body);
}
.row-panel h2 { font-size: var(--t-body); margin: var(--sp-2) 0; overflow-wrap: anywhere; }
.row-panel h3 { font-size: var(--t-small); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 0 0 var(--sp-2); }
.row-panel-meta { color: var(--muted); font-size: var(--t-small); margin: 0; }
.row-panel-close { position: absolute; top: var(--sp-2); right: var(--sp-2); font-size: 1.25rem; line-height: 1; }
.row-panel-rule { margin-top: var(--sp-3); padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-3); background: var(--wash); }
.row-panel-rule label { display: block; margin-top: var(--sp-3); font-size: var(--t-small); color: var(--muted); }
.row-panel-rule select { width: 100%; }
.row-panel-match { margin: 0; overflow-wrap: anywhere; }
.row-panel-count { font-weight: 600; margin: var(--sp-2) 0 0; }
.row-panel-note { margin: 0; }
.row-panel-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-3) 0; }

/* =================================================================================================
   Widths.

   60rem - the rail lies down and becomes the scope strip.
   48rem - the table becomes one card per transaction.
   30rem - PHONE. 390px is 24.4rem, so this block is what Johan sees on his phone.
   ================================================================================================= */

@media (max-width: 60rem) {
    /* The scope strip. Same list, laid on its side, still visible. */
    /* Task 528 - rows auto/1fr: on a short page the 100vh minimum was shared between both rows and
       the sticky rail was stretched to ~380px of empty navy. The spare height belongs to the page. */
    .app { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
    /* Task 528 - min-width: 0, or the grid cell grows to the rail rows' natural width (~875px) and
       the side-scroll never happens: a phone got an 887px layout viewport and zoomed the page out. */
    .bc-nav { min-width: 0; }
    .bc-nav {
        flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
        padding: var(--sp-2) var(--sp-3); position: sticky; top: 0; z-index: 5;
        box-shadow: 0 2px 10px rgba(0, 0, 0, .25);
        /* Task 498 - stacked: no fixed height, no own scroll; the strip is as tall as its content. */
        height: auto; overflow-y: visible; overscroll-behavior: auto;
    }
    .bc-nav .brand small { display: none; }
    .nav-sections { grid-auto-flow: column; gap: .2rem; }
    .nav-heading { display: none; }
    .nav-group { display: none; }   /* task 728: the strip stays one run of links */
    .nav-accounts {
        grid-auto-flow: column; grid-auto-columns: minmax(10rem, max-content);
        overflow-x: auto; flex: 1 1 100%; padding-bottom: .2rem;
    }
    .nav-foot { display: none; }
    /* Task 484. THE STATEMENT LIST GOES at this width and that is deliberate: the rail is STICKY
       here, so every row in it is a permanent tax on the viewport, and task 471 spent the whole of
       390 getting this strip down to ~110px. Three statements per account would put it straight
       back over 200. The account head stays, so the active scope - the one thing the strip exists
       to show - is still on screen, and the same coverage is in "What we read" on the board. */
    .stmts { display: none; }
    .bc-main { padding: var(--sp-4) var(--sp-4) 4rem; }
    .kpis { grid-template-columns: repeat(2, 1fr); }
    .board-cards { grid-template-columns: 1fr; }
    .card.span-2 { grid-column: span 1; }
}

@media (max-width: 48rem) {
    /* One card per transaction: the header row is meaningless at this width. */
    .txn-table thead { display: none; }
    .txn-table, .txn-table tbody, .txn-table tr, .txn-table td { display: block; width: 100%; }
    .txn-table tbody tr {
        border: 1px solid var(--border); border-radius: var(--r-2);
        padding: var(--sp-2); margin-bottom: var(--sp-2); background: var(--surface);
    }
    .txn-table tbody tr:last-child td { border-bottom: 0; }
    .txn-table td { border: 0; padding: .1rem .25rem; }
    .txn-table .num { text-align: left; font-size: var(--t-lead); font-weight: 600; }
    /* A caption inside a table that has been turned into blocks keeps `display: table-caption`, and
       a table-caption in a block box shrinks to fit its widest word - "19 transactions, newest
       first" came out as three lines in a 40mm column. Found at 390, not guessed. */
    .txn-table caption { display: block; }
    #transactions-view, #merchants-view { padding: var(--sp-2); border: 0; box-shadow: none; background: none; }
    .action-bar { justify-content: space-between; }

    /* The bar rows lose their columns: an 11rem label and a 6.5rem amount do not fit beside a bar
       on a phone, and the bar is the part worth keeping. */
    .bar-row { grid-template-columns: 1fr auto; grid-template-areas: "name value" "bar share"; }
    .bar-name { grid-area: name; }
    .bar { grid-area: bar; }
    .bar-value { grid-area: value; }
    .bar-share { grid-area: share; }
    .other-name, .other-value { min-width: 0; }

    #account-form, #account-password-form { grid-template-columns: 1fr; }
    .panel, .merge-panel, .projection, .privacy-notice, .changes,
    #account-form, #account-password-form { padding: var(--sp-3) var(--sp-4); }
}

/* PHONE - 390px. Task 471 puts this width in scope from the start rather than as a follow-up,
   because the comp was judged at both widths and shipping desktop-only would break the thing Johan
   approved. Everything here was found by opening the real page at 390, not by guessing.

   The rail STILL does not become a hamburger. The brand takes its own line so the three section
   links get the full width, and both rows scroll sideways rather than wrapping into a stack that
   pushes the board below the fold. */
@media (max-width: 30rem) {
    /* THE RAIL IS STICKY, SO ITS HEIGHT IS A TAX ON EVERY SCREENFUL. At 390 the first cut of this
       cost 180px of an 844px viewport - a fifth of the phone, permanently, for a brand mark and two
       account names. Everything below shrinks it to roughly 110px without hiding the active scope,
       which is the one thing this strip exists to do. Measured at 390, not guessed. */
    .bc-nav { gap: var(--sp-1); padding: var(--sp-2) var(--sp-3); }
    .bc-nav .brand { flex: 1 0 100%; font-size: var(--t-small); gap: var(--sp-2); }
    .bc-nav .brand .dot { width: 1.4rem; height: 1.4rem; font-size: var(--t-micro); border-radius: var(--r-1); }
    .nav-sections {
        flex: 1 1 100%; grid-auto-flow: column; grid-auto-columns: max-content;
        overflow-x: auto; gap: .15rem;
    }
    .nav-item { padding: .25rem .45rem; font-size: var(--t-small); }
    .nav-accounts { grid-auto-columns: minmax(8.5rem, max-content); gap: var(--sp-2); }
    .nav-accounts li { padding: .3rem .5rem; }
    /* One line each, clipped. A wrapped account name is three lines of sticky header. */
    .acct-name, .acct-sub {
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis; overflow-wrap: normal;
    }
    .acct-name { font-size: var(--t-small); }

    .bc-main { padding: var(--sp-3) var(--sp-3) 4rem; }
    .page-head { margin-bottom: var(--sp-4); }
    .page-head h1 { font-size: var(--t-h2); }
    .lede { font-size: var(--t-body); }

    /* Two tiles across, not one. Four full-width tiles is four screenfuls of KPI before the first
       chart, and the numbers fit: R38 500.00 at 1.25rem is 110px in a 150px content box. */
    .kpis { grid-template-columns: repeat(2, 1fr); gap: var(--sp-2); }
    .tile { padding: var(--sp-2) var(--sp-3); }
    .tile .val { font-size: 1.25rem; }
    .tile .val-small { font-size: var(--t-body); }
    .tile .delta { font-size: var(--t-micro); }
    .board-cards { gap: var(--sp-3); }
    .card { padding: var(--sp-3); }

    /* Both pill rows stop being capsules and become rounded blocks: a 999px radius round a control
       that has wrapped to four lines reads as a mistake, and the row is far too tall to float. */
    .filters, .toolbar { border-radius: var(--r-3); padding: var(--sp-2); }
    .filters button, .toolbar button { padding: .3rem .7rem; }

    /* Task 486. At 390 the two capsules stack rather than sitting side by side, and the category
       select takes the width it needs - a <select> squeezed to 90px truncates "Water & electricity"
       to "Water & e...", which is the one control on this strip whose value is a word. */
    /* Task 521 replaces the stack: a sticky bar two capsules deep ate a third of a 390 screen. It is
       one line now that scrolls sideways, and every capsule keeps its natural width, so the select
       still is not squeezed - it is reached with a swipe instead. */
    .page-filters { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
    .page-filters > * { flex: none; }
    .filters-period { flex-wrap: nowrap; }
    .filters-category { padding: var(--sp-2); }
    .filters-category select { flex: 1 1 auto; min-width: 0; }
    .toolbar { gap: var(--sp-1) var(--sp-2); }
    .toolbar label { flex-basis: 100%; }
    .toolbar input[type="search"], .toolbar select { flex: 1 1 100%; min-width: 0; }
    .view-toggle { flex: 1 1 100%; }
    .view-toggle button { flex: 1 1 auto; }

    /* The projection rows were laid out on desktop minimums that do not fit 358px of content box;
       they become two lines per expected debit rather than one line that overflows. */
    .expected-date, .expected-name, .expected-amount { min-width: 0; }
    .expected-where { min-width: 0; flex-basis: 100%; }
    .expected-amount { margin-left: auto; }
    .projection-list button { flex-basis: 100%; margin-left: 0; }

    .upload-panel form { gap: var(--sp-2); }
    .upload-panel form button[type="submit"] { flex: 1 1 100%; }
    .account-state { font-size: var(--t-small); }

    /* The sticky bar is a phone's whole bottom edge. Two rows - what is selected, then what to do
       with it - rather than a single row that scrolls its own Set-category button out of reach. */
    .action-bar { padding: var(--sp-2) var(--sp-3); gap: var(--sp-1) var(--sp-2); }
    #selection-count { flex: 1 1 100%; font-size: var(--t-small); }
    #bulk-category { flex: 1 1 auto; min-width: 0; }
    .toast { left: var(--sp-2); right: var(--sp-2); transform: none; bottom: 7rem; }
}

/* ------------------------------------------------------------------------------------------------
   TASK 485 - the privacy notice: a rail link and a first-visit gate.

   Johan answered question 86 in free text: the link goes in the LEFT-HAND NAV, and the notice pops
   on a first visit with an Accept button. The notice used to be a block above the board; the block
   is gone and the wording is untouched.
   ------------------------------------------------------------------------------------------------ */

/* The rail row that is a BUTTON rather than a link. Everything .nav-item does not set - the border,
   the surface fill, the cursor - comes from the global button rule and would put a white box in the
   navy rail, so it is unset here rather than by giving the markup a second class. */
button.nav-item {
    background: none; border: 0; width: 100%; font: inherit; font-size: var(--t-body);
    text-align: left;
}
button.nav-item:hover { background: var(--navy-wash); border: 0; }

/* WIDTH IS min() AGAINST 100vw, NOT A PERCENTAGE, AND THAT IS NOT PEDANTRY. The first version was
   `width: calc(100% - 2rem)`, and at 390 the dialog came out wider than the phone - the percentage
   does not resolve against the visual viewport the way a fixed-position element's does once the
   document behind it is scrolling sideways, so the notice ran off the right edge mid-word. Measured
   on the real phone frame, not reasoned about: the right-hand 90px of the 780px capture were dialog
   where they should have been backdrop. 44rem still wins on a desktop, where the viewport is wider
   than the card. */
.privacy-modal {
    width: min(44rem, calc(100vw - 2rem)); max-height: 85vh; overflow: auto;
    padding: var(--sp-5); border: 1px solid var(--border); border-radius: var(--r-3);
    background: var(--surface); color: var(--ink); box-shadow: var(--shadow);
}
.privacy-modal::backdrop { background: rgba(16, 24, 40, .55); }
/* Inside the dialog the notice IS the dialog - the second frame around it was a box in a box. */
.privacy-modal .privacy-notice {
    background: none; border: 0; padding: 0; margin: 0; font-size: var(--t-body);
}
.privacy-modal .privacy-notice h2 {
    font-size: var(--t-h3); text-transform: none; letter-spacing: normal; color: var(--ink);
    margin-bottom: var(--sp-3);
}
.privacy-modal-actions {
    display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center;
    margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--border);
    /* Sticky, because the notice is longer than a phone screen and an Accept button below eleven
       paragraphs is an Accept button nobody can find. */
    position: sticky; bottom: calc(var(--sp-5) * -1); background: var(--surface);
    padding-bottom: var(--sp-2);
}
.privacy-modal-note { flex: 1 1 14rem; margin: 0; font-size: var(--t-small); color: var(--ink-2); }
#privacy-accept {
    background: var(--s1); color: var(--s1-ink); border-color: var(--s1); font-weight: 600;
}
#privacy-accept:hover { background: color-mix(in srgb, var(--s1) 85%, #000); border-color: transparent; }
.notice-more { margin-top: var(--sp-3); }

/* ------------------------------------------------------------------------------------------------
   TASK 474 - the privacy policy and the terms, as pages. Same .standalone card as the unsupported
   -bank page; what is added is what a legal document has and the rest of the app does not.
   ------------------------------------------------------------------------------------------------ */
.legal { max-width: 46rem; }
.legal h2 { font-size: var(--t-body); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.legal blockquote {
    margin: var(--sp-3) 0; padding: var(--sp-3) var(--sp-4);
    border-left: 3px solid var(--warning); background: var(--warning-wash); border-radius: var(--r-1);
}
.legal blockquote p { margin: 0; }
.legal-back { font-size: var(--t-small); }
.legal-updated { font-size: var(--t-small); color: var(--muted); }

/* THE BLANKS JOHAN HAS TO FILL. Deliberately loud: a legal page that ships with [REGISTERED ENTITY
   NAME] in body type reads as finished, and this one must not. It is <mark>, so it is also announced
   by a screen reader rather than being a colour nobody can hear. */
mark.blank {
    background: var(--warning-wash); color: var(--warning); border: 1px dashed var(--warning);
    border-radius: var(--r-1); padding: 0 .3em; font-weight: 600;
}

@media (max-width: 60rem) {
    /* The strip is STICKY at this width and task 471 spent the whole of 390 getting it to ~110px.
       The notice itself stays - Johan's answer was that it is permanently reachable - and the two
       long-form links go, because both are linked from inside the notice that button opens. */
    .nav-legal a.nav-item { display: none; }
    /* Task 528 - Account and Sign out ride on the brand's row, top right, so the sticky strip gains
       no row for them. The brand gives up its full width to make room. */
    .bc-nav .brand { order: -2; flex: 1 1 auto; }
    .nav-account { order: -1; margin: 0; flex: none; }
    .privacy-modal { padding: var(--sp-4); max-height: 92vh; }
    .privacy-modal-actions { bottom: calc(var(--sp-4) * -1); }
}
/* Task 683 - ON A PHONE THE NOTICE IS A BOTTOM SHEET, not a card over the whole screen. At 92vh it
   hid the page it was gating (747 of 812px at 375). Anchored to the bottom edge, full width, capped
   at 60vh: the page stays visible above it, the notice scrolls inside it, and the sticky Accept row
   is always on screen. Inset 0 left/right rather than a width, so nothing can push it past the edge. */
@media (max-width: 40rem) {
    .privacy-modal {
        inset: auto 0 0 0; margin: 0; width: auto; max-width: none; max-height: 60vh;
        border-radius: var(--r-3) var(--r-3) 0 0; border-bottom: 0;
        padding: var(--sp-4) var(--sp-4) calc(var(--sp-2) + env(safe-area-inset-bottom, 0px));
    }
    .privacy-modal .privacy-notice h2 { margin-bottom: var(--sp-2); }
    .privacy-modal-actions {
        bottom: calc((var(--sp-2) + env(safe-area-inset-bottom, 0px)) * -1);
        margin-top: var(--sp-3); padding-top: var(--sp-3); gap: var(--sp-2);
    }
    /* Task 694 - the people & businesses edit panel is a form, not a notice: full screen on a phone. */
    .privacy-modal.entity-edit-dialog {
        inset: 0; width: 100%; max-width: none; height: 100%; max-height: none; border-radius: 0; border: 0;
    }
    .privacy-modal.entity-edit-dialog .privacy-modal-actions { position: sticky; bottom: calc((var(--sp-2) + env(safe-area-inset-bottom, 0px)) * -1); }
}
/* Task 528 - a rail BUTTON (Privacy notice, Sign out) is the same size as a rail link on a phone.
   button.nav-item above outranks the phone block's .nav-item, so it is restated after it. */
@media (max-width: 30rem) {
    button.nav-item { font-size: var(--t-small); }
}

/* Task 491 - the CSV mapping preview. Drawn from the same tokens as every other card. */
.csv-preview { margin: .75rem 0; padding: .75rem 1rem; border: 1px solid var(--border); border-radius: var(--r-3); background: var(--surface); }
.csv-preview table { border-collapse: collapse; margin: .5rem 0; font-size: .9rem; }
.csv-preview caption { text-align: left; font-weight: 600; padding-bottom: .25rem; }
.csv-preview th, .csv-preview td { text-align: left; padding: .2rem .75rem .2rem 0; border-bottom: 1px solid var(--border); }
.csv-preview .csv-confirm { margin-top: .5rem; }

/* ---------------------------------------------------------------------------------------------
   Task 495 - the Uncategorised triage page. Lines on the left, categories on the right, both as
   plain buttons on the same surface tokens as everything else.
   --------------------------------------------------------------------------------------------- */
.triage-status { min-height: 1.4em; margin: 0 0 var(--sp-3); font-size: var(--t-small); color: var(--ink-2); }
/* Task 523 - LINES LEFT, PICKER RIGHT, placed by grid AREA so the markup's order cannot flip it and
   no page-level modifier can either (task 515's .triage-grid-picker-left did exactly that; deleted). */
.triage-grid { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); grid-template-areas: "rows side";
    gap: var(--sp-4); align-items: start; }
.triage-grid > .triage-rows { grid-area: rows; }
.triage-grid > .triage-side { grid-area: side; }
.triage-count { color: var(--muted); font-weight: 400; }
/* Task 543 (Johan) - the list scrolls WITH THE PAGE: no fixed height, no scroll box of its own. */
.triage-list { list-style: none; margin: 0; padding: 0; }
.triage-list li + li { border-top: 1px solid var(--grid); }
.triage-row { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr) auto; grid-template-areas: "date desc amt" "date acct amt";
    gap: 0 var(--sp-3); width: 100%; padding: var(--sp-2) var(--sp-2); text-align: left; background: none; border: 0;
    border-radius: var(--r-1); color: var(--ink); font: inherit; cursor: pointer; }
.triage-row:hover { background: var(--wash); }
.triage-row[aria-pressed="true"], .triage-row[aria-pressed="true"]:hover { background: var(--s1-wash); box-shadow: inset 3px 0 0 var(--s1); outline: 2px solid var(--s1); outline-offset: -2px; }
.triage-date { grid-area: date; font-size: var(--t-small); color: var(--muted); font-variant-numeric: tabular-nums; }
.triage-desc { grid-area: desc; overflow-wrap: anywhere; }
.triage-acct { grid-area: acct; font-size: var(--t-micro); color: var(--muted); }
.triage-amt { grid-area: amt; font-variant-numeric: tabular-nums; white-space: nowrap; }
.triage-amt.in { color: var(--delta-good); }
.triage-side { position: sticky; top: var(--sp-4); }
.triage-picked { padding-bottom: var(--sp-3); margin-bottom: var(--sp-3); border-bottom: 1px solid var(--grid); }
.triage-picked-line { margin: 0 0 var(--sp-2); overflow-wrap: anywhere; }
.triage-picked-line span { color: var(--muted); font-size: var(--t-small); }
.triage-buttons { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.triage-pick { padding: .35rem .75rem; border: 1px solid var(--border); border-radius: 999px; background: var(--surface);
    color: var(--ink); font: inherit; font-size: var(--t-small); cursor: pointer; }
.triage-pick:hover { border-color: var(--s1); }
.triage-pick.suggested { background: var(--s1-wash); border-color: var(--s1); display: inline-grid; text-align: left; }
.triage-pick.suggested small { color: var(--muted); font-size: var(--t-micro); }
/* Task 515 - a filter box over the picker, A-Z in columns, a flag per category. */
.triage-filter { width: 100%; box-sizing: border-box; padding: .45rem .8rem; border: 1px solid var(--border);
    border-radius: 999px; font-size: var(--t-small); margin: 0 0 var(--sp-2); }
.triage-legend { margin: 0 0 var(--sp-3); font-size: var(--t-micro); color: var(--muted); }
.triage-cats { columns: 2 11rem; column-gap: var(--sp-3); }
.triage-cat { display: flex; align-items: stretch; gap: 2px; margin: 0 0 var(--sp-1); break-inside: avoid; }
.triage-cat[hidden] { display: none; }
.triage-cat .triage-pick { flex: 1 1 auto; min-width: 0; text-align: left; border-radius: 999px 0 0 999px;
    display: inline-grid; }
.triage-cat .triage-pick small { color: var(--muted); font-size: var(--t-micro); }
.triage-cat.suggested .triage-pick { background: var(--s1-wash); border-color: var(--s1); }
.triage-rule { flex: none; padding: .35rem .6rem; border: 1px solid var(--border); border-radius: 0 999px 999px 0;
    background: var(--surface); color: var(--ink-2); cursor: pointer; }
.triage-rule:hover, .triage-rule:focus-visible { border-color: var(--s1); background: var(--s1-wash); color: var(--ink); }
.triage-rule-ico { font-weight: 600; }
.triage-rules { margin-top: var(--sp-4); }
.triage-rule-list { list-style: none; margin: 0; padding: 0; }
.triage-rule-list li { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: baseline; padding: var(--sp-2) 0; border-top: 1px solid var(--grid); }
.triage-rule-key { font-family: ui-monospace, Consolas, monospace; font-size: var(--t-small); }
.triage-rule-list small { color: var(--muted); }
.triage-rule-list button { margin-left: auto; }
@media (max-width: 60rem) {
    /* Stacked, the lines still come first - the phone's "left" is "above". */
    .triage-grid { grid-template-columns: 1fr; grid-template-areas: "rows" "side"; }
    .triage-side { position: static; }
}

/* Task 497 - "Start again". Set apart from the table's own controls, at the foot of the panel, so the
   one button on the page that deletes things is never next to the ones that only move them. */
.danger-zone {
    margin-top: var(--sp-5); padding: var(--sp-4); border: 1px solid #d92d20; border-radius: var(--r-3);
    background: var(--surface);
}
.danger-zone h2 { font-size: var(--t-h3); margin: 0 0 var(--sp-2); }
button.danger { background: #d92d20; color: #fff; border-color: #d92d20; font-weight: 600; }
button.danger:hover { background: #b42318; border-color: #b42318; }
button.danger:disabled { opacity: .45; cursor: not-allowed; }
.clear-dialog label { display: block; margin-top: var(--sp-3); }
.clear-dialog input[type="text"] { width: 100%; max-width: 16rem; font: inherit; }
/* Task 690 - Delete my data now! and Delete on a person or business. */
.clear-dialog .account-delete-close { display: flex; gap: var(--sp-2); align-items: center; font-weight: 600; }
.clear-dialog .account-delete-close:has(input:disabled) { opacity: .6; }
#account-delete-close-row .privacy-modal-note { display: block; margin-top: var(--sp-1); }
#account-delete-form .account-message { margin-top: var(--sp-2); }
.entity-delete { color: var(--critical); }
#entity-delete-goes, #entity-delete-kept { margin: 0 0 var(--sp-3); padding-left: 1.2rem; }

/* Task 499 - the shared pager (pager.js), under /transactions and /uncategorised. */
.pager { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin: var(--sp-2) 0 0; font-size: var(--t-small); }
.pager[hidden] { display: none; }
.pager-status { color: var(--muted); font-variant-numeric: tabular-nums; margin-right: auto; }
.pager button { border-radius: 999px; padding: .3rem .9rem; font-size: var(--t-small); }
.pager-jump input { width: 4.5rem; border-radius: 999px; padding: .25rem .6rem; font-size: var(--t-small); }

/* Task 500 - /add-statement: one line per file, no transaction rows. */
.import-summary { margin-top: 1rem; }
.import-summary-head { font-weight: 600; }
.import-summary-table { width: 100%; border-collapse: collapse; background: var(--surface);
    border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
.import-summary-table th, .import-summary-table td { text-align: left; padding: .55rem .75rem;
    border-bottom: 1px solid var(--border); vertical-align: top; }
.import-summary-table th { color: var(--muted); font-weight: 600; font-size: .85rem; }
.import-summary-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.import-summary-table tr.import-failed td:nth-child(2) { color: #b42318; font-weight: 600; }
.import-summary-table tr.import-ok td:nth-child(2) { color: #067647; font-weight: 600; }
.import-preview-file { font-weight: 600; margin-top: 1rem; }
.import-links { display: flex; gap: .75rem; flex-wrap: wrap; margin-top: 1rem; }
.import-link { display: inline-block; padding: .45rem .9rem; border-radius: 999px;
    border: 1px solid var(--border); background: var(--surface); color: var(--s1);
    text-decoration: none; font-weight: 600; }
@media (prefers-color-scheme: dark) {
    .import-summary-table tr.import-failed td:nth-child(2) { color: #f97066; }
    .import-summary-table tr.import-ok td:nth-child(2) { color: #47cd89; }
}

/* Task 502 slice 2 - People & businesses, and the booking-role picker on a cross-entity pair. */
.entity-list, .entity-accounts { list-style: none; margin: 0; padding: 0; }
.entity-row, .entity-account { border-top: 1px solid var(--border); padding: .6rem 0; }
.entity-form, .entity-add, .entity-account { display: flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: center; }
.entity-add { margin-top: 1rem; }
.entity-add h3 { flex-basis: 100%; margin: 0; }
.entity-default { color: var(--muted); font-size: .85em; }
/* Task 694 (Johan, chat 988) - people & businesses as a plain read-only table, edited in a panel. */
.entity-table-wrap { overflow-x: auto; }
.entity-table { width: 100%; border-collapse: collapse; font-size: var(--t-small, 13px); }
.entity-table th, .entity-table td { text-align: left; padding: .45rem .6rem; border-bottom: 1px solid var(--border); vertical-align: top; }
.entity-table th { font-weight: 600; color: var(--muted); white-space: nowrap; }
.entity-table .entity-name { font-weight: 600; }
.entity-table .entity-name .entity-default { display: block; font-weight: 400; }
.entity-table .entity-actions { white-space: nowrap; text-align: right; }
.entity-table .entity-actions button + button { margin-left: .4rem; }
.entity-table .mw-cap-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.entity-edit-dialog { width: min(48rem, calc(100vw - 2rem)); }
.entity-edit-dialog .privacy-modal-actions { position: static; }
.entity-edit-error { color: var(--critical); }
@media (max-width: 40rem) {
    /* On a phone each row is a card of caption: value lines - eleven columns do not fit across. */
    .entity-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
    .entity-table, .entity-table tbody, .entity-table tr, .entity-table td { display: block; width: auto; }
    .entity-table tr { border-bottom: 1px solid var(--border); padding: .4rem 0; }
    .entity-table td { border: 0; padding: .15rem 0; display: flex; gap: .75rem; justify-content: space-between; }
    .entity-table td[data-label]::before { content: attr(data-label); color: var(--muted); }
    .entity-table .entity-actions { justify-content: flex-start; padding-top: .4rem; }
}
.entity-account-label { flex: 1 1 16rem; }
.pair-role { display: block; margin-top: .35rem; max-width: 100%; }

/* Task 503 - the Rules page. The add form is one line on a monitor and stacks on a phone; the rule
   list is a real table because it is read across (match -> category -> count), and the lines a rule
   sets reuse the triage page's list. */
.rulebook-add-fields { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: end; }
.rulebook-add-fields label { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--t-small); color: var(--muted); }
.rulebook-add-fields input[type="text"] { min-width: 16rem; }
.rulebook-hint { color: var(--muted); font-size: var(--t-small); margin: var(--sp-2) 0 0; }
.rulebook-table { width: 100%; border-collapse: collapse; }
.rulebook-table th { text-align: left; font-size: var(--t-small); color: var(--muted); font-weight: 600; padding: var(--sp-1) var(--sp-2); }
.rulebook-table td { padding: var(--sp-2); border-top: 1px solid var(--grid); vertical-align: middle; }
/* Task 600 - an amount never breaks at its thousands spaces: "R2 397" over "498.72" read as a missing figure. */
.rulebook-table .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.rulebook-table tr[aria-selected="true"] td { background: var(--s1-wash); }
.rulebook-key { font-family: ui-monospace, Consolas, monospace; font-size: var(--t-small); background: none; border: 0;
    padding: 0; color: var(--s1); text-decoration: underline; cursor: pointer; text-align: left; }
.rulebook-kind { color: var(--muted); font-size: var(--t-small); margin-right: var(--sp-1); }
.rulebook-actions { display: flex; gap: var(--sp-2); justify-content: flex-end; flex-wrap: wrap; }
.rulebook-actions input[type="text"] { min-width: 12rem; }
@media (max-width: 60rem) {
    .rulebook-add-fields input[type="text"] { min-width: 0; width: 100%; }
    .rulebook-add-fields label { flex: 1 1 100%; }
    .rulebook-table thead { display: none; }
    .rulebook-table tr { display: grid; grid-template-columns: 1fr auto; border-top: 1px solid var(--grid); padding: var(--sp-2) 0; }
    .rulebook-table td { border: 0; padding: var(--sp-1) 0; }
    .rulebook-table td.rulebook-actions-cell { grid-column: 1 / -1; }
}.rulebook-actions.start { justify-content: flex-start; }
/* Task 532 - "this rule covers a longer one" offer under the status line. */
.rulebook-covered-item { border: 1px solid var(--line, #d0d4da); border-radius: 6px; padding: var(--sp-2); margin: var(--sp-2) 0; }
.rulebook-covered-item p { margin: 0 0 var(--sp-1); }
.rulebook-covered-item ul { margin: 0 0 var(--sp-1); padding-left: 1.2rem; }

/* Task 518 - the ONE category picker (picker.js), on every page that sets a category. It reuses the
   Uncategorised grid (.triage-cats / .triage-cat) so the picker looks the same wherever it is. */
.picker { display: grid; gap: var(--sp-2); }
.picker-grid { columns: 3 10rem; }
.row-panel .picker-grid, .rulebook-edit-picker .picker-grid { columns: 2 9rem; }
.triage-cat.current .triage-pick { background: var(--s1); border-color: var(--s1); color: var(--s1-ink); font-weight: 600; }
.triage-cat.current .triage-rule { border-color: var(--s1); }
.picker-new { margin-bottom: var(--sp-1); }
.picker-new input { flex: 1 1 auto; min-width: 0; padding: .35rem .75rem; border: 1px solid var(--border);
    border-radius: 999px 0 0 999px; background: var(--surface); color: var(--ink); font: inherit; }
.picker-new .picker-add { flex: none; border-radius: 0; }
.picker-new:not(:has(.triage-rule)) .picker-add { border-radius: 0 999px 999px 0; }
.picker-confirm { padding: var(--sp-3); border: 1px solid var(--s1); border-radius: var(--r-2); background: var(--s1-wash); }
.picker-confirm p { margin: 0; }
.picker-count { font-weight: 600; }
.picker-commit { background: var(--s1); color: var(--s1-ink); border-color: var(--s1); }
.picker-popover { position: absolute; z-index: 60; max-height: 70vh; overflow: auto; padding: var(--sp-3);
    background: var(--raised); border: 1px solid var(--border); border-radius: var(--r-3); box-shadow: var(--shadow); }
.picker-popover h3 { margin: 0 0 var(--sp-2); font-size: var(--t-small); color: var(--muted); }
.cat-button { max-width: 100%; padding: .25rem .6rem; border: 1px solid var(--border); border-radius: 999px;
    background: var(--surface); color: var(--ink); font: inherit; font-size: var(--t-small); text-align: left;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
.cat-button::after { content: " \25BE"; color: var(--muted); }
.cat-button:hover, .cat-button:focus-visible { border-color: var(--s1); }
.row-panel-picker { margin-top: var(--sp-3); }
.rulebook-add-picker { margin-top: var(--sp-2); }
.rulebook-edit-picker { margin-top: var(--sp-2); }

/* Task 533 - Debit Watch. One card per flag; the rows behind it reuse the triage list. */
/* Task 554 - the lists are .txn-table grids; this only adds what a grid of payees needs. */
.debitwatch-scroll { overflow-x: auto; }
.debitwatch-table td.num, .debitwatch-table td.date { white-space: nowrap; }
.debitwatch-payee { min-width: 12rem; }
.debitwatch-name { font-weight: 600; font-family: ui-monospace, Consolas, monospace; font-size: var(--t-small); }
.debitwatch-figure { font-variant-numeric: tabular-nums; }
.debitwatch-figure.up { color: #b42318; font-weight: 600; }
@media (max-width: 48rem) {
    /* The grid folds into one card per payee like Transactions; each figure keeps its column name. */
    .debitwatch-table td[data-label]:not(.debitwatch-payee)::before {
        content: attr(data-label) ": "; font-size: var(--t-micro); color: var(--muted); font-weight: 400;
        text-transform: uppercase; letter-spacing: .06em;
    }
    .debitwatch-table td:empty { display: none; }
}
.debitwatch-rows summary { cursor: pointer; font-size: var(--t-small); color: var(--s1); margin-top: var(--sp-1); }
.debitwatch-rows .triage-row { cursor: default; }
.debitwatch-open { display: inline-block; margin: var(--sp-1) 0; font-size: var(--t-small); color: var(--s1); }
@media (prefers-color-scheme: dark) { .debitwatch-figure.up { color: #f97066; } }
/* Task 556: a month off its usual amount. A drop is flagged too - it may be a missed part-payment. */
.debitwatch-figure.down { color: #b54708; font-weight: 600; }
.debitwatch-off > td:first-child { box-shadow: inset 3px 0 0 #b54708; }
@media (prefers-color-scheme: dark) { .debitwatch-figure.down { color: #fdb022; } .debitwatch-off > td:first-child { box-shadow: inset 3px 0 0 #fdb022; } }

/* Task 534 - "only on this account" beside a rule flag, and the scope on a listed rule. */
.rule-scope { display: block; margin: 0.25rem 0 0.5rem; font-size: 0.9em; }
.rule-scope-label { color: var(--muted, #666); font-size: 0.9em; margin-left: 0.35rem; }
/* Task 544 - rules the batch AI pass made: a small pill before the key, the reason on its own line. */
.rule-ai-badge {
    display: inline-block; margin-right: var(--sp-1); padding: 0 .45rem; border-radius: 999px;
    background: var(--s1-wash); border: 1px solid var(--border); color: var(--ink);
    font-size: var(--t-micro); font-weight: 600; line-height: 1.5; vertical-align: middle;
}
.rule-ai-reason { display: block; color: var(--muted); font-size: var(--t-small); margin-top: 2px; }

/* Task 535 - the rules a selection's flag would make, previewed before anything is saved. */
.rule-draft { width: min(640px, calc(100vw - 32px)); max-height: 85vh; overflow: auto; padding: var(--sp-3); }
.rule-draft::backdrop { background: rgba(16, 24, 40, 0.45); }
.rule-draft h2 { margin: 0 0 var(--sp-2); font-size: 1.1rem; }
.rule-draft-label { display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-2) 0 var(--sp-3); font-weight: 600; }
.rule-draft-label input { flex: 1; font: inherit; padding: var(--sp-1) var(--sp-2); }
.selection-rules { width: min(760px, calc(100vw - 32px)); max-height: 80vh; overflow: auto; padding: var(--sp-3); }
.selection-rules::backdrop { background: rgba(16, 24, 40, 0.45); }
.selection-rules h2 { margin: 0 0 var(--sp-2); font-size: 1.1rem; }
.selection-rule { border-top: 1px solid var(--border); padding-top: var(--sp-2); margin-top: var(--sp-2); }
.selection-rule h3 { margin: 0; font-size: var(--t-small); }
.selection-rows { list-style: none; margin: 0; padding: 0; font-size: var(--t-small); }
.selection-rows li { display: grid; grid-template-columns: 6.5rem 1fr auto; gap: var(--sp-1) var(--sp-2); padding: 2px 0; }
.selection-rows .selection-amount { text-align: right; font-variant-numeric: tabular-nums; }
.selection-rows .selection-account { grid-column: 2 / -1; color: var(--muted); }
.selection-actions { display: flex; gap: var(--sp-2); margin-top: var(--sp-3); }

/* Task 543 - grouped Uncategorised rows: a tick box, the row, and the lines behind it on demand. */
.triage-tools { display: flex; align-items: center; gap: var(--sp-2); margin: 0 0 var(--sp-2); }
.triage-item { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: var(--sp-1); align-items: start; }
.triage-tick { margin: calc(var(--sp-2) + 2px) 0 0 var(--sp-1); }
.triage-times { color: var(--muted); font-weight: 600; }
.triage-expand { grid-column: 2; justify-self: start; margin: 0 0 var(--sp-1) var(--sp-2); padding: 0; background: none; border: 0;
    color: var(--s1); font: inherit; font-size: var(--t-small); cursor: pointer; }
.triage-lines { grid-column: 2; list-style: none; margin: 0 0 var(--sp-2) var(--sp-2); padding: 0 0 0 var(--sp-2); border-left: 2px solid var(--grid); }
.triage-lines li { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr) auto; grid-template-areas: "date desc amt" "date acct amt";
    gap: 0 var(--sp-3); padding: var(--sp-1) 0; font-size: var(--t-small); }

/* ------------------------------------------------------------------------------------------------
   Task 546 (Johan, chat 599) - the filter bar STANDS OUT, and the status line sits under it.
   The sticky block is .page-top now (bar + status together), so the outcome of the last action is
   on screen however far down the page you are. The bar keeps its task 521 phone behaviour.
   ------------------------------------------------------------------------------------------------ */
.page-top {
    position: sticky; top: var(--stick-top, 0px); z-index: 4;
    background: var(--plane); padding: var(--sp-2) 0 var(--sp-2); max-width: 76rem;
}
.page-filters {
    position: static; top: auto; z-index: auto;
    background: var(--s1-wash); border: 2px solid var(--s1); border-left-width: 6px;
    border-radius: var(--r-3); padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-2);
    box-shadow: var(--shadow);
}
.page-filters .filters { background: var(--surface); }
.filters-text { gap: var(--sp-2); padding: 6px 12px; }
.filters-text label {
    font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}
.filters-text input[type="search"] {
    border-radius: 999px; padding: .3rem .8rem; font-size: var(--t-small); min-width: 14rem;
    border: 1px solid var(--border); background: var(--surface); color: var(--ink);
}
.filters-text input[type="search"]:not(:placeholder-shown) { border-color: var(--s1); box-shadow: 0 0 0 2px var(--s1-wash); }

.page-status {
    display: flex; align-items: center; gap: var(--sp-2); min-height: 2.25rem;
    padding: var(--sp-1) var(--sp-3); border-radius: var(--r-2); font-size: var(--t-small);
    border: 1px solid var(--border); background: var(--surface); color: var(--muted);
}
.page-status-dot { width: .6rem; height: .6rem; border-radius: 50%; background: var(--border); flex: none; }
.page-status-text { flex: 1 1 auto; min-width: 0; }
.page-status-time { color: var(--muted); font-variant-numeric: tabular-nums; flex: none; }
.page-status-action { flex: none; padding: .2rem .8rem; font-weight: 600; }
.page-status.is-ok { color: var(--delta-good); border-color: var(--delta-good); background: rgba(27, 107, 58, .08); }
.page-status.is-ok .page-status-dot { background: var(--delta-good); }
.page-status.is-error { color: var(--critical); border-color: var(--critical); background: rgba(179, 38, 30, .08); font-weight: 600; }
.page-status.is-error .page-status-dot { background: var(--critical); }
.page-status.is-busy { color: var(--ink-2); }
.page-status.is-busy .page-status-dot { background: var(--s1); }
.page-status.is-ok .page-status-text, .page-status.is-busy .page-status-text { color: var(--ink); }
@media (max-width: 60rem) {
    .filters-text input[type="search"] { min-width: 10rem; }
}

/* Task 548 (Johan, chat 601) - click-to-sort headers (sort.js). The header is a plain-looking button;
   the active column carries the arrow. The Overview lists and Uncategorised get a Sort by strip. */
th[data-sort] { padding: 0; }
button.sort-head {
    background: none; border: 0; padding: .45rem .5rem; font: inherit; font-weight: 600; color: inherit;
    cursor: pointer; border-radius: var(--r-1); width: 100%; text-align: inherit;
}
th.num button.sort-head { text-align: right; }
button.sort-head:hover { background: var(--wash); }
button.sort-head.active { color: var(--s1); }
.sort-arrow { font-size: .75em; }
.sort-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); margin: 0 0 var(--sp-2); font-size: var(--t-small); }
.sort-bar-label { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin-right: var(--sp-1); }
.sort-bar button.sort-head, .sort-bar [data-sort] > button.sort-head {
    width: auto; padding: .2rem .6rem; border: 1px solid var(--border); border-radius: 999px; font-weight: 500;
}
.sort-bar button.sort-head.active { border-color: var(--s1); background: var(--s1-wash); font-weight: 600; }
/* Task 549 (Johan, chats 605/607) - the Transactions totals footer. Fixed to the bottom of the
   screen beside the rail, so money in / out / net for the filtered rows is always in view. The
   selection action bar, when it shows, sits on top of it rather than under it. */
.totals-bar {
    /* Task 551 (Johan, chat 612) - z 5, above the sticky filter block (z 4), which on a short or
       narrow window wraps tall enough to reach the bottom and put Category over the footer. */
    position: fixed; left: var(--nav-w); right: 0; bottom: 0; z-index: 5;
    display: flex; flex-wrap: wrap; align-items: baseline; justify-content: flex-end; gap: var(--sp-2) var(--sp-5);
    padding: var(--sp-2) var(--sp-6); border-top: 2px solid var(--s1);
    background: var(--surface); box-shadow: 0 -4px 16px rgba(16, 24, 40, .08); font-size: var(--t-body);
}
.totals-count { margin-right: auto; color: var(--muted); font-size: var(--t-small); }
.totals-label { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.totals-figure { font-variant-numeric: tabular-nums; }
.totals-figure.in { color: var(--delta-good); }
.totals-figure.out { color: var(--critical); }
body:has(.totals-bar:not([hidden])) .action-bar { bottom: 2.9rem; }
body:has(.totals-bar:not([hidden])) .bc-main { padding-bottom: 6rem; }

/* Task 549 - the clear-category button beside the Category select. */
.filters-clear {
    flex: none; width: 1.7rem; height: 1.7rem; padding: 0; border-radius: 50%; line-height: 1;
    border: 1px solid var(--border); background: var(--surface); color: var(--muted); cursor: pointer;
    font-size: .8rem;
}
.filters-clear:hover { color: var(--s1); border-color: var(--s1); }
@media (max-width: 60rem) {
    .totals-bar { left: 0; padding: var(--sp-2) var(--sp-4); gap: var(--sp-1) var(--sp-3); }
}

/* Task 553 - a payment to SARS on the VAT lines: shown, but not a VAT line. */
.vat-paid-line td { color: var(--muted); font-style: italic; }
.category-vat-table select { max-width: 100%; }

/* Task 573 (Johan) - the view box: the tick boxes that change how money is counted, stacked in a
   small box at the left of the filter bar. On a phone it is one more capsule in the sideways scroll. */
.filters-view { flex-direction: column; flex-wrap: nowrap; align-items: flex-start; gap: 2px; padding: 4px 10px; border-radius: var(--r-3); }
.filters-view .filter-tick { margin-left: 0; line-height: 1.2; }
.filters-view .filter-tick input:disabled + * , .filters-view .filter-tick:has(input:disabled) { opacity: .55; cursor: default; }
@media (max-width: 640px) {
    .filters-view { padding: 2px 8px; }
    .filters-view .filter-tick { font-size: 12px; gap: 4px; }
}

/* Task 575 (Johan, chat 670) - the view box sits at the RIGHT end of the bar. The wrapper round the
   filters is invisible to layout on wide screens; margin-left:auto pushes the box to the edge, and
   when the bar runs out of room the box wraps onto the next line rather than squeezing the filters. */
.page-filters-main { display: contents; }
.filters-view { margin-left: auto; flex: none; }
/* At phone width the filters are one strip that scrolls sideways (task 521). The strip moves onto the
   wrapper, so the view box can take its own line underneath, its three ticks side by side. */
@media (max-width: 30rem) {
    .page-filters { flex-wrap: wrap; overflow-x: visible; }
    .page-filters > .page-filters-main {
        display: flex; flex: 1 1 100%; min-width: 0; gap: var(--sp-2);
        flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    }
    .page-filters-main > * { flex: none; }
    .filters-view { flex: 1 1 100%; margin-left: 0; flex-direction: row; flex-wrap: wrap; gap: 2px var(--sp-3); }
}

/* Task 574 - the Duplicates page. One card per group: date, amount, account; one ticked line per copy. */
.dupes-hint { color: var(--muted); font-size: var(--t-small); margin: 0 0 var(--sp-3); }
.dupes-groups { display: grid; gap: var(--sp-3); }
.dupes-group { border: 1px solid var(--grid); border-radius: var(--r-1); padding: var(--sp-2) var(--sp-3); }
.dupes-group-head { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: baseline; margin-bottom: var(--sp-2); }
.dupes-amount { font-variant-numeric: tabular-nums; font-weight: 600; }
.dupes-amount.neg { color: var(--neg, inherit); }
.dupes-account, .dupes-meta { color: var(--muted); font-size: var(--t-small); }
.dupes-lines { list-style: none; margin: 0; padding: 0; }
.dupes-lines li { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--sp-2); padding: var(--sp-1) 0; border-top: 1px solid var(--grid); }
.dupes-lines label { display: flex; gap: var(--sp-2); align-items: baseline; cursor: pointer; }
.dupes-tools { display: flex; gap: var(--sp-2); margin-top: var(--sp-2); }
.dupes-dropped { list-style: none; margin: 0; padding: 0; }
.dupes-dropped li { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: baseline; padding: var(--sp-2) 0; border-top: 1px solid var(--grid); }
.dupes-dropped .dupes-restore { margin-left: auto; }

/* Task 580 (Johan, chat 685) - Entities and Accounts are tick-box lists, any mix ticked. Wide: the
   boxes sit in the bar and the dropdown button is hidden. Phone: one button that says what is ticked
   ("All 4 accounts", "Cheque", "3 accounts") and the list drops under it - position:fixed (board.js
   placeMulti), because the bar scrolls sideways and would clip anything placed inside it. */
.filter-multi { display: inline-flex; align-items: center; }
.filters .filter-multi-toggle { display: none; }
.filter-multi-list { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 2px var(--sp-3); padding: 0 var(--sp-2); }
.filters .filter-multi-list .filter-tick { margin-left: 0; }
@media (max-width: 40rem) {
    .filters .filter-multi-toggle {
        display: inline-flex; align-items: center; gap: 4px; white-space: nowrap;
        border: 1px solid var(--border);
    }
    .filters .filter-multi-toggle::after { content: '\25BE'; font-size: .75em; }
    .filter-multi-list {
        display: none; position: fixed; z-index: 60; flex-direction: column; flex-wrap: nowrap; align-items: stretch;
        gap: 0; padding: var(--sp-1) 0; min-width: 12rem; max-width: calc(100vw - 16px); max-height: 60vh; overflow-y: auto;
        background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-3); box-shadow: var(--shadow);
    }
    .filter-multi.open .filter-multi-list { display: flex; }
    .filters .filter-multi-list .filter-tick {
        font-size: var(--t-body); white-space: normal; padding: var(--sp-2) var(--sp-3); gap: var(--sp-2);
    }
    .filters .filter-multi-list .filter-tick-all { border-bottom: 1px solid var(--border); }
}

/* Task 581 - Income & expenses per entity. One table per entity, read down: income, expenses, net.
   An allocated share of another entity's cost is its own line with an "Allocated" badge - never told
   apart by colour alone - because SARS only allows it if the entity actually pays the share back. */
.ie-entity .ie-table { max-width: 46rem; }
.ie-table .ie-group th { font-size: var(--t-small); color: var(--ink-2); text-transform: uppercase; letter-spacing: .04em;
    padding-top: var(--sp-4); }
.ie-table .ie-total td, .ie-table .ie-net td, .ie-table .ie-net th { font-weight: 600; }
.ie-table .ie-net th { text-align: left; padding: var(--sp-2); border-top: 2px solid var(--border); }
.ie-table .ie-net td { border-top: 2px solid var(--border); }
.ie-table .ie-subtotal td { color: var(--ink-2); font-size: var(--t-small); }
/* Task 591 - Expenses split as the tax estimate treats them: Deductible / Medical and retirement / Not deductible. */
.ie-table .ie-subgroup td { font-weight: 600; font-size: var(--t-small); color: var(--ink-2); padding-top: var(--sp-3); }
.ie-table .ie-subtotal.ie-tax-deductible td, .ie-table .ie-subtotal.ie-tax-role td,
.ie-table .ie-subtotal.ie-tax-nd td { font-weight: 600; border-top: 1px solid var(--border); }
.ie-table tr.ie-tax-nd:not(.ie-subgroup):not(.ie-subtotal) td { color: var(--muted); }
.ie-table tr.ie-allocated td { background: var(--s2-wash); }
.ie-allocated-badge { display: inline-block; font-size: var(--t-micro); font-weight: 600; text-transform: uppercase;
    letter-spacing: .04em; color: var(--s2); border: 1px solid var(--s2); border-radius: var(--r-1); padding: 0 var(--sp-1);
    vertical-align: middle; }
.ie-allocated-from { color: var(--muted); font-size: var(--t-small); }
.income-rules select, .income-rules input[type="number"] { max-width: 100%; }
.income-rules input[type="number"] { width: 6rem; text-align: right; }
.income-rule-add { margin-top: var(--sp-4); }
.income-rule-add input[type="number"] { width: 6rem; }

/* Task 582 - the tax estimate. The disclaimer is a bordered box, not small print; an unverified SARS
   table gets a badge with words, never colour alone. */
.ie-tax-disclaimer { border-left: 3px solid var(--s2); padding: var(--sp-2) var(--sp-3); background: var(--s2-wash); max-width: 60rem; }
.ie-tax-table { max-width: 60rem; }
.ie-tax-table .ie-tax-amount { font-weight: 600; }
.ie-tax-unverified { display: inline-block; margin-left: var(--sp-1); font-size: var(--t-micro); font-weight: 600;
    text-transform: uppercase; letter-spacing: .04em; color: var(--s2); border: 1px solid var(--s2); border-radius: var(--r-1);
    padding: 0 var(--sp-1); }
.ie-tax-notes { max-width: 60rem; padding-left: 1.2rem; color: var(--ink-2); font-size: var(--t-small); }
.ie-tax-notes li { margin: var(--sp-1) 0; }
.ie-tax .ie-allocated-from { display: block; }
@media (max-width: 40rem) { .ie-tax-table { display: block; overflow-x: auto; } }
/* Task 586 - the working under each estimate row: RA deduction, rebates, s6A, s6B, tax payable. */
.ie-tax-working > td { padding-top: 0; }
.ie-tax-working-table { width: 100%; max-width: 44rem; margin-left: auto; font-size: var(--t-small); color: var(--ink-2); border-collapse: collapse; }
.ie-tax-working-table td { padding: 2px var(--sp-2); border: 0; }
.ie-tax-working-table td.num { white-space: nowrap; text-align: right; }
.ie-tax-working-table tr.ie-tax-working-head td { font-weight: 600; color: var(--ink); }
.ie-tax-working-table td.ie-tax-actual { color: var(--muted); }
.ie-tax-working-table tr.ie-tax-working-sub td { font-weight: 600; color: var(--ink); border-top: 1px solid var(--line, #ddd); }

/* Task 650 - the progressive menu's More group (Nav:Progressive). A <details>, so it needs no script. */
.nav-more > summary { cursor: pointer; }


/* Task 651 - one-time NEW marker on a newly unlocked More row (merge plan 648 s4). */
.nav-new { margin-left: auto; padding: 0 6px; border-radius: 8px; font-size: 10px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; background: var(--accent, #f5a623); color: #fff; }
.nav-new + .nav-count { margin-left: 6px; }

/* Task 665 - the shared burger menu's wrapper. On the rail it is display:contents, so the rail's flex
   layout is exactly what it was; mw-nav.css (MaxWealth brand only) folds it behind a button below 768px. */
.bc-nav > .mw-menu { display: contents; }
.mw-burger { display: none; }
/* Task 665 - a grid cell defaults to min-width:auto, so one wide table, chart or input widened the whole
   column and the page scrolled sideways on a phone. The cell may now shrink; wide content scrolls inside. */
.bc-main, .pub-main { min-width: 0; }
.bc-main img, .bc-main svg, .bc-main canvas, .pub-main img, .pub-main svg { max-width: 100%; }