/* Vortex design system — sits on top of premium.css and reuses its
   color tokens (--premium-accent, --premium-bg, --premium-panel, ...)
   so the light/dark modern-neutral palette applies everywhere
   automatically. Adds the structural layer premium.css doesn't own: a
   spacing/radius scale, a fixed sidebar shell, and a real button
   hierarchy (primary/secondary/ghost/danger) to replace flat rows of
   identical .premium-btn links. No glow effects anywhere by design. */

:root {
    --vx-space-1: 4px;
    --vx-space-2: 8px;
    --vx-space-3: 12px;
    --vx-space-4: 16px;
    --vx-space-5: 28px;
    --vx-space-6: 36px;
    --vx-space-7: 52px;

    /* Card radius is 12px everywhere in the system; the 14/18 steps
       were what made a panel, a toast and a kanban column each look
       like they came from a different kit. */
    --vx-radius-sm: 9px;
    --vx-radius-md: 12px;
    --vx-radius-lg: 12px;

    /* The scale was missing its 20/24 steps, which is why those two
       numbers appear hardcoded all over this file. */
    --vx-space-4b: 20px;
    --vx-space-4c: 24px;

    /* Duration had no token at all -- three ad-hoc values (0.12/0.15/0.2s)
       were spread across 22 rules. These are the same three, named. */
    --vx-dur-fast: 0.12s;
    --vx-dur: 0.16s;
    --vx-dur-slow: 0.24s;

    /* A type scale, so a new component stops inventing a 15th font size.
       These are the sizes already in use, named by role. */
    --vx-text-xs: 11px;
    --vx-text-sm: 12px;
    --vx-text-base: 13px;
    --vx-text-md: 14px;
    --vx-text-lg: 15px;
    --vx-text-xl: 20px;
    --vx-text-2xl: 24px;
    --vx-text-3xl: 30px;

    --vx-rail-w: 64px;
    /* The rail's sizing is one number now, not a ladder.
       --vx-rail-slots is the *measured* ratio between the rail's content
       height and one icon box: 14 children at a 24px item measure 376px of
       content, and 376 / 24 = 15.67, so 15.7 is the ratio at the tightest
       end of the range. Solving `slots * item <= viewport` for the item
       size (see the computed rule at the end of this file) gives a size at
       which every child fits by construction instead of by five hand-tuned
       breakpoints that a human had to remember to re-tune.

       Everything else is derived from the item box, so no secondary size
       can be left behind when the item changes -- which is exactly how the
       store mark ended up needing its own override in two of the old
       steps.

       If a child is ever ADDED to the rail, re-measure this number rather
       than guessing it, in the console on a gym page:
         var r = document.querySelector('.vx-rail');
         r.style.cssText += ';height:auto;--vx-rail-item:24px;--vx-rail-gap:3.6px';
         r.getBoundingClientRect().height / 24
    */
    --vx-rail-slots: 15.7;
    /* Plain numbers here, derived ones on .vx-rail itself.
       A custom property is substituted in the element that DECLARES it, so
       `--vx-rail-brand-size: calc(var(--vx-rail-item) * 0.85)` written here
       would resolve against *this* element's --vx-rail-item and freeze --
       measured: the store mark stayed 36px while the icon computed to 40px.
       The derivations therefore live in the same rule that computes the item
       size, where they can actually see it. */
    --vx-rail-item: 42px;
    --vx-rail-gap: 6px;
    --vx-rail-expand-h: 30px;
    --vx-rail-brand-size: 36px;
    --vx-flyout-w: 264px;
    --vx-rail-h-mobile: 58px;

    --vx-danger: var(--premium-danger);
    --vx-danger-bg: var(--premium-danger-bg);
    --vx-danger-border: var(--premium-danger-border);
    --vx-success: var(--premium-success);
    --vx-success-bg: var(--premium-success-bg);
    --vx-success-border: var(--premium-success-border);
}

/* ---------- app shell ---------- */

.vx-app {
    display: flex;
    align-items: stretch;
    min-height: 100vh;
    width: 100%;
    position: relative;
    z-index: 1;
}

/* ---------- nav rail + contextual flyout ----------
   Replaces the old wide sidebar (a permanently long vertical list of
   ~30 links) with a slim, always icon-only rail: Home plus at most 7
   category icons, so at most ~8 destinations are ever visible at once.
   A category icon opens a small flyout with that category's 3-6 links
   instead of a page-length list -- level 1 is the rail, level 2 is the
   flyout, matching how Linear separates a short flat top-level list from
   a collapsible Workspace group rather than ever showing everything at
   once. See _vx_sidebar.html for the open/close JS. */

.vx-rail {
    width: var(--vx-rail-w);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--vx-rail-gap, var(--vx-space-2));
    background: var(--premium-panel);
    border-inline-end: 0.5px solid var(--premium-border);
    position: fixed;
    /* Logical, not physical: the rail sits on the reading-start side, so
       it moves to the left under dir=ltr the same way every other logical
       property in this stylesheet does. It used to be pinned to `right`
       because .premium-page hardcoded direction:rtl and nothing ever
       flipped -- that declaration is gone. */
    inset-block-start: 0;
    inset-inline-start: 0;
    height: 100vh;
    padding: var(--vx-space-3) 0;
    z-index: 60;
}

.vx-rail-brand {
    /* Back to 36px: the 44px pointer-target floor applied while this was a
       button. It is a mark now, so it goes back to being sized for reading.
       Tokenised so the height steps can shrink it -- it used to be hidden
       outright at short heights, which is the bug this replaces. */
    width: var(--vx-rail-brand-size, 36px);
    height: var(--vx-rail-brand-size, 36px);
    border-radius: var(--vx-radius-sm);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: var(--vx-space-2);
    flex-shrink: 0;
}

/* Not a control any more -- see the note in _vx_sidebar.html. position is
   kept because the logo <img> is absolutely placed over the fallback
   initial, so a logo whose file has gone reveals the initial rather than a
   blank tile. */
.vx-rail-brand {
    position: relative;
}

.vx-rail-brand img {
    /* Absolute so it covers the fallback initial rather than sitting next to
       it -- if the file 404s, onerror removes the <img> and the initial is
       simply revealed, with no layout shift and no blank box. */
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.vx-rail-brand-fallback {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--premium-accent-soft);
    color: var(--premium-accent);
    font-weight: 600;
    font-size: 15px;
}

.vx-rail-icon {
    position: relative;
    /* Driven by tokens so the height queries at the end of this section can
       retune every item at once rather than restating each rule. */
    width: var(--vx-rail-item, 42px);
    height: var(--vx-rail-item, 42px);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: none;
    border-radius: var(--vx-radius-sm);
    color: var(--premium-text-muted);
    cursor: pointer;
    text-decoration: none;
    transition: background 0.15s var(--premium-ease), color 0.15s var(--premium-ease);
}

.vx-rail-icon:hover {
    background: var(--premium-hover-tint);
    color: var(--premium-text);
}

.vx-rail-icon.is-active,
.vx-rail-icon.is-open {
    background: var(--premium-accent);
    color: #ffffff;
}

/* A filled icon alone still reads as "just another hover state" from across
   a room -- this bar is the thing that says "you are here" even glanced at.
   inset-inline-end (not a physical side) so it stays on the edge that
   actually faces .vx-main in both RTL and the LTR/English toggle. */
.vx-rail-icon.is-active::after {
    content: '';
    position: absolute;
    inset-inline-end: -7px;
    top: 15%;
    bottom: 15%;
    width: 3px;
    border-radius: 2px;
    background: var(--premium-accent);
}

.vx-rail-categories {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--vx-rail-gap, var(--vx-space-2));
    padding-top: var(--vx-rail-gap, var(--vx-space-2));
    margin-top: var(--vx-rail-gap, var(--vx-space-2));
    border-top: 0.5px solid var(--premium-border);
}

.vx-rail-foot {
    /* Pin account/sign-out to the bottom with a margin, not with a
       flexible 14th child. The old .vx-rail-spacer was an empty element
       whose only job was to be stretchy, and a stretchy sibling next to a
       stretchy .vx-rail-categories is what the two of them kept fighting
       over -- the fight the old five-line comment on the spacer existed to
       explain. `margin-block-start: auto` takes the leftover space without
       being an item that can win or lose it. */
    margin-block-start: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--vx-rail-gap, var(--vx-space-2));
}

.vx-rail-badge {
    position: absolute;
    inset-block-start: 2px;
    inset-inline-start: 2px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 8px;
    background: var(--premium-danger);
    color: #ffffff;
    font-size: 10px;
    font-weight: 600;
    line-height: 16px;
    text-align: center;
}

/* ---------- flyout (category sub-links) ---------- */

/* Only actually shown on mobile (see the max-width:960px block) -- the
   flyout there is a real full-screen overlay so a tap outside it needs
   something to catch and close it. On desktop the flyout pushes content
   rather than covering it, so there's nothing to "catch": the pushed
   content stays fully clickable while the flyout is open. */
.vx-flyout-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 55;
    background: transparent;
}

/* Pushes the main content over instead of floating on top of it -- an
   in-flow flex item (next to .vx-rail, before .vx-main in the DOM) whose
   width animates from 0, so .vx-main's flex:1 naturally reflows to fill
   whatever's left rather than the flyout covering it. .vx-flyout-inner
   holds a fixed width so its content doesn't reflow/wrap mid-transition
   while the OUTER element's width (and overflow:hidden) does the actual
   clipping/reveal. Mobile overrides this back to a full-screen sheet --
   see the max-width:960px block -- since there's no room to push a
   phone-width layout sideways. */
.vx-flyout {
    flex: 0 0 0px; /* explicit flex-basis, not width -- width alone (with
        flex-basis defaulting to auto) didn't reliably resolve, since
        flex-basis:auto's fallback to the width property is finicky
        across engines the moment a child forces an intrinsic size. */
    min-width: 0; /* flex items default to min-width:auto, which would size
        this to its content's intrinsic width (the inner wrapper's fixed
        264px) regardless of the flex-basis above -- this is what actually
        lets it collapse. */
    margin-inline-start: var(--vx-rail-w); /* .vx-rail is position:fixed, out of flex flow */
    overflow: hidden;
    background: var(--premium-panel);
    border-inline-start: 0.5px solid var(--premium-border);
    transition: flex-basis 0.2s var(--premium-ease);
}

.vx-flyout.is-open {
    flex-basis: var(--vx-flyout-w);
}

.vx-flyout-inner {
    /* Same missing-reset bug as the rail: height:100vh plus 12px of padding
       on each side made this 24px taller than the screen. */
    box-sizing: border-box;
    width: var(--vx-flyout-w);
    height: 100vh;
    overflow-y: auto;
    padding: var(--vx-space-3);
}

.vx-flyout-panel {
    display: none;
}

.vx-flyout[data-open-category="members"] .vx-flyout-panel[data-category="members"],
.vx-flyout[data-open-category="operations"] .vx-flyout-panel[data-category="operations"],
.vx-flyout[data-open-category="team"] .vx-flyout-panel[data-category="team"],
.vx-flyout[data-open-category="inventory"] .vx-flyout-panel[data-category="inventory"],
.vx-flyout[data-open-category="finance"] .vx-flyout-panel[data-category="finance"],
.vx-flyout[data-open-category="messages"] .vx-flyout-panel[data-category="messages"],
.vx-flyout[data-open-category="admin"] .vx-flyout-panel[data-category="admin"],
/* Controller nav categories (accounts/_controller_nav.html) -- separate
   category names from gym_system's rail since the Controller manages a
   different set of sections; both navs share this one stylesheet. */
.vx-flyout[data-open-category="accounts"] .vx-flyout-panel[data-category="accounts"],
.vx-flyout[data-open-category="stores"] .vx-flyout-panel[data-category="stores"],
.vx-flyout[data-open-category="support"] .vx-flyout-panel[data-category="support"],
.vx-flyout[data-open-category="billing"] .vx-flyout-panel[data-category="billing"],
.vx-flyout[data-open-category="system"] .vx-flyout-panel[data-category="system"] {
    display: block;
}

.vx-flyout-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 13px;
    font-weight: 600;
    color: var(--premium-text-muted);
    padding: 4px var(--vx-space-2) var(--vx-space-3);
}

.vx-flyout-close {
    display: none;
    background: none;
    border: none;
    color: var(--premium-text-muted);
    cursor: pointer;
    padding: 4px;
    border-radius: 6px;
}

