.custom-nav.navbar-brand-logo {
    min-width: unset;
    max-width: unset;
}

.svg-illustrator {
    max-height: 15rem;
    max-width: 30rem;
}

.tom-select-custom {
    min-width: 95px;
}

/* Disable click navigation on multi-step form progress indicators.
   HSStepForm's validator skips hidden required fields (offsetParent=null),
   which would let users jump past steps that gate on conditionally-shown inputs. */
.step-progress-static {
    pointer-events: none;
}

/* Telegraphic Transfer's 5-step wizard stacks labels centred below each icon (inline labels clip
   5-across at this width). The theme draws the step connector as a fixed-width .step-icon::after
   sized for the tighter inline 3-step layout, so across the wider 5-step row it floats mid-gap and
   leaves a dangling stub after the last icon. Re-anchor it to the step item so it spans icon-to-icon,
   and drop the trailing line. Keeps the theme's own state colouring (still the same ::after).
   Scoped to sm+ only: below 576px the theme lays the steps out as a vertical timeline with its own
   (correct) connector, so leave that untouched — the horizontal re-anchor would overflow it. */
@media (min-width: 576px) {
    /* Equal-width steps so every icon-to-icon gap is identical — otherwise the last step (a short
       label like "Authorization") shrinks under space-between and its connector overshoots the circle. */
    .step-stacked .step-item { position: relative; flex: 1 1 0; }
    .step-stacked .step-icon { position: static; }          /* ::after now resolves against .step-item */
    .step-stacked .step-item:not(:last-child) .step-icon::after {
        top: 18px;                /* icon vertical centre (icon ≈ 39px tall) */
        left: calc(50% + 22px);   /* start just past this icon */
        width: calc(100% - 44px); /* reach just before the next icon */
        right: auto;
    }
    .step-stacked .step-item:last-child .step-icon::after { display: none; }
}

.home-background {
    width: 100%;
    height: 141px;
    background: transparent url('/img/bg/home-banner-b1245e2e20740612ed0621beb82e2276.png') 0 0 no-repeat padding-box;
    background-size: cover;
    border-radius: 19px;
    opacity: 1;
}

.accounts-background {
    width: 100%;
    height: 141px;
    background: transparent url('/img/bg/accounts-banner-8b0d5660224e34a0d2b67b7a226ff4f6.png') 0 0 no-repeat padding-box;
    background-size: cover;
    border-radius: 19px;
    opacity: 1;
}

.transfer-background {
    width: 100%;
    height: 141px;
    background: transparent url('/img/bg/transfers-banner-904ac756aaebeb2d4429b8a5b8f2cfe3.png') 0 0 no-repeat padding-box;
    background-size: cover;
    border-radius: 19px;
    opacity: 1;
}

.transactions-background {
    width: 100%;
    height: 141px;
    background: transparent url('/img/bg/transactions-banner-75f26ba0549aba961c5bb9f0c89c0f71.png') 0 0 no-repeat padding-box;
    background-size: cover;
    border-radius: 19px;
    opacity: 1;
}

.exchange-rates-background {
    width: 100%;
    height: 141px;
    background: transparent url('/img/bg/exchange-rates-banner-aea38db5fb2a57abba114b97173bf06e.png') 0 0 no-repeat padding-box;
    background-size: cover;
    border-radius: 19px;
    opacity: 1;
}

.account-statement-background {
    width: 100%;
    height: 141px;
    background: transparent url('/img/bg/account-statement-banner-57b5e2e730d0e313cace5c82f584cd1b.png') 0 0 no-repeat padding-box;
    background-size: cover;
    border-radius: 19px;
    opacity: 1;
}

.account-details-background {
    width: 100%;
    height: 141px;
    background: transparent url('/img/bg/account-details-banner-d58b370420ba2989e94c43309582060e.png') 0 0 no-repeat padding-box;
    background-size: cover;
    border-radius: 19px;
    opacity: 1;
}

.support-background {
    width: 100%;
    height: 141px;
    background: transparent url('/img/bg/support-banner-da725316598ebf26eb9a32fe2ad68912.png') 0 0 no-repeat padding-box;
    background-size: cover;
    border-radius: 19px;
    opacity: 1;
}

.user-profile-background {
    width: 100%;
    height: 141px;
    background: transparent url('/img/bg/user-profile-banner-d6c4b84966575d1dd9932e5f56329617.png') 0 0 no-repeat padding-box;
    background-size: cover;
    border-radius: 19px;
    opacity: 1;
}

.ad-banner {
    opacity: 1;
    max-width: 1136px;
    max-height: 357px;
    width: 100%;
    height: auto;
    object-fit: cover;
}

.styled-ad-image {
    border: 1px solid #001c39;
    border-radius: 0.75rem;
}

.notification-card-body {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: 100%;
}

.auth-bg {
    width: 100%;
    /* min-height (not fixed height) so the background covers the full scrollable
       area when a form is taller than the viewport (e.g. the 6-input MFA / change
       -password pages on phones); 100vw would also add a horizontal scrollbar. */
    min-height: 100vh;
    /* The IMAGE alone is not enough, and min-height does not save it: <html> here renders
       ~24px TALLER than <body> (an anonymous line box from a stray text node the parser
       leaves as a direct child of <html>). <html> carried no background, so that strip
       painted WHITE below the form on any viewport short enough to scroll — reported from
       a real browser, reproduced at 1280x500 and confirmed by painting <html> red.
       A background-COLOR on the body propagates to the canvas and fills the whole of it,
       which the image on its own was not doing. #012244 is sampled from the PNG's own
       edge pixels (uniform on all four corners), so the join is invisible. */
    background: url('/img/bg/auth-bg-7c74c9a2125870f76598d48fee73e2ed.png') #012244;
    background-size: cover;
}

.nav-vertical.nav-pills .nav-link.active {
    background-color: #001c39 !important;
    color: white !important;
}

/* DataTables server-side processing indicator - centered */
.dataTables_processing {
    position: absolute !important;
    top: 50% !important;
    left: 50% !important;
    transform: translate(-50%, -50%) !important;
    margin: 0 !important;
    padding: 1rem 2rem !important;
    background: rgba(255, 255, 255, 0.9) !important;
    border-radius: 0.5rem !important;
    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
    z-index: 100 !important;
}

.datatable-custom {
    position: relative;
    min-height: 200px;
}

/* Disabled soft buttons: Bootstrap's .btn.disabled keeps the soft variant's coloured text
   while the theme switches to the SOLID background of the same hue — green-on-green renders
   disableFormSubmit's "Processing..." label invisible. Match the theme's hover treatment
   instead (solid background + white text); warning keeps dark text for contrast on amber. */
.btn-soft-primary.disabled, .btn-soft-primary:disabled,
.btn-soft-secondary.disabled, .btn-soft-secondary:disabled,
.btn-soft-success.disabled, .btn-soft-success:disabled,
.btn-soft-danger.disabled, .btn-soft-danger:disabled,
.btn-soft-info.disabled, .btn-soft-info:disabled,
.btn-soft-dark.disabled, .btn-soft-dark:disabled {
    color: #fff;
}

.btn-soft-warning.disabled, .btn-soft-warning:disabled {
    color: #000;
}

/* --- Back-office dashboard: staggered card reveal. Each card animates in
   slightly after the previous one (driven by --dash-delay), so the page builds
   itself instead of popping in at once. Respects reduced-motion. */
.dash-reveal {
    opacity: 0;
    animation: dash-reveal 0.45s ease forwards;
    animation-delay: calc(var(--dash-delay, 0) * 70ms);
}
@keyframes dash-reveal {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
    .dash-reveal { animation: none; opacity: 1; }
}

/* --- Back-office dashboard: pulse dot on the verdict strip. Breathing ring
   while healthy, urgent ring when something needs attention. */
