/* =============================================================================
   Personal Manager - shared design system.
   Loaded by every host (Blazor Web and the MAUI Hybrid WebView), so it owns the
   theme tokens, the application shell and every reusable component. No web fonts
   or CDN assets are referenced: the app is installable as a PWA and must look
   identical with no network.
   ============================================================================= */

:root {
    /* Brand ---------------------------------------------------------------- */
    --pm-accent: #f97316;
    /* The accent as *text*: every one of its colour usages is ink on a pale wash - links, the
       avatar initials, the active nav item, the collapse badge. At #ea580c that was 3.56:1 on a
       card, 3.23:1 on the canvas and 3.24:1 on the accent wash, so ordinary links were failing
       everywhere. Darkened one step on the same hue to clear 4.5:1 on all three (5.18 / 4.70 /
       4.71). The brand orange itself is --pm-accent above and is deliberately untouched; the two
       non-text uses of this token are a gradient stop and the pressed state of the accent button,
       and both only benefit from being deeper. */
    --pm-accent-strong: #c2410c;
    --pm-accent-soft: #fff2e8;
    --pm-accent-ring: rgba(249, 115, 22, 0.25);

    /* Neutrals ------------------------------------------------------------- */
    --pm-ink: #16181d;
    --pm-ink-soft: #3f434d;
    /* Darkened from #8a9099, which measured 2.92:1 on the canvas and 3.22:1 on a card - well under
       the 4.5:1 normal-size text needs, and inherited by every muted label in the app. This is the
       lightest grey that clears the minimum against both surfaces (4.59:1 and 5.05:1), so it stays
       plainly secondary next to --pm-ink-soft. The dark theme's value was already comfortable and
       is unchanged. */
    --pm-muted: #696f7a;
    --pm-line: #ececef;
    --pm-line-strong: #dcdde1;
    --pm-surface: #ffffff;
    --pm-canvas: #f4f4f2;
    --pm-hover: #faf9f7;

    /* Semantics ------------------------------------------------------------ */
    --pm-income: #0f9d58;
    --pm-income-soft: #e7f6ee;
    /* Darkened from #e04b2a, which was 4.03:1 on a card and 3.66:1 on the canvas. This token is
       read as text far more than it is used as a fill - validation messages, negative KPI values,
       activity amounts - and at the old value none of them met 4.5:1. The solid .btn-danger was
       failing from the other side too, its white label sitting at 4.03:1. #c93c1d fixes both ends
       (5.06 / 4.60 for text, 5.06 for the label). The dark theme has its own value below and is
       unchanged. */
    --pm-expense: #c93c1d;
    --pm-expense-soft: #fdeee9;

    /* "Not wrong, but not finished either" - the state an offline edit sits in. Distinct from
       expense red, which is reserved for things that will not come right on their own. */
    --pm-warning: #b45309;
    --pm-warning-soft: rgba(180, 83, 9, 0.16);

    /* Forms ---------------------------------------------------------------- */
    /* The field label's type scale, shared rather than inlined because .form-group-action::before
       has to reproduce a label's exact height to keep its button level with the inputs beside it.
       Change these and both move together. */
    --pm-field-label-size: 0.76rem;
    --pm-field-label-gap: 0.22rem;

    /* Shape ---------------------------------------------------------------- */
    --pm-radius: 14px;
    --pm-radius-sm: 9px;
    --pm-radius-pill: 999px;
    --pm-shadow: 0 1px 2px rgba(19, 20, 24, 0.04), 0 1px 3px rgba(19, 20, 24, 0.03);
    --pm-shadow-lg: 0 10px 30px rgba(19, 20, 24, 0.08);

    --pm-sidebar-width: 248px;

    /* How wide ONE column of running prose is allowed to get - About and the five legal pages. It
       is a measure, not a taste: the panel's own 1.15rem padding and 1px border come off this,
       leaving about 72 characters a line, which is the width those pages were already setting on
       their paragraphs. Setting it on the COLUMN rather than the paragraph is the whole point.
       Capping the paragraph alone left the panel stretched to the window with the text stopping two
       thirds of the way across, so every card on the Disclaimer page had a third of itself empty.

       A wide window is then filled by running more of these columns side by side, not by letting
       one of them grow - see .doc-page. Widening this token lengthens every line in the app's
       prose; it does not fill more screen, because the column count simply drops to suit.

       On a phone held upright it does nothing at all - every common portrait viewport (360, 390,
       414, 430) is narrower than 38rem, so the track cannot reach its maximum and the mobile web,
       the installed PWA and the Android WebView render exactly what they rendered before. */
    --pm-doc-width: 38rem;
}

html[data-organizer-theme="dark"] {
    color-scheme: dark;
    --pm-ink: #edf0f7;
    --pm-ink-soft: #ccd1dc;
    --pm-muted: #aab2c1;
    --pm-line: #343c4c;
    --pm-line-strong: #505b6e;
    --pm-surface: #202735;
    --pm-canvas: #171d27;
    --pm-hover: #30394b;
    --pm-accent-soft: #493022;
    --pm-accent-strong: #ffae73;

    /* The semantic colours used to stop here, because the dark theme only ever reached the organizer
       page and nothing there draws a KPI, pill, progress bar or alert. The light values do not
       survive the inversion: the -soft tokens are pale washes that stay pale while the text over
       them turns light, and the solid pair are mid-tone inks chosen to sit on white. */
    --pm-income: #3ddc84;
    --pm-income-soft: #14342a;
    --pm-expense: #ff8367;
    --pm-expense-soft: #3d2019;
    --pm-accent-ring: rgba(249, 115, 22, 0.38);

    /* The light amber is a mid-tone picked to sit on white; on these surfaces it reads as mud. */
    --pm-warning: #fbbf24;
    --pm-warning-soft: rgba(251, 191, 36, 0.22);
}

html, body {
    background: var(--pm-canvas);
    color: var(--pm-ink);
    /* System stack only, so nothing has to be fetched before the first paint. */
    font-family: "Segoe UI Variable Display", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto,
                 "Helvetica Neue", Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
}

body {
    font-size: 0.9rem;
}

h1, h2, h3, h4, h5, h6 {
    color: var(--pm-ink);
    letter-spacing: -0.011em;
}

/* Bootstrap's default heading scale is far too large for a dense finance UI, where h2/h3 are
   section labels inside a card rather than page titles. */
h2 {
    font-size: 1.05rem;
    font-weight: 650;
}

h3 {
    font-size: 0.94rem;
    font-weight: 650;
}

h4, h5, h6 {
    font-size: 0.88rem;
    font-weight: 650;
}

a {
    color: var(--pm-accent-strong);
    text-decoration: none;
}

a:hover {
    color: var(--pm-accent-strong);
    text-decoration: underline;
}

::selection {
    background: var(--pm-accent-ring);
}

/* =============================================================================
   Application shell
   ============================================================================= */

.app-shell {
    display: flex;
    min-height: 100vh;
    /* Phones only, where 100vh measures the viewport as if the browser's own bars
       were not there and leaves the page a chrome's worth too tall. Identical to
       100vh everywhere else, and ignored by browsers that do not know it. */
    min-height: 100dvh;
    background: var(--pm-canvas);
}

.app-sidebar {
    width: var(--pm-sidebar-width);
    flex: 0 0 var(--pm-sidebar-width);
    background: var(--pm-surface);
    border-right: 1px solid var(--pm-line);
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100dvh;
}

.app-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.app-content {
    /* How much empty space the page has to keep below its last element. Held in a variable because
       two separate concerns raise it - the fixed phone tab bar (the breakpoint below) and a
       host-injected banner (the block at the end of this file) - and they have to ADD UP. Writing
       `padding-bottom` twice does not: the later rule simply replaces the earlier one, which is how
       the phone's tab-bar allowance was being silently thrown away and the last control on every
       page ended up underneath the tabs. */
    --pm-content-bottom: 2.5rem;
    padding: 1.35rem 1.6rem;
    padding-bottom: calc(var(--pm-content-bottom) + var(--pm-host-overlay, 0px));
    flex: 1 1 auto;
}

/* --- Brand ------------------------------------------------------------------ */
.app-brand {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 1.15rem 1.15rem 0.9rem;
    font-weight: 650;
    font-size: 1rem;
    color: var(--pm-ink);
}

.app-brand:hover {
    text-decoration: none;
    color: var(--pm-ink);
}

.app-brand-mark {
    width: 28px;
    height: 28px;
    border-radius: 8px;
    background: linear-gradient(140deg, var(--pm-accent) 0%, var(--pm-accent-strong) 100%);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    box-shadow: 0 2px 6px rgba(249, 115, 22, 0.35);
}

.app-brand-mark svg {
    width: 16px;
    height: 16px;
}

/* --- Navigation ------------------------------------------------------------- */
.app-nav {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 0.25rem 0.7rem 1rem;
}

.app-nav-section {
    padding: 0.9rem 0.55rem 0.35rem;
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--pm-muted);
}

.app-nav .nav-link {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.5rem 0.6rem;
    margin-bottom: 0.12rem;
    border-radius: var(--pm-radius-sm);
    color: var(--pm-ink-soft);
    font-size: 0.86rem;
    font-weight: 500;
    line-height: 1.25;
    transition: background-color 0.14s ease, color 0.14s ease;
}

.app-nav .nav-link:hover {
    background: var(--pm-hover);
    color: var(--pm-ink);
    text-decoration: none;
}

.app-nav .nav-link.active {
    background: var(--pm-accent-soft);
    color: var(--pm-accent-strong);
    font-weight: 600;
}

.app-nav .nav-link svg {
    width: 17px;
    height: 17px;
    flex: 0 0 auto;
    opacity: 0.75;
}

.app-nav .nav-link.active svg {
    opacity: 1;
}

/* --- Sidebar footer / user card --------------------------------------------- */
.app-user {
    border-top: 1px solid var(--pm-line);
    padding: 0.8rem 0.9rem;
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.app-avatar {
    width: 32px;
    height: 32px;
    flex: 0 0 auto;
    border-radius: var(--pm-radius-pill);
    background: var(--pm-accent-soft);
    color: var(--pm-accent-strong);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 0.78rem;
}

.app-user-text {
    min-width: 0;
    flex: 1 1 auto;
    line-height: 1.2;
}

.app-user-name {
    display: block;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--pm-ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-user-role {
    display: block;
    font-size: 0.72rem;
    color: var(--pm-muted);
}

.app-signout {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border-radius: var(--pm-radius-sm);
    color: var(--pm-muted);
}

.app-signout:hover {
    background: var(--pm-expense-soft);
    color: var(--pm-expense);
}

.app-signout svg {
    width: 16px;
    height: 16px;
}

/* Install button. `.app-nav .nav-link` sets `display: flex`, which outranks the user-agent
   `[hidden] { display: none }` rule, so hiding has to be restated explicitly here. */
.app-nav .app-install {
    width: 100%;
    border: 0;
    background: none;
    text-align: left;
    font-family: inherit;
    cursor: pointer;
    color: var(--pm-accent-strong);
    font-weight: 600;
}

.app-nav .app-install:hover {
    background: var(--pm-accent-soft);
}

.app-nav .app-install svg {
    opacity: 1;
}

.app-nav .app-install[hidden] {
    display: none;
}

/* --- Top bar ---------------------------------------------------------------- */
.app-topbar {
    /* Held in a variable so the WebKit safe-area block at the end of this file can add the status
       bar inset to whichever height the current breakpoint chose, instead of hardcoding one of
       them and silently resizing the bar on phones. */
    --pm-topbar-height: 58px;
    height: var(--pm-topbar-height);
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: 0 1.6rem;
    background: rgba(255, 255, 255, 0.86);
    backdrop-filter: saturate(180%) blur(8px);
    border-bottom: 1px solid var(--pm-line);
    position: sticky;
    top: 0;
    z-index: 20;
}

.app-topbar-title {
    font-weight: 600;
    font-size: 0.95rem;
    margin: 0;
}

.app-topbar-spacer {
    flex: 1 1 auto;
}

.app-topbar-date {
    font-size: 0.8rem;
    color: var(--pm-muted);
    white-space: nowrap;
}

/* Sync light. Green when what the user entered is in the database, red when it is not.

   Built on the existing income/expense tokens rather than new colours of its own: those are already
   the app's "good" and "bad" pair, they already have dark-theme values that survive the inversion,
   and a second green would only drift from the first. */
.app-sync {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    flex: 0 0 auto;
    height: 30px;
    padding: 0 0.6rem;
    border: 1px solid transparent;
    border-radius: var(--pm-radius-pill);
    background: var(--pm-sync-soft);
    color: var(--pm-sync);
    font-family: inherit;
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
}

.app-sync-ok {
    --pm-sync: var(--pm-income);
    --pm-sync-soft: var(--pm-income-soft);
}

.app-sync-bad {
    --pm-sync: var(--pm-expense);
    --pm-sync-soft: var(--pm-expense-soft);
}

.app-sync:hover:not(:disabled) {
    border-color: var(--pm-sync);
}

.app-sync:disabled {
    cursor: default;
}

.app-sync-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--pm-sync);
    flex: 0 0 auto;
}

/* Only while a sync or a re-check is in flight, so the press is visibly doing something on a
   connection slow enough for that to be in doubt. */
.app-sync-dot-busy {
    animation: pm-sync-pulse 1s ease-in-out infinite;
}

@keyframes pm-sync-pulse {
    50% {
        opacity: 0.25;
    }
}

.app-sync-icon {
    display: inline-flex;
}

.app-sync-icon svg {
    width: 15px;
    height: 15px;
}

@media (prefers-reduced-motion: reduce) {
    .app-sync-dot-busy {
        animation: none;
    }
}

/* Reminder bell ----------------------------------------------------------------------------------
   Only ever on screen when there is something behind it, so it is styled as a live count rather
   than as a permanent control: the amber warning token when things are merely approaching, the
   expense red once something is actually late. */
.app-bell-wrap {
    position: relative;
    flex: 0 0 auto;
    display: inline-flex;
}

.app-bell {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    height: 30px;
    padding: 0 0.55rem;
    border: 1px solid transparent;
    border-radius: var(--pm-radius-pill);
    background: var(--pm-warning-soft);
    color: var(--pm-warning);
    font-family: inherit;
    font-size: 0.75rem;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
}

.app-bell-late {
    background: var(--pm-expense-soft);
    color: var(--pm-expense);
}

.app-bell:hover {
    border-color: currentColor;
}

.app-bell-icon {
    display: inline-flex;
}

.app-bell-icon svg {
    width: 16px;
    height: 16px;
}

.app-bell-badge {
    font-variant-numeric: tabular-nums;
}

/* Sits under the panel and over everything else, so a tap anywhere outside closes it. Transparent
   rather than dimmed: this is a small popover, not a modal, and darkening the whole page for it
   would overstate what it is. */
.app-bell-scrim {
    position: fixed;
    inset: 0;
    z-index: 30;
    border: 0;
    padding: 0;
    background: transparent;
    cursor: default;
}

.app-bell-panel {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    z-index: 31;
    width: min(21rem, calc(100vw - 2rem));
    max-height: min(26rem, calc(100vh - 6rem));
    overflow-y: auto;
    background: var(--pm-surface);
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius);
    box-shadow: var(--pm-shadow-lg);
}

.app-bell-head {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 0.7rem 0.85rem 0.55rem;
    border-bottom: 1px solid var(--pm-line);
}

.app-bell-head-note {
    font-size: 0.75rem;
    color: var(--pm-muted);
}

.app-bell-list {
    list-style: none;
    margin: 0;
    padding: 0.25rem 0;
}

.app-bell-item {
    display: flex;
    align-items: flex-start;
    gap: 0.55rem;
    width: 100%;
    padding: 0.5rem 0.85rem;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.app-bell-item:hover {
    background: var(--pm-hover);
}

.app-bell-item-main {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
}

.app-bell-item-title {
    font-size: 0.85rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.app-bell-item-when {
    font-size: 0.72rem;
    color: var(--pm-muted);
}

/* The colour is the only thing distinguishing the three buckets in the list, so it carries meaning
   - but the wording under every title says the same thing in words, which is what keeps the list
   readable without colour. */
.app-bell-pip {
    width: 8px;
    height: 8px;
    margin-top: 0.3rem;
    border-radius: 50%;
    flex: 0 0 auto;
    background: var(--pm-muted);
}

.app-bell-pip-overdue {
    background: var(--pm-expense);
}

.app-bell-pip-duetoday {
    background: var(--pm-warning);
}

.app-bell-pip-duesoon {
    background: var(--pm-accent);
}

.app-bell-more {
    margin: 0;
    padding: 0.1rem 0.85rem 0.55rem;
    font-size: 0.72rem;
    color: var(--pm-muted);
}

.app-bell-foot {
    display: flex;
    gap: 0.9rem;
    padding: 0.55rem 0.85rem;
    border-top: 1px solid var(--pm-line);
    font-size: 0.78rem;
}

/* Per-row sync mark ------------------------------------------------------------------------------
   Only ever rendered on a row that is NOT on the server, so it has to read as an exception rather
   than as decoration. Sized to sit inline ahead of the first cell's text without changing the row
   height, which keeps a marked row the same shape as every other one. */
.app-rowsync {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 14px;
    height: 14px;
    margin-inline-end: 0.35rem;
    vertical-align: middle;
    flex: 0 0 auto;
    cursor: help;
}

.app-rowsync-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--pm-rowsync);
    /* The halo keeps a 8px dot findable against a striped table without making it bigger. */
    box-shadow: 0 0 0 3px var(--pm-rowsync-soft);
}

/* Amber, not red: being offline is normal and resolves itself. */
.app-rowsync-waiting {
    --pm-rowsync: var(--pm-warning);
    --pm-rowsync-soft: var(--pm-warning-soft);
}

