/* Disables Android Chromium WebView's "text autosizing" (a.k.a. font boosting) — on any reflow
   (tapping Jour/Mois/Année rebuilds the chart SVGs + a chunk of the DOM), the WebView briefly
   recomputes font sizes across the WHOLE page to "improve readability", then settles back a split
   second later. That's why it hit unrelated elements at once — stat-card icons, the segmented
   toggle, even the bottom-nav "Plus" button, in both Quitaye.Maui and Quitaye.Fleet (both render
   through this same shared app.css) — it's a page-wide WebView behavior, not a per-component CSS
   bug. Two earlier fixes (2026-08-25) targeted individual components' CSS specificity and didn't
   touch this at all, which is why they didn't fully resolve it. No effect on Web/desktop, which
   don't have this autosizer. */
html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

/* ============================================================
   Framework/template boilerplate (unchanged from the original
   per-host copies — kept here now that this is the single source)
   ============================================================ */
h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: var(--color-danger, #e50000);
}

#blazor-error-ui {
    background: lightyellow;
    /* Fixed banner background regardless of app theme, so its text is fixed too — the bright
       dark-mode --color-text would be unreadable against light yellow. */
    color: #1a1a1a;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

*, *::before, *::after {
    box-sizing: border-box;
}

/* ============================================================
   Quitaye design system — theme palettes
   Toggle via `document.documentElement.dataset.theme = "dark"|"light"`
   (see ThemeService / interop.js). Dark is the default (unset root
   falls back to dark) matching Account.ThemePreference's own default.

   Brand colors (cyan #01F9FF / purple #920CFF) are taken directly from
   the legacy WinForms app's `Services/ThemeManager.cs` — it swaps which
   one is "primary" between light/dark (light: purple primary, cyan
   secondary; dark: cyan primary, purple secondary), which --color-text/
   --color-secondary below mirror. --color-text/--color-subtext/the
   background shades are this app's own earlier choices (tuned for body-
   text readability, which legacy doesn't attempt — it only ever uses
   these two colors for icon accents, never as body text) and are
   deliberately kept rather than replaced.

   2026-09-02: the diagonal cyan->purple gradient sweep that used to be
   the default background for every button/badge/toggle/CTA in the app
   (--gradient-bar/--gradient-text) was removed — see
   docs/plans/2026-09-02-remove-gradients-human-design.md for the full
   audit and research. It read as the exact tell design critics call
   "VibeCode Purple": a decorative gradient wash applied by default to
   every interactive surface, not a deliberate signature. The two brand
   colors themselves were kept (they're this app's own real heritage,
   not an arbitrary AI pick) — every former gradient consumer switched
   to a plain solid --color-secondary fill.

   Same day, follow-up: an all-solid app read as flat/less appealing, so
   --gradient-bar/--gradient-text are back — deliberately reintroduced
   on only the small handful of genuinely singular "hero" spots (the
   raised bottom-nav Sale button, the Login/Onboarding h1) rather than
   restored as the default for every button/badge/toggle again. The
   distinction that matters is "one deliberate accent per screen" vs.
   "a wash on every interactive surface" — see each consumer's own
   comment below for why that specific spot earns it. The literal logo
   raster image (wwwroot/images/logo.png) was never CSS and is
   untouched either way.
   ============================================================ */
:root,
:root[data-theme="dark"] {
    --color-bg: #010922;
    --color-footer: #031B34;
    --color-blog: #010922;
    --color-text: #01F9FF;
    /* 2026-08-18: was #FF8A71 (full-strength coral) — read as "too much red" once secondary/muted
       text (labels, captions, field hints) covers most of a dense screen. Fully desaturating it to
       gray (a same-day intermediate step) then read as flat/unengaging on the tenant app specifically
       — settled on a toned-down coral here: warm enough to still feel like Quitaye's own brand accent,
       softer than the original so it doesn't dominate a text-heavy screen. Quitaye Fleet deliberately
       overrides this back toward neutral (see .fleet-shell below) — a platform-admin console reads
       better calmer, with coral reserved for genuine alerts (--color-danger), not routine labels. */
    --color-subtext: #E0876A;
    --color-secondary: #920CFF;
    --color-danger: #FF6B6B;
    --color-success: #3DDC97;
    --color-warning: #FFB020;
    --color-border: rgba(1, 249, 255, 0.18);
    --color-card-shadow: rgba(0, 0, 0, 0.35);

    /* Reintroduced 2026-09-03 (see the header comment above) — used sparingly, only by a
       small named set of consumers, never as a default. */
    --gradient-bar: linear-gradient(103.22deg, #01F9FF -13.86%, #920CFF 99.55%);
    --gradient-text: linear-gradient(103.22deg, #01F9FF -13.86%, #920CFF 99.55%);

    --radius-control: 12px;
    --radius-pill: 10px;
}

:root[data-theme="light"] {
    --color-bg: #FAF9FC;
    --color-footer: #F1EEF7;
    --color-blog: #FFFFFF;
    --color-text: #2596BE;
    /* Same 2026-08-18 reasoning as the dark block above — toned-down coral, not full-strength and
       not flat gray. */
    --color-subtext: #B8543C;
    --color-secondary: #920CFF;
    --color-danger: #D6455B;
    --color-success: #1E9E6B;
    --color-warning: #B8720C;
    --color-border: rgba(146, 12, 255, 0.18);
    --color-card-shadow: rgba(31, 27, 46, 0.08);

    /* Same reintroduction as the dark block above — #01C7D6 (not the literal #01F9FF) is cyan
       darkened for light-background contrast, same choice this app already made the first time it
       had a gradient. */
    --gradient-bar: linear-gradient(103.22deg, #920CFF -13.86%, #01C7D6 99.55%);
    --gradient-text: linear-gradient(103.22deg, #920CFF -13.86%, #01C7D6 99.55%);

    --radius-control: 12px;
    --radius-pill: 10px;
}

/* Quitaye Fleet-only override (2026-08-18) — an admin console reads calmer with muted/structural
   text kept neutral, reserving the tenant app's warmer coral --color-subtext for genuine alerts
   (--color-danger already covers those) rather than routine labels/captions across a dense screen
   full of business rows. Scoped to FleetLayout.razor's own wrapping .fleet-shell div so the tenant
   app (MainLayout.razor, no such wrapper) keeps the warmer value untouched. */
.fleet-shell {
    --color-subtext: #8B9BB4;
}

/* Real, confirmed root cause of the status-bar overlap (2026-08-18): the tenant app's own
   MainLayout.razor always renders a <TrialBanner /> as the first child of .content, above @Body —
   on mobile that banner (even when it renders nothing visible for a business past its trial) still
   occupies enough layout height to incidentally push the routed page's own <h1> clear of the status
   bar. FleetLayout.razor has nothing above @Body at all, so Fleet's page titles sit at the literal
   first pixel row with zero buffer. Two native Android fixes (a CSS-variable bridge via
   EvaluateJavascript, then a WindowInsets listener on the WebView) were both tried and confirmed
   live to not work — see MainPage.xaml.cs's own doc comment for why. This is the simple, guaranteed
   fix: explicit extra top padding on mobile only, sized to clear a typical status bar. */
@media (max-width: 860px) {
    .fleet-shell .content {
        padding-top: 3.5rem;
    }
}

/* ============================================================
   Quitaye app shell
   ============================================================ */
body {
    margin: 0;
    font-family: -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
    background: var(--color-bg);
    color: var(--color-text);
    transition: background-color 0.15s ease, color 0.15s ease;
    /* Page-level infinite scroll (2026-09-02, report pages) appends new rows just above a sentinel
       element near the bottom of the (document-scrolled, not modal-scrolled) page — the exact shape
       that already caused a real runaway IntersectionObserver-retrigger loop once for the Top
       Performers drill-down modal (see .sale-detail-modal's own overflow-anchor:none for the full
       mechanism). This app has no other scroll container for a normal page (the sidebar/.content
       layout never sets its own overflow-y), so `body` is the actual anchor context here — disabling
       it once, globally, protects every current and future page-level infinite-scroll consumer
       instead of chasing this bug a second time on a second page. */
    overflow-anchor: none;
}

a {
    color: var(--color-secondary);
}

.app-layout {
    display: flex;
    min-height: 100vh;
}

.sidebar {
    width: 220px;
    flex-shrink: 0;
    background: var(--color-footer);
    color: var(--color-text);
    padding: 1.5rem 1rem;
    border-inline-end: 1px solid var(--color-border);
    position: sticky;
    top: 0;
    align-self: flex-start;
    height: 100vh;
    overflow-y: auto;
}

.sidebar-brand {
    display: flex;
    justify-content: center;
    margin: 0 0 1.5rem;
}

.sidebar-brand img {
    height: 140px;
    width: auto;
    max-width: 100%;
    display: block;
}

/* Account page Phase 1 — sidebar identity chip + quick-switch dropdown (see IdentityChip.razor).
   Shares the app's real theme tokens, not an invented palette. */
.avatar {
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--color-bg);
    /* 2026-09-02: was the app-wide gradient sweep — the exact "gradient avatar-letter circle"
       tell this project's own ux-frontend-design skill names as a vibe-coded default, see
       docs/plans/2026-09-02-remove-gradients-human-design.md. Solid brand purple instead. */
    background: var(--color-secondary);
}

.avatar.sq {
    border-radius: 8px;
}

.avatar.lg {
    width: 3.25rem;
    height: 3.25rem;
    font-size: 1.1rem;
}

.identity-chip {
    position: relative;
    margin-bottom: 1rem;
}

.identity-chip-toggle {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.5rem;
    border-radius: var(--radius-control);
    border: 1px solid var(--color-border);
    background: transparent;
    background-image: none;
    color: var(--color-text);
    cursor: pointer;
    text-align: start;
}

.identity-chip-toggle:hover {
    background: rgba(146, 12, 255, 0.08);
}

.identity-chip-toggle .identity-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.identity-chip-toggle .identity-name-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    min-width: 0;
}

.identity-chip-toggle .identity-name {
    font-size: 0.8rem;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}

/* Plan badge next to the account name (2026-08-11) — same spot Claude's own sidebar shows the
   current plan next to the account name. flex-shrink: 0 so a long name truncates first, never this. */
.plan-badge {
    flex-shrink: 0;
    font-size: 0.6rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    padding: 0.1rem 0.4rem;
    border-radius: 999px;
    background: color-mix(in srgb, var(--color-text) 14%, transparent);
    color: var(--color-text);
}

.identity-chip-toggle .identity-sub {
    font-size: 0.68rem;
    color: var(--color-subtext);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Per-business identity color dot (2026-08-29 large-feedback-batch Phase H) — deliberately just
   these two always-on-screen spots (the sidebar chip + the Business Picker's own list), not spread
   across the whole app, per the user's own "visible almost all the time, not everywhere" ask. */
.identity-color-dot {
    display: inline-block;
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    margin-inline-end: 0.35rem;
    flex-shrink: 0;
    vertical-align: middle;
}

.identity-chip-toggle .nav-icon-svg {
    margin: 0;
    opacity: 0.6;
    flex-shrink: 0;
}

.identity-chip-overlay {
    position: fixed;
    inset: 0;
    z-index: 29;
    background: transparent;
}

.quickswitch {
    position: absolute;
    top: calc(100% + 6px);
    inset-inline-start: 0;
    width: 100%;
    min-width: 240px;
    z-index: 30;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 18px 40px -14px var(--color-card-shadow);
    padding: 0.4rem;
}

.qs-label {
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-subtext);
    padding: 0.4rem 0.5rem 0.25rem;
}

.qs-row {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.5rem;
    border-radius: 8px;
    background: transparent;
    background-image: none;
    color: var(--color-text);
    cursor: pointer;
    text-align: start;
}

.qs-row:hover:not(:disabled) {
    background: rgba(146, 12, 255, 0.08);
}

.qs-row.active {
    background: rgba(146, 12, 255, 0.12);
    cursor: default;
}

.qs-row:disabled {
    opacity: 1;
}

.qs-row .avatar {
    width: 1.6rem;
    height: 1.6rem;
    font-size: 0.65rem;
}

.qs-text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    text-align: start;
}

.qs-name {
    font-size: 0.82rem;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.qs-role {
    font-size: 0.65rem;
    color: var(--color-subtext);
}

.qs-divider {
    height: 1px;
    background: var(--color-border);
    margin: 0.35rem 0.1rem;
}

.qs-manage {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem;
    border-radius: 8px;
    background: transparent;
    background-image: none;
    color: var(--color-subtext);
    font-size: 0.78rem;
    cursor: pointer;
}

.qs-manage:hover {
    background: rgba(255, 138, 113, 0.08);
}

/* Notification bell (2026-08-22 mega-backlog Phase 3) — same overlay+absolute-panel popover shape
   .identity-chip/.quickswitch already established above, reusing the exact same tokens/z-index
   band rather than a new visual language. .sidebar-toolbar is the small always-visible row it sits
   in, right above the identity chip (both MainLayout.razor and FleetLayout.razor). */
.sidebar-toolbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}

/* Green/amber/red connectivity signal (2026-09-07) — semantic color, not the app's own accent hue,
   same "state color is separate from the brand accent" convention chip/badge colors already use
   elsewhere. */
.connectivity-indicator {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.72rem;
    color: var(--color-text);
    opacity: 0.7;
}

.connectivity-dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--color-success);
}

.connectivity-online .connectivity-dot { background: var(--color-success); }
.connectivity-syncing .connectivity-dot { background: var(--color-warning); }
.connectivity-offline .connectivity-dot { background: var(--color-danger); }

/* Compact form: just the dot, used as a badge on the mobile tab bar's "More" button (always present
   regardless of section permissions) so status is visible without opening the drawer. */
.connectivity-compact {
    position: absolute;
    top: 0.1rem;
    inset-inline-end: 0.35rem;
}

.connectivity-compact .connectivity-dot {
    width: 0.4rem;
    height: 0.4rem;
    box-shadow: 0 0 0 2px var(--color-footer);
}

/* Inline page-header connectivity slot (2026-09-07, direct user request — supersedes an earlier
   same-day site-wide mobile banner, moved to app.css history) — title on the left, the indicator
   pushed to the far right of the SAME line, only on a page that actually embeds it (today, just
   Sales.razor's own <h1> row). */
.pos-page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

/* Hidden on desktop by default — the sidebar's own full-label indicator is already visible there at
   all times; the @media (max-width: 860px) block below re-enables it, mobile only. */
.pos-page-header .connectivity-indicator {
    display: none;
}

.notification-bell {
    position: relative;
}

.notification-bell-toggle {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    background: transparent;
    background-image: none;
    color: var(--color-text);
    cursor: pointer;
}

.notification-bell-toggle:hover {
    background: rgba(146, 12, 255, 0.08);
}

.notification-bell-badge {
    position: absolute;
    top: 0;
    inset-inline-end: 0;
    min-width: 1.1rem;
    height: 1.1rem;
    padding: 0 0.25rem;
    border-radius: 999px;
    background: var(--color-danger);
    color: #fff;
    font-size: 0.6rem;
    font-weight: 700;
    line-height: 1.1rem;
    text-align: center;
}

.notification-bell-overlay {
    position: fixed;
    inset: 0;
    z-index: 29;
    background: transparent;
}

.notification-panel {
    position: absolute;
    top: calc(100% + 6px);
    inset-inline-end: 0;
    width: 320px;
    max-width: calc(100vw - 2rem);
    z-index: 30;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 18px 40px -14px var(--color-card-shadow);
    padding: 0.4rem;
    max-height: 70vh;
    overflow-y: auto;
}

.notification-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.4rem 0.5rem 0.3rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--color-subtext);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.notification-mark-all {
    background: none;
    background-image: none;
    color: var(--color-secondary);
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: none;
    letter-spacing: normal;
    cursor: pointer;
    padding: 0;
}

.notification-empty {
    padding: 1rem 0.5rem;
    text-align: center;
    color: var(--color-subtext);
    font-size: 0.8rem;
}

.notification-row {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 0.55rem 0.6rem;
    border-radius: 8px;
    background: transparent;
    background-image: none;
    color: var(--color-text);
    text-align: start;
    cursor: pointer;
}

.notification-row:hover {
    background: rgba(146, 12, 255, 0.08);
}

.notification-row.unread {
    background: rgba(146, 12, 255, 0.06);
}

.notification-row.unread .notification-row-title::before {
    content: "";
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-secondary);
    margin-inline-end: 0.4rem;
    vertical-align: middle;
}

.notification-row-title {
    font-size: 0.82rem;
    font-weight: 600;
}

.notification-row-body {
    font-size: 0.72rem;
    color: var(--color-subtext);
}

.notification-row-time {
    font-size: 0.62rem;
    color: var(--color-subtext);
    opacity: 0.7;
}

.sidebar nav a {
    display: flex;
    align-items: center;
    padding: 0.6rem 0.75rem;
    margin-bottom: 0.25rem;
    border-radius: var(--radius-control);
    border-inline-start: 3px solid transparent;
    color: var(--color-text);
    text-decoration: none;
    opacity: 0.7;
    transition: background-color 0.15s ease, opacity 0.15s ease, border-color 0.15s ease;
}

.sidebar nav a:hover {
    background: rgba(146, 12, 255, 0.12);
    opacity: 1;
}

.sidebar nav a.active {
    background: rgba(146, 12, 255, 0.2);
    opacity: 1;
    font-weight: 600;
    border-inline-start-color: var(--color-secondary);
}

.nav-icon-svg {
    width: 1.1rem;
    height: 1.1rem;
    flex-shrink: 0;
    margin-inline-end: 0.6rem;
    color: currentColor;
    opacity: 0.85;
}

.sidebar nav a.active .nav-icon-svg,
.nav-group-toggle .nav-icon-svg {
    opacity: 1;
}

/* LockIcon.razor (2026-08-11) — a plan-gated nav item stays visible (unlike a section restriction,
   which hides the item entirely) with a small lock badge pushed to the far end of the row, so
   what's locked reads at a glance without needing to tap in and hit the paywall first. Wraps
   <Icon> in a span rather than passing it a class directly — Icon.razor has no attribute-splatting
   parameter, so an unrecognized attribute on the component tag is a compile error, not a no-op. */
.nav-lock-icon {
    display: inline-flex;
    margin-inline-start: auto;
    margin-inline-end: 0;
    flex-shrink: 0;
    opacity: 0.55;
}

.nav-lock-icon .nav-icon-svg {
    width: 0.8rem;
    height: 0.8rem;
    margin-inline-end: 0;
}

.nav-group {
    margin-bottom: 0.25rem;
}

.nav-group-toggle {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    justify-content: flex-start;
    padding: 0.6rem 0.75rem;
    margin-bottom: 0.25rem;
    font-size: 0.95rem;
    border-radius: var(--radius-control);
    border-inline-start: 3px solid transparent;
    /* Overrides the generic `button` element rule's solid-fill/white-text/pill styling below —
       a group toggle is a nav item, not an action button, so it should rest at the same plain
       look as a `.sidebar nav a` link (e.g. "Dashboard") until a child page makes it active. */
    background: transparent;
    background-image: none;
    color: var(--color-text);
    font-weight: 400;
    opacity: 0.7;
    transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}

.nav-group-toggle:hover {
    background: rgba(146, 12, 255, 0.12);
    opacity: 1;
}

.nav-group-title {
    flex: 1;
    text-align: start;
}

/* Highlights the group's own toggle button (the "main button") whenever one of its links is the
   current page — so e.g. "Products" reads as selected while you're on /stock, not just "Stock"
   itself inside the (possibly collapsed) sub-list. */
.nav-group:has(.nav-group-items a.active) > .nav-group-toggle {
    background: rgba(146, 12, 255, 0.14);
    border-inline-start-color: var(--color-secondary);
    font-weight: 600;
}

.nav-group-chevron {
    font-size: 0.7rem;
    opacity: 0.7;
    display: inline-block;
    transition: transform 0.2s ease;
}

.nav-group-chevron.expanded {
    transform: rotate(90deg);
}

.nav-group-items {
    padding-inline-start: 0.75rem;
    border-inline-start: 1px solid var(--color-border);
    margin-inline-start: 0.5rem;
    overflow: hidden;
    /* Raised from 500px (2026-07-10): the Products/Transactions groups now nest Manage/Operate/
       Analyze sub-groups inside this same class (NavGroup composes into NavGroup), which can
       exceed the old cap once fully expanded and silently clip the tail of the list — this is a
       transition cap, not a real content limit, so generous headroom costs nothing. */
    max-height: 2000px;
    opacity: 1;
    transition: max-height 0.25s ease, opacity 0.2s ease, margin-block 0.25s ease, border-color 0.25s ease;
}

/* A nested NavGroup (the Manage/Operate/Analyze sub-groups added 2026-07-10) needs to read as a
   lower-emphasis sub-level, not an equal-weight second group stacked on top of the first — smaller,
   dimmer, tighter padding than a top-level .nav-group-toggle. */
.nav-group-items .nav-group-toggle {
    font-size: 0.82rem;
    padding: 0.4rem 0.6rem;
    opacity: 0.55;
}

.nav-group-items .nav-group-items {
    margin-inline-start: 0.35rem;
    padding-inline-start: 0.5rem;
}

.nav-group-items.collapsed {
    max-height: 0;
    opacity: 0;
    margin-block: 0;
    border-inline-start-color: transparent;
    pointer-events: none;
}

.nav-group-items a {
    font-size: 0.92rem;
}

.preferences-bar {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin: 1rem 0;
    padding: 0.75rem 0;
    border-block: 1px solid var(--color-border);
}

.preferences-bar button,
.preferences-bar select {
    width: 100%;
}

.preferences-bar select {
    padding: 0.5rem 0.75rem;
    border-radius: var(--radius-control);
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text);
}

.content {
    flex: 1;
    padding: 2rem;
    /* Real-device (status bar/notch) clearance for iOS/WKWebView, which supports env() natively —
       a no-op (0px) everywhere else (every desktop browser, Windows MAUI). Android is deliberately
       NOT handled here — env(safe-area-inset-top) is unreliable inside Android's embedded WebView
       (confirmed live, 2026-08-18: reported 0, page <h1>s rendered under the status bar), and a CSS-
       variable-via-JS-injection attempt also failed (timing raced the WebView's real page load, see
       MainPage.xaml.cs's own doc comment). Android's fix lives natively instead — MainPage.xaml.cs
       (both Quitaye.Maui and Quitaye.Fleet) pads the WebView's own top edge via
       ViewCompat.SetOnApplyWindowInsetsListener, which has no such race. */
    padding-top: calc(2rem + env(safe-area-inset-top, 0px));
}