.vx-flyout-link {
    display: flex;
    align-items: center;
    gap: var(--vx-space-3);
    padding: 10px var(--vx-space-3);
    border-radius: var(--vx-radius-sm);
    color: var(--premium-text);
    text-decoration: none;
    font-size: 14px;
    font-weight: 500;
    transition: background 0.15s var(--premium-ease), color 0.15s var(--premium-ease);
}

.vx-flyout-link:hover {
    background: var(--premium-hover-tint);
}

.vx-flyout-link.is-active {
    background: var(--premium-accent-soft);
    color: var(--premium-accent);
    font-weight: 600;
}

.vx-flyout-link-badge {
    margin-inline-start: auto;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 9px;
    background: var(--premium-danger);
    color: #ffffff;
    font-size: 11px;
    font-weight: 600;
    line-height: 18px;
    text-align: center;
}

/* Itemized notifications at the top of the Team/Messages flyout -- what
   exactly is pending (who, what) with a direct link, not just a bare
   count on the rail icon. */
.vx-flyout-notifications {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-bottom: var(--vx-space-3);
    padding-bottom: var(--vx-space-3);
    border-bottom: 0.5px solid var(--premium-border);
}

.vx-flyout-notification {
    display: flex;
    align-items: center;
    gap: var(--vx-space-2);
    padding: 8px var(--vx-space-3);
    border-radius: var(--vx-radius-sm);
    background: var(--premium-warning-bg);
    color: var(--premium-warning);
    text-decoration: none;
    font-size: 13px;
    font-weight: 600;
    transition: background 0.15s var(--premium-ease);
}

.vx-flyout-notification:hover {
    background: color-mix(in srgb, var(--premium-warning) 20%, transparent);
}

.vx-icon {
    display: inline-block;
    vertical-align: -3px;
    flex-shrink: 0;
}

/* ---------- command palette (Ctrl/Cmd+K) ---------- */

.vx-cmdk-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 300;
    background: rgba(10, 16, 26, 0.5);
    backdrop-filter: blur(3px);
    padding: 10vh 16px 0;
}

.vx-cmdk-overlay.is-open {
    display: block;
}

.vx-cmdk {
    width: 100%;
    max-width: 560px;
    margin: 0 auto;
    background: var(--premium-panel);
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-lg);
    box-shadow: 0 2px 4px rgba(16, 24, 40, 0.08), 0 24px 60px rgba(16, 24, 40, 0.28);
    overflow: hidden;
    animation: vxCmdkIn 0.16s var(--premium-ease);
}

@keyframes vxCmdkIn {
    from { opacity: 0; transform: translateY(-8px) scale(0.98); }
    to { opacity: 1; transform: translateY(0) scale(1); }
}

.vx-cmdk-input-row {
    display: flex;
    align-items: center;
    gap: var(--vx-space-3);
    padding: var(--vx-space-4) var(--vx-space-4);
    border-bottom: 0.5px solid var(--premium-border);
}

.vx-cmdk-search-icon {
    display: flex;
    color: var(--premium-text-muted);
    flex-shrink: 0;
}

.vx-cmdk-input {
    flex: 1;
    min-width: 0;
    border: none;
    background: none;
    outline: none;
    color: var(--premium-text);
    font-size: 16px;
    font-family: inherit;
}

.vx-cmdk-input::placeholder {
    color: var(--premium-text-muted);
}

.vx-cmdk-esc {
    flex-shrink: 0;
    padding: 3px 8px;
    border-radius: 6px;
    border: 0.5px solid var(--premium-border);
    color: var(--premium-text-muted);
    font-size: 11px;
    font-family: inherit;
}

.vx-cmdk-results {
    /* Same again: the cap is meant to be 55vh, not 55vh + 16px. */
    box-sizing: border-box;
    max-height: 55vh;
    overflow-y: auto;
    padding: var(--vx-space-2);
}

.vx-cmdk-group {
    margin-bottom: var(--vx-space-2);
}

.vx-cmdk-group-title {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--premium-text-muted);
    padding: 6px var(--vx-space-3);
}

.vx-cmdk-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--vx-space-2);
    padding: 10px var(--vx-space-3);
    border-radius: var(--vx-radius-sm);
    color: var(--premium-text);
    text-decoration: none;
    font-size: 14px;
}

.vx-cmdk-item.is-active,
.vx-cmdk-item:hover {
    background: var(--premium-accent-soft);
    color: var(--premium-accent);
}

.vx-cmdk-item-sub {
    color: var(--premium-text-muted);
    font-size: 12px;
    flex-shrink: 0;
}

.vx-cmdk-item.is-active .vx-cmdk-item-sub,
.vx-cmdk-item:hover .vx-cmdk-item-sub {
    color: inherit;
    opacity: 0.75;
}

.vx-cmdk-empty {
    padding: var(--vx-space-6) var(--vx-space-4);
    text-align: center;
    color: var(--premium-text-muted);
    font-size: 13px;
}

/* ---------- keyboard shortcuts help ("?") ---------- */

.vx-shortcuts-modal {
    max-width: 400px;
}

.vx-shortcuts-list {
    padding: var(--vx-space-2) var(--vx-space-4) var(--vx-space-4);
}

.vx-shortcuts-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--vx-space-3);
    padding: 10px 0;
    border-bottom: 0.5px solid var(--premium-border);
    font-size: 13px;
    color: var(--premium-text);
}

.vx-shortcuts-row:last-child {
    border-bottom: none;
}

.vx-shortcuts-row kbd {
    flex-shrink: 0;
    padding: 3px 8px;
    border-radius: 6px;
    border: 0.5px solid var(--premium-border);
    background: var(--premium-hover-tint);
    color: var(--premium-text-muted);
    font-size: 11px;
    font-family: inherit;
    font-weight: 600;
}

/* ---------- toast notifications ----------
   Replaces the old per-page "banner at the top of the content" pattern
   (10 templates each rolled their own, none of them actually varying
   color by success/error/warning) with one shared, auto-dismissing
   corner stack that reads Django's real message level. */

.vx-toast-container {
    position: fixed;
    bottom: var(--vx-space-5);
    left: var(--vx-space-5);
    z-index: 400;
    display: flex;
    flex-direction: column-reverse;
    gap: var(--vx-space-2);
    max-width: min(360px, calc(100vw - 2 * var(--vx-space-5)));
}

@media (max-width: 960px) {
    .vx-toast-container {
        bottom: calc(var(--vx-rail-h-mobile) + var(--vx-space-3));
        left: var(--vx-space-3);
        right: var(--vx-space-3);
        max-width: none;
    }
}

.vx-toast {
    display: flex;
    align-items: flex-start;
    gap: var(--vx-space-2);
    padding: var(--vx-space-3) var(--vx-space-3);
    border-radius: var(--vx-radius-md);
    background: var(--premium-panel);
    border: 0.5px solid var(--premium-border);
    box-shadow: var(--premium-shadow-lg);
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 0.2s var(--premium-ease), transform 0.2s var(--premium-ease);
}

.vx-toast.is-visible {
    opacity: 1;
    transform: translateY(0);
}

.vx-toast-success { border-inline-start: 3px solid var(--premium-success); }
.vx-toast-error { border-inline-start: 3px solid var(--premium-danger); }
.vx-toast-warning { border-inline-start: 3px solid var(--premium-warning); }
.vx-toast-info { border-inline-start: 3px solid var(--premium-info); }

.vx-toast-text {
    flex: 1;
    font-size: 13px;
    font-weight: 600;
    color: var(--premium-text);
    line-height: 1.5;
}

.vx-toast-close {
    flex-shrink: 0;
    background: none;
    border: none;
    color: var(--premium-text-muted);
    cursor: pointer;
    font-size: 16px;
    line-height: 1;
    padding: 0 2px;
}

.vx-toast-action {
    flex-shrink: 0;
    background: none;
    border: none;
    color: var(--premium-accent);
    cursor: pointer;
    font-size: 13px;
    font-weight: 600;
    padding: 0;
    white-space: nowrap;
}

.vx-toast-action:hover {
    text-decoration: underline;
}

/* ---------- quick-preview popover ---------- */

.vx-preview-popover {
    display: none;
    /* fixed, not absolute: vxPosition() writes viewport coordinates so the
       card can be kept clear of the rail and the mobile bottom bar. */
    position: fixed;
    z-index: 250;
    width: 280px;
    background: var(--premium-panel);
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-md);
    box-shadow: var(--premium-shadow-lg);
    padding: var(--vx-space-4);
}

.vx-preview-popover.is-open {
    display: block;
}

.vx-preview-head {
    display: flex;
    align-items: center;
    gap: var(--vx-space-3);
    margin-bottom: var(--vx-space-3);
}

.vx-preview-name {
    font-weight: 600;
    font-size: 14px;
    color: var(--premium-text);
    margin-bottom: 4px;
}

.vx-preview-rows {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.vx-preview-row {
    display: flex;
    justify-content: space-between;
    gap: var(--vx-space-2);
    font-size: 12px;
}

.vx-preview-row span:first-child {
    color: var(--premium-text-muted);
}

.vx-preview-row span:last-child {
    color: var(--premium-text);
    font-weight: 600;
}

/* Small icon-only affordance that only appears on row/name hover --
   opens the quick-preview popover without cluttering the row the rest
   of the time. */
.vx-quick-preview-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 6px;
    color: var(--premium-text-muted);
    opacity: 0;
    transition: opacity 0.12s var(--premium-ease), background 0.12s var(--premium-ease), color 0.12s var(--premium-ease);
    cursor: pointer;
    vertical-align: -6px;
}

tr:hover .vx-quick-preview-trigger,
.vx-quick-preview-trigger:focus-visible {
    opacity: 1;
}

.vx-quick-preview-trigger:hover {
    background: var(--premium-accent-soft);
    color: var(--premium-accent);
}

/* ---------- skeleton loading (quick-preview popover fetch) ---------- */

.vx-preview-skeleton {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 4px 0;
}

.vx-skeleton-line {
    height: 10px;
    border-radius: 5px;
    background: linear-gradient(90deg, var(--premium-hover-tint) 25%, var(--premium-border) 50%, var(--premium-hover-tint) 75%);
    background-size: 200% 100%;
    animation: vxSkeletonShimmer 1.2s ease-in-out infinite;
}

@keyframes vxSkeletonShimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* ---------- sticky table headers ---------- */

.premium-table thead th {
    position: sticky;
    top: 0;
    background: var(--premium-panel);
    z-index: 2;
    box-shadow: 0 1px 0 var(--premium-border);
}

/* Single tooltip element, positioned via JS (see _vx_sidebar.html) and
   appended to <body> rather than living inside the rail -- keeping it
   fully independent of any ancestor's overflow/positioning avoids the
   clipping issues a same-DOM ::after-based tooltip would otherwise hit. */
.vx-tooltip {
    position: fixed;
    z-index: 200;
    background: var(--premium-panel);
    color: var(--premium-text);
    border: 0.5px solid var(--premium-border);
    padding: 6px 12px;
    border-radius: var(--vx-radius-sm);
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
    box-shadow: var(--premium-shadow-lg);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s var(--premium-ease);
}

.vx-tooltip.is-visible {
    opacity: 1;
}

/* The flyout's scrollbar used to be styled here, on its own. It was the
   only scroll container in the system that got any treatment, so every
   table, the kanban board, the command palette and the nav rail all showed
   the raw OS bar -- a thick grey slab under every dashboard table. The
   shared treatment lives at the bottom of this file under "Scrollbars". */

/* ---------- native form controls, themed everywhere ---------- */

/* Filter-bar selects/date-inputs (universal filtering, period reports,
   the messaging compose form, etc.) were added as bare <select>/<input>
   elements outside the .premium-field wrapper the add/edit forms use,
   so they kept the browser's default OS chrome. One rule, scoped to
   .vx-app so it only touches gym_system pages, covers every one of them
   without having to hunt down and re-wrap each instance individually.
   Wrapped in :where() (zero specificity) on both sides so any existing
   or future purpose-built control — .premium-field input, .vx-search-
   input's borderless pill style, etc. — still wins outright instead of
   this needing !important or a specificity arms race. */