/* Red: the server has actively refused this one, so waiting will not fix it. */
.app-rowsync-blocked {
    --pm-rowsync: var(--pm-expense);
    --pm-rowsync-soft: var(--pm-expense-soft);
}

/* Mobile drawer toggle: a plain checkbox drives the sidebar so no JS is needed
   (which also means it works during prerender, before the circuit connects). */
.app-nav-toggle {
    display: none;
}

.app-nav-toggle-label {
    display: none;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: var(--pm-radius-sm);
    border: 1px solid var(--pm-line-strong);
    background: var(--pm-surface);
    color: var(--pm-ink-soft);
    cursor: pointer;
    flex: 0 0 auto;
}

.app-nav-toggle-label svg {
    width: 17px;
    height: 17px;
}

.app-scrim {
    display: none;
}

/* --- Bottom tab bar (phones) ------------------------------------------------
   Hidden here and revealed by the phone breakpoint below, rather than the other
   way round, so a desktop browser never downloads a layout it will not use and
   the bar cannot flash during the first paint. */
.app-tabbar {
    display: none;
}

.app-tab {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.18rem;
    /* Tall enough for the 44px target every mobile platform asks for, with the
       label still legible underneath. */
    min-height: 54px;
    padding: 0.35rem 0.2rem;
    font-size: 0.68rem;
    font-weight: 550;
    letter-spacing: 0.01em;
    color: var(--pm-muted);
    text-decoration: none;
    background: none;
    border: 0;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.app-tab svg {
    width: 21px;
    height: 21px;
}

.app-tab:hover,
.app-tab:focus-visible {
    color: var(--pm-ink);
    text-decoration: none;
}

.app-tab.active {
    color: var(--pm-accent);
}

@media (max-width: 900px) {
    :root {
        --pm-sidebar-width: 264px;
    }

    .app-nav-toggle-label {
        display: inline-flex;
    }

    .app-sidebar {
        position: fixed;
        top: 0;
        left: 0;
        z-index: 40;
        transform: translateX(-100%);
        transition: transform 0.2s ease;
        box-shadow: var(--pm-shadow-lg);
    }

    .app-nav-toggle:checked ~ .app-shell .app-sidebar {
        transform: translateX(0);
    }

    .app-nav-toggle:checked ~ .app-shell .app-scrim {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 30;
        background: rgba(17, 18, 22, 0.35);
    }

    /* Side/top only - the bottom allowance is composed from --pm-content-bottom so the phone
       breakpoint and the host banner can both contribute to it. */
    .app-content {
        padding-top: 1rem;
        padding-left: 1rem;
        padding-right: 1rem;
    }

    .app-topbar {
        padding: 0 1rem;
    }

    .app-topbar-date {
        display: none;
    }

    /* The label goes, the icon never does: colour alone is unreadable to anyone red/green
       colour-blind, and on this control the colour carries the entire message. The explanation is
       still one tap away in the tooltip. */
    .app-sync-text {
        display: none;
    }

    .app-sync {
        padding: 0 0.5rem;
    }
}

/* Phones: the shell stops being a page with a menu and becomes an app with tabs.
   The drawer stays, now reached from the "More" tab, because the eleven
   destinations do not fit five slots and burying the rest would be worse than a
   second tap. */
@media (max-width: 720px) {
    .app-tabbar {
        display: flex;
        align-items: stretch;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        /* Under the drawer and its scrim (40 and 30), above the page. Opening the
           menu should dim the tabs, not leave them floating over it. */
        z-index: 25;
        background: rgba(255, 255, 255, 0.94);
        backdrop-filter: saturate(180%) blur(10px);
        border-top: 1px solid var(--pm-line);
        /* The gesture bar on a modern phone sits below the web viewport; without
           this the last row of labels ends up underneath it. */
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }

    /* The "More" tab does the hamburger's job, and two ways to open one drawer on
       a screen this size is just clutter. */
    .app-nav-toggle-label {
        display: none;
    }

    .app-topbar {
        --pm-topbar-height: 50px;
        padding: 0 0.9rem;
    }

    .app-topbar-title {
        font-size: 0.88rem;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* Stays on phones, where it matters most - this is the screen that loses signal. Shrunk to a
       dot and a glyph so it cannot squeeze the title. */
    .app-sync {
        height: 28px;
        padding: 0 0.45rem;
        gap: 0.3rem;
    }

    /* Clears the fixed bar. Anything less and the last transaction in a list - or
       worse, a form's save button, or the Previous/Next pager at the end of the
       personal workspace - sits permanently under the tabs. Set as the variable,
       not as padding-bottom, so a host banner adds to it instead of replacing it. */
    .app-content {
        --pm-content-bottom: calc(54px + env(safe-area-inset-bottom, 0px) + 1.5rem);
        padding-top: 0.85rem;
        padding-left: 0.85rem;
        padding-right: 0.85rem;
    }
}

/* -----------------------------------------------------------------------------
   Top inset, WebKit only.

   The counterpart to the bottom inset above, but it has to be fenced off because
   the two platforms solve it in different places. Android applies the top inset
   natively as padding on the content view (MainActivity) and consumes it, so
   adding it again here would push the topbar down twice on a notched phone -
   the exact bug that native handling was written to fix.

   `-webkit-touch-callout` is supported by WebKit and not by Chromium, so this
   block matches iOS and skips both the Android WebView and Windows WebView2.
   The topbar is sticky at top:0 and the viewport is `viewport-fit=cover`, so
   without this the clock and battery icons sit on top of the app's own header.
   ----------------------------------------------------------------------------- */
@supports (-webkit-touch-callout: none) {
    .app-topbar {
        height: calc(var(--pm-topbar-height) + env(safe-area-inset-top, 0px));
        padding-top: env(safe-area-inset-top, 0px);
    }

    /* Landscape puts the camera housing on one side, where the top inset is zero. */
    .app-shell {
        padding-left: env(safe-area-inset-left, 0px);
        padding-right: env(safe-area-inset-right, 0px);
    }
}

/* =============================================================================
   Page header
   ============================================================================= */

.page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.1rem;
}

.page-header h1 {
    font-size: 1.45rem;
    font-weight: 650;
    margin: 0;
}

.page-subtitle {
    margin: 0.2rem 0 0;
    font-size: 0.83rem;
    color: var(--pm-muted);
}

.page-header-actions {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

/* =============================================================================
   Surfaces
   ============================================================================= */

.panel,
.card {
    background: var(--pm-surface);
    /* Bootstrap 5.3 sets `color: var(--bs-body-color)` on .card, which beats inheritance and pinned
       card text to near-black on the dark surface - 1.03:1, i.e. invisible. Set the colour here so
       the panel never depends on inheritance or on a --bs-* value we do not own. The MauiHybrid host
       is still on Bootstrap 5.1, whose .card sets no colour at all, so this keeps the two hosts
       identical instead of correct on one and accidental on the other. */
    color: var(--pm-ink);
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius);
    box-shadow: var(--pm-shadow);
}

.panel {
    padding: 1.05rem 1.15rem;
    margin-bottom: 1.1rem;
}

/* Bootstrap cards are used by a few pages (Quick Add); make them read as panels. */
.card-body {
    padding: 1.05rem 1.15rem;
}

.card-body > h2,
.card-body > h3 {
    font-size: 0.98rem;
    font-weight: 650;
    margin: 0 0 0.75rem;
}

.card {
    margin-bottom: 1.1rem;
}

.panel > .table:last-child,
.panel > .table-responsive:last-child {
    margin-bottom: 0;
}

.panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-bottom: 0.85rem;
}

.panel h2 {
    font-size: 0.98rem;
    font-weight: 650;
    margin: 0;
}

.panel-note {
    font-size: 0.76rem;
    color: var(--pm-muted);
    margin: 0.15rem 0 0.75rem;
}

.panel-head + .panel-note {
    margin-top: -0.55rem;
}

/* The heading and its pill travel together so that `space-between` on .panel-head pushes only the
   Edit button to the far edge. Without this wrapper a three-child head spreads the pill into the
   middle of the row, where it reads as unrelated to the title it belongs to. */
.panel-head-main {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    /* Lets a long title wrap rather than shoving the button off the panel. */
    min-width: 0;
}

/* The form half of an editable panel. Focus is moved here when the form opens, so it carries
   tabindex="-1" - and the ring is suppressed because the user did not tab here and a box that
   lights up around the whole form reads as an error. Keyboard navigation inside is unaffected. */
.editable-region:focus {
    outline: none;
}

/* .panel-actions is the same row outside an EditableSection - a group of buttons closing a settings
   panel. It shares these rules rather than redeclaring them so the two cannot drift apart.
   Without a container a .btn group is three inline-blocks with no margin: they touch on one line,
   and when they wrap, the line boxes are shorter than the buttons and the second row overlaps the
   first. Flex removes both failures at once. */
.editable-actions,
.panel-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.85rem;
}

/* Full-width stacked buttons on a phone, matching .form-group-action in the same breakpoint:
   Save and Cancel are the two most consequential targets on the screen. Turning a PIN lock off
   belongs in that company, so .panel-actions follows the same rule. */
@media (max-width: 640px) {
    .editable-actions .btn,
    .panel-actions .btn {
        width: 100%;
    }
}

/* The organizer dresses its own containers through scoped CSS, which reaches only the elements that
   page writes itself - so an EditableSection nested inside it arrives as a plain .panel and reads as
   a visitor from another screen. These are .workspace-card's values, applied from here because a
   scoped rule cannot cross into a child component. Kept to the geometry that differs; the surface
   and text colours already match. */
.workspace .editable-panel {
    padding: 1.2rem;
    margin: 1rem 0;
    border-color: var(--pm-line-strong);
    border-radius: 0.85rem;
}

@media (max-width: 640px) {
    .workspace .editable-panel {
        padding: 1rem;
    }
}

/* Label/value rows inside a panel, for facts the user reads rather than edits:
   the backup schedule, and the profile details shown when the device is offline.
   A <dl> carries the pairing for a screen reader; the wrapper <div> around each
   dt/dd is what makes them one flex row, since a dl cannot do that itself.
   Built on the theme variables so the dark palette needs no second rule. */
.stat-list {
    margin: 0;
    border-top: 1px solid var(--pm-line);
}

.stat-list > div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--pm-line);
}

.stat-list dt {
    font-size: 0.76rem;
    font-weight: 500;
    color: var(--pm-muted);
    margin: 0;
    /* The label keeps its line; it is the value that wraps. */
    flex-shrink: 0;
}

.stat-list dd {
    font-size: 0.84rem;
    font-weight: 550;
    margin: 0;
    text-align: right;
    /* Long values (a mobile number, a backup summary) wrap instead of pushing
       the label off the row. */
    min-width: 0;
    overflow-wrap: anywhere;
}

/* On a narrow screen a label and a long value fight for the same line, so give
   each its own. This is the offline case's usual screen, not an edge case. */
@media (max-width: 420px) {
    .stat-list > div {
        display: block;
    }

    .stat-list dd {
        text-align: left;
        margin-top: 0.15rem;
    }
}

/* Keeps previously loaded figures on screen (dimmed) while a new range loads,
   instead of flashing the page back to a loading state on every filter change. */
.is-busy {
    opacity: 0.5;
    transition: opacity 0.15s ease;
    pointer-events: none;
}

/* =============================================================================
   KPI cards
   ============================================================================= */

/* Names the currency the figures below are counted in. Quiet on purpose: it is a unit label rather
   than a message, and it sits directly above the numbers it applies to, so it only needs to be
   noticed by someone already wondering. */
.dash-currency-note {
    font-size: 0.76rem;
    color: var(--pm-muted);
    margin: 0 0 0.5rem;
}

.kpi-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 0.85rem;
    margin-bottom: 1.1rem;
}

.kpi-card {
    background: var(--pm-surface);
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius);
    box-shadow: var(--pm-shadow);
    padding: 0.95rem 1.05rem 1.05rem;
    min-width: 0;
}

.kpi-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.55rem;
}

.kpi-label {
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.075em;
    text-transform: uppercase;
    color: var(--pm-muted);
    margin: 0;
}

.kpi-value {
    font-size: 1.5rem;
    font-weight: 650;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    margin: 0;
    color: var(--pm-ink);
    overflow-wrap: anywhere;
}

.kpi-value.is-negative {
    color: var(--pm-expense);
}

.kpi-value.is-positive {
    color: var(--pm-income);
}

.kpi-foot {
    margin: 0.35rem 0 0;
    font-size: 0.74rem;
    color: var(--pm-muted);
}

/* Trend chip: green when the change is good for the user, red when it is not.
   "Good" is direction-dependent, so the caller decides which class to apply. */
.kpi-delta {
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    padding: 0.13rem 0.45rem;
    border-radius: var(--pm-radius-pill);
    font-size: 0.72rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.kpi-delta-up {
    background: var(--pm-income-soft);
    color: #0b7a45;
}

.kpi-delta-down {
    background: var(--pm-expense-soft);
    color: #b03a1c;
}

.kpi-delta-flat {
    background: #f2f3f5;
    color: var(--pm-muted);
}

.kpi-delta svg {
    width: 11px;
    height: 11px;
}

/* =============================================================================
   Charts
   ============================================================================= */

.chart-axis {
    font-size: 11px;
    fill: var(--pm-muted);
}

.chart-bars svg {
    width: 100%;
    height: auto;
    display: block;
}

.chart-bar {
    transition: opacity 0.12s ease;
}

.chart-bars:hover .chart-bar {
    opacity: 0.55;
}

.chart-bars .chart-bar:hover {
    opacity: 1;
}

.chart-key {
    display: flex;
    gap: 1rem;
    justify-content: center;
    margin-top: 0.5rem;
    font-size: 0.76rem;
    color: var(--pm-muted);
}

.chart-key span {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.chart-key-dot {
    width: 8px;
    height: 8px;
    border-radius: 2px;
    display: inline-block;
}

.chart-empty {
    color: var(--pm-muted);
    font-size: 0.84rem;
    text-align: center;
    padding: 2.5rem 0;
    margin: 0;
}

/* --- Cumulative line -------------------------------------------------------- */
.chart-lines svg {
    width: 100%;
    height: auto;
    display: block;
}

/* --- Donut ------------------------------------------------------------------ */
.chart-donut {
    display: flex;
    align-items: center;
    gap: 1.1rem;
    flex-wrap: wrap;
}

.chart-donut-figure {
    flex: 0 0 auto;
    width: 168px;
}

.chart-donut-figure svg {
    width: 100%;
    height: auto;
    display: block;
}

.chart-donut-value {
    font-size: 17px;
    font-weight: 650;
    fill: var(--pm-ink);
}

.chart-donut-caption {
    font-size: 11px;
    fill: var(--pm-muted);
}

.chart-legend {
    list-style: none;
    margin: 0;
    padding: 0;
    flex: 1 1 190px;
    min-width: 0;
}

.chart-legend li {
    display: grid;
    grid-template-columns: 10px minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 0.5rem;
    padding: 0.34rem 0;
    font-size: 0.82rem;
    border-bottom: 1px solid var(--pm-line);
}

.chart-legend li:last-child {
    border-bottom: 0;
}

.chart-legend-empty {
    display: block !important;
    color: var(--pm-muted);
    border-bottom: 0 !important;
}

.chart-legend-dot {
    width: 10px;
    height: 10px;
    border-radius: 3px;
}

.chart-legend-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--pm-ink-soft);
}