/* Mobile nav: a bottom tab bar replaces the always-visible desktop sidebar below the breakpoint,
   since a fixed 220px sidebar next to a phone-width viewport leaves almost no room for content
   (this is literally what happened before this was added — confirmed live on an Android emulator).
   A top hamburger bar previously existed alongside it but was removed (2026-07-31) — it was purely
   redundant with the tab bar's own "More" button, and on a real device it rendered stuck under the
   OS status bar (no safe-area inset applied), making it functionally unreachable there anyway. The
   tab bar is hidden by default (desktop) and only shown inside the @media block; .sidebar becomes
   a full-screen off-canvas drawer there too, toggled by the tab bar's "More" button. .sidebar-close
   only makes sense once the drawer covers the whole screen (see @media block below), so it stays
   hidden on desktop where .sidebar is always visible and never a drawer. */
.mobile-tabbar,
.sidebar-close {
    display: none;
}

.sidebar-backdrop {
    display: none;
}

/* Quitaye Fleet's own compact brand row (2026-08-17 Overview redesign) — replaces the tenant app's
   140px logo image inside the same shared .sidebar-brand slot with a small mark + wordmark, since
   Fleet is its own product surface, not a tenant-branded screen. */
.fleet-brand {
    justify-content: flex-start;
    gap: 0.6rem;
    padding: 0 0.4rem;
}

.fleet-mark {
    width: 30px;
    height: 30px;
    border-radius: 9px;
    background: var(--color-secondary);
    flex-shrink: 0;
}

.fleet-brand-text {
    display: flex;
    flex-direction: column;
    line-height: 1.15;
}

.fleet-brand-text b {
    font-size: 0.95rem;
}

.fleet-brand-text span {
    font-size: 0.68rem;
    color: var(--color-subtext);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.fleet-logout {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-top: 1.5rem;
    padding: 0.6rem 0.8rem;
    background: none;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    color: var(--color-text);
    font-weight: 600;
}

@media (max-width: 860px) {
    .app-layout {
        display: block;
    }

    .sidebar {
        position: fixed;
        inset: 0;
        z-index: 50;
        width: 100%;
        transform: translateX(-100%);
        transition: transform 0.2s ease;
        box-shadow: 4px 0 24px rgba(0, 0, 0, 0.3);
    }

    .sidebar-close {
        display: flex;
        align-items: center;
        justify-content: center;
        margin-inline-start: auto;
        margin-block-end: 1rem;
        width: 2.25rem;
        height: 2.25rem;
        border-radius: 50%;
        background: none;
        border: none;
        color: var(--color-text);
    }

    .sidebar-close .nav-icon-svg {
        width: 1.4rem;
        height: 1.4rem;
        margin: 0;
    }

    /* inset-inline-start is logical (anchors the drawer to the right edge in RTL), but
       transform: translateX isn't — it always shifts left, regardless of direction. In RTL the
       drawer is anchored on the right, so shifting left by its own width moves it INTO view
       instead of off-screen, leaving it stuck open with no way to close it. Flip the closed-state
       offset for RTL specifically; the open state (translateX(0), no offset) needs no direction. */
    .sidebar:dir(rtl) {
        transform: translateX(100%);
    }

    .sidebar.drawer-open {
        transform: translateX(0);
    }

    .sidebar-backdrop.visible {
        display: block;
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.5);
        z-index: 40;
    }

    /* Direct user request (2026-09-07): "remember this apply only to the mobile (indicator)" —
       shown here (mobile breakpoint only); the base, non-media rule below hides it on desktop,
       where the sidebar's own full-label indicator is already visible at all times. */
    .pos-page-header .connectivity-indicator {
        display: flex;
        padding: 0.3rem 0.6rem;
        border-radius: var(--radius-control);
        background: var(--color-card);
        border: 1px solid var(--color-border);
        opacity: 1;
    }

    .content {
        /* Real bug found live (2026-09-07), then corrected same-day after a further live check:
           adding an explicit padding-top here (to fix the mobile-connectivity-banner overlapping
           the status bar) DOUBLE-reserved space — MainPage.xaml.cs's own ApplyStatusBarInset
           (SetDecorFitsSystemWindows(true)) already reserves the real system-bar area natively, so
           stacking a second CSS buffer on top of it pushed TrialBanner/every page's own <h1> down
           far more than needed ("still the trial banner way down"). The actual root cause of the
           original overlap was the now-removed mobile-connectivity-banner rendering as literally the
           FIRST thing in .content, before the native inset had settled — moving the connectivity
           indicator inline into Sales.razor's own page header (see .pos-page-header) removes that
           first-child position entirely, so the plain padding below is sufficient again. */
        padding: 1rem;
        padding-block-end: 5.5rem; /* clears the fixed bottom tab bar */
    }

    .mobile-tabbar {
        display: flex;
        position: fixed;
        inset-block-end: 0;
        inset-inline: 0;
        z-index: 30;
        background: var(--color-footer);
        border-block-start: 1px solid var(--color-border);
        padding: 0.35rem 0;
    }

    .mobile-tabbar a,
    .mobile-tabbar button {
        position: relative;
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 0.15rem;
        padding: 0.35rem 0.25rem;
        background: none;
        border: none;
        color: var(--color-text);
        opacity: 0.7;
        text-decoration: none;
        font-size: 0.7rem;
    }

    .mobile-tabbar .nav-icon-svg {
        width: 1.25rem;
        height: 1.25rem;
        margin: 0;
    }

    .mobile-tabbar a.active,
    .mobile-tabbar button.active {
        opacity: 1;
        color: var(--color-secondary);
    }

    /* Center CTA tab (Sales) — a raised circular badge instead of a plain icon, same "central,
       most-reachable action" pattern the ux-frontend-design skill calls out for bottom navs. Always
       full-opacity/white icon regardless of active state, since it's a call-to-action, not a plain
       destination indicator like the other four tabs. */
    .mobile-tabbar a.primary-tab {
        opacity: 1;
    }

    /* One of the small handful of spots the gradient was deliberately reintroduced to (2026-09-03,
       see the header comment near the top of this file) — a single raised circular CTA, genuinely
       one-per-screen, not a wash applied to every button by default. */
    .mobile-tabbar a.primary-tab .tab-icon-badge {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 2.75rem;
        height: 2.75rem;
        border-radius: 50%;
        background: var(--gradient-bar);
        margin-block-start: -1.1rem;
        box-shadow: 0 4px 12px var(--color-card-shadow);
    }

    .mobile-tabbar a.primary-tab .tab-icon-badge .nav-icon-svg {
        color: white;
        opacity: 1;
    }

    .mobile-tabbar a.primary-tab.active span:last-child {
        color: var(--color-secondary);
    }
}

.login-card {
    max-width: 340px;
    width: 100%;
    margin: 4rem auto;
    padding: 2rem;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 8px 24px var(--color-card-shadow);
}

@media (max-width: 420px) {
    .login-card {
        margin: 1.5rem auto;
        padding: 1.25rem;
    }
}

/* The other spot the gradient was deliberately reintroduced to (2026-09-03) — the entry-point hero
   heading, once per page load, not a default applied to every heading in the app. display:inline-block
   is required for background-clip:text to size to the text's own box rather than the whole block. */
.login-card h1 {
    display: inline-block;
    background: var(--gradient-text);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.field {
    margin-bottom: 1rem;
    display: flex;
    flex-direction: column;
}

/* Ergonomic form layout matching the legacy WinForms pages: short fields (name, barcode, size,
   stock min/max, ...) sit several to a row instead of each eating the full page width; a field
   marked .field-wide (comboboxes with an inline quick-add panel, price-tier sections, anything
   that needs real room) always takes the whole row regardless of how narrow the viewport gets. */
.form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 0 1.5rem;
    align-items: start;
}

.form-grid .field-wide {
    grid-column: 1 / -1;
}

.field label {
    font-size: 0.85rem;
    margin-bottom: 0.25rem;
    color: var(--color-subtext);
}

.field input,
.field select,
.combo-quickadd-select {
    height: 2.75rem;
    box-sizing: border-box;
    padding: 0.6rem 0.85rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-bg);
    color: var(--color-text);
    font: inherit;
}

/* A native <input type="file"> (every bulk-import "choose a file" field, the Products photo picker,
   Data Extraction's PDF picker) doesn't reliably stretch to its .field's own width the way a plain
   text input does — some mobile browsers give its internal "Choose File" button + placeholder text a
   fixed intrinsic minimum width it refuses to shrink below, wider than the .field itself gets on a
   narrow phone. Its .field, as a .toolbar flex item, has the ordinary flex-item default min-width:auto
   too, which lets that unshrinkable content force the whole toolbar row (and, inside an import modal,
   the modal itself) wider than the viewport instead of just wrapping onto its own line. Both halves of
   the fix are needed together — max-width on the input alone still lets its own .field refuse to
   shrink below the input's un-capped intrinsic size first. */
.field:has(input[type="file"]) {
    min-width: 0;
}

.field input[type="file"] {
    max-width: 100%;
}

/* A native <select>'s intrinsic chrome (dropdown-arrow rendering) makes it taller than a <button>
   with identical padding, even before accounting for the button having no border at all (see
   .toolbar .field above) — matching padding alone doesn't produce matching height. An explicit
   shared height + box-sizing removes the guesswork: every direct-child action button in a toolbar
   (Add line, Save, Apply to selected, ...) is now pixel-identical in height to the input/select
   it sits beside, not just bottom-aligned with it. */
.toolbar > button {
    height: 2.75rem;
    box-sizing: border-box;
}

.field input:focus,
.field select:focus,
.combo-quickadd-select:focus {
    outline: none;
    border-color: var(--color-secondary);
}

/* DateRangePicker's date+time pair — narrower time input beside the date input, wrapping on
   narrow phones rather than squeezing both into unreadable widths. */
.date-time-pair {
    display: flex;
    gap: 0.4rem;
    flex-wrap: wrap;
}

.date-time-pair input[type="time"] {
    max-width: 6.5rem;
}

.combo-quickadd {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
}

.combo-quickadd-select {
    flex: 1;
    min-width: 140px;
}

/* Icon+text (not icon-only) since 2026-08-04 — a bare "+" glyph relies on a hover tooltip to
   explain itself, which doesn't exist on a touch device; pairing the icon with the caller's own
   AddButtonTitle text makes the button self-explanatory at a glance, matching this app's own
   icon+text button convention elsewhere. Grows with its label instead of a fixed icon-only square
   (button .nav-icon-svg's global margin already spaces the icon from the text) — height matches
   .combo-quickadd-select's so the two don't render at mismatched heights side by side, and
   .combo-quickadd's own flex-wrap lets the button drop to its own line on a narrow screen rather
   than squeezing the select down to an unreadable width. */
.combo-quickadd-toggle {
    flex-shrink: 0;
    height: 2.75rem;
    box-sizing: border-box;
    padding: 0 0.9rem;
    display: flex;
    align-items: center;
    justify-content: center;
    white-space: nowrap;
    border-radius: var(--radius-control);
}

.quickadd-panel {
    margin-top: 0.5rem;
    padding: 0.75rem;
    border: 1px dashed var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-blog);
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    align-items: end;
}

.quickadd-panel .field {
    margin-bottom: 0;
    min-width: 8rem;
}

/* CountryPicker.razor (mega-backlog Phase E item 2) — a search box narrowing a native <select>'s own
   option list, same "matching height/border/radius, flex row that wraps on a narrow screen" shape
   .combo-quickadd already established, just a 2-control row (search + select) instead of a
   select + add-button one. */
.country-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.country-picker-search,
.country-picker select {
    height: 2.75rem;
    box-sizing: border-box;
    padding: 0.6rem 0.85rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-bg);
    color: var(--color-text);
    font: inherit;
}

.country-picker-search {
    flex: 1;
    min-width: 10rem;
}

.country-picker select {
    flex: 1;
    min-width: 10rem;
}

.country-picker-search:focus,
.country-picker select:focus {
    outline: none;
    border-color: var(--color-secondary);
}

.segmented-toggle {
    display: inline-flex;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    overflow: hidden;
    height: 2.75rem;
    box-sizing: border-box;
    margin-bottom: 1rem;
}

/* Same bug as .toolbar .field, one level deeper: a SegmentedToggle inside a .field carries its
   OWN margin-bottom independent of the field wrapper's (already zeroed for toolbar context above),
   so it still rendered lower than its sibling inputs/selects/buttons even after that fix — the
   height addition above alone wasn't enough. Zero it here too. */
.toolbar .segmented-toggle {
    margin-bottom: 0;
}

.segmented-toggle button {
    background: transparent;
    background-image: none;
    color: var(--color-text);
    border: none;
    border-radius: 0;
    padding: 0.5rem 1.1rem;
    font-weight: 500;
    opacity: 0.7;
}

.segmented-toggle button:not(:last-child) {
    border-inline-end: 1px solid var(--color-border);
}

.segmented-toggle button.active {
    background: var(--color-secondary);
    color: white;
    opacity: 1;
}

.tab-group-strip-wrap {
    position: relative;
}

.tab-group-strip {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    border-bottom: 1px solid var(--color-border);
    margin-bottom: 1.5rem;
}

/* Trailing "more tabs to the right" hint for the mobile single-row scroll strip below — hidden on
   desktop (the strip wraps instead, every tab is already visible) and hidden by default on mobile
   too; interop.js's edgeScrollHint toggles .visible only while the strip actually has unscrolled
   content, so it never shows once you've scrolled all the way to the last tab. */
.tab-scroll-hint {
    display: none;
}

@media (max-width: 860px) {
    .tab-group-strip {
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
    }

    .tab-group-strip::-webkit-scrollbar {
        display: none;
    }

    .tab-group-strip button {
        flex: 0 0 auto;
        white-space: nowrap;
    }

    .tab-scroll-hint {
        display: flex;
        align-items: center;
        justify-content: center;
        position: absolute;
        top: 0;
        bottom: 1.5rem;
        right: 0;
        width: 2.25rem;
        pointer-events: none;
        opacity: 0;
        transition: opacity 0.15s ease;
        background: linear-gradient(to right, transparent, var(--color-bg) 55%);
        color: var(--color-subtext);
    }

    .tab-scroll-hint.visible {
        opacity: 1;
    }
}

.tab-group-strip button {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    background: transparent;
    background-image: none;
    color: var(--color-subtext);
    border: none;
    border-radius: 0;
    padding: 0.75rem 1rem;
    font-weight: 500;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}

.tab-group-strip button:hover {
    color: var(--color-text);
}

.tab-group-strip button.active {
    color: var(--color-text);
    border-bottom-color: var(--color-secondary);
    font-weight: 600;
}

.tab-group-strip .nav-icon-svg {
    width: 18px;
    height: 18px;
}

/* Real on/off switch, distinct from SegmentedToggle's pill (used for a small fixed option set) —
   per ui-fundamentals.md's control table, a genuine binary state reads as a toggle, not a 2-option
   segmented control. Backed by a plain checkbox for accessibility/keyboard support. */
.switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.9rem 0;
    border-bottom: 1px solid var(--color-border);
}

.switch-row:last-child {
    border-bottom: none;
}

.switch-row-label {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.switch-row-label .switch-caption {
    font-size: 0.85rem;
    color: var(--color-subtext);
}

.switch {
    position: relative;
    display: inline-block;
    width: 2.75rem;
    height: 1.6rem;
    flex-shrink: 0;
}

.switch input {
    opacity: 0;
    width: 0;
    height: 0;
}

.switch-track {
    position: absolute;
    inset: 0;
    background: var(--color-border);
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: background-color 0.15s ease;
}

.switch-track::before {
    content: "";
    position: absolute;
    height: 1.2rem;
    width: 1.2rem;
    left: 0.2rem;
    top: 0.2rem;
    background: white;
    border-radius: 50%;
    transition: transform 0.15s ease;
}

.switch input:checked + .switch-track {
    background: var(--color-secondary);
}

.switch input:checked + .switch-track::before {
    transform: translateX(1.15rem);
}

.switch input:disabled + .switch-track {
    opacity: 0.5;
    cursor: not-allowed;
}

.settings-card {
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg, 12px);
    padding: 1.25rem 1.5rem;
    margin-bottom: 1.5rem;
}

.settings-card h3 {
    margin-top: 0;
}

/* CollapsibleSection.razor — progressive-disclosure form section (e.g. Products.razor's "Advanced
   details"). .collapsible-section itself needs no rule of its own beyond .settings-card; only the
   header/chevron/body need styling. */
.collapsible-section {
    padding: 0;
    overflow: hidden;
}

.collapsible-section-header {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 1rem 1.25rem;
    /* Overrides the generic `button` element rule's solid-fill/white-text/pill styling — this is a
       section label a user clicks to expand, not an action button, so it rests at plain text/border
       weight instead, same reasoning .nav-group-toggle already documents for the identical override. */
    background: transparent;
    background-image: none;
    color: var(--color-text);
    font-weight: 600;
    font-size: 1rem;
    border: none;
    border-radius: 0;
    text-align: start;
}

.collapsible-section-header:hover {
    background: rgba(146, 12, 255, 0.08);
}

.collapsible-section-chevron {
    flex: none;
    display: flex;
    color: var(--color-subtext);
    transition: transform 0.15s ease;
}

.collapsible-section-chevron.expanded {
    transform: rotate(180deg);
}

.collapsible-section-body {
    padding: 0 1.25rem 1.25rem;
    border-top: 1px solid var(--color-border);
    padding-top: 1rem;
}

/* Members.razor's section-permissions checkbox picker — a compact multi-column grid, not a long
   single-column checkbox list, since there are 13 real values (section-permissions.md §2/§3). */
.section-checkbox-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 0.5rem 1rem;
    margin: 0.5rem 0 1rem;
}

.section-checkbox {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.9rem;
    color: var(--color-text);
    cursor: pointer;
}

.section-checkbox input[type="checkbox"] {
    flex: none;
}

/* Account.razor (Phase 1) — profile header, business list, preferences, admin shortcuts. Reuses
   .settings-card for the card shell (same visual language as Settings.razor's tabs) — these rules
   only cover the row/list shapes specific to the Account page itself. */
.profile-row {
    display: flex;
    align-items: center;
    gap: 1rem;
}

/* The text/edit column next to the avatar — needs min-width:0 to actually shrink inside the flex
   row instead of forcing the whole card wider (the same flex-blowout class of bug this file has
   already fixed several times elsewhere, e.g. .pos-layout > *), since a long email/name has no
   natural break point for the browser to wrap at. */
.profile-info {
    min-width: 0;
    flex: 1;
}

.profile-name {
    font-size: 1.05rem;
    font-weight: 700;
    overflow-wrap: anywhere;
    min-width: 0;
    flex: 1;
}

.profile-email {
    font-size: 0.8rem;
    color: var(--color-subtext);
    margin-top: 0.15rem;
    overflow-wrap: anywhere;
}

.biz-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem;
    padding: 0.65rem 0.4rem;
}

/* PendingInvitationsBanner.razor (Home.razor only) — collapsed by default: a one-line strip in the
   exact same visual language as .trial-banner (solid accent fill, white text, pill shape) per the
   user's own explicit comparison ("6 days left in your free trial ... Upgrade now"), tap anywhere on
   it to expand, with its own small "x" icon beside it for Dismiss — never a bordered "Dismiss" button
   competing with the tap target for the same line. Expanded, .invitation-card below moves to a
   separate, neutral (non-colored) card — Accept/Decline are real decisions, not styled with the same
   urgency-color weight as the collapsed nudge. Real bug fixed here (2026-09-06, confirmed live on a
   physical phone): the very first version crammed avatar + wrapping business name + wrapping "Invited
   by X" + two inline Accept/Decline buttons onto one .biz-row-style flex row — there is no width on a
   real ~360dp phone screen that doesn't force some of that content to overlap. .invitation-card stacks
   info on top and actions on their own full-width row instead, so nothing there shares a line with
   text that can wrap to more than one line either. */
.invitations-banner {
    margin-bottom: 1.5rem;
}

.invitations-banner-strip {
    display: flex;
    align-items: stretch;
    border-radius: var(--radius-control);
    background: var(--color-secondary);
    color: #FFFFFF;
    overflow: hidden;
}

.invitations-banner-toggle {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.6rem 1rem;
    background: transparent;
    background-image: none;
    color: inherit;
    font-weight: 600;
    font-size: 0.88rem;
    border: none;
    border-radius: 0;
    text-align: start;
}

.invitations-banner-label {
    flex: 1;
    min-width: 0;
}

/* Same rotate-on-expand convention as .collapsible-section-chevron. */
.invitations-banner-chevron {
    flex: none;
    display: flex;
    transition: transform 0.15s ease;
}

.invitations-banner-chevron.expanded {
    transform: rotate(180deg);
}

/* Icon-only, deliberately never full-button weight — a small "x" beside the tap-to-expand strip,
   not a "Dismiss" button competing with it for the same line. */
.invitations-banner-close {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    background: transparent;
    background-image: none;
    color: inherit;
    border: none;
    border-radius: 0;
}

.invitations-banner-close:hover {
    background: rgba(255, 255, 255, 0.15);
}

.invitations-banner-list {
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg, 12px);
    margin-top: 0.75rem;
    overflow: hidden;
}

/* Desktop: info and actions share one row (actions right-aligned, sized to their own text — a
   flex:1 Accept/Decline pair reads oversized next to a short business name on a wide screen). Below
   860px (see the media query further down), this switches to the stacked, full-width-actions layout
   that fixed the real overlap bug confirmed live on a phone — info can wrap to several lines there
   with no width to spare, so actions need their own row, not a sliver of a shared one. */
