/* Bikabo brand overrides — kept separate from the vendor theme.css.
 *
 * The theme's own color ramp is only half-migrated to the Bikabo brand:
 * --primary-600 (--brand) was updated to the brand green (#005949), but
 * --primary-700/800/900 were never updated and are still the original
 * template's blue. Plain .btn-primary is also never styled anywhere in
 * style.css (only numbered variants like .btn-primary-600 are), so every
 * "btn btn-primary" across the app falls through to Bootstrap's own
 * default blue. This file fixes the plain .btn-primary class to use the
 * actual brand green, with hover/active states derived from it via
 * brightness() rather than reusing the stale blue --primary-700/800.
 */

.btn-primary {
    background-color: var(--primary-600);
    border-color: var(--primary-600);
    color: #fff;
}

.btn-primary:hover {
    background-color: var(--primary-600);
    border-color: var(--primary-600);
    color: #fff !important;
    filter: brightness(90%);
}

.btn-primary:active,
.btn-primary:focus {
    background-color: var(--primary-600) !important;
    border-color: var(--primary-600) !important;
    color: #fff !important;
    filter: brightness(80%);
}

/* .btn-outline-primary has the exact same gap as .btn-primary above: the
 * theme correctly defines .btn-outline-success/-danger/-warning/-info with
 * their own color ramps, but never defined .btn-outline-primary at all —
 * only numbered variants like .btn-outline-primary-600 exist — so every
 * "btn btn-outline-primary" (60+ usages) fell through to Bootstrap's
 * default blue. No --primary-hover/--primary-pressed aliases exist (unlike
 * success/danger), so hover/active reuse --primary-600 with a solid fill /
 * brightness() the same way .btn-primary's active state does.
 */
.btn-outline-primary {
    background-color: transparent;
    color: var(--primary-600);
    border-color: var(--primary-600);
}

.btn-outline-primary:hover {
    background-color: var(--primary-600);
    color: #fff !important;
    border-color: var(--primary-600);
}

.btn-outline-primary:active,
.btn-outline-primary:focus {
    background-color: var(--primary-600) !important;
    color: #fff !important;
    border-color: var(--primary-600) !important;
    filter: brightness(80%);
}

/* Plain "nav nav-tabs" tab strips (settings.php, engagement_edit.php,
 * task_view.php, ...). Same gap as .btn-primary above: the theme only
 * styles specific tab variants (.bordered-tab, .pill-tab, etc.) and never
 * touches the base .nav-tabs .nav-link, so inactive tabs fall through to
 * Bootstrap's default link color (blue) while the active tab's color is
 * already overridden by Bootstrap's own .nav-tabs .nav-link.active rule.
 */
.nav-tabs .nav-link {
    color: var(--text-secondary-light);
}

.nav-tabs .nav-link:hover,
.nav-tabs .nav-link:focus {
    color: var(--primary-600);
}

/* Tables in dark mode. Bootstrap ships its own dark palette, but it's
 * gated behind [data-bs-theme="dark"], which nothing in this app ever
 * sets — app.js's theme toggle only sets [data-theme]. So .table cells
 * (color/background-color are hardcoded per-cell to --bs-table-color /
 * --bs-table-bg, which resolve to --bs-emphasis-color / --bs-body-bg,
 * Bootstrap's light-mode root values) stayed white-on-black regardless
 * of the app's own dark mode. Repoint those to the app's own theme vars
 * under [data-theme="dark"] instead of trying to wire up Bootstrap's
 * separate dark-mode system.
 */
[data-theme=dark] .table {
    --bs-table-bg: var(--white);
    --bs-table-color: var(--text-primary-light);
    --bs-table-border-color: var(--border-color);
}

/* Clickable dashboard stat cards (dashboard.php's stat_card()). The whole
 * card is a link via Bootstrap's .stretched-link, which is invisible by
 * itself — without a hover state the card gives no sign it can be clicked,
 * so the affordance has to be on the card. Border + shadow only: no
 * transform, since these sit in a grid and shifting one nudges the row's
 * baseline against its neighbours.
 */
.dashboard-stat-link {
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
    /* Contains the hint's z-index (below) inside the card. The theme's
     * .navbar-header is `position: sticky; z-index: 2`, and the hint needed
     * a z-index of its own to sit above the card's .stretched-link overlay.
     * Without a stacking context here those two 2s competed globally, and
     * because the card comes later in the DOM the card won: scrolling made
     * the label and its tooltip paint straight through the sticky navbar.
     * isolation creates the context without changing anything about how the
     * card itself paints. */
    isolation: isolate;
}