.chart-legend-value {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.chart-legend-share {
    font-size: 0.74rem;
    color: var(--pm-muted);
    font-variant-numeric: tabular-nums;
    min-width: 2.6rem;
    text-align: right;
}

/* =============================================================================
   Layout helpers
   ============================================================================= */

.grid-2 {
    display: grid;
    grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
    gap: 1.1rem;

    /* Paired panels take the height of the taller one. With `start` each card stopped wherever its
       content ran out, so the shorter of the pair left a ragged hole of page background beside its
       partner. It is worst next to the two charts, because their heights are decided by completely
       unrelated things: the bar chart has a fixed viewBox at height:auto and so scales with the
       column width, while the donut is pinned to a fixed pixel size and never grows. Measured at a
       1524px viewport that was 355px against 245px - 107px of dead space under Top Categories. The
       difference is not even always the same way round: at just over the breakpoint the donut's
       legend wraps and it is the bar chart that comes up short instead.

       Only the desktop layout is affected. Below the breakpoint further down there is a single
       column, so there is no second card to align to and nothing changes - which is why the phone
       and the MAUI WebView never showed this. */
    align-items: stretch;
}

/* A stretched card is taller than its own content, so it needs to be able to lay that content out
   down the card instead of piling it against the header. */
.grid-2 > .panel {
    display: flex;
    flex-direction: column;
}

/* The donut is the one child that should absorb the slack, so the chart sits in the middle of the
   card rather than hanging from the heading with the gap left underneath it. `.chart-donut` already
   centres its own items, so growing it is enough. Lists, tables and progress bars are deliberately
   left alone: they read as top-aligned and centring a single table row would look like a mistake. */
.grid-2 > .panel > .chart-donut {
    flex: 1 1 auto;
}

.grid-3 {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
    gap: 1.1rem;
    align-items: start;
}

@media (max-width: 1050px) {
    .grid-2 {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Mirrors the breakpoint above: only once the pair actually sits side by side is a card taller than
   its own content, and only then is there slack for a bigger ring to use. Measured on the dashboard,
   this is the difference between a 168px ring adrift in a 425px card and one that looks placed.

   It is deliberately NOT applied to the single-column layout. There the panel spans the full page,
   so a percentage width would size the ring off the window and the phone, the tablet and the MAUI
   WebView would all start rendering a donut they never had - a change nobody asked for, on the one
   layout that was already right. The upper bound keeps the ring off the legend beside it. */
@media (min-width: 1051px) {
    .grid-2 > .panel > .chart-donut > .chart-donut-figure {
        width: clamp(168px, 34%, 208px);
    }
}

/* Retained for pages still using the older three-column arrangement. */
.dashboard-columns {
    display: flex;
    gap: 1.1rem;
    flex-wrap: wrap;
    align-items: flex-start;
}

.dashboard-columns section {
    flex: 1 1 320px;
    min-width: 280px;
}

/* =============================================================================
   Activity list
   ============================================================================= */

.activity {
    list-style: none;
    margin: 0;
    padding: 0;
}

.activity li {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    padding: 0.55rem 0;
    border-bottom: 1px solid var(--pm-line);
}

.activity li:last-child {
    border-bottom: 0;
}

.activity-icon {
    width: 32px;
    height: 32px;
    flex: 0 0 auto;
    border-radius: var(--pm-radius-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--pm-expense-soft);
    color: var(--pm-expense);
}

.activity-icon.is-income {
    background: var(--pm-income-soft);
    color: var(--pm-income);
}

.activity-icon svg {
    width: 15px;
    height: 15px;
}

.activity-text {
    flex: 1 1 auto;
    min-width: 0;
    line-height: 1.25;
}

.activity-title {
    display: block;
    font-size: 0.85rem;
    font-weight: 550;
    color: var(--pm-ink);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.activity-meta {
    display: block;
    font-size: 0.74rem;
    color: var(--pm-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.activity-amount {
    flex: 0 0 auto;
    font-size: 0.86rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    color: var(--pm-expense);
    white-space: nowrap;
}

.activity-amount.is-income {
    color: var(--pm-income);
}

/* =============================================================================
   Filters
   ============================================================================= */

.filter-bar {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 0.9rem;
    flex-wrap: wrap;
    padding: 0.8rem 0.95rem;
    margin-bottom: 1.1rem;
    background: var(--pm-surface);
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius);
    box-shadow: var(--pm-shadow);
}

.filter-bar-wrap {
    align-items: flex-end;
    justify-content: flex-start;
}

.filter-bar-wrap .form-group {
    min-width: 8.5rem;
}

.filter-presets {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.filter-presets + .filter-bar {
    margin-top: 0.55rem;
}

/* .filter-presets and .form-group are both positioned by their parent's flex `gap`, so neither
   carries a bottom margin of its own. That is correct where they sit inside a filter bar, but
   Transactions drops the chip row and Categories a lone search field straight into the page flow,
   and the card that follows has no top margin either - so the two ended up flush against each
   other while every other block on those pages was spaced by 0.9rem.

   Deliberately written as an adjacency rather than a bottom margin on .filter-presets itself:
   the chips are also used inside Dashboard's .filter-bar and inside .panel-head next to the donut,
   where the container's own gap already spaces them and a margin would double up. */
.filter-presets + .collapsible,
.filter-presets + .panel,
.form-group + .collapsible,
.form-group + .panel {
    margin-top: 0.9rem;
}

.filter-range {
    display: flex;
    gap: 0.55rem;
    flex-wrap: wrap;
}

.filter-range .form-control {
    min-width: 9.5rem;
}

/* --- Chips ------------------------------------------------------------------ */
.chip {
    border: 1px solid var(--pm-line-strong);
    background: var(--pm-surface);
    color: var(--pm-ink-soft);
    border-radius: var(--pm-radius-pill);
    padding: 0.3rem 0.8rem;
    font-size: 0.79rem;
    font-weight: 500;
    line-height: 1.3;
    cursor: pointer;
    transition: background-color 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}

/* Excludes the active chip: its white label would otherwise sit on the pale hover
   background and become unreadable (the hover selector outranks .chip-active). */
.chip:hover:not(:disabled):not(.chip-active) {
    background: var(--pm-accent-soft);
    border-color: #f8c9a4;
    color: var(--pm-accent-strong);
}

.chip:disabled {
    opacity: 0.55;
    cursor: default;
}

.chip-active,
.chip-active:hover {
    background: var(--pm-ink);
    border-color: var(--pm-ink);
    color: #fff;
    font-weight: 600;
}

/* Compact chip row used inside a panel header (chart range switchers). */
.chip-mini {
    padding: 0.15rem 0.55rem;
    font-size: 0.72rem;
}

/* =============================================================================
   Buttons (overrides Bootstrap so every host matches the theme)
   ============================================================================= */

.btn {
    border-radius: var(--pm-radius-sm);
    font-size: 0.84rem;
    font-weight: 550;
    padding: 0.42rem 0.9rem;
    transition: background-color 0.14s ease, border-color 0.14s ease, color 0.14s ease;
}

.btn-sm {
    font-size: 0.78rem;
    padding: 0.24rem 0.6rem;
    border-radius: 7px;
}

.btn:focus-visible,
.form-control:focus,
.form-select:focus,
.chip:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--pm-accent-ring);
    border-color: var(--pm-accent);
}

/* The rest of the app's controls are not buttons or fields, and none of them carried a focus style,
   so they fell back to whatever ring the browser draws - which differs per engine and is easy to
   lose against a dark surface. .app-tab did have a rule, but it set text-decoration: none, which the
   base rule already sets, leaving a colour change as the only indicator; colour alone does not
   qualify. Outline rather than the box-shadow ring used above: outline is not clipped by the sticky
   bars these controls sit in, and it survives forced-colors mode, where box-shadow is dropped. */
.app-nav .nav-link:focus-visible,
.app-signout:focus-visible,
.app-install:focus-visible,
.combo-toggle:focus-visible,
.modal-pm-close:focus-visible,
.toast-close:focus-visible,
.toast-action:focus-visible,
.insight-toggle:focus-visible,
.insight-showall:focus-visible,
.insight-snooze:focus-visible {
    outline: 2px solid var(--pm-accent);
    outline-offset: 2px;
}

/* Tabs sit edge to edge in the bottom bar, so an outset ring would run under its neighbour. */
.app-tab:focus-visible {
    outline: 2px solid var(--pm-accent);
    outline-offset: -3px;
}

.btn-primary {
    background: var(--pm-ink);
    border-color: var(--pm-ink);
    color: #fff;
}

.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active {
    background: #000;
    border-color: #000;
    color: #fff;
}

.btn-primary:disabled {
    background: var(--pm-ink);
    border-color: var(--pm-ink);
    opacity: 0.45;
}

/* The label is a very dark brown rather than white. White on this orange measures 2.80:1, which
   is below the 4.5:1 minimum on what is usually the page's primary action; #2b1405 measures
   6.22:1 and leaves the brand orange itself untouched. The same value is correct in dark mode,
   so it is set once here rather than overridden per theme. */
.btn-accent {
    background: var(--pm-accent);
    border-color: var(--pm-accent);
    color: #2b1405;
}

.btn-accent:hover,
.btn-accent:focus,
.btn-accent:active {
    background: var(--pm-accent-strong);
    border-color: var(--pm-accent-strong);
    color: #2b1405;
}

.btn-outline-primary {
    color: var(--pm-ink);
    border-color: var(--pm-line-strong);
    background: var(--pm-surface);
}

.btn-outline-primary:hover,
.btn-outline-primary:focus {
    background: var(--pm-hover);
    border-color: var(--pm-ink);
    color: var(--pm-ink);
}

.btn-outline-secondary {
    color: var(--pm-ink-soft);
    border-color: var(--pm-line-strong);
    background: var(--pm-surface);
}

.btn-outline-secondary:hover {
    background: var(--pm-hover);
    color: var(--pm-ink);
    border-color: var(--pm-line-strong);
}

.btn-outline-danger {
    color: var(--pm-expense);
    border-color: #f3c7bb;
    background: var(--pm-surface);
}

.btn-outline-danger:hover,
.btn-outline-danger:focus {
    background: var(--pm-expense);
    border-color: var(--pm-expense);
    color: #fff;
}

.btn-danger {
    background: var(--pm-expense);
    border-color: var(--pm-expense);
}

/* Bootstrap's disabled tokens win over the colours above, so they are restated here. Without this
   a disabled outline button reverts to Bootstrap blue and a disabled primary turns flat grey. */
.btn:disabled,
.btn.disabled {
    opacity: 0.45;
    box-shadow: none;
}

.btn-primary:disabled {
    --bs-btn-disabled-bg: var(--pm-ink);
    --bs-btn-disabled-border-color: var(--pm-ink);
    --bs-btn-disabled-color: #fff;
}

.btn-accent:disabled {
    --bs-btn-disabled-bg: var(--pm-accent);
    --bs-btn-disabled-border-color: var(--pm-accent);
    --bs-btn-disabled-color: #2b1405;
}

.btn-outline-primary:disabled,
.btn-outline-secondary:disabled {
    --bs-btn-disabled-color: var(--pm-ink-soft);
    --bs-btn-disabled-border-color: var(--pm-line-strong);
    --bs-btn-disabled-bg: var(--pm-surface);
}

.btn-outline-danger:disabled {
    --bs-btn-disabled-color: var(--pm-expense);
    --bs-btn-disabled-border-color: #f3c7bb;
    --bs-btn-disabled-bg: var(--pm-surface);
}

/* =============================================================================
   Forms
   ============================================================================= */

.form-control,
.form-select {
    border-radius: var(--pm-radius-sm);
    border: 1px solid var(--pm-line-strong);
    font-size: 0.85rem;
    padding: 0.4rem 0.65rem;
    color: var(--pm-ink);
    background-color: var(--pm-surface);
}

.form-control::placeholder {
    color: #b3b7bd;
}

.form-control:disabled,
.form-select:disabled {
    background-color: #f6f6f7;
}

/* Password reveal --------------------------------------------------------------------------------
   The eye sits inside the field rather than beside it so the control keeps the same footprint as
   every other input and nothing in the surrounding layouts had to move. */
.password-field {
    position: relative;
    display: block;
}

/* Room for the button, so a long password never runs underneath it. */
.password-field > input {
    width: 100%;
    padding-inline-end: 2.2rem;
}

/* Chromium and Edge draw their own reveal control inside password inputs. Left alone there would be
   two eyes side by side, one of which the app does not control. */
.password-field > input::-ms-reveal,
.password-field > input::-ms-clear {
    display: none;
}

.password-reveal {
    position: absolute;
    inset-inline-end: 1px;
    inset-block-start: 1px;
    inset-block-end: 1px;
    width: 2.1rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    border-start-end-radius: var(--pm-radius-sm);
    border-end-end-radius: var(--pm-radius-sm);
    background: transparent;
    color: var(--pm-muted);
    cursor: pointer;
}

.password-reveal:hover {
    color: var(--pm-ink);
}

.password-reveal svg {
    width: 17px;
    height: 17px;
}

/* One icon is shown at a time; app-ui.js flips the container class. Doing it in CSS keeps both
   glyphs in the DOM, so the swap cannot flash while an SVG is parsed. */
.password-reveal-hide {
    display: none;
}

.password-field.is-revealed .password-reveal-show {
    display: none;
}

.password-field.is-revealed .password-reveal-hide {
    display: inline-flex;
}

/* Revealed is a state worth seeing: it means the password is on screen for anyone nearby. */
.password-field.is-revealed .password-reveal {
    color: var(--pm-accent, var(--pm-ink));
}

/* Top-aligned, not bottom-aligned. Each .form-group is a column of label -> control -> help text
   and/or validation message, and those trailing parts are optional, so bottom-aligning lines up the
   BOTTOM of each column rather than the controls: any field carrying help text is taller, so its
   label and input get pushed up by exactly the height of that text. That is what tilted Add User
   (Password sat 22.9px above Display name) and Add to the vault (Kind of item above Name).

   The dynamic case is worse than the static one: a validation message appears under a single field
   on submit, which with flex-end shoved that field 18.7px up and the row visibly jumped at the
   moment the user most needs it to hold still.

   Top-aligning is safe because every column starts with the same thing - a one-line label, or the
   .form-label span that stands in for one where the value is read-only. Labels are a fixed size, so
   aligning the tops aligns the controls, and anything optional hangs off the bottom where it
   belongs. The sole exception is the action group, handled below. */
.form-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.7rem;
    align-items: flex-start;
}

.form-group {
    display: flex;
    flex-direction: column;
    min-width: 140px;
}

.form-group-grow {
    flex: 1 1 220px;
}

.form-group-action {
    min-width: auto;
}

/* The action group is the one column with no label, so top-aligning would park its button level
   with the labels instead of with the inputs. This reserves exactly one label's worth of space
   above it to put it back on the control line.

   A non-breaking space rather than an empty string: `content: ""` on a block box produces no line
   box and therefore no height. Giving the spacer the label's own font size and gap - through the
   same custom properties the label uses - means the two cannot drift apart if that type scale is
   ever retuned. visibility:hidden keeps the space while removing the character from the
   accessibility tree, so nothing announces a stray space before the button. */
.form-row .form-group-action::before {
    content: "\00a0";
    font-size: var(--pm-field-label-size);
    font-weight: 550;
    line-height: inherit;
    margin-bottom: var(--pm-field-label-gap);
    visibility: hidden;
}

/* Inline guidance under a form row. Deliberately not an .alert: these explain what a setting will
   do, so they must not compete visually with an actual error. */
.form-hint {
    font-size: 0.76rem;
    color: var(--pm-muted);
    margin: 0 0 0.6rem;
}

.form-hint-warning {
    color: var(--pm-warning, #b45309);
    font-weight: 600;
}

/* Toggle switch ----------------------------------------------------------------------------------
   Driven by a real <input type="checkbox">, so it is focusable, keyboard-operable and announces its
   own state without script. The input is taken out of the layout but NOT hidden with
   display:none/visibility:hidden, either of which would make it unfocusable and strand keyboard
   users on a control they can see but cannot reach. */
.switch {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    cursor: pointer;
    /* Clicking anywhere on the label toggles, so the label is the real tap target. At the track's
       own height that target was 22px tall, which is below the 24px WCAG 2.2 floor and small for a
       thumb on a phone - the one place this setting actually matters. */
    min-height: 44px;
    /* The label wraps the control, so stop a drag from selecting the caption text. */
    user-select: none;
}

.switch-input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    opacity: 0;
    pointer-events: none;
}

.switch-track {
    position: relative;
    flex: 0 0 auto;
    /* 24px is the WCAG 2.2 minimum target size; the control itself clears it rather than leaning on
       the label's padding to do it. */
    width: 44px;
    height: 24px;
    border-radius: var(--pm-radius-pill);
    background: var(--pm-line-strong);
    transition: background 0.15s ease, box-shadow 0.15s ease;
}

.switch-thumb {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 2px rgba(19, 20, 24, 0.3);
    transition: transform 0.15s ease;
}

.switch-input:checked ~ .switch-track {
    background: var(--pm-accent);
}

/* 44 track - 20 thumb - 2 inset each side = 20, so the thumb rests symmetrically at both ends. */
.switch-input:checked ~ .switch-track .switch-thumb {
    transform: translateX(20px);
}

/* :focus-visible only, so the ring appears for keyboard users and not on every tap. */
.switch-input:focus-visible ~ .switch-track {
    box-shadow: 0 0 0 3px var(--pm-accent-ring);
}

.switch-input:disabled ~ .switch-track,
.switch-input:disabled ~ .switch-text {
    opacity: 0.55;
}

.switch:has(.switch-input:disabled) {
    cursor: default;
}

.switch-text {
    font-size: 0.82rem;
    color: var(--pm-ink);
}

@media (prefers-reduced-motion: reduce) {
    .switch-track,
    .switch-thumb {
        transition: none;
    }
}

/* Size and gap come from the shared tokens because .form-row's action spacer has to reproduce this
   block's exact height to keep its button on the control line. See .form-group-action::before. */
.form-group label,
.form-label {
    font-size: var(--pm-field-label-size);
    font-weight: 550;
    color: var(--pm-ink-soft);
    margin-bottom: var(--pm-field-label-gap);
}

.validation-message {
    color: var(--pm-expense);
    font-size: 0.78rem;
}

.invalid {
    border-color: var(--pm-expense) !important;
}

/* --- Searchable select (CategorySelect) ------------------------------------
   The list is positioned here as a fallback only. Once open, script switches it to fixed
   positioning measured against the field, because inside the edit dialog the scrolling body would
   clip it and on a phone there may be no room below the field at all. Keeping a workable
   absolutely-positioned default means the picker still drops down if that script never runs. */

.combo {
    display: flex;
    flex-direction: column;
    position: relative;
}

.combo-field {
    position: relative;
    display: flex;
}

.combo-input {
    width: 100%;
    /* Room for the chevron, so a long category name never runs underneath it. */
    padding-right: 1.9rem;
    text-overflow: ellipsis;
}

.combo-toggle {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 1.9rem;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--pm-muted);
    cursor: pointer;
}

.combo-toggle svg {
    width: 0.85rem;
    height: 0.85rem;
}

.combo-toggle:disabled {
    cursor: default;
    opacity: 0.45;
}

.combo-list {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    /* Above the modal backdrop (1080), so the picker works inside the edit dialog. */
    z-index: 1200;
    margin: 4px 0 0;
    padding: 0.25rem;
    max-height: 16rem;
    overflow-y: auto;
    /* A flick inside the list must not carry on and scroll the page behind it. */
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    list-style: none;
    background: var(--pm-surface);
    border: 1px solid var(--pm-line-strong);
    border-radius: var(--pm-radius-sm);
    box-shadow: var(--pm-shadow-lg);
}

.combo-option {
    padding: 0.42rem 0.6rem;
    border-radius: var(--pm-radius-sm);
    font-size: 0.85rem;
    color: var(--pm-ink);
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Where the keyboard is, and what Enter will pick. */
.combo-option.is-active {
    background: var(--pm-accent-soft);
}

/* What is currently set - shown as well as the highlight, not instead of it, so arrowing away from
   your own choice does not hide where you started. */
.combo-option.is-selected {
    font-weight: 650;
}

.combo-empty {
    padding: 0.5rem 0.6rem;
    font-size: 0.8rem;
    color: var(--pm-muted);
}

@media (max-width: 640px) {
    .form-row {
        flex-direction: column;
        /* Same reason as .pager below: the base rule wraps, and a wrapping column is multi-line, so
           its line would be sized to the widest field rather than to the row. The `width: 100%` on
           the groups below already pins them, but that only holds while every child opts in - this
           keeps the container itself honest. */
        flex-wrap: nowrap;
        align-items: stretch;
    }

    /* Thumb-sized rows on a phone: the old native picker supplied these for free. */
    .combo-option {
        padding: 0.62rem 0.7rem;
        font-size: 0.9rem;
    }

    /* flex-basis follows the MAIN axis. Once the row stacks, the 220px that was meant as a
       minimum WIDTH starts acting as a minimum HEIGHT, so every field grew to 220px tall with
       a huge dead gap under each control. Resetting flex restores natural label+input height. */
    .form-group,
    .form-group-grow,
    .form-group-action {
        flex: 0 0 auto;
        min-width: 0;
        width: 100%;
    }

    /* Once the row stacks there is nothing beside the button to line it up with, so the spacer that
       holds it on the control line turns into a blank gap above it. */
    .form-row .form-group-action::before {
        content: none;
    }
}

/* =============================================================================
   Tables
   ============================================================================= */

.table {
    margin-bottom: 0.5rem;
    --bs-table-bg: transparent;
    color: var(--pm-ink);
}

.table thead th {
    font-size: 0.67rem;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--pm-muted);
    font-weight: 700;
    border-bottom: 1px solid var(--pm-line);
    padding-top: 0.35rem;
    padding-bottom: 0.45rem;
    background: transparent;
}

.table tbody td {
    vertical-align: middle;
    color: var(--pm-ink-soft);
    border-color: var(--pm-line);
    font-size: 0.85rem;
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
}

.table tbody tr:hover > * {
    background: var(--pm-hover);
}

.table > :not(caption) > * > * {
    box-shadow: none;
}

.num {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.table-actions {
    display: flex;
    gap: 0.3rem;
    flex-wrap: wrap;
    justify-content: flex-end;
}

/* =============================================================================
   Collapsible sections (phones only)
   ============================================================================= */

/* Desktop: the summary bar does not exist and the panel is simply always open. */
.collapse-toggle {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
    pointer-events: none;
}

.collapse-summary {
    display: none;
}

@media (max-width: 640px) {
    .collapsible {
        margin-bottom: 0.9rem;
        border: 1px solid var(--pm-line);
        border-radius: var(--pm-radius);
        background: var(--pm-surface);
        box-shadow: var(--pm-shadow);
        overflow: hidden;
    }

    .collapse-summary {
        display: flex;
        align-items: center;
        gap: 0.6rem;
        /* Comfortably above the 40px touch floor used elsewhere. */
        min-height: 48px;
        padding: 0.6rem 0.9rem;
        margin: 0;
        cursor: pointer;
        user-select: none;
        font-weight: 650;
        color: var(--pm-ink);
    }

    .collapse-title {
        flex: 1 1 auto;
    }

    .collapse-badge {
        flex: 0 0 auto;
        padding: 0.1rem 0.5rem;
        border-radius: var(--pm-radius-pill);
        background: var(--pm-accent-soft);
        color: var(--pm-accent-strong);
        font-size: 0.72rem;
        font-weight: 700;
    }

    .collapse-chevron {
        flex: 0 0 auto;
        display: inline-flex;
        color: var(--pm-muted);
        transition: transform 0.18s ease;
    }

    .collapse-chevron svg {
        width: 18px;
        height: 18px;
    }

    .collapse-toggle:checked ~ .collapse-summary .collapse-chevron {
        transform: rotate(180deg);
    }

    .collapse-toggle:checked ~ .collapse-summary {
        border-bottom: 1px solid var(--pm-line);
    }

    /* Keyboard users get the same focus ring the checkbox would have had. */
    .collapse-toggle:focus-visible ~ .collapse-summary {
        outline: 2px solid var(--pm-accent);
        outline-offset: -2px;
    }

    .collapse-toggle:not(:checked) ~ .collapse-panel {
        display: none;
    }

    /* The panel already sits inside the collapsible card, so a nested card would
       double the border and padding. */
    .collapse-panel > .card,
    .collapse-panel > .panel,
    .collapse-panel > .filter-bar {
        margin: 0;
        border: 0;
        border-radius: 0;
        box-shadow: none;
    }

    /* The summary bar already states the section name, so the panel's own heading
       would read as a duplicate on phones. */
    .collapse-panel > .card > .card-body > h2:first-child,
    .collapse-panel > .panel > h2:first-child {
        display: none;
    }
}

/* Horizontal scroll container so wide tables never blow out the mobile layout. */
.table-scroll {
    overflow-x: auto;
}

/* --- Stacked table cards on phones -------------------------------------------
   A six or seven column table is unreadable at 390px: the columns that matter
   most (Amount, and the row actions) end up hidden behind a horizontal scroll.
   Below 640px each row is re-flowed into a card and every cell is labelled from
   its own `data-label`, so nothing is hidden and nothing needs side-scrolling.
   Tables opt in with `.table-stack` and every informational <td> must carry a
   `data-label`; the trailing actions cell deliberately has none. */
@media (max-width: 640px) {
    .table-stack,
    .table-stack tbody,
    .table-stack tr,
    .table-stack td {
        display: block;
        width: 100%;
    }

    .table-stack thead {
        display: none;
    }

    .table-stack tr {
        margin-bottom: 0.6rem;
        padding: 0.5rem 0.75rem;
        border: 1px solid var(--pm-line);
        border-radius: var(--pm-radius-sm);
        background: var(--pm-surface);
    }

    /* Bootstrap paints striping and row status on the CELLS. Once the row is a
       card that has to move to the card itself, or the colour is lost. */
    .table-stack.table-striped > tbody > tr:nth-of-type(odd) > * {
        --bs-table-accent-bg: transparent;
        background-color: transparent;
    }

    .table-stack tr.table-danger {
        background: var(--pm-expense-soft);
        border-color: var(--pm-expense);
    }

    .table-stack tr.table-warning {
        background: #fff7e6;
        border-color: #f0a020;
    }

    .table-stack tbody tr:hover > * {
        background: transparent;
    }

    .table-stack td {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 0.9rem;
        padding: 0.3rem 0;
        border: 0;
        text-align: right;
        font-size: 0.9rem;
    }

    .table-stack td::before {
        content: attr(data-label);
        flex: 0 0 auto;
        text-align: left;
        font-size: 0.72rem;
        font-weight: 700;
        letter-spacing: 0.06em;
        text-transform: uppercase;
        color: var(--pm-muted);
    }

    /* The trailing actions cell carries no label and gets the full card width. */
    .table-stack td:not([data-label]) {
        justify-content: flex-end;
        padding-top: 0.5rem;
    }

    .table-stack td:not([data-label])::before {
        content: none;
    }

    /* A cell with no value (blank comment or note) is noise as a card row. */
    .table-stack td:empty {
        display: none;
    }

    .table-stack .table-actions {
        width: 100%;
        justify-content: flex-end;
    }

    /* Controls inside a card row need room rather than a squeezed column. */
    .table-stack td .form-control,
    .table-stack td .form-select,
    .table-stack td .progress {
        flex: 1 1 55%;
        width: auto;
    }
}

/* --- Touch ergonomics ---------------------------------------------------------
   Keyed off `pointer: coarse` rather than width: a small laptop keeps the compact
   desktop sizing, while a large tablet still gets finger-sized targets. The audit
   found 28px buttons and a 30px sign-out; ~40px is the practical floor. */
@media (pointer: coarse) {
    .btn-sm,
    .btn.btn-sm {
        min-height: 40px;
        padding-inline: 0.85rem;
    }

    /* A default-size .btn is 36px here, so it missed the floor this block exists to
       enforce - found on an Oppo CPH2359, where the vault's "Add an item", "Lock the
       vault" and "Vault settings" were the last controls still coming out at 36px.
       Padding rather than min-height, because .btn is inline-block: min-height would
       leave the label sitting at the top of an <a class="btn">, which is not a button
       element and so does not centre its own content. Excluding .btn-sm leaves the
       rule above in charge of the small variant. */
    .btn:not(.btn-sm) {
        padding-block: 0.58rem;
    }

    .chip {
        min-height: 40px;
    }

    /* The insights strip's "Budgets" / "Transactions" shortcuts are flex items, not inline links in a
       sentence, so the WCAG 2.5.8 inline exemption does not cover them and 17px was simply too small.
       Padding rather than min-height keeps the link on the band's baseline: .insight aligns its items
       with `align-items: baseline`, and a centred inline-flex box would hang its baseline in the
       middle and shift the row. Measured cost is nil on a phone - the advisory text wraps to three
       lines there, so the band is already taller than a 40px link. */
    .insight-link {
        padding-block: 0.72rem;
    }

    /* The snooze control is a real target rather than an inline link, so it takes the full 40px
       here. It sits inside a band that is already taller than this on a phone - the advisory text
       wraps to three lines - so the row does not grow to accommodate it. */
    .insight-snooze {
        width: 40px;
        height: 40px;
    }

    /* min-height only - the labels have to keep their natural width. */
    .insight-toggle,
    .insight-showall {
        min-height: 40px;
    }

    .app-signout {
        width: 40px;
        height: 40px;
    }

    .pager-controls .btn-sm {
        min-width: 40px;
    }

    .app-nav .nav-link {
        padding-block: 0.7rem;
    }

    .form-control,
    .form-select,
    select.form-control {
        min-height: 42px;
        /* iOS Safari zooms the entire page when a focused field is under 16px.
           This is the only reliable way to stop it. */
        font-size: 16px;
    }
}

/* Text that is comfortable on a desktop sidebar becomes unreadable on a phone. */
@media (max-width: 640px) {
    .app-nav-section {
        font-size: 0.74rem;
    }

    .table thead th {
        font-size: 0.72rem;
    }

    .kpi-label {
        font-size: 0.74rem;
    }
}

/* =============================================================================
   Share bars / progress
   ============================================================================= */

.cat-name {
    display: block;
    color: var(--pm-ink);
}

/* The "from -> to" line under a category on the movement lists. Carries no colour of its own beyond
   the shared muted token, so it follows the dark theme without a second rule. */
.cat-sub {
    display: block;
    margin-top: 0.15rem;
    font-size: 0.72rem;
    color: var(--pm-muted);
}

/* The dashboard's compact budget list. The Budgets page keeps its table; this is the same data at
   panel width, where five columns would wrap into noise. */
.budget-mini {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.85rem;
}

.budget-mini-head {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 0.3rem;
}

.budget-mini-head .cat-name {
    display: inline;
    font-weight: 500;
}

/* Pushed to the end so the percentages line up down the panel regardless of name length. */
.budget-mini-pct {
    margin-left: auto;
    font-size: 0.78rem;
    color: var(--pm-muted);
}

/* The budget table's equivalent. Block rather than .budget-mini-pct's `margin-left: auto`, which
   only moves anything inside a flex row; here the label sits above the bar in a table cell. */
.budget-row-pct {
    display: block;
    margin-bottom: 0.2rem;
    font-size: 0.78rem;
    color: var(--pm-muted);
}

/* --- Insights --------------------------------------------------------------- */
/* The margin lives on the block rather than the list, so folding the strip away does not also
   remove the gap between it and the figures below. */
.insight-block {
    margin: 0 0 1rem;
}

/* Deliberately quiet: a disclosure control sitting above the thing it discloses should not compete
   with it. Text rather than an icon alone, because "4 insights" is the one piece of information a
   folded strip still has to carry - a bare chevron would hide the count as well as the bands.
   --pm-ink-soft rather than --pm-muted: measured at 2.92:1 on the light canvas, --pm-muted fails
   the 4.5:1 this size of text needs, where --pm-ink-soft reaches 8.99:1 light and 11.05:1 dark. */
.insight-toggle {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    /* Padding and line-height alone land this at 23.8px, just under the WCAG 2.5.8 minimum, and
       while folded it is the only control on screen. */
    min-height: 24px;
    margin: 0 0 0.4rem;
    padding: 0.2rem 0.35rem;
    background: none;
    border: 0;
    border-radius: var(--pm-radius-sm);
    font-size: 0.78rem;
    font-weight: 500;
    line-height: 1.4;
    color: var(--pm-ink-soft);
    cursor: pointer;
}

.insight-toggle:hover {
    background: var(--pm-hover);
}

.insight-toggle-chevron {
    flex: none;
    font-size: 0.7rem;
    line-height: 1;
}

/* Count of the bands that need attention, shown only while folded. Shape and text carry it as well
   as colour: the number is inside the pill and the word "critical" is read out beside it. */
.insight-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.2rem;
    padding: 0.05rem 0.35rem;
    border-radius: 999px;
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1.4;
}

.insight-badge-critical {
    background: #fee2e2;
    color: #991b1b;
}

/* Shown in place of the strip when the reader's own filters have emptied it. Deliberately quiet -
   --pm-ink-soft and the same size as the summary row - because it reports the absence of news, and
   anything louder would compete with the bands it appears instead of. */
.insight-filtered {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    min-height: 24px;
    margin: 0 0 0.4rem;
    padding: 0.2rem 0.35rem;
    font-size: 0.78rem;
    line-height: 1.4;
    color: var(--pm-ink-soft);
}

/* A real button rather than a link: it changes a setting, it does not go anywhere. */
.insight-showall {
    flex: none;
    min-height: 24px;
    padding: 0.1rem 0.5rem;
    background: none;
    border: 1px solid currentcolor;
    border-radius: var(--pm-radius-sm);
    font-size: 0.74rem;
    font-weight: 600;
    line-height: 1.4;
    color: inherit;
    cursor: pointer;
}

.insight-showall:hover {
    background: var(--pm-hover);
}

.insight-strip {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.4rem;
}

.insight {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    padding: 0.55rem 0.8rem;
    border-radius: var(--pm-radius);
    border: 1px solid transparent;
    font-size: 0.84rem;
    line-height: 1.35;
}

.insight-icon {
    flex: none;
    font-size: 0.9rem;
    line-height: 1;
}

.insight-text {
    flex: 1 1 auto;
}

.insight-link {
    flex: none;
    font-size: 0.78rem;
    font-weight: 500;
    text-decoration: underline;
    color: inherit;
}

/* 24px exactly, which is the WCAG 2.5.8 minimum rather than the 44px the app uses elsewhere: the
   band's own text has to stay readable beside it, and the coarse-pointer block below raises it for
   the devices where a mis-tap is actually likely. align-self rather than inheriting the band's
   baseline alignment - a centred flex box would hang its baseline in the middle and shift the row. */
.insight-snooze {
    flex: none;
    align-self: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    background: none;
    border: 0;
    border-radius: var(--pm-radius-sm);
    color: inherit;
    /* Faded rather than a different colour, so it stays legible against all three band palettes
       without needing one rule per level. 0.75 not 0.65: measured against the amber band, the
       fainter value left only 3.19:1, a hair over the 3:1 an icon control needs. */
    opacity: 0.75;
    cursor: pointer;
}

.insight-snooze svg {
    width: 14px;
    height: 14px;
}

.insight-snooze:hover {
    opacity: 1;
    background: rgba(0, 0, 0, 0.06);
}

.insight-critical {
    background: #fef2f2;
    border-color: #fecaca;
    color: #991b1b;
}

.insight-warning {
    background: #fffbeb;
    border-color: #fde68a;
    color: #92400e;
}

.insight-info {
    background: var(--pm-surface-2, #f8fafc);
    border-color: var(--pm-border);
    color: var(--pm-ink);
}

.bar {
    display: block;
    height: 5px;
    margin-top: 0.3rem;
    background: #f0f1f3;
    border-radius: var(--pm-radius-pill);
    overflow: hidden;
}

.bar-fill {
    display: block;
    height: 100%;
    background: var(--pm-accent);
    border-radius: var(--pm-radius-pill);
    transition: width 0.25s ease;
}

.bar-fill.bar-income {
    background: var(--pm-income);
}

/* Bootstrap progress bars (budget usage) restyled to match the share bars above. */
.progress {
    background: #f0f1f3;
    border-radius: var(--pm-radius-pill);
    height: 1.05rem !important;
    font-size: 0.68rem;
    overflow: hidden;
}

.progress-bar {
    border-radius: var(--pm-radius-pill);
    font-weight: 650;
}

.progress-bar.bg-success {
    background-color: var(--pm-income) !important;
}

.progress-bar.bg-warning {
    background-color: #f0a020 !important;
    color: #3d2a05;
}

.progress-bar.bg-danger {
    background-color: var(--pm-expense) !important;
}

.table-danger > * {
    --bs-table-bg: var(--pm-expense-soft);
}

.table-warning > * {
    --bs-table-bg: #fff7e6;
}

/* =============================================================================
   Pills and badges
   ============================================================================= */

.pill {
    display: inline-block;
    padding: 0.12rem 0.55rem;
    border-radius: var(--pm-radius-pill);
    font-size: 0.71rem;
    font-weight: 650;
    letter-spacing: 0.015em;
    background: #f2f3f5;
    color: var(--pm-ink-soft);
}

.pill-income {
    background: var(--pm-income-soft);
    color: #0b6b3a;
}

.pill-expense {
    background: var(--pm-expense-soft);
    color: #a63a1c;
}

/* Outcome pills, as opposed to the two above which classify money. Used by the backup schedule, the
   email-confirmation badge and the support history; all three were asking for these names already
   and silently getting the plain grey base, so "Succeeded" and "Failed" looked identical. */
.pill-positive {
    background: var(--pm-income-soft);
    color: #0b6b3a;
}

.pill-negative {
    background: var(--pm-expense-soft);
    color: #a63a1c;
}

.pill-muted {
    background: #f2f3f5;
    color: var(--pm-muted);
}

/* =============================================================================
   Pager
   ============================================================================= */

.pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.65rem 0.95rem;
    background: var(--pm-surface);
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius);
    box-shadow: var(--pm-shadow);
}

.pager-info {
    font-size: 0.82rem;
    color: var(--pm-muted);
    font-variant-numeric: tabular-nums;
}

.pager-controls {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    flex-wrap: wrap;
}

/* The page-number buttons live in their own box so narrow screens can drop them
   onto a dedicated row instead of letting "Next ›"/"Last »" wrap off on their own.
   The gap matches .pager-controls so the desktop row looks completely unchanged. */
.pager-pages {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    flex-wrap: wrap;
}

.pager-size {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.pager-size label {
    font-size: 0.78rem;
    color: var(--pm-muted);
    margin: 0;
}

.pager-size .form-select {
    width: auto;
    min-width: 4.5rem;
    padding-right: 1.8rem;
}

/* On a phone the flex row cannot fit "« First ‹ Prev 1 2 3 4 5 Next › Last »",
   so it used to wrap arbitrarily and strand "Next ›"/"Last »" on a ragged,
   left-aligned row of their own. Lay the four step buttons out as one even
   4-column row and give the page numbers a full-width row underneath.
   `order` is used so the four arrows stay visually together without changing
   the DOM order (which stays First, Prev, pages, Next, Last for assistive tech,
   and each button carries an explicit aria-label). */
@media (max-width: 720px) {
    .pager {
        flex-direction: column;
        /* The base rule wraps. A wrapping COLUMN container is multi-line, which sizes its line to
           the widest item instead of to the container and then stretches every item to that - so a
           long page-number run would widen the pager past the screen and give the whole document a
           horizontal scrollbar. Single-line keeps the line at the container's own width. */
        flex-wrap: nowrap;
        align-items: stretch;
        gap: 0.6rem;
    }

    .pager-info {
        text-align: center;
    }

    .pager-controls {
        display: grid;
        grid-template-columns: repeat(4, minmax(0, 1fr));
        gap: 0.35rem;
        align-items: stretch;
    }

    .pager-first { order: 1; }
    .pager-prev  { order: 2; }
    .pager-next  { order: 3; }
    .pager-last  { order: 4; }

    .pager-pages {
        order: 5;
        grid-column: 1 / -1;
    }

    .pager-controls > .btn {
        width: 100%;
        min-height: 44px;
        padding-inline: 0.25rem;
    }

    .pager-pages > .btn {
        flex: 0 0 auto;
        min-width: 44px;
        min-height: 44px;
    }

    .pager-size {
        justify-content: center;
    }
}

/* =============================================================================
   Alerts
   ============================================================================= */

.alert {
    border-radius: var(--pm-radius-sm);
    font-size: 0.84rem;
    border-width: 1px;
}

/* A settings panel answers an action with an alert at the bottom - saved, failed, lock is on.
   Bootstrap gives .alert margin underneath only, so the reply sat flush against the button that
   caused it and read as part of the control rather than a response to it. Bootstrap's 1rem padding
   is also set against its own 1rem type; at the 0.84rem above it leaves the box looking inflated.
   Bottom margin is dropped on the last one because the panel's own padding already closes it. */
.panel > .alert {
    padding: 0.7rem 0.85rem;
}

/* Only when something precedes it. The gap exists to separate the reply from the control that
   produced it, and an alert opening a panel has nothing to be separated from. */
.panel > .alert:not(:first-child) {
    margin-top: 0.85rem;
}

.panel > .alert:last-child {
    margin-bottom: 0;
}

.alert-danger {
    background: var(--pm-expense-soft);
    border-color: #f5cec2;
    color: #96331a;
}

.alert-success {
    background: var(--pm-income-soft);
    border-color: #bfe6d1;
    color: #0b6b3a;
}

.alert-warning {
    background: #fff7e6;
    border-color: #f7dfae;
    color: #8a5a09;
}

.alert-info {
    background: #eef6fd;
    border-color: #c9e2f7;
    color: #0f5c8f;
}

.text-muted {
    color: var(--pm-muted) !important;
}

.empty-state {
    text-align: center;
    color: var(--pm-muted);
    font-size: 0.85rem;
    padding: 2rem 1rem;
}

/* =============================================================================
   Loading skeletons
   ============================================================================= */

.skeleton-line {
    display: block;
    height: 12px;
    border-radius: var(--pm-radius-pill);
    background: linear-gradient(90deg, #eceef1 25%, #f6f7f9 37%, #eceef1 63%);
    background-size: 400% 100%;
    animation: skeleton-shimmer 1.3s ease-in-out infinite;
}

@keyframes skeleton-shimmer {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

/* A shimmer across the whole screen is a vestibular trigger, so honour the OS setting. */
@media (prefers-reduced-motion: reduce) {
    .skeleton-line {
        animation: none;
        background: #eceef1;
    }
}

.skeleton-line-xs { width: 35%; height: 10px; }
.skeleton-line-sm { width: 45%; }
.skeleton-line-md { width: 65%; height: 14px; }
.skeleton-line-lg { width: 60%; height: 26px; }
.skeleton-line-title { width: 190px; height: 30px; }

.skeleton-line + .skeleton-line {
    margin-top: 0.6rem;
}

/* Mirrors .kpi-grid exactly so the real cards drop straight into the same boxes. */
.skeleton-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 0.85rem;
    margin-bottom: 1.1rem;
}

.skeleton-card {
    background: var(--pm-surface);
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius);
    box-shadow: var(--pm-shadow);
    padding: 0.9rem 1rem 1.1rem;
}

.skeleton-rows {
    background: var(--pm-surface);
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius);
    box-shadow: var(--pm-shadow);
    padding: 0.4rem 1rem;
}

.skeleton-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--pm-line);
}