:where(.vx-app) :where(
    select,
    input[type="text"],
    input[type="date"],
    input[type="datetime-local"],
    input[type="number"],
    input[type="email"],
    input[type="tel"],
    input[type="password"],
    input[type="search"],
    textarea
) {
    color-scheme: light dark;
    background: var(--premium-input-bg);
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-sm);
    padding: 8px 10px;
    color: var(--premium-text);
    font-size: 13px;
    font-family: inherit;
    box-sizing: border-box;
    transition: border-color 0.15s var(--premium-ease), box-shadow 0.15s var(--premium-ease);
}

:where(.vx-app) select {
    cursor: pointer;
}

:where(.vx-app) select option {
    background: var(--premium-panel);
    color: var(--premium-text);
}

:where(.vx-app) :where(select, input, textarea):focus {
    outline: none;
    border-color: var(--premium-accent);
    box-shadow: 0 0 0 3px var(--premium-accent-soft);
}

:where(.vx-app) :where(input[type="checkbox"], input[type="radio"]) {
    accent-color: var(--premium-accent);
    width: 16px;
    height: 16px;
}

/* File inputs (equipment/product photos, member attachments/progress
   photos, store logo) can't be restyled as a box the way other inputs
   can — the "Choose File" part is a distinct pseudo-element the browser
   renders itself, left as its OS-default button/text pair otherwise. */
:where(.vx-app) input[type="file"] {
    color: var(--premium-text-muted);
    font-size: 13px;
    font-family: inherit;
}

:where(.vx-app) input[type="file"]::file-selector-button,
:where(.vx-app) input[type="file"]::-webkit-file-upload-button {
    background: var(--premium-accent-soft);
    color: var(--premium-accent);
    border: 0.5px solid var(--premium-accent);
    border-radius: var(--vx-radius-sm);
    padding: 8px 14px;
    margin-inline-start: 10px;
    font-size: 13px;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;
    transition: background 0.15s var(--premium-ease), color 0.15s var(--premium-ease);
}

:where(.vx-app) input[type="file"]:hover::file-selector-button,
:where(.vx-app) input[type="file"]:hover::-webkit-file-upload-button {
    background: var(--premium-accent-contrast);
    color: #ffffff;
}

/* ---------- main area ---------- */

.vx-main {
    flex: 1;
    min-width: 0;
    margin-inline-start: var(--vx-rail-w);
    padding: var(--vx-space-5) var(--vx-space-6) var(--vx-space-7);
}

.vx-topbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--vx-space-4);
    flex-wrap: wrap;
    margin-bottom: var(--vx-space-5);
}

.vx-topbar-title {
    font-size: 22px;
    font-weight: 600;
    margin: 0;
}

.vx-topbar-subtitle {
    color: var(--premium-text-muted);
    font-size: 13px;
    margin-top: 2px;
}

.vx-menu-toggle {
    display: none;
    background: var(--premium-panel);
    border: 0.5px solid var(--premium-border);
    color: var(--premium-text);
    border-radius: var(--vx-radius-sm);
    width: 38px;
    height: 38px;
    font-size: 16px;
    cursor: pointer;
}

/* ---------- button hierarchy ---------- */

.vx-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--vx-space-2);
    padding: 10px 18px;
    border-radius: var(--r-btn);
    font-size: var(--fs-body-sm);
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    border: 0.5px solid transparent;
    transition: background 0.2s var(--premium-ease), color 0.2s var(--premium-ease), box-shadow 0.2s var(--premium-ease), border-color 0.2s var(--premium-ease);
    white-space: nowrap;
}

.vx-btn-primary {
    background: var(--premium-accent-contrast);
    color: var(--premium-on-accent);
    border-color: var(--premium-accent-contrast);
}

.vx-btn-primary:hover {
    filter: brightness(0.92);
}

/* Thin border, transparent fill -- the accent color is reserved for the
   one primary action per screen; every other button reads as neutral
   (border-strong + regular text) so it never competes with it. */
.vx-btn-secondary {
    background: var(--premium-accent-soft);
    color: var(--premium-text);
    border-color: transparent;
}

.vx-btn-secondary:hover {
    background: var(--premium-surface-2);
    border-color: var(--premium-text-muted);
}

.vx-btn-ghost {
    background: transparent;
    color: var(--premium-text-muted);
    border-color: var(--premium-border);
}

.vx-btn-ghost:hover {
    color: var(--premium-text);
    border-color: var(--premium-text-muted);
}

.vx-btn-danger {
    background: var(--vx-danger-bg);
    color: var(--vx-danger);
    border-color: var(--vx-danger-border);
}

.vx-btn-danger:hover {
    background: var(--vx-danger);
    color: var(--premium-on-accent);
}

/* Applied automatically at load by vx-forms.js to any submit button whose
   own label marks it as destructive -- see the "at-rest danger styling"
   block there. Deliberately just a color/border shift, not a filled
   background, so a table full of these doesn't read as a wall of red. */
:where(.vx-app, .premium-page) .premium-toggle-btn.is-danger,
:where(.vx-app, .premium-page) .vx-btn.is-danger {
    color: var(--vx-danger);
    border-color: var(--vx-danger-border);
}

:where(.vx-app, .premium-page) .premium-toggle-btn.is-danger:hover,
:where(.vx-app, .premium-page) .vx-btn.is-danger:hover {
    background: var(--vx-danger-bg);
    border-color: var(--vx-danger);
}

.vx-btn-sm {
    padding: 7px 14px;
    font-size: 12px;
}

/* Inline banner for views that pass {{ error }}/{{ success }} directly in
   the render context instead of going through django.contrib.messages
   (most Controller views -- see accounts/views.py) -- same color intent
   as the toast system's error/success states, just anchored in-page for
   forms that redisplay with the submitted values still filled in. */
.premium-inline-alert {
    display: flex;
    align-items: center;
    gap: var(--vx-space-2);
    padding: 12px 16px;
    border-radius: 10px;
    font-size: 13px;
    margin-bottom: 16px;
    border: 0.5px solid transparent;
}

.premium-inline-alert.is-error {
    background: var(--vx-danger-bg);
    color: var(--vx-danger);
    border-color: var(--vx-danger-border);
}

.premium-inline-alert.is-success {
    background: var(--vx-success-bg);
    color: var(--vx-success);
    border-color: var(--vx-success-border);
}

.vx-quick-actions {
    display: flex;
    gap: var(--vx-space-3);
    flex-wrap: wrap;
    margin-bottom: var(--vx-space-5);
}

/* ---------- smart suggestion bar (single highest-priority "do this today" line) ---------- */

.vx-smart-alert {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--vx-space-3);
    flex-wrap: wrap;
    background: var(--premium-accent-soft);
    border: 0.5px solid var(--premium-accent-glow);
    border-radius: var(--vx-radius-sm);
    padding: 12px 18px;
    margin-bottom: var(--vx-space-4);
    font-size: 13px;
    color: var(--premium-text);
}

.vx-smart-alert-text {
    flex: 1;
    min-width: 200px;
}

/* ---------- day/numbers tab switcher ---------- */

.vx-daytabs {
    display: flex;
    gap: 4px;
    border-bottom: 0.5px solid var(--premium-border);
    margin-bottom: var(--vx-space-5);
}

.vx-daytab {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 10px 20px;
    font-size: 15px;
    font-weight: 500;
    color: var(--premium-text-muted);
    background: none;
    border: none;
    border-radius: var(--vx-radius-sm) var(--vx-radius-sm) 0 0;
    cursor: pointer;
    font-family: inherit;
}

.vx-daytab.is-active {
    background: var(--premium-accent-soft);
    color: var(--premium-accent);
}

.vx-daytab-panel[hidden] {
    display: none;
}

/* ---------- flat secondary stat tiles (Today tab: present/income/bookings) ---------- */

.vx-flat-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--vx-space-3);
    margin-bottom: var(--vx-space-5);
}

.vx-flat-stat {
    display: block;
    background: var(--premium-surface-2);
    border-radius: var(--vx-radius-md);
    padding: var(--vx-space-4);
    text-decoration: none;
    color: inherit;
}

.vx-flat-stat-label {
    font-size: 13px;
    color: var(--premium-text-muted);
    margin: 0 0 6px;
}

.vx-flat-stat-value {
    font-size: 26px;
    font-weight: 500;
    margin: 0;
    color: var(--premium-text);
}

/* ---------- "needs your attention" card ---------- */

.vx-needs-card {
    background: var(--premium-panel);
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-lg);
    padding: 4px 20px 4px;
    margin-bottom: var(--vx-space-5);
}

.vx-needs-title {
    font-size: 15px;
    font-weight: 600;
    margin: 0;
    padding: 14px 0 10px;
}

.vx-needs-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 11px 0;
    border-top: 0.5px solid var(--premium-border);
    text-decoration: none;
    color: inherit;
}

.vx-needs-icon {
    font-size: 18px;
    line-height: 1;
    flex-shrink: 0;
    color: var(--premium-text-muted);
}

.vx-needs-icon.is-warning { color: var(--premium-warning); }
.vx-needs-icon.is-danger { color: var(--premium-danger); }

.vx-needs-text {
    font-size: 14px;
    flex: 1;
    color: var(--premium-text);
}

.vx-needs-action {
    font-size: 13px;
    font-weight: 600;
    color: var(--premium-accent);
    white-space: nowrap;
    background: none;
    border: none;
    cursor: pointer;
    font-family: inherit;
    padding: 0;
}

.vx-needs-action:hover {
    text-decoration: underline;
}

.vx-needs-empty {
    font-size: 13px;
    color: var(--premium-text-muted);
    padding: 11px 0;
}

/* ---------- alerts (notification list, replaces full-width callout cards) ---------- */

.vx-alert-list {
    display: flex;
    flex-direction: column;
    gap: var(--vx-space-2);
    margin-bottom: var(--vx-space-5);
}

.vx-alert-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--vx-space-3);
    padding: var(--vx-space-3) var(--vx-space-4);
    border-radius: var(--vx-radius-sm);
    background: var(--premium-accent-soft);
    border: 0.5px solid var(--premium-accent-glow);
    color: var(--premium-accent);
    text-decoration: none;
    font-size: 13px;
    font-weight: 600;
    transition: background 0.15s var(--premium-ease);
}

.vx-alert-item:hover {
    background: color-mix(in srgb, var(--premium-accent) 32%, transparent);
}

.vx-alert-item-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border-radius: 8px;
    font-size: 14px;
    background: color-mix(in srgb, var(--premium-accent) 18%, transparent);
}

.vx-alert-item-arrow {
    color: var(--premium-text-muted);
    font-weight: 400;
}

/* ---------- content grid / panels ---------- */

.vx-panels-grid {
    display: grid;
    /* min() clamps the track floor to the container on a narrow screen.
       A bare minmax(360px, ...) keeps its 360px minimum even when only
       343px is available, which pushed the three dashboard panels 9px
       past the edge of a 375px phone and gave the whole page a sideways
       scroll. No effect at any width where 360px actually fits. */
    grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
    gap: var(--vx-space-5);
    margin-top: var(--vx-space-5);
}

.vx-panel {
    background: var(--premium-panel);
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-md);
    overflow: hidden;
    box-shadow: var(--premium-shadow);
    transition: box-shadow 0.15s var(--premium-ease);
}

.vx-panel:hover {
    box-shadow: var(--premium-shadow-lg);
}

.vx-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--vx-space-4) var(--vx-space-4) var(--vx-space-3);
    border-bottom: 0.5px solid var(--premium-border);
}

.vx-panel-title {
    font-size: var(--fs-title);
    font-weight: 500;
}

.vx-panel-link {
    font-size: 12px;
    color: var(--premium-accent);
    text-decoration: none;
    font-weight: 600;
}

.vx-panel-link:hover {
    text-decoration: underline;
}

.vx-panel .premium-table-wrap {
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: none;
}

.vx-panel .premium-table th,
.vx-panel .premium-table td {
    padding: 11px var(--vx-space-4);
}

/* ---------- member portal nav (tabs, not a sidebar) ----------
   The portal has no roles/permissions and only ~6 destinations, and it's
   the one surface actual end customers (not staff) use, often on their
   phone — a full desktop sidebar doesn't fit that shape. Reuses the same
   tokens/colors as the staff sidebar for visual consistency. */

.vx-portal-shell {
    width: 100%;
    max-width: 900px;
    margin: 0 auto;
    padding: var(--vx-space-5) var(--vx-space-4) var(--vx-space-7);
    position: relative;
    z-index: 1;
}

