/* cx-ui.css - the design tokens, and the layer, density and action primitives.
   ------------------------------------------------------------------
   TOKENS LIVE HERE. This file publishes the one :root in the app;
   dashboard.css and the page sheets consume it and declare nothing of
   their own. It used to be the other way round, and three sheets each
   published the same fifteen names while 2,484 hex literals were spelled
   out across the other 35 - one palette, thirty-eight opinions of it.

   The `--cx-*` names are canonical. The unprefixed names at the bottom of
   the block (--primary-color, --text-secondary, ...) are the legacy
   vocabulary the page sheets already consume; they are kept as aliases
   with byte-identical values, so nothing on screen moved. Do not retire an
   alias without converting its call sites first, and do not add a hex
   literal to a page sheet - add a token here.

   Three questions decide the shape of every screen in this app. They are
   one law - hide the many behind the one - applied to space, to size,
   and to choice:

     1. LAYER    Does this need the user's input RIGHT NOW?
                 If no, it goes behind a small trigger, not in front of them.
     2. DENSITY  Does this object need the space it is taking?
                 If no, shrink it. Space is attention.
     3. ACTIONS  How many things can be done here?
                 One primary, at most two secondary, the rest behind a menu.
                 A row of four equal buttons says we do not know what the
                 user came to do, so we made it their problem.

   The components below answer those three questions. .cx-btn and .cx-notice
   still live in dashboard.css; they read their colors from the block below.
   Rules and worked examples: .claude/skills/CX-ux-layers/.

   Everything below rides a platform feature rather than a library:
   <dialog> for layer 2 (backdrop, Escape, focus trap, inert page), <details>
   for layer 1.5, and the popover attribute for menus (top layer, so a row
   menu is not clipped by the table's overflow, plus free light dismiss).
   cx-ui.js is two listeners.

   Loaded globally by includes/header.php, ahead of dashboard.css. A page
   that renders its own <html> must link this file too, or every var()
   below it resolves to nothing.
   ------------------------------------------------------------------ */