.dash-pulse-dot {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    flex: 0 0 auto;
    position: relative;
    background: var(--bs-secondary, #71869d);
}
.dash-pulse-dot::after {
    content: '';
    position: absolute;
    inset: -4px;
    border-radius: 50%;
    border: 2px solid currentColor;
    opacity: 0;
}
.dash-pulse-dot[data-tone="ok"]      { background: var(--bs-success, #00c9a7); color: var(--bs-success, #00c9a7); }
.dash-pulse-dot[data-tone="warning"] { background: #f5a623; color: #f5a623; }
.dash-pulse-dot[data-tone="danger"]  { background: var(--bs-danger, #ed4c78);  color: var(--bs-danger, #ed4c78); }
.dash-pulse-dot[data-tone="ok"]::after,
.dash-pulse-dot[data-tone="warning"]::after,
.dash-pulse-dot[data-tone="danger"]::after {
    animation: dash-pulse 2s ease-out infinite;
}
@keyframes dash-pulse {
    0%   { transform: scale(0.6); opacity: 0.8; }
    70%  { transform: scale(1.6); opacity: 0; }
    100% { transform: scale(1.6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
    .dash-pulse-dot::after { animation: none !important; }
}

/* --- Back-office dashboard: attention chips on the pulse strip — each is a
   deep link to the page where the problem gets fixed. The theme's warning
   amber (#f5ca99) is too pale for text, so warning chips use a darker amber. */
.dash-attention-chip {
    display: inline-flex;
    align-items: center;
    padding: 0.3rem 0.75rem;
    border-radius: 2rem;
    font-size: 0.8125rem;
    font-weight: 500;
    text-decoration: none;
    transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.dash-attention-chip:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.dash-attention-chip.is-warning {
    background: rgba(245, 202, 153, 0.25);
    color: #9a6700;
}
.dash-attention-chip.is-warning:hover { color: #9a6700; }
/* The theme danger (#ed4c78) reads 3.1:1 on its own 10% tint — below AA. Use the
   darker danger the theme itself uses for text inside .bg-soft-danger badges. */
.dash-attention-chip.is-danger {
    background: rgba(237, 76, 120, 0.1);
    color: #c1274c;
}
.dash-attention-chip.is-danger:hover { color: #c1274c; }

/* --- Review timeline containers: a long history (a retried disbursement logs every
   attempt) must scroll inside its card, not stretch the page. Newest renders first,
   so the fold hides only the older events. */
.review-timeline-scroll {
    max-height: 30rem;
    overflow-y: auto;
}

/* --- Queue filter bars (invoices, STP messages, telegraphic transfers share these ids).
   The bars use btn-sm to keep the header compact on desktop, which lands at 37px — under
   the 44px touch target a phone needs. Breakpoint-scoped, so desktop is unchanged. */
@media (max-width: 575.98px) {
    #filterBtn,
    #clearFilterBtn {
        min-height: 44px;
    }

    /* The attention chips are controls, not labels — each one filters a queue or opens a desk, and
       the danger ones are the only route to work nobody is waiting on. At 29px they are the smallest
       tap target on the dashboard and on the queue headers that carry one. Phones only. */
    .dash-attention-chip {
        min-height: 44px;
    }
}

/* --- Back-office dashboard: quick-action tiles. Borderless to keep the page
   light — a soft tint wash appears on hover instead of a boxed lift. */
.dash-action-tile {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem 1rem;
    border-radius: 0.5rem;
    color: var(--bs-body-color, #677788);
    text-decoration: none;
    height: 100%;
    transition: background-color 0.15s ease, color 0.15s ease;
}
.dash-action-tile:hover {
    background: rgba(55, 125, 255, 0.07);
    color: var(--bs-primary, #377dff);
}
.dash-action-tile i {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    font-size: 1rem;
    color: var(--bs-primary, #377dff);
    background: rgba(55, 125, 255, 0.1);
}
.dash-action-tile span {
    font-weight: 500;
    font-size: 0.875rem;
}

/* --- Back-office dashboard: loading skeletons. The theme build ships without
   Bootstrap's .placeholder component, so this is a minimal local equivalent. */
.dash-skeleton {
    display: inline-block;
    min-width: 2rem;
    border-radius: 0.25rem;
    background: var(--bs-border-color, rgba(231, 234, 243, 0.7));
    animation: dash-skeleton 1.2s ease-in-out infinite;
}
.dash-skeleton.col-8 { width: 66%; }
.dash-skeleton.col-10 { width: 83%; }
.dash-skeleton.col-12 { width: 100%; }
.dash-skeleton-num { width: 3rem; }
@keyframes dash-skeleton {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
}

/* --- Back-office dashboard: exchange-rates card rendered as a table so each
   Buy/Mid/Sell heading sits directly above its column and the figures line up. */
.dash-rates-table th,
.dash-rates-table td {
    padding: 0.35rem 0.5rem;
    border: 0;
    white-space: nowrap;
    vertical-align: middle;
}
.dash-rates-table th:first-child,
.dash-rates-table td:first-child { padding-left: 0; }
.dash-rates-table th:last-child,
.dash-rates-table td:last-child { padding-right: 0; }
.dash-rates-table thead th {
    font-weight: 400;
    border-bottom: 1px solid var(--bs-border-color, #e7eaf3);
}
/* tabular-nums keeps the digits in vertical alignment down each column. */
.dash-rate-num { font-variant-numeric: tabular-nums; }

/* --- Back-office dashboard: stale state. When a card's source stops answering,
   the last-known numbers stay (stale beats blank for ops) but dim, and an amber
   note flags them until the next successful refresh clears it. */
.dash-section.dash-stale .card-body > *:not(.js-unavailable) {
    opacity: 0.45;
}
.dash-stale-note {
    color: #9a6700;
}
html.dash-theme-dark .dash-stale-note {
    color: #f0b357;
}

/* --- Back-office dashboard: attention tone for headline numbers. The theme's
   .text-warning amber (#f5ca99) is unreadable at headline size on white. */
.dash-num-attention {
    color: #b76e00 !important;
}

/* --- Back-office dashboard: status-first header. The greeting is a quiet
   eyebrow line; the live verdict is the page's headline, sitting straight on
   the page background — no banner, no extra box. */
.dash-eyebrow {
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--bs-secondary, #71869d);
}
.dash-verdict {
    font-size: 2rem;
    font-weight: 600;
    letter-spacing: -0.02em;
}
.dash-verdict-sub {
    /* aligns the subline with the headline text, past the pulse dot */
    margin-left: 2.125rem;
}

/* --- Back-office dashboard: load choreography. Cards stagger in (dash-reveal),
   then each card's icon pops a beat after its card, bars sweep to their value,
   chips and rate rows cascade. All gated behind prefers-reduced-motion below. */
.dash-reveal .icon-circle {
    animation: dash-pop 0.4s ease both;
    /* --dash-delay inherits from the card's reveal wrapper */
    animation-delay: calc(var(--dash-delay, 0) * 70ms + 300ms);
}
@keyframes dash-pop {
    from { transform: scale(0.4); opacity: 0; }
    60%  { transform: scale(1.08); }
    to   { transform: scale(1); opacity: 1; }
}
.dash-section .progress-bar {
    transition: width 0.9s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.dash-attention-chip {
    animation: dash-chip-in 0.3s ease backwards;
}
@keyframes dash-chip-in {
    from { transform: translateY(6px); opacity: 0; }
    to   { transform: none; opacity: 1; }
}
.dash-row-in {
    animation: dash-chip-in 0.35s ease backwards;
    animation-delay: calc(var(--dash-row, 0) * 50ms);
}
.dash-verdict-swap {
    animation: dash-verdict-in 0.45s ease;
}
@keyframes dash-verdict-in {
    from { transform: translateY(4px); opacity: 0; }
    to   { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .dash-reveal .icon-circle,
    .dash-attention-chip,
    .dash-row-in,
    .dash-verdict-swap { animation: none !important; }
    .dash-section .progress-bar { transition: none !important; }
}

/* --- Back-office dashboard: dark-theme overrides. The .dash-theme-dark class
   is mirrored onto <html> by the dashboard JS (the theme itself only swaps
   stylesheets, leaving CSS nothing to select on). Literal ambers below need
   brightening on dark surfaces; var(--bs-*) styles adapt on their own. */
html.dash-theme-dark .dash-attention-chip.is-warning {
    background: rgba(245, 202, 153, 0.15);
    color: #f0b357;
}
html.dash-theme-dark .dash-attention-chip.is-warning:hover {
    color: #f0b357;
}
html.dash-theme-dark .dash-num-attention {
    color: #f0b357 !important;
}
/* On dark surfaces the pale theme danger has the better contrast — restore it. */
html.dash-theme-dark .dash-attention-chip.is-danger,
html.dash-theme-dark .dash-attention-chip.is-danger:hover {
    color: #ed4c78;
}

/* --- Page-header banner titles. The theme applies `m-lg-8` (3rem margin) to
   .page-header-title, so the heading only gets breathing room from lg (992px)
   up. Below lg it has zero margin and sits flush in the banner's rounded
   top-left corner, reading as clipped/cramped on phones and tablets. Restore
   proportional spacing on smaller screens without disturbing the lg+ layout the
   theme ships. Scoped to the image banners (.page-header + *-background) so
   plain page headers are untouched. */
/* The banner titles are white over a photographic cover image whose crop shifts
   with viewport width, so contrast against light regions is left to chance. A
   soft shadow guarantees legibility across all 33 banners without an overlay or
   any colour-system change. */
.page-header[class*="-background"] .page-header-title {
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
}

@media (max-width: 991.98px) {
    .page-header[class*="-background"] .page-header-title {
        margin: 1.25rem 1.5rem !important;
    }
}

@media (max-width: 575.98px) {
    /* On phones the 141px banner (a desktop constant) eats a big slice of the
       viewport for a decorative image + one heading, and top-anchors the title
       leaving dead space below. Shrink it and vertically centre the title to
       reclaim fold space so the first card shows sooner. */
    .page-header[class*="-background"] {
        height: 96px !important;
        display: flex;
        align-items: center;
        padding-bottom: 0 !important;
    }
    /* Cap the title size on phones so long dynamic headings (e.g. "Account
       Details as at 01 Jul 2026 14:30 CAT") don't wrap out of the band, and use
       horizontal margin only (the flex parent centres it vertically). */
    .page-header[class*="-background"] .page-header-title {
        font-size: 1.25rem;
        line-height: 1.3;
        margin: 0 1.25rem !important;
    }
}

/* --- DataTables pagination on phones. The pagination row doesn't wrap, so on
   narrow viewports it runs a few pixels past the edge and introduces a
   horizontal page scroll. Let it wrap and centre below sm. */
@media (max-width: 575.98px) {
    .datatable-custom-pagination {
        flex-wrap: wrap;
        justify-content: center;
    }
    /* Comfortable tap height for the page links — the primary way users move
       through long lists on a phone. */
    .datatable-custom-pagination .page-link {
        min-height: 44px;
        display: flex;
        align-items: center;
        padding-top: 0;
        padding-bottom: 0;
    }
}

/* --- Global app toast layering. The layout pins #appToast with an inline
   z-index:1000, which sits BELOW Bootstrap's modal-backdrop (1050) and modal
   (1055) — so a success/error toast fired while an auth/OTP or confirm modal is
   open is hidden behind the backdrop. Lift it to the toast layer at all widths. */
#appToast {
    z-index: 1090 !important;
}

/* --- Global app toast (#appToast in the main layout). Its inline style anchors
   it top-right at a fixed ~350px width; on phones that width pushes the toast's
   left edge off-screen, clipping the title and close button. Below sm, pin it to
   both edges with a small inset so it stays fully on-screen. !important overrides
   the element's inline top/right/width. */
@media (max-width: 575.98px) {
    #appToast {
        top: 1rem !important;
        right: 0.75rem !important;
        left: 0.75rem !important;
        width: auto !important;
    }
    /* Enlarge the toast close hit area on phones without changing the icon. */
    #appToast .btn-close {
        padding: 0.75rem;
        margin: -0.375rem -0.375rem -0.375rem auto;
    }
    /* The in-cell "View"/action buttons are the payoff at the far right of a
       sideways-scrolled list table — give them a comfortable minimum tap size
       (scoped to scrolled tables so btn-sm elsewhere is untouched). */
    .table-responsive .btn-sm {
        min-height: 38px;
        display: inline-flex;
        align-items: center;
    }
}

/* --- Session-expiry modal. The countdown number and the word "seconds" both
   render at display-3 (~4.5rem); at phone width "60 seconds" overflows one line
   and wraps awkwardly. Scale the type down below sm so it reads on one line. */
@media (max-width: 575.98px) {
    #sessionTimeoutModal .display-3 { font-size: 2.5rem; }
    #sessionTimeoutModal .display-5 { font-size: 1.25rem; }
}

/* --- Scrollable modals whose <form> wraps header/body/footer. Bootstrap's
   .modal-dialog-scrollable caps .modal-content and scrolls .modal-body, but only
   when the body is a flex child of .modal-content; with a <form> in between the
   form keeps its full height, nothing scrolls and the footer (the submit button)
   is clipped whenever the form is taller than the window. Make the form the flex
   column so its body shrinks and scrolls. The short-viewport block below applies
   the same shape to every modal. */
.modal-dialog-scrollable > .modal-content > form {
    display: flex;
    flex-direction: column;
    min-height: 0;
    max-height: 100%;
}
.modal-dialog-scrollable > .modal-content > form > .modal-body {
    overflow-y: auto;
}

/* --- Modals on phones. Client modals are top-anchored (plain .modal-dialog, not
   centered) and not scrollable, so on short viewports (landscape) or with tall
   content (the auth/OTP modal, or the confirm dialog's 15rem illustration) the
   action buttons fall below the page fold and the user must scroll the whole page
   to reach them. Below sm: centre every modal and cap it to the viewport height
   with an internally-scrollable body, so the dialog always sits fully on-screen
   and its actions stay reachable without scrolling the page. Mirrors Bootstrap's
   own modal-dialog-centered + modal-dialog-scrollable, applied on phones. The
   height condition also catches landscape phones (wide but short), where the
   scroll-below-the-fold problem is worst; desktop/laptop (wide AND tall) is
   untouched. */
@media (max-width: 575.98px), (max-height: 600px) {
    .modal-dialog {
        display: flex;
        align-items: center;
        /* definite height (not min/max) so .modal-content max-height:100% resolves
           and the body can scroll internally. Sized from the theme's own margin
           variable (0.5rem on phones, 1.75rem from sm up) because custom.css loads
           BEFORE theme.min.css, so a margin set here loses to the theme's and a
           hardcoded 1rem overran short laptop/landscape windows by ~20px. */
        height: calc(100% - var(--bs-modal-margin) * 2);
    }
    .modal-dialog > .modal-content {
        max-height: 100%;
        overflow: hidden;
    }
    .modal-content > .modal-body {
        overflow-y: auto;
        /* reserve the scrollbar track so a tall, internally-scrolling modal shows
           a visible scroll cue rather than silently cutting content at the fold */
        scrollbar-gutter: stable;
    }
    /* Most form modals wrap header/body/footer in a <form> directly under
       .modal-content, so the rule above never reaches their body: the content is
       capped and clipped but nothing scrolls, and the footer's submit button is
       cut off (a client's batch upload on a zoomed laptop). Make the form the
       flex column instead so its body scrolls and the footer stays pinned. */
    .modal-dialog > .modal-content > form {
        display: flex;
        flex-direction: column;
        min-height: 0;
        max-height: 100%;
    }
    .modal-content > form > .modal-body {
        overflow-y: auto;
        scrollbar-gutter: stable;
    }
    /* The shared confirm dialog's 15rem illustration is purely decorative and
       makes the dialog too tall to fit on short screens, pushing the No/Yes
       buttons out of view. Hide it on phones/short viewports so the heading,
       message and buttons all stay on-screen without any scroll. */
    #confirmationDialog .modal-body img {
        display: none;
    }
    /* Stretch the confirm No/Yes buttons to share the row for bigger, balanced
       tap targets on phones (mirrors the .account-actions treatment). */
    .confirm-actions {
        width: 100%;
    }
    .confirm-actions .btn {
        flex: 1 1 0;
    }
}

/* --- Account action buttons (Refresh / Details / Statement) on the accounts
   page. On phones, drop the icons and let each button size to its own label with
   the theme's standard padding, then centre the group — so all three read as one
   consistent set on a single row (equal-width stretching made the longest label,
   "Statement", look cramped next to the others). Icons + natural row on md+. */
@media (max-width: 575.98px) {
    /* Separate the identity block (number / type / date) from the balances on
       phones, where they otherwise stack into one undifferentiated block. Theme
       border colour + spacing scale; desktop side-by-side layout unaffected. */
    .account-balances {
        border-top: 1px solid var(--bs-border-color, #e7eaf3);
        margin-top: 0.5rem;
        padding-top: 0.75rem;
    }
    .account-actions {
        justify-content: center;
    }
    /* Lift the btn-sm actions to a comfortable ~44px touch target on phones
       (they are the card's primary controls) without touching desktop. */
    .account-actions .btn {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    .account-actions .btn i {
        display: none;
    }
}

/* --- OTP entry (.otp-row, the 6 single-digit code boxes shown on the auth step
   of every wizard and on detail-page approvals). The boxes sit in a 6-column row
   inside a spacious card; on phones the fixed box width + card padding push the
   6th box onto a second row (5 + 1). Force the columns equal-width and
   non-wrapping and let the inputs shrink so all six stay on one line at any phone
   width, regardless of the enclosing card's padding. (.otp-row is a shared class
   on every OTP wrapper so all entry points are covered uniformly.) */
@media (max-width: 575.98px) {
    .otp-row {
        flex-wrap: nowrap;
    }
    .otp-row > .col {
        flex: 1 1 0;
        min-width: 0;
        padding-left: 0.25rem;
        padding-right: 0.25rem;
    }
    .otp-row .form-control-single-number {
        width: 100%;
        min-width: 0;
        padding-left: 0.25rem;
        padding-right: 0.25rem;
    }
}

/* --- List tables as cards on phones/small tablets. Server-side list tables are
   marked `.mobile-cards` by helper.js, which also stamps each body cell with its
   column header in data-label. Below md we reflow the wide, sideways-scrolling
   table into a stack of cards — one card per row, each cell a label→value line —
   so nothing needs horizontal scrolling and the key columns lead. The table
   layout is untouched from md up. */
@media (max-width: 767.98px) {
    /* Header row is only needed as the data-label source (already read by JS),
       so hide it visually while keeping it in the DOM. */
    table.mobile-cards thead {
        display: none;
    }
    table.mobile-cards,
    table.mobile-cards tbody,
    table.mobile-cards tr,
    table.mobile-cards td {
        display: block;
        width: 100% !important;
    }
    table.mobile-cards tr {
        border: 1px solid var(--bs-card-border-color, rgba(231, 234, 243, 0.7));
        /* left edge carries an accent bar (alternated per row below) so adjacent
           cards separate at a glance; surface/radius/shadow reuse the Front card
           tokens so light and dark themes adapt automatically. */
        border-left: 3px solid var(--bs-primary, #377dff);
        border-radius: var(--bs-card-border-radius, 0.75rem);
        margin-bottom: 0.75rem;
        padding: 0.35rem 1rem;
        background: var(--bs-card-bg, #fff);
        box-shadow: var(--bs-card-box-shadow, 0 0.375rem 0.75rem rgba(140, 152, 164, 0.075));
    }
    /* Alternating non-semantic accent (brand blue / cyan). Deliberately NOT red/
       green — those carry danger/success meaning elsewhere and would misread as a
       status on a plain list. The statement card overrides this with its own
       semantic debit/credit accent (higher specificity via #statementDataTable). */
    table.mobile-cards tbody tr:nth-child(odd) {
        border-left-color: var(--bs-primary, #377dff);
    }
    table.mobile-cards tbody tr:nth-child(even) {
        border-left-color: var(--bs-info, #00c9db);
    }
    /* Each cell becomes a label(left)→value(right) line. The theme centres list
       cells (.card-table text-center); override to the label/value split. */
    table.mobile-cards td {
        display: flex !important;
        justify-content: space-between;
        align-items: center;
        gap: 1rem;
        text-align: right;
        border: 0;
        border-bottom: 1px solid var(--bs-border-color, #f1f3f9);
        padding: 0.55rem 0;
        white-space: normal;
        /* let long unbreakable values (account numbers, references, pasted
           strings) wrap so a card never forces a horizontal scroll */
        overflow-wrap: anywhere;
        /* Kill the .table-striped zebra inside cards — its 9999px inset shadow tints
           whole cards (notably in dark mode). The card border + elevation + accent
           edge already separate rows, so the stripe just muddies the surface. */
        box-shadow: none !important;
    }
    table.mobile-cards td > * {
        min-width: 0;
    }
    table.mobile-cards tr td:last-child {
        border-bottom: 0;
    }
    table.mobile-cards td::before {
        content: attr(data-label);
        font-weight: 600;
        color: var(--bs-secondary, #71869d);
        text-align: left;
        white-space: nowrap;
        flex-shrink: 0;
    }
    /* Action cell — ONLY when the last cell directly holds button(s)/link(s). The
       :has(> .btn) guard self-scopes the rule two ways: tables whose last column is
       a Status badge or running Balance (no button) keep their label untouched; and
       a kebab dropdown (its .btn toggle is nested in .hs-unfold, not a direct child)
       is left in the default label→value form rather than a lone, unlabelled ⋮.
       Drop the "Actions" label, let the control(s) fill the card width as a proper
       44px tap target, and stack a button group (Edit/Delete) neatly. */
    table.mobile-cards td:last-child:has(> .btn) {
        flex-wrap: wrap;
        gap: 0.5rem;
        padding-top: 0.7rem;
    }
    table.mobile-cards td:last-child:has(> .btn)::before {
        display: none;
    }
    table.mobile-cards td:last-child:has(> .btn) > .btn {
        flex: 1 1 auto;
        padding: 0.5rem;
        /* theme .btn is display:flex with justify-content:normal (left); centre the
           icon+label now that the button spans the card width, and add a gap since
           flex collapses the whitespace between the icon and the label */
        justify-content: center;
        align-items: center;
        gap: 0.4rem;
    }
    /* Promote the neutral "View" button (btn-white) to a soft-primary CTA so the
       card's main action stands out; theme's btn-soft-* actions keep their colour. */
    table.mobile-cards td:last-child:has(> .btn) > .btn-white {
        background: rgba(55, 125, 255, 0.1);
        border-color: transparent;
        color: var(--bs-primary, #377dff);
        font-weight: 600;
    }
    table.mobile-cards td:last-child:has(> .btn) > .btn-white:active {
        background: var(--bs-primary, #377dff);
        color: #fff;
    }
    /* Empty / loading state renders as one spanning cell — keep it centred, no
       label, no card split. */
    table.mobile-cards td.dataTables_empty {
        display: block !important;
        text-align: center;
        border-bottom: 0;
    }
    table.mobile-cards td.dataTables_empty::before {
        display: none;
    }

    /* Status filter tab strips above back-office lists (telegraphic transfers,
       invoices — shared #statusTabs idiom). The theme forces .nav-tabs to
       flex-wrap: nowrap, so a strip of six or seven tabs pushes the whole page
       into horizontal scroll on a phone — let it wrap instead. */
    #statusTabs {
        flex-wrap: wrap;
    }
}
/* Hover affordances only where a real pointer exists (tablets in card range);
   phones and reduced-motion users are unaffected. */
@media (max-width: 767.98px) and (hover: hover) and (prefers-reduced-motion: no-preference) {
    table.mobile-cards tr {
        transition: box-shadow 0.2s ease, transform 0.2s ease;
    }
    table.mobile-cards tr:hover {
        box-shadow: 0 0.75rem 1.5rem -0.5rem rgba(33, 50, 91, 0.2);
        transform: translateY(-1px);
    }
    table.mobile-cards td:last-child:has(> .btn) > .btn-white {
        transition: background-color 0.15s ease, color 0.15s ease;
    }
    table.mobile-cards td:last-child:has(> .btn) > .btn-white:hover {
        background: var(--bs-primary, #377dff);
        color: #fff;
    }
}

/* --- Statement card = ledger row, not a generic 6-field card. A statement is
   read as a running ledger (amount out/in + balance carried down), so the plain
   label→value stack buries the balance and prints a dead "Debit/Credit: - - -"
   line on every entry. Below md we reshape the same six cells into a compact
   2-column ledger row. Cells map by column position:
   1 Date · 2 Description · 3 Reference · 4 Debit · 5 Credit · 6 Balance.
       Description ........................ 50.00 USD DR
       04 Jul 2026 14:30 ............. Bal 1,250.00 USD
       Ref A1B2C3
   Debit vs credit is triple-coded — the DR/CR tag (colour-independent, keyed on
   the Debit/Credit column position), the amount colour, and the left accent bar —
   so it never depends on colour alone. Card surface/border/shadow reuse the Front
   card tokens (--bs-card-*), so light and dark themes adapt automatically.
   Layers on top of .mobile-cards (both classes are set by the draw callback). */
@media (max-width: 767.98px) {
    table#statementDataTable.statement-cards tbody tr {
        display: grid;
        grid-template-columns: 1fr auto;
        column-gap: 1rem;
        row-gap: 0.1rem;
        align-items: baseline;
        padding: 0.85rem 1rem;
        margin-bottom: 0.65rem;
        border: 1px solid var(--bs-card-border-color, rgba(231, 234, 243, 0.7));
        /* left edge carries the direction accent (set per row below) */
        border-left: 3px solid var(--bs-card-border-color, rgba(231, 234, 243, 0.7));
        border-radius: var(--bs-card-border-radius, 0.75rem);
        background: var(--bs-card-bg, #fff);
        box-shadow: var(--bs-card-box-shadow, 0 0.375rem 0.75rem rgba(140, 152, 164, 0.075));
    }
    /* Direction accent bar: money out (debit) danger, money in (credit) success.
       Keys on whichever side is the REAL movement — i.e. the debit/credit cell that
       is NOT tagged .stmt-zero-amount (the API sends 0.00 on the other side). A row
       with both zero (Opening Balance) keeps the neutral default edge. */
    table#statementDataTable.statement-cards tbody tr:has(td:nth-child(4):not(.stmt-zero-amount)) {
        border-left-color: var(--bs-danger, #ed4c78);
    }
    table#statementDataTable.statement-cards tbody tr:has(td:nth-child(5):not(.stmt-zero-amount)) {
        border-left-color: var(--bs-success, #00c9a7);
    }
    /* Reset the generic label→value flex line; each cell is placed by the grid. */
    table#statementDataTable.statement-cards td {
        display: block !important;
        width: auto !important;
        border-bottom: 0;
        padding: 0.1rem 0;
        text-align: left;
    }
    table#statementDataTable.statement-cards td::before {
        display: none;   /* fields are self-evident; custom prefixes re-added below */
    }
    /* Line 1: Description (lead) | amount (right — colour comes from the span). */
    table#statementDataTable.statement-cards td:nth-child(2) {
        grid-column: 1;
        grid-row: 1;
        font-weight: 600;
        line-height: 1.35;
    }
    table#statementDataTable.statement-cards td:nth-child(4),
    table#statementDataTable.statement-cards td:nth-child(5) {
        grid-column: 2;
        grid-row: 1;
        text-align: right;
        font-weight: 700;
        font-size: 0.95rem;
        white-space: nowrap;
        letter-spacing: -0.01em;
    }
    /* The API sends 0.00 (not null) on the side that doesn't apply, so BOTH debit
       and credit render a value — the JS reads each entry's `type` and tags the
       non-applicable side .stmt-zero-amount. Hide it so only the real movement shows
       on line 1 (no overlap, no "$0.00" noise). Balance lines (type "", e.g. Opening
       Balance) tag both sides and lead with the running balance. */
    table#statementDataTable.statement-cards td.stmt-zero-amount {
        display: none !important;
    }
    /* Colour-independent DR/CR tag by column position (empty side already hidden),
       so debit vs credit reads without relying on the red/green amount colour. */
    table#statementDataTable.statement-cards td:nth-child(4)::after,
    table#statementDataTable.statement-cards td:nth-child(5)::after {
        font-size: 0.7rem;
        font-weight: 600;
        margin-left: 0.3rem;
        color: var(--bs-secondary, #71869d);
        vertical-align: 0.05em;
    }
    table#statementDataTable.statement-cards td:nth-child(4)::after { content: "DR"; }
    table#statementDataTable.statement-cards td:nth-child(5)::after { content: "CR"; }
    /* Line 2: Date (muted) | running balance (right, "Bal" prefix). */
    table#statementDataTable.statement-cards td:nth-child(1) {
        grid-column: 1;
        grid-row: 2;
        color: var(--bs-secondary, #71869d);
        font-size: 0.8125rem;
    }
    table#statementDataTable.statement-cards td:nth-child(6) {
        grid-column: 2;
        grid-row: 2;
        text-align: right;
        white-space: nowrap;
        font-size: 0.8125rem;
        color: var(--bs-secondary, #71869d);
    }
    /* Balance is a running total, not a debit/credit — mute the theme's green span
       so the eye reads amount (coloured) first, balance (secondary) second. */
    table#statementDataTable.statement-cards td:nth-child(6) span {
        color: inherit !important;
        font-weight: 600 !important;
    }
    table#statementDataTable.statement-cards td:nth-child(6)::before {
        content: "Bal ";
        display: inline;
        color: var(--bs-secondary, #71869d);
        font-weight: 400;
    }
    /* Line 3: Reference, full width, muted, "Ref" prefix. */
    table#statementDataTable.statement-cards td:nth-child(3) {
        grid-column: 1 / -1;
        grid-row: 3;
        margin-top: 0.15rem;
        color: var(--bs-secondary, #71869d);
        font-size: 0.78rem;
        overflow-wrap: anywhere;
    }
    table#statementDataTable.statement-cards td:nth-child(3)::before {
        content: "Ref ";
        display: inline;
        font-weight: 600;
        opacity: 0.75;
    }
    /* Balance lines have no reference — the JS tags the blank cell so we drop the
       whole "Ref" line rather than printing "Ref -". */
    table#statementDataTable.statement-cards td:nth-child(3).stmt-blank-ref {
        display: none !important;
    }
    /* Empty / loading state is one spanning cell — keep it block, not gridded, and
       drop the accent/shadow so it doesn't look like a real entry. */
    table#statementDataTable.statement-cards tbody tr:has(td.dataTables_empty) {
        display: block;
        border-left-width: 1px;
        box-shadow: none;
    }
}
/* Hover lift only where a real pointer exists (tablets in card range); phones and
   reduced-motion users are unaffected. */
@media (max-width: 767.98px) and (hover: hover) and (prefers-reduced-motion: no-preference) {
    table#statementDataTable.statement-cards tbody tr {
        transition: box-shadow 0.2s ease, transform 0.2s ease;
    }
    table#statementDataTable.statement-cards tbody tr:hover {
        box-shadow: 0 0.75rem 1.5rem -0.5rem rgba(33, 50, 91, 0.25);
        transform: translateY(-1px);
    }
}

/* --- Auth-modal status/error text. The shared auth panels render their status
   ("Processing Request") and, in several flows, verbose core-banking error
   messages into an <h3 id="…SpinnerText"> / <h4 id="…InputText"> — display-scale
   type that reads oversized and shouty on a phone. Cap these to a readable body
   size below sm across every auth flow (client + back-office). Suffix selectors
   cover the standard and divergent ids (authSpinnerText, soAuthSpinnerText,
   authInputText, createAuthInputText, …). */
@media (max-width: 575.98px) {
    [id$="SpinnerText"],
    [id$="InputText"] {
        font-size: 1.1rem;
        line-height: 1.35;
    }
}

/* --- Detail label→value rows. The shared detail-page pattern
   `<li class="list-group-item"><div class="d-flex justify-content-between">
   <span>Label</span><span>Value</span>` has no column gap, so long values
   (references, beneficiary bank names, narrations) touch or overrun the label on
   phones. Add a gap, and let the value wrap + stay right-aligned instead of
   colliding. Platform-wide: ~224 rows across 17 client + back-office templates;
   scoped to <li> detail rows only, below sm, so desktop is unchanged. */
@media (max-width: 575.98px) {
    .list-group-item > .d-flex.justify-content-between {
        column-gap: 1rem;
    }
    .list-group-item > .d-flex.justify-content-between > :last-child:not(:first-child) {
        min-width: 0;
        text-align: right;
        overflow-wrap: anywhere;
    }
}

/* --- BO dashboard exchange-rates widget. The hand-built 4-column table (Currency +
   Buy/Mid/Sell at 4 decimals) isn't a card-reflow list and overruns a phone card by
   ~23px. Tighten cell padding + type below sm so it fits without a horizontal scroll;
   desktop unchanged. */
@media (max-width: 575.98px) {
    .dash-rates-table th,
    .dash-rates-table td {
        padding: 0.35rem 0.25rem;
        font-size: 0.8125rem;
    }
}

/* --- Card-header action groups. The `card-header-content-md-between` header stacks
   its title above its action row below md, but the action row itself
   (`<div class="d-flex align-items-center gap-2">` of badges + buttons) keeps
   flex-wrap:nowrap, so a multi-button toolbar (e.g. approval-levels "Save
   Configuration") overruns the phone width. Let those action rows wrap; the gap
   utility already supplies the row gap. Shared across back-office card headers. */
@media (max-width: 575.98px) {
    .card-header-content-md-between > .d-flex {
        flex-wrap: wrap;
    }
}

/* --- "Warning / attention" colour convention.
   The theme's stock warning is a pale peach (#f5ca99): unreadable as text on a light soft
   background and washed-out as a button/badge fill next to the saturated soft-success/-danger.
   Rather than keep patching opacity, adopt the theme's OWN orange (--bs-orange #fd7e14) as the
   warning tone wherever it renders "soft" — a legible burnt-orange on a clearly visible
   soft-orange fill, exactly the weight the teal/pink softs carry. This is the house convention:
   any `text-warning` / `bg-soft-warning` / `btn-soft-warning` / `alert-soft-warning` now reads as
   orange, light + dark.
   `theme-dark` is mirrored onto <html> by helper.js.
     Light: text #b5480a on rgba(253,126,20,.12)   Dark: text #fdba74 on rgba(253,126,20,.20)  */

/* Status pills — badges carry `text-warning` (!important utility), so the pill needs
   !important + extra specificity to win both the colour and the fill. */
.badge.bg-soft-warning {
    color: #b5480a !important;
    background-color: rgba(253, 126, 20, 0.12) !important;
}
html.theme-dark .badge.bg-soft-warning {
    color: #fdba74 !important;
    background-color: rgba(253, 126, 20, 0.20) !important;
}

/* Soft-warning buttons — resting + hover both redefined so the orange is consistent through
   the interaction (the theme's default hover flips to solid pale peach, which we replace).
   !important is required: the theme stylesheet loads AFTER custom.css, so a plain override at
   equal specificity loses (same reason the dashboard's attention colour uses !important). */
.btn-soft-warning {
    color: #b5480a !important;
    background-color: rgba(253, 126, 20, 0.12) !important;
    border-color: transparent !important;
}
html.theme-dark .btn-soft-warning {
    color: #fdba74 !important;
    background-color: rgba(253, 126, 20, 0.20) !important;
}
.btn-soft-warning:hover,
.btn-soft-warning:focus,
.btn-soft-warning:active {
    color: #fff !important;
    background-color: #fd7e14 !important;
    border-color: transparent !important;
}

/* Plain `text-warning` outside a badge (icons, inline labels) — keep it legible too. */
.text-warning {
    color: #b5480a !important;
}
html.theme-dark .text-warning {
    color: #fdba74 !important;
}

/* --- Soft ALERT text colours (WCAG AA).
   The theme builds every `.alert-soft-*` by setting the text colour to the SAME hex as its own
   background tint — e.g. `.alert-soft-warning { color:#f5ca99; background:rgba(245,202,153,.15) }`.
   On a light page that is pale sand on off-white: measured 1.43:1 where AA body text needs 4.5:1.
   It is why International Banking reported they "could not see" the request-for-information message
   during UAT — the panel was rendering correctly all along, the words just were not readable.
   Whole family measured on the live page (light / dark):
     light 1.04/1.19 · warning 1.43/7.42 · success 1.87/5.89 · info 2.51/4.47 · danger 2.96/3.86
     · primary 3.17/3.60 · secondary 3.19/2.93 · dark 11.71/9.51 (the only one that already passed)
   Fix the TEXT only — the tints are the house look and stay untouched, so nothing shifts visually
   except legibility. Backgrounds differ per theme (dark mode swaps the whole stylesheet, so the
   same tint composites over #1e2022 instead of white); every variant therefore needs BOTH rules —
   a light-only override would leak its dark ink onto the dark tint and fail the other way.
   `!important` is required for the same reason as the warning block above: the theme stylesheet
   loads AFTER custom.css, so a plain override at equal specificity loses.
   `-dark` is deliberately left alone: it already passes in both themes. */
.alert-soft-primary   { color: #0b47c4 !important; }   /* 6.89:1 on #ebf2ff */
.alert-soft-secondary { color: #4a5763 !important; }   /* 6.66:1 on #f1f3f5 */
.alert-soft-success   { color: #05705f !important; }   /* 5.52:1 on #e6f9f6 */
.alert-soft-info      { color: #0b6879 !important; }   /* 5.78:1 on #e6f6f9 */
.alert-soft-danger    { color: #c1274c !important; }   /* 5.07:1 on #fdedf1 */
.alert-soft-light     { color: #4a5568 !important; }   /* 7.47:1 on #fefeff */

html.theme-dark .alert-soft-primary   { color: #8ab4ff !important; }  /* 6.53:1 on #222e43 */
html.theme-dark .alert-soft-secondary { color: #aab4be !important; }  /* 6.59:1 on #2a2d2f */
html.theme-dark .alert-soft-success   { color: #00c9a7 !important; }  /* 5.89:1 on #1a3936 — theme's own, already legible */
html.theme-dark .alert-soft-info      { color: #3fc9e0 !important; }  /* 6.66:1 on #1b3439 */
html.theme-dark .alert-soft-danger    { color: #ff85a3 !important; }  /* 5.97:1 on #3d272f */
html.theme-dark .alert-soft-light     { color: #d5d8dc !important; }  /* 11.16:1 on #202224 */

/* Warning follows the house orange convention set above (badge / button / text-warning) rather
   than the theme's peach, so an alert matches the pill beside it. */
.alert-soft-warning {
    color: #b5480a !important;                              /* 4.83:1 on #fff0e3 */
    background-color: rgba(253, 126, 20, 0.12) !important;
}
html.theme-dark .alert-soft-warning {
    color: #fdba74 !important;                              /* 6.95:1 on #4b331f */
    background-color: rgba(253, 126, 20, 0.20) !important;
}

/* Links inside a soft alert inherit the corrected ink (the theme hard-codes them to the same
   unreadable tint); the underline carries the affordance instead of colour. */
[class*="alert-soft-"] .alert-link {
    color: inherit !important;
    text-decoration: underline;
}

/* --- Soft BADGE text colours (WCAG AA) — the same accessible ink set as the alerts above.
   Status pills are the app's most-read status signal and the theme tints them the same way it
   tints alerts, so they fail the same way. Measured before (light / dark):
     light 1.04/1.11 · success 1.95/6.49 · info 2.65/4.85 · danger 3.14/4.14 · primary 3.37/3.85
     · secondary 3.37/3.05 · warning 4.83/6.95 (already fixed above) · dark 13.44/12.10
   Badges are 10.5px BOLD — under 18.66px bold is still "normal" text to WCAG, so the bar is 4.5,
   not 3.0. Deliberately reusing the alert inks: one ink per hue across both components, so a
   status pill and the alert beside it agree. Fills untouched. `.badge.bg-soft-*` (0,2,0) outranks
   the `.text-*` utility (0,1,0) it sits next to, and both carry !important. */
.badge.bg-soft-primary   { color: #0b47c4 !important; }   /* 6.89:1 */
.badge.bg-soft-secondary { color: #4a5763 !important; }   /* 6.66:1 */
.badge.bg-soft-success   { color: #05705f !important; }   /* 5.55:1 */
.badge.bg-soft-info      { color: #0b6879 !important; }   /* 5.78:1 */
.badge.bg-soft-danger    { color: #c1274c !important; }   /* 5.07:1 */
.badge.bg-soft-light     { color: #4a5568 !important; }   /* 7.51:1 */

html.theme-dark .badge.bg-soft-primary   { color: #8ab4ff !important; }  /* 6.99:1 */
html.theme-dark .badge.bg-soft-secondary { color: #aab4be !important; }  /* 6.87:1 */
html.theme-dark .badge.bg-soft-success   { color: #00c9a7 !important; }  /* 6.49:1 — theme's own, already legible */
html.theme-dark .badge.bg-soft-info      { color: #3fc9e0 !important; }  /* 7.24:1 */
html.theme-dark .badge.bg-soft-danger    { color: #ff85a3 !important; }  /* 6.40:1 */
html.theme-dark .badge.bg-soft-light     { color: #d5d8dc !important; }  /* 8.48:1 */

/* --- Muted body copy (WCAG AA).
   `.text-muted` / `.form-text` carry most of the app's explanatory prose — helper text under
   fields, timestamps, the "what happens next" paragraphs in review modals. At the theme's #8c98a4
   on white that is **2.94:1**, so the copy written to explain the hard parts is the least readable
   text on the page. `.text-secondary` (#71869d) is 3.75:1 and fails too.
   ⚠️ LIGHT MODE ONLY for muted/form-text: on the dark page (#1e2022) the theme's #91989e already
   measures 5.59:1, so the dark rules below deliberately RESTORE it rather than change it — without
   them the darkened light ink would follow into dark mode and fail there instead.
   Kept a clear step below body text (#1e2022, ~15:1) so the muted/primary hierarchy still reads,
   and kept muted one step lighter than secondary, as the theme intends.
   🪤 These need the `html` prefix, not just `!important`. The theme declares `.text-muted` as an
   `!important` utility at the SAME specificity (0,1,0) and its stylesheet loads AFTER ours, so a
   bare `.text-muted` override loses the tie on source order — measured still rendering at 2.94:1
   until the prefix was added. `html .text-muted` (0,1,1) wins on specificity regardless of order,
   and `html.theme-dark .text-muted` (0,2,1) still outranks that for the dark case. */
html .text-muted,
html .form-text {
    color: #5c6b7a !important;    /* 5.47:1 on white (was #8c98a4, 2.94:1) */
}
html.theme-dark .text-muted,
html.theme-dark .form-text {
    color: #91989e !important;    /* 5.59:1 on #1e2022 — the theme's own value, preserved */
}

html .text-secondary {
    color: #4f5d6b !important;    /* 6.75:1 on white (was #71869d, 3.75:1) */
}
html.theme-dark .text-secondary {
    color: #aab4be !important;    /* 7.77:1 on #1e2022 (was #6d747b, 3.45:1) */
}

/* --- App toast (#appToast) — icon-forward soft style. Replaces the old solid colour slab:
   a tinted background (bg-soft-* set per type by displayToaster) with a coloured leading icon,
   bold title and muted message. Icon colours use the theme's own accent values so they read on
   both light and dark; the warning icon uses the legible amber (and its lighter dark variant),
   matching the badge/button fix above. */
/* BOLD, opaque, colour-filled toast with a bit of delight. The app's page is white, so a
   subtle outline gets missed — this keeps the old solid-colour attention but lifts it with a
   white circular icon badge, a slide-in, and a slim auto-dismiss progress bar. Own app-toast-*
   type classes carry a solid theme colour; text/close are white and the icon glyph takes the
   type colour inside its white badge. Solid colours read the same in light + dark.
   !important beats the theme's translucent .toast / bg-* backgrounds (theme loads after us). */
.app-toast {
    border: none !important;
    border-radius: 0.625rem;
    box-shadow: 0 0.75rem 2rem rgba(0, 0, 0, 0.25);
    color: #fff !important;
    overflow: hidden;                 /* clip the progress bar to the rounded corners */
}
.app-toast .toast-body {
    align-items: center;
    padding: 0.9rem 1rem;
}
.app-toast .toast-title {
    color: #fff;
    font-weight: 600;
}
.app-toast .toast-message {
    color: rgba(255, 255, 255, 0.92) !important;
}
.app-toast .btn-close {
    filter: invert(1) grayscale(100%) brightness(200%);
}
/* Big white type icon on the solid colour — bold and unmistakable. */
.app-toast .app-toast-icon {
    font-size: 1.75rem;
    line-height: 1;
    color: #fff;
    flex-shrink: 0;
}
.app-toast-success {
    background-color: #00c9a7 !important;
}
.app-toast-info {
    background-color: #377dff !important;
}
.app-toast-warning {
    background-color: #fd7e14 !important;
}
.app-toast-danger {
    background-color: #ed4c78 !important;
}
/* Slide-in + auto-dismiss progress bar. Gated on .app-toast-animate, which displayToaster
   removes and re-adds (with a reflow) each time so the animation restarts on every toast. */
.app-toast-animate {
    animation: appToastIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
.app-toast-animate::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    height: 3px;
    width: 100%;
    background: rgba(255, 255, 255, 0.55);
    transform-origin: left;
    animation: appToastProgress 5s linear forwards;
}
@keyframes appToastIn {
    from { opacity: 0; transform: translateX(1.5rem); }
    to   { opacity: 1; transform: translateX(0); }
}
@keyframes appToastProgress {
    from { transform: scaleX(1); }
    to   { transform: scaleX(0); }
}
@media (prefers-reduced-motion: reduce) {
    .app-toast-animate,
    .app-toast-animate::after { animation: none; }
}

/* --- Auth page message tone.
   The login page carries one message slot for four very different events, and every one of them used
   to render in error red — including "You have been logged out successfully!". The tone now follows the
   message. These sit on the dark navy auth artwork, NOT on a white card, so the theme's stock text-*
   tones don't all work: text-warning is the pale peach the house convention already retired (see the
   warning/attention block above), and text-info's cyan collides with the background's own cyan dots.
   Colours below are picked for contrast on that navy at heading size. */
.auth-message-success { color: #34d399; }   /* clean sign-out — reassurance, not alarm */
.auth-message-info    { color: #cbd5e1; }   /* session timed out — informational; quiet slate, not a cyan that fights the artwork */
.auth-message-warning { color: #fdba74; }   /* signed out by a login elsewhere — worth noticing; house orange */
.auth-message-danger  { color: #fb7185; }   /* a genuine failure: bad credentials, locked account */

/* --- Audit dashboard: the trail-health status dot.
   Every way an audit trail fails is silent — the switch is off, the monthly partitions ran out and
   PostgreSQL is rejecting inserts, the database is down and records are queuing on disk. In all three
   the counters read zero, which looks exactly like a quiet day. The dot is the one thing on the screen
   that distinguishes them, so it carries a live pulse when (and only when) the trail is recording. */
.audit-status-dot {
    display: inline-block;
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    background: var(--bs-secondary, #8c98a4);
}
.audit-status-dot-success {
    background: var(--bs-success, #00c9a7);
    animation: audit-status-pulse 2s ease-out infinite;
}
.audit-status-dot-warning { background: var(--bs-warning, #f5ca99); }
.audit-status-dot-danger  { background: var(--bs-danger, #ed4c78); }

/* The pulse says "live" — it is the heartbeat, so it belongs only on the healthy state. A warning or a
   danger state is static: a blinking alarm reads as activity, which is the opposite of what it means. */
@keyframes audit-status-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(0, 201, 167, 0.55); }
    70%  { box-shadow: 0 0 0 0.5rem rgba(0, 201, 167, 0); }
    100% { box-shadow: 0 0 0 0 rgba(0, 201, 167, 0); }
}
@media (prefers-reduced-motion: reduce) {
    .audit-status-dot-success { animation: none; }
}

/* Counter cards that are also links into the filtered search. They must not look like buttons — the
   card IS the target — so the only affordance is the theme's own hover shadow plus the "View records"
   cue inside. Inherit the colour, or the whole card turns link-blue. */
.audit-stat-card {
    color: inherit;
    text-decoration: none;
}
.audit-stat-card:hover,
.audit-stat-card:focus {
    color: inherit;
}

/* The activity feed is capped to the hourly chart's height beside it and scrolls. Uncapped, it would
   stretch its whole row taller as the day fills up — and on a phone the cards stack, where a scrolling
   panel inside a scrolling page is a trap, so there it simply grows. */
@media (min-width: 992px) {
    .audit-feed {
        max-height: 24rem;
        overflow-y: auto;
    }
}

/* Phone: the dashboard's header actions become a full-width row of comfortable targets rather than
   three small controls crowding the right edge (which overflowed the page at 375px). */
@media (max-width: 575.98px) {
    .audit-header-actions {
        width: 100%;
    }
    .audit-header-actions .btn {
        flex: 1 1 auto;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    .audit-header-actions #lastUpdated {
        width: 100%;
    }
}

/* Phone: the feature-exceptions card stacks. As a two-column table at 375px the feature name is
   squeezed to a five-line sliver and the dropdown is pushed off-screen, reachable only by scrolling
   sideways inside the table. Below sm each row becomes label-above-control, full width. */
@media (max-width: 575.98px) {
    #featureExceptionsCard .table-responsive {
        overflow-x: visible;
    }
    #featureExceptionsCard table,
    #featureExceptionsCard tbody,
    #featureExceptionsCard tr,
    #featureExceptionsCard td {
        display: block;
        width: 100%;
    }
    #featureExceptionsCard thead {
        display: none;
    }
    #featureExceptionsCard tr + tr {
        border-top: 1px solid var(--bs-border-color);
    }
    #featureExceptionsCard td {
        padding: 0.75rem 1.25rem;
    }
    #featureExceptionsCard td:first-child {
        padding-bottom: 0.25rem;
    }
    /* The desktop floor would keep it wider than the phone viewport. */
    #featureExceptionsCard .js-exception-select {
        min-width: 0;
        min-height: 44px;
    }
}

/* Phone: the client-features matrix stacks. As a three-column grid at 375px the INDIVIDUAL column sits
   off the right edge — reachable only by scrolling sideways inside the table, so an admin can neither
   see both client types at once nor tell the second one is there. Below sm each feature becomes a card
   with one labelled switch row per client type. */
@media (max-width: 575.98px) {
    #clientFeaturesTable,
    #clientFeaturesTable tbody,
    #clientFeaturesTable tr,
    #clientFeaturesTable td {
        display: block;
        width: 100%;
    }
    #clientFeaturesTable thead {
        display: none;
    }
    #clientFeaturesTable tr + tr {
        border-top: 1px solid var(--bs-border-color);
    }
    #clientFeaturesTable td {
        padding: 0.5rem 1.25rem;
    }
    #clientFeaturesTable td:first-child {
        padding-top: 1rem;
        padding-bottom: 0.5rem;
    }
    /* label ← → switch, with a 44px row so the toggle is a comfortable target */
    #clientFeaturesTable .feature-toggle-cell {
        display: flex;
        align-items: center;
        justify-content: space-between;
        min-height: 44px;
        text-align: left;
    }
    #clientFeaturesTable .feature-toggle-cell::before {
        content: attr(data-label);
        font-size: .8125rem;
        color: var(--bs-secondary-color);
    }
}

/* Profile switcher (topbar + the account menu on phones).
   The toggle must still read as the page's profile heading, not a link: only the caret marks it as
   clickable, so a single-profile user and a multi-profile user see the same name in the same colour. */
.profile-switch-toggle,
.profile-switch-toggle:hover,
.profile-switch-toggle:focus {
    color: inherit;
    text-decoration: none;
}

.profile-switch-menu {
    min-width: 15rem;
}

/* Switching profile is a touch action first — the phone entry point lives in the account menu, where
   the default dropdown row is well under a comfortable target. */
.profile-switch-item {
    min-height: 44px;
}

/* Copy-to-clipboard icon buttons (.copy-id-btn — references, account numbers, SWIFT codes).
   The bare icon is a ~14x17 hit area, far under a usable touch target, and it is the ONLY way to
   lift a reference off a detail page on a phone. Padding rather than min-height: the button sits
   inline beside the value it copies, so growing its box vertically would push the row apart.
   Phones only — the desktop cursor does not need the extra area and the rows stay tight. */
@media (max-width: 575.98px) {
    .copy-id-btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 44px;
        min-height: 44px;
        margin: -0.75rem -0.5rem -0.75rem 0.25rem;   /* grow the hit area, not the layout */
        vertical-align: middle;
    }
}

/* STP message reference chips (#messageReferenceChips — Reference, Tracking ID (UETR), Settlement ref).
   A UETR is 36 characters inside a Bootstrap .badge, which is white-space:nowrap and therefore refuses
   to shrink: the chip runs off the right edge and takes the whole page into horizontal scroll (437px
   of content in a 375px viewport). The flex-wrap on the container cannot help — it wraps between
   chips, never inside one. Let the chip wrap its own contents instead.
   Phones only — the desktop row fits on one line and stays exactly as it was. */
@media (max-width: 575.98px) {
    #messageReferenceChips .badge {
        max-width: 100%;
        white-space: normal;
        text-align: left;
    }

    #messageReferenceChips code {
        overflow-wrap: anywhere;
    }
}

/* STP messages work-queue tiles (#workQueueTiles).
   .stp-tile-scope is the one-line scope caption under each count ("Last 31 days", "Inbound, all
   statuses") - smaller than .small so the count and label stay the tile's voice.
   .stp-tile-active marks the tile whose filter preset the table currently shows; the border (not a
   fill) keeps the count's tone colour legible. Cleared the moment the filters are hand-tuned. */
.stp-tile-scope {
    font-size: .6875rem;
}

.card.stp-tile-active {
    border-color: var(--bs-primary);
}

/* --- Client dual control: the old/new comparison on a phone.
   Three columns (Field / Currently / Submitted) do not fit 375px, and a table-responsive wrapper
   answers that with sideways scroll — which puts the SUBMITTED value, the one thing the checker
   opened the page to read, off the edge of the screen behind a scrollbar they may not notice.
   Below md the row becomes a block and each cell announces itself, the same shape
   applyMobileCardLabels gives every server-side list. Breakpoint-scoped: desktop is untouched. */
@media (max-width: 767.98px) {
    .change-diff thead {
        display: none;
    }
    .change-diff, .change-diff tbody, .change-diff tr, .change-diff td {
        display: block;
        width: 100%;
    }
    .change-diff tr {
        padding: .75rem 0;
        border-bottom: 1px solid var(--bs-border-color, #e7eaf3);
    }
    .change-diff tr:last-child {
        border-bottom: 0;
    }
    .change-diff td {
        padding: .125rem 0;
        white-space: normal;
        word-break: break-word;
    }
    /* The field name is the row's heading, so it leads and the values sit under it. */
    .change-diff td.change-diff-field {
        font-weight: 600;
        padding-bottom: .25rem;
    }
    .change-diff td.change-diff-field::before {
        content: none;
    }
    .change-diff td[data-label]:not(.change-diff-field)::before {
        content: attr(data-label) ": ";
        display: inline;
        margin-right: .25rem;
        color: var(--bs-secondary-color, #8c98a4);
        font-size: .8125rem;
    }
}

/* Outstanding-change badges in the client header (fragments/client-change-banner.html).
   They sit in a list of otherwise INERT metadata — "16 linked accounts", the ACTIVE badge — but
   these two are links, and at the badge's natural ~20px they are the one tappable thing in that
   line and the hardest to hit on the page. Lifted to the house 44px touch target on phones only,
   so the desktop header line keeps the badge proportions it shares with ACTIVE/INACTIVE. */
@media (max-width: 575.98px) {
    #clientChangePendingItem,
    #clientChangeFailedItem {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
        padding-top: .5rem;
        padding-bottom: .5rem;
    }
}

/* .search-roomy — a filter-bar search box whose placeholder NAMES what it matches ("Search client,
   user, staff name or reason") rather than just the list ("Search invoices"). The theme sizes these
   boxes at ~216px, which clips that sentence mid-word, so the one line telling an officer what the
   box will actually search is the line they cannot read.
   A max-width, never a min-width: from md up the box takes the room its column already has spare;
   below it the column is full-width and the box collapses with it, exactly as before. */
@media (min-width: 768px) {
    .input-group.search-roomy {
        width: 21rem;
        max-width: 100%;
    }
}

/* Client Approvals status badge. It names the state in the SAME words as the status filter and the
   attention chip ("Approved but not applied") rather than the enum, which is what stops one state
   reading as three different things across the page. The cost is a long badge, and a Bootstrap badge
   is white-space:nowrap — so between the card-view breakpoint and the desktop layout it becomes the
   widest cell on the row and pushes the whole table into its own sideways scroll (a tablet, or a
   phone held landscape). Let it wrap in that band only; the desktop badge stays on one line. */
@media (max-width: 991.98px) {
    #changeRequestsDataTable td .badge {
        white-space: normal;
    }
}