.skeleton-row:last-child {
    border-bottom: 0;
}

.skeleton-row .skeleton-line {
    margin-top: 0;
}

.skeleton-row .skeleton-line-sm {
    width: 22%;
    flex: 0 0 auto;
}

.skeleton-row .skeleton-line-md {
    flex: 1 1 auto;
    max-width: 55%;
}

/* --- Boot placeholder (shown until the SignalR circuit connects) -------------- */

/* A statically rendered page - About, the auth pages - arrives with its markup in the SAME response
   as the skeleton, so there is nothing to wait for. app-ui.js clears it, but not until
   DOMContentLoaded, which is long enough to paint a grey placeholder above a page that is already
   readable. This suppresses it in the first style pass instead, and still works if the script fails
   to load at all. Interactive content with prerender disabled emits only comment markers, which are
   not elements, so a pending data page correctly keeps its skeleton.

   [data-pm-chrome] is excluded because it is shell furniture that is present from the first byte on
   every page - the footer. Counting it as "the page rendered" made this rule match before the page
   existed, which hid the skeleton on every data page and left a blank content area with the footer
   stranded under the topbar. app-ui.js skips the same attribute, and a test pins both. Anything else
   added permanently inside .app-content must carry it too. */
.app-content:has(> :not(.app-boot):not([data-pm-chrome])) > .app-boot {
    display: none;
}