.invitation-card {
    padding: 0.85rem 1rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.invitation-card + .invitation-card {
    border-top: 1px solid var(--color-border);
}

.invitation-card-info {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 0;
    flex: 1;
}

.invitation-card-actions {
    display: flex;
    flex: none;
    gap: 0.6rem;
}

@media (max-width: 860px) {
    .invitation-card {
        flex-direction: column;
        align-items: stretch;
        gap: 0.65rem;
    }

    .invitation-card-actions {
        flex: none;
    }

    .invitation-card-actions button {
        flex: 1;
    }
}

.biz-row + .biz-row {
    border-top: 1px solid var(--color-border);
}

.biz-row.active {
    background: rgba(146, 12, 255, 0.06);
    border-radius: var(--radius-control);
}

.biz-info {
    flex: 1;
    min-width: 0;
}

.biz-name {
    font-weight: 600;
    font-size: 0.9rem;
}

.biz-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.2rem;
}

.biz-current-tag {
    font-size: 0.68rem;
    font-weight: 700;
    color: var(--color-text);
}

.add-biz-row {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.65rem 0.4rem;
    border-top: 1px dashed var(--color-border);
    margin-top: 0.25rem;
    background: transparent;
    background-image: none;
    color: var(--color-text);
    cursor: pointer;
}

.pref-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.55rem 0.25rem;
    font-size: 0.85rem;
}

.pref-row + .pref-row {
    border-top: 1px solid var(--color-border);
}

.shortcut-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 0.75rem;
}

.shortcut {
    display: block;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    padding: 0.75rem 0.9rem;
    font-size: 0.8rem;
    color: var(--color-subtext);
    text-decoration: none;
}

.shortcut:hover {
    background: rgba(146, 12, 255, 0.06);
}

.shortcut strong {
    display: block;
    color: var(--color-text);
    font-size: 0.9rem;
    margin-bottom: 0.2rem;
}

.danger-btn {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    background: transparent;
    background-image: none;
    color: var(--color-danger);
    padding: 0.5rem 0.25rem;
    font-weight: 400;
    cursor: pointer;
}

.color-swatch-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.color-swatch-picker button {
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border-radius: 50%;
    /* A fully transparent resting border makes the White swatch invisible against a light-mode
       card (both near-white) — a visible neutral border keeps every swatch legible regardless
       of theme or the swatch's own color. */
    border: 2px solid var(--color-border);
    background-image: none;
    cursor: pointer;
}

.color-swatch-picker button.active {
    border-color: var(--color-text);
}

.error {
    color: var(--color-danger);
    font-size: 0.9rem;
}

.success-text {
    color: var(--color-success);
    font-size: 0.9rem;
}

/* Dismissible connectivity banner (MainLayout.razor) — shown when the initial theme/language
   preferences fetch can't reach the API (see OnInitializedAsync's catch), so being offline is visible
   instead of silently falling back to defaults. Not the same as .page-error-boundary below: this is
   a thin top-of-content strip, the app shell still renders underneath it. */
.connectivity-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.6rem 1rem;
    margin-bottom: 1rem;
    border: 1px solid var(--color-warning);
    border-radius: var(--radius-control);
    background: var(--color-blog);
    color: var(--color-warning);
    font-size: 0.9rem;
}

.connectivity-banner button {
    background: none;
    background-image: none;
    padding: 0;
    color: inherit;
    font-weight: 400;
    box-shadow: none;
}

/* TrialBanner.razor (2026-08-17) — same shape as .connectivity-banner but an upsell nudge, not a
   fault, so it uses the solid brand accent rather than the warning color everywhere except its own
   "urgent" state (3 days or fewer left), where a real sense of urgency is the point. Solid, not
   gradient, since 2026-09-02 — see docs/plans/2026-09-02-remove-gradients-human-design.md. */
.trial-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 1rem;
    padding: 0.6rem 1rem;
    margin-bottom: 1rem;
    border-radius: var(--radius-control);
    background: var(--color-secondary);
    color: #FFFFFF;
    font-size: 0.88rem;
    font-weight: 600;
}

/* Real, confirmed live on a physical Android device (2026-08-23): this banner is the very first
   thing rendered inside .content, above @Body — the existing status-bar-clearance fix (see .content's
   own doc comment + the Fleet-specific note near the top of this file) only ever accounted for the
   case where the banner renders NOTHING (still occupying enough layout height via its own
   margin-bottom to incidentally push a page's <h1> clear of the status bar) — nobody had considered
   the banner actually rendering real, visible content of its own with zero clearance above it. Same
   fix shape already proven working for Quitaye Fleet's own titles: extra top margin on mobile only. */
@media (max-width: 860px) {
    .trial-banner,
    .connectivity-banner {
        margin-top: 2.5rem;
    }
}

.trial-banner.urgent {
    background: var(--color-danger);
}

.trial-banner a {
    color: #FFFFFF;
    text-decoration: underline;
    white-space: nowrap;
    font-weight: 700;
}

/* The offer-trial state's button + "Upgrade now" link, sitting beside each other on desktop and
   wrapping onto their own row on mobile via .trial-banner's own flex-wrap above. */
.trial-banner-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.trial-banner-actions button {
    background: #FFFFFF;
    color: var(--color-secondary);
    border: none;
    border-radius: var(--radius-control);
    padding: 0.4rem 0.9rem;
    font-weight: 700;
    font-size: 0.85rem;
    white-space: nowrap;
}

.trial-banner-actions button:disabled {
    opacity: 0.7;
}

/* Plans.razor's self-serve "start free trial" CTA (2026-08-15) — a bordered card, not the slim
   .trial-banner strip (that's a persistent countdown once a trial is already running; this is a
   one-time call to action to start one). */
.trial-cta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 1rem;
    padding: 1.25rem 1.5rem;
    margin-bottom: 1.5rem;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg, 12px);
}

.trial-cta button {
    background: var(--color-secondary);
    color: #FFFFFF;
    border: none;
    white-space: nowrap;
}

/* Global ErrorBoundary fallback (MainLayout.razor) — a page-level card shown in place of the whole
   routed page when an unprotected event handler throws, instead of the framework's #blazor-error-ui
   banner (which has no retry path short of a full reload). */
.page-error-boundary {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.85rem;
    max-width: 420px;
    margin: 3rem auto;
    padding: 1.5rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-blog);
    box-shadow: 0 4px 16px var(--color-card-shadow);
    text-align: start;
}

.page-error-boundary p {
    color: var(--color-danger);
    margin: 0;
}

/* PlanUpgradeCard.razor (2026-08-16) — the rich per-feature soft-paywall card ("names the feature,
   sells the outcome, offers an exit"), replacing the old plain-text .page-error-boundary.plan-upgrade
   fallback everywhere a PlanUpgradeRequiredException surfaces (MainLayout's ErrorBoundary,
   LoadErrorDisplay.razor wrapping a page's own _loadError). Deliberately reuses the app's own dark
   navy/purple language (--color-blog/--color-secondary), the same one-off dark-card precedent
   .chart-card already established for the Insights section, rather than the plain
   .page-error-boundary card it replaces — this is an upsell moment, not an error state, and reads
   better with its own distinct visual weight. */
.plan-upgrade-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.65rem;
    max-width: 420px;
    margin: 3rem auto;
    padding: 1.75rem;
    border: 1px solid color-mix(in srgb, var(--color-secondary) 35%, transparent);
    border-radius: var(--radius-control);
    background: var(--color-blog);
    box-shadow: 0 4px 16px var(--color-card-shadow);
    text-align: start;
}

.plan-upgrade-icon {
    width: 2.75rem;
    height: 2.75rem;
    border-radius: var(--radius-control);
    display: flex;
    align-items: center;
    justify-content: center;
    background: color-mix(in srgb, var(--color-secondary) 20%, transparent);
    color: var(--color-secondary);
    margin-bottom: 0.35rem;
}

.plan-upgrade-card h3 {
    margin: 0;
    font-size: 1.35rem;
    color: var(--color-text);
}

.plan-upgrade-tagline {
    margin: 0;
    font-weight: 600;
    color: var(--color-secondary);
}

.plan-upgrade-desc {
    margin: 0;
    color: var(--color-subtext);
    line-height: 1.5;
}

.plan-upgrade-checklist {
    list-style: none;
    margin: 0.25rem 0 0.5rem;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    width: 100%;
}

.plan-upgrade-checklist li {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    color: var(--color-text);
}

/* A plain ::before checkmark rather than an <Icon> — cheaper than wiring a new SVG glyph through
   Icon.razor just for this one list, and a logical-property-free single character mirrors correctly
   for RTL with zero extra work. */
.plan-upgrade-checklist li::before {
    content: "✓";
    color: var(--color-success);
    font-weight: 700;
    flex-shrink: 0;
}

.plan-upgrade-cta {
    background: var(--color-secondary);
    color: #FFFFFF;
    border: none;
    border-radius: var(--radius-pill);
    padding: 0.65rem 1.6rem;
    font-weight: 600;
    cursor: pointer;
}

.plan-upgrade-back {
    color: var(--color-subtext);
    font-size: 0.9rem;
}

/* Shared Spinner.razor — replaces the plain "Loading..." text every page's initial-load state used
   to show, so a slow/stuck fetch reads as "in progress" instead of looking like a frozen page.
   inline-flex, not flex: a plain flex box is block-level and stretches to its container's full
   width, so when this is the ONLY child of a <button> (Sales.razor's commit-button spinner), the
   ring stuck to the button's left edge instead of sitting where the button's own centered label
   text normally does — the button's default text-align:center only centers inline-level content.
   inline-flex shrink-wraps to the ring's own size and participates in that centering; it has no
   effect on the plain page-level "Loading..." usage (an inline box still starts at the left edge
   of its own line in this app's left-aligned page content, identical to the old block behavior). */
.spinner-row {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--color-subtext);
}

.spinner {
    width: 1.25rem;
    height: 1.25rem;
    flex-shrink: 0;
    border: 2px solid var(--color-border);
    border-top-color: var(--color-secondary);
    border-radius: 50%;
    animation: spinner-rotate 0.7s linear infinite;
}

@keyframes spinner-rotate {
    to {
        transform: rotate(360deg);
    }
}

/* Skeleton.razor (Phase 7, 2026-08-23) — shimmering placeholder blocks shaped like a .stat-card/
   .chart-card, shown while Home.razor/the 3 Insights pages load instead of a bare Spinner. Reuses
   .stat-card/.chart-card's own background/border/radius/shadow (see their own rules further down)
   so a skeleton card is already sized/positioned identically to the real one that replaces it. */
.skeleton-card {
    overflow: hidden;
    gap: 0.6rem;
}

.skeleton-line,
.skeleton-block {
    border-radius: 4px;
    background: linear-gradient(
        90deg,
        color-mix(in srgb, var(--color-border) 55%, transparent) 25%,
        color-mix(in srgb, var(--color-border) 25%, transparent) 37%,
        color-mix(in srgb, var(--color-border) 55%, transparent) 63%
    );
    background-size: 400% 100%;
    animation: skeleton-shimmer 1.4s ease infinite;
}

.skeleton-line-label {
    width: 55%;
    height: 0.75rem;
}

.skeleton-line-value {
    width: 75%;
    height: 1.5rem;
}

.skeleton-line-title {
    width: 40%;
    height: 0.85rem;
}

.skeleton-block {
    width: 100%;
    height: 140px;
}

@keyframes skeleton-shimmer {
    0% {
        background-position: 100% 0;
    }
    100% {
        background-position: 0 0;
    }
}

/* SuccessCheckmark.razor (Phase 7) — a brief animated checkmark shown right after a commit
   succeeds (Sale/Purchase/Stock Write-off/Purchase Order), replacing a flat confirmation line with
   the exact moment a user most wants reassurance something worked. Pure CSS stroke-dashoffset
   draw-in, plays once on mount — the consuming page must @key this component on something that
   changes per commit (e.g. the sale number) or the animation won't replay for a second success. */
.success-banner {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    animation: success-banner-in 0.2s ease-out;
}