.vx-portal-topbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--vx-space-3);
    margin-bottom: var(--vx-space-4);
}

.vx-portal-tabs {
    display: flex;
    gap: var(--vx-space-2);
    flex-wrap: wrap;
    margin-bottom: var(--vx-space-5);
    padding-bottom: var(--vx-space-3);
    border-bottom: 0.5px solid var(--premium-border);
}

.vx-portal-tab {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 16px;
    border-radius: var(--vx-radius-sm);
    color: var(--premium-text-muted);
    text-decoration: none;
    font-size: 13px;
    font-weight: 600;
    transition: background 0.15s var(--premium-ease), color 0.15s var(--premium-ease);
}

.vx-portal-tab:hover {
    background: var(--premium-hover-tint);
    color: var(--premium-text);
}

.vx-portal-tab.is-active {
    background: var(--premium-accent-soft);
    color: var(--premium-accent);
}

/* ---------- responsive: rail becomes a bottom bar under 960px ---------- */

@media (max-width: 960px) {
    .vx-rail {
        flex-direction: row;
        align-items: center;
        justify-content: space-around;
        width: 100%;
        height: var(--vx-rail-h-mobile);
        inset-block-start: auto;
        inset-block-end: 0;
        inset-inline-start: 0;
        border-inline-end: none;
        border-top: 0.5px solid var(--premium-border);
        padding: 0 var(--vx-space-2);
        /* The bar holds 11 icons at a 52px floor -- about 620px of content
           in a 390px viewport -- so it has to scroll horizontally, and a
           fixed box contributes no scrollable overflow to the document, so
           nothing else can reach those icons for it. Restored to exactly
           the form it had before this session's rail work; see the note on
           .vx-rail's overflow-y below for why the shorthand was dropped. */
        overflow-x: auto;
        gap: var(--vx-space-1);
    }

    /* Hidden again. It was pinned into the bar while it was the account
       button; as a decorative store mark it would only be taking a slot
       away from the nav icons on the width that has the least to spare.
       "حسابي" has its own icon at the end of the bar. */
    .vx-rail-brand {
        display: none;
    }

    .vx-rail-categories {
        flex-direction: row;
        border-top: none;
        padding-top: 0;
        margin-top: 0;
        gap: var(--vx-space-1);
    }

    /* Profile/logout fold into the rest of the row instead of being
       pushed to a "bottom" that no longer exists once the rail itself is
       the bottom bar. `display: contents` makes the wrapper vanish from
       layout and hands its two links straight back to the bar as direct
       children, so space-around distributes them exactly as it did when
       they had no wrapper at all. */
    .vx-rail-foot {
        display: contents;
    }

    .vx-main {
        margin-inline-start: 0;
        padding: var(--vx-space-4) var(--vx-space-4) calc(var(--vx-rail-h-mobile) + var(--vx-space-4));
    }

    /* A pushed side panel has nowhere to push on a phone-width screen --
       the flyout becomes a full-screen sheet overlay instead, same as
       before this change, with the backdrop re-enabled to catch a tap
       outside it. Needs an explicit z-index above .vx-flyout-backdrop's 55:
       .vx-main is a flex item of .vx-app, and flex items paint as if
       position:relative regardless of their actual position property, so
       with both elements at the implicit z-index:auto stacking level,
       .vx-main (later in the DOM than .vx-flyout) would otherwise paint
       on top of this fixed-position sheet and hide it entirely. */
    .vx-flyout {
        display: none;
        position: fixed;
        inset-block-start: 0;
        inset-block-end: var(--vx-rail-h-mobile);
        inset-inline: 0;
        width: auto;
        margin-inline-start: 0;
        border-radius: 0;
        border: none;
        border-top: 0.5px solid var(--premium-border);
        z-index: 56;
    }

    .vx-flyout.is-open {
        display: block;
    }

    .vx-flyout-inner {
        width: auto;
        height: auto;
    }

    .vx-flyout-backdrop.is-open {
        display: block;
    }

    .vx-flyout-close {
        display: flex;
        align-items: center;
        justify-content: center;
    }
}

/* ---------- chart boxes ---------- */
/* Chart.js sizes a responsive canvas from its own width unless the aspect
   ratio is switched off; with it off the canvas needs a positioned parent
   of a known height to fill, otherwise it collapses. Every report canvas
   sits in one of these. */
:where(.vx-app) .vx-chart-box {
    position: relative;
    width: 100%;
}

:where(.vx-app) .vx-chart-box { direction: ltr; }

:where(.vx-app) .vx-chart-box > canvas {
    position: absolute;
    inset: 0;
}

/* Report charts: one per row on a phone, two side by side from tablet up.
   These were inline "1fr 1fr" grids with no breakpoint, which held the
   page at 796px on a 375px screen -- the reports page was the only one in
   the system that scrolled sideways as a whole. auto-fit was not the fix:
   on a desktop it packed four narrow charts into a row instead of two. */
:where(.vx-app) .vx-chart-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 24px;
    margin-bottom: 24px;
}

@media (min-width: 900px) {
    :where(.vx-app) .vx-chart-grid {
        grid-template-columns: 1fr 1fr;
    }
}

/* ---------- pager + sortable headers ---------- */
:where(.vx-app) .vx-pager {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
    justify-content: space-between;
    margin-top: 16px;
}

:where(.vx-app) .vx-pager-info {
    font-size: 13px;
    color: var(--premium-text-muted);
}

:where(.vx-app) .vx-pager-count {
    opacity: 0.75;
}

:where(.vx-app) .vx-pager-links {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

:where(.vx-app) .premium-toggle-btn.is-disabled {
    opacity: 0.4;
    cursor: default;
    pointer-events: none;
}

:where(.vx-app) .vx-sort-link {
    color: inherit;
    text-decoration: none;
    white-space: nowrap;
}

:where(.vx-app) .vx-sort-link:hover {
    color: var(--premium-accent);
}

:where(.vx-app) .vx-sort-link.is-active {
    color: var(--premium-accent);
}

:where(.vx-app) .vx-sort-arrow {
    font-size: 10px;
}

/* ---------- table -> cards on a phone ---------- */
/* Opt-in per table with .vx-cards-sm. A 7-column table inside a 375px
   screen shows about three columns; the rest -- status, dates, and every
   action button -- sits off to the side behind a horizontal swipe. On the
   members and staff lists that is where the work actually happens, so
   below 700px each row becomes a card with the column name printed
   beside its value (from data-label on the cell). One set of markup
   serves both layouts; there is no duplicated mobile row to keep in sync. */
@media (max-width: 700px) {
    :where(.vx-app) .premium-table-wrap:has(.vx-cards-sm) {
        overflow-x: visible;
        background: transparent;
        border: none;
        box-shadow: none;
    }

    :where(.vx-app) .premium-table.vx-cards-sm,
    :where(.vx-app) .premium-table.vx-cards-sm tbody,
    :where(.vx-app) .premium-table.vx-cards-sm tr,
    :where(.vx-app) .premium-table.vx-cards-sm td {
        display: block;
        width: auto;
        min-width: 0;
    }

    :where(.vx-app) .premium-table.vx-cards-sm thead {
        display: none;
    }

    :where(.vx-app) .premium-table.vx-cards-sm tr {
        background: var(--premium-panel);
        border: 0.5px solid var(--premium-border);
        border-radius: var(--vx-radius-md);
        box-shadow: var(--premium-shadow);
        margin-bottom: 12px;
        padding: 4px 0;
    }

    :where(.vx-app) .premium-table.vx-cards-sm td {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        align-items: center;
        justify-content: space-between;
        padding: 9px 16px;
        border-bottom: 0.5px solid var(--premium-border);
    }

    :where(.vx-app) .premium-table.vx-cards-sm td:last-child {
        border-bottom: none;
    }

    /* The cell keeps its column name so a value is never orphaned once
       the header row is gone. Cells with no label (the actions column)
       simply render their buttons full width. */
    :where(.vx-app) .premium-table.vx-cards-sm td[data-label]::before {
        content: attr(data-label);
        color: var(--premium-text-muted);
        font-size: 12px;
        flex: 0 0 auto;
    }

    /* The name is the card's heading, not another labelled row. */
    :where(.vx-app) .premium-table.vx-cards-sm td[data-card-title] {
        justify-content: flex-start;
        padding-top: 14px;
        font-size: 15px;
        font-weight: 600;
    }

    :where(.vx-app) .premium-table.vx-cards-sm td[data-card-actions] {
        justify-content: flex-start;
        gap: 8px;
        padding-bottom: 14px;
    }
}

/* ---------- rail labels ---------- */
/* The rail was icons only, with the name in data-tooltip. That works on
   a desktop with a mouse, but a phone has no hover at all: the bottom
   bar was eleven unlabelled icons with no way to learn what any of them
   does. Labels are printed under the icons there, and the desktop rail
   gets a toggle that widens it and shows the same labels beside them. */
.vx-rail-label {
    display: none;
    font-size: 10px;
    line-height: 1.2;
    text-align: center;
    white-space: nowrap;
}

.vx-rail-expand {
    width: var(--vx-rail-item, 42px);
    height: var(--vx-rail-expand-h, 32px);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: none;
    border-radius: var(--vx-radius-sm);
    color: var(--premium-text-muted);
    cursor: pointer;
    transition: background 0.15s var(--premium-ease), color 0.15s var(--premium-ease);
}

.vx-rail-expand:hover {
    background: var(--premium-hover-tint);
    color: var(--premium-accent);
}

/* --- desktop: opt-in expanded rail --- */
@media (min-width: 961px) {
    .vx-app.vx-rail-expanded {
        --vx-rail-w: 190px;
    }

    .vx-app.vx-rail-expanded .vx-rail {
        align-items: stretch;
        padding-inline: var(--vx-space-3);
        /* Redundant under the global reset in premium.css, kept because
           this width is only correct with border-box: content-box would add
           the inline padding on top of --vx-rail-w and make the rail 25px
           wider than the margin .vx-main reserves for it. */
        box-sizing: border-box;
    }

    .vx-app.vx-rail-expanded .vx-rail-icon {
        width: 100%;
        justify-content: flex-start;
        gap: var(--vx-space-2);
        padding: 0 var(--vx-space-2);
    }

    .vx-app.vx-rail-expanded .vx-rail-label {
        display: block;
        font-size: 13px;
        text-align: start;
    }

    .vx-app.vx-rail-expanded .vx-rail-categories {
        width: 100%;
    }

    /* The hover tooltip is redundant once the name is on screen, and it
       would sit on top of the widened rail. */
    .vx-app.vx-rail-expanded .vx-rail-icon[data-tooltip]::after,
    .vx-app.vx-rail-expanded .vx-rail-icon[data-tooltip]::before {
        display: none;
    }
}

/* --- phone: labels under the icons in the bottom bar --- */
@media (max-width: 960px) {
    :root {
        --vx-rail-h-mobile: 64px;
    }

    .vx-rail-expand {
        display: none;
    }

    .vx-rail-icon {
        flex-direction: column;
        gap: 2px;
        width: auto;
        min-width: 52px;
        height: auto;
        padding: 5px 6px;
    }

    .vx-rail-label {
        display: block;
    }
}

/* ---------- dashboard hierarchy ---------- */
/* The dashboard used to be ten stat cards at one size in a ragged 6+4
   grid, so total revenue read no louder than the equipment count. The
   first four cards (in the staff member's own saved order) get the
   headline treatment; the rest become a compact strip that is still one
   tap away but no longer competes for attention. */
:where(.vx-app) .vx-hero-stats {
    display: grid;
    /* 150px lets a phone fit two per row instead of one, so the headline
       figures and the trend chart below them are reachable without four
       screens of scrolling; a desktop still lays all four across. */
    grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
    gap: var(--vx-space-3);
    margin-bottom: var(--vx-space-4);
}

:where(.vx-app) .vx-hero-stat .premium-stat-value {
    font-size: 30px;
}

:where(.vx-app) .vx-trend-card {
    max-width: 100%;
    margin-bottom: var(--vx-space-4);
}

:where(.vx-app) .vx-trend-title {
    margin: 0 0 14px;
    font-size: 15px;
}

:where(.vx-app) .vx-mini-stats {
    display: flex;
    flex-wrap: wrap;
    gap: var(--vx-space-2);
    margin-bottom: var(--vx-space-4);
}

:where(.vx-app) .vx-mini-stat {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-radius: var(--vx-radius-sm);
    border: 0.5px solid var(--premium-border);
    background: var(--premium-panel);
    color: var(--premium-text-muted);
    text-decoration: none;
    font-size: 13px;
    transition: border-color 0.15s var(--premium-ease), color 0.15s var(--premium-ease);
}

:where(.vx-app) .vx-mini-stat:hover {
    border-color: var(--premium-accent);
    color: var(--premium-accent);
}

:where(.vx-app) .vx-mini-stat-icon {
    display: inline-flex;
    color: var(--premium-accent);
}

:where(.vx-app) .vx-mini-stat-value {
    font-weight: 600;
    color: var(--premium-text);
}

:where(.vx-app) .vx-mini-stat.is-warning .vx-mini-stat-value,
:where(.vx-app) .vx-mini-stat.is-warning .vx-mini-stat-icon {
    color: var(--premium-warning);
}

:where(.vx-app) .vx-mini-stat.is-caution .vx-mini-stat-value,
:where(.vx-app) .vx-mini-stat.is-caution .vx-mini-stat-icon {
    color: var(--premium-warning);
}

/* Point of sale: products beside the cart on a desktop, stacked on a
   phone. It was a hard "1.4fr 1fr" with no breakpoint, which held the
   page 24px wider than a 360px screen. */
:where(.vx-app) .vx-pos-layout {
    display: grid;
    grid-template-columns: 1fr;
    gap: 20px;
    align-items: start;
}

@media (min-width: 860px) {
    :where(.vx-app) .vx-pos-layout {
        grid-template-columns: 1.4fr 1fr;
    }
}

/* ---------- bulk action bar ---------- */
:where(.vx-app) .vx-bulk-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-top: 16px;
    padding: 12px 14px;
    border: 0.5px solid var(--premium-accent);
    border-radius: var(--vx-radius-md);
    background: var(--premium-accent-soft);
}