.app-boot-head {
    margin-bottom: 1.1rem;
}

.app-boot-head .skeleton-line-xs {
    width: 120px;
}

/* Only revealed once the wait stops looking normal, so a fast load never shows it. */
.app-boot-slow-note {
    display: none;
    text-align: center;
    color: var(--pm-muted);
    font-size: 0.82rem;
    margin: 0;
}

.app-boot.is-slow .app-boot-slow-note {
    display: block;
}

/* Takes the skeleton's place once waiting has stopped being an honest answer, so the content area
   is never handed back empty. An empty area says nothing at all, and a user cannot tell it apart
   from a page that loaded with no data in it - which is precisely how the blank screen read. */
.app-boot-failed {
    max-width: 30rem;
    margin: 3rem auto;
    padding: 1.5rem 1.25rem;
    border: 1px solid var(--pm-line-strong);
    border-radius: var(--pm-radius);
    background: var(--pm-surface);
    box-shadow: var(--pm-shadow);
    text-align: center;
}

/* Stated rather than left to the user agent: every rule above sets display implicitly through its
   box, and one careless `display` added here would strand this on screen under a working page. */
.app-boot-failed[hidden] {
    display: none;
}

.app-boot-failed h2 {
    margin: 0 0 0.4rem;
    font-size: 1.05rem;
}

.app-boot-failed p {
    margin: 0;
    color: var(--pm-muted);
    font-size: 0.88rem;
    line-height: 1.5;
}

.app-boot-failed .panel-actions {
    margin-top: 1rem;
}

/* Two classes to outrank `.app-boot-failed p` above without reaching for !important. */
.app-boot-failed .app-boot-failed-note {
    margin-top: 0.85rem;
    font-size: 0.78rem;
}

/* =============================================================================
   Modal dialog
   ============================================================================= */

.modal-backdrop-pm {
    position: fixed;
    inset: 0;
    z-index: 1080;
    background: rgba(24, 24, 27, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    overflow-y: auto;
}

.modal-pm {
    background: var(--pm-surface);
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius);
    box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22);
    width: 100%;
    max-width: 420px;
    /* The dialog itself scrolls, so a long form never pushes its buttons off screen. */
    max-height: calc(100vh - 2rem);
    display: flex;
    flex-direction: column;
    outline: none;
}

.modal-pm-wide {
    max-width: 620px;
}

.modal-pm-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.9rem 1.15rem;
    border-bottom: 1px solid var(--pm-line);
    flex: 0 0 auto;
}

.modal-pm-head h2 {
    font-size: 0.98rem;
    font-weight: 650;
    margin: 0;
}

.modal-pm-close {
    background: none;
    border: 0;
    color: var(--pm-muted);
    padding: 0.3rem;
    line-height: 0;
    border-radius: var(--pm-radius-sm);
    cursor: pointer;
}

.modal-pm-close:hover {
    background: var(--pm-hover);
    color: var(--pm-ink);
}

.modal-pm-close svg {
    width: 18px;
    height: 18px;
}

.modal-pm-body {
    padding: 1.05rem 1.15rem;
    overflow-y: auto;
    flex: 1 1 auto;
}

/* Buttons sit on one row and stay reachable at the bottom of the dialog. */
.modal-actions {
    display: flex;
    gap: 0.5rem;
    justify-content: flex-end;
    flex-wrap: wrap;
    margin-top: 1rem;
}

.modal-actions .btn {
    min-width: 96px;
}

.modal-note {
    font-size: 0.82rem;
    color: var(--pm-muted);
    margin: 0.5rem 0 0;
}

.modal-summary {
    background: var(--pm-canvas);
    border-radius: var(--pm-radius-sm);
    padding: 0.6rem 0.75rem;
    font-size: 0.85rem;
    margin: 0.75rem 0 0;
}

.modal-summary dl {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.2rem 0.75rem;
    margin: 0;
}

.modal-summary dt {
    color: var(--pm-muted);
    font-weight: 500;
}

.modal-summary dd {
    margin: 0;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

@media (max-width: 640px) {
    .modal-backdrop-pm {
        padding: 0;
        align-items: flex-end;
    }

    /* Full-width sheet anchored to the bottom - closer to the thumb than a centred box. */
    .modal-pm {
        max-width: none;
        max-height: 92vh;
        border-radius: var(--pm-radius) var(--pm-radius) 0 0;
        border-bottom: 0;
    }

    .modal-actions .btn {
        flex: 1 1 auto;
    }
}

/* Keeps Edit and Delete side by side, and stops them stretching full width inside a stacked card. */
.row-actions {
    display: flex;
    gap: 0.4rem;
    justify-content: flex-end;
    flex-wrap: wrap;
}

/* =============================================================================
   Pending form submit (static SSR auth forms)
   ============================================================================= */

.btn-spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    margin-right: 0.45rem;
    vertical-align: -2px;
    border: 2px solid currentColor;
    /* One transparent edge is what makes the ring look like it is spinning. */
    border-right-color: transparent;
    border-radius: 50%;
    animation: btn-spin 0.7s linear infinite;
}

@keyframes btn-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .btn-spinner {
        animation-duration: 2.4s;
    }
}

/* Disabled buttons are normally dimmed; while submitting the label must stay readable.
   The `is-submitting` class is added by app-ui.js on the static-SSR host; an interactive host renders
   its own spinner into the button instead, so match that too rather than leaving the installed app
   with a greyed-out, unreadable "Signing you in...". */
.is-submitting .btn[disabled],
.btn[disabled]:has(.btn-spinner) {
    opacity: 1;
    cursor: progress;
}

/* Stops the fields being edited while their values are already on the way to the server. */
.is-submitting .form-control,
.is-submitting .form-select {
    pointer-events: none;
    background-color: var(--pm-canvas);
}

.form-pending-status {
    font-size: 0.82rem;
    color: var(--pm-muted);
    margin: 0.6rem 0 0;
}

.quick-add {
    margin-bottom: 1.1rem;
}

/* =============================================================================
   Anonymous auth pages (AuthLayout: login / register)
   ============================================================================= */