@keyframes success-banner-in {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.success-checkmark {
    width: 28px;
    height: 28px;
    flex-shrink: 0;
}

.success-checkmark-circle {
    stroke: var(--color-success);
    stroke-width: 3;
    stroke-dasharray: 145;
    stroke-dashoffset: 145;
    animation: success-checkmark-draw 0.4s ease-out forwards;
}

.success-checkmark-check {
    stroke: var(--color-success);
    stroke-width: 4;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-dasharray: 36;
    stroke-dashoffset: 36;
    animation: success-checkmark-draw 0.3s ease-out 0.35s forwards;
}

@keyframes success-checkmark-draw {
    to {
        stroke-dashoffset: 0;
    }
}

.success-message {
    color: var(--color-text);
}

.success-message strong {
    color: var(--color-success);
}

/* MilestoneCelebration.razor (Phase 7) — a small, one-time celebratory toast for a genuine
   first-time milestone (first sale ever recorded, a free trial started). Deliberately low-key per
   the mega-backlog's own instruction ("small, not a full confetti-cannon") — a bordered card plus a
   handful of CSS-only falling dots, not an overlay/full-screen takeover. */
.milestone-celebration {
    position: fixed;
    top: 1rem;
    inset-inline-end: 1rem;
    z-index: 60;
    animation: milestone-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Same status-bar-clearance issue as .trial-banner/.connectivity-banner (see their own doc comment) —
   this is `position: fixed`, so it's pinned to the true screen edge regardless of scroll position or
   .content's own padding, sitting directly under the Android status bar on mobile. Confirmed live
   (2026-08-23): the "first sale ever" celebration toast fired on a real sale commit and overlapped the
   clock/battery icons exactly like the trial banner did. Same proven clearance value. */
@media (max-width: 860px) {
    .milestone-celebration {
        top: 3.5rem;
    }
}

@keyframes milestone-in {
    from {
        opacity: 0;
        transform: translateY(-12px) scale(0.96);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

.milestone-card {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    max-width: 320px;
    padding: 1rem 1.1rem;
    background: var(--color-blog);
    border: 1px solid color-mix(in srgb, var(--color-success) 45%, var(--color-border));
    border-radius: var(--radius-control);
    box-shadow: 0 8px 24px var(--color-card-shadow);
    overflow: hidden;
}

.milestone-icon {
    flex-shrink: 0;
    width: 2.25rem;
    height: 2.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: color-mix(in srgb, var(--color-success) 20%, transparent);
    color: var(--color-success);
}

.milestone-text {
    flex: 1;
    min-width: 0;
}

.milestone-text strong {
    display: block;
    color: var(--color-text);
}

.milestone-text p {
    margin: 0.2rem 0 0;
    color: var(--color-subtext);
    font-size: 0.88rem;
}

.milestone-dismiss {
    flex-shrink: 0;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    background-image: none;
    box-shadow: none;
    color: var(--color-subtext);
}

.milestone-dismiss .nav-icon-svg {
    margin: 0;
    width: 14px;
    height: 14px;
}

/* A handful of small dots drifting down + fading — genuinely "a handful," not a real particle
   system. Each dot gets its own angle/delay/color via a per-index nth-child rule rather than inline
   styles, so the whole burst is pure CSS. */
.milestone-confetti {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
}

.milestone-confetti-dot {
    position: absolute;
    top: -8px;
    width: 6px;
    height: 6px;
    border-radius: 1px;
    opacity: 0;
    animation: milestone-confetti-fall 1.1s ease-in forwards;
}

/* Only left position/color/animation-delay vary per dot — a static transform here would be
   pointless, the shared keyframe below already drives transform for the whole animation and
   would silently override any per-dot rotate set on the base rule. */
.milestone-confetti-dot.dot-0 { left: 8%; background: var(--color-success); animation-delay: 0ms; }
.milestone-confetti-dot.dot-1 { left: 18%; background: var(--color-secondary); animation-delay: 60ms; }
.milestone-confetti-dot.dot-2 { left: 28%; background: var(--color-warning); animation-delay: 120ms; }
.milestone-confetti-dot.dot-3 { left: 40%; background: var(--color-success); animation-delay: 40ms; }
.milestone-confetti-dot.dot-4 { left: 52%; background: var(--color-secondary); animation-delay: 160ms; }
.milestone-confetti-dot.dot-5 { left: 62%; background: var(--color-warning); animation-delay: 90ms; }
.milestone-confetti-dot.dot-6 { left: 72%; background: var(--color-success); animation-delay: 20ms; }
.milestone-confetti-dot.dot-7 { left: 82%; background: var(--color-secondary); animation-delay: 140ms; }
.milestone-confetti-dot.dot-8 { left: 90%; background: var(--color-warning); animation-delay: 70ms; }
.milestone-confetti-dot.dot-9 { left: 48%; background: var(--color-success); animation-delay: 180ms; }

@keyframes milestone-confetti-fall {
    0% {
        opacity: 1;
        transform: translateY(0) rotate(0deg);
    }
    100% {
        opacity: 0;
        transform: translateY(90px) rotate(180deg);
    }
}

/* PullToRefresh.razor — the pull-down-to-refresh indicator. Gesture math (transform/opacity) is
   driven directly by interop.js as the user drags, not through Blazor re-renders (a touchmove
   fires far too often for that) — this CSS only sets the resting state and the ptr-ready/
   ptr-spinning transitions. Fixed position + translateY(-100%) means it takes no layout space and
   is fully invisible until a real pull gesture starts, on any viewport width — no media-query gate
   needed, a mouse/trackpad simply never fires the touch events that would reveal it. */
.ptr-indicator {
    position: fixed;
    top: 0;
    /* Horizontally centered via a fixed left offset (half its own width), not a translateX
       transform — interop.js sets this element's transform to a bare translateY(...) directly
       while the user drags, which would otherwise clobber a translateX centering component. */
    left: calc(50% - 1.125rem);
    transform: translateY(-100%);
    z-index: 40;
    width: 2.25rem;
    height: 2.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    color: var(--color-subtext);
    opacity: 0;
    pointer-events: none;
    transition: transform 0.15s ease-out, opacity 0.15s ease-out, background 0.15s ease-out, color 0.15s ease-out;
}

.ptr-indicator.ptr-ready {
    color: var(--color-secondary);
    border-color: var(--color-secondary);
}

/* The spinning state swaps the static refresh-cw icon for a plain ring (the same shape
   Spinner.razor's own .spinner already uses) instead of rotating the icon glyph in place —
   refresh-cw's two offset arrow-arcs aren't radially symmetric, so spinning that glyph around its
   own box-center visibly "orbits"/wobbles rather than spinning cleanly. A perfect circle has no
   such asymmetry: it looks identical at every rotation angle, so it can never wobble. */
.ptr-spin-ring {
    display: none;
    width: 1.1rem;
    height: 1.1rem;
    border: 2px solid var(--color-border);
    border-top-color: var(--color-secondary);
    border-radius: 50%;
}

.ptr-indicator.ptr-spinning .nav-icon-svg {
    display: none;
}

.ptr-indicator.ptr-spinning .ptr-spin-ring {
    display: block;
    animation: spinner-rotate 0.7s linear infinite;
}

button {
    padding: 0.6rem 1.4rem;
    background: var(--color-secondary);
    color: white;
    border: none;
    border-radius: var(--radius-pill);
    cursor: pointer;
    font-weight: 600;
    transition: transform 0.1s ease, opacity 0.15s ease;
}

button:hover:not(:disabled) {
    opacity: 0.9;
}

button .nav-icon-svg {
    vertical-align: middle;
    margin-inline-end: 0.35rem;
    margin-bottom: 0.15rem;
}

button:active:not(:disabled) {
    transform: scale(0.98);
}

button:disabled {
    opacity: 0.5;
    cursor: default;
}

button.secondary {
    background: transparent;
    background-image: none;
    border: 1px solid var(--color-border);
    color: var(--color-text);
}

/* A plain button styled as inline text — used where a report's own drill-down affordance (e.g. a
   Sale Number opening its own items, see SaleReport.razor) sits inside an otherwise-plain table
   cell and shouldn't look like a pill-shaped action button. */
button.link-button {
    padding: 0;
    background: none;
    background-image: none;
    border: none;
    color: var(--color-text);
    font-weight: 600;
    text-decoration: underline;
    cursor: pointer;
}

button.link-button:hover:not(:disabled) {
    opacity: 0.75;
}

.oauth-divider {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 1.25rem 0 1rem;
    color: var(--color-subtext);
    font-size: 0.8rem;
}

.oauth-divider::before,
.oauth-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--color-border);
}

.oauth-buttons {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.oauth-buttons button {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
}

.oauth-icon {
    display: flex;
    flex: none;
}

table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    overflow: hidden;
}

table th, table td {
    text-align: left;
    padding: 0.6rem 0.85rem;
    border-bottom: 1px solid var(--color-border);
}

table tr:last-child td {
    border-bottom: none;
}

/* Column separators, desktop only — user feedback (2026-08-08): a wide, mostly-numeric report table
   (many adjacent .num columns with no visual break between them) made it hard to tell at a glance
   where one column ends and the next begins, even though rows already had their own border-bottom
   above. Gated to non-mobile widths only, same precedent as .row-actions above — the mobile
   card-table layout stacks each td as its own full-width block (see the "Opt-in mobile card layout"
   block below), where a vertical divider has no columns left to separate. Logical property
   (inline-end, not right) so it lands on the correct edge under this app's RTL/Arabic locale too. */
@media (min-width: 861px) {
    table th:not(:last-child), table td:not(:last-child) {
        border-inline-end: 1px solid var(--color-border);
    }
}

/* A checkbox-only column (row-select) otherwise gets the browser's default auto-width share of
   the table's 100% width, same as every text column — on a wide table with few narrow-content
   columns (e.g. AdvancedPricing's Barcode/Brand/Category/Size stock-item picker) this leaves the
   checkbox floating alone in a very wide cell, throwing off the whole header row's spacing.
   Targets every checkbox-only th/td in the app generically, no per-page markup change needed. */
table th:has(> input[type="checkbox"]),
table td:has(> input[type="checkbox"]) {
    width: 2.75rem;
    text-align: center;
}

/* Wraps every <table> in the app (many-column grids like Products/Stock don't fit a phone
   width) — scrolls the table horizontally within its own box instead of the table forcing the
   whole page wider or clipping columns off-screen (both observed live on a phone-width viewport
   before this existed). Desktop is unaffected since the table already fits at width:100% there. */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

@media (max-width: 860px) {
    .table-scroll table {
        width: auto;
        min-width: 100%;
    }
}

/* Opt-in mobile card layout for tables (add class="card-table") — replaces horizontal scroll
   with a stacked card per row below the mobile breakpoint, since a user often doesn't realize a
   table scrolls at all, and can't compare two values side by side while scrolling back and forth.
   Every <td> needs a data-label attribute (the desktop column header text) since the <thead> is
   hidden here. Secondary tds (add class="mobile-secondary") stay collapsed until the row's own
   toggle button adds .expanded to the <tr> — show only what identifies/lets you act on the row at
   a glance, put the rest behind one tap, per the "card + progressive disclosure" mobile-table
   pattern (see the ux-frontend-design skill's mobile-and-navigation.md). */
@media (max-width: 860px) {
    .table-scroll table.card-table,
    .table-scroll table.card-table tbody,
    .table-scroll table.card-table tr,
    .table-scroll table.card-table td {
        display: block;
        width: 100%;
    }

    /* The table itself stops being its own bordered/backgrounded box in card mode — only the
       per-row cards below get a border, otherwise every card sits nested inside one big outer
       card, which read as a double border rather than a list of distinct cards. */
    .table-scroll table.card-table {
        background: none;
        border: none;
        border-radius: 0;
    }

    .table-scroll table.card-table thead {
        display: none;
    }

    .table-scroll table.card-table tr {
        display: flex;
        flex-wrap: wrap;
        column-gap: 0.75rem;
        background: var(--color-blog);
        border: 1px solid var(--color-border);
        border-radius: var(--radius-control);
        margin-bottom: 0.75rem;
        padding: 0.6rem 0.85rem;
    }

    /* Higher specificity than the plain tr rule above (needs to be, not just later in source) since
       both target the same element — without this, a low-stock row's warning tint would be silently
       overridden by the flat card background instead of layering on top of it. */
    .table-scroll table.card-table tr.row-alert {
        background: color-mix(in srgb, var(--color-warning) 8%, var(--color-blog));
    }

    .table-scroll table.card-table td {
        display: flex;
        flex: 1 1 100%;
        min-width: 0;
        justify-content: space-between;
        /* flex-start, not center — a long value (e.g. a product name) wrapping to two lines must
           not pull its label down to align with the value's vertical midpoint; both need to start
           flush at the top of the row regardless of how many lines either side takes. */
        align-items: flex-start;
        /* A td's label/value/badge are separate flex items on one row by default (nowrap) — if an
           injected chip (e.g. a low-stock badge next to a quantity) doesn't fit alongside its
           siblings, nowrap forces it to overflow the td's own box with nothing to clip it, visually
           bleeding into whatever sits next in the DOM — for a .mobile-pair cell, that's the
           adjacent paired cell (e.g. Sell Price), which is exactly the overlap this fixes. Wrapping
           lets an overflowing item (the chip) drop to its own line and stay within the td's real
           width instead — a no-op for every td whose content already fits on one line. */
        flex-wrap: wrap;
        gap: 0.5rem;
        padding: 0.35rem 0;
        border-bottom: none;
    }

    .table-scroll table.card-table td::before {
        content: attr(data-label);
        font-weight: 600;
        color: var(--color-subtext);
        flex-shrink: 0;
    }

    .table-scroll table.card-table td:empty {
        display: none;
    }

    /* Opt-in: pair two adjacent <td>s onto one row instead of each getting its own full-width
       row — shortens the card for fields that don't need a whole line to themselves (e.g. Name
       + Category, or Quantity + Sell price). Add class="mobile-pair" to both tds in the pair;
       flex-wrap on the tr above naturally breaks to a new line once two ~50%-basis cells fill it,
       so an odd one out or a display:none'd sibling still lays out fine, just alone on its row. */
    .table-scroll table.card-table td.mobile-pair {
        flex: 1 1 calc(50% - 0.375rem);
    }

    /* Uneven pair split — e.g. Name next to Category — since a product name is typically longer
       than the value beside it. The two must sum to 100% (minus the shared gap) or the pair
       overflows the row and the "narrow" cell wraps onto the next line instead of sitting beside
       its "wide" partner — pair these two classes together, never mobile-pair-wide with a plain
       mobile-pair. */
    .table-scroll table.card-table td.mobile-pair-wide {
        flex: 1.5 1 calc(60% - 0.375rem);
    }

    .table-scroll table.card-table td.mobile-pair-narrow {
        flex: 1 1 calc(40% - 0.375rem);
    }

    .table-scroll table.card-table td.mobile-secondary {
        display: none;
    }

    .table-scroll table.card-table tr.expanded td.mobile-secondary {
        display: flex;
    }

    .table-scroll table.card-table td.mobile-toggle {
        justify-content: center;
        padding-top: 0.5rem;
    }

    .table-scroll table.card-table td.mobile-toggle::before {
        content: none;
    }

    .table-scroll table.card-table td.mobile-toggle button {
        background: none;
        color: var(--color-text);
        opacity: 0.7;
        padding: 0.2rem 0.6rem;
    }

    .table-scroll table.card-table td.mobile-toggle button .nav-icon-svg {
        margin-inline-end: 0;
    }

    .table-scroll table.card-table tr.expanded td.mobile-toggle .nav-icon-svg {
        transform: rotate(180deg);
        transition: transform 0.15s ease;
    }

    /* Press-and-hold feedback for LongPressTracker-driven rows (Sale/Purchase/Quote/Writeoff Report
       — see LongPressTracker.cs) — a brief scale + tint confirms the hold registered before the
       ~450ms threshold reveals the row's .mobile-secondary columns, same "background blur + slight
       zoom on the pressed element" convention the mobile-and-navigation skill describes for
       long-press. Applied to every card-table row unconditionally (harmless no-op for rows with no
       long-press wired up, since .pressing is only ever added by LongPressTracker) rather than
       trying to target only long-press rows with a selector — @onpointerdown is a Blazor event
       registration, not a literal DOM attribute, so it can't be matched with an attribute selector.
       touch-action:manipulation suppresses the double-tap-to-zoom delay that would otherwise make
       the hold feel laggy on a touch device. */
    .table-scroll table.card-table tr {
        touch-action: manipulation;
        user-select: none;
        -webkit-user-select: none;
        transition: transform 0.12s, background 0.12s;
    }

    .table-scroll table.card-table tr.pressing {
        transform: scale(0.98);
        background: color-mix(in srgb, var(--color-text) 8%, var(--color-blog));
    }
}

.toolbar {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1rem;
    align-items: end;
    flex-wrap: wrap;
}

/* BusinessPicker.razor's "choose a business" list (2026-08-11) — plain sibling <button>s have no
   margin of their own (see the base button rule below), so consecutive businesses rendered edge-
   to-edge with no breathing room between them. */
.business-picker-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    align-items: stretch;
    margin-bottom: 1rem;
}

.business-picker-list button {
    text-align: start;
}

/* .field's own margin-bottom (1rem, for stacked-form contexts) is part of the flex item's margin
   box, so align-items: end on .toolbar aligns to the bottom of that margin, not the visible
   input/select — a bare <button> sibling has no such margin, so it renders ~1rem lower than the
   field's actual control. Same fix as .quickadd-panel .field already applies for its own nested
   fields; this covers every top-level "field(s) + action button" toolbar (Add line, Save, Apply
   to selected, ...). */
.toolbar .field {
    margin-bottom: 0;
}

.stat-cards {
    display: flex;
    gap: 1rem;
    margin-bottom: 1.5rem;
    flex-wrap: wrap;
}

/* Mobile: cards vary in content length (a delta indicator, a "View details" link, neither) so
   plain flex-wrap lets same-row cards land at different natural heights with ragged bottom edges
   (reported by the user against a real Dashboard screenshot, 2026-07-30). A 2-column grid fixes
   this for free — grid's default align-items:stretch makes every card in a row match the row's
   tallest card, unlike flex-wrap where each wrapped line's items only stretch to their own line,
   not to a shared column. Falls back to 1-per-row on very narrow phones where 2 columns would
   squeeze the value text. */
@media (max-width: 860px) {
    .stat-cards {
        display: grid;
        /* minmax(0, 1fr), not a bare 1fr — a bare 1fr track's implicit minimum is auto, i.e. its
           content's own min-content size, so one long unbreakable number (a real bug found live:
           a 14-digit total) forces the whole grid wider than the viewport instead of wrapping/
           shrinking inside its own card. The overflow doesn't show as a scrollbar — the next card's
           own opaque background paints over it in DOM order, so it silently reads as the number
           being "cut off" a few digits in, not as a layout bug. minmax(0, 1fr) caps the track at 0,
           forcing content to wrap/overflow-wrap within its own card instead. */
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 0.75rem;
    }
}

@media (max-width: 320px) {
    .stat-cards {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Stat cards inside .sale-detail-modal (Sale Report's own detail dialog, Top Performers' drill-down)
   sit inside a narrower, padded modal — not a full-width report page — and .stat-card's base
   min-width:160px + 1rem/1.25rem padding were sized for the latter. On a genuinely narrow phone the
   modal's own content width (100vw - 2rem, minus the modal's own 1.25rem padding, minus the grid
   gap) can land close enough to 2×160px that the fixed min-width wins the fight against the grid's
   own minmax(0, 1fr) track sizing, pushing the row past the modal's right edge instead of shrinking
   to fit it. min-width:0 lets the track genuinely control the card's width; tighter padding reclaims
   room for the icon+label+value content at that smaller size. Scoped to this modal only — every
   other page's .stat-cards keeps its current (already-verified) sizing. */
@media (max-width: 860px) {
    .sale-detail-modal .stat-cards .stat-card,
    .fleet-drawer .stat-cards .stat-card {
        min-width: 0;
        padding: 0.75rem 0.85rem;
    }

    /* Explicit condition: the modal's card text must read smaller on mobile than it does on
       desktop, not just fit within a narrower card. Desktop keeps the base .stat-card sizing
       (1.5rem value / 0.8rem label, plenty of room in a 900px-capped modal) — this only applies
       below the breakpoint, for every card in the row, not just the CompactValue-flagged ones.
       Redefines --stat-value-base (not a plain font-size override) — see StatValueFontSize.cs's own
       2026-08-25 doc comment: a plain font-size here would lose to AbbreviatedValue's inline style
       regardless of this selector's own specificity, which is exactly the bug that fix closed. The
       custom-property redefinition is what CompactValue's separate, unconditional 1.15rem already
       matched by coincidence; both paths now genuinely share one source instead of two hardcoded
       numbers that only agreed by luck. */
    .sale-detail-modal .stat-cards .stat-card .value,
    .fleet-drawer .stat-cards .stat-card .value {
        --stat-value-base: 1.15rem;
    }

    .sale-detail-modal .stat-cards .stat-card .label,
    .fleet-drawer .stat-cards .stat-card .label {
        font-size: 0.7rem;
    }
}

/* Multi-widget report layout (Insights pages): several small tables side by side on wide
   screens, stacked on narrow ones — same auto-fit approach as .form-grid. */
.insights-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 1.5rem;
    margin-bottom: 1.5rem;
}

/* Custom inline-SVG charts (RankedBarChart/TrendColumnChart/DivergingColumnChart) — this app's
   first charting components, no JS/canvas library. Text stays on --color-text like every other
   element in this app (its own established look — see CLAUDE.md's Theming section), just dimmed
   for axis/gridline hierarchy instead of introducing a separate muted-ink token. */
.chart-scroll {
    overflow-x: auto;
    margin-bottom: 0.5rem;
}

.ranked-bar-chart,
.trend-column-chart,
.diverging-column-chart {
    width: 100%;
    max-width: 640px;
    height: auto;
    display: block;
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}

.chart-label,
.chart-value,
.chart-axis-label {
    fill: var(--color-text);
    font-size: 14px;
}

.chart-axis-label {
    opacity: 0.65;
}

.chart-value {
    font-variant-numeric: tabular-nums;
}

.chart-track {
    fill: var(--color-border);
}

.chart-gridline,
.chart-baseline {
    stroke: var(--color-border);
    stroke-width: 1;
}

.chart-baseline {
    stroke-opacity: 0.9;
}

.chart-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1rem;
    margin-bottom: 1rem;
    font-size: 0.85rem;
}

.chart-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--color-text);
}

.chart-legend-swatch {
    width: 10px;
    height: 10px;
    border-radius: 3px;
    display: inline-block;
}

/* TrendColumnChart/TrendAreaChart's hover tooltip (2026-08-25, quitayeInterop.chartTooltip) — ONE
   shared floating box, appended to <body> once and reused across every chart on the page, positioned
   via JS at the mouse/touch position. Same floating-popover visual language as .quickswitch (colors/
   border/shadow), position:fixed since it's positioned in raw viewport coordinates, not relative to
   any ancestor. pointer-events:none so it can never itself intercept the mousemove that's driving it. */
.chart-tooltip {
    position: fixed;
    z-index: 80;
    pointer-events: none;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 12px 28px -10px var(--color-card-shadow);
    padding: 0.5rem 0.7rem;
    font-size: 0.8rem;
    color: var(--color-text);
    max-width: 220px;
}

.chart-tooltip-title {
    font-weight: 600;
    margin-bottom: 0.2rem;
}

.chart-tooltip-line {
    display: flex;
    justify-content: space-between;
    gap: 0.6rem;
    white-space: nowrap;
}

/* Insights dashboard (KpiTile/ChartCard/PieChart) — previously a one-off dark navy/magenta
   gradient reproducing the legacy WinForms "Statistique" screens' exact look, always dark
   regardless of the app's own light/dark toggle. Retired 2026-08-17 per explicit user direction
   (part of the approved Insights redesign) — now the same theme-aware card styling every other
   report page's .stat-card already uses, so these three pages stop being a one-off exception.
   (The .legacy-gradient-card selector this rule used to also carry was dropped 2026-09-02 — it
   was already unreferenced by any markup, a stale name left over from that same 2026-08-17 pass.) */
.kpi-tile,
.chart-card {
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 4px 16px var(--color-card-shadow);
}

.kpi-row {
    display: flex;
    gap: 1.5rem;
    margin-bottom: 1.5rem;
    flex-wrap: wrap;
}

.kpi-tile {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1.25rem 1.5rem;
    min-width: 260px;
    flex: 1;
    position: relative;
}

.kpi-icon {
    color: var(--color-secondary);
    flex-shrink: 0;
}

.kpi-icon svg {
    width: 40px;
    height: 40px;
}

.kpi-body {
    flex: 1;
    min-width: 0;
}

.kpi-delta {
    position: absolute;
    top: 1rem;
    inset-inline-end: 1.25rem;
    display: inline-flex;
    align-items: center;
    gap: 0.2rem;
    font-size: 0.8rem;
    font-weight: 600;
}

.kpi-delta svg {
    width: 14px;
    height: 14px;
}

.kpi-delta-up {
    color: var(--color-success);
}

.kpi-delta-down {
    color: var(--color-danger);
}

.kpi-value {
    font-size: 1.6rem;
    font-weight: 700;
    color: var(--color-text);
    font-variant-numeric: tabular-nums;
}

.kpi-separator {
    border-block-start: 1px solid var(--color-border);
    margin: 0.5rem 0;
}

.kpi-caption {
    font-size: 0.85rem;
    color: var(--color-subtext);
}

/* Chart-card grid — 3-across on wide screens (matches legacy's tableLayoutPanel3/4 3-column chart
   rows), stacks to 1 on mobile. */
.chart-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1.5rem;
    margin-bottom: 1.5rem;
}

/* Priority-ordered Insights layout (2026-08-17, approved redesign) — the one card checked most
   often (the period's own trend) spans the full row instead of sharing the same footprint as a
   secondary card like By Hour. */
.chart-card-hero {
    grid-column: 1 / -1;
}

/* Gross Margin card content — a plain percentage + point delta, not a KpiTile: Current/Previous
   on MarginKpi are both already percentages, so KpiTile's own relative-%-change math would read as
   "percent change of a percentage," a confusing number for this one figure specifically (see
   SalesInsightsReportDto's own doc comment). */
.margin-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    min-height: 120px;
    gap: 0.35rem;
}

.margin-card-value {
    font-size: 1.8rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.margin-card-delta {
    font-size: 0.85rem;
    color: var(--color-subtext);
}

.chart-card {
    padding: 1rem;
}

.chart-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.5rem;
}

.chart-card-title {
    color: var(--color-text);
    font-size: 0.95rem;
    font-weight: 600;
}

/* Same subtle color-mix badge treatment .stat-card-icon-neutral already uses elsewhere, not a
   bespoke dark-card-only look. */
.chart-card-details-btn {
    background: color-mix(in srgb, var(--color-border) 45%, transparent);
    border: 1px solid var(--color-border);
    border-radius: 50%;
    color: var(--color-text);
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
}

.chart-card-details-btn svg {
    width: 16px;
    height: 16px;
}

/* Icon-only button — the generic button .nav-icon-svg rule adds margin-inline-end/margin-bottom
   sized for an icon sitting beside label text, which pushes a lone icon off-center inside this
   circular button. Same override precedent as .pos-cart-line-remove/.mobile-toggle button. */
.chart-card-details-btn .nav-icon-svg {
    margin: 0;
}

.chart-card-details-btn:hover {
    background: color-mix(in srgb, var(--color-border) 75%, transparent);
}

.chart-card-details {
    margin-top: 0.75rem;
    border-block-start: 1px solid var(--color-border);
    padding-top: 0.75rem;
    max-height: 260px;
    overflow-y: auto;
}

/* No .chart-card-scoped chart-label/chart-axis-label/chart-track/chart-gridline/chart-baseline/
   chart-legend-item overrides anymore — those base rules (above, ~line 2077) already use
   var(--color-text)/var(--color-border) and already render correctly for every OTHER chart in the
   app (Dashboard's trend chart, Top Performers' drill-down). They only needed overriding here
   because .chart-card used to be a fixed dark gradient no theme token could see through; now that
   it's the same var(--color-blog) surface as everywhere else, the base rules already work.
   Same reasoning removes the .chart-card .card-table override below — the real 2026-07-12 bug
   (a light table background stayed opaque over a dark card, since only text color was ever
   overridden) can't reproduce anymore: the table and its card now share the same background
   token by construction. */

.pie-chart {
    width: 100%;
    max-width: 220px;
    height: auto;
    display: block;
    margin-inline: auto;
}

.stat-card {
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    padding: 1rem 1.25rem;
    box-shadow: 0 4px 16px var(--color-card-shadow);
    min-width: 160px;
    display: flex;
    flex-direction: column;
}

/* When the grid above stretches a short card (e.g. Net Cash Flow, no trailing link) to match a
   taller sibling (e.g. Items Needing Restock, with a "View details" link), pin the link to the
   card's bottom instead of leaving it stranded right under the value with a dead gap beneath —
   makes the equalized height look intentional rather than just padded. */
.stat-card .subtext {
    margin-top: auto;
    margin-bottom: 0;
    padding-top: 0.5rem;
}

.stat-card .label {
    font-size: 0.8rem;
    color: var(--color-subtext);
}