.dashboard-stat-link:hover {
    border-color: var(--primary-600) !important;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

/* .stretched-link's ::after covers the card, so the pointer has to be set
 * on the overlay rather than inherited from the anchor's own box. */
.dashboard-stat-link .stretched-link::after {
    cursor: pointer;
}

/* Dashboard stat card values. The theme sizes every h4 at
 * `--h4: clamp(1.375rem, 1.095rem + 1.4vw, 2.25rem)`, which reaches 36px on
 * a desktop — wide enough that a formatted money value ("KES 4,460.00")
 * did not fit in the space left beside the card's 56px icon and broke onto
 * a second line. That made the text block taller, which in turn pushed the
 * icon down relative to the cards next to it.
 *
 * The markup now top-aligns the icon so alignment holds regardless, but the
 * wrap itself still looked wrong, so values get their own smaller scale and
 * are not allowed to break: a currency and its amount belong on one line.
 * !important is required because the theme's own h4 rule uses it.
 */
.dashboard-stat-value {
    font-size: clamp(1.25rem, 0.9rem + 0.9vw, 1.75rem) !important;
    white-space: nowrap;
}

/* min-width:0 lets the text column actually shrink inside the flex row —
 * a flex item defaults to min-width:auto, which refuses to go below its
 * content's intrinsic width and would push the icon out of the card
 * instead of letting the value scale down. */
.dashboard-stat-text {
    min-width: 0;
}

/* Quick actions (dashboard.php). Now pills on the page header line rather
 * than a full-width card of their own: as a card they were four small icons
 * in a wide band of empty white that cost roughly a fifth of the first
 * screen to say nothing. Still flat rather than .btn — four buttons of equal
 * weight in a row reads as a toolbar, which is heavier than four shortcuts
 * deserve — but they now carry a border, because without the card behind
 * them there is nothing else marking them as controls.
 *
 * Cosmetics only. The pill's structure (the inline-flex row, the gap, the
 * icon circle, the nowrap) is carried by Bootstrap utilities in the markup,
 * because this file arrives as a separate upload and a control that needs it
 * to lay out at all collapses into an unstyled icon beside a word whenever
 * it does not. Everything here can go missing and the pill still reads. */
.quick-action {
    text-decoration: none;
    color: var(--text-primary-light);
    border: 1px solid var(--neutral-200);
    background-color: var(--white);
    transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

.quick-action:hover,
.quick-action:focus-visible {
    background-color: var(--neutral-50);
    border-color: var(--primary-600);
    color: var(--text-primary-light);
}

.quick-action-label {
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.25;
}

/* The separator between tenant name and date in the dashboard header. A
 * middot needs breathing room that a plain space does not give it. */
.dashboard-header-dot {
    margin: 0 0.375rem;
    opacity: 0.5;
}

/* The greeting's entrance, applied only on the first dashboard render after
 * a sign-in (dashboard.php consumes a one-shot session marker set by
 * auth_login.php, so the class is simply absent on every later visit).
 *
 * A keyframe rather than Bootstrap's .fade/.show pair, deliberately.
 * Bootstrap ships no entrance animation — .fade is a 0.15s opacity
 * transition that starts at `opacity: 0` and only becomes visible once
 * JavaScript adds .show. On the app's landing page that trade is bad: any
 * failure to run that script leaves a signed-in user staring at an
 * invisible greeting, and 0.15s linear is too quick to register as anything
 * but a flicker. An animation needs nothing to run it, and a page with no
 * CSS at all still shows the text.
 *
 * `both` fill mode holds the opening frame before the animation starts, so
 * there is no flash of final-position text on a slow paint.
 *
 * Deliberately small: 6px of travel and under half a second. This is meant
 * to catch the eye on the way past, not to be watched. */
@keyframes bikabo-greeting-in {
    from {
        opacity: 0;
        transform: translateY(-6px);
    }
    to {
        opacity: 1;
        transform: none;
    }
}

.dashboard-greeting {
    animation: bikabo-greeting-in 0.45s ease-out both;
}

/* Motion is the whole mechanism here, so honouring the preference means
 * turning it off rather than shortening it. The greeting still arrives, it
 * just arrives already in place. */
@media (prefers-reduced-motion: reduce) {
    .dashboard-greeting {
        animation: none;
    }
}

/* Headline tile values (dashboard.php's hero_tile()).
 *
 * nowrap for the reason .dashboard-stat-value gives below: a currency and
 * its amount belong on one line. !important because the theme's own h4 rule
 * uses it.
 *
 * The size is a width budget, not a taste call, and it is the thing that
 * broke once already. At 2rem this value overflowed a column it shared with
 * the icon and painted underneath it. The markup now gives the value the
 * full width of the card (the icon moved up onto the label's line), which
 * buys back roughly the icon's 48px plus its gap — but the cap comes down
 * from 2rem to 1.875rem anyway, because the margin at col-xxl-3 on a 1280px
 * screen is thin enough that "KES 2,450,000.00" on a seven-figure tenant
 * would spend it.
 *
 * The vw term is what actually keeps this safe across screens: a wider
 * viewport means a wider card AND a larger value, roughly in step, so the
 * ratio of text width to available width stays put instead of drifting into
 * a collision at one particular breakpoint.
 *
 * Hierarchy over .dashboard-stat-value therefore does NOT come from size
 * alone — there is not enough width in a quarter-row card for that to be
 * the lever. It comes from the gradient ground, the coloured icon chip and
 * the context line, none of which the plain stat card has. */
.dashboard-hero-value {
    font-size: clamp(1.25rem, 0.85rem + 1vw, 1.875rem) !important;
    white-space: nowrap;
    line-height: 1.2;
}

/* The context line under a headline value (a delta, a movement, a
 * denominator). Deliberately quiet: it qualifies the number, it does not
 * compete with it. Allowed to wrap, unlike the value above it — this is
 * prose and a second line of it is fine. */
.dashboard-hero-context {
    line-height: 1.35;
}

/* Attention chips (dashboard.php's attention_chip()) — the six work queues
 * that used to be six more full stat cards, mostly reading 0.
 *
 * Cosmetics only, same rule as .quick-action above: the row, the gap and
 * the icon circle are Bootstrap utilities in the markup.
 *
 * A chip at rest (count 0) is dimmed rather than hidden. Hiding would take
 * away the way to reach the empty list behind it, and would make the strip
 * reflow from one day to the next as queues empty and fill. */
.attention-chip {
    color: var(--text-primary-light);
    border: 1px solid var(--neutral-200);
    background-color: var(--white);
    transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

.attention-chip:hover,
.attention-chip:focus-visible {
    background-color: var(--neutral-50);
    border-color: var(--primary-600);
    color: var(--text-primary-light);
}

.attention-chip-count {
    font-size: 1rem;
    line-height: 1;
}

.attention-chip-label {
    color: var(--text-secondary-light);
}

/* Bootstrap's own .opacity-50 is what stat_card() uses for the same idea;
 * this needs a rule instead because the dimming has to reach the chip's
 * border and its two text spans together, and an opacity on the anchor
 * would also fade the focus ring. */
.attention-chip.is-at-rest .attention-chip-count,
.attention-chip.is-at-rest .attention-chip-label {
    color: var(--text-secondary-light);
    opacity: 0.65;
}

/* The staleness badge on a chip (dashboard.php's queue_age_label()).
 *
 * Only rendered once the oldest item in that queue passes the threshold, so
 * it is always an exception and can afford to look like one. A tinted pill
 * rather than more grey text: grey would put it at the same weight as the
 * label it follows, and the entire reason it is there is that it outranks
 * the label. warning rather than danger — a queue slowing down is worth
 * noticing, not an emergency, and danger is spoken for by queues that are
 * themselves failures (Needs Decision). */
.attention-chip-age {
    background-color: var(--warning-100, #FFF3E0);
    color: var(--warning-600);
    border-radius: 999px;
    padding: 0.0625rem 0.4375rem;
    line-height: 1.5;
    white-space: nowrap;
}

/* The cost pipeline bar (dashboard.php's Financial Overview).
 *
 * Taller than the theme's .progress-sm (8px, right for a row-level meter in
 * a table) and shorter than Bootstrap's 1rem default, which as a
 * three-segment bar reads as a block of colour rather than a measure.
 *
 * overflow is set explicitly rather than inherited: Bootstrap's .progress
 * carries overflow:hidden, but this build also ships a bare
 * `.progress{overflow:visible}` rule, and which one wins decides whether the
 * end segments get the radius-8 corners the markup asks for. Stating it here
 * removes the question. */
.cost-pipeline {
    height: 14px;
    overflow: hidden;
}

/* The bar is composed of three disjoint amounts, so a segment can
 * legitimately be a sliver. A minimum width keeps a small-but-real segment
 * visible instead of collapsing it to nothing and making the bar read as
 * two-part. Segments that are actually zero are not rendered at all, so this
 * never invents one. */
.cost-pipeline .progress-bar {
    min-width: 3px;
}

/* Status legend rows under the donut (dashboard.php's "Where work is").
 * Each row is a link to that status filtered on task_list.php, and needs to
 * look like one without becoming six blue links in a stack. */
.status-legend-row {
    color: var(--text-primary-light);
    padding: 0.25rem 0.375rem;
    margin: 0 -0.375rem;
    border-radius: 0.375rem;
    transition: background-color 0.15s ease-in-out;
}

.status-legend-row:hover,
.status-legend-row:focus-visible {
    background-color: var(--neutral-50);
    color: var(--text-primary-light);
}

/* ApexCharts renders its own SVG text nodes with inline colours resolved at
 * render time, which the dark-mode observer in dashboard.php updates. The
 * tooltip, though, is a DOM element Apex styles from its own stylesheet, and
 * that stylesheet hardcodes a light background — so in dark mode a tooltip
 * came out white-on-white until this. Scoped to the app's own [data-theme],
 * not Bootstrap's [data-bs-theme], which this app's toggle never sets. */
[data-theme=dark] .apexcharts-tooltip {
    background: var(--neutral-100) !important;
    border-color: var(--neutral-200) !important;
    color: var(--text-primary-light) !important;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4) !important;
}

[data-theme=dark] .apexcharts-tooltip-title {
    background: var(--neutral-200) !important;
    border-color: var(--neutral-300) !important;
    color: var(--text-primary-light) !important;
}

/* Hint icons (helpers.php's hint_icon() / label_with_hint()).
 *
 * <iconify-icon> is a web component and nothing in the theme styles it, so
 * placed inline next to a label it aligns on its own intrinsic baseline and
 * lands visibly below the text instead of beside it. The fix is to stop
 * relying on inline alignment at all: .hint-label is an inline-flex row, so
 * align-items:center optically centres the icon against the label's line box
 * whatever the element's own metrics are.
 *
 * inline-flex rather than flex so a label keeps its natural width and can
 * still sit inside a heading, table cell, or paragraph without becoming a
 * full-width block.
 *
 * THIS IS THE PATTERN TO REUSE for any icon sitting beside text.
 */
.hint-label {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

.hint-icon {
    color: var(--text-secondary-light, #7c8091);
    cursor: help;
    flex-shrink: 0;
    /* Independent of the label's own font-size, so the icon stays a
     * consistent, unobtrusive size next to both a 14px card label and a
     * larger heading. */
    font-size: 0.95rem;
    line-height: 1;
}

.hint-icon:hover,
.hint-icon:focus-visible {
    color: var(--primary-600);
}

/* Password reveal toggle (the button partials/scripts.php injects into every
 * input[type="password"]).
 *
 * The wrapper is added by that script rather than asked of each page, because
 * the pages carrying password fields agree on nothing structurally: the auth
 * screens wrap theirs in .position-relative next to an .icon-field, while
 * profile.php, portal_account.php and team_transfer_ownership.php put a bare
 * input inside a plain column. Positioning against a wrapper the script owns
 * is the only version that lands in the same place on all five.
 */
.password-field {
    position: relative;
    display: block;
}

/* Padding, not a shorter input: the field keeps its full width and the typed
 * text stops short of the button, so a long password never runs underneath
 * the thing you would click to read it. Logical property, so it flips with
 * the .icon-field lock padding rather than fighting it in RTL. */
.password-field .form-control {
    padding-inline-end: 2.75rem;
}

.password-toggle {
    position: absolute;
    inset-inline-end: 0;
    top: 50%;
    transform: translateY(-50%);
    margin-inline-end: 0.75rem;
    padding: 0.25rem;
    border: 0;
    background: transparent;
    color: var(--text-secondary-light);
    font-size: 1.125rem;
    line-height: 1;
    cursor: pointer;
}

.password-toggle:hover,
.password-toggle:focus-visible {
    color: var(--primary-600);
}

/* The currency-mismatch warning icon (helpers.php's
 * currency_mismatch_warning()). Same geometry as .hint-icon above, and it
 * sits in the same .hint-label row, but it deliberately does not reuse that
 * class: .hint-icon pins its own neutral colour, and here the colour is the
 * signal. Inheriting lets the icon take the warning colour from the text it
 * belongs to, and stay correct if that text is ever recoloured.
 *
 * No :hover rule either. A hint icon brightens because it invites a hover;
 * this one is a statement about the figure beside it, and it reads as such
 * whether or not anyone points at it. */
.mismatch-icon {
    color: inherit;
    cursor: help;
    flex-shrink: 0;
    font-size: 0.95rem;
    line-height: 1;
}

/* A stat card's stretched-link overlay covers the whole card, including the
 * hint. Lifting the label above it keeps the icon hoverable instead of the
 * card-wide link swallowing the pointer. Bootstrap's .stretched-link::after
 * is z-index 1, so 2 is enough, and .dashboard-stat-link's `isolation`
 * keeps that 2 scoped to the card instead of competing with the navbar. */
.dashboard-stat-link .hint-label {
    position: relative;
    z-index: 2;
}

/* DataTables control rows on small screens (the "entries per page" + "Search:"
 * strip above every table, and the "Showing 1 to N" + paging strip below it).
 *
 * dataTables.min.css stacks those two cells into full-width blocks under
 * 767px, which is right. What goes wrong is that style.css styles the groups
 * inside them as flex rows pinned to opposite ends, at every width and with
 * no media query of its own:
 *
 *   .basic-data-table .dt-layout-row .dt-search { justify-content: flex-end }
 *   div.dt-container .dt-paging          { justify-content: flex-end }
 *
 * Those two never collide on a desktop row, where "left cell, right cell" is
 * exactly the intent. Once the cells stack, the pinning survives and the ends
 * it justifies against become the full card width, so a phone gets "entries
 * per page" hard against the left edge and "Search:" hard against the right
 * on the line below it, with a gap down the middle and nothing tying the two
 * rows together. The same split repeats under the table with the row count and
 * the pager. Nothing else in the app is right-aligned on mobile, which is what
 * makes the top of a table read as though it belongs to a different screen.
 *
 * The vendor's own mobile rule centres all three cell alignments instead
 * (dt-full/dt-start/dt-end), which is a third alignment again and is
 * overridden here too. Stacked controls read from the left, like every card,
 * label and heading around them.
 */
@media screen and (max-width: 767px) {
    div.dt-container div.dt-layout-cell.dt-full,
    div.dt-container div.dt-layout-cell.dt-start,
    div.dt-container div.dt-layout-cell.dt-end {
        text-align: left;
    }

    .basic-data-table .dt-layout-row .dt-search,
    div.dt-container .dt-paging {
        justify-content: flex-start;
    }

    /* The search input carries an intrinsic ~20-character width and, as a
     * flex item, defaults to min-width:auto — so it refuses to shrink below
     * that and pushes past a narrow card instead. Letting it grow into the
     * space left over from the "Search:" label, and allowing it to shrink,
     * gives a full-width tap target that stays inside the card. */
    .basic-data-table .dt-layout-row .dt-search .dt-input {
        flex: 1 1 auto;
        min-width: 0;
    }

    /* style.css zeroes .dt-layout-cell padding for the desktop row, where the
     * cells sit side by side and need none. Stacked, they need the gap back
     * or the two control rows touch. */
    .basic-data-table .dt-layout-row .dt-layout-cell + .dt-layout-cell {
        margin-top: 0.75rem;
    }

    /* "entries per page" costs a stacked row plus its margin, roughly 65px of
     * the first screen, to expose a preference nobody sets on a phone: the
     * answer there is to keep scrolling. Search stays — it is the fastest way
     * into a long list on a small screen, and the only one that beats
     * scrolling. The select is only hidden, never removed, so DataTables
     * keeps its 25-row default and the desktop control is untouched. */
    .basic-data-table .dt-layout-row .dt-length {
        display: none;
    }

    /* And the cell it sat in, so it does not leave its margin behind. Split
     * from the rule above deliberately: a browser without :has() drops only
     * this one and the result is a 12px gap, not a visible control. */
    .basic-data-table .dt-layout-row .dt-layout-cell:has(.dt-length) {
        display: none;
    }
}

/* Task list columns on small screens (task_list.php).
 *
 * The table carries nine columns. On a phone that is a horizontal scroller
 * roughly three screens wide, which asks someone checking their work to drag
 * sideways to find out whether a Task is theirs and what it needs — the two
 * things they opened the page for.
 *
 * Three columns survive: what the Task is, what state it is in, and the one
 * button that moves it forward. The rest is context, and the Task cell now
 * carries the Engagement and Client on their own line (d-md-none in the
 * markup) so hiding those columns loses no information, only the sideways
 * drag.
 *
 * Hidden with CSS rather than dropped from the markup, which matters: the
 * cells stay in the DOM, so DataTables still sorts and — more useful —
 * still searches them. Typing a Client name into Search finds that Client's
 * Tasks on a phone exactly as it does on a desktop, even though the column
 * itself is not on screen. Dropping the columns server-side would have
 * quietly broken that.
 *
 * Column order is Task, Engagement, Client, Assignee, Status, Next Action,
 * Scope, Created, Actions. Actions holds a View link that duplicates the
 * Task title beside it, so it goes too; the title link carries the same
 * return_to. Any change to the columns in task_list.php has to be made here
 * as well, which is the cost of doing it positionally — Bootstrap ships no
 * per-column responsive utility and DataTables' Responsive extension is a
 * vendor file this manual-upload deployment does not carry.
 */
@media screen and (max-width: 767px) {
    #taskTable th:nth-child(2),
    #taskTable td:nth-child(2),
    #taskTable th:nth-child(3),
    #taskTable td:nth-child(3),
    #taskTable th:nth-child(4),
    #taskTable td:nth-child(4),
    #taskTable th:nth-child(7),
    #taskTable td:nth-child(7),
    #taskTable th:nth-child(8),
    #taskTable td:nth-child(8),
    #taskTable th:nth-child(9),
    #taskTable td:nth-child(9) {
        display: none;
    }

    /* Three columns fit without scrolling, so the Task title can have the
     * room the other six were taking and wrap instead of being clipped. */
    #taskTable td:nth-child(1) {
        white-space: normal;
        min-width: 0;
    }
}

/* Filter bars: the select + select + Filter + "Add ..." strip in the card
 * directly above most list tables (engagement_list.php, ledger_view.php,
 * client_list.php, payment_client_list.php, action_list.php).
 *
 * These were each written inline as a flex row whose selects carry a fixed
 * `style="max-width: 200px"` and whose trailing button carries `ms-auto`.
 * That is a desktop layout with no mobile answer in it. Two 200px selects
 * plus gaps need more than 400px, and a phone card offers a little over 300,
 * so the row breaks up: one select on its own line with dead space beside
 * it, the next below it, and then Filter and the Add button on a third line
 * shoved to opposite edges by ms-auto. Ragged, and it reads as the top of
 * the table because it sits flush above the table card.
 *
 * One class replaces the four inline utility classes those forms shared, so
 * the desktop row is unchanged (same 0.5rem gap, same centring) and there is
 * a single place to say what happens on a narrow screen: one control per
 * row, each full width, in the order they are read.
 */
.filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

@media screen and (max-width: 767px) {
    .filter-bar {
        flex-direction: column;
        align-items: stretch;
    }

    .filter-bar > * {
        /* !important earns its place three times here, and only here. An
         * inline style="max-width:200px" outranks any selector this file could
         * write, and Bootstrap's .ms-auto is itself margin-left:auto
         * !important, which on a stacked column pins the last button against
         * nothing useful.
         *
         * flex is the third, and it is the subtle one: these controls carry an
         * inline flex-basis (engagement_list.php's client picker asks for
         * `flex: 1 1 200px`) sized for the row this bar is on a wide screen.
         * Turning the bar into a column turns the main axis vertical, so that
         * same 200px stops meaning "200px wide" and starts meaning "200px
         * tall" — a ~48px input sitting in a 200px box, i.e. a slab of blank
         * card between that control and the next one. Stacked, every control
         * is exactly as tall as its content. */
        max-width: none !important;
        margin-left: 0 !important;
        flex: 0 0 auto !important;
        width: 100%;
    }
}

/* Tab strips on small screens (task_list.php's To Do / Needs Attention /
 * Awaiting Review, session_dashboard.php's eight Scheduled Work buckets,
 * settings.php, engagement_edit.php).
 *
 * .nav-tabs is a flex row that wraps by default, and these strips carry
 * enough tabs — eight on Scheduled Work, each with a count badge — that a
 * phone wraps them onto three or four lines. Wrapping is particularly bad
 * for this component specifically: a tab strip draws a continuous bottom
 * border that the active tab notches into, and that border is what tells you
 * which tab is selected. Split it across four stacked rows and the strip
 * stops reading as one control at all.
 *
 * Scrolling sideways keeps the strip a strip. The tabs stay one line, in
 * order, at full size, and the row scrolls to reach the rest, which is the
 * behaviour a tab bar has on a phone everywhere else. The scrollbar is left
 * visible deliberately: on touch it auto-hides anyway, and on a narrow
 * desktop window it is the only hint that more tabs exist.
 */
@media screen and (max-width: 767px) {
    .nav-tabs {
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        -webkit-overflow-scrolling: touch;
    }

    .nav-tabs .nav-item {
        flex: 0 0 auto;
    }

    .nav-tabs .nav-link {
        white-space: nowrap;
    }
}

/* Card headers that carry action buttons (33 of them: team_list.php,
 * compensation_list.php, client_view.php, review_queue.php, dashboard.php
 * and on down).
 *
 * All share one shape, a flex row with the heading at one end and a button
 * group at the other:
 *
 *   <div class="card-header d-flex justify-content-between align-items-center">
 *     <h6>Team Members (4)</h6>
 *     <div class="d-flex gap-2"> ...buttons... </div>
 *
 * Nothing in that row may wrap and nothing may shrink — the buttons
 * themselves carry flex-shrink-0 and text-nowrap so their labels stay
 * intact. On a desktop header that is correct. On a phone it means a header
 * holding "Transfer Ownership" next to "Add Member", or "Generate Period
 * Compensation" next to "Add Agreement", simply runs out past the edge of
 * its own card, because there is no other way for it to resolve.
 *
 * Letting the row wrap gives it one: heading on its own line, buttons
 * beneath, and the button group wrapping again if two long labels still do
 * not fit side by side. The buttons keep their whole labels throughout,
 * which is what flex-shrink-0/text-nowrap were protecting in the first
 * place.
 */
@media screen and (max-width: 767px) {
    .card-header.d-flex {
        flex-wrap: wrap;
        gap: 0.75rem;
    }

    .card-header.d-flex > .d-flex {
        flex-wrap: wrap;
    }
}

/* A flex item will not shrink below its content's intrinsic width, because
 * min-width defaults to auto — which is why .text-truncate silently does
 * nothing inside a flex row and the content overflows instead of ellipsing.
 * Bootstrap 5 ships no utility for this. Needed anywhere a truncating label
 * sits in a flex row; the message list is full of them.
 */
.min-w-0 {
    min-width: 0;
}

/* Message inbox rows (conversation_list.php, and the navbar's message
 * dropdown, both through partials/chat_conversation_row.php).
 *
 * The theme's own .chat-sidebar-single (theme/chat-empty.php) is built for a
 * contact list inside a fixed-width chat sidebar and hard-codes that
 * context. These rows carry more — a title, a context line, a preview and a
 * badge strip — and live both full-width in a card and inside a dropdown, so
 * they get their own class rather than fighting that one's assumptions.
 *
 * Colours come from the theme's neutral variables, which [data-theme=dark]
 * already remaps, so both themes work without a second set of rules.
 */
.chat-conversation-row {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid var(--neutral-100);
    transition: background-color 0.15s ease-in-out;
}

.chat-conversation-row:last-child {
    border-bottom: 0;
}

.chat-conversation-row:hover,
.chat-conversation-row:focus-visible {
    background-color: var(--neutral-50);
    color: inherit;
}

/* An unread row is marked by a left edge rather than a heavier background:
 * the badge and the bolder title already carry the signal, and a filled row
 * competes with the Client-facing / Internal badges that have to stay
 * legible at a glance. */
.chat-conversation-row.is-unread {
    border-left: 3px solid var(--primary-600);
}

.chat-conversation-row:not(.is-unread) {
    border-left: 3px solid transparent;
}

/* Active filter tokens above a filtered list (conversation_list.php,
 * task_list.php).
 *
 * One per facet currently narrowing the list, each removing only itself. They
 * exist because the facet controls collapse behind a Filters button: a
 * collapsed filter that leaves no trace is an invisible filter, and a reader
 * then has a short list with no way to see why. Deliberately quiet — they
 * report state, they are not a call to action.
 *
 * Named .chat-filter-token when the inbox was the only list that collapsed its
 * filters. Nothing about it was ever chat-specific, and task_list.php now
 * needs the identical component for the identical reason. */
.filter-token {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.625rem;
    border: 1px solid var(--input-form-light);
    border-radius: 999px;
    background-color: var(--white, #fff);
    color: var(--text-secondary-light);
    font-size: 0.75rem;
    text-decoration: none;
    max-width: 100%;
}

.filter-token > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.filter-token:hover,
.filter-token:focus-visible {
    border-color: var(--danger-main);
    color: var(--danger-main);
}

/* ---------------------------------------------------------------------------
 * Conversation thread (conversation_view.php).
 *
 * Built on the theme's .chat-single-message bubbles, which already supply the
 * left/right fills and tails. Everything below is what a work thread needs
 * that a demo chat page did not have to answer: sender grouping, day
 * dividers, reply quoting, per-message actions, and telling a Client-facing
 * thread from an internal one at a glance.
 * ------------------------------------------------------------------------ */

.chat-thread-header {
    padding: 1rem 1.5rem;
    border-bottom: 1px solid var(--input-form-light);
}

/* The theme reserves the sidebar's width off .chat-main from 768px up
 * (`width: calc(100% - 324px)`), but the conversation list is hidden below
 * 992px here — a 300px list plus a thread is too cramped to be useful on a
 * tablet. Without this the thread would stop 324px short of the right edge
 * between those two breakpoints, with nothing in the gap. Its stacked
 * margin-top goes too, since there is no longer a sidebar above it. */
@media screen and (max-width: 991.98px) {
    .chat-wrapper .chat-main {
        width: 100%;
        margin-top: 0;
    }
}

/* A Client thread carried a blue edge down its full height here. Removed:
 * three other signals already say the same thing, and two of them are in
 * better places — the "Client-facing" badge in the thread header, the Client
 * badge on each of their messages, and the "Client-facing message. The Client
 * will receive this message." notice sitting directly above the composer,
 * which is where the mistake would actually be made. The edge was a fourth
 * copy of that warning attached to the least useful part of the layout. */

/* Spacing between messages carries the grouping. The theme's own
 * `.chat-single-message + .chat-single-message { margin-top: 2rem }` no
 * longer applies, since each message is wrapped to carry its action row. */
.chat-msg + .chat-msg {
    margin-top: 1.5rem;
}

.chat-msg.is-grouped {
    margin-top: 0.25rem;
}

/* Hidden, not removed: the bubble's 56px avatar gutter has to stay reserved
 * or every grouped message would shift left and break the column. */
.chat-msg.is-grouped .chat-msg-avatar {
    visibility: hidden;
}

.chat-msg-avatar {
    flex-shrink: 0;
}

.chat-msg-sender {
    font-size: 0.75rem;
    font-weight: 600;
    opacity: 0.75;
}

/* Quoted stub above a reply. Colour is inherited rather than set, so it
 * works on both the neutral left fill and the primary right fill without a
 * second rule; the tinted background is an alpha of whatever sits behind it
 * for the same reason. */
.chat-msg-quote {
    border-inline-start: 2px solid currentColor;
    padding: 0.25rem 0.5rem;
    margin-bottom: 0.5rem;
    font-size: 0.8125rem;
    opacity: 0.85;
    background-color: rgba(0, 0, 0, 0.06);
    border-radius: 4px;
}

.chat-single-message.right .chat-msg-quote {
    background-color: rgba(255, 255, 255, 0.15);
}

/* The theme paints every <p> inside a .right bubble white, so an attachment
 * link there needs to be white too — the default primary-600 link would be
 * unreadable on the primary-600 fill. */
.chat-msg-attachment a {
    color: var(--primary-600);
    text-decoration: none;
}

.chat-single-message.right .chat-msg-attachment a {
    color: #fff;
    text-decoration: underline;
}

.chat-msg-attachment a:hover {
    text-decoration: underline;
}

/* Day divider: a centred label with a rule running through it. */
.chat-day-divider {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 1.5rem 0 1rem;
    color: var(--text-secondary-light);
    font-size: 0.75rem;
}

.chat-day-divider::before,
.chat-day-divider::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background-color: var(--input-form-light);
}

/* Per-message actions live outside the bubble, because inside a .right
 * bubble every <p> is painted white and an outline button on the primary
 * fill is unreadable. Kept quiet so a thread does not read as a wall of
 * buttons, but always rendered rather than hover-only: Forward Internally
 * and Use in Client Reply are core workflows (§18-24), and a hover-only
 * control does not exist at all on a touch screen. */
.chat-msg-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 0.25rem;
}

.chat-msg.own .chat-msg-actions {
    justify-content: flex-end;
}

.chat-msg.other .chat-msg-actions {
    padding-inline-start: 56px;
}

.chat-msg-action {
    background: none;
    border: 0;
    padding: 0;
    font-size: 0.75rem;
    color: var(--text-secondary-light);
    text-decoration: none;
}

.chat-msg-action:hover,
.chat-msg-action:focus-visible {
    color: var(--primary-600);
    text-decoration: underline;
}

.chat-msg-action.is-danger:hover,
.chat-msg-action.is-danger:focus-visible {
    color: var(--danger-main);
}

/* Composer */
.chat-compose {
    padding: 1rem 1.5rem 1.5rem;
    border-top: 1px solid var(--input-form-light);
}

.chat-scope-note {
    padding: 0.625rem 0.875rem;
    border-radius: 8px;
    margin-bottom: 0.75rem;
    font-size: 0.8125rem;
}

.chat-scope-note.is-client {
    background-color: var(--info-focus, rgba(13, 110, 253, 0.08));
    border: 1px solid var(--info-main);
}

.chat-scope-note.is-internal {
    background-color: var(--neutral-50);
    border: 1px solid var(--input-form-light);
}

.chat-reply-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.5rem 0.75rem;
    margin-bottom: 0.5rem;
    border-inline-start: 3px solid var(--primary-600);
    background-color: var(--neutral-50);
    border-radius: 6px;
    font-size: 0.8125rem;
}

/* The thread pane is a flex column whose message list grows; on a phone the
 * theme's 511px cap plus a composer overflows the viewport, so the list is
 * given a viewport-relative ceiling instead. */
@media screen and (max-width: 767px) {
    .chat-message-list {
        max-height: 55vh;
        min-height: 200px;
        padding: 1rem;
    }

    .chat-thread-header,
    .chat-compose {
        padding-inline: 1rem;
    }

    .chat-msg.other .chat-msg-actions {
        padding-inline-start: 0;
    }
}

/* ---------------------------------------------------------------------------
 * Mentions (conversation_view.php).
 *
 * §32 says only users authorized for the underlying resource are mentionable.
 * That was previously enforced but never shown: the compose box said "use
 * @Name to mention someone" and the suggestion list appeared only after you
 * had typed "@" and guessed a first letter, so who you were allowed to reach
 * was discoverable by trial and error. The roster is now simply on screen,
 * with each person's role on this piece of work, and each chip inserts the
 * mention when clicked.
 * ------------------------------------------------------------------------ */

.chat-participants {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.375rem;
}

.chat-participants-label {
    font-size: 0.75rem;
    color: var(--text-secondary-light);
    margin-inline-end: 0.25rem;
}

/* A button, not a chip-shaped div: it inserts text, so it has to be reachable
 * by keyboard and announced as something that does.
 *
 * The chips also carry `.mention-chip`, which deliberately has no rule here —
 * it is the behaviour hook conversation_view.php's querySelectorAll uses, kept
 * separate from the class that styles them so restyling a chip cannot
 * accidentally unbind it. */
.chat-participant {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.1875rem 0.5rem 0.1875rem 0.1875rem;
    border: 1px solid var(--input-form-light);
    border-radius: 999px;
    background: none;
    font-size: 0.75rem;
    line-height: 1.4;
    color: inherit;
}

.chat-participant:hover,
.chat-participant:focus-visible {
    border-color: var(--primary-600);
    background-color: var(--neutral-50);
}

.chat-participant-name {
    font-weight: 600;
}

/* The role is the answer to "why is this person mentionable", so it is always
 * present but never competes with the name. */
.chat-participant-role {
    color: var(--text-secondary-light);
    font-size: 0.6875rem;
}

/* The suggestion menu sits above the composer rather than below it. Dropped
 * downward it opened off the bottom of the viewport on a phone, where the
 * compose box is already at the bottom of the screen. */
.chat-mention-menu {
    bottom: 100%;
    left: 0;
    right: 0;
    z-index: 20;
    margin-bottom: 0.25rem;
    max-height: 15rem;
    overflow-y: auto;
    box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.08);
}