.auth-page {
    min-height: 100vh;
    display: flex;
    /* Column so the About footnote sits under the card. As a row it would be placed
       beside it, splitting the centred composition in two. */
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    background: radial-gradient(1200px 600px at 15% -10%, #ffe9d6 0%, transparent 55%),
                radial-gradient(900px 500px at 95% 110%, #ffe2cc 0%, transparent 55%),
                #f7f6f3;
}

.auth-card {
    width: 100%;
    max-width: 25rem;
    background: var(--pm-surface);
    border: 1px solid var(--pm-line);
    border-radius: 18px;
    padding: 2rem 1.9rem;
    box-shadow: 0 18px 44px rgba(24, 20, 16, 0.1);
}

.auth-brand {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
    font-size: 0.95rem;
    font-weight: 650;
    color: var(--pm-ink);
    margin-bottom: 1.35rem;
}

.auth-card h1 {
    font-size: 1.4rem;
    font-weight: 650;
    margin-bottom: 0.3rem;
    text-align: center;
}

.auth-lede {
    text-align: center;
    font-size: 0.83rem;
    color: var(--pm-muted);
    margin-bottom: 1.3rem;
}

.auth-card .form-group {
    margin-bottom: 0.8rem;
}

.auth-card .form-group label {
    display: block;
}

.auth-card .btn-primary {
    width: 100%;
    margin-top: 0.5rem;
    padding: 0.5rem 0.9rem;
}

.auth-card .auth-alt {
    margin-top: 1.1rem;
    margin-bottom: 0;
    text-align: center;
    font-size: 0.84rem;
    color: var(--pm-muted);
}

/* Configuration/server errors are long and must stay readable rather than overflow the card. */
.auth-card .alert {
    font-size: 0.83rem;
    word-break: break-word;
}

/* The one link out of the login screen that is not another form to fill in. Deliberately
   quiet and below the card: someone who came here to sign in should not be offered a
   detour, but someone weighing up whether to hand over a password should be able to find
   out who is asking. */
.auth-aside {
    margin: 1.15rem 0 0;
    font-size: 0.8rem;
}

.auth-aside a {
    color: var(--pm-accent-strong);
    text-decoration: none;
}

.auth-aside a:hover,
.auth-aside a:focus-visible {
    text-decoration: underline;
}

/* =============================================================================
   Dark theme completions
   -----------------------------------------------------------------------------
   Everything above is written against the light palette. Most of it inverts by
   itself because it is expressed in tokens, but these rules pin literal colours
   or use a token in a role that does not survive the flip, so each needs a dark
   counterpart. Without them the app is not merely off-palette, it is unusable in
   places: .btn-primary fills with var(--pm-ink), which is near-white in dark, so
   its #fff label would be white on white.

   Grouped and kept last so the dark theme reads as one unit. Specificity already
   wins over the single-class rules above; the position is for maintenance.
   ============================================================================= */

/* Solid buttons: the light theme fills with ink and writes in white. In dark, ink
   IS the light colour, so the fill and the label swap roles. */
html[data-organizer-theme="dark"] .btn-primary {
    background: var(--pm-ink);
    border-color: var(--pm-ink);
    color: var(--pm-canvas);
}

html[data-organizer-theme="dark"] .btn-primary:hover,
html[data-organizer-theme="dark"] .btn-primary:focus,
html[data-organizer-theme="dark"] .btn-primary:active {
    background: #ffffff;
    border-color: #ffffff;
    color: var(--pm-canvas);
}

html[data-organizer-theme="dark"] .btn-primary:disabled {
    --bs-btn-disabled-bg: var(--pm-ink);
    --bs-btn-disabled-border-color: var(--pm-ink);
    --bs-btn-disabled-color: var(--pm-canvas);
}

/* .btn-accent needs no dark override: the accent fill is the same orange in both themes and the
   dark label is now set on the base rule, which covers both. */

/* The danger fills keep a deep red in dark: --pm-expense is lightened above for the
   many places it is text, and white does not read on the lightened value. */
html[data-organizer-theme="dark"] .btn-danger,
html[data-organizer-theme="dark"] .btn-outline-danger:hover,
html[data-organizer-theme="dark"] .btn-outline-danger:focus {
    background: #c03d1e;
    border-color: #c03d1e;
    color: #ffffff;
}

html[data-organizer-theme="dark"] .btn-outline-danger {
    border-color: #7d3a2b;
}

html[data-organizer-theme="dark"] .btn-outline-danger:disabled {
    --bs-btn-disabled-border-color: #7d3a2b;
}

html[data-organizer-theme="dark"] .chip:hover:not(:disabled):not(.chip-active) {
    border-color: #8a4f2c;
}

/* Same trap as .btn-primary: the selected chip fills with var(--pm-ink) and writes #fff, and ink
   is the near-white end of the scale in dark, so the selected category filter measured 1.14:1 -
   invisible. Matching .btn-primary keeps "filled with ink" reading as the selected state in both
   themes rather than introducing a second selected-chip colour. */
html[data-organizer-theme="dark"] .chip-active,
html[data-organizer-theme="dark"] .chip-active:hover {
    color: var(--pm-canvas);
}

/* KPI deltas and pills: dark inks on pale washes in light, so both halves flip. */
html[data-organizer-theme="dark"] .kpi-delta-up,
html[data-organizer-theme="dark"] .pill-income,
html[data-organizer-theme="dark"] .pill-positive {
    color: #5ee79b;
}

html[data-organizer-theme="dark"] .kpi-delta-down,
html[data-organizer-theme="dark"] .pill-expense,
html[data-organizer-theme="dark"] .pill-negative {
    color: #ffa78f;
}

html[data-organizer-theme="dark"] .kpi-delta-flat,
html[data-organizer-theme="dark"] .pill,
html[data-organizer-theme="dark"] .bar,
html[data-organizer-theme="dark"] .progress {
    background: #2b3446;
}

/* Warning has no token pair, so it is spelled out in both themes. */
html[data-organizer-theme="dark"] .alert-warning {
    background: #3a2f14;
    border-color: #7d6420;
    color: #f5c657;
}

html[data-organizer-theme="dark"] .table-stack tr.table-warning {
    background: #3a2f14;
    border-color: #b8871f;
}

html[data-organizer-theme="dark"] .table-warning > * {
    --bs-table-bg: #3a2f14;
}

html[data-organizer-theme="dark"] .alert-info {
    background: #14293c;
    border-color: #2c567d;
    color: #8fc4f0;
}

html[data-organizer-theme="dark"] .alert-success {
    border-color: #2c5f47;
    color: #6fe3a8;
}

html[data-organizer-theme="dark"] .alert-danger {
    border-color: #7d3a2b;
    color: #ffa78f;
}

/* Insight bands are pale washes in light mode, which read as glare on a dark page. Matched to the
   alert palette above so the two warning languages on the dashboard stay the same colour. */
html[data-organizer-theme="dark"] .insight-critical {
    background: #3a1d17;
    border-color: #7d3a2b;
    color: #ffa78f;
}

html[data-organizer-theme="dark"] .insight-warning {
    background: #3a2f14;
    border-color: #7d6420;
    color: #f5c657;
}

html[data-organizer-theme="dark"] .insight-info {
    background: #2b3446;
    border-color: var(--pm-line);
}

/* The shimmer is a light-grey sweep; on a dark page it reads as a row of bright bars. */
html[data-organizer-theme="dark"] .skeleton-line {
    background: linear-gradient(90deg, #2b3446 25%, #3b465b 37%, #2b3446 63%);
    background-size: 400% 100%;
}

@media (prefers-reduced-motion: reduce) {
    html[data-organizer-theme="dark"] .skeleton-line {
        background: #2b3446;
    }
}

/* The app shell's two bars are translucent white over the page, which is the whole point of the
   backdrop blur - but white is written literally rather than as a token, so in dark mode they
   stayed white while the text over them turned near-white. The top bar's title was the visible
   casualty: #edf0f7 on white is about 1.1:1, i.e. invisible.

   These use --pm-surface's dark value rather than the token itself because the alpha has to be
   baked into the literal for the blur to still show through; a var() cannot supply a component of
   rgba(). Kept at the same alpha as the light rules so the blur reads identically.

   No media query: .app-tabbar is display:none above 720px, so giving it a colour at every width
   costs nothing and keeps the two bars defined together. */
html[data-organizer-theme="dark"] .app-topbar {
    background: rgba(32, 39, 53, 0.86);
}

html[data-organizer-theme="dark"] .app-tabbar {
    background: rgba(32, 39, 53, 0.94);
}

/* --- App footer -------------------------------------------------------------
   Used once, at the foot of the profile page. It lives inside .app-content, so it
   inherits the bottom padding that clears the fixed tab bar on phones and needs no
   breakpoint of its own. Muted and hairline-separated: it is a legal/attribution
   line, not a destination, and must not compete with the page it closes. */
.app-footer {
    margin-top: 2.25rem;
    padding-top: 1rem;
    border-top: 1px solid var(--pm-line);
    text-align: center;
    color: var(--pm-muted);
    font-size: 0.76rem;
    line-height: 1.5;
}

.app-footer-line {
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    /* Doubles as the gap either side of the separator, so the two halves stay
       evenly spaced when they wrap onto separate lines on a narrow phone. */
    gap: 0.45rem;
}

.app-footer-sep {
    opacity: 0.55;
}

/* The About page's main entry point. Inherits the footer's colour rather than taking
   the link accent: a bright link in the footer competes with the page above it for a
   destination almost nobody is looking for. It announces itself on hover and focus,
   which is when someone is looking. */
.app-footer-about {
    color: inherit;
    text-decoration: none;
}

.app-footer-about:hover,
.app-footer-about:focus-visible {
    color: inherit;
    text-decoration: underline;
}

/* --- Theme picker -----------------------------------------------------------
   Three cards rather than a <select>, because the choice is visual: the swatch
   shows what each option does without the user having to apply it to find out.
   Real <button role="radio"> elements, so they are reachable by keyboard and
   announced as a group. auto-fit lets the row become a column on a narrow phone
   without a breakpoint. */
.theme-choices {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
    gap: 0.6rem;
    margin-bottom: 0.25rem;
}

.theme-choice {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.15rem;
    padding: 0.7rem 0.8rem;
    border: 1px solid var(--pm-line-strong);
    border-radius: var(--pm-radius-sm);
    background: var(--pm-surface);
    color: var(--pm-ink);
    text-align: left;
    cursor: pointer;
    transition: border-color 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
}

.theme-choice:hover:not(:disabled) {
    background: var(--pm-hover);
}

.theme-choice:disabled {
    cursor: default;
    opacity: 0.6;
}

.theme-choice.is-selected {
    border-color: var(--pm-accent);
    box-shadow: 0 0 0 3px var(--pm-accent-ring);
}

/* The swatches are the only colours on the page that must NOT follow the current
   theme - they are a preview of the other options, so they stay literal. */
.theme-choice-swatch {
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    border: 1px solid var(--pm-line-strong);
    margin-bottom: 0.35rem;
}

.theme-swatch-light {
    background: #ffffff;
}

.theme-swatch-dark {
    background: #16181d;
}

.theme-swatch-system {
    background: linear-gradient(135deg, #ffffff 0 50%, #16181d 50% 100%);
}

.theme-choice-label {
    font-weight: 600;
    font-size: 0.86rem;
}

.theme-choice-note {
    font-size: 0.74rem;
    color: var(--pm-muted);
}

/* --- Document pages ----------------------------------------------------------
   About, Terms, Privacy, Copyright, Disclaimer and Contact. Six pages that are
   read rather than operated, and the only ones in the app with no table, no form
   and nothing to click but a link.

   A line of prose has a readable length and a window does not, so the width of the
   window and the width of a line are settled separately here. The paragraphs used to
   try to settle both at once and lost: a card stretched to 1600px with its text
   stopping at 535px meant two thirds of every panel on the Disclaimer page was blank.

   So the cap moved off the paragraph and onto a COLUMN, and a wide window is spent
   running more columns rather than one long one. The cards are the column content, which
   is why no page needed a wrapper per column - they flow into whatever columns exist.

   A GRID of cards was tried first and is wrong here, for a reason worth recording. A grid
   lays out in rows, so every card in a row starts level with the tallest card in it and
   the next row starts below all of them. That is fine when cards are roughly equal and
   ruinous when they are not: the Copyright page has a 1032px list of components beside a
   223px paragraph, so the row was 1032px tall, the short card left 800px of white under
   itself, and the card after it began most of a screen further down with nothing beside
   it. Measured across these six pages the tallest card is three to five times the
   shortest, so the holes were not an edge case, they were the normal result.

   Multi-column has no rows. Each column is filled to the brim before the next one starts,
   so a card follows the card above it by one gap and nothing else, whatever either of them
   weighs. The price is reading order - a column is read to the bottom and then back up to
   the top of the next - and it is worth paying here because every card on these pages
   carries its own heading. Clause 4 announces itself as clause 4 wherever it has landed,
   which is not true of a row of unlabelled tiles.

   The count is stated and the page is then capped at that many measures. Both halves are
   needed and they fail in opposite directions. Columns always stretch to share whatever
   the container gives them, so a count on its own hands the line length back to the window
   - two columns of a 1621px page are 801px, or 97 characters, which is the problem the
   measure exists to prevent. A cap on its own cannot raise the count. Together the columns
   are as wide as they can be up to one measure and no wider, and the leftover becomes a
   margin only once they have reached it. Leaving the count to column-width instead was
   tried: the browser divides by that width and stretches the remainder, so the real column
   lands anywhere between the token and twice it, and 1760px produced 99-character lines.

   Two columns is the end of it, and that is a fact about these six pages rather than a
   taste. A column is filled in document order, so the cards cannot be rearranged to even
   the columns up, and one card much taller than the rest decides the whole shape. About's
   "What it does" is 1358px of a 3945px page - a third of it in one unbreakable block - and
   no way of cutting that sequence into three contiguous pieces gets the tallest below
   2031px, so the third column took one small card and left 1726px of white beside the
   other two. Measured over all six pages: at three columns the worst ragged bottom was
   2151px, at two it was 423px and the typical one about 280px. Copyright says the same
   thing more simply - four cards do not fill three columns.

   Past 1234px of content the page therefore stops widening and centres. That is about 11%
   of a 1920px window spent on margins and more on anything wider, which is the price of
   columns that stay full; the alternative was a third column that is empty half the time.

   break-inside keeps the page's furniture whole. Cards are the opposite case. A card that
   cannot be broken has to land in one column or the other entire, so the browser reaches one
   that will not fit in what is left of the first column, moves the whole thing across, and
   the shortfall stays behind as a hole above the index at the foot of the page. Measured
   over the six pages those holes ran from 102px to 525px - a third of a screen on Copyright.
   Letting a card's prose flow between the columns, which is the thing columns are for, takes
   them to between 0 and 157px and 0 to 13px at 1920px and wider. The price is that one card
   per page is cut at the column boundary and carries on at the top of the next; the cut edge
   draws no border, because box-decoration-break defaults to slicing the card's decoration
   rather than repeating it, so the card reads as continuing rather than as two cards.

   What must not break is listed with the rules below. A sentence, a list item and a heading
   are each indivisible, a heading may not be the last thing in a column, and the sideloading
   warning may not start one - it belongs beside the button it is warning about.

   Below 1280px it is one centred column of the measure, which is what the page did before
   any of this and what a tablet wants. On a phone held upright the measure is wider than
   the viewport, so it cannot bind, and the mobile web, the installed PWA and the Android
   WebView render exactly what they rendered before. */
.doc-page {
    max-width: var(--pm-doc-width);
    margin-inline: auto;
}

/* Two columns from 1280px. The number is a window width but the thing being decided is the
   content width, and the two differ by a constant here: the sidebar is 248px on every
   desktop width, signed in or out, so one maps onto the other. 1280px of window is 981px of
   content, which is two columns of 482px - 61 characters, the narrowest they ever get.
   Measured across the six pages they run between 61 and 77 characters from 1280px up. */
@media (min-width: 1280px) {
    .doc-page {
        column-count: 2;
        column-gap: 1.1rem;
        max-width: calc(2 * var(--pm-doc-width) + 1.1rem);
    }

    .doc-page > * {
        break-inside: avoid;
    }

    /* More specific than the rule above, so the furniture stays atomic while the cards flow.
       Only the card itself is released: everything inside it that a reader would notice being
       cut is pinned by the rules that follow. */
    .doc-page > .panel {
        break-inside: auto;
    }

    /* Having allowed the break, this says what the two halves should look like.

       By default a cut box keeps its padding, its rounded corners and its border on the
       outer edges only: the top half gets no bottom padding and no bottom corners, the
       bottom half gets no top padding and no top corners. Its *side* borders, though, are
       drawn on both halves - so the download card's accent stripe ran the full height of
       the continuation and ended in a square, text-flush corner, which is the dent the
       cut was being read as. `clone` draws the whole decoration again on each half: both
       get the full border, the 14px corners and the 1.05rem of padding, so each one reads
       as a finished card rather than as something torn.

       The cost is honest: the second half carries no heading, so it reads as a short
       follow-on card. The alternative is a square edge with text pressed against it. This
       only costs 5-18px of page height, and it leaves the columns as level as before. */
    .doc-page > .panel {
        -webkit-box-decoration-break: clone;
        box-decoration-break: clone;
    }

    .doc-page .panel-head,
    .doc-page .panel > p,
    .doc-page .panel li,
    .doc-page .panel h3,
    .doc-page .legal-warning,
    .doc-page .app-download-lead,
    .doc-page .app-download-actions {
        break-inside: avoid;
    }

    /* A heading at the foot of a column is a promise the column does not keep. */
    .doc-page .panel-head,
    .doc-page .panel h3 {
        break-after: avoid;
    }

    /* Keeps the "install it from that folder and nowhere else" warning in the column that
       holds the download button, rather than letting it open the next one on its own where
       it reads as a general remark instead of a condition on the thing just offered. */
    .doc-page .legal-warning {
        break-before: avoid;
    }

    /* The page's furniture, as opposed to its content: the heading, the "in effect from"
       line and the index of the other notices. These are not cards and have nothing to say
       in a column of their own.

       Only these three. A spanning element cuts the flow in two and each half balances its
       own columns, so anything that spans from the middle of a document leaves a short run
       above it and a shorter one below - which is how the holes got there in the first
       place. All three of these sit at one end of the page or the other, so the cards
       between them stay one unbroken run. The summary that opens a notice and the hero that
       opens About used to span as well and now do not: both are cards with real prose in
       them, and at the full width of the page that prose was running to 200 characters. In
       a column they are simply the first card, which is where they were already read. */
    .doc-page > .page-header,
    .doc-page > .legal-meta,
    .doc-page > .legal-nav {
        column-span: all;
    }

    /* A card's bottom margin belongs to its last fragment, so the cut half that now ends a
       column contributes none and the index underneath lands flush against it - a square cut
       edge touching the index's rounded top corners, which reads as one dented box rather
       than two. The gap above the columns and the gap between cards are both 1.1rem, so the
       index asks for the same and the rhythm holds whether the last card was cut or not.
       Inside the media query because the single-column pages below 1280px are still getting
       that gap from the margin of the whole card above, and would otherwise double it. */
    .doc-page > .legal-nav {
        margin-top: 1.1rem;
    }

    /* The two bits of prose that stayed spanning. A subtitle sits under its heading rather
       than in a card, so a measure on it reads as typography rather than as a card that
       failed to fill; and the index is a list of short notes, which is a grid's job. */
    .doc-page > .page-header .page-subtitle {
        max-width: var(--pm-doc-width);
    }

    .doc-page > .legal-nav .legal-index {
        grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
        column-gap: 1.1rem;
    }
}

/* For the pages that are one card and a heading - the 404 and the error page. Nothing on
   them has anything to follow, so a second column would only stretch the heading that
   spans it and leave the card alone under one half of it. Declared after the media query
   rather than inside it: same specificity, so it is source order that lets this opt back
   out, and a wider band added above would be covered too. */
.doc-page--single {
    columns: 1;
    max-width: var(--pm-doc-width);
}

/* --- Document masthead ---
   The three lines a document opens with: what kind of document it is, its title, and
   one line saying what it settles. Rendered by DocumentPageHeader so all six pages
   carry the same one.

   An app screen's header and a document's masthead want opposite things. A screen's
   has to sit quietly over the thing you came to operate, which is why .page-header is
   a modest 1.45rem with its buttons on the right. A document has nothing to operate -
   the title IS the page - and at the screen size it read as a card that had lost its
   panel. So the type steps up, the kicker above it says which of the six you are on
   before the title has been read, and a hairline underneath separates the masthead
   from the columns instead of leaving it floating over them.

   Scoped to .doc-page throughout. Every other page in the app keeps the header it
   already had. */
.doc-masthead-text {
    /* min-width:0 so a long title wraps instead of stretching the flex row it sits in. */
    min-width: 0;
}

.doc-kicker {
    margin: 0 0 0.3rem;
    font-size: 0.7rem;
    font-weight: 650;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--pm-accent-strong);
}

.doc-page > .page-header {
    padding-bottom: 0.85rem;
    border-bottom: 1px solid var(--pm-line-strong);
}

.doc-page > .page-header h1 {
    font-size: 1.7rem;
    line-height: 1.2;
    /* Tightened because the size was raised: display type set at body tracking looks
       loose, and the same -0.01em at 1.45rem would have been invisible. */
    letter-spacing: -0.012em;
}

.doc-page > .page-header .page-subtitle {
    margin-top: 0.4rem;
    font-size: 0.92rem;
    line-height: 1.5;
}

/* A section heading inside a document, as against a card heading on a screen. The app's
   h2 is 0.98rem, which is right for a panel you are working in and too quiet for a
   clause of a contract - at the old size the headings on Terms were barely louder than
   the sentences under them, and a page of small print with no visible structure is the
   thing people mean when they call a legal page unreadable. The accent rule on the left
   is what makes the sections countable down a column at a glance. */
.doc-page > .panel > .panel-head > h2 {
    position: relative;
    padding-left: 0.72rem;
    font-size: 1.06rem;
    letter-spacing: -0.008em;
}

.doc-page > .panel > .panel-head > h2::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.18em;
    bottom: 0.18em;
    width: 3px;
    border-radius: 2px;
    background: var(--pm-accent);
}

/* --- About page --------------------------------------------------------------
   The most mixed of the six: prose, grouped lists and a grid of fact cards, and the
   one that gains most from the columns above - it has eight cards to pack where the
   legal pages have four or five, and the page is 44% shorter for it. Its paragraphs
   are capped by the column like everyone else's, because the alternative was them
   looking exactly as unfinished as the legal pages' did. The fact cards stay in a
   single file inside their column and are the better for it - see .about-facts. The
   hero is the first card in the flow rather than a spanning band: at the full width
   of the page its lede ran past 200 characters. No colour of its own: every value
   here comes from a token already measured for contrast in both themes. */
.about-hero {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
}

/* The same mark as the sidebar and the login screen, enlarged. Overriding the two
   sizes rather than cloning the rule keeps the gradient and shadow in one place. */
.about-hero-mark {
    width: 52px;
    height: 52px;
    border-radius: 14px;
}

.about-hero-mark svg {
    width: 30px;
    height: 30px;
}

/* min-width:0 so a long word in the lede wraps instead of stretching the flex row. */
.about-hero-text {
    min-width: 0;
}

.about-hero-name {
    margin: 0 0 0.35rem;
    font-size: 1.15rem;
    font-weight: 650;
}

.about-hero-lede {
    margin: 0 0 0.5rem;
    color: var(--pm-ink-soft);
    font-size: 0.88rem;
    line-height: 1.6;
}

.about-hero-meta {
    margin: 0;
    font-size: 0.76rem;
    color: var(--pm-muted);
}

/* Call to action. Wraps to a column on a narrow screen rather than letting two buttons squeeze
   themselves into unreadable slivers. */
.about-cta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-top: 1.1rem;
}

/* Grow to share the line, and so fill it when they wrap to one each on a phone - two 11rem buttons
   stacked against a ragged right edge look like an afterthought. Capped so they do not become
   absurd banners on a wide monitor. The padding buys a ~46px tap target: the default .btn height of
   36px is under the 44px minimum, which is the wrong place to be stingy on the one control this
   page exists to get pressed. */
.about-cta .btn {
    flex: 1 1 11rem;
    max-width: 18rem;
    padding: 0.62rem 1.25rem;
    font-size: 0.92rem;
}

.about-cta-closing {
    margin-top: 1.4rem;
}

.about-cta-note {
    margin: 0.55rem 0 0;
    font-size: 0.78rem;
    color: var(--pm-muted);
}

/* Running prose, as opposed to the lists and fact cards elsewhere on the page.

   It carries no max-width of its own, and that is the fix rather than an omission. It used to cap
   itself at 68ch, which kept the line readable but left the panel around it stretched to the window
   - a card with a third of itself blank, on every panel of every one of these pages. The measure
   now comes from .doc-page, which narrows the CARD to the same width, so the text fills what it
   sits in at every screen size instead of only at one.

   The .legal-* twins are listed alongside rather than given rules of their own: the legal pages are
   the same kind of document as this one and must read identically, and two definitions of "body
   paragraph" would drift the moment either was tuned. They are named separately only so the legal
   pages do not have to describe themselves as "about". */
.about-prose,
.legal-prose {
    margin: 0 0 0.85rem;
    font-size: 0.92rem;
    line-height: 1.6;
    color: var(--pm-ink-soft);
}

.about-prose:last-child,
.legal-prose:last-child {
    margin-bottom: 0;
}

.about-list,
.legal-list {
    margin: 0;
    padding-left: 1.15rem;
    display: grid;
    gap: 0.4rem;
    font-size: 0.86rem;
    line-height: 1.55;
    color: var(--pm-ink-soft);
}

/* The licence notices are a reference list, not prose - bullets add nothing. */
.about-list-plain {
    list-style: none;
    padding-left: 0;
}

/* Splits "What it does" into the same groups the sidebar uses. Deliberately quieter
   than .panel-head: these sit INSIDE a panel, and giving them the same weight as the
   panel's own heading would read as four panels crammed into one. The top margin is
   dropped on the first so the group does not start with a gap under the heading. */
.about-group-title,
.legal-group-title {
    margin: 1.4rem 0 0.5rem;
    font-size: 0.8rem;
    font-weight: 650;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--pm-muted);
}