.stat-card .value {
    /* --stat-value-base: the one shared source of truth StatValueFontSize.cs's inline font-size
       reads via var() — see that file's own doc comment (2026-08-25 fix) for why this exists: an
       inline style always beats an external stylesheet rule regardless of specificity, so a plain
       hardcoded font-size here could never actually be overridden by the mobile-scoped rule below;
       a custom property CAN, since var() resolution respects the normal cascade at the point it's
       read. Redefining --stat-value-base is now the only thing a narrower context (the mobile
       .sale-detail-modal/.fleet-drawer rule below) needs to do — no !important, no specificity race. */
    --stat-value-base: 1.5rem;
    font-size: var(--stat-value-base);
    font-weight: 700;
    /* Numbers never wrap onto a second line — AbbreviatedValue.razor/StatValueFontSize.cs shrink the
       inline font-size per-render once the displayed text is long enough to risk overflow, so the
       figure stays one unbroken line instead of breaking mid-digit. white-space/overflow/ellipsis
       here are a last-resort safety net (a plain, non-shrinking Value string, or a figure long enough
       that even the minimum shrunk size doesn't fit) independent of the grid-blowout fix above. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ReportStatCard's CompactValue opt-in (2026-08-09, Top Performers drill-down) — a plain Value
   string (e.g. a percentage) has no auto-shrink of its own the way RawValue's AbbreviatedValue path
   does, so a longer wrapped label above it left no vertical gap before the card's own bottom edge on
   a narrow mobile card. Smaller font + a real bottom margin, narrower max-width so it never claims
   more horizontal room than a short percentage figure actually needs. */
.stat-card .value.value-compact {
    font-size: 1.15rem;
    max-width: 6rem;
    margin-bottom: 0.35rem;
}

/* AbbreviatedValue.razor's own two elements (the tap-to-reveal button when a value is abbreviated,
   or a plain span otherwise) both carry an inline font-size StatValueFontSize.cs computes per-render
   — a real, legitimate change whenever the displayed text's length crosses a shrink threshold, or
   whenever --stat-value-base itself changes (e.g. this exact element remounting inside a narrower
   host, like Top Performers' drill-down modal, right after its own data finishes loading). Without a
   transition, a real size change here reads as an abrupt "reduce and pop back to normal" snap —
   reported live 2026-08-25 — rather than a smooth resize; this doesn't change WHEN the size changes,
   only how it's perceived when it does. */
.stat-value-toggle,
.stat-value-text {
    transition: font-size 0.15s ease;
}

/* AbbreviatedValue.razor's tap-to-reveal control — must look identical to the plain .value text it
   replaces (same font/color/size, no button chrome), while staying a real, focusable, tappable
   control rather than a styled <span> with a manual click handler. */
.stat-value-toggle {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    text-align: inherit;
    -webkit-tap-highlight-color: transparent;
}

.stat-value-toggle:hover,
.stat-value-toggle:focus-visible {
    text-decoration: underline dotted;
    text-underline-offset: 4px;
}

/* Alert-state stat card — for a headline number that means "something needs attention" (e.g. a
   count of unbalanced cash-register closings), not a neutral figure. Reuses --color-danger/-warning
   rather than a bespoke color, keeping the same semantic-color-only rule chips follow below. */
.stat-card.alert .value {
    color: var(--color-danger);
}

/* Period-over-period delta indicator on a stat-card (Dashboard's Sales tile) — same semantic-color
   rule as everywhere else: green = up, red = down, never a decorative/brand color. */
.stat-delta {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.85rem;
    font-weight: 600;
    margin-top: 0.25rem;
}

.stat-delta.up {
    color: var(--color-success);
}

.stat-delta.down {
    color: var(--color-danger);
}

.stat-delta .nav-icon-svg {
    width: 14px;
    height: 14px;
}

.text-danger {
    color: var(--color-danger);
}

/* Dashboard's two secondary panels (Account Balances / Top Products) — side by side on desktop,
   stacked on mobile, matching the same breakpoint every other responsive rule in this file uses. */
.dashboard-columns {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5rem;
    margin-top: 1rem;
}

@media (max-width: 860px) {
    .dashboard-columns {
        grid-template-columns: 1fr;
    }
}

/* Status chip — turns a bare enum-like value (paid/owed, balanced/unbalanced) into something
   scannable at a glance instead of a plain number a reader has to interpret themselves. One shared
   class for every report table that needs this, rather than a bespoke span per screen. */
.chip {
    display: inline-block;
    padding: 0.15rem 0.6rem;
    border-radius: var(--radius-pill);
    font-size: 0.8rem;
    font-weight: 600;
    white-space: nowrap;
    /* A chip is often a flex item itself (e.g. sitting beside a card-table td's ::before label) —
       flex items default to min-width:auto, i.e. never allowed to shrink below their own unwrapped
       text width, which is exactly what let a long chip (e.g. a category name) get hard-clipped at
       the card's edge instead of shrinking. min-width:0 lets it shrink to fit; max-width/overflow/
       text-overflow then turn "clipped mid-character" into a clean single-line ellipsis once it
       does. Inert everywhere the chip already fits (the overwhelmingly common case), so this is
       safe for every existing .chip usage across the app, not just this one cell. */
    min-width: 0;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chip-success {
    background: color-mix(in srgb, var(--color-success) 18%, transparent);
    color: var(--color-success);
}

.chip-danger {
    background: color-mix(in srgb, var(--color-danger) 18%, transparent);
    color: var(--color-danger);
}

.chip-warning {
    background: color-mix(in srgb, var(--color-warning) 20%, transparent);
    color: var(--color-warning);
}

.chip-neutral {
    background: color-mix(in srgb, var(--color-text) 12%, transparent);
    color: var(--color-text);
}

/* An icon sitting inline inside a .chip (e.g. a lock icon next to "Closed" text) needs to be much
   smaller than the default 1.1rem nav-icon-svg size or it blows out the chip's own line-height. */
.chip .nav-icon-svg {
    width: 0.8rem;
    height: 0.8rem;
    margin-inline-end: 0.25rem;
    vertical-align: -1px;
}

tr.row-alert {
    background: color-mix(in srgb, var(--color-warning) 8%, transparent);
}

/* Closed/locked row — a neutral "no further action possible" state, distinct from .row-alert (which
   stays a warning/needs-attention tint for its own other call sites, e.g. low-stock rows). Dimming
   the whole row communicates "inert" without borrowing the warning color for a non-warning state. */
tr.row-closed {
    opacity: 0.6;
}

.subtext {
    font-size: 0.85rem;
    color: var(--color-subtext);
    margin-top: -0.75rem;
    margin-bottom: 1rem;
}

/* Right-align numeric table columns — makes scanning/comparing amounts trivial (digits line up by
   place value) instead of ragged-left text. Apply via <td class="num"> on any report/table screen. */
td.num, th.num {
    text-align: end;
}

/* Reusable pagination control (Pager.razor) — Prev/Next either side of a "Page X of Y" status,
   same secondary-button treatment used elsewhere (Stock.razor's +/- adjust buttons). */
.pager {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-top: 1rem;
}

.pager-status {
    color: var(--color-subtext);
    font-size: 0.9rem;
}

.pager-range {
    color: var(--color-subtext);
    font-size: 0.85rem;
    margin-top: 0.25rem;
}

/* Sale screen ("point of sale") layout — a search-first product grid on the left, a persistent
   cart+payment panel on the right. Replaces the old single-row dropdown-form toolbar; this is the
   one screen a cashier lives in all day, so it gets a dedicated layout instead of reusing the
   generic admin-form .toolbar pattern every other CRUD page uses. */
.pos-layout {
    display: grid;
    grid-template-columns: 1fr 380px;
    gap: 1.5rem;
    align-items: start;
}

/* A CSS Grid item defaults to min-width:auto, same as a flex item — without this, the 1fr column
   (holding .pos-frequent-row's non-shrinkable tile row) grows to fit that row's full intrinsic
   width instead of staying at its 1fr track size, which makes .pos-frequent-row's own
   `max-width: 100%` a no-op (100% of an already-grown ancestor doesn't clip anything) and pushes
   the overflow onto the whole page instead of the tile row's own overflow-x:auto. Caught live
   verifying the "Frequently sold" tile row with 10+ tiles — HorizontalScrollBar.razor's own doc
   comment already flags this exact trap for flex ancestors; a grid item hits the identical default. */
.pos-layout > * {
    min-width: 0;
}

@media (max-width: 860px) {
    .pos-layout {
        grid-template-columns: 1fr;
    }
}

.pos-toolbar {
    display: flex;
    gap: 1rem;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

.pos-search {
    position: relative;
    flex: 1;
    min-width: 220px;
}

.pos-search input {
    width: 100%;
    height: 2.75rem;
    box-sizing: border-box;
    padding-inline-start: 2.5rem;
    padding-inline-end: 0.85rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-bg);
    color: var(--color-text);
    font: inherit;
}

.pos-search input:focus {
    outline: none;
    border-color: var(--color-secondary);
}

.pos-search .nav-icon-svg {
    position: absolute;
    inset-inline-start: 0.8rem;
    top: 50%;
    transform: translateY(-50%);
    opacity: 0.6;
    pointer-events: none;
    margin: 0;
}

/* Product cards: 44px+ tap targets throughout, one primary action (Add to Cart) revealed only on
   the selected card — progressive disclosure instead of cramming a qty stepper onto every tile. */
.pos-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: 0.85rem;
}

.pos-card {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    min-height: 112px;
    padding: 0.85rem;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    color: var(--color-text);
    text-align: start;
    cursor: pointer;
    transition: border-color 0.15s ease, transform 0.1s ease;
}

.pos-card:hover {
    border-color: var(--color-secondary);
}

.pos-card:active {
    transform: scale(0.98);
}

.pos-card.selected {
    border-color: var(--color-secondary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-secondary) 35%, transparent);
}

.pos-card-name {
    font-weight: 600;
    line-height: 1.25;
}

.pos-card-meta {
    font-size: 0.78rem;
    color: var(--color-subtext);
}

.pos-card-price {
    margin-top: auto;
    font-size: 1.1rem;
    font-weight: 700;
}

.pos-card-price.unavailable {
    font-size: 0.85rem;
    font-weight: 400;
    color: var(--color-subtext);
}

/* The expanded quantity/tier/add-to-cart controls, shown only inside the currently-selected card. */
.pos-card-expand {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-top: 0.5rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--color-border);
    /* Let taps inside the expanded controls (stepper, Add button) work without also re-triggering
       the card's own onclick toggle-select handler underneath them. */
    cursor: default;
}

.pos-qty-stepper {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.pos-qty-stepper button {
    width: 2.75rem;
    height: 2.75rem;
    flex-shrink: 0;
    padding: 0;
    font-size: 1.2rem;
    line-height: 1;
}

.pos-qty-stepper input {
    width: 100%;
    height: 2.75rem;
    box-sizing: border-box;
    text-align: center;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-bg);
    color: var(--color-text);
    font: inherit;
}

.pos-card-expand > button {
    width: 100%;
}

/* Phase 7 (2026-08-23): "a gentle bounce/scale on POS Add to cart — a natural extension of the
   tile-tap interaction that's already there" (.pos-card/.pos-tile's own :active scale, below). Two
   different transition timings per direction, not one: pressing down uses a snappy linear shrink
   (defined on the :active rule itself); releasing uses the base rule's slower, overshooting
   cubic-bezier easing — CSS resolves which `transition` applies from the element's *new* computed
   state, so press-down reads as a normal quick tap while release reads as a real settling bounce,
   with zero JS/state needed for either. */
.pos-add-to-cart-btn {
    transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.pos-add-to-cart-btn:active {
    transform: scale(0.9);
    transition: transform 0.06s ease-in;
}

/* 2026-08-20: compact modifier for .pos-qty-stepper when it sits inline inside a narrow
   .pos-cart-line-end (Requisitions.razor's inline cart-quantity editor), alongside the Remove
   button — the full 2.75rem/100%-width sizing above is tuned for the roomier card-expand form,
   not a tight row already sharing space with other controls. */
.pos-cart-line-qty {
    gap: 0.3rem;
    flex-shrink: 0;
}

.pos-cart-line-qty button {
    width: 1.75rem;
    height: 1.75rem;
    font-size: 0.85rem;
}

.pos-cart-line-qty input {
    width: 3.25rem;
    height: 1.75rem;
    font-size: 0.85rem;
    padding: 0;
}

.pos-empty {
    grid-column: 1 / -1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 3rem 1rem;
    color: var(--color-subtext);
    text-align: center;
}

.pos-empty .nav-icon-svg {
    width: 36px;
    height: 36px;
    opacity: 0.5;
    margin: 0;
}

/* "Frequently sold" quick-add tile row (Sales.razor) — fills the dead gap between the search box
   and the pre-search "search to find a product" placeholder. Compact tiles (name + price only) that
   expand in place (reusing .pos-card-expand, same as any selected .pos-card) once tapped, backed by
   HorizontalScrollBar.razor for scroll/drag. */
.pos-frequent {
    margin-bottom: 1rem;
}

.pos-frequent-title {
    margin: 0 0 0.5rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--color-subtext);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.pos-frequent-row {
    display: flex;
    gap: 0.75rem;
    padding-bottom: 0.5rem;
    /* Both this row's children (flex: 0 0 auto, non-shrinkable) and this row's own ancestor chain
       (.pos-layout's first grid-column item) share the same min-width:auto flex/grid-item default
       HorizontalScrollBar.razor's own doc comment warns about — an explicit max-width here is what
       actually lets overflow-x:auto (from .h-scroll-target) clip and scroll instead of silently
       growing the whole page. */
    max-width: 100%;
}

.pos-tile {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    flex: 0 0 auto;
    width: 150px;
    padding: 0.65rem;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    color: var(--color-text);
    text-align: start;
    cursor: pointer;
    transition: border-color 0.15s ease, transform 0.1s ease, width 0.15s ease;
}

.pos-tile:hover {
    border-color: var(--color-secondary);
}

.pos-tile:active {
    transform: scale(0.98);
}

.pos-tile.selected {
    border-color: var(--color-secondary);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-secondary) 35%, transparent);
    /* A selected tile shows the same expand controls a selected .pos-card does — a bit more width
       gives the tier <select>/qty stepper/Add button room without wrapping awkwardly. */
    width: 220px;
}

.pos-tile-name {
    font-weight: 600;
    font-size: 0.88rem;
    line-height: 1.25;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pos-tile-price {
    font-weight: 700;
}

.pos-tile-price.unavailable {
    font-size: 0.76rem;
    font-weight: 400;
    color: var(--color-subtext);
}

.pos-frequent-scrollbar {
    margin-top: 0.4rem;
}

/* The persistent cart + payment panel — sticky on desktop so it stays visible while the product
   grid scrolls; stacks below the catalog (no sticky) once the layout collapses to one column. */
.pos-cart {
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
    padding: 1.25rem;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 4px 16px var(--color-card-shadow);
    position: sticky;
    top: 1rem;
}

@media (max-width: 860px) {
    .pos-cart {
        position: static;
    }
}

.pos-cart-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
}

.pos-cart-header h2 {
    margin: 0;
    font-size: 1.1rem;
}

.pos-cart-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    max-height: 320px;
    overflow-y: auto;
}

.pos-cart-line {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 0.5rem;
    padding-bottom: 0.6rem;
    border-bottom: 1px solid var(--color-border);
}

.pos-cart-line:last-child {
    border-bottom: none;
    padding-bottom: 0;
}

.pos-cart-line-name {
    font-weight: 600;
}

.pos-cart-line-meta {
    font-size: 0.78rem;
    color: var(--color-subtext);
}

.pos-cart-line-end {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-shrink: 0;
}

.pos-cart-line-amount {
    font-weight: 700;
    white-space: nowrap;
}

.pos-cart-line-remove {
    background: none;
    background-image: none;
    border: none;
    padding: 0.3rem;
    color: var(--color-danger);
    flex-shrink: 0;
}

.pos-cart-line-remove .nav-icon-svg {
    width: 16px;
    height: 16px;
    margin: 0;
}

.pos-cart-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 2rem 1rem;
    color: var(--color-subtext);
    text-align: center;
}

.pos-cart-empty .nav-icon-svg {
    width: 32px;
    height: 32px;
    opacity: 0.5;
    margin: 0;
}

.pos-totals {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--color-border);
}

.pos-total-row {
    display: flex;
    justify-content: space-between;
    font-size: 0.95rem;
}

.pos-total-row.grand {
    font-size: 1.3rem;
    font-weight: 800;
}

.pos-cart-fields {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--color-border);
}

.pos-cart-fields .field {
    margin-bottom: 0;
}

.pos-cart-actions {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.pos-cart-actions button {
    width: 100%;
    padding: 0.9rem;
    font-size: 1.02rem;
}

/* ===== Hold/resume (2026-09-02, Sales.razor) — parks the current cart under a ticket number so a
   cashier can start a new sale for a different customer, then switch back. See SaleService's own
   HoldCurrentCartAsync/ListHeldTicketsAsync doc comments. ===== */

.pos-hold-btn {
    margin-inline-start: auto;
    padding: 0.35rem 0.75rem;
    font-size: 0.8rem;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.pos-hold-btn .nav-icon-svg {
    width: 14px;
    height: 14px;
    margin: 0;
}

.pos-active-ticket-banner {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    margin-bottom: 0.6rem;
    border-radius: var(--radius-control);
    background: color-mix(in srgb, var(--color-secondary) 12%, var(--color-blog));
    font-size: 0.85rem;
    font-weight: 600;
}

.pos-held-tickets {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-bottom: 0.75rem;
}

.pos-held-ticket-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    border: none;
    cursor: pointer;
}

.pos-held-ticket-discard {
    display: inline-flex;
    color: var(--color-danger);
}

.pos-held-ticket-discard .nav-icon-svg {
    width: 12px;
    height: 12px;
    margin: 0;
}

/* ===== Multi-tender split-tender payment (2026-09-02, Sales.razor) — a repeatable list of
   Payment Method + Amount Tendered pairs instead of one fixed pair. Reuses .pos-cart-fields' own
   spacing/border-top convention for the surrounding list, and .pos-cart-line-remove for the per-row
   remove button (same visual language, not a new one). ===== */

.pos-tender-lines {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--color-border);
}

.pos-tender-line {
    display: flex;
    align-items: flex-end;
    gap: 0.5rem;
}

.pos-tender-line .field {
    flex: 1;
    margin-bottom: 0;
    min-width: 0;
}

/* ===== Report filter bar (ReportFilterBar.razor) + report stat-card icon (ReportStatCard.razor) —
   Stage 1 of the report-screens redesign. Replaces the old always-visible .toolbar (search + several
   selects + date pickers + Filter/Export buttons) with a pill search box + date range + at most one
   primary dropdown up front, everything else (advanced/field-specific filters, Export Excel/PDF)
   tucked behind a kebab (⋯) overflow menu. Reuses the existing --color-* / --radius-* theme variables
   and the 2.75rem height/box-sizing convention every other .toolbar/.field control already follows —
   see the .toolbar > button / .field input / .combo-quickadd-toggle notes above for the class of bug
   (a control rendering shorter/lower than its siblings) this convention exists to avoid. ===== */

.filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: end;
    margin-bottom: 1rem;
}

/* DateRangePicker renders plain .field divs (label + input) straight into .filter-bar. .field's
   default margin-bottom: 1rem (meant for vertically-stacked forms) is invisible but still
   participates in align-items: end, so the date inputs' visible bottom edge sat 1rem above
   .filter-search/.filter-kebab's — same bug class as .toolbar .field / .quickadd-panel .field /
   .pos-cart-fields .field below, just never applied here. */
/* Same bug/fix as .toolbar .segmented-toggle above, just never applied to this second toolbar-like
   context: a SegmentedToggle inside a .filter-bar .field (e.g. Top Performers' Order toggle sitting
   beside its Count <input>) carries its own margin-bottom:1rem regardless of the field's own margin
   already being zeroed below — real, reported misalignment (2026-08-23), the toggle rendered visibly
   lower than its sibling input. */
.filter-bar .segmented-toggle {
    margin-bottom: 0;
}

.filter-bar .field {
    margin-bottom: 0;
    /* Every other PrimaryFilterContent in this app uses <input>/SegmentedToggle here (StockAlert's
       threshold number field, CategoryProfitability's toggle, ...), which have their own bounded
       intrinsic width regardless of container context. Products.razor's category filter is the
       first bare native <select> in this slot — under .field's own flex-direction:column (default
       align-items: stretch), a <select> stretches to fill .field's cross-axis width, and .field
       itself (no flex-grow/shrink set) was still sizing wider than intended once the filter bar's
       row had leftover space to give it. Pin it to content size explicitly rather than rely on
       flex defaults resolving the same way in every browser (found live 2026-08-17: it was visibly
       spanning the full bar width). */
    flex: 0 0 auto;
}

.filter-bar .field select {
    width: auto;
    min-width: 160px;
}

/* The Filter button rendered by ReportFilterBar.razor is a bare <button> direct child of .filter-bar
   with no rule of its own, falling back to the generic button { padding: 0.6rem 1.4rem } elsewhere in
   this file — rendering shorter than its siblings (.filter-search/.filter-kebab/date-range-pill, all
   an explicit 2.75rem). Same fix pattern already applied for .toolbar > button / .segmented-toggle
   above. */
.filter-bar > button {
    height: 2.75rem;
    box-sizing: border-box;
}

.filter-search {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: 1;
    min-width: 160px;
    height: 2.75rem;
    box-sizing: border-box;
    padding: 0 0.85rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    background: var(--color-bg);
    color: var(--color-subtext);
}

.filter-search input {
    flex: 1;
    height: 100%;
    border: none;
    background: transparent;
    color: var(--color-text);
    font: inherit;
    padding: 0;
}

.filter-search input:focus {
    outline: none;
}

/* Search gets its own full-width row on mobile, rather than sharing a flex-wrap row with whatever
   else happens to fit beside it (the Filter button, a PrimaryFilterContent dropdown, the kebab) — a
   deliberate layout call, not just a side effect of narrow width, per the mobile UI redesign request
   (2026-08-04): the single most-used control on a list page gets full-width prominence and a
   predictable position, rather than jostling for row space with less-frequent actions. flex-basis:
   100% on a flex-wrap item forces every subsequent item onto the next line for free, no JS needed. */
@media (max-width: 860px) {
    .filter-search {
        flex-basis: 100%;
    }
}

.filter-kebab-wrap {
    position: relative;
}

.filter-kebab {
    flex-shrink: 0;
    width: 2.75rem;
    height: 2.75rem;
    box-sizing: border-box;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-control);
}

/* Transparent click-catcher behind .filter-kebab-panel — closes the panel on an outside click.
   Fully transparent on purpose (a click-catcher, not a dimmer): no color/opacity, only exists/is
   clickable while the panel is open. z-index sits one below the panel's own (20) so the panel still
   renders on top and remains clickable/scrollable. */
.filter-kebab-overlay {
    position: fixed;
    inset: 0;
    z-index: 19;
    background: transparent;
    border: none;
    padding: 0;
}

/* position:fixed (not absolute) is load-bearing at every viewport width, not just mobile:
   ReportFilterBar.razor's OnAfterRenderAsync calls quitayeInterop.anchoredPanel.position()
   unconditionally on every open, no matchMedia/breakpoint check in the C# — it always measures the
   real button via getBoundingClientRect() (viewport-relative coordinates) and writes the result as
   inline style.left/top. Those only land in the right place under position:fixed, whose containing
   block is the viewport; under position:absolute they'd resolve against the nearest positioned
   ancestor (.filter-kebab-wrap) instead, silently drifting by however far that wrapper sits from the
   top-left corner of the page. top/left are intentionally left unset here (default auto) rather than
   given some other static value — the JS always overwrites both the instant the panel opens, so an
   initial resting position was never worth the complexity of also keeping it correct. */
.filter-kebab-panel {
    position: fixed;
    z-index: 20;
    min-width: 240px;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 8px 24px var(--color-card-shadow);
    padding: 0.85rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

/* Independently-scrollable sub-region for a long checkbox list inside a kebab panel (Top
   Performers' Nature filter, Tracabilité Actions' Nature+Origin filters) — without this, a long
   list and the panel's trailing Export Excel/PDF buttons scroll together as one blob (via
   .filter-kebab-panel's own outer max-height/overflow-y below), so reaching Export means first
   scrolling past every checkbox. Wrapping just the checkbox group(s) in this class keeps Export
   always visible/reachable on its own line beneath the list instead. `min(40vh, 16rem)` shrinks
   the cap on a short-height device automatically (real vh units, not a fixed px value or a
   width-based @media breakpoint — screen height and width are independent, this needs to react to
   the former) rather than needing its own breakpoint. The outer panel's own max-height/overflow-y
   stays as a last-resort safety net for the case where even the capped list plus every other
   sibling in the panel still doesn't fit a very short screen. */
.filter-kebab-scroll-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    max-height: min(40vh, 16rem);
    overflow-y: auto;
    padding-inline-end: 0.25rem;
}

/* Real bug found live (2026-08-05): without this, the checkbox chips inside the list above
   silently collapsed to ~5px tall the moment their combined natural height exceeded the list's own
   max-height (labels still fully present/correct in the DOM, just rendered unreadably squashed) —
   a genuine CSS Flexbox spec gotcha, not a sizing mistake: .chip has `overflow: hidden` (needed for
   its own, unrelated ellipsis-truncation behavior), and per spec, a flex item's automatic minimum
   size resolves to 0 instead of "don't shrink below content" the instant its own `overflow` is
   anything but `visible` — flex-shrink (default 1, since .chip never opts out) then happily
   squeezes it past its actual content height to force everything into max-height instead of
   scrolling. Plain `.filter-kebab-scroll-list span` labels ("Origines:" etc.) were never affected
   by this — no `overflow:hidden` on them, so their automatic minimum still protected their real
   content size — which is exactly why only the checkbox rows visually collapsed while the group
   headers stayed legible; a useful diagnostic trail, not a coincidence. flex-shrink:0 on every
   direct child opts them all out of shrinking entirely, so overflow genuinely scrolls instead. */