/* The class sets display:flex, which outranks the [hidden] attribute's
   UA display:none -- without this the bar shows "0 selected" on every
   page load instead of staying out of the way until something is ticked. */
:where(.vx-app) .vx-bulk-bar[hidden] {
    display: none;
}

:where(.vx-app) .vx-bulk-count {
    font-size: 13px;
    color: var(--premium-text);
    margin-inline-end: 4px;
}

:where(.vx-app) .vx-bulk-text {
    flex: 1;
    min-width: 180px;
    padding: 7px 12px;
    border-radius: var(--vx-radius-sm);
    border: 0.5px solid var(--premium-border);
    background: var(--premium-input-bg);
    color: var(--premium-text);
    font-family: inherit;
    font-size: 13px;
}

:where(.vx-app) .vx-bulk-cell {
    width: 34px;
}

:where(.vx-app) .vx-bulk-cell input {
    width: 17px;
    height: 17px;
    cursor: pointer;
}

/* ---------- chart empty state ---------- */
/* A chart with no data used to render anyway: a flat line against an
   invented -1..1 axis, or (for the doughnut) a blank white card. Both
   imply the axis means something. This replaces the canvas instead. */
:where(.vx-app) .vx-chart-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    gap: 6px;
    text-align: center;
    padding: 12px;
}

:where(.vx-app) .vx-chart-empty-icon {
    color: var(--premium-text-muted);
    opacity: 0.5;
}

:where(.vx-app) .vx-chart-empty-title {
    margin: 0;
    font-size: 14px;
    font-weight: 600;
    color: var(--premium-text);
}

:where(.vx-app) .vx-chart-empty-hint {
    margin: 0;
    font-size: 12px;
    color: var(--premium-text-muted);
}

:where(.vx-app) .vx-chart-empty:empty {
    display: none;
}

/* A doughnut with one category is a solid ring that encodes nothing --
   vx-charts.js swaps it for the figure itself. */
:where(.vx-app) .vx-chart-single {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    gap: 4px;
}

:where(.vx-app) .vx-chart-single-value {
    margin: 0;
    font-size: var(--fs-num);
    font-weight: 600;
    color: var(--premium-accent);
}

:where(.vx-app) .vx-chart-single-label {
    margin: 0;
    font-size: 13px;
    color: var(--premium-text-muted);
}

/* ==========================================================================
   Interaction layer
   Added after an audit found: :focus-visible on exactly one element in the
   whole system (while `outline: none` was set in four places, making the
   nav rail completely invisible to a keyboard user), :active on exactly one
   button, and no :disabled styling anywhere.
   ========================================================================== */

/* --- keyboard focus -------------------------------------------------- */
/* One ring, on everything focusable, drawn outside the element so it is
   never clipped by a rounded corner or an overflow:hidden parent. */
:where(.vx-app, .premium-page) :is(
    a, button, [role="button"], summary,
    input, select, textarea, [tabindex]:not([tabindex="-1"])
):focus-visible {
    outline: 2px solid var(--premium-accent);
    outline-offset: 2px;
    border-radius: var(--vx-radius-sm);
}

/* Inputs draw their own accent border + ring on :focus already; adding the
   outline on top would double it. Keep the outline only for keyboard. */
:where(.vx-app) :is(input, select, textarea):focus:not(:focus-visible) {
    outline: none;
}

/* --- press feedback --------------------------------------------------- */
/* .premium-btn was the only element in the system that acknowledged a
   press. .vx-btn is the primary family, used across 75 templates.

   The first pass used scale(0.97) alone -- measurably applied, but a 3%
   shrink on a 36px control moves its edge by half a pixel, which is not
   something a hand feels. A press now reads on three channels at once:
   the control sinks a whole pixel, shrinks enough to see, and loses its
   lift (the shadow flattens and the surface darkens), which is what a
   physical button does. Duration stays at --vx-dur-fast; the point is to
   land instantly under the finger, not to animate. */
/* transition-duration: 0s is the whole point of this rule.
   Measured on the live site: at the instant of pointerdown, with :active
   already matching, the button was still at scale(1)/brightness(1) -- the
   press was being *animated in* over 0.12s while a real click lasts 60-100ms,
   so the release began easing back before the effect had arrived. The press
   now lands on the first frame; releasing drops :active, the base transition
   takes over again and eases it back out. Snap in, ease out. */
:where(.vx-app, .premium-page) :is(.vx-btn, .vx-btn-sm, .premium-btn, .premium-btn-sm, .premium-toggle-btn, .vx-mini-stat, .vx-portal-tab, .vx-view-btn):active {
    transition-duration: 0s;
    transform: scale(0.93) translateY(1px);
    box-shadow: inset 0 3px 8px rgba(16, 24, 40, 0.3);
    filter: brightness(0.9);
}

:where(.vx-app, .premium-page) :is(.vx-rail-icon, .vx-rail-expand, .vx-flyout-link, .vx-cmdk-item, .vx-panel-link, .premium-link):active {
    transition-duration: 0s;
    transform: scale(0.9) translateY(1px);
    filter: brightness(0.88);
}

/* A card or row that is a link should acknowledge a press too -- these are
   the biggest click targets in the system and were completely inert. */
:where(.vx-app) :is(a.vx-kanban-card, .vx-alert-item, .vx-cards-view tr, .vx-panel-link):active {
    transition-duration: 0s;
    transform: scale(0.99);
    filter: brightness(0.95);
}

:where(.vx-app) .vx-btn {
    /* filter was being changed on :hover for the primary variant but was
       not in the transition list, so that hover snapped. */
    transition: background var(--vx-dur) var(--premium-ease),
                color var(--vx-dur) var(--premium-ease),
                box-shadow var(--vx-dur) var(--premium-ease),
                border-color var(--vx-dur) var(--premium-ease),
                filter var(--vx-dur) var(--premium-ease),
                transform var(--vx-dur-fast) var(--premium-ease);
}

:where(.vx-app, .premium-page) :is(.vx-rail-icon, .vx-rail-expand, .vx-flyout-link, .premium-toggle-btn, .premium-btn, .premium-btn-sm, .vx-mini-stat, .vx-portal-tab, .vx-cmdk-item, .vx-view-btn) {
    /* filter and box-shadow are what the press state changes, so they have
       to be in the list or the release snaps instead of easing back. */
    transition-property: background, color, border-color, box-shadow, transform, filter;
    transition-duration: var(--vx-dur);
    transition-timing-function: var(--premium-ease);
}

/* --- disabled --------------------------------------------------------- */
/* There was no :disabled styling at all -- a genuinely disabled button
   looked identical to an enabled one. */
:where(.vx-app, .premium-page) :is(button, .vx-btn, .premium-btn, .premium-toggle-btn, input, select, textarea):disabled,
:where(.vx-app, .premium-page) :is(button, .vx-btn, .premium-btn)[aria-disabled="true"] {
    opacity: 0.55;
    cursor: not-allowed;
    filter: none;
    transform: none;
    box-shadow: none;
}

/* --- the rail no longer snaps between its two widths ------------------ */
@media (min-width: 961px) {
    .vx-app .vx-rail,
    .vx-app .vx-main {
        transition: width var(--vx-dur-slow) var(--premium-ease),
                    margin-inline-start var(--vx-dur-slow) var(--premium-ease),
                    padding var(--vx-dur-slow) var(--premium-ease);
    }
}

/* --- floating surfaces get an entrance ------------------------------- */
@keyframes vxPopIn {
    from { opacity: 0; transform: translateY(-4px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

.vx-preview-popover.is-open {
    animation: vxPopIn var(--vx-dur) var(--premium-ease);
}

.vx-tooltip.is-visible {
    animation: vxPopIn var(--vx-dur-fast) var(--premium-ease);
}

/* --- respect the OS setting ------------------------------------------- */
/* There was no prefers-reduced-motion handling anywhere in the repo, while
   an infinite shimmer loop and transform-based hovers were live. Motion is
   removed rather than merely shortened; state changes stay instant and
   fully visible. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    /* This used to be `transform: none !important`, which -- because of the
       !important -- was the one rule in the file that could beat the press
       state, and it removed the movement completely for anyone with
       reduce-motion on. That over-read the setting: reduce-motion is about
       animation, and a press is a state change with a 0s duration, not an
       animation. What it should drop is the *travel*, not the feedback, so
       the scale is kept and merely made shallower, and translateY (the part
       that actually reads as movement) is dropped. Everything above is what
       supplies the darkening, which stays either way. */
    :where(.vx-app, .premium-page) :is(.vx-btn, .vx-btn-sm, .premium-btn, .premium-btn-sm, .premium-toggle-btn, .vx-mini-stat, .vx-portal-tab, .vx-view-btn, .vx-rail-icon, .vx-rail-expand, .vx-flyout-link, .vx-cmdk-item, .vx-panel-link, .premium-link):active {
        /* !important only to beat the blanket 0.01ms above, which is itself
           !important. Without it the press arrives on the second frame here
           instead of the first -- measured at 32ms rather than 0ms. */
        transition-duration: 0s !important;
        transform: scale(0.96);
    }

    /* Anything else that scales on press loses it entirely -- cards and rows
       are large surfaces where the movement is the only thing being said. */
    :where(.vx-app) :is(a.vx-kanban-card, .vx-alert-item, .vx-cards-view tr):active {
        transform: none;
    }
}

/* Dialogs animated in but disappeared instantly. */
@keyframes vxCmdkOut {
    from { opacity: 1; transform: none; }
    to   { opacity: 0; transform: translateY(-6px) scale(0.985); }
}

.vx-cmdk-overlay.is-closing {
    display: flex;
    pointer-events: none;
}

.vx-cmdk-overlay.is-closing .vx-cmdk {
    animation: vxCmdkOut var(--vx-dur) var(--premium-ease) forwards;
}

/* ---------- confirm dialog + busy buttons ---------- */
/* Replaces the native window.confirm() that fired on every destructive
   POST -- the one surface that ignored the product's own design. */
/* .vx-cmdk-overlay anchors its child near the top, which is right for a
   command palette and wrong for a confirmation. */
.vx-confirm-overlay.is-open,
.vx-confirm-overlay.is-closing {
    /* Explicit display:flex -- align-items/justify-content are inert on the
       block layout this overlay otherwise computes to. */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--vx-space-4);
}

:where(.vx-app, .premium-page) .vx-confirm {
    /* Redundant under the global reset; the width below is only correct
       with border-box. */
    box-sizing: border-box;
    width: min(400px, calc(100vw - 32px));
    background: var(--premium-panel);
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-md);
    box-shadow: var(--premium-shadow-lg);
    padding: var(--vx-space-5) var(--vx-space-4c);
    animation: vxCmdkIn var(--vx-dur) var(--premium-ease);
}