:root {
    /* --- Brand / accent --------------------- */
    --cx-accent:         #2563eb;
    --cx-accent-hover:   #1d4ed8;
    --cx-accent-strong:  #1e40af;  /* chip text, gradient dark stop */
    --cx-accent-soft:    #dbeafe;  /* chip background */
    --cx-accent-faint:   #eff6ff;  /* tinted panel */
    --cx-purple:         #6c6ace;

    /* --- Status: each is a background/text PAIR -
       The -soft value is a chip background, the -strong value is the text
       that sits on it. They are not interchangeable: -strong is unreadable
       as a fill and -soft is unreadable as text. */
    --cx-success:        #10b981;
    --cx-success-strong: #065f46;
    --cx-success-soft:   #d1fae5;

    --cx-warning:        #f59e0b;
    --cx-warning-strong: #92400e;
    --cx-warning-soft:   #fef3c7;

    --cx-danger:         #ef4444;
    --cx-danger-hover:   #dc2626;
    --cx-danger-strong:  #991b1b;
    --cx-danger-soft:    #fee2e2;
    /* --cx-danger is a foreground: white on #ef4444 is 3.76:1 and fails AA,
       so a filled destructive surface uses -solid (4.83:1) instead. Same hex
       as -hover, different job: one is a state, one is a resting fill. */
    --cx-danger-solid:       #dc2626;
    --cx-danger-solid-hover: #b91c1c;

    --cx-indigo-strong:  #3730a3;
    --cx-indigo-soft:    #e0e7ff;

    /* --- Text ------------------------------- */
    --cx-text:           #1f2937;
    --cx-text-strong:    #111827;
    /* Between body text and the muted note: -firm carries a heading or a label
       that must not shout, -soft carries secondary prose that must still read
       as prose. Darkest to lightest: strong, text, firm, soft, muted, subtle. */
    --cx-text-firm:      #374151;
    --cx-text-soft:      #4b5563;
    --cx-text-muted:     #6b7280;
    --cx-text-subtle:    #9ca3af;
    --cx-dark-gray:      #6e717f;

    /* --- Surfaces and borders --------------- */
    --cx-surface:        #ffffff;
    --cx-surface-alt:    #f9fafb;
    --cx-surface-sunken: #f3f4f6;
    --cx-border:         #e5e7eb;
    --cx-border-strong:  #d1d5db;

    /* --- Elevation and shape ---------------- */
    --cx-shadow-sm:      0 1px 2px 0 rgba(0,0,0,.05);
    --cx-shadow-md:      0 4px 6px -1px rgba(0,0,0,.1);
    --cx-shadow-lg:      0 10px 15px -3px rgba(0,0,0,.1);
    /* For top-layer things that float over the page - menus, popovers.
       Slate-tinted rather than black because it lands on colored surfaces
       as often as white. */
    --cx-shadow-pop:     0 12px 28px -6px rgba(15,23,42,.22), 0 2px 6px -2px rgba(15,23,42,.12);
    /* Rings and glows carry a hue; the four elevation shadows above do not.
       --cx-shadow-tint is that hue, and it is the ONE thing a rule overrides:
       set it alongside the shadow and the ring or glow follows the status.
           .field.is-invalid { --cx-shadow-tint: var(--cx-danger);
                               box-shadow: var(--cx-focus-ring); }
       It cascades, so set it on the element that wears the ring, not on a
       container whose children wear rings of their own. */
    --cx-shadow-tint:      var(--cx-accent);
    /* The halo on a focused input or a selected card. */
    --cx-focus-ring:       0 0 0 3px color-mix(in srgb, var(--cx-shadow-tint) 12%, transparent);
    /* The same job where the halo has to read as a border: solid, tighter. */
    --cx-focus-ring-solid: 0 0 0 2px var(--cx-shadow-tint);
    /* The lift under a raised primary or status button - elevation in the
       button's own color rather than in black. */
    --cx-shadow-glow:      0 4px 12px color-mix(in srgb, var(--cx-shadow-tint) 30%, transparent);
    --cx-radius-sm:      4px;
    --cx-radius:         8px;
    --cx-radius-lg:      12px;
    --cx-radius-pill:    999px;

    /* --- Layout, for the primitives below --- */
    --cx-gap:            0.75rem;
    --cx-pad:            1rem;
    --cx-line:           1px solid var(--cx-border);
    --cx-drawer-w:       30rem;
    --cx-field-max:      26rem;
    --cx-quiet:          var(--cx-text-muted);

    /* --- Legacy aliases - values unchanged -- */
    --primary-color:   var(--cx-accent);
    --primary-hover:   var(--cx-accent-hover);
    --secondary-color: var(--cx-success);
    --danger-color:    var(--cx-danger);
    --danger-solid:    var(--cx-danger-solid);
    --danger-hover:    var(--cx-danger-solid-hover);
    --warning-color:   var(--cx-warning);
    --purple:          var(--cx-purple);
    --dark-gray:       var(--cx-dark-gray);
    --text-primary:    var(--cx-text);
    --text-secondary:  var(--cx-text-muted);
    --bg-primary:      var(--cx-surface);
    --bg-secondary:    var(--cx-surface-alt);
    --border-color:    var(--cx-border);
}


/* ══ LAYER 2: DRAWER ═══════════════════════════════════════════════
   The default home for anything that asks for input but is not the
   reason the user opened the page: create, edit, filter, configure,
   import. A form that greets you on load has decided what you came to
   do. A "New user" button has not.

   <button class="cx-btn cx-btn-primary" data-cx-open="new-user">New user</button>
   <dialog id="new-user" class="cx-drawer" aria-label="New user">
     <form method="post">
       <header class="cx-drawer-head">
         <h2>New user</h2>
         <button type="button" class="cx-drawer-x" data-cx-close aria-label="Close">&times;</button>
       </header>
       <div class="cx-drawer-body"> ...fields... </div>
       <footer class="cx-drawer-foot">
         <button type="button" class="cx-btn cx-btn-secondary" data-cx-close>Cancel</button>
         <button type="submit" class="cx-btn cx-btn-primary">Create user</button>
       </footer>
     </form>
   </dialog>
   ══════════════════════════════════════════════════════════════════ */