.filter-kebab-scroll-list > * {
    flex-shrink: 0;
}

/* DateRangePicker's compact pill trigger + popover — same toggle/panel shape as .filter-kebab-wrap
   above, just left-anchored (inset-inline-start) since this pill sits earlier in the filter row and
   a right-anchored panel would overflow off the left edge of the page on narrow viewports. */
.date-range-pill-wrap {
    position: relative;
}

.date-range-pill {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    height: 2.75rem;
    box-sizing: border-box;
    padding: 0 0.9rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    background: var(--color-bg);
    color: var(--color-text);
    font: inherit;
    font-size: 0.85rem;
    white-space: nowrap;
}

/* Transparent click-catcher behind .date-range-panel — closes the panel on an outside click, same
   shape as .filter-kebab-overlay above (z-index sits one below the panel's own so the panel still
   renders on top and remains clickable/scrollable). */
.date-range-overlay {
    position: fixed;
    inset: 0;
    z-index: 19;
    background: transparent;
    border: none;
    padding: 0;
}

.date-range-panel {
    position: absolute;
    top: calc(100% + 0.4rem);
    inset-inline-start: 0;
    z-index: 20;
    min-width: 260px;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 8px 24px var(--color-card-shadow);
    padding: 0.85rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.date-range-panel .field {
    margin-bottom: 0;
}

/* Real bug found live (2026-08-04, phone report): this block used to sit ABOVE the plain
   .filter-kebab-panel/.date-range-pill rules it's meant to override (right after .filter-search's
   own @media 860 block, before .filter-kebab-wrap even appears in the file) — since a @media block
   and a plain rule targeting the same selector have equal specificity, CSS resolves the tie by
   SOURCE ORDER, and the later (plain) rule always won regardless of viewport width. Confirmed via
   the WebView's own devtools: window.matchMedia('(max-width:480px)').matches was true, yet
   getComputedStyle(.filter-kebab-panel).position still read "absolute", not "fixed" — the media
   query matched but every property it set was silently overridden a moment later in the cascade.
   Moving the whole block down here (after every rule it overrides) is the actual fix; the CSS
   content itself was already correct, it just never took effect. On a narrow phone viewport, the
   date-range pill + Filter button together are also just wide enough to push the kebab trigger onto
   its own row even though the kebab itself is only 2.75rem/44px — the padding-shrink below buys back
   enough width to keep all three on one row, without ever shrinking below the 44px tap-target
   minimum. */
@media (max-width: 480px) {
    .filter-bar {
        gap: 0.4rem;
    }

    .filter-bar > button {
        padding: 0 1rem;
    }

    .date-range-pill {
        padding: 0 0.6rem;
        gap: 0.3rem;
        font-size: 0.8rem;
    }

    /* History, so a future pass doesn't re-walk the same dead ends: a static CSS anchor
       (inset-inline-end:0, then inset-inline-start:0) each only worked for one of "kebab lands near
       the left" vs. "lands near the right" depending on page layout, since a media query knows the
       viewport width but not where the button actually rendered. A screen-centered `position:fixed;
       top:50%; left:50%; transform:translate(-50%,-50%)` fixed that but was rejected on real
       hardware ("i want the menu to be near the kebab itself, not centered to the page"). The actual
       positioning is now owned entirely by quitayeInterop.anchoredPanel.position() (interop.js),
       invoked from ReportFilterBar.razor's OnAfterRenderAsync — it measures the real button and
       writes inline style.left/top, with position:fixed on the base .filter-kebab-panel rule above
       making those viewport-relative coordinates apply correctly. That JS never touches `transform`,
       so this media query must not set one either — a leftover `translate(-50%,-50%)` here (found
       live 2026-08-05, all 3 phone screenshots) silently shifted the JS's already-correct top-left
       corner by another half-width/half-height, shoving the panel off-screen on every report page,
       worse the taller the panel's own content (Tracabilité Actions' Nature/Origin checkboxes pushed
       it entirely above the viewport). What's left here is purely a narrow-screen sizing/scroll
       safety net, independent of wherever the JS decides to place it: cap the width so it can't get
       uncomfortably wide relative to a narrow phone, and cap the height with an internal scrollbar so
       a panel taller than the viewport (again, Tracabilité Actions) never gets clipped without a way
       to reach its lower content. */
    .filter-kebab-panel {
        width: min(300px, calc(100vw - 2rem));
        max-width: none;
        max-height: calc(100vh - 4rem);
        overflow-y: auto;
    }
}

/* Products.razor's per-row actions kebab (2026-08-08 row redesign). Originally plain
   position:absolute anchored to the row's own trigger (like .date-range-pill-wrap/.date-range-panel),
   on the reasoning that wiring up quitayeInterop.anchoredPanel's JS positioning per VISIBLE ROW
   (potentially 20 on a page) was unnecessary complexity a single page-level filter kebab never has to
   deal with. Real bug found live (2026-08-31): that reasoning missed that .table-scroll (the table's
   own wrapper) sets `overflow-x: auto`, which per the CSS overflow spec silently computes
   `overflow-y: auto` too — an author can't opt out of that pairing by writing `overflow-y: visible`
   explicitly, browsers still enforce it — so any row whose menu would extend past the table's bottom
   edge (most visibly the last row) got it clipped almost entirely off-screen. Only one row's menu is
   EVER open at once (Products.razor's own _openRowKebabId is a single nullable value, not a per-row
   flag), so the "20 rows" complexity concern never actually applied — the JS positioning call only
   ever runs once per open, exactly like .filter-kebab-panel's own single instance. Now uses
   quitayeInterop.anchoredPanel.position() the same way, which flips upward automatically when there's
   no room below and is immune to any scrollable ancestor since it's `position: fixed`. */
.row-kebab-wrap {
    position: relative;
    display: inline-block;
}

.row-kebab-btn {
    width: 2.25rem;
    height: 2.25rem;
    box-sizing: border-box;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-control);
}

.row-kebab-overlay {
    position: fixed;
    inset: 0;
    z-index: 19;
    background: transparent;
    border: none;
    padding: 0;
}

.row-kebab-menu {
    /* position:fixed (not absolute) is load-bearing: quitayeInterop.anchoredPanel.position() measures
       the real button via getBoundingClientRect() (viewport-relative) and writes the result as inline
       style.left/top — those only land correctly under position:fixed, whose containing block is the
       viewport. top/left are intentionally left unset (default auto): the JS always overwrites both
       the instant the panel opens. See the long comment on .row-kebab-wrap above for why this replaced
       the original position:absolute anchor. */
    position: fixed;
    z-index: 20;
    min-width: 13rem;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 8px 24px var(--color-card-shadow);
    padding: 0.35rem;
    display: flex;
    flex-direction: column;
    font-size: 0.85rem;
}

.row-kebab-menu button,
.row-kebab-menu a {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    width: 100%;
    background: none;
    border: none;
    text-align: start;
    padding: 0.55rem 0.6rem;
    border-radius: 8px;
    color: var(--color-text);
    text-decoration: none;
    font: inherit;
    cursor: pointer;
}

.row-kebab-menu button:hover,
.row-kebab-menu a:hover {
    background: var(--color-footer);
}

.row-kebab-menu button.danger {
    color: var(--color-danger);
}

.row-kebab-menu hr {
    border: none;
    border-top: 1px solid var(--color-border);
    margin: 0.3rem 0;
}

/* The Adjust Stock / Print Label detail panel a row's kebab opens (colspan'd full-width row right
   below it) — same "tinted footer strip" idea as .txn-detail on mobile, just for desktop's own
   card-table. Reuses .toolbar for its two control rows rather than inventing a new grid, since
   .toolbar already IS the app's shared "label + inputs + button in a row" shape. */
.expand-row td {
    padding: 0;
    border-bottom: 1px solid var(--color-border);
}

.expand-panel {
    padding: 1rem 1.1rem 1.1rem;
    background: color-mix(in srgb, var(--color-text) 4%, var(--color-footer));
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    position: relative;
}

.expand-panel h4 {
    margin: 0 0 0.4rem;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-subtext);
}

.expand-panel-close {
    position: absolute;
    top: 0.5rem;
    inset-inline-end: 0.5rem;
    width: 2rem;
    height: 2rem;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
}

.products-thumb-cell {
    width: 2.5rem;
}

.products-thumb {
    width: 40px;
    height: 40px;
    border-radius: 9px;
    object-fit: cover;
    display: block;
    background: var(--color-footer);
}

.products-item-cell {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    align-items: flex-start;
}

.products-item-name {
    font-weight: 600;
    color: var(--color-text);
}

.products-item-barcode {
    font-size: 0.76rem;
    color: var(--color-subtext);
    font-variant-numeric: tabular-nums;
}

.products-photo-picker {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    flex-wrap: wrap;
}

.products-photo-preview {
    width: 64px;
    height: 64px;
    border-radius: var(--radius-control);
    object-fit: cover;
    display: block;
    background: var(--color-footer);
    border: 1px solid var(--color-border);
}

.products-photo-actions {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

/* Row-hover-reveal for per-row table actions (e.g. a Delete button) — reduces visual noise on a
   dense report table. Gated to non-mobile widths only: this app's mobile card-table pattern already
   handles small screens differently (one card per row, not a hover-capable pointer device), so
   actions stay always-visible there instead of requiring a tap-to-reveal that doesn't exist. */
.row-actions {
    transition: opacity 0.12s;
}

@media (min-width: 861px) {
    .row-actions {
        opacity: 0;
    }

    tr:hover .row-actions {
        opacity: 1;
    }
}

/* Small icon badge inside a ReportStatCard — same visual language as KpiTile.razor's own .kpi-icon
   (icon beside the label/value, not above it) but using this app's normal light/dark theme vars
   instead of the Insights section's dark-navy palette. */
.stat-card-with-icon {
    flex-direction: row;
    align-items: center;
    gap: 1rem;
}

.stat-card-icon {
    flex-shrink: 0;
    color: var(--color-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
}

.stat-card-icon .nav-icon-svg {
    width: 20px;
    height: 20px;
    margin-inline-end: 0;
}

/* Tone variants — a circular colored badge behind the icon, tinted per what the KPI actually means
   (inflow/outflow/etc.), same color-mix(in srgb, var(--color-X) N%, transparent) pattern the
   .chip-* classes already use above, not a new hardcoded hex. Fixed diameter so every tone/icon
   combination reads as the same-sized badge regardless of which glyph is inside it. */
.stat-card-icon-accent,
.stat-card-icon-danger,
.stat-card-icon-success,
.stat-card-icon-warning,
.stat-card-icon-neutral {
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
}

.stat-card-icon-accent {
    background: color-mix(in srgb, var(--color-secondary) 18%, transparent);
    color: var(--color-secondary);
}

.stat-card-icon-danger {
    background: color-mix(in srgb, var(--color-danger) 18%, transparent);
    color: var(--color-danger);
}

.stat-card-icon-success {
    background: color-mix(in srgb, var(--color-success) 18%, transparent);
    color: var(--color-success);
}

.stat-card-icon-warning {
    background: color-mix(in srgb, var(--color-warning) 20%, transparent);
    color: var(--color-warning);
}

.stat-card-icon-neutral {
    background: color-mix(in srgb, var(--color-subtext) 18%, transparent);
    color: var(--color-subtext);
}

.stat-card-body {
    flex: 1;
    min-width: 0;
}

/* Optional sub-line under a ReportStatCard's value (ReportStatCard.razor's Sub parameter) — mirrors
   .fleet-kpi .sub's own sizing/color exactly, so a Fleet KPI card converted to ReportStatCard reads
   identically to before. */
.stat-card-sub {
    font-size: 0.76rem;
    color: var(--color-subtext);
    margin-top: 0.15rem;
}

/* Mobile: a real device screenshot showed even short figures ("263...", "5,000"'s siblings)
   ellipsis-clipped inside a ReportStatCard, not abbreviated — the row layout above reserves 2.25rem
   icon + 1rem gap (~52px) from an already-narrow 2-column mobile card, leaving too little width for
   the value to render in full even though it's well under AbbreviatedValue's 1,000,000 threshold (so
   it's a plain, non-tappable span — nothing to "reveal"). Stacking the icon above the label/value
   instead of beside it, same as Home.razor's own plain .stat-card already does on mobile, gives the
   number the card's full width back. */
@media (max-width: 860px) {
    .stat-card-with-icon {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.5rem;
    }

    .stat-card-icon-accent,
    .stat-card-icon-danger,
    .stat-card-icon-success,
    .stat-card-icon-warning,
    .stat-card-icon-neutral {
        width: 1.75rem;
        height: 1.75rem;
    }
}

/* Account page, Phase 2 — BottomSheet.razor (long-press action menu on a business row) + the
   press-and-hold feedback on the row that opens it + the profile/password/connected-logins card
   markup Account.razor adds this pass. Both the overlay and the sheet are always rendered (see
   BottomSheet.razor's own doc comment) and animate via the "open" class instead of an @if. */
.bottom-sheet-overlay {
    position: fixed;
    inset: 0;
    z-index: 49;
    background: rgba(0, 0, 0, 0.35);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
}

.bottom-sheet-overlay.open {
    opacity: 1;
    pointer-events: auto;
}

.bottom-sheet {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 50;
    background: var(--color-blog);
    border-top: 1px solid var(--color-border);
    border-radius: 16px 16px 0 0;
    box-shadow: 0 -18px 40px -14px var(--color-card-shadow);
    padding: 0.5rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
    max-width: 420px;
    margin-inline: auto;
    transform: translateY(100%);
    transition: transform 0.2s ease;
}

.bottom-sheet.open {
    transform: translateY(0);
}

.bottom-sheet-handle {
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--color-border);
    margin: 0.25rem auto 0.75rem;
}

.bottom-sheet-title {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-subtext);
    padding: 0 0.25rem 0.5rem;
}

.bottom-sheet-body {
    display: flex;
    flex-direction: column;
}

.bottom-sheet-action {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    width: 100%;
    padding: 0.75rem 0.5rem;
    background: transparent;
    background-image: none;
    color: var(--color-text);
    font-size: 0.9rem;
    text-align: start;
    border-radius: 10px;
}

.bottom-sheet-action:hover:not(:disabled) {
    background: rgba(146, 12, 255, 0.08);
}

.bottom-sheet-action:disabled {
    opacity: 0.5;
}

.bottom-sheet-action.danger {
    color: var(--color-danger);
}

/* ConfirmDialog.razor — in-app replacement for the native browser confirm() (2026-08-02), used by
   every delete/void/leave/close action across the app. Centered modal, not a bottom sheet (unlike
   BottomSheet.razor above) — a yes/no question reads naturally centered, and this needs to work
   identically on desktop, where an off-canvas bottom sheet would look out of place. Both overlay
   and dialog are always rendered and animate via the "open" class, same precedent. */
.confirm-dialog-overlay {
    position: fixed;
    inset: 0;
    z-index: 59;
    background: rgba(0, 0, 0, 0.45);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
}

.confirm-dialog-overlay.open {
    opacity: 1;
    pointer-events: auto;
}

.confirm-dialog {
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 60;
    width: min(360px, calc(100vw - 2rem));
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 18px 40px -14px var(--color-card-shadow);
    padding: 1.5rem;
    opacity: 0;
    pointer-events: none;
    transform: translate(-50%, -50%) scale(0.95);
    transition: opacity 0.15s ease, transform 0.15s ease;
}

.confirm-dialog.open {
    opacity: 1;
    pointer-events: auto;
    transform: translate(-50%, -50%) scale(1);
}

.confirm-dialog-message {
    margin: 0 0 1.25rem;
    color: var(--color-text);
    line-height: 1.5;
}

.confirm-dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.6rem;
}

.confirm-dialog-actions button.danger {
    background: var(--color-danger);
    background-image: none;
}

/* Press-and-hold feedback for LongPressTracker-driven rows outside a card-table (the .biz-row long-
   press that opens the sheet above) — same "brief scale + tint" convention already established for
   card-table rows (see the earlier .table-scroll table.card-table tr.pressing rule) but not gated to
   that mobile-only context, since this row's long-press is meant to work on both desktop and mobile
   (a mouse-hold fires the identical pointer events a touch-hold does). */
.pressable {
    touch-action: manipulation;
    user-select: none;
    -webkit-user-select: none;
    transition: transform 0.12s, background 0.12s;
}

.pressable.pressing {
    transform: scale(0.98);
    background: color-mix(in srgb, var(--color-text) 8%, var(--color-blog));
}

/* Account page profile-name inline edit + change-password form + connected-logins list. */
.profile-name-edit {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    min-width: 0;
    max-width: 100%;
}

/* The name-edit <input> has no explicit width by default, so on a narrow viewport it (plus the
   two buttons beside it) forced .profile-name-edit past the card's own edge — same fix shape as
   .profile-info above, applied one level deeper. */
.profile-name-edit input {
    flex: 1 1 8rem;
    min-width: 0;
}

.profile-edit-btn {
    background: transparent;
    background-image: none;
    color: var(--color-subtext);
    padding: 0.2rem;
}

.profile-edit-btn:hover {
    color: var(--color-text);
}

.connected-login-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--color-border);
}

.connected-login-row:last-child {
    border-bottom: none;
}

/* ============================================================
   HorizontalScrollBar.razor — a custom, "more native"-looking horizontal scrollbar (a dark rounded
   pill/capsule containing a lighter inset track, a solid rounded-rectangle thumb, and a triangular
   arrow button at each end) replacing the bare native browser scrollbar. Standalone/reusable —
   nothing wires this into a real page yet.

   Pill vs. track lightness: rather than compare two named tokens' absolute lightness (which flips
   direction between the two themes — --color-footer is lighter than --color-bg in dark mode but
   darker in light mode), the inset track always mixes a slice of --color-text (the theme's bright
   accent) into the pill's own background — guaranteed to read as "lighter/brighter than the pill"
   in both themes without hardcoding a theme-specific relationship. The thumb uses the app's own
   solid --color-secondary (the same brand purple every primary button already uses — was the
   cyan->purple gradient sweep before 2026-09-02's removal, see docs/plans/2026-09-02-remove-
   gradients-human-design.md), which reads clearly "lighter" than the dark track/pill in both
   themes and ties the control back to the app's one existing brand identity instead of inventing
   a new accent color for it.

   RTL: every offset below is a logical property (inset-inline-start/end, border-inline-start/end,
   never left/right) — same precedent .pos-search's icon and this app's report kebab already
   established — so both the triangle glyphs and the thumb's own position mirror automatically for
   Arabic with zero extra C#/JS branching; HorizontalScrollBar.razor's C# only needs to know direction
   for the numeric scrollLeft sign convention (RTL's negative-scrollLeft model), not for layout. */
.h-scroll-target {
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.h-scroll-target::-webkit-scrollbar {
    display: none;
}

.h-scrollbar {
    display: flex;
    align-items: center;
    width: 100%;
    height: 2.75rem;
    box-sizing: border-box;
    padding: 0.25rem;
    background: var(--color-footer);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
}

.h-scrollbar-arrow {
    flex: 0 0 auto;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    background-image: none;
    border: none;
    border-radius: var(--radius-pill);
    color: var(--color-text);
    cursor: pointer;
    transition: background 0.12s ease, opacity 0.12s ease;
}

.h-scrollbar-arrow:hover:not(:disabled) {
    background: color-mix(in srgb, var(--color-text) 14%, transparent);
}

.h-scrollbar-arrow:active:not(:disabled) {
    background: color-mix(in srgb, var(--color-text) 22%, transparent);
}

.h-scrollbar-arrow:disabled {
    opacity: 0.3;
    cursor: default;
}

.h-scrollbar-triangle {
    display: inline-block;
    width: 0;
    height: 0;
    border-top: 6px solid transparent;
    border-bottom: 6px solid transparent;
}

/* "start" points toward the reading-start edge (left in LTR, mirrors to right in RTL automatically
   via the logical border-inline-* property), "end" points the opposite way. */
.h-scrollbar-triangle-start {
    border-inline-end: 8px solid currentColor;
}

.h-scrollbar-triangle-end {
    border-inline-start: 8px solid currentColor;
}

.h-scrollbar-track {
    position: relative;
    flex: 1;
    min-width: 0;
    height: 1.5rem;
    margin: 0 0.15rem;
    background: color-mix(in srgb, var(--color-text) 10%, var(--color-footer));
    border-radius: var(--radius-pill);
    /* Prevents the browser's own touch-scroll/pan gesture from fighting a touch-drag of the thumb. */
    touch-action: none;
}

.h-scrollbar-thumb {
    position: absolute;
    top: 0.2rem;
    bottom: 0.2rem;
    min-width: 24px;
    background: var(--color-secondary);
    border-radius: 6px;
    cursor: grab;
    box-shadow: 0 1px 3px var(--color-card-shadow);
}

.h-scrollbar-thumb:active {
    cursor: grabbing;
}

/* "Stacked transaction list" — a mobile-only alternative to the generic card-table pattern (see the
   `@media (max-width: 860px)` block above), for pages whose rows represent a dated financial
   transaction. First applied to Sale Report per the mobile-UI design-review proposal
   (2026-08-02, artifact 7a9ca104): date-group headers replace a per-row Date field entirely, each
   row is a 2-line summary (name+amount, reference+time+status chip) behind a leading avatar-style
   initial swatch (reuses the app's existing `.avatar.sq` — same solid-fill/contrast handling as the
   Account page's business-row badges, deliberately NOT a per-row color rotation, to stay consistent
   with that single-fixed-color convention), with a visible chevron as the reveal affordance
   instead of an invisible long-press. The consuming page renders BOTH its desktop
   <table class="card-table"> and this list unconditionally — give the table's own wrapper the
   `.txn-desktop-table` class to hide it below the mobile breakpoint; `.txn-list` is display:none by
   default and only shows there, mirroring the exact hidden-by-default/shown-in-media-query
   convention `.mobile-tabbar`/`.sidebar-close` already use elsewhere in this file. */
.txn-list {
    display: none;
}

@media (max-width: 860px) {
    .txn-desktop-table {
        display: none;
    }

    .txn-list {
        display: block;
    }
}

/* Desktop-list opt-in (2026-08-09) — for Insights' "Recent Activity"/"Daily Trend" sections
   specifically: the user asked those two to use the same list/card style on desktop too, not just
   mobile, instead of the old plain <table>. Rather than widen the general mobile-only rule above
   (which every other .txn-list page still relies on), this is a second, higher-specificity class a
   page opts a `.txn-list` into — `.txn-list.txn-list-desktop` always wins over the bare `.txn-list`
   rule regardless of viewport, so these sections render as a list everywhere and never need the
   `.txn-desktop-table` wrapper at all (the consuming page should drop that wrapper entirely here,
   not render both). */
.txn-list.txn-list-desktop {
    display: block;
}

.txn-date-header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.5rem;
    margin: 1rem 0 0.4rem;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--color-subtext);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.txn-date-header:first-child {
    margin-top: 0;
}