.chat-mention-menu .mention-option {
    font-size: 0.8125rem;
    padding: 0.5rem 0.75rem;
}

/* Keyboard selection has to look selected. Bootstrap's .active on a
 * list-group-item already paints the primary fill; this only makes sure the
 * role text stays legible on it. */
.chat-mention-menu .mention-option.active .chat-participant-role {
    color: rgba(255, 255, 255, 0.75);
}

/* ------------------------------------------------------------------
 * Reference codes (helpers: reference_codes.php's reference_code_badge()).
 *
 * Every quotable record carries one — CL22344255, EN02939485, TA92837323.
 * See Markdown_bikabo_reference_codes.md.
 *
 * A code is supporting information, never a headline: it sits under the
 * record's title, small and muted, and must not compete with the name for
 * attention. What it MUST do is be readable character by character and be
 * one click from the clipboard, because it gets quoted into emails, search
 * boxes and support conversations far more often than it is read.
 *
 * inline-flex for the same reason .hint-label above is: the copy button is
 * an <iconify-icon> inside a <button>, and a web component with no theme
 * styling drops below the text baseline if left to inline layout. THIS IS
 * THE PATTERN TO REUSE for any icon sitting beside text.
 * ------------------------------------------------------------------ */
.reference-code {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-family: var(--bs-font-monospace, ui-monospace, SFMono-Regular, Menlo, monospace);
    /* Slightly wider tracking: a monospace font alone still renders 8 and B
     * close enough to mistake when a code is being read aloud. */
    letter-spacing: 0.03em;
    font-size: 0.8125rem;
    color: var(--text-secondary-light, #7c8091);
    line-height: 1.2;
}

/* Selecting the code must not also select the button's icon glyph, so a
 * double-click-and-copy picks up exactly the code and nothing else. */
.reference-code-value {
    user-select: all;
}

.reference-code-copy {
    display: inline-flex;
    align-items: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    line-height: 1;
    cursor: pointer;
    opacity: 0.55;
    transition: opacity 0.15s ease, color 0.15s ease;
}

.reference-code-copy:hover,
.reference-code-copy:focus-visible {
    opacity: 1;
    color: var(--primary-600);
}

/* Confirmation is the tick itself, swapped in by the script in
 * partials/scripts.php. Colour reinforces it for anyone who reads state
 * from colour faster than from shape. */
.reference-code-copy.is-copied {
    opacity: 1;
    color: var(--success-main, #16a34a);
}

/* A row that predates the backfill, showing "#418" instead of a code. A
 * NULL reference_code is a bug; rendering nothing would hide it, so this
 * makes it visibly a different thing without shouting on a page the user
 * cannot do anything about. */
.reference-code-missing {
    font-style: italic;
    opacity: 0.7;
}

/* A reference code inside a card whose whole surface is a Bootstrap
 * .stretched-link (the portal Engagement cards, the dashboard stat cards).
 *
 * .stretched-link paints an absolutely-positioned overlay across the entire
 * card so any click on it navigates. That overlay sits ABOVE the card's own
 * contents, so a copy button underneath never receives the click at all —
 * stopPropagation() in the handler cannot help, because the handler never
 * runs. Lifting the chip into its own stacking context puts it back on top,
 * which is the same fix .dashboard-stat-link .hint-label above already uses
 * for the same reason.
 *
 * Applied to every .reference-code rather than scoped to those cards: it is
 * inert everywhere there is no overlay, and scoping it would mean this
 * breaking silently the next time a code is put inside a linked card. */
.reference-code {
    position: relative;
    z-index: 2;
}

/* ---------------------------------------------------------------------
 * Onboarding checklist (partials/onboarding_checklist.php)
 *
 * Cosmetics only, per this file's standing rule: the card is built entirely
 * out of Bootstrap and theme utilities in the markup, so a deploy that
 * uploads the PHP before this stylesheet renders a correct, usable, slightly
 * plainer checklist rather than a broken one.
 *
 * <details>/<summary> is doing the expanding, chosen because it needs no
 * JavaScript — this card is the first thing a brand new tenant ever sees, and
 * a script that has not finished loading must not be what stands between them
 * and their first instruction. What that costs is the browser's default
 * disclosure triangle, which lands beside a flex row of icons and counters
 * and reads as debris. These rules replace it with the chevron the markup
 * already carries.
 * --------------------------------------------------------------------- */

/* Both selectors are needed and neither is redundant: `list-style: none` is
 * how Firefox and modern Chrome drop the marker, while Safari and older
 * WebKit only respond to the pseudo-element. */
.onboarding-stage > summary {
    list-style: none;
    cursor: pointer;
    user-select: none;
}
.onboarding-stage > summary::-webkit-details-marker {
    display: none;
}

/* An affordance the row would otherwise lack. A <summary> looks like a
 * heading until it is hovered, and nothing else on the card suggests the
 * whole strip is clickable. */
.onboarding-stage > summary:hover {
    background-color: var(--neutral-50, #f8f9fa);
}

.onboarding-stage-chevron {
    transition: transform 0.15s ease-in-out;
}
.onboarding-stage[open] > summary .onboarding-stage-chevron {
    transform: rotate(180deg);
}

/* Motion is the entire mechanism here, so reduced-motion turns it off rather
 * than shortening it — the same call the dashboard greeting animation makes.
 * The chevron still points the right way, it just arrives there instantly. */
@media (prefers-reduced-motion: reduce) {
    .onboarding-stage-chevron {
        transition: none;
    }
}

/* ---------------------------------------------------------------------------
 * Client Portal
 * (Markdown_bikabo_clientportal_redesign.md §6, §13, §14)
 *
 * Short, and deliberately shorter than it has been. Two earlier versions of
 * this block defined .portal-chip, .portal-progress, .portal-raised,
 * .portal-header and .portal-navrow — a parallel design system beside a
 * complete one, and then a set of alignment patches for a bespoke top bar.
 * All of it is gone. The portal now renders the theme's own dashboard shell
 * (.sidebar + .dashboard-main + .navbar-header + .dashboard-main-body), which
 * style.css already positions, and the theme's own badges, progress bars,
 * cards and gradients, which style.css already draws.
 *
 * What is left is the handful of things the theme has no answer for, because
 * the staff app it was built for has no equivalent: a tenant's own logo in a
 * slot designed for Bikabo's, and a list row that is itself a link.
 *
 * Everything here is cosmetic. The portal's layout and navigation are carried
 * by style.css and bootstrap.min.css, so a deploy landing PHP ahead of this
 * file renders a plainer portal, never a broken one — and the sidebar keeps
 * working, because app.js and the theme own it.
 *
 * Light only, deliberately: partials/portal_layout_top.php hardcodes
 * data-theme="light" and the portal has no theme switch, so a dark block
 * would be dead code pretending to be support.
 * ------------------------------------------------------------------------ */

/* The tenant's logo in .sidebar-logo, a slot the theme sizes for Bikabo's own
 * wordmark. Capped rather than sized so any aspect ratio a tenant uploaded
 * stays undistorted — a wide wordmark and a square badge both have to sit in
 * the same box. */
.portal-sidebar-logo {
    max-height: 40px;
    max-width: 100%;
    width: auto;
    object-fit: contain;
}

/* The text fallback for a tenant with no uploaded logo (§12.1). Wraps rather
 * than truncates: a business name is the one thing on this screen a Client
 * must be able to read in full, and "Good Content Writers L…" reads as a
 * broken page. */
.portal-sidebar-wordmark {
    display: block;
    line-height: 1.25;
    word-break: break-word;
}

/* Dense list rows.
 *
 * The theme's .list-group-item covers a row that contains links; this covers
 * the row that IS one, which .list-group-item does not style and which the
 * portal uses wherever a whole row navigates somewhere. */
.portal-row {
    display: block;
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid var(--neutral-200);
    transition: background-color 0.12s ease;
}

.portal-row:last-child {
    border-bottom: 0;
}

/* :focus-visible as well as :hover — the row is an <a>, and a keyboard user
 * gets no hover. Without this the only focus indicator on a full-width row is
 * the browser default outline, which a row-height element swallows. */
.portal-row:hover,
.portal-row:focus-visible {
    background-color: var(--neutral-50);
    color: inherit;
}

/* A card hosting .portal-row children has to clip them: the rows paint their
 * hover background edge to edge and the card has a border radius, so without
 * this the first and last rows paint square corners over rounded ones.
 * :has() rather than a class on every such card, so a page adopting
 * .portal-row later gets the fix without knowing this rule exists. Where
 * :has() is unsupported the declaration drops and one hover state has square
 * corners. */
.card:has(> .portal-row) {
    overflow: hidden;
}

/* Compact empty states. Eleven of these were py-32/py-40/py-48, so a new
 * Client's home page was mostly padding around sentences explaining that
 * there was nothing there. An empty state is a caption, not a feature. */
.portal-empty {
    padding: 16px;
    color: var(--neutral-500);
    font-size: 0.875rem;
}

/* Two-line clamp for Engagement descriptions on the card grid. Without it one
 * Engagement with a long brief makes its card several times the height of its
 * neighbours and tears a hole in the row. Falls back to showing the whole
 * description where line-clamp is unsupported, which is untidy but hides
 * nothing. */
.portal-clamp-2 {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* The portal thread reuses .chat-single-message but is not inside the theme's
 * .chat-wrapper, which is where the bubble's own max-width is set. Without a
 * cap here, a one-word reply on a wide screen still draws a bubble the full
 * width of the card. */
.chat-message-list .chat-msg .chat-message-content {
    max-width: 100%;
}

@media screen and (min-width: 768px) {
    .chat-message-list .chat-msg .chat-message-content {
        max-width: 75%;
    }
}

/* The account dropdown needs a floor on its width.
 *
 * Bootstrap gives .dropdown-menu a 10rem min-width and otherwise sizes it to
 * its content, and the widest thing in this one is a form-switch label —
 * "Sound for new messages" — sitting in the space left over beside a 2rem
 * toggle. At 10rem that label broke onto three lines, which made a menu of
 * four short items look like a paragraph.
 *
 * A min-width rather than a fixed width, so a long business name on the line
 * above can still push it wider, and max-width so it cannot grow past what a
 * small phone can show. */
.portal-account-menu {
    min-width: 17rem;
    max-width: calc(100vw - 2rem);
}

/* ---------------------------------------------------------------------------
 * Platform console (platform_*.php, partials/platform_*.php)
 *
 * Two rules. The console renders the theme's dashboard shell now — the same
 * .sidebar + .dashboard-main + .navbar-header the staff app and the Client
 * Portal use — so everything that used to live here is gone: a bespoke
 * two-column flex layout, a hand-built <aside> carrying inline width and
 * position, a dark top bar carrying inline height/padding/sticky/z-index, and
 * a .platform-nav-link hover fix for a nav-pills menu that no longer exists.
 * The theme styles all of it.
 * ------------------------------------------------------------------------ */

/* The brand block is two stacked lines plus a badge rather than the single
 * wordmark .sidebar-logo is padded for, and it must not clip when the theme
 * collapses the sidebar's width at its own breakpoints. */
.sidebar-logo .line-height-1 {
    line-height: 1.1;
}

/* The account dropdown needs a floor on its width, same as the Client
 * Portal's: Bootstrap sizes .dropdown-menu to its content above a 10rem
 * min-width, and "Back to My Tenant" beside a 20px icon wraps at that width.
 * A min-width rather than a fixed one, so a long name on the line above can
 * still push it wider; max-width so it cannot overflow a small phone. */
.platform-account-menu {
    min-width: 16rem;
    max-width: calc(100vw - 2rem);
}

/* ---------------------------------------------------------------------------
 * Tenant application UI pass
 * (Markdown_bikabo_staff_ui_pass.md, phase 2)
 *
 * Additive. Nothing adopts any of this until phase 3, so a deploy that lands
 * this file early changes nothing visible.
 *
 * The rule this block follows is the one the Client Portal pass arrived at the
 * hard way: do not add a class for anything style.css already ships. What is
 * here is the handful of things it does not — a money figure that must not
 * wrap, a density scale for a screen people sit in front of all day, and the
 * retirement of a colour family the theme ships but this product must not use.
 * ------------------------------------------------------------------------ */

/* A money figure, anywhere.
 *
 * Promoted out of dashboard.php, which solved this for its own tiles and
 * nowhere else: .dashboard-stat-value and .dashboard-hero-value are nowrap
 * with a clamp()ed size, and every other page went on rendering amounts in a
 * plain h5 that breaks between the currency and the number. "USD" on one line
 * and "1,100.00" on the next is what engagement_view.php shows today.
 *
 * The clamp is a width budget rather than a taste call, and it is the same one
 * .dashboard-stat-value already proved: the vw term means a wider viewport
 * gives both a wider column and a larger figure, roughly in step, so the ratio
 * of text width to available width holds instead of colliding at one
 * particular breakpoint.
 *
 * No !important. Unlike the dashboard's h4-based tiles this is applied to a
 * <p>, which the theme does not size with !important, so specificity alone
 * carries it. */
.money-value {
    font-size: clamp(1.05rem, 0.85rem + 0.6vw, 1.5rem);
    line-height: 1.25;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* The record title in partials/record_header.php. Long Engagement and Task
 * names are the norm rather than the exception — they carry a subject and a
 * code — so the title wraps rather than truncating, but balances its lines so
 * a three-word overflow does not leave one word alone on the second row. */
.record-title {
    text-wrap: balance;
    overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------------------
 * Density (§9.4)
 *
 * A step tighter than the Client Portal's, because this application is used
 * all day by people who know it, where the portal is visited occasionally by
 * people who do not. Applied through a wrapper class rather than by editing
 * the theme's own table and list rules, so nothing outside the tenant app —
 * the portal, the platform console — is affected by it.
 * ------------------------------------------------------------------------ */

/* Table rows across the tenant app's list pages. The theme's default cell
 * padding is tuned for a marketing demo with six rows; a real Task list has
 * eighty, and the difference between 16px and 8px of vertical padding is
 * roughly fifteen more rows on a laptop screen. */
.app-dense .table > :not(caption) > * > * {
    padding-top: 8px;
    padding-bottom: 8px;
}

.app-dense .table {
    font-size: 0.875rem;
}

/* DataTables' own controls, which ship unstyled and match nothing else on the
 * page — a bare select and a bare text input labelled "Search:". These bring
 * them onto the theme's form controls without touching the library. */
.app-dense .dataTables_length select,
.app-dense .dataTables_filter input {
    border: 1px solid var(--neutral-200);
    border-radius: 8px;
    padding: 6px 12px;
    background-color: var(--neutral-50);
    font-size: 0.875rem;
}

.app-dense .dataTables_length,
.app-dense .dataTables_filter,
.app-dense .dataTables_info {
    font-size: 0.875rem;
    color: var(--text-secondary-light);
}

/* ---------------------------------------------------------------------------
 * The overflow menu (§9.3)
 *
 * Archive, Block, Void, End Early, Revoke. Items arrive as pre-rendered forms
 * carrying their own confirm dialogs, so they are block-level buttons rather
 * than dropdown-items and need the menu to give them room.
 * ------------------------------------------------------------------------ */
.overflow-action-menu {
    min-width: 13rem;
}

/* The forms inside carry .btn classes of their own (usually outline-danger),
 * so they only need to fill the row and lose the inline-block gap. */
.overflow-action-menu form,
.overflow-action-menu .btn,
.overflow-action-menu > li > a {
    display: block;
    width: 100%;
    text-align: start;
}

/* ---------------------------------------------------------------------------
 * Retiring the blue (§2)
 *
 * The theme ships a full `info-*` family and a `cyan`, both blue, and
 * `--primary-100` is `#BFDCFF` — blue — left over from the vendor theme where
 * primary WAS blue. This product's primary is `#005949`, deep green, so
 * `bg-primary-100` paints pale blue on a green-branded page and reads as
 * deliberate.
 *
 * The markup is being changed page by page in later phases to stop asking for
 * these at all. This block is the safety net for the ones missed, and it is
 * deliberately NOT a blanket override of the theme's variables: doing that
 * would also repaint the vendor demo pages under theme/, which are not ours to
 * restyle, and would hide the remaining call sites from the audit instead of
 * surfacing them.
 *
 * `--primary-100` is the exception and IS redefined, because nothing should
 * ever want its current value: a token named primary that renders a different
 * hue from primary-600 is a bug in the palette, not a design choice.
 *
 * Safe to redefine, checked rather than assumed. All 29 of style.css's uses
 * are its own utility classes — .bg-primary-100, .text-primary-100,
 * .btn-primary-100, and the hover/group-hover variants — which only apply
 * where markup asks for them, and no page in this application asks for any of
 * them today. So this changes nothing on screen now and makes the next use of
 * it render brand green instead of blue. The vendor demo pages under theme/
 * never load this stylesheet, so they keep the palette they shipped with.
 *
 * The value is a pale tint of #005949 at roughly the lightness the theme's
 * other -100 tokens sit at (success-100 #DCFCE7, danger-100 #FEE2E2), so it
 * reads as a member of that family rather than as a mid-tone.
 * ------------------------------------------------------------------------ */
:root {
    --primary-100: #dceae7;
}

/* ---------------------------------------------------------------------------
 * Form pages (Markdown_bikabo_staff_ui_pass.md §6.C, phase 7)
 * ------------------------------------------------------------------------ */

/* The action row, pinned to the bottom of the viewport.
 *
 * On task_add.php the submit button sits about 1,400px down the page: the
 * reader finishes the field they care about and then scrolls past every field
 * they left alone to reach Create. Sticky keeps it one click away at all
 * times and — because it never moves — keeps Cancel from sliding under a
 * cursor aimed at Save.
 *
 * `bottom: -1px` rather than `0` so the bar's own border does not leave a
 * hairline of page showing beneath it on fractional device pixel ratios.
 *
 * The bar is a direct child of <form>, never inside a .card, because a sticky
 * child cannot escape an ancestor that scrolls or clips. */
.form-actions {
    position: sticky;
    bottom: -1px;
    z-index: 2;
    padding-inline: 4px;
    /* The card edges it sits under are 8px-rounded; matching the top corners
     * stops the bar reading as a separate slab pasted over the form. */
    border-start-start-radius: 8px;
    border-start-end-radius: 8px;
}

/* On a short viewport a pinned bar is a tax rather than a convenience: it
 * takes a fixed ~76px out of a screen that has little to give, for the whole
 * length of the form. Below that height it rides with the page. */
@media (max-height: 560px) {
    .form-actions {
        position: static;
    }
}

/* The rail beside a form.
 *
 * Sticky for the mirror-image reason the action bar is: the rail is three or
 * four lines tall and the form is thousands of pixels, so left in flow it
 * would scroll out of sight within the first screen and be gone for the rest
 * of the form — which is precisely when "what am I editing" gets asked.
 *
 * The offset clears the theme's 4.5rem .navbar-header, which is itself sticky;
 * without it the rail slides under the header rather than stopping below it.
 * Only from lg, since below that the rail is stacked under the form and has
 * nothing to stick to. */
@media (min-width: 992px) {
    .form-aside {
        position: sticky;
        top: calc(4.5rem + 16px);
        /* A sticky element taller than the space below it can never show its
         * own bottom: it pins at `top` and everything past the viewport edge
         * becomes unreachable. Today's rails are two short cards, so this
         * never engages — it is here so that adding a third card degrades to
         * a rail that scrolls inside itself rather than one with content
         * nobody can read. `scroll-sm` is the theme's own thin scrollbar. */
        max-height: calc(100vh - 4.5rem - 32px);
        overflow-y: auto;
    }
}

/* ---------------------------------------------------------------------------
 * The blue that no grep could find
 * (Markdown_bikabo_staff_ui_pass.md §7, phase 7)
 *
 * Every blue fixed in phases 1 to 6 was a class written into markup, so it was
 * findable by searching for `info-` or `cyan`. These are not. They are
 * *defaults* — a Bootstrap variable and a vendor palette token — so nothing in
 * this application asks for them and they were painting the product blue
 * anyway, on controls that appear on almost every page.
 * ------------------------------------------------------------------------ */

:root {
    /* 1. `--primary-700: #486CEA`.
     *
     * The theme ships primary-600 as this product's `#005949` deep green and
     * primary-700 as a blue left over from the vendor palette, which is the
     * same fault `--primary-100` had. It is not decorative: style.css uses it
     * 33 times, and every one of them is a hover or active state —
     * `.btn-primary:hover`, `.hover-text-primary`, `.text-primary-600` links
     * on hover. So a brand-green button flashed blue under the cursor,
     * everywhere in the app, and no page contained the word.
     *
     * The replacement is #005949 stepped darker, which is what a -700 beside
     * a -600 is supposed to be. */
    --primary-700: #00463a;

    /* 2. Bootstrap's own link blue.
     *
     * `--bs-link-color: #0d6efd`, and `.nav` sets
     * `--bs-nav-link-color: var(--bs-link-color)`. So every inactive
     * `.nav-link` in the product renders in Bootstrap blue: the Work / Money /
     * History tabs on a Task, the bucket strip on Scheduled Work, the report
     * switcher. The theme styles `.button-tab .nav-link.active` and nothing
     * else, which is why only the selected pill was ever on-brand.
     *
     * Retargeting the variable rather than the components fixes the tab
     * strips, bare `<a>` elements and anything added later in one place. */
    --bs-link-color: #005949;
    --bs-link-color-rgb: 0, 89, 73;
    --bs-link-hover-color: #00463a;
    --bs-link-hover-color-rgb: 0, 70, 58;

    /* 3. The focus ring. `--bs-focus-ring-color` is a blue wash, and it is
     * what draws the halo around a selected tab pill. */
    --bs-focus-ring-color: rgba(0, 89, 73, 0.25);

    /* 4. `.nav-pills` active background, for any pill group that is not
     * `.button-tab` and so never got the theme's own active rule. */
    --bs-nav-pills-link-active-bg: #005949;
}

/* `.nav-link:focus-visible` hard-codes `rgba(13,110,253,.25)` rather than
 * reading --bs-focus-ring-color, so the variable above cannot reach it. */
.nav-link:focus-visible,
.btn:focus-visible {
    box-shadow: 0 0 0 0.25rem var(--bs-focus-ring-color);
}

/* An inactive tab in a `.button-tab` strip is a control, not a link.
 *
 * Even in brand green, six green words beside one green pill is a row that
 * reads as seven equal things. Secondary text with the brand on hover makes
 * the selected pill the only coloured item in the strip, which is the whole
 * job of a tab bar. */
.button-tab .nav-link {
    color: var(--text-secondary-light);
    font-weight: 500;
}

.button-tab .nav-link:hover,
.button-tab .nav-link:focus-visible {
    color: var(--primary-600);
    background-color: var(--primary-50);
}

.button-tab .nav-link.active:hover {
    color: #fff !important;
}

/* ---------------------------------------------------------------------------
 * Closing the §7 audit: the `info` family itself
 *
 * Phase 2 deliberately did NOT redefine these, and said why: a blanket
 * override "would hide the remaining call sites from the audit instead of
 * surfacing them". That was right while the audit was running. It has now run
 * — phases 3 to 6 went through every `info-` in markup and reassigned it by
 * meaning — and what is left is a tail of roughly twenty status badges, five
 * alerts and two buttons where `info` is being used correctly for its stated
 * meaning: a state that is confirmed but not yet settled.
 *
 * An invoice that is issued or sent. A payout that is ready. An advance that
 * is approved. A subscription that is trialing. A session that is upcoming.
 * Every one of them is committed to and none of them has landed, which is
 * exactly what §7 assigns to lilac. So rather than editing twenty-seven call
 * sites to say `lilac` where they already say the right thing, the family is
 * pointed at lilac here, once.
 *
 * This also catches Bootstrap's `.alert-info`, `.btn-info` and
 * `.bg-info-subtle`, which are built from these tokens and which no search for
 * a Bikabo class would ever have turned up.
 *
 * Only this application is affected: the vendor demo pages under theme/ never
 * load this stylesheet, so they keep the palette they shipped with.
 * ------------------------------------------------------------------------ */

:root {
    /* Lilac, stepped to match what each -N slot is used for. --lilac-100
     * (#EBD7FF) and --lilac-600 (#8252E9) are the theme's own two, and the
     * rest are interpolated between them so gradients and hover states move
     * smoothly instead of jumping. */
    --info-50:  #F6EFFF;
    --info-100: #EBD7FF;
    --info-200: #DCC0FB;
    --info-300: #C7A2F6;
    --info-400: #AC7CF1;
    --info-500: #9765EC;
    --info-600: #8252E9;
    --info-700: #6C3ED2;
    --info-800: #5730AC;
    --info-focus: rgba(130, 82, 233, 0.15);
    --info-main: #6C3ED2;

    /* `--cyan: #00B8F2` is the other blue in the palette, used by the theme's
     * `.bg-cyan` / `.text-cyan-600` utilities. Nothing in this application
     * asks for it today; this is here so that the next thing that does gets a
     * colour from the product's own range. */
    --cyan: #14B8A6;

    /* Bootstrap's OWN info set, which is a separate family from the theme's
     * and had to be checked rather than assumed. `.text-info-main`,
     * `.bg-info-50`, `.text-info-600` and `.btn-info` are the theme's and read
     * `--info-*` above; `.bg-info-subtle`, `.alert-info` and
     * `.text-info-emphasis` are Bootstrap's and read these. Retargeting only
     * the first set would have produced badges with a cyan background and
     * lilac text — worse than leaving them alone.
     *
     * `.alert-info` is the one that matters most: fifteen alerts across the
     * app use it, and render_flash() falls back to it for any flash type it
     * does not recognise, so it is the default appearance of a system message. */
    --bs-info: #8252E9;
    --bs-info-rgb: 130, 82, 233;
    --bs-info-bg-subtle: #F1E4FF;
    --bs-info-border-subtle: #DCC0FB;
    --bs-info-text-emphasis: #5730AC;
}

/* ---------------------------------------------------------------------------
 * The soft gradient set
 *
 * The theme's eleven `bg-gradient-*` tints are what gives its own dashboards
 * (index-9, index-13, index-18) their look: a row of tiles where each card
 * carries a different pale wash. This application uses exactly one of them —
 * `bg-gradient-start-4`, the green — on every hero, every tile and every
 * summary strip, because four of the eleven are blue and §7 ruled blue out, so
 * the safe one got used for everything and the row lost its variety.
 *
 * Retargeting the four blue ones is the better answer than avoiding them. All
 * eleven then sit inside the product's range and a row of tiles can be four
 * different things again.
 *
 *   start-1  #E6F9FF cyan   -> mint-teal
 *   start-3  #E6EBFF blue   -> sand (nothing else in the set is warm-neutral)
 *   end-1    #EFF4FF blue   -> pale mint
 *   end-6    #EEFBFF cyan   -> lilac, matching --info-*'s new home
 *
 * Each keeps its second stop, so the direction and softness of the original
 * gradient are unchanged; only the hue moves. The theme also defines a
 * dark-mode variant of each at low alpha, redefined here to match.
 * ------------------------------------------------------------------------ */

.bg-gradient-start-1 { background: linear-gradient(to right, #E6FBF7, #FEFFFF); }
.bg-gradient-start-3 { background: linear-gradient(to right, #FFF6E6, #FFFFFF); }
.bg-gradient-end-1   { background: linear-gradient(to right, #FFFFFF, #EEF9F5); }
.bg-gradient-end-6   { background: linear-gradient(to right, #FFFFFF, #F4F0FF); }

[data-theme="dark"] .bg-gradient-start-1 { background: linear-gradient(to right, rgba(230, 251, 247, 0.15), rgba(254, 255, 255, 0.1)); }
[data-theme="dark"] .bg-gradient-start-3 { background: linear-gradient(to right, rgba(255, 246, 230, 0.15), rgba(255, 255, 255, 0.1)); }
[data-theme="dark"] .bg-gradient-end-1   { background: linear-gradient(to right, rgba(255, 255, 255, 0.075), rgba(238, 249, 245, 0.125)); }
[data-theme="dark"] .bg-gradient-end-6   { background: linear-gradient(to right, rgba(255, 255, 255, 0.075), rgba(244, 240, 255, 0.125)); }

/* Business / My Work pane switch (dashboard.php's $showMyWork block).
 *
 * Two btn-sm pills differing only in background colour read as decoration
 * rather than as a control: the inactive one looked like a disabled button,
 * and nothing about the pair said "these swap the page". A segmented control
 * says it structurally — one track, two halves, a thumb that is visibly
 * somewhere rather than nowhere — and the thumb sliding between them is what
 * makes the swap legible as a swap rather than as a redraw.
 *
 * The thumb is one element that moves, not a class toggled on each half. A
 * toggled class cannot animate between two elements, and the movement is the
 * whole point: it shows the two panes are the same surface seen twice.
 *
 * Geometry, since the arithmetic is load-bearing. Percentages on an absolutely
 * positioned child resolve against the padding box, so with 0.25rem of padding
 * each side the content box is (100% - 0.5rem) and one segment is half of
 * that: calc(50% - 0.25rem). The thumb is given exactly that width, which is
 * what makes translateX(100%) — its own width — land it precisely on the
 * second segment. The buttons are flex:1 1 0 so they are that same width
 * whatever their labels weigh, which also stops the badge on one side
 * widening its half and desynchronising the thumb.
 */
.dash-switch {
    position: relative;
    display: inline-flex;
    align-items: stretch;
    max-width: 100%;
    padding: 0.25rem;
    border: 1px solid var(--neutral-200);
    border-radius: 0.75rem;
    background-color: var(--neutral-100);
}

.dash-switch-thumb {
    position: absolute;
    top: 0.25rem;
    left: 0.25rem;
    bottom: 0.25rem;
    width: calc(50% - 0.25rem);
    border-radius: 0.5rem;
    background-color: var(--base);
    box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.1);
    transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
    pointer-events: none;
}

.dash-switch[data-active="mywork"] .dash-switch-thumb {
    transform: translateX(100%);
}

/* inline-flex with align-items:center for the same reason .hint-label above
 * uses it: <iconify-icon> is an unstyled web component and aligns on its own
 * baseline, so inline it drops below the label instead of sitting beside it. */
.dash-switch-btn {
    position: relative;
    z-index: 1;
    flex: 1 1 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    min-width: 0;
    padding: 0.5rem 1.25rem;
    border: 0;
    border-radius: 0.5rem;
    background: transparent;
    /* The theme's global reset strips button chrome, so everything this
     * control looks like has to be stated here — including the pointer, which
     * is most of what makes it read as clickable before you hover it. */
    -webkit-appearance: none;
    appearance: none;
    cursor: pointer;
    font-family: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    line-height: 1.4;
    white-space: nowrap;
    color: var(--text-secondary-light);
    transition: color 0.2s ease-in-out;
}

.dash-switch-btn iconify-icon {
    flex-shrink: 0;
    font-size: 1.05rem;
}

.dash-switch-btn[aria-selected="true"] {
    color: var(--primary-600);
}

.dash-switch-btn:not([aria-selected="true"]):hover {
    color: var(--text-primary-light);
}

.dash-switch-btn:focus-visible {
    outline: 2px solid var(--primary-600);
    outline-offset: 2px;
}

/* --base stays #fff under [data-theme=dark] (it is what white text resolves
 * from), so the thumb needs a real surface colour here or it stays white on a
 * dark track. neutral-300 sits one step above the neutral-100 track, which is
 * the same relationship the light theme gets from white on #F3F4F6. */
[data-theme=dark] .dash-switch-thumb {
    background-color: var(--neutral-300);
    box-shadow: none;
}

[data-theme=dark] .dash-switch-btn[aria-selected="true"] {
    color: var(--neutral-900);
}

/* --text-secondary-light is #D1D5DB in dark, which sits close enough to the
 * active #F5F6FA that the two halves rely on the thumb alone to tell them
 * apart. One step down restores the contrast the light theme gets for free
 * from the green. */
[data-theme=dark] .dash-switch-btn:not([aria-selected="true"]) {
    color: var(--neutral-500);
}

/* On a phone the control takes the full row and splits it evenly, so the two
 * halves stay equal (which the thumb geometry depends on) instead of the
 * longer label pushing its side wider. The label truncates rather than
 * overflowing the track at the very smallest widths; the icon stays, so the
 * two sides remain distinguishable even when the words are clipped. */
@media (max-width: 575.98px) {
    .dash-switch {
        display: flex;
        width: 100%;
    }

    .dash-switch-btn {
        gap: 0.375rem;
        padding: 0.5rem 0.5rem;
        font-size: 0.8125rem;
    }

    .dash-switch-btn > span:first-of-type {
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

/* The incoming pane rises into place rather than appearing. Short and small:
 * this runs on a click the reader already understands, so it is there to
 * confirm the swap, not to announce it. */
@keyframes dashPaneIn {
    from { opacity: 0; transform: translateY(0.5rem); }
    to   { opacity: 1; transform: none; }
}

.dash-pane-enter {
    animation: dashPaneIn 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Both animations are confirmation of something the click already said, so
 * honouring the preference means removing them rather than shortening them.
 * The switch still switches; it just arrives already in place. */
@media (prefers-reduced-motion: reduce) {
    .dash-switch-thumb { transition: none; }
    .dash-pane-enter { animation: none; }
}