.about-group-title:first-of-type,
.legal-group-title:first-of-type {
    margin-top: 0;
}

/* Sits between a group heading and its list, so it needs the list's voice rather than
   the panel note's - same size and colour, without the note's separating top margin. */
.about-group-note {
    margin: 0 0 0.5rem;
    font-size: 0.84rem;
    line-height: 1.55;
    color: var(--pm-ink-soft);
}

/* After a list, it is a footnote to that list rather than an introduction to it. */
.about-list + .about-group-note {
    margin: 0.55rem 0 0;
}

/* auto-fit rather than a breakpoint, so the number of claims can change without the
   layout needing to be told. 20rem is picked for the line length it produces, not to
   hit a column count: below it the cards start reading as columns of ~38 characters,
   which is where a short paragraph becomes a stack of fragments.

   Inside .doc-page this now resolves to a single file at every width - the column is
   38rem, so a second 20rem track cannot fit - and the seven claims read as seven
   bands rather than three-then-three-then-one with a half-empty last row. The
   expression below is kept rather than replaced by `1fr`: the column count should
   still follow --pm-doc-width if that is ever widened, and the guard inside it is
   load-bearing on a phone at any width. */
.about-facts {
    display: grid;
    /* The max() caps it at three columns without a media query: a column can never be
       narrower than a third of the row, so a fourth can never fit. Below that the 20rem
       floor wins and the count drops on its own. Without the cap a 1600px window gave
       4 + 2, leaving the last row visibly half empty.

       The outer min(100%, ...) is what keeps that floor honest. A minmax() minimum is a
       hard one: on a 320px screen the 20rem floor is wider than the panel it sits in, so
       the track refuses to shrink and pushes 33px of the page off the right edge. Clamping
       to 100% lets the single column fall back to the width actually available. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, max(20rem, (100% - 2.8rem) / 3)), 1fr));
    gap: 0.9rem 1.4rem;
}

.about-fact-title {
    margin: 0 0 0.3rem;
    font-size: 0.88rem;
    font-weight: 650;
}

.about-fact p {
    margin: 0;
    font-size: 0.84rem;
    line-height: 1.55;
    color: var(--pm-ink-soft);
}

/* --- Get the app (About page) ------------------------------------------------
   The only card on these six pages that asks the reader to do something outside the
   app, and the only one whose instructions have a cost if they are misread - so it is
   built to be scanned in the order the decision is actually made: what this is, where
   it comes from, the button, what Android will say, and what not to trust.

   Marked with the accent border the summary panels use, because it is the one card on
   About that is an offer rather than a statement, and an offer buried in eight
   identical panels is one nobody finds. */
.app-download {
    border-left: 3px solid var(--pm-accent);
}

/* Said next to the heading rather than in the prose under it, because "not on the
   Store" is the fact that explains everything else in the card and the reason someone
   would otherwise assume the app is unreleased or unsafe. Quiet type, not a red
   warning: it is a status, and the warning in this card is further down and real. */
.app-download-badge {
    padding: 0.14rem 0.5rem;
    border: 1px solid var(--pm-line-strong);
    border-radius: var(--pm-radius-pill);
    font-size: 0.68rem;
    font-weight: 650;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--pm-muted);
    white-space: nowrap;
}

.app-download-lead {
    display: flex;
    align-items: flex-start;
    gap: 0.9rem;
}

/* The brand-mark tile at the app's largest size, between the sidebar's 28px and the
   About hero's 52px. Big enough to read as the thing you are being offered, small
   enough that the sentence beside it is still the first thing read. */
.app-download-mark {
    width: 44px;
    height: 44px;
    border-radius: 12px;
}

.app-download-mark svg {
    width: 25px;
    height: 25px;
}

/* min-width:0 so a URL or a long word wraps rather than stretching the flex row. */
.app-download-text {
    min-width: 0;
}

.app-download-actions {
    margin-top: 0.75rem;
    display: flex;
    align-items: center;
    gap: 0.6rem 0.75rem;
    flex-wrap: wrap;
}

.app-download-action {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
}

.app-download-action-icon {
    width: 17px;
    height: 17px;
    flex: 0 0 auto;
}

/* Where the link goes, in words, beside the button rather than hidden in its href. A
   reader being asked to install software from outside the Store should not have to
   hover a button to find out who is hosting the file. */
.app-download-host {
    font-size: 0.78rem;
    color: var(--pm-muted);
}

/* "Installing it" is the only h3 in this card, so :first-of-type above matches it and
   zeroes the margin that separates a group from what precedes it. That rule exists for
   headings sitting directly under a panel head, which already provides the gap; here
   the heading follows a flex row with no bottom margin of its own, and without this it
   touches the download button. Put back the 1.4rem every other group heading gets. */
.panel.app-download > .about-group-title {
    margin-top: 1.4rem;
}

/* Numbered, and the only numbered list on these pages. The other lists are sets where
   the order does not matter; this one is a sequence, and a reader who takes step three
   before step two is standing in front of an Android permission prompt wondering what
   it is for. */
.app-download-steps {
    margin: 0;
    padding-left: 1.25rem;
    display: grid;
    gap: 0.45rem;
    font-size: 0.86rem;
    line-height: 1.55;
    color: var(--pm-ink-soft);
}

.app-download-steps::marker,
.app-download-steps > li::marker {
    font-weight: 650;
    color: var(--pm-accent-strong);
}

.app-download-steps code {
    padding: 0.05rem 0.3rem;
    border-radius: 5px;
    background: var(--pm-hover);
    border: 1px solid var(--pm-line);
    font-size: 0.92em;
    color: var(--pm-ink);
}

/* --- Build diagnostics ---
   A definition list rather than a table: it is a set of name/value pairs, and a
   table would need a header row saying nothing the labels do not already say. */
.about-meta {
    margin: 0;
    display: grid;
    gap: 0.5rem;
}

.about-meta-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.2rem 0.9rem;
    padding-bottom: 0.45rem;
    border-bottom: 1px dashed var(--pm-line);
}

.about-meta-row:last-child {
    padding-bottom: 0;
    border-bottom: 0;
}

.about-meta dt {
    margin: 0;
    font-size: 0.76rem;
    font-weight: 500;
    color: var(--pm-muted);
}

.about-meta dd {
    margin: 0;
    font-size: 0.84rem;
    font-weight: 550;
}

/* break-all, not break-word: this is a URL, and the useful failure mode is an ugly
   wrap rather than a line that pushes the panel wider than the phone. */
.about-meta-mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.78rem;
    word-break: break-all;
}

/* Icon plus words. The reading is never carried by colour alone, for the same
   reason the topbar sync light is not: see Icon.CloudOff. */
.about-status {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.about-status-mark svg {
    display: block;
    width: 1rem;
    height: 1rem;
}

/* --- Legal pages -------------------------------------------------------------
   Terms, Privacy, Copyright, Disclaimer and Contact. They borrow .panel and the
   .about-prose / .about-list rules above, so what follows is only the parts a
   legal page needs and the About page does not.

   The governing idea is that these must be *readable*, because the usual reason
   nobody reads terms is that nobody is meant to. Line length is capped, headings
   stay distinguishable, and nothing is set smaller than the body text of the app
   itself - small print is a choice, and this is the choice not to use it. */

/* The dateline. Quiet, but the first thing a reader checks on a page like this,
   so it sits above the first panel rather than inside one. */
.legal-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.45rem;
    margin: -0.35rem 0 1.1rem;
    font-size: 0.78rem;
    color: var(--pm-muted);
}

.legal-meta-label {
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 0.68rem;
    font-weight: 700;
}

.legal-meta-value {
    color: var(--pm-ink-soft);
    font-weight: 600;
}

/* The "short version" panel. Tinted and rule-marked so it reads as a summary of
   the page rather than its first clause - a reader must never mistake it for the
   agreement itself, which is why each page also says so in words. */
.legal-summary {
    border-left: 3px solid var(--pm-accent);
    background: var(--pm-accent-soft);
}

/* The copyright assertion on the Copyright page. Given the weight of a notice
   rather than a sentence, because it is the one line on that page a reader may
   have come specifically to copy down. */
.legal-notice-block {
    margin: 0 0 0.9rem;
    padding: 0.6rem 0.8rem;
    border: 1px solid var(--pm-line-strong);
    border-radius: var(--pm-radius-sm);
    background: var(--pm-surface);
    font-size: 0.86rem;
    font-weight: 600;
    color: var(--pm-ink);
}

/* "Never send us your passphrase." Warning-toned rather than muted: it is the one
   instruction on these pages whose being skimmed past has a cost, and the app has
   spent the rest of its surface teaching people that secrets are safe here. */
.legal-warning {
    margin: 0.9rem 0 0;
    padding: 0.6rem 0.8rem;
    border-left: 3px solid var(--pm-warning);
    border-radius: var(--pm-radius-sm);
    background: var(--pm-warning-soft);
    font-size: 0.86rem;
    line-height: 1.55;
    color: var(--pm-ink);
}

/* A note normally hugs the thing it annotates, which is why .panel-note carries almost
   no top margin. A warning is a bordered, filled box, so the same 0.15rem leaves the
   note looking like it fell out of the box. Give it the gap notes use between each
   other instead. */
.legal-warning + .panel-note {
    margin-top: 0.75rem;
}

/* --- Third-party components (Copyright page) ---
   One block per component rather than a table: each entry is a name, a reason and
   a licence, and a table would need a header row saying exactly that. Stacked so
   the licence line can wrap on a phone without dragging a column with it. */
.legal-components {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 0.55rem;
}

.legal-component {
    display: grid;
    gap: 0.1rem;
    padding: 0.55rem 0.7rem;
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius-sm);
}

.legal-component-name {
    font-size: 0.88rem;
    font-weight: 650;
    color: var(--pm-ink);
}

.legal-component-use {
    font-size: 0.82rem;
    color: var(--pm-ink-soft);
}

/* The part the licences actually require to be reproduced, so it is never the
   thing that gets visually dropped. Muted, but not smaller than the line above. */
.legal-component-licence {
    font-size: 0.78rem;
    color: var(--pm-muted);
}

/* --- Cross-links between the legal pages ---
   Rendered at the foot of each of them. A link plus the one line explaining what
   that page settles: the labels alone ("Disclaimer") do not tell a reader which
   of five pages holds their answer, which is the whole problem this solves. */
.legal-index {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 0.5rem;
}

.legal-index-link {
    display: block;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--pm-accent-strong);
    text-decoration: none;
}

.legal-index-link:hover,
.legal-index-link:focus-visible {
    text-decoration: underline;
}

.legal-index-note {
    display: block;
    font-size: 0.8rem;
    line-height: 1.5;
    color: var(--pm-muted);
}

/* The address on the Contact page. Deliberately the largest thing there: it is
   the single fact the page exists to deliver, and anyone who opened it looking
   for an address should find it without reading a word. Breaks rather than
   overflows on a narrow phone. */
.legal-contact-address {
    margin: 0 0 0.75rem;
    font-size: 1.05rem;
    font-weight: 650;
    overflow-wrap: anywhere;
}

.legal-contact-address a {
    color: var(--pm-accent-strong);
    text-decoration: none;
}

.legal-contact-address a:hover,
.legal-contact-address a:focus-visible {
    text-decoration: underline;
}

/* --- Legal links in the shells ---
   Two places offer the same set: the app footer under every page, and the
   signed-out auth card, which is the one screen the shell is not wrapped around.
   Both use the same quiet treatment, because in both these are a reference, not a
   destination - they should be findable when looked for and invisible when not.

   They were once in the sidebar too. Five multi-word titles cannot share a line in
   a 264px rail, so they wrapped into a ragged 2-2-1 block wedged between the
   navigation and the user card; both the rule and the short labels that replaced
   the full titles exist because of what that looked like.

   flex with wrap rather than inline text so a label is never split across lines;
   the row-gap only comes into play on the narrowest screens, where the set gives
   up and wraps as whole links. */
.auth-legal,
.app-footer-legal {
    display: flex;
    flex-wrap: wrap;
    /* Tighter than the old 0.5rem because the separator is now a character in the
       markup rather than whitespace alone: the gap applies either side of it, so
       0.35rem here reads as the same visual spacing the old value did. */
    gap: 0.25rem 0.35rem;
    justify-content: center;
    font-size: 0.72rem;
}

.auth-legal a,
.app-footer-legal a {
    color: var(--pm-muted);
    text-decoration: none;
}

.auth-legal a:hover,
.auth-legal a:focus-visible,
.app-footer-legal a:hover,
.app-footer-legal a:focus-visible {
    color: var(--pm-ink-soft);
    text-decoration: underline;
}

/* Under the auth card, centred to match .auth-aside directly above it. */
.auth-legal {
    margin: 0.45rem 0 0;
}

.app-footer-legal {
    margin: 0.35rem 0 0;
}

/* The consent line on the registration form. Sits between the last field and the
   button, so it is read in the moment it describes; muted enough not to look like
   a validation message, which is the one thing it must not be mistaken for. */
.auth-consent {
    margin: 0 0 0.75rem;
    font-size: 0.76rem;
    line-height: 1.5;
    color: var(--pm-muted);
}

.auth-consent a {
    color: var(--pm-accent-strong);
    text-decoration: none;
}

.auth-consent a:hover,
.auth-consent a:focus-visible {
    text-decoration: underline;
}