.cx-drawer {
    /* The UA sheet hides a closed dialog with `dialog:not([open]){display:none}`.
       An author `display` on the bare class outranks it and every drawer on the
       page renders at once - so the layout rules below are scoped to [open]. */
    margin: 0 0 0 auto;            /* pinned to the right edge */
    padding: 0;
    width: min(var(--cx-drawer-w), 100vw);
    max-width: 100vw;
    height: 100dvh;
    max-height: 100dvh;
    border: 0;
    border-left: var(--cx-line);
    border-radius: 0;
    background: var(--cx-surface);
    color: var(--text-primary, #1f2937);
    box-shadow: var(--cx-shadow-lg);
}

/* Head and foot hold still; only the body scrolls, so the primary action is
   always on screen no matter how long the form gets. */
.cx-drawer[open],
.cx-drawer[open] > form {
    display: grid;
    grid-template-rows: auto 1fr auto;
    min-height: 0;
}
.cx-drawer[open] > form {
    grid-row: 1 / -1;
    height: 100%;
}

.cx-drawer[open] { animation: cx-drawer-in .18s ease-out; }
@keyframes cx-drawer-in {
    from { transform: translateX(1.5rem); opacity: 0; }
}

.cx-drawer::backdrop { background: rgba(15, 23, 42, .32); }

.cx-drawer-head {
    display: flex;
    align-items: center;
    gap: var(--cx-gap);
    padding: var(--cx-pad);
    border-bottom: var(--cx-line);
}
.cx-drawer-head h2,
.cx-drawer-head h3 {
    margin: 0;
    font-size: 1rem;
    font-weight: 700;
}
.cx-drawer-sub {
    font-size: .8rem;
    color: var(--cx-quiet);
}

.cx-drawer-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-left: auto;
    /* 24px floor - WCAG 2.5.8 AA. It is the one control every drawer has
       and the easiest one to miss; padding alone left it at 21x22. */
    min-width: 24px;
    min-height: 24px;
    padding: 0 .25rem;
    border: 0;
    background: none;
    color: var(--cx-quiet);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    border-radius: 6px;
}
.cx-drawer-x:hover { color: var(--text-primary, #1f2937); }

.cx-drawer-body {
    padding: var(--cx-pad);
    overflow-y: auto;
    min-height: 0;
    /* Says "there is more below" without a scroll listener. Two cover
       strips painted on the scrolling layer (local) sit over two shadows
       painted on the element (scroll); at either end of the range the
       cover hides its shadow, in between it slides away and the shadow
       shows. The head/foot borders alone read as a hard stop. */
    background-image:
        linear-gradient(var(--cx-surface) 30%, transparent),
        linear-gradient(transparent, var(--cx-surface) 70%),
        radial-gradient(farthest-side at 50% 0, rgba(15, 23, 42, .14), transparent),
        radial-gradient(farthest-side at 50% 100%, rgba(15, 23, 42, .14), transparent);
    background-position: top, bottom, top, bottom;
    background-size: 100% 24px, 100% 24px, 100% 8px, 100% 8px;
    background-repeat: no-repeat;
    background-attachment: local, local, scroll, scroll;
}

.cx-drawer-foot {
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
    padding: var(--cx-pad);
    border-top: var(--cx-line);
    background: var(--bg-secondary, #f9fafb);
}


/* ══ LAYER 2: MODAL ════════════════════════════════════════════════
   Centered, small, and only for a question that blocks: confirm a
   delete, resolve a conflict. Anything the user is composing belongs
   in a drawer - a modal that scrolls is a drawer wearing a costume.
   Same markup as the drawer, class swapped.
   ══════════════════════════════════════════════════════════════════ */

.cx-modal {
    margin: auto;
    padding: 0;
    width: min(26rem, calc(100vw - 2rem));
    max-height: calc(100dvh - 4rem);
    border: var(--cx-line);
    border-radius: 12px;
    background: var(--cx-surface);
    color: var(--text-primary, #1f2937);
    box-shadow: var(--cx-shadow-lg);
}
.cx-modal[open],
.cx-modal[open] > form {
    display: grid;
    /* An implicit grid column sizes to max-content, which quietly overrides
       the width above: one unbreakable token - a URL, a filename, an API key
       in an error - and the panel grows past the viewport. minmax(0, 1fr)
       makes the declared width binding. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto 1fr auto;
    min-height: 0;
}
.cx-modal[open] > form { grid-row: 1 / -1; }
.cx-modal[open] { animation: cx-modal-in .15s ease-out; }
@keyframes cx-modal-in {
    from { transform: scale(.97); opacity: 0; }
}
.cx-modal::backdrop { background: rgba(15, 23, 42, .32); }

/* The three interior slots are shared with the drawer. */
.cx-modal .cx-drawer-head,
.cx-modal .cx-drawer-body,
.cx-modal .cx-drawer-foot { padding: var(--cx-pad); }

/* .cx-ask is the modal cxConfirm/cxAlert/cxPrompt build - the branded
   replacement for confirm()/alert()/prompt(). Nothing constructs this
   markup by hand; the rules exist because a question is not a form.
   A heading, one sentence and two buttons need no rules between them,
   so the dividing lines and the footer's grey come off. */
/* Pin the three slots to their rows. .cx-modal lays out auto/1fr/auto by
   source order, which is right until a slot is absent: cxAlert builds no
   header, so its footer would inherit the 1fr row and the body - the part
   that must scroll - would get the auto one. Placing them explicitly makes
   the primitive correct however many slots a caller ends up with. */
.cx-ask .cx-drawer-head { grid-row: 1; }
.cx-ask .cx-drawer-body { grid-row: 2; }
.cx-ask .cx-drawer-foot { grid-row: 3; }

.cx-ask .cx-drawer-head { border-bottom: 0; padding-bottom: 0; }
.cx-ask .cx-drawer-foot { border-top: 0; background: none; }
/* The message is often machine text - a filename, an id, a server error -
   so it needs a break opportunity that normal wrapping does not give it. */
.cx-ask-message { margin: 0; line-height: 1.5; overflow-wrap: anywhere; white-space: pre-line; }
/* The prompt input is the only thing in the body, so it takes the width. */
.cx-ask .cx-field { max-width: none; margin-bottom: 0; }


/* ══ LAYER 1.5: DISCLOSURE ═════════════════════════════════════════
   For secondary content that belongs on this page but not in the first
   screenful: advanced filters, raw payloads, per-row detail, help text.
   Native <details> - no JS, keyboard-operable, survives with CSS off.

   <details class="cx-disclosure">
     <summary>Advanced filters</summary>
     <div class="cx-disclosure-body"> ... </div>
   </details>
   ══════════════════════════════════════════════════════════════════ */

.cx-disclosure {
    border: var(--cx-line);
    border-radius: var(--cx-radius);
    background: var(--cx-surface);
    margin-bottom: var(--cx-gap);
}
.cx-disclosure > summary {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .6rem .9rem;
    font-size: .85rem;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
    border-radius: var(--cx-radius);
}
.cx-disclosure > summary::-webkit-details-marker { display: none; }
.cx-disclosure > summary::after {
    content: "";
    width: .4rem;
    height: .4rem;
    margin-left: auto;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform .15s ease;
    opacity: .5;
}
.cx-disclosure[open] > summary::after { transform: rotate(45deg); }
.cx-disclosure[open] > summary {
    border-bottom: var(--cx-line);
    border-radius: var(--cx-radius) var(--cx-radius) 0 0;
}
.cx-disclosure-body { padding: .9rem; }


/* ══ ACTIONS: THE OVERFLOW MENU ════════════════════════════════════
   The budget, per row and per view: ONE primary action, at most two
   secondary, everything else here. Four equal buttons side by side is
   not a menu, it is an unmade decision handed to the user.

   Ordering inside the menu: most-used first, destructive last and
   separated. Destructive actions NEVER get their own button.

   <button type="button" class="cx-menu-btn" popovertarget="row-42-menu"
           aria-label="More actions">&#8943;</button>
   <div id="row-42-menu" class="cx-menu" popover>
     <button type="button" class="cx-menu-item">Upload for</button>
     <button type="button" class="cx-menu-item" aria-disabled="true"
             title="Enable FastBlue first">Full CRM sync</button>
     <hr class="cx-menu-sep">
     <button type="button" class="cx-menu-item cx-menu-danger">Delete</button>
   </div>

   The menu must be the button's NEXT SIBLING - that adjacency is what
   marks the open row (see .cx-menu-btn below) and what the placement
   code expects.

   The popover attribute puts the menu in the top layer, so it is not
   clipped by a scrolling table, and the browser closes it on outside
   click and on Escape. type="button" is required: a popovertarget
   button inside a <form> still submits without it.

   Use aria-disabled, not disabled, for an action the viewer cannot take
   right now. A disabled button fires no pointer events, so its title
   never appears and Tab skips it: the user is told nothing. aria-disabled
   keeps it focusable and hoverable, so the reason in title is readable,
   and cx-ui.js swallows the click.

   This is a button group in a popover, not an ARIA menu: no role="menu",
   no roving arrow keys. Tab walks the items, Escape closes and returns
   focus to the trigger. Buttons that look like buttons and answer to Tab
   are worth more here than a widget pattern half-implemented.
   ══════════════════════════════════════════════════════════════════ */

.cx-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.85rem;
    height: 1.85rem;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 6px;
    background: none;
    color: var(--cx-quiet);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}
.cx-menu-btn:hover {
    background: var(--bg-secondary, #f9fafb);
    border-color: var(--border-color, #e5e7eb);
    color: var(--text-primary, #1f2937);
}

.cx-menu {
    /* Positioned by cx-ui.js on open; margin:0 defeats the UA centering. */
    position: fixed;
    margin: 0;
    padding: .25rem;
    min-width: 11rem;
    max-width: 18rem;
    border: var(--cx-line);
    border-radius: var(--cx-radius);
    background: var(--cx-surface);
    color: var(--text-primary, #1f2937);
    /* A top-layer surface floating over rows needs more separation than a
       card resting on the page; --cx-shadow-lg reads flat here. */
    box-shadow: var(--cx-shadow-pop);
    overflow: visible;
}

/* An open menu must stay traceable to the row it belongs to - otherwise a
   menu holding Delete is just floating near some row. */
.cx-menu-btn:has(+ .cx-menu:popover-open) {
    background: var(--bg-secondary, #f9fafb);
    color: var(--text-primary, #1f2937);
}
.cx-menu:popover-open { animation: cx-menu-in .12s ease-out; }
@keyframes cx-menu-in {
    from { transform: translateY(-.25rem); opacity: 0; }
}
/* Without the popover API there is no top layer to live in, so the menu stays
   in flow as a plain list: degraded, but every action still reachable and
   clickable. This is the whole fallback - no second code path to rot. */
@supports not selector(:popover-open) {
    .cx-menu { position: static; box-shadow: none; }
}

.cx-menu-item {
    display: flex;
    align-items: center;
    gap: .55rem;
    width: 100%;
    padding: .45rem .6rem;
    border: 0;
    border-radius: 6px;
    background: none;
    color: inherit;
    font: inherit;
    font-size: .85rem;
    text-align: left;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
}
.cx-menu-item:hover,
.cx-table tbody tr:hover { background: var(--bg-secondary, #f9fafb); }
.cx-menu-item[disabled],
.cx-menu-item[aria-disabled="true"] {
    opacity: .45;
    cursor: not-allowed;
}
/* It stays focusable so its title is readable, but it must not look armed. */
.cx-menu-item[aria-disabled="true"]:hover { background: none; }
/* --danger-solid, not --danger-color: this is a label read on a white menu,
   and #ef4444 on white is 3.76:1, under the 4.5:1 AA floor for body text. */
.cx-menu-danger { color: var(--cx-danger-solid); }
.cx-menu-danger:hover { background: rgba(239, 68, 68, .08); }

.cx-menu-sep {
    height: 0;
    margin: .25rem .35rem;
    border: 0;
    border-top: var(--cx-line);
}
/* An item with a second line under its name: the menu behind a Download
   button says "Excel / Headers + lines, for AP import" rather than making the
   user guess what each format is for. */
.cx-menu-item > .cx-icon { width: 16px; height: 16px; flex-shrink: 0; }
.cx-menu-item-text { display: flex; flex-direction: column; gap: 1px; }
.cx-menu-item-text b { font-weight: 600; }
/* The hint wraps; the item itself is nowrap so a bare label never breaks mid-word. */
.cx-menu-item-text small { font-size: .75rem; color: var(--cx-quiet); white-space: normal; }
.cx-menu-label {
    padding: .4rem .6rem .25rem;
    font-size: .68rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--cx-quiet);
}

/* The Download trigger (cx_download_menu / cxDownloadMenu): the chevron
   after the label is a hint, not a second icon, so it stays small. */
.cx-dl-btn > .cx-icon:last-child { width: 12px; height: 12px; margin-left: -2px; }

/* The right-hand actions cell of a table row: the row's own link is the
   primary action, so most rows need nothing here but the menu button. */
.cx-row-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: .35rem;
    white-space: nowrap;
}

/* ══ ACTIONS: TOOLTIP ══════════════════════════════════════════════
   An icon-only button says what it does on hover and on keyboard focus,
   at once. The native title does the same a second late, which reads as
   no tooltip at all.

   <button type="button" class="cx-btn" data-cx-tip="Find or create service"
           aria-label="Find or create service">...</button>

   data-cx-tip is the sighted name; aria-label stays for readers. Do not
   also set title, or the browser shows a second bubble. Add data-cx-tip-end
   on the last button of a row so its bubble does not run past the card. The bubble hides
   while a menu the same button opened is showing, so the two never stack.
   ══════════════════════════════════════════════════════════════════ */

[data-cx-tip] { position: relative; }
[data-cx-tip]::after {
    content: attr(data-cx-tip);
    position: absolute;
    left: 50%;
    bottom: calc(100% + 6px);
    transform: translate(-50%, 3px);
    padding: .3rem .55rem;
    border-radius: 6px;
    background: var(--cx-text-strong);
    color: var(--cx-surface);
    font-size: .75rem;
    font-weight: 500;
    line-height: 1.3;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity .12s ease-out, transform .12s ease-out;
    z-index: 20;
}
[data-cx-tip]:hover::after,
[data-cx-tip]:focus-visible::after {
    opacity: 1;
    transform: translate(-50%, 0);
}
[data-cx-tip]:has(+ .cx-menu:popover-open)::after { opacity: 0; }
/* The last button in a clipped row hangs its bubble from its right edge instead of
   its centre, so the bubble stays inside the card: data-cx-tip-end. */
[data-cx-tip-end]::after { left: auto; right: 0; transform: translate(0, 3px); }
[data-cx-tip-end]:hover::after,
[data-cx-tip-end]:focus-visible::after { transform: none; }


/* ══ DENSITY: PAGE HEAD ════════════════════════════════════════════
   One row: what this page is, and the one action it wants. This is the
   replacement for the gradient hero - a 200px block that repeats the
   nav label costs a fifth of the first screenful and tells nobody anything.

   <div class="cx-toolbar">
     <h1>Users</h1><span class="cx-toolbar-sub">42 active</span>
     <div class="cx-toolbar-end">
       <button class="cx-btn cx-btn-primary" data-cx-open="new-user">New user</button>
     </div>
   </div>
   ══════════════════════════════════════════════════════════════════ */

.cx-toolbar {
    display: flex;
    align-items: center;
    gap: var(--cx-gap);
    flex-wrap: wrap;
    margin-bottom: var(--cx-pad);
}
.cx-toolbar h1,
.cx-toolbar h2 {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 700;
    line-height: 1.2;
}
.cx-toolbar-sub {
    font-size: .82rem;
    color: var(--cx-quiet);
    /* Usually a count. When it is a sentence, it takes the measure the field
       column already uses, rather than setting 200 characters to a line. */
    max-width: var(--cx-field-max);
}
.cx-toolbar-end {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: .5rem;
}


/* ══ DENSITY: TABLE ════════════════════════════════════════════════
   A list of records is a table, not a stack of cards. Rows are compact
   so a screenful is a screenful of data; numbers are tabular so columns
   of money line up on the decimal.

   Wide tables scroll inside .cx-table-wrap. The page body never scrolls
   sideways.
   ══════════════════════════════════════════════════════════════════ */

.cx-table-wrap { overflow-x: auto; }

.cx-table {
    width: 100%;
    border-collapse: collapse;
    font-size: .85rem;
}
.cx-table th {
    padding: .5rem .75rem;
    text-align: left;
    white-space: nowrap;
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--cx-quiet);
    border-bottom: var(--cx-line);
}
.cx-table td {
    padding: .5rem .75rem;
    border-bottom: var(--cx-line);
    vertical-align: middle;
}
.cx-table tbody tr:last-child td { border-bottom: 0; }
/* Auto layout hands an empty actions header a third of the table. width:1%
   is the table-layout idiom for "shrink to your content" - but it only means
   that while the element is still a table-cell. The flex above would take the
   cell out of the table formatting context, after which width:1% sizes the
   flex box (1% of the row) and the actions column goes wide again. So inside
   a table the cell stays a cell and the buttons lay out inline. */
.cx-table th.cx-row-actions,
.cx-table td.cx-row-actions {
    display: table-cell;
    width: 1%;
    text-align: right;
    vertical-align: middle;
}
.cx-table .cx-row-actions > * + * { margin-left: .35rem; }

/* For the one word a sighted user reads off the buttons themselves and a
   screen reader has no way to guess: <th class="cx-row-actions">
   <span class="cx-sr">Actions</span></th>. */
.cx-sr {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
.cx-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.cx-table-tight td,
.cx-table-tight th { padding: .35rem .6rem; }


/* ══ DENSITY: FIELDS ═══════════════════════════════════════════════
   Dense enough that a ten-field form fits a drawer without scrolling.
   Label above input, hint below, no card per field.
   ══════════════════════════════════════════════════════════════════ */

.cx-field {
    display: grid;
    gap: .3rem;
    margin-bottom: .85rem;
    /* A drawer caps these at ~26rem for free. In a full-width disclosure
       nothing does, and a date input 648px wide is not a date input. */
    max-width: var(--cx-field-max);
}
/* A checkbox or radio is not a field with a control under it; it is a control
   with a label beside it. Without this, `.cx-field input { width: 100% }`
   stretches a checkbox into a bar - which is what sent admin.php's two role
   checkboxes outside .cx-field entirely. */
.cx-field-check {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-bottom: .5rem;
}
.cx-field-check input {
    width: auto;
    flex: none;
    /* 16px is the platform default and reads as small next to 40px inputs. */
    inline-size: 1.05rem;
    block-size: 1.05rem;
    margin: 0;
    accent-color: var(--primary-color, #2563eb);
}
.cx-field-check label {
    font-size: .85rem;
    /* The whole row is the hit target, not the 17px box. */
    cursor: pointer;
}

.cx-field > label,
.cx-field-group > label {
    font-size: .78rem;
    font-weight: 600;
    color: var(--cx-quiet);
}
/* The star follows the required attribute, so it can never disagree with the
   form: remove `required` and the star goes with it. */
.cx-field:has(input:required) > label::after,
.cx-field:has(select:required) > label::after,
.cx-field:has(textarea:required) > label::after {
    content: ' *';
    color: var(--cx-danger-solid);
}

/* A heading over a set of .cx-field-check rows - "What are these files?" above
   two radios. Use <div class="cx-field-group" role="group" aria-labelledby="x">
   with a <label id="x">: the heading names the SET, so pointing `for` at the
   first radio would make clicking it select that option, which is worse than
   no association at all. `role=group` + aria-labelledby is what announces it. */
.cx-field-group { margin-bottom: .85rem; }
.cx-field-group > label { display: block; margin-bottom: .3rem; }
.cx-field-group > .cx-field-check:last-child { margin-bottom: 0; }
.cx-field input,
.cx-field select,
.cx-field textarea {
    width: 100%;
    padding: .5rem .65rem;
    font: inherit;
    font-size: .9rem;
    color: var(--text-primary, #1f2937);
    background: var(--cx-surface);
    border: var(--cx-line);
    border-radius: 6px;
}
.cx-field-hint {
    font-size: .75rem;
    color: var(--cx-quiet);
}
/* Two fields that belong together (first/last, from/to) share a row. The
   PAIR is capped at the single-field width, not each half - otherwise a form
   with one paired row is twice as wide on that line as on every other, and
   the column of inputs stops reading as a column. */
.cx-field-row {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 .75rem;
    max-width: var(--cx-field-max);
}
/* Three that belong together: City / State / ZIP. Same cap as the pair, for
   the same reason - the column of inputs stays one column wide. */
.cx-field-row-3 {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0 .75rem;
    max-width: var(--cx-field-max);
}


/* ══ DENSITY: EMPTY STATE ══════════════════════════════════════════
   One line saying what would be here and how to put something here.
   Nothing is emptier than a full-page illustration of emptiness.
   ══════════════════════════════════════════════════════════════════ */

.cx-empty {
    padding: 1.5rem;
    text-align: center;
    font-size: .85rem;
    color: var(--cx-quiet);
    border: 1px dashed var(--border-color, #e5e7eb);
    border-radius: var(--cx-radius);
}


/* ══ LAYER: FLASH ══════════════════════════════════════════════════
   The one-line result of the last action ("Your approval rule has been
   saved."), rendered by PHP after a save-and-redirect. Adding cx-flash to
   that notice lifts it out of the page flow into a popup just under the
   header, top-centre, where cxToast() also lands, so the page no longer
   jumps when there is a message and settles when there is not. Pure CSS:
   it slides in, holds, and hides itself; hovering pauses the clock. The
   element stays in the DOM, so a test can still read its text after it
   fades. The page class it sits on (cx-notice, message, sal-alert, ...)
   still paints the colours; this rule only owns where it is and how long.
   ══════════════════════════════════════════════════════════════════ */

.cx-flash {
    --cx-flash-life: 6s;
    position: fixed;
    /* Under the sticky header: 10px wrap padding + 56px card + 2px border. */
    top: 80px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 10000;                  /* the plane the cxToast host uses */
    width: max-content;
    max-width: min(560px, calc(100vw - 2rem));
    box-shadow: var(--cx-shadow-pop);
    animation: cx-flash-in .25s ease-out,
               cx-flash-out .35s ease-in var(--cx-flash-life) forwards;
}
.cx-flash:hover { animation-play-state: paused; }
@keyframes cx-flash-in {
    from { opacity: 0; transform: translate(-50%, -8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes cx-flash-out {
    to { opacity: 0; visibility: hidden; }
}


/* ══ NAVIGATION ════════════════════════════════════════════════════
   Every save on the settings, team and admin pages is a plain <form
   method="POST"> - a full navigation, so the browser painted a blank frame
   between the old document and the new one on each save. Opting into a
   cross-document view transition keeps the old page painted until the new
   one is ready and crossfades between them. Chromium and Safari 18.2+
   honour it; a browser without it behaves as before. Reduced motion turns
   the crossfade into a cut, which is still not a blink.
   ══════════════════════════════════════════════════════════════════ */

@view-transition { navigation: auto; }


/* ══ SHARED ════════════════════════════════════════════════════════ */

.cx-drawer :focus-visible,
.cx-modal :focus-visible,
.cx-menu :focus-visible,
.cx-menu-btn:focus-visible,
.cx-disclosure > summary:focus-visible,
.cx-field :focus-visible {
    outline: 2px solid var(--primary-color, #2563eb);
    outline-offset: 1px;
}

/* An open modal dialog already blocks the page; this stops the layer
   underneath from scrolling behind the backdrop on iOS. A popover menu
   is deliberately excluded - it must not lock the page. */
body:has(dialog[open]) { overflow: hidden; }

/* A spinning icon marks work in flight: cx_icon('loader-circle', ['class' => 'cx-spin']). */
.cx-spin { animation: cx-spin 1s linear infinite; transform-origin: 50% 50%; }
@keyframes cx-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
    .cx-spin { animation: none; }
    ::view-transition-old(*),
    ::view-transition-new(*) { animation: none; }
    /* No slide-in; it still retires itself on the same clock. */
    .cx-flash { animation: cx-flash-out .1s linear var(--cx-flash-life) forwards; }
    .cx-drawer[open],
    .cx-modal[open],
    .cx-menu:popover-open { animation: none; }
    .cx-disclosure > summary::after { transition: none; }
}

@media (max-width: 640px) {
    /* Below this width a right-hand sheet is just a screen, so make it one. */
    .cx-drawer {
        width: 100vw;
        border-left: 0;
    }
    .cx-field-row,
    .cx-field-row-3 { grid-template-columns: 1fr; }
}