:where(.vx-app, .premium-page) .vx-confirm-title {
    margin: 0 0 var(--vx-space-2);
    font-size: var(--vx-text-lg);
    font-weight: 600;
    color: var(--premium-text);
}

:where(.vx-app, .premium-page) .vx-confirm-body {
    margin: 0;
    font-size: var(--vx-text-base);
    color: var(--premium-text-muted);
    line-height: 1.6;
}

:where(.vx-app, .premium-page) .vx-confirm-body:empty {
    display: none;
}

:where(.vx-app, .premium-page) .vx-confirm-actions {
    display: flex;
    gap: var(--vx-space-2);
    justify-content: flex-start;
    margin-top: var(--vx-space-5);
}

.vx-confirm-overlay.is-closing .vx-confirm {
    animation: vxCmdkOut var(--vx-dur) var(--premium-ease) forwards;
}

/* --- a button that is waiting on the server --------------------------- */
/* Nothing in the system had a pending state, so a double click sent the
   request twice -- a duplicated payment row, not a cosmetic problem. */
@keyframes vxSpin {
    to { transform: rotate(360deg); }
}

:where(.vx-app, .premium-page) .is-busy {
    position: relative;
    color: transparent !important;
    pointer-events: none;
}

:where(.vx-app, .premium-page) .is-busy::after {
    content: "";
    position: absolute;
    inset: 0;
    margin: auto;
    width: 15px;
    height: 15px;
    border-radius: 50%;
    border: 2px solid currentColor;
    border-top-color: transparent;
    /* currentColor is transparent on the button itself, so the ring needs
       its own colour taken from the accent. */
    border-color: var(--premium-accent) var(--premium-accent) var(--premium-accent) transparent;
    animation: vxSpin 0.6s linear infinite;
}

:where(.vx-app, .premium-page) .vx-btn-primary.is-busy::after {
    border-color: #fff #fff #fff transparent;
}

/* --- list skeletons --------------------------------------------------- */
:where(.vx-app) .vx-skeleton-row {
    height: 44px;
    border-radius: var(--vx-radius-sm);
    margin-bottom: var(--vx-space-2);
    background: linear-gradient(90deg,
        var(--premium-hover-tint) 25%,
        var(--premium-border) 37%,
        var(--premium-hover-tint) 63%);
    background-size: 200% 100%;
    animation: vxSkeletonShimmer 1.2s ease-in-out infinite;
}

/* Stale results should read as stale while a newer query is in flight. */
:where(.vx-app) .vx-cmdk-results.is-loading {
    opacity: 0.45;
    transition: opacity var(--vx-dur) var(--premium-ease);
}

/* ---------- view switcher ---------- */
:where(.vx-app) .vx-view-switch {
    display: inline-flex;
    gap: 2px;
    padding: 3px;
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-sm);
    background: var(--premium-input-bg);
}

:where(.vx-app) .vx-view-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 11px;
    border-radius: 7px;
    font-size: var(--vx-text-sm);
    color: var(--premium-text-muted);
    text-decoration: none;
    white-space: nowrap;
    transition: background var(--vx-dur) var(--premium-ease),
                color var(--vx-dur) var(--premium-ease);
}

:where(.vx-app) .vx-view-btn:hover {
    background: var(--premium-hover-tint);
    color: var(--premium-text);
}

:where(.vx-app) .vx-view-btn.is-active {
    background: var(--premium-panel);
    color: var(--premium-accent);
    font-weight: 600;
    box-shadow: var(--premium-shadow);
}

/* The label is the affordance on a desktop; on a phone the row would wrap,
   so the icon carries it and the title attribute keeps the name reachable. */
@media (max-width: 700px) {
    :where(.vx-app) .vx-view-btn-label {
        display: none;
    }
}

/* ---------- breadcrumb ---------- */
/* "Where am I": القسم ‹ التبويب. Sits above the section tab strip (and
   still renders even on the handful of pages where the strip itself is
   hidden because there's only one tab -- knowing the section doesn't
   depend on there being a choice of tabs). */
:where(.vx-app) .vx-breadcrumb {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--vx-text-sm);
    color: var(--premium-text-muted);
    margin-bottom: var(--vx-space-2);
}

:where(.vx-app) .vx-breadcrumb-sep {
    color: var(--premium-text-muted);
    opacity: 0.6;
}

:where(.vx-app) .vx-breadcrumb-current {
    color: var(--premium-text);
    font-weight: 600;
}

/* ---------- section tabs ---------- */
/* The rail category flyout's replacement: every sibling page inside one
   section (e.g. "الأعضاء") as one big, clear tab strip instead of a
   submenu you had to open first. Renders only when the section_tabs tag
   found more than one tab the logged-in staff can open. */
:where(.vx-app) .vx-section-tabs {
    display: flex;
    gap: var(--vx-space-2);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border-bottom: 0.5px solid var(--premium-border);
    margin-bottom: var(--vx-space-5);
}

:where(.vx-app) .vx-section-tab {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 12px 16px;
    font-size: var(--vx-text-lg);
    font-weight: 500;
    color: var(--premium-text-muted);
    text-decoration: none;
    white-space: nowrap;
    border-bottom: 2px solid transparent;
    transition: color var(--vx-dur) var(--premium-ease),
                border-color var(--vx-dur) var(--premium-ease);
}

:where(.vx-app) .vx-section-tab:hover {
    color: var(--premium-text);
}

:where(.vx-app) .vx-section-tab.is-active {
    color: var(--premium-accent);
    background: var(--ctx-bg, var(--premium-accent-soft));
    border-bottom-color: var(--ctx, var(--premium-accent));
    font-weight: 600;
    border-radius: var(--vx-radius-sm) var(--vx-radius-sm) 0 0;
}

/* ---------- cards view ---------- */
/* Reuses the data-label / data-card-title / data-card-actions contract that
   .vx-cards-sm already established for mobile, so a page opting into cards
   needs no second set of markup. */
:where(.vx-app) .premium-table.vx-cards-view {
    display: block;
    min-width: 0;
    width: auto;
}

:where(.vx-app) .premium-table.vx-cards-view thead {
    display: none;
}

:where(.vx-app) .premium-table.vx-cards-view tbody {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
    gap: var(--vx-space-3);
}

:where(.vx-app) .premium-table.vx-cards-view tr {
    display: flex;
    flex-direction: column;
    background: var(--premium-panel);
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-md);
    box-shadow: var(--premium-shadow);
    padding: 4px 0;
    transition: box-shadow var(--vx-dur) var(--premium-ease),
                transform var(--vx-dur) var(--premium-ease);
}

:where(.vx-app) .premium-table.vx-cards-view tr:hover {
    box-shadow: var(--premium-shadow-lg);
    transform: translateY(-2px);
}

:where(.vx-app) .premium-table.vx-cards-view td {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    align-items: center;
    justify-content: space-between;
    padding: 8px 16px;
    border-bottom: 0.5px solid var(--premium-border);
}

:where(.vx-app) .premium-table.vx-cards-view td:last-child {
    border-bottom: none;
}

:where(.vx-app) .premium-table.vx-cards-view td[data-label]::before {
    content: attr(data-label);
    color: var(--premium-text-muted);
    font-size: var(--vx-text-sm);
    flex: 0 0 auto;
}

:where(.vx-app) .premium-table.vx-cards-view td[data-card-title] {
    justify-content: flex-start;
    padding-top: 14px;
    font-size: var(--vx-text-lg);
    font-weight: 600;
}

:where(.vx-app) .premium-table.vx-cards-view td[data-card-actions] {
    justify-content: flex-start;
    gap: 8px;
    padding-bottom: 14px;
}

/* A cards grid has no horizontal overflow to manage. */
:where(.vx-app) .premium-table-wrap:has(.vx-cards-view) {
    overflow: visible;
    background: transparent;
    border: none;
    box-shadow: none;
}

/* ---------- kanban ---------- */
:where(.vx-app) .vx-kanban {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(260px, 1fr);
    gap: var(--vx-space-3);
    overflow-x: auto;
    padding-bottom: var(--vx-space-2);
    align-items: start;
}

:where(.vx-app) .vx-kanban-col {
    background: var(--premium-hover-tint);
    border-radius: var(--vx-radius-md);
    padding: var(--vx-space-3);
    min-height: 120px;
}

:where(.vx-app) .vx-kanban-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--vx-space-2);
    margin-bottom: var(--vx-space-3);
    font-size: var(--vx-text-base);
    font-weight: 600;
    color: var(--premium-text);
}

:where(.vx-app) .vx-kanban-count {
    font-size: var(--fs-micro);
    font-weight: 500;
    color: var(--premium-text-muted);
    background: var(--premium-panel);
    border-radius: var(--r-pill);
    padding: 1px 7px;
}

:where(.vx-app) .vx-kanban-card {
    display: block;
    background: var(--premium-panel);
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-sm);
    box-shadow: var(--premium-shadow);
    padding: var(--vx-space-3);
    margin-bottom: var(--vx-space-2);
    text-decoration: none;
    color: inherit;
    transition: box-shadow var(--vx-dur) var(--premium-ease),
                transform var(--vx-dur) var(--premium-ease);
}

:where(.vx-app) a.vx-kanban-card:hover {
    box-shadow: var(--premium-shadow-lg);
    transform: translateY(-2px);
}

:where(.vx-app) .vx-kanban-card-title {
    font-size: var(--vx-text-md);
    font-weight: 600;
    color: var(--premium-text);
    margin-bottom: 4px;
}

:where(.vx-app) .vx-kanban-card-meta {
    font-size: var(--vx-text-sm);
    color: var(--premium-text-muted);
    display: flex;
    flex-wrap: wrap;
    gap: 4px 10px;
}

:where(.vx-app) .vx-kanban-empty {
    font-size: var(--vx-text-sm);
    color: var(--premium-text-muted);
    text-align: center;
    padding: var(--vx-space-4) 0;
}

/* Drag-and-drop is only wired where a status-change endpoint exists. */
:where(.vx-app) .vx-kanban-card[draggable="true"] {
    cursor: grab;
}

:where(.vx-app) .vx-kanban-card.is-dragging {
    opacity: 0.45;
    cursor: grabbing;
}

:where(.vx-app) .vx-kanban-col.is-drop-target {
    outline: 2px dashed var(--premium-accent);
    outline-offset: -4px;
}

/* The status select on a kanban card: the touch and keyboard path for a
   move, since HTML5 drag fires on neither. */
:where(.vx-app) .vx-kanban-card-status {
    width: 100%;
    margin-top: var(--vx-space-2);
    padding: 5px 8px;
    font-size: var(--vx-text-sm);
    border-radius: 7px;
}

:where(.vx-app) .vx-kanban-card.is-saving {
    opacity: 0.6;
    pointer-events: none;
}

/* ==========================================================================
   Calendar view (_vx_calendar.html)
   The switcher listed 'calendar' from the start with no page offering it.
   The month heat-map on /calendar/ keeps its own inline styles: it shows a
   count per day, this shows the rows themselves, so they are not the same
   component. The explicit box-sizing on each box here predates the global
   reset in premium.css and is now redundant -- the overflow it was written
   to stop is one of the three bugs that reset was added for.
   ========================================================================== */

:where(.vx-app) .vx-cal {
    box-sizing: border-box;
    width: 100%;
    min-width: 0;
}

:where(.vx-app) .vx-cal-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--vx-space-3);
    flex-wrap: wrap;
    margin-bottom: var(--vx-space-3);
}

:where(.vx-app) .vx-cal-nav,
:where(.vx-app) .vx-cal-bar-end {
    display: flex;
    align-items: center;
    gap: var(--vx-space-2);
}

:where(.vx-app) .vx-cal-month {
    font-size: var(--vx-text-md);
    font-weight: 600;
    color: var(--premium-text);
    min-width: 8ch;
    text-align: center;
}

:where(.vx-app) .vx-cal-count {
    font-size: var(--vx-text-sm);
    color: var(--premium-text-muted);
}

:where(.vx-app) .vx-cal-grid {
    display: grid;
    /* minmax(0, 1fr) rather than 1fr: a long member name in one cell would
       otherwise widen its column and push the grid past the viewport. */
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 6px;
    margin-bottom: 6px;
}