/* Real gap found and fixed (2026-08-08): the date header showed only the date, with the day's own
   total nowhere in sight until every one of that day's rows was scanned by eye — the exact same
   "subtotal next to the group key" fact .report-group-row/.txn-group-header-total already surface
   for transaction-number grouping. Not uppercase/letter-spaced like the date label — it's a real
   number, not a section label. */
.txn-date-header-total {
    text-transform: none;
    letter-spacing: normal;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Transaction-number grouping (2026-08) — Sale/Purchase/Quote/Writeoff Report group their line items
   by their own transaction number (matching legacy Rapport Vente/Rapport Achat's own concept, which
   is a real per-transaction grouping there too — just done via a "blank the repeated number" DataGrid
   trick with no dedicated header row) instead of showing every line as an unrelated-looking flat row.
   Desktop: a full-width .report-group-row <tr> (colspan across every real column) sits above that
   group's own line <tr>s, inside the existing <table class="card-table">. Mobile: .txn-group-header
   replaces .txn-date-header as the .txn-list's own group marker — same "number / date / author on
   one side, subtotal on the other" facts as the desktop version, styled as a small card (reusing
   .txn-row's border/radius/background tokens) so it reads as part of the same list, not a foreign
   element dropped on top of it. */
.report-group-row td {
    background: var(--color-blog);
    font-weight: 600;
    padding: 0.6rem 0.9rem;
}

.report-group-row-content {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.3rem 1rem;
}

.report-group-meta {
    font-weight: 400;
    color: var(--color-subtext);
    font-size: 0.82rem;
}

.report-group-total {
    margin-inline-start: auto;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Two-level grouping (2026-08-09) — Sale/Purchase/Quote/Writeoff/Stock-Transfer Report and Payment
   Method Analysis all group first by calendar date (most-recent-day-first, matching every other
   report page's own date-grouping convention), then by their own transaction number/mode underneath
   each date. The outer date row/header reuses the plain .report-group-row/.txn-date-header exactly as
   every single-level date-grouped report already does; the inner (per-transaction/per-mode) row/header
   gets this .nested modifier instead — same info shape, styled lighter and indented so the two levels
   read as a real hierarchy rather than two identical bold bars stacked directly on top of each other. */
.report-group-row.nested td {
    background: transparent;
    font-weight: 500;
    padding-inline-start: 1.8rem;
    border-bottom: 1px dashed var(--color-border);
}

.report-group-row.nested .report-group-number {
    font-weight: 600;
}

.txn-group-header.nested {
    background: transparent;
    border-style: dashed;
    margin-inline-start: 0.75rem;
    padding: 0.4rem 0.65rem;
}

.txn-group-header {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-blog);
    padding: 0.5rem 0.75rem;
    margin: 1rem 0 0.5rem;
}

.txn-group-header:first-child {
    margin-top: 0;
}

.txn-group-header-top {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.5rem;
}

.txn-group-header-title {
    font-weight: 700;
    font-size: 0.86rem;
}

.txn-group-header-total {
    font-weight: 700;
    font-size: 0.86rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.txn-group-header-meta {
    margin-top: 0.15rem;
    font-size: 0.72rem;
    color: var(--color-subtext);
}

/* Month/Year period totals (2026-09-02, Sale Report) — a third and fourth grouping tier ABOVE the
   existing day-level .report-group-row/.txn-date-header: the report shows a running month total the
   instant the last day-with-data of that month is reached (scanning newest-first, that's the FIRST
   day-group encountered for that month — see SaleReport.razor's own MonthBoundaryDates doc comment),
   and a year total the same way at year boundaries. Reuses the day-level markup shapes verbatim (same
   .report-group-row/.txn-date-header DOM), just a modifier class one tier more prominent than the day
   header it sits above — not new markup, since this really is "one more grouping level," not a
   different kind of thing.

   Year used to jump straight to the full gradient sweep (removed 2026-09-02, see docs/plans/
   2026-09-02-remove-gradients-human-design.md) while month used a light tint — the one gradient
   consumer in the app that was genuinely data-adjacent rather than pure decoration, so it couldn't
   just go flat like everything else without losing the day<month<year weight signal. Solved by
   intensity instead: month keeps its existing light tint, year is now the SOLID brand purple
   (--color-secondary is identical in both themes, so this is the one color guaranteed to stay
   legible with white text regardless of theme — a color-mix tint strong enough to read as
   "prominent" would have gone too pale against light mode's near-white --color-blog to keep white
   text readable). The hierarchy still reads clearly: plain day header < month's tint < year's
   solid fill. */
.report-group-row.period-month td {
    background: color-mix(in srgb, var(--color-secondary) 16%, var(--color-blog));
    font-weight: 700;
}

.report-group-row.period-year td {
    background: var(--color-secondary);
    color: #fff;
    font-weight: 800;
}

.report-group-row.period-year .report-group-meta,
.report-group-row.period-year .report-group-total {
    color: #fff;
}

.txn-date-header.period-month {
    margin-top: 1.5rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--radius-control);
    background: color-mix(in srgb, var(--color-secondary) 16%, var(--color-blog));
    font-size: 0.92rem;
    font-weight: 700;
    color: var(--color-text);
    text-transform: none;
    letter-spacing: normal;
}

.txn-date-header.period-year {
    margin-top: 1.75rem;
    padding: 0.6rem 0.85rem;
    border-radius: var(--radius-control);
    background: var(--color-secondary);
    font-size: 0.98rem;
    font-weight: 800;
    color: #fff;
    text-transform: none;
    letter-spacing: normal;
}

.txn-date-header.period-month:first-child,
.txn-date-header.period-year:first-child {
    margin-top: 0;
}

.txn-row {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-blog);
    margin-bottom: 0.6rem;
    overflow: hidden;
}

/* Same neutral "inert, no further action possible" dimming tr.row-closed already applies to the
   desktop table's own <tr> — kept visually consistent between the two presentations. */
.txn-row.row-closed {
    opacity: 0.6;
}

.txn-main {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.6rem 0.75rem;
    cursor: pointer;
}

.txn-body {
    flex: 1;
    min-width: 0;
}

.txn-line1 {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.5rem;
}

.txn-title {
    font-weight: 600;
    font-size: 0.88rem;
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.txn-amount {
    font-weight: 700;
    font-size: 0.88rem;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    /* Real bug found live (2026-08-09): unlike .txn-title/.txn-meta right above/below, this had no
       explicit color at all — normally a harmless inherit (every other page's ambient text color is
       already this same value), except inside .chart-card (the Insights section's dark-navy one-off,
       see the .chart-card .card-table fix above for the same root cause on a different element),
       whose own ambient text color is white. A bare, unmodified .txn-amount (no .pos/.credit/.loss —
       PurchaseInsights.razor's Daily Trend detail is the one real case, since a purchase has no
       Paid/Credit concept to color it by) then rendered white-on-white against its own light
       .txn-row card background — present in the DOM (confirmed live) but completely invisible. */
    color: var(--color-text);
}

/* "pos" (a real inflow, i.e. a fully-paid line) vs "credit" (still owed) — same green/amber
   semantic pairing the app already uses for Paid/Credit everywhere else (Sales Insights' own
   pie-chart legend, this same report's other columns). "loss" is a third tone added for Writeoff
   Report — always a stock reduction, never a Paid/Credit-style status, so red (matching that
   report's own ReportStatCard IconTone="danger") reads more accurately than reusing "credit"'s
   amber, which specifically means "still owed," not "gone." */
.txn-amount.pos {
    color: var(--color-success);
}

.txn-amount.credit {
    color: var(--color-warning);
}

.txn-amount.loss {
    color: var(--color-danger);
}

/* Profitability.razor's what-if calculator (2026-08-17 redesign) — one bordered card per row
   instead of a raw table, since the old layout stacked a free-text search input directly on top
   of its own <select> inside one table cell, a genuinely awkward double-control shape once the
   collapsible "New X" treatment made room to do better. Reuses .pos-qty-stepper (Purchases.razor's
   own quantity stepper) for Quantity and the same pos/loss color language .txn-amount already
   established, applied here to a label+value row instead of a single inline amount. */
.calc-row-card {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    padding: 1rem 1.15rem;
    margin-bottom: 0.85rem;
    background: var(--color-bg);
}

.calc-row-top {
    display: flex;
    align-items: flex-end;
    gap: 0.6rem;
}

.calc-row-product {
    flex: 1;
    min-width: 0;
}

.calc-remove-btn {
    background: transparent;
    background-image: none;
    color: var(--color-subtext);
    height: 2.75rem;
    width: 2.75rem;
    flex-shrink: 0;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.calc-remove-btn:hover {
    color: var(--color-danger);
}

.calc-row-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 0.75rem;
    margin-top: 0.75rem;
}

.calc-readonly-value {
    display: flex;
    align-items: center;
    height: 2.75rem;
    padding: 0 0.85rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-blog);
    color: var(--color-subtext);
    font-variant-numeric: tabular-nums;
}

.calc-row-result {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 0.85rem;
    padding-top: 0.75rem;
    border-top: 1px dashed var(--color-border);
    font-weight: 600;
}

.calc-gain-value {
    font-size: 1.15rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.calc-row-result.pos .calc-gain-value {
    color: var(--color-success);
}

.calc-row-result.loss .calc-gain-value {
    color: var(--color-danger);
}

.calc-total-bar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 1.1rem;
    padding: 0.85rem 1.15rem;
    border-radius: var(--radius-control);
    background: var(--color-blog);
    font-weight: 700;
}

.calc-total-bar.pos .calc-gain-value {
    color: var(--color-success);
}

.calc-total-bar.loss .calc-gain-value {
    color: var(--color-danger);
}

.txn-line2 {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.15rem;
}

.txn-meta {
    color: var(--color-subtext);
    font-size: 0.72rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* min-width:0 — a flex item's automatic minimum size already collapses to 0 once it has
       overflow:hidden (per spec), so this line isn't load-bearing for Chrome/WebView today, but
       makes the intent explicit rather than relying on that implicit exception. */
    min-width: 0;
}

/* A status chip (e.g. "Paid in Full") sharing .txn-line2 with .txn-meta must never shrink/ellipsize
   — a truncated chip ("Paid i…") is illegible, unlike a truncated cashier-name-and-date, which is
   still recoverable from the tap-to-expand detail panel. flex-shrink:0 keeps the chip at its full
   content width no matter how little room is left; .txn-meta (already shrinkable) absorbs the
   squeeze instead. Scoped to .txn-line2 rather than added to the shared .chip rule, since chips
   elsewhere (e.g. a long category name on a Products card) are deliberately allowed to shrink. */
.txn-line2 .chip {
    flex-shrink: 0;
}

.txn-chevron {
    flex: none;
    display: flex;
    color: var(--color-subtext);
    transition: transform 0.15s ease;
}

/* The chevron glyph is vertically symmetric (its SVG points are mirrored about its own vertical
   center), so rotating it 180deg is visually identical to a horizontal flip — this makes it point
   toward the RTL "forward" direction (left) at rest without needing a second scaleX(-1) transform
   composed with the expanded-state rotate() below (composing the two would require reasoning about
   transform-function application order; a single absolute rotate() per state sidesteps that
   entirely). Specificity note: `.txn-row.expanded .txn-chevron` (3 class-level selectors) is more
   specific than `.txn-chevron:dir(rtl)` (2) and so already wins whenever a row is both expanded and
   RTL — no dir(rtl)-scoped override of the expanded rule is needed, since "pointing down" for an
   expanded/open row reads the same regardless of text direction. */
.txn-chevron:dir(rtl) {
    transform: rotate(180deg);
}

.txn-row.expanded .txn-chevron {
    transform: rotate(90deg);
}

.txn-detail {
    padding: 0 0.75rem 0.7rem;
    border-top: 1px solid var(--color-border);
    margin-top: 0.1rem;
    padding-top: 0.55rem;
}

.txn-detail-row {
    display: flex;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.2rem 0;
    font-size: 0.78rem;
}

.txn-detail-row span:first-child {
    color: var(--color-subtext);
    font-weight: 600;
    flex-shrink: 0;
}

/* A section of the mobile tap-detail panel whose content is more than one short value (a whole
   sub-form, a toolbar of controls) doesn't fit .txn-detail-row's label/value flex shape — that
   shape assumes exactly one short label and one short value side by side, and a wider "value"
   drifts into a lopsided stack instead. .detail-card gives that kind of section its own bordered,
   self-contained panel sized to its real content instead of fighting the row shape. */
.detail-card {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-blog);
    padding: 0.65rem 0.75rem 0.72rem;
    margin: 0.4rem 0;
}

.detail-card.compact {
    padding: 0.55rem 0.75rem;
}

.detail-card h4 {
    margin: 0 0 0.55rem;
    font-size: 0.78rem;
    font-weight: 700;
    color: var(--color-subtext);
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.detail-card.compact h4 {
    margin-bottom: 0;
}

.detail-card h4 .swatch {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--color-secondary);
    flex: none;
}

.detail-card .chip-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
}

.txn-detail-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.4rem;
}

.txn-detail-actions button {
    flex: 1 1 auto;
}

/* The expiration-batches link is a secondary, occasional action (most StockItems have no batches
   yet) riding along the same row as the two primary buttons — always giving it its own full-width
   line keeps it from squeezing into whatever space the buttons leave over and running off-screen. */
.txn-detail-actions a {
    flex: 1 1 100%;
    text-align: center;
}

.txn-edit-form {
    padding: 0.7rem 0.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.txn-edit-form label {
    font-size: 0.76rem;
    font-weight: 600;
    color: var(--color-subtext);
}

/* Barcode camera-scan (Phase 0, 2026-08) — the scan button sits absolutely-positioned inside an
   input that already has room reserved for it, same "icon overlaying a relative wrapper" shape
   .pos-search already used for its own (non-clickable) leading search icon, just at the trailing
   edge and interactive here. Reused by both Products.razor's Barcode field and Sales.razor's search
   box, not a one-off per page. */
.field-scan-wrap {
    position: relative;
}

.field-scan-wrap input {
    padding-inline-end: 2.75rem;
}

.field-scan-btn {
    position: absolute;
    inset-inline-end: 0.25rem;
    top: 50%;
    transform: translateY(-50%);
    height: 2.25rem;
    width: 2.25rem;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    background-image: none;
    border: none;
    padding: 0;
    color: var(--color-text);
    opacity: 0.7;
    cursor: pointer;
}

.field-scan-btn:hover {
    opacity: 1;
}

.field-scan-btn .nav-icon-svg {
    margin: 0;
}

/* The scanner modal itself — same fixed-overlay + centered-panel shape as .confirm-dialog (reuses
   its .confirm-dialog-overlay class directly, see BarcodeScannerModal.razor), just wider/taller to
   hold a live video feed instead of a short message. */
.barcode-scanner-modal {
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 60;
    width: min(420px, calc(100vw - 2rem));
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 18px 40px -14px var(--color-card-shadow);
    padding: 1rem;
    opacity: 0;
    pointer-events: none;
    transform: translate(-50%, -50%) scale(0.95);
    transition: opacity 0.15s ease, transform 0.15s ease;
}

.barcode-scanner-modal.open {
    opacity: 1;
    pointer-events: auto;
    transform: translate(-50%, -50%) scale(1);
}

/* Sale Report's detail dialog (2026-08-08, Details_Vente parity) — same fixed-overlay + centered-panel
   shape as .confirm-dialog/.barcode-scanner-modal (reuses .confirm-dialog-overlay directly), wider
   still to hold a line-item table, stat cards, and an Add Payment form. Height-capped with its own
   scroll, same precedent as .filter-kebab-panel, since the combined content can exceed the viewport
   on a phone. */
.sale-detail-modal {
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 60;
    width: min(900px, calc(100vw - 2rem));
    max-height: calc(100vh - 4rem);
    overflow-y: auto;
    /* Real root cause of the Top Performers drill-down "loads everything at once" bug (2026-08-25,
       found by live CDP tracing — see TopPerformers.razor's DisplayedRows doc comment for the full
       history): Chromium's scroll anchoring (on by default) auto-adjusts scrollTop whenever content
       is inserted ABOVE the current viewport to keep whatever was visible still visible. The
       incremental-reveal table/list inserts each new 200-row chunk directly above its own sentinel
       element — if the sentinel was near the bottom of the viewport (the common case: the user
       scrolled down, or just switched Day/Month/Year with the modal still scrolled from before, which
       clamps scrollTop down near the new, much-shorter content's own bottom), scroll anchoring keeps
       re-pinning the sentinel back into view after every chunk loads, so IntersectionObserver's "fires
       once immediately on observe() with the current intersection state" behavior keeps re-triggering
       LoadMoreRows in a tight, user-input-free cascade until the whole dataset is loaded. Disabling
       anchoring on this container removes the auto-repin entirely — LoadMoreRows only fires again once
       the user genuinely scrolls the sentinel back into range. */
    overflow-anchor: none;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    box-shadow: 0 18px 40px -14px var(--color-card-shadow);
    padding: 1.25rem;
    opacity: 0;
    pointer-events: none;
    transform: translate(-50%, -50%) scale(0.95);
    transition: opacity 0.15s ease, transform 0.15s ease;
}

.sale-detail-modal.open {
    opacity: 1;
    pointer-events: auto;
    transform: translate(-50%, -50%) scale(1);
}

.sale-detail-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1rem;
}

.sale-detail-modal-header h2 {
    margin: 0;
}

/* Top Performers' item/category drill-down (.sale-detail-modal, reused) — the toggle/stat-cards/
   charts sections had no breathing room of their own on a narrow phone, on top of the two trend
   charts being a hard-coded 2-column grid with no mobile override, squeezing each chart into an
   unreadable sliver. Own margins per section instead of relying on each element's own default
   spacing, and the chart pair stacks to one column below the same 860px breakpoint every other
   mobile layout in this app already uses. */
.topperf-detail-toggle {
    margin: 0 0 1.25rem;
}

.topperf-detail-stats {
    margin-bottom: 1.5rem;
}

.topperf-detail-charts {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.25rem;
    margin-bottom: 1.25rem;
}

.topperf-detail-charts h3 {
    margin: 0 0 0.5rem;
}

@media (max-width: 860px) {
    .topperf-detail-charts {
        grid-template-columns: 1fr;
        gap: 1.75rem;
    }
}

.barcode-scanner-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-weight: 600;
    color: var(--color-text);
    margin-bottom: 0.75rem;
}

.barcode-scanner-close {
    background: transparent;
    background-image: none;
    border: none;
    padding: 0.25rem;
    color: var(--color-text);
    opacity: 0.7;
    cursor: pointer;
}

.barcode-scanner-close:hover {
    opacity: 1;
}

.barcode-scanner-video {
    width: 100%;
    aspect-ratio: 4 / 3;
    background: #000;
    border-radius: var(--radius-control);
    object-fit: cover;
}

.barcode-scanner-hint,
.barcode-scanner-message {
    margin: 0.75rem 0 0;
    color: var(--color-subtext);
    font-size: 0.85rem;
    text-align: center;
}

/* Required-field marker — a small asterisk appended to a <label>'s text on any form field that's
   actually validated as required (e.g. Employees.razor's SaveEmployeeAsync). Reusable anywhere a
   form mixes required and optional fields with no other visual distinction. */
.required-mark {
    color: var(--color-danger);
    margin-inline-start: 0.15rem;
}

/* Cash Register (CashRegister.razor) — a big, always-tappable "just open it" primary action, since
   opening the register needs no input at all by default (the opening amount is an optional,
   collapsed-by-default CollapsibleSection below it, not a precondition). Full-width on mobile so
   it reads as the obvious single next step; back to a normal inline button on desktop, where a
   full-width CTA would look oversized next to a short page. */
.open-register-cta {
    display: block;
    width: 100%;
    max-width: 22rem;
    margin: 0 0 1.5rem;
    padding: 0.9rem 1.5rem;
    font-size: 1.05rem;
    font-weight: 600;
}

@media (max-width: 480px) {
    .open-register-cta {
        max-width: none;
    }
}

/* The per-denomination Quantity <input type="number"> lives directly inside a card-table cell, not
   inside a .field — so it never picked up .field input's theming (border/height/background) and had
   no width constraint at all, letting it render at the browser's unstyled default size and overflow
   its mobile card. Matches .field input's own visual language exactly, just scoped to this one
   table-embedded input rather than nested in a form field. Un-gated by any media query — the same
   lack of theming/width existed on desktop too, just less visibly. */
.denomination-qty-input {
    width: 100%;
    max-width: 7rem;
    height: 2.5rem;
    box-sizing: border-box;
    padding: 0.4rem 0.6rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-bg);
    color: var(--color-text);
    font: inherit;
}

.denomination-qty-input:focus {
    outline: none;
    border-color: var(--color-secondary);
}

/* Plans.razor (2026-08-10) — an informational plan-comparison grid, not a stat-card dashboard, so it
   gets its own card shape rather than reusing .stat-cards/.kpi-tile. */
.plans-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 1rem;
    margin: 1.5rem 0;
}

.plan-card {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 1.25rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-blog);
    box-shadow: 0 4px 16px var(--color-card-shadow);
}