/* --- Support page -----------------------------------------------------------
   A single centred column inside the standard .panel surface. Narrow on purpose:
   the body text is the ask, and a full-width paragraph on a desktop monitor reads
   as terms and conditions rather than a note from a person. */
.support-panel {
    text-align: center;
    max-width: 34rem;
    margin-inline: auto;
}

.support-mark {
    display: block;
    font-size: 2.6rem;
    line-height: 1;
    margin-bottom: 0.65rem;
}

.support-heading {
    margin: 0 0 0.55rem;
    font-size: 1.15rem;
    font-weight: 650;
}

.support-text {
    margin: 0 auto 1.35rem;
    color: var(--pm-ink-soft);
    font-size: 0.88rem;
    line-height: 1.6;
}

.support-button {
    min-width: 13rem;
}

.support-note {
    margin: 0.85rem 0 0;
    font-size: 0.76rem;
    color: var(--pm-muted);
}

.support-note-warn {
    color: var(--pm-expense);
}

/* --- Amount picker ---
   auto-fit rather than four fixed columns so the row becomes two-up on a narrow
   phone instead of shrinking the labels until the rupee sign collides. */
.support-amounts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(4.75rem, 1fr));
    gap: 0.5rem;
    margin-bottom: 0.9rem;
}

.support-amount {
    padding: 0.55rem 0.4rem;
    border: 1px solid var(--pm-line-strong);
    border-radius: var(--pm-radius-sm);
    background: var(--pm-surface);
    color: var(--pm-ink);
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
    transition: border-color 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
}

.support-amount:hover:not(:disabled) {
    background: var(--pm-hover);
}

.support-amount.is-selected {
    border-color: var(--pm-accent);
    box-shadow: 0 0 0 3px var(--pm-accent-ring);
}

.support-amount:disabled {
    cursor: default;
    opacity: 0.6;
}

.support-custom {
    margin-bottom: 1.1rem;
    text-align: left;
}

.support-custom > label {
    display: block;
    margin-bottom: 0.3rem;
    font-size: 0.78rem;
    color: var(--pm-muted);
}

.support-custom-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* The field grows and the button does not, so a long UTR stays readable rather
   than the control shrinking to fit a fixed-width button beside it. */
.support-custom-row > .form-control {
    flex: 1 1 auto;
    min-width: 0;
}

.support-custom-prefix {
    font-weight: 650;
    color: var(--pm-ink-soft);
}

/* --- Handoff ---
   The amount and reference are the two things the user must still be able to see
   after they come back from their banking app, so they lead. */
.support-amount-shown {
    margin: 0 0 0.2rem;
    font-size: 1.9rem;
    font-weight: 700;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

.support-reference {
    margin: 0 0 1.1rem;
    font-size: 0.78rem;
    color: var(--pm-muted);
}

.support-reference code,
.support-vpa code {
    font-size: 0.82rem;
    /* Selectable on purpose: on a desktop this is how the value gets copied, and
       a donate page is the wrong place to make someone retype an identifier. */
    user-select: all;
    -webkit-user-select: all;
}

.support-fallback,
.support-confirm {
    margin-top: 1.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--pm-line);
}

.support-fallback-head {
    margin: 0 0 0.7rem;
    font-size: 0.82rem;
    font-weight: 650;
}

/* Fixed white plate with its own padding. The quiet zone is already encoded into
   the viewBox, but a scanner needs the light modules to actually be light - so
   this stays white in the dark theme rather than inheriting the surface. */
.support-qr {
    display: block;
    margin: 0 auto 0.75rem;
    width: 208px;
    height: 208px;
    max-width: 100%;
    border-radius: var(--pm-radius-sm);
    background: #ffffff;
    box-shadow: var(--pm-shadow);
}

.support-vpa {
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    flex-wrap: wrap;
}

.support-vpa-label {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--pm-muted);
}

.support-alt {
    margin: 0.9rem 0 0;
    font-size: 0.8rem;
}

/* --- History --- */
.support-history {
    list-style: none;
    margin: 0;
    padding: 0;
    text-align: left;
}

.support-history-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.55rem 0;
    border-bottom: 1px solid var(--pm-line);
}

.support-history-row:last-child {
    border-bottom: 0;
}

.support-history-amount {
    font-weight: 650;
    font-variant-numeric: tabular-nums;
}

/* Pushes the pill to the right edge and lets the date take whatever is left. */
.support-history-date {
    flex: 1 1 auto;
    font-size: 0.78rem;
    color: var(--pm-muted);
}

/* --- Pointer split ---
   A upi:// anchor resolves on a phone and does precisely nothing on a desktop,
   where the honest answer is the code and the address instead. Keyed off
   `pointer: coarse` rather than width, for the same reason as the table rules
   above: a narrow window on a laptop is still a laptop. */
.support-coarse-only {
    display: none;
}

@media (pointer: coarse) {
    .support-coarse-only {
        display: inline-block;
    }

    /* The installed app on a phone leads with the native button, so the code is
       redundant there - but it is still shown when no UPI app answered, which is
       why the class is applied conditionally rather than always. */
    .support-fine-only {
        display: none;
    }
}

/* =============================================================================
   Toasts
   =============================================================================

   Confirmations and failures for writes the user just made. Anchored to the bottom on every host:
   on a phone that is where the thumb already is, and on the desktop it keeps the message away from
   the top bar, which is where the eye is least likely to be after pressing a button lower down. */

.toast-stack {
    position: fixed;
    /* Above the modal backdrop (1080) and the category picker (1200). A save that fails from inside
       a dialog has to be readable without closing the dialog first - otherwise the only way to see
       why it failed is to throw away what was typed. */
    z-index: 1300;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    /* The stack spans the corner but only the toasts themselves are solid, so clicks between them
       must fall through to whatever is underneath. */
    pointer-events: none;
    right: 1rem;
    bottom: calc(1rem + var(--pm-host-overlay, 0px));
    width: min(26rem, calc(100vw - 2rem));
}

.toast-item {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    /* The extra 3px at the bottom is the countdown bar's lane, so a two-line message never runs
       underneath it. Applied to every toast, errors included, so a stack of mixed kinds keeps one
       baseline instead of stepping in and out by three pixels. */
    padding: 0.7rem 0.75rem calc(0.7rem + 3px);
    border-radius: var(--pm-radius-sm);
    border: 1px solid var(--pm-line-strong);
    border-left-width: 4px;
    background: var(--pm-surface);
    color: var(--pm-ink);
    box-shadow: var(--pm-shadow-lg);
    animation: toast-enter 160ms ease-out;
    /* Anchors the countdown bar, and clips it to the rounded bottom corners. */
    position: relative;
    overflow: hidden;
}

/* The countdown. Sits in the padding lane reserved above, full width of the padding box so it reads
   as part of the toast rather than as content inside it. */
.toast-progress {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 3px;
    /* A faint track behind the fill, so the remaining time is legible as a proportion rather than as
       a bar of unknown original length. */
    background: var(--pm-line);
}

.toast-progress::after {
    content: "";
    display: block;
    height: 100%;
    width: 100%;
    background: currentColor;
    /* scaleX rather than width: it runs on the compositor, so a toast counting down costs no layout
       or paint work on a page that may be rendering a chart at the same time. */
    transform-origin: left center;
    animation: toast-countdown var(--pm-toast-life, 4000ms) linear forwards;
}

/* Pointed at or focused, so the countdown is on hold. The animation freezes exactly where it is -
   which is why the duration is never re-rendered, as changing it would restart the bar from full. */
.toast-item.is-held .toast-progress::after {
    animation-play-state: paused;
}

/* currentColor on the fill, so each kind's bar matches the icon and the left edge without another
   four rules. */
.toast-success .toast-progress::after {
    color: var(--pm-income);
}

.toast-warning .toast-progress::after {
    color: var(--pm-warning);
}

.toast-info .toast-progress::after {
    color: var(--pm-accent);
}

@keyframes toast-countdown {
    from {
        transform: scaleX(1);
    }

    to {
        transform: scaleX(0);
    }
}

.toast-icon {
    flex: 0 0 auto;
    width: 1.15rem;
    height: 1.15rem;
    margin-top: 0.05rem;
}

.toast-icon svg {
    width: 100%;
    height: 100%;
    display: block;
}

.toast-message {
    flex: 1 1 auto;
    margin: 0;
    font-size: 0.86rem;
    line-height: 1.35;
    /* An API error can arrive as one unbroken string with no spaces; without this it widens the
       toast until it runs off the screen. */
    overflow-wrap: anywhere;
}

.toast-close {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 28px is below the 44px touch target the rest of the app uses, but a mis-tap here only
       dismisses a message that was going to leave anyway, and a larger button crowds the text. */
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 0;
    border-radius: var(--pm-radius-sm);
    background: transparent;
    color: var(--pm-muted);
    cursor: pointer;
}

.toast-close svg {
    width: 0.85rem;
    height: 0.85rem;
}

.toast-close:hover {
    background: var(--pm-hover);
    color: var(--pm-ink);
}

/* The way back out of whatever the message is reporting. Outlined rather than filled so it reads as
   the secondary action it is - the primary one has already happened - while staying a visibly
   pressable control rather than a link. currentColor ties it to the toast's own palette, so one
   rule works on all four kinds. */
.toast-action {
    flex: 0 0 auto;
    align-self: center;
    padding: 0.25rem 0.6rem;
    border: 1px solid currentColor;
    border-radius: var(--pm-radius-sm);
    background: transparent;
    color: inherit;
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.4;
    cursor: pointer;
}

.toast-action:hover {
    background: var(--pm-hover);
}

.toast-success {
    border-left-color: var(--pm-income);
}

.toast-success .toast-icon {
    color: var(--pm-income);
}

.toast-error {
    border-left-color: var(--pm-expense);
}

.toast-error .toast-icon {
    color: var(--pm-expense);
}

.toast-warning {
    border-left-color: var(--pm-warning);
}

.toast-warning .toast-icon {
    color: var(--pm-warning);
}

.toast-info {
    border-left-color: var(--pm-accent);
}

.toast-info .toast-icon {
    color: var(--pm-accent);
}

@keyframes toast-enter {
    from {
        opacity: 0;
        transform: translateY(0.5rem);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .toast-item {
        animation: none;
    }

    /* Removed rather than frozen. With the animation off the bar would sit at full width for the
       whole four seconds and then vanish with the toast, which states the opposite of the truth.
       The message still leaves on its own; it just stops advertising when. */
    .toast-progress {
        display: none;
    }
}

/* Colour and shadow are both dropped in forced-colors mode, which would leave four identical
   rectangles. The border keeps them apart from the page and the icon keeps them apart from each
   other. */
@media (forced-colors: active) {
    .toast-item {
        border: 1px solid CanvasText;
        border-left-width: 4px;
    }

    /* Both halves of the bar would otherwise resolve to the same system colour and show nothing.
       Highlight against Canvas is the one pairing guaranteed to differ in every system theme. */
    .toast-progress {
        background: Canvas;
        border-top: 1px solid CanvasText;
    }

    .toast-progress::after {
        background: Highlight;
    }
}

@media (max-width: 720px) {
    .toast-stack {
        left: 0.6rem;
        right: 0.6rem;
        width: auto;
        /* Clears the fixed tab bar, the gesture bar below it, and any host banner - the same three
           allowances .app-content makes. Landing on top of the tabs would cover the only way out
           of the page. */
        bottom: calc(54px + env(safe-area-inset-bottom, 0px) + var(--pm-host-overlay, 0px) + 0.6rem);
    }
}

/* =============================================================================
   Credential vault
   ============================================================================= */

/* A list rather than a table, and not only because a 15-field bank record does not fit in columns:
   every row here is a button that opens a secret, and a <table> of buttons is a worse target on a
   phone than a stack of full-width rows. */
.vault-list {
    display: grid;
    gap: 0.5rem;
    margin: 0.85rem 0 0;
    padding: 0;
    list-style: none;
}

.vault-item {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    width: 100%;
    text-align: left;
    padding: 0.6rem 0.7rem;
    background: var(--pm-surface);
    color: var(--pm-ink);
    border: 1px solid var(--pm-line);
    border-radius: var(--pm-radius-sm);
    cursor: pointer;
    transition: background-color 0.14s ease, border-color 0.14s ease;
}

.vault-item:hover,
.vault-item:focus-visible {
    background: var(--pm-hover);
    border-color: var(--pm-line-strong);
}

.vault-item-icon {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 2.1rem;
    height: 2.1rem;
    border-radius: 0.55rem;
    background: var(--pm-canvas);
    color: var(--pm-muted);
}

.vault-item-icon svg {
    width: 1.1rem;
    height: 1.1rem;
}

/* min-width: 0 is what lets the ellipsis below actually apply - a flex item defaults to
   min-width: auto and refuses to shrink below its content, so a long title would widen the row
   instead of being truncated. */
.vault-item-main {
    flex: 1 1 auto;
    min-width: 0;
}

.vault-item-title,
.vault-item-sub {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.vault-item-title {
    font-size: 0.88rem;
    font-weight: 600;
}

.vault-item-sub {
    font-size: 0.75rem;
    color: var(--pm-muted);
}

.vault-item-star {
    flex: 0 0 auto;
    color: var(--pm-warning);
}

.vault-item-star svg {
    width: 0.95rem;
    height: 0.95rem;
    fill: currentColor;
}

.vault-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.6rem;
}

/* The recovery code, shown exactly once. Monospace and spaced out because it is read off the screen
   and copied by hand onto paper, which is the whole point of it; `user-select: all` makes one click
   select the lot, since a partial selection of a recovery code is worse than none. */
.vault-code {
    display: block;
    margin: 0.6rem 0;
    padding: 0.85rem 0.9rem;
    font-family: ui-monospace, SFMono-Regular, "Cascadia Mono", Menlo, Consolas, monospace;
    font-size: 1.02rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    line-height: 1.7;
    text-align: center;
    overflow-wrap: anywhere;
    user-select: all;
    background: var(--pm-canvas);
    color: var(--pm-ink);
    border: 1px dashed var(--pm-line-strong);
    border-radius: var(--pm-radius-sm);
}

/* A checkbox and its caption on one line. Bootstrap's .form-check is deliberately not used: the
   two hosts are on different Bootstrap majors and its markup contract changed between them, so a
   checkbox that looked right on the web app sat half a line low in the installed one. */
.vault-check {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin: 0.55rem 0;
    font-size: 0.85rem;
}

.vault-check input {
    flex: 0 0 auto;
    width: 1rem;
    height: 1rem;
    margin: 0;
}

/* The decrypted record. A plain container rather than .stat-list, whose rows are label/value flex
   pairs - a reveal control and a copy button do not fit in one of those. */
.vault-fields {
    margin-top: 0.4rem;
    border-top: 1px solid var(--pm-line);
}

/* One decrypted field on the detail view: its label, its value, and the copy control. */
.vault-field {
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--pm-line);
}

.vault-field:last-child {
    border-bottom: 0;
}

.vault-field-label {
    display: block;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--pm-muted);
    margin-bottom: 0.2rem;
}

.vault-field-value {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

/* The value takes the room and the copy button keeps its own, so a long URL truncates rather than
   pushing the button off the right-hand edge. */
.vault-field-value > .password-field,
.vault-field-value > .form-control {
    flex: 1 1 auto;
    min-width: 0;
}

.vault-field-text {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 0.88rem;
    overflow-wrap: anywhere;
}

/* Square, icon-only, and sized to the input beside it. Deliberately exempt from the phone rule that
   makes .panel-actions buttons full width - this one belongs on the same line as its value. */
.vault-copy {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 2.3rem;
    height: 2.3rem;
    padding: 0;
    background: var(--pm-surface);
    color: var(--pm-ink-soft);
    border: 1px solid var(--pm-line-strong);
    border-radius: var(--pm-radius-sm);
    cursor: pointer;
}

.vault-copy:hover:not(:disabled) {
    background: var(--pm-hover);
    color: var(--pm-ink);
}

.vault-copy svg {
    width: 1rem;
    height: 1rem;
}

/* A repeatable security question. The fields stack on a phone and sit side by side above it; the
   remove control is a row of its own so it never shares a line with a half-width input. */
.vault-qa {
    padding: 0.65rem 0;
    border-top: 1px solid var(--pm-line);
}

.vault-qa:first-of-type {
    border-top: 0;
}

/* =============================================================================
   Host-injected overlay accommodation
   ============================================================================= */

/* Some hosting plans append a fixed advertising banner to the bottom of every page they serve,
   at the very top of the stacking order. Its topmost layer is transparent, so it is invisible
   over a light page yet still absorbs every click that lands inside it - any control of ours in
   that strip silently stops working. app-ui.js measures the banner and publishes its height as
   --pm-host-overlay; the rules below turn that into space, so the layout ends above the banner
   instead of underneath it. The variable is absent on a host that injects nothing, and the
   fallback of 0px then leaves these paddings exactly as they were. */

/* `.app-content` needs no rule here: its padding-bottom is already
   `calc(var(--pm-content-bottom) + var(--pm-host-overlay, 0px))`, so the banner is added to
   whatever allowance the current breakpoint asked for rather than overwriting it. */

/* The phone tab bar is fixed to the bottom edge, so the banner lands on top of it and swallows every
   tap meant for a tab. Lifting it by the measured height is the same trick, applied to the one piece
   of chrome that content padding cannot move. */
@media (max-width: 720px) {
    .app-tabbar {
        bottom: var(--pm-host-overlay, 0px);
    }
}

/* The card is centred in the flex container, so bottom padding lifts it clear by half the banner
   and keeps the whole card - including the links under the button - reachable. */
.auth-page {
    padding-bottom: calc(1.5rem + var(--pm-host-overlay, 0px));
}