:where(.vx-app) .vx-cal-head {
    box-sizing: border-box;
    padding: 6px 4px;
    text-align: center;
    font-size: var(--vx-text-sm);
    font-weight: 600;
    color: var(--premium-text-muted);
}

:where(.vx-app) .vx-cal-head-row {
    margin-bottom: 2px;
}

:where(.vx-app) .vx-cal-cell {
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
    min-height: 96px;
    padding: 6px;
    border: 0.5px solid var(--premium-border);
    border-radius: var(--vx-radius-sm);
    background: var(--premium-panel);
}

:where(.vx-app) .vx-cal-cell.is-outside {
    background: var(--premium-hover-tint);
    opacity: 0.6;
}

:where(.vx-app) .vx-cal-cell.is-today {
    border-color: var(--premium-accent);
    box-shadow: inset 0 0 0 1px var(--premium-accent);
}

:where(.vx-app) .vx-cal-daynum {
    font-size: var(--vx-text-sm);
    font-weight: 600;
    color: var(--premium-text-muted);
    text-align: start;
}

:where(.vx-app) .vx-cal-cell.is-today .vx-cal-daynum {
    color: var(--premium-accent);
}

:where(.vx-app) .vx-cal-entry {
    box-sizing: border-box;
    display: block;
    min-width: 0;
    padding: 3px 6px;
    border-radius: 6px;
    background: var(--premium-accent-soft);
    color: var(--premium-accent);
    font-size: var(--vx-text-xs);
    line-height: 1.4;
    text-decoration: none;
    /* One line per entry, clipped -- a wrapping name would make the rows
       different heights and the grid stop reading as a calendar. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: background var(--vx-dur) var(--premium-ease),
                transform var(--vx-dur-fast) var(--premium-ease);
}

:where(.vx-app) a.vx-cal-entry:hover {
    background: var(--premium-accent);
    color: #fff;
}

:where(.vx-app) a.vx-cal-entry:active {
    transform: scale(0.96);
}

:where(.vx-app) .vx-cal-entry-sub {
    font-variant-numeric: tabular-nums;
    opacity: 0.75;
    margin-inline-end: 4px;
}

/* Tones are for state, never decoration -- same rule the charts follow. */
:where(.vx-app) .vx-cal-entry.is-muted {
    background: var(--premium-hover-tint);
    color: var(--premium-text-muted);
}

:where(.vx-app) .vx-cal-entry.is-warning {
    background: var(--premium-warning-bg, var(--premium-hover-tint));
    color: var(--premium-warning-text, var(--premium-text));
}

:where(.vx-app) .vx-cal-more {
    font-size: var(--vx-text-xs);
    color: var(--premium-text-muted);
    padding-inline-start: 6px;
}

:where(.vx-app) .vx-cal-empty {
    margin-top: var(--vx-space-3);
    text-align: center;
}

/* A seven-column grid of readable cells does not fit a phone. The month
   becomes a list of the days that actually have something on them, which
   is the part anyone opens a calendar on a phone to see. */
@media (max-width: 700px) {
    :where(.vx-app) .vx-cal-head-row {
        display: none;
    }

    :where(.vx-app) .vx-cal-grid {
        grid-template-columns: 1fr;
        gap: 4px;
        margin-bottom: 4px;
    }

    :where(.vx-app) .vx-cal-cell {
        min-height: 0;
        flex-direction: row;
        align-items: center;
        flex-wrap: wrap;
        gap: 6px;
    }

    /* An empty day would be a row of nothing, 31 times over. */
    :where(.vx-app) .vx-cal-cell:not(:has(.vx-cal-entry)) {
        display: none;
    }

    :where(.vx-app) .vx-cal-daynum {
        min-width: 3ch;
    }

    :where(.vx-app) .vx-cal-entry {
        flex: 1 1 auto;
    }
}

/* ==========================================================================
   Navigation progress bar (vx-progress.js)
   The submit busy state is correct but only on screen for the 100-300ms
   before the document is swapped, which is why the app still felt like
   nothing happened on a click. This is the part that is actually legible.
   Unscoped on purpose: it is appended to <body> and has to work on the
   standalone pages (login, receipt) that have no .vx-app wrapper.
   ========================================================================== */

.vx-progress {
    position: fixed;
    top: 0;
    inset-inline: 0;
    height: 3px;
    /* Above the rail (60) and the flyouts, below the dialogs (300) -- a
       confirm dialog must never have a bar drawn over it. */
    z-index: 250;
    background: var(--premium-accent);
    box-shadow: 0 0 10px 1px var(--premium-accent-glow);
    /* Scaling one element beats animating width: it stays on the compositor,
       so the bar keeps moving while the browser is busy fetching.

       transform-origin takes no logical keywords -- `inline-start` is not a
       valid value, and an invalid one silently falls back to the 50% default,
       which grew the bar outwards from the middle of the screen. It has to be
       stated physically and flipped by direction. */
    transform: scaleX(0);
    transform-origin: left center;
    opacity: 0;
    pointer-events: none;
    /* The opacity transition here is the fade OUT. Appearing is instant --
       see .is-active below -- because a bar that fades in over 0.18s on a
       page that loads in 250ms never becomes visible. */
    transition: transform 0.18s linear, opacity 0.18s var(--premium-ease);
}

[dir="rtl"] .vx-progress {
    transform-origin: right center;
}

.vx-progress.is-active {
    opacity: 1;
    /* No fade-in: full opacity on the frame it is added. */
    transition: transform 0.18s linear, opacity 0s;
}

@media (prefers-reduced-motion: reduce) {
    /* The bar is feedback, not decoration, so it stays -- but it should not
       creep. It appears at full width and simply fades. */
    .vx-progress {
        transition: opacity 0.01ms;
        transform: scaleX(1) !important;
    }
}

/* ==========================================================================
   Greeting avatar (dashboard, next to "أهلاً <name>")
   Was a bare <img class="premium-avatar is-lg"> with no link, no cursor and
   no hover -- decoration sitting in the one place a person looks for their
   own account. It is a control now, and it says so before it is clicked.
   ========================================================================== */

:where(.vx-app) .vx-greet-avatar {
    box-sizing: border-box;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 48px: it is the largest avatar in the product and the one most likely
       to be aimed at, and it comfortably clears the 44px pointer floor. */
    width: 48px;
    height: 48px;
    flex-shrink: 0;
    border-radius: 50%;
    overflow: hidden;
    text-decoration: none;
    box-shadow: 0 0 0 0 var(--premium-accent-glow);
    transition: box-shadow var(--vx-dur) var(--premium-ease),
                transform var(--vx-dur) var(--premium-ease);
}

:where(.vx-app) .vx-greet-avatar-initial {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    background: var(--premium-accent-soft);
    color: var(--premium-accent);
    font-size: 18px;
    font-weight: 600;
    /* The name may be Latin or Arabic; uppercase only affects the former and
       makes a lowercase initial look deliberate rather than accidental. */
    text-transform: uppercase;
}

:where(.vx-app) .vx-greet-avatar img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The affordance: a person icon fades up over the photo on hover, so the
   circle reads as "go to my account" rather than "here is a picture". */
:where(.vx-app) .vx-greet-avatar-hint {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    background: color-mix(in srgb, var(--premium-accent) 72%, transparent);
    opacity: 0;
    transition: opacity var(--vx-dur) var(--premium-ease);
}

:where(.vx-app) .vx-greet-avatar:hover {
    box-shadow: 0 0 0 3px var(--premium-accent-glow);
    transform: scale(1.06);
}

:where(.vx-app) .vx-greet-avatar:hover .vx-greet-avatar-hint,
:where(.vx-app) .vx-greet-avatar:focus-visible .vx-greet-avatar-hint {
    opacity: 1;
}

/* Instant, like every other press in the system -- see the note on the
   press layer: animating a press in over 0.12s means a real click releases
   before it arrives. */
:where(.vx-app) .vx-greet-avatar:active {
    transition-duration: 0s;
    transform: scale(0.92);
}

@media (prefers-reduced-motion: reduce) {
    :where(.vx-app) .vx-greet-avatar:hover {
        transform: none;
    }

    :where(.vx-app) .vx-greet-avatar:active {
        transition-duration: 0s !important;
        transform: scale(0.96);
    }
}

/* The greeting row is tight on a phone; the avatar keeps its full size
   because it is a target, and the heading gives up the space instead. */
@media (max-width: 700px) {
    :where(.vx-app) .vx-greet-avatar {
        width: 44px;
        height: 44px;
    }

    :where(.vx-app) .vx-greet-avatar-initial {
        font-size: 16px;
    }
}

/* ==========================================================================
   Gate cards (finance PIN)
   The PIN screens used the wide inner-page shell, so the one screen that
   stops a staff member mid-task looked like an unfinished form instead of
   part of the product. They use the login page's card now; these are the
   two pieces that card did not already have.
   ========================================================================== */

:where(.premium-page) .vx-gate-note {
    box-sizing: border-box;
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-bottom: 20px;
    padding: 12px 14px;
    border-radius: var(--vx-radius-sm, 10px);
    background: var(--premium-accent-soft);
    color: var(--premium-text);
    font-size: 13px;
    line-height: 1.65;
}

:where(.premium-page) .vx-gate-note-icon {
    display: flex;
    flex-shrink: 0;
    /* Optical alignment with the first line of text rather than the box. */
    margin-top: 1px;
    color: var(--premium-accent);
}

:where(.premium-page) .vx-gate-success {
    margin: 0 0 16px;
    padding: 12px 14px;
    border-radius: var(--vx-radius-sm, 10px);
    background: color-mix(in srgb, var(--premium-success, #0ca30c) 12%, transparent);
    color: var(--premium-success, #0ca30c);
    font-size: 13px;
    text-align: center;
}

/* A code is read back digit by digit, so it gets the spacing and the
   monospaced figures to match -- and centring makes a short code look
   deliberate in a field sized for an email address. */
/* .premium-field input is (0,2,0) and comes from premium.css, so the bare
   class loses the font-size. Matching its shape rather than reaching for
   !important. */
:where(.premium-page) .premium-field input.vx-pin-input,
:where(.premium-page) input.vx-pin-input {
    text-align: center;
    letter-spacing: 0.5em;
    /* letter-spacing adds a trailing gap after the last character, which
       throws the centring off by half a space without this. */
    text-indent: 0.5em;
    font-size: 19px;
    font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   Entrance choreography (vx-enter.js)
   The starting state is applied by JavaScript, never by this stylesheet, so
   a script that fails to run leaves a normal fully-visible page instead of a
   blank one. That is why .vx-enter-item is only ever added by the script.
   ========================================================================== */

:where(.vx-app) .vx-enter-item {
    opacity: 0;
    transform: translateY(10px);
}

:where(.vx-app) .vx-enter-item.is-in {
    opacity: 1;
    transform: none;
    transition: opacity 0.26s var(--premium-ease) var(--vx-enter-delay, 0ms),
                transform 0.26s var(--premium-ease) var(--vx-enter-delay, 0ms);
}

/* A number counting up must not resize its card while it counts. */
:where(.vx-app) [data-vx-count] {
    font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
    /* Travel goes, the reveal stays -- and the stagger goes with it, so the
       page arrives at once rather than rippling. */
    :where(.vx-app) .vx-enter-item {
        transform: none;
    }

    :where(.vx-app) .vx-enter-item.is-in {
        transform: none;
        transition: opacity 0.01ms !important;
    }
}

/* ==========================================================================
   Hero figures
   The dashboard's first cards carry the numbers the day is judged on. They
   were the same weight as everything else on the screen.
   ========================================================================== */

:where(.vx-app) .vx-hero-stat .premium-stat-value {
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
}

:where(.vx-app) .vx-hero-stat {
    position: relative;
    overflow: hidden;
}

/* ==========================================================================
   SVG chart draw-in (charts.py::bar_chart_svg)
   The four server-rendered charts never animated at all, unlike the eight
   Chart.js ones. The starting state is applied by adding .is-armed from
   script, so a chart with no JS is simply drawn at full height.
   ========================================================================== */

:where(.vx-app) .vx-svg-chart.is-armed .vx-svg-bar {
    transform: scaleY(0);
}

:where(.vx-app) .vx-svg-chart.is-drawn .vx-svg-bar {
    transform: none;
    transition: transform 0.42s var(--premium-ease) var(--vx-bar-delay, 0ms);
}

@media (prefers-reduced-motion: reduce) {
    /* No growth; the bars just fade up together, which says "this is new"
       without anything moving. */
    :where(.vx-app) .vx-svg-chart.is-armed .vx-svg-bar {
        transform: none;
        opacity: 0;
    }

    :where(.vx-app) .vx-svg-chart.is-drawn .vx-svg-bar {
        opacity: 1;
        transition: opacity 0.01ms !important;
    }
}

/* ==========================================================================
   Cross-document transitions
   The white flash between pages has two separate causes and both are fixed
   here. First, the browser paints its own canvas before our CSS applies, so
   an explicit background on the root removes the flash even where the
   transition below is unsupported. Second, one document replacing another is
   an instant cut; @view-transition asks the browser to cross-fade it
   instead. Chrome and Edge honour it today; everywhere else simply keeps the
   current behaviour, so there is nothing to fall back from.
   ========================================================================== */

@view-transition {
    navigation: auto;
}

html {
    background: var(--premium-bg);
    /* Tells the browser which way to paint its own surfaces (scrollbars, the
       pre-paint canvas) so they are not light on a dark theme. */
    color-scheme: light dark;
}

::view-transition-old(root),
::view-transition-new(root) {
    /* Shorter than the browser default, and no slide: this is a page swap,
       not a slideshow. A cross-fade alone reads as "same app, new content";
       anything that moves reads as a card game. */
    animation-duration: 0.18s;
    animation-timing-function: var(--premium-ease);
}

@media (prefers-reduced-motion: reduce) {
    /* A cross-fade is opacity only, which is what reduce-motion asks for,
       but it should not linger. */
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation-duration: 0.01ms;
    }
}

/* ==========================================================================
   Scrollbars
   One treatment for every scroll container in the product. Previously only
   .vx-flyout-inner was styled, so a horizontal OS scrollbar -- 15px of grey
   slab, measured -- sat under every table on the dashboard.

   Deliberately visible rather than hover-only: premium.css switched these
   wrappers from overflow:hidden to overflow-x:auto precisely so a table
   wider than the screen would not silently lose its columns, and a
   scrollbar nobody can see until they hover takes that affordance back on
   the width where it matters most. So it is present, thin, and quiet, and
   it gains contrast on hover. 6px instead of 15 also gives nine pixels of
   height back to every panel it sits under.
   ========================================================================== */

:where(.vx-app, .premium-page) :is(
    .premium-table-wrap,
    .vx-flyout-inner,
    .vx-cmdk-results,
    .vx-kanban,
    .vx-rail-categories,
    .vx-scroll
) {
    /* Firefox: the only two knobs it has, and they are enough. */
    scrollbar-width: thin;
    scrollbar-color: var(--premium-border) transparent;
}

:where(.vx-app, .premium-page) :is(
    .premium-table-wrap,
    .vx-flyout-inner,
    .vx-cmdk-results,
    .vx-kanban,
    .vx-rail-categories,
    .vx-scroll
)::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}

:where(.vx-app, .premium-page) :is(
    .premium-table-wrap,
    .vx-flyout-inner,
    .vx-cmdk-results,
    .vx-kanban,
    .vx-rail-categories,
    .vx-scroll
)::-webkit-scrollbar-track {
    background: transparent;
}

:where(.vx-app, .premium-page) :is(
    .premium-table-wrap,
    .vx-flyout-inner,
    .vx-cmdk-results,
    .vx-kanban,
    .vx-rail-categories,
    .vx-scroll
)::-webkit-scrollbar-thumb {
    background: var(--premium-border);
    border-radius: 10px;
}

:where(.vx-app, .premium-page) :is(
    .premium-table-wrap,
    .vx-flyout-inner,
    .vx-cmdk-results,
    .vx-kanban,
    .vx-rail-categories,
    .vx-scroll
):hover::-webkit-scrollbar-thumb {
    background: var(--premium-text-muted);
}

/* The corner where a horizontal and vertical bar meet renders as a small
   opaque square in the OS colour if left alone. */
:where(.vx-app, .premium-page) ::-webkit-scrollbar-corner {
    background: transparent;
}

/* ==========================================================================
   Nav rail: nothing in it may ever be out of reach

   This was reported five separate times, and every previous fix was a patch
   on the symptom. The root cause was structural: whether the rail fitted
   the screen was a *human judgement*, encoded as five hand-tuned
   `max-height` steps, re-decided by hand every time a nav item was added.
   When the judgement was wrong the failure was silent -- an item rendered
   outside a box, or clipped by an `overflow`, or hidden by a `display:none`
   I had added myself -- and it always looked the same from the user's
   chair: "the top of the menu vanishes when I scroll down".

   It is arithmetic now. The icon box is *defined* as the viewport height
   divided by the rail's measured content ratio, so "does it fit" is a
   single inequality the browser re-solves on every resize. And the three
   mechanisms that can hide a child that exists in the DOM are all gone
   from this rail: no `display: none`, no `overflow` other than `visible`,
   and no `flex-shrink`. Above a 377px-tall viewport there is no longer a
   mechanism left for an item to go missing, rather than merely no known
   case of it happening.

   See the computed rule at the very end of this file.
   ========================================================================== */

.vx-rail-categories {
    /* `0 0 auto` is deliberate, and it is the load-bearing line of this
       whole fix. `flex-shrink: 1` plus `min-height: 0` is precisely the
       mechanism that lets this box become shorter than its own content, at
       which point its children render outside it with nothing able to
       reach them -- that is what "الإدارة disappeared" was. With no
       shrink, the box is always exactly as tall as its content, and
       fitting the viewport is the computed item size's job alone (see the
       end of this file). One responsibility per rule. */
    flex: 0 0 auto;
    /* NO inner scroll on a normal desktop height.
       An inner scroll is the one mechanism that can hide the *top* of the
       list, and hiding the top is exactly what was reported: "الرئيسية and
       الأعضاء disappear when I scroll down". A wheel over the rail scrolls
       the innermost scrollable box first, so the moment this box is
       scrollable by even a few pixels, pointing at the rail and scrolling
       the page walks the category list out of its own top edge -- while the
       rail itself never moves, which is precisely what "the menu is being
       dragged with the page" looks like.
       The height queries below already guarantee all thirteen items fit down
       to ~500px of viewport, so nothing needs to scroll above that. The
       scroll comes back only under the last query, where the alternative is
       clipping items with no way to reach them.
       Longhand again -- see the note on .vx-rail above. */
    overflow-y: visible;
}

/* ==========================================================================
   Short viewports: a computed size, not a hand-tuned ladder

   Five `@media (max-height: ...)` blocks used to live here. They are gone.
   They were compressing about 30% harder than they needed to at every step
   -- at 640px they gave a 30px icon (under the 32px touch floor) while the
   content added up to 442px in a 640px viewport, so 198px of the rail sat
   empty -- and, worse, they had to be re-tuned by hand every time a nav
   item was added, which is the mistake that kept coming back.
   ========================================================================== */

@media (min-width: 961px) {
    .vx-rail {
        /* Solve `slots * item <= viewport` for the item box, clamped to a
           comfortable 44px and floored at 24px.
           100svh, not vh and not dvh: svh is the *smallest* the viewport
           gets, so a rail that fits svh fits every state of the browser
           chrome, and unlike dvh it does not resize the icons while you
           scroll. The vh line above it is the fallback for an engine
           without svh. */
        --vx-rail-item: clamp(24px, (100vh - 8px) / 15.7, 44px);
        --vx-rail-item: clamp(24px, (100svh - 8px) / var(--vx-rail-slots), 44px);
        /* Declared alongside the item, not on :root -- see the note there.
           Every other box in the rail is a ratio of the icon box, so none of
           them can be left behind when the icon size changes. That is what
           the old ladder kept getting wrong: two of its five steps needed a
           hand-written override for the store mark alone. */
        --vx-rail-gap: calc(var(--vx-rail-item) * 0.15);
        --vx-rail-expand-h: calc(var(--vx-rail-item) * 0.7);
        --vx-rail-brand-size: calc(var(--vx-rail-item) * 0.85);
        height: 100vh;
        height: 100svh;
        padding-block: var(--vx-rail-gap);
        /* The only overflow value this rail may ever have on desktop.
           `hidden` is a scroll *container*: the browser scrolls it on its
           own to reveal a focused descendant and never scrolls back, so one
           Tab onto sign-out used to drag الرئيسية and the store mark off
           the top permanently -- the rail itself never moving, which is
           exactly what "the menu is being dragged with the page" looks
           like. Scoped inside this width query so it can never argue with
           the bottom bar's `overflow-x: auto` again; the two rules now
           apply to disjoint widths instead of racing on source order. */
        overflow: visible;
    }

    .vx-rail-categories {
        overflow: visible;
    }

    /* The Controller rail (accounts/_controller_nav.html) shares these
       classes with fewer children -- no expand toggle and no "حسابي" link
       -- so the gym rail's ratio would size it smaller than it needs to be.
       Measured the same way as --vx-rail-slots itself, on the أدمن role,
       which is the Controller's worst case (5 categories; a client account
       gets 3): content 257px at a 24px item, 257/24 = 10.72, rounded up.
       Measured, not estimated -- an estimate of 12.6 was 17% out. */
    .vx-rail:not(:has(.vx-rail-expand)) {
        --vx-rail-slots: 11;
    }

    /* Last resort, below the height at which even a 24px item fits (377px).
       The scroll goes on the rail, never on .vx-rail-categories: an inner
       scroll on the category list can hide the *top* of the list while the
       rail itself stays put, and that is the precise shape of the bug this
       whole section exists to end. */
    @media (max-height: 376px) {
        .vx-rail {
            overflow-y: auto;
            overscroll-behavior: contain;
            scrollbar-gutter: stable;
        }
    }
}

/* ============================================================
   File picker (static/vx-file.js)
   The native control's "Choose File / No file chosen" comes from the
   browser's UI language and can't be translated or themed from the page,
   so it was the one English pair inside an otherwise Arabic form. The
   real input stays in the DOM (it submits, it focuses, it's the a11y
   target) and is collapsed to zero visual size behind this pair.
   ============================================================ */
.vx-file {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    position: relative;
}
/* Not display:none and not visibility:hidden -- either would take the
   input out of the focus order, and the button in front of it is not a
   <label>, so there would be no keyboard path to the picker at all. */
.vx-file > input[type="file"] {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.vx-file__btn {
    display: inline-flex; align-items: center; gap: 7px;
    background: var(--surface-2, var(--premium-input-bg)); color: var(--text-1, var(--premium-text));
    border: var(--hair, .5px) solid var(--border, var(--premium-border));
    border-radius: var(--r-btn, 9px); padding: 9px 16px;
    font-family: inherit; font-size: var(--fs-body-sm, 13px); font-weight: 500;
    cursor: pointer; transition: background .15s ease, border-color .15s ease;
}
.vx-file__btn:hover { background: var(--premium-hover-tint); border-color: var(--ctx, var(--premium-accent)); }
.vx-file__btn:active { transform: scale(0.94) translateY(1px); filter: brightness(0.94); }
.vx-file.is-focus .vx-file__btn {
    outline: 2px solid var(--ctx, var(--premium-accent)); outline-offset: 2px;
}
.vx-file__name {
    font-size: var(--fs-body-sm, 13px); color: var(--text-3, var(--premium-text-muted));
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.vx-file__name.is-set { color: var(--text-1, var(--premium-text)); }

/* The one "عرض المؤشرات" switch (gym_system/_vx_stats_toggle.html).
   It used to exist twice -- .vxm-stats-toggle and .vxo-stats-toggle,
   identical apart from the prefix -- which is why it never reached the
   inventory, team or sales pages that have the same metric row. */
.vx-stats-toggle {
    display: flex; align-items: center; justify-content: flex-start;
    gap: 8px; margin-bottom: 10px; cursor: pointer;
}
.vx-stats-toggle span { font-size: 12px; color: var(--text-2); }
.vx-stats-toggle input {
    width: 15px; height: 15px; accent-color: var(--ctx, var(--premium-accent)); cursor: pointer;
}

/* The inline "ask for more credit" form on the messages page: a <details>
   so the request is one click away without a second screen, and closed by
   default because the answer most visits want is the balance above it. */
.vx-inline-request > summary::-webkit-details-marker { display: none; }
.vx-inline-request > summary { cursor: pointer; display: inline-flex; }