.plan-card h2 {
    margin: 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* The recommended tier (Growth, 2026-08-11 — every business starts Free, so this is the one card
   worth pulling the eye toward instead of five neutral, equal-weight options). */
.plan-card.reco {
    border-color: var(--color-secondary);
    background: color-mix(in srgb, var(--color-secondary) 10%, var(--color-blog));
    box-shadow: 0 6px 20px color-mix(in srgb, var(--color-secondary) 25%, transparent);
}

.plan-reco-badge {
    background: var(--color-secondary);
    color: var(--color-bg);
}

.plan-tagline {
    margin: 0;
    color: var(--color-subtext);
    font-size: 0.85rem;
}

.plan-price {
    margin: 0.15rem 0 0;
    font-size: 1.6rem;
    font-weight: 700;
}

.plan-price small {
    font-size: 0.85rem;
    font-weight: 400;
    color: var(--color-subtext);
}

.plan-price-sub {
    margin: 0;
    font-size: 0.72rem;
    color: var(--color-subtext);
}

.plan-capacity {
    margin: 0.4rem 0 0;
    font-weight: 600;
}

.plan-everything {
    margin: 0.25rem 0 0;
    font-size: 0.85rem;
    color: var(--color-subtext);
}

.plan-card ul {
    margin: 0.25rem 0 0;
    padding-inline-start: 1.1rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.plan-contact {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
    max-width: 420px;
    margin: 2rem 0;
    padding: 1.5rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-blog);
}

.plan-contact p {
    margin: 0;
}

/* ============================================================
   Onboarding.razor (large-feedback-batch Phase J) — reuses the
   same card/button/chip vocabulary as .login-card/.plan-card
   rather than inventing a new visual language, per ux-frontend-
   design's own "one component vocabulary" default.
   ============================================================ */
.onboarding-shell {
    position: relative;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 2rem 1.25rem;
    box-sizing: border-box;
}

.onboarding-skip {
    position: absolute;
    top: 1.25rem;
    inset-inline-end: 1.25rem;
    opacity: 0.75;
}

/* Mobile WebViews (Android especially) don't reliably report env(safe-area-inset-top), so the
   button sits right under the status bar unless pushed down explicitly — same fix shape already
   proven for Quitaye Fleet's own page titles (2026-08-18). */
@media (max-width: 860px) {
    .onboarding-skip {
        top: 3rem;
    }
}

.onboarding-skip:hover {
    opacity: 1;
}

.onboarding-progress {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
}

.onboarding-progress-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--color-border);
    transition: background 0.25s ease;
}

.onboarding-progress-dot.done {
    background: var(--color-secondary);
}

.onboarding-card {
    width: 100%;
    max-width: 460px;
    padding: 2rem;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg, 12px);
    box-shadow: 0 8px 24px var(--color-card-shadow);
    box-sizing: border-box;
    text-align: center;
}

/* Hero heading gets the gradient accent, matching .login-card h1 */
.onboarding-card h1 {
    display: inline-block;
    background: var(--gradient-text);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    margin-top: 0;
}

.onboarding-card h2 {
    color: var(--color-text);
    margin-top: 0;
}

.onboarding-card .hint {
    text-align: center;
}

.onboarding-welcome-list {
    list-style: none;
    margin: 0 0 1.5rem;
    padding: 0;
    display: inline-flex;
    flex-direction: column;
    gap: 0.6rem;
    text-align: start;
}

.onboarding-welcome-list li {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--color-subtext);
    font-size: 0.95rem;
}

.onboarding-welcome-list li::before {
    content: "✓";
    color: var(--color-text);
    font-weight: 700;
    flex-shrink: 0;
}

.onboarding-logo-reveal {
    display: flex;
    justify-content: center;
    margin-bottom: 1.25rem;
}

.onboarding-logo-reveal img {
    width: 96px;
    height: 96px;
    object-fit: contain;
    animation: onboarding-logo-in 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes onboarding-logo-in {
    from { opacity: 0; transform: scale(0.6); }
    to { opacity: 1; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
    .onboarding-logo-reveal img {
        animation: none;
    }
}

.onboarding-primary {
    width: 100%;
    background: var(--color-secondary);
    color: #FFFFFF;
    border: none;
    height: 2.75rem;
    box-sizing: border-box;
    font-weight: 600;
    margin-top: 0.5rem;
}

.onboarding-primary:disabled {
    opacity: 0.7;
}

.onboarding-question-label {
    text-align: start;
    font-weight: 600;
    margin: 1.25rem 0 0.5rem;
}

/* Bigger, friendlier touch targets than .chip's report-filter-sized pills — this is a one-time
   choice screen, not a dense toolbar, so each option gets real room (min 44px tall, per this app's
   own mobile tap-target rule) and a real hover/selected/focus state, not just an active-color swap. */
.onboarding-choice-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 0.65rem;
    margin-bottom: 0.5rem;
}

.onboarding-choice {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 3rem;
    padding: 0.75rem 1rem;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    background: var(--color-bg);
    color: var(--color-text);
    text-align: start;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
}

.onboarding-choice:hover {
    border-color: var(--color-secondary);
}

.onboarding-choice:has(input:focus-visible) {
    outline: 2px solid var(--color-secondary);
    outline-offset: 2px;
}

.onboarding-choice.selected {
    border-color: var(--color-secondary);
    background: color-mix(in srgb, var(--color-secondary) 12%, var(--color-bg));
}

.onboarding-choice input[type="checkbox"] {
    /* The real checkbox stays in the DOM (native keyboard/a11y semantics) but visually hidden — the
       whole card is the toggle, matching .segmented-toggle's own "button IS the control" convention. */
    position: absolute;
    opacity: 0;
    width: 1px;
    height: 1px;
}

.onboarding-actions {
    display: flex;
    gap: 0.75rem;
    margin-top: 1.5rem;
}

.onboarding-actions button {
    flex: 1;
}

.onboarding-plan-chip {
    margin-bottom: 0.5rem;
}

/* Discover.razor's recommendation screen (2026-08-30): the specific matched plan is deliberately
   never shown pre-signup — this replaces the plan badge/title/reason with a generic "it's ready,
   sign up to see it" teaser instead. */
.onboarding-lock-teaser {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
}

.onboarding-lock-teaser svg {
    width: 28px;
    height: 28px;
    color: var(--color-secondary);
}

.onboarding-lock-teaser h2 {
    margin: 0;
}

.onboarding-help-list {
    list-style: none;
    margin: 0 0 1.25rem;
    padding: 0;
    text-align: start;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.onboarding-help-list li {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    font-size: 0.9rem;
    color: var(--color-subtext);
}

.onboarding-upgrade-note,
.onboarding-contact-note {
    font-size: 0.8rem;
    margin-top: 1rem;
}

/* The one-line relief-from-pain payoff tied to the tracking-method question (step 2) — sits right
   under the plan reason, so it reads as a second, smaller "here's what changes for you" beat
   before the longer bulleted help list below it. */
.onboarding-tracking-reassurance {
    color: var(--color-secondary);
    font-weight: 600;
    margin-top: -0.5rem;
}

/* OnboardingChecklistCard.razor — a persistent, collapsible Dashboard widget, not a one-time modal
   (see the component's own doc comment for why). Reuses .trial-cta's card shape. */
.onboarding-checklist {
    margin-bottom: 1.5rem;
    padding: 1rem 1.25rem;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg, 12px);
}

.onboarding-checklist-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    background: transparent;
    border: none;
    color: var(--color-text);
    font-weight: 600;
    padding: 0;
}

.onboarding-checklist-header:hover {
    opacity: 0.85;
}

.onboarding-checklist-count {
    font-weight: 400;
    color: var(--color-subtext);
}

.onboarding-checklist-items {
    list-style: none;
    margin: 1rem 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.onboarding-checklist-items li {
    display: flex;
    align-items: center;
    gap: 0.65rem;
}

.onboarding-checklist-check {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.35rem;
    height: 1.35rem;
    flex-shrink: 0;
    border: 1px solid var(--color-border);
    border-radius: 50%;
    font-size: 0.75rem;
    color: var(--color-success);
}

.onboarding-checklist-items li.done .onboarding-checklist-check {
    border-color: var(--color-success);
    background: color-mix(in srgb, var(--color-success) 15%, transparent);
}

.onboarding-checklist-items li.done a {
    color: var(--color-subtext);
    text-decoration: line-through;
}

@media (max-width: 480px) {
    .onboarding-card {
        padding: 1.25rem;
    }

    .onboarding-choice-grid {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
   Quitaye Fleet — Overview page (2026-08-17 redesign, approved
   mockup): KPI row + accounts table + slide-in detail drawer.
   Reuses this app's own tokens throughout, no invented palette.
   ============================================================ */
.fleet-kpis {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0.9rem;
    margin-bottom: 1.75rem;
}

.fleet-kpi {
    position: relative;
    overflow: hidden;
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    padding: 1rem 1.1rem;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.fleet-kpi::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
}

.fleet-kpi.ok::before { background: var(--color-success); }
.fleet-kpi.warn::before { background: var(--color-warning); }
.fleet-kpi.risk::before { background: var(--color-danger); }
.fleet-kpi.neutral::before { background: var(--color-text); }

.fleet-kpi .label {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--color-subtext);
}

.fleet-kpi .value {
    font-size: 1.6rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.fleet-kpi .sub {
    font-size: 0.76rem;
    color: var(--color-subtext);
}

.fleet-panel {
    background: var(--color-blog);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    overflow: hidden;
    margin-bottom: 1.75rem;
}

/* New campaign page (2026-08-18) — the hint text/name field/source-tabs/segment field/template
   row/schedule-tabs/submit button all stacked with no breathing room between them. Scoped to this
   one page (not a general .fleet-panel > * rule) since the other 5 .fleet-panel consumers don't
   share this "many distinct form steps in one panel" shape and shouldn't be affected. Padding on the
   panel itself too — .fleet-panel has none of its own, content was flush against the card edges. */
.new-campaign-panel {
    padding: 1.5rem;
}

.new-campaign-panel > * + * {
    margin-top: 1.5rem;
}

.fleet-panel-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 1.25rem;
    border-bottom: 1px solid var(--color-border);
    flex-wrap: wrap;
    gap: 0.75rem;
}

.fleet-panel-head h2 {
    margin: 0;
    font-size: 1rem;
}

/* WhatsApp-style filter chips (2026-09-01): one flat row that scrolls horizontally on overflow
   instead of wrapping onto a second line — same overflow-x:auto/hidden-scrollbar recipe
   .h-scroll-target already established for the POS "Frequently sold" tile row, applied directly
   here rather than requiring every caller to also add that class in markup. flex-shrink:0 on each
   .fleet-tab is the other required half — without it a squeezed flex row shrinks each button's own
   text instead of leaving every button full-size and letting the row scroll. */
.fleet-tabs {
    display: flex;
    gap: 0.35rem;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    padding-block: 2px;
}

.fleet-tabs::-webkit-scrollbar {
    display: none;
}

.fleet-tab {
    font-size: 0.78rem;
    padding: 0.35rem 0.7rem;
    border-radius: var(--radius-pill);
    color: var(--color-subtext);
    cursor: pointer;
    border: 1px solid transparent;
    background: none;
    background-image: none;
    flex-shrink: 0;
    white-space: nowrap;
}

.fleet-tab.active {
    background: rgba(146, 12, 255, 0.14);
    color: var(--color-text);
    border-color: var(--color-border);
}

.fleet-account-row {
    cursor: pointer;
}

.fleet-account-row.selected {
    background: rgba(146, 12, 255, 0.1);
}

.fleet-biz-owner {
    color: var(--color-subtext);
    font-size: 0.78rem;
}

.fleet-health {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.78rem;
    font-weight: 600;
}

.fleet-health .dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
}

.fleet-health.healthy { color: var(--color-success); }
.fleet-health.healthy .dot { background: var(--color-success); }
.fleet-health.gettingstarted { color: var(--color-warning); }
.fleet-health.gettingstarted .dot { background: var(--color-warning); }
.fleet-health.noactivity { color: var(--color-danger); }
.fleet-health.noactivity .dot { background: var(--color-danger); }

.fleet-spark {
    display: block;
}

.fleet-drawer-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(1, 9, 34, 0.55);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
    z-index: 45;
}

:root[data-theme="light"] .fleet-drawer-backdrop {
    background: rgba(27, 35, 51, 0.35);
}

.fleet-drawer-backdrop.open {
    opacity: 1;
    pointer-events: auto;
}

.fleet-drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: 420px;
    max-width: 92vw;
    background: var(--color-blog);
    border-left: 1px solid var(--color-border);
    box-shadow: -18px 0 40px var(--color-card-shadow);
    transform: translateX(100%);
    transition: transform 0.22s cubic-bezier(.2, .8, .2, 1);
    z-index: 46;
    display: flex;
    flex-direction: column;
}

.fleet-drawer:dir(rtl) {
    right: auto;
    left: 0;
    border-left: none;
    border-right: 1px solid var(--color-border);
    transform: translateX(-100%);
}

.fleet-drawer.open {
    transform: translateX(0);
}

.fleet-drawer-head {
    padding: 1.25rem 1.4rem 1rem;
    border-bottom: 1px solid var(--color-border);
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
}

.fleet-drawer-head h3 {
    margin: 0 0 0.2rem;
    font-size: 1.15rem;
}

.fleet-drawer-head p {
    margin: 0;
    color: var(--color-subtext);
    font-size: 0.8rem;
}

.fleet-drawer-close {
    background: none;
    border: none;
    color: var(--color-subtext);
    cursor: pointer;
    width: 30px;
    height: 30px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.fleet-drawer-close:hover {
    background: rgba(146, 12, 255, 0.12);
    color: var(--color-text);
}

.fleet-drawer-body {
    padding: 1.1rem 1.4rem;
    overflow-y: auto;
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 1.4rem;
}

.fleet-drawer-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.7rem;
}

.fleet-mini-stat {
    background: var(--color-footer);
    border-radius: var(--radius-control);
    padding: 0.7rem 0.85rem;
}

.fleet-mini-stat .label {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-subtext);
}

.fleet-mini-stat .value {
    font-size: 1.05rem;
    font-weight: 600;
    margin-top: 0.15rem;
    font-variant-numeric: tabular-nums;
}

.fleet-drawer h4 {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--color-subtext);
    margin: 0 0 0.6rem;
}

.fleet-timeline {
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
}

.fleet-timeline-row {
    display: flex;
    justify-content: space-between;
    gap: 0.5rem;
    font-size: 0.82rem;
}

.fleet-timeline-row .what {
    color: var(--color-text);
}

.fleet-timeline-row .when {
    color: var(--color-subtext);
    flex-shrink: 0;
}

.fleet-alert-box {
    display: flex;
    gap: 0.6rem;
    padding: 0.75rem 0.9rem;
    background: rgba(255, 176, 32, 0.12);
    border: 1px solid var(--color-warning);
    border-radius: var(--radius-control);
    font-size: 0.8rem;
    line-height: 1.45;
}

.fleet-alert-box svg {
    flex-shrink: 0;
    margin-top: 0.1rem;
    color: var(--color-warning);
}

.fleet-drawer-actions {
    display: flex;
    gap: 0.6rem;
    margin-top: auto;
}

@media (max-width: 860px) {
    .fleet-kpis {
        grid-template-columns: repeat(2, 1fr);
    }

    .fleet-drawer {
        width: 100%;
        max-width: 100%;
    }
}

/* ===================== Accounts & Usage redesign (2026-08-18, artifact 249d60d0/AdminBusinessesContent
   + AdminAnalyticsContent) — reuses .chart-card/.filter-search/.fleet-tab*/.chip/ReportStatCard for
   everything that's already an established shape (see each component's own doc comment for exactly
   which); the classes below are the genuinely new pieces the mockup introduced (risk cards, segment
   pills, the health bar, the "going quiet" headline card, the mini-stat grid). All --color-*/--radius-*
   tokens, matching every other page in this app, not the mockup's own literal --bg/--footer/--card
   hex values. */

.admin-page {
    max-width: 1180px;
    margin: 0 auto;
}

.topline {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 1rem;
    margin-bottom: 1.4rem;
    flex-wrap: wrap;
}

.topline h1 {
    margin: 0 0 0.15rem;
    font-size: 1.3rem;
}

.topline p {
    margin: 0;
    color: var(--color-subtext);
    font-size: 0.85rem;
}

.topline .filter-search {
    flex: 0 1 280px;
}

/* ---- risk strip (Accounts) — a risk-card is a real button (keyboard/AT accessible), not a div with
   a click handler, since tapping one filters the table below. ---- */
.risk-strip {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.8rem;
    margin-bottom: 1.4rem;
}

.risk-card {
    position: relative;
    overflow: hidden;
    background: var(--color-footer);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    padding: 0.85rem 1rem 0.85rem 1.15rem;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    text-align: start;
    cursor: pointer;
    color: inherit;
    font: inherit;
}

.risk-card::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
}

.risk-card.critical::before { background: var(--color-danger); }
.risk-card.watch::before { background: var(--color-warning); }
.risk-card.good::before { background: var(--color-success); }

.risk-card.active {
    border-color: var(--color-secondary);
}

.risk-card .rlabel {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-subtext);
}

.risk-card .rvalue {
    font-size: 1.35rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.risk-card .rsub {
    font-size: 0.74rem;
    color: var(--color-subtext);
}

.risk-card .rlink {
    font-size: 0.74rem;
    color: var(--color-secondary);
    font-weight: 600;
    margin-top: 0.15rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---- segment pills (Accounts) ---- */
.segment-row {
    display: flex;
    gap: 1.5rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
}

.segment-group {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    /* Without these two, a flex item defaults to min-width:auto / width:auto — it grows to fit its
       own .segment-pills row's full unclamped content width instead of being clamped to the row's
       real available width, which leaves .segment-pills' own overflow-x:auto with nothing to actually
       scroll (nothing constrains it smaller than its content). The unclamped .segment-group then
       forces the whole flex-wrapped .segment-row, and with it the entire page, wider than the
       viewport instead of scrolling internally — the exact "ancestor needs min-width:0" bug class
       already fixed for .pos-layout/.h-scroll-target/.pos-frequent-row elsewhere in this file. */
    min-width: 0;
    width: 100%;
}

.segment-label {
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-subtext);
}

/* Same WhatsApp-style horizontal-scroll treatment as .fleet-tabs above, per-category (Plan/Region/
   Signed up each get their own independently-scrollable strip, not one shared scroll for all three —
   .segment-row, the outer container stacking the 3 category rows, keeps its own flex-wrap:wrap
   unchanged, since 3 separate filter dimensions stacking vertically on a narrow screen is a
   different, unrelated concern from "don't wrap the chips within one row"). */
.segment-pills {
    display: flex;
    gap: 0.4rem;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    padding-block: 2px;
}

.segment-pills::-webkit-scrollbar {
    display: none;
}

.pill {
    font-size: 0.76rem;
    padding: 0.3rem 0.65rem;
    border-radius: var(--radius-pill);
    border: 1px solid var(--color-border);
    color: var(--color-subtext);
    background: none;
    background-image: none;
    cursor: pointer;
    flex-shrink: 0;
    white-space: nowrap;
}

.pill.active {
    background: rgba(146, 12, 255, 0.16);
    color: var(--color-text);
    border-color: var(--color-secondary);
    font-weight: 600;
}

.pill .n {
    opacity: 0.75;
    margin-inline-start: 0.3rem;
}

/* ---- accounts table extras ---- */
.acct-name {
    font-weight: 600;
}

.acct-owner {
    color: var(--color-subtext);
    font-size: 0.76rem;
}

.acct-health-row {
    display: flex;
    align-items: center;
}

.acct-health-bar {
    width: 60px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--color-border);
    overflow: hidden;
    display: inline-block;
    vertical-align: middle;
    margin-inline-end: 0.5rem;
    flex-shrink: 0;
}

.acct-health-bar > span {
    display: block;
    height: 100%;
    border-radius: var(--radius-pill);
}

.acct-health-score {
    font-weight: 700;
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
}

table.admin-table thead th.sortable {
    cursor: pointer;
    user-select: none;
}

table.admin-table thead th.sortable .arrow {
    margin-inline-start: 0.25rem;
    opacity: 0.6;
}

.acct-kebab {
    background: none;
    background-image: none;
    border: none;
    color: var(--color-subtext);
    font-size: 1.1rem;
    cursor: pointer;
    padding: 0.2rem 0.5rem;
    border-radius: 6px;
}

.acct-kebab:hover {
    color: var(--color-text);
    background: rgba(146, 12, 255, 0.1);
}

.acct-footnote {
    margin-top: 1.25rem;
    padding-top: 1rem;
    border-top: 1px solid var(--color-border);
    color: var(--color-subtext);
    font-size: 0.8rem;
    line-height: 1.5;
}

/* ---- Usage & Product insight grid ---- */
.insight-grid {
    display: grid;
    grid-template-columns: 1.3fr 1fr;
    gap: 1rem;
    margin-bottom: 1rem;
}

.headline-card {
    background: var(--color-footer);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    padding: 1.1rem 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.headline-card.attention {
    border-color: var(--color-warning);
}

.headline-card .hlabel {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-subtext);
}

.headline-card .hlabel svg {
    width: 14px;
    height: 14px;
    color: var(--color-warning);
}

.headline-card .hvalue {
    font-size: 2rem;
    font-weight: 700;
}

.headline-card .hsub {
    font-size: 0.8rem;
    color: var(--color-subtext);
}

.headline-card .hlist {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin-top: 0.3rem;
}

.headline-card .hlist-row {
    display: flex;
    justify-content: space-between;
    gap: 0.5rem;
    font-size: 0.8rem;
}

.headline-card .hlist-row .biz {
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.headline-card .hlist-row .days {
    color: var(--color-warning);
    font-weight: 600;
    flex-shrink: 0;
}

.stat-mini-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 0.7rem;
}

.stat-mini {
    background: var(--color-footer);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-control);
    padding: 0.85rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.stat-mini .slabel {
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-subtext);
}

.stat-mini .svalue {
    font-size: 1.3rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.stat-mini .ssub {
    font-size: 0.72rem;
    color: var(--color-subtext);
}

.two-col {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
}

@media (max-width: 860px) {
    .risk-strip {
        grid-template-columns: 1fr;
    }

    .insight-grid,
    .two-col {
        grid-template-columns: 1fr;
    }

    .topline .filter-search {
        flex: 1 1 100%;
    }
}
