/* =========================================================================
   Communicate tab — ui-refresh list-card view
   Shared by the email template / automated / session / notification /
   planner-alert grids and the sent & scheduled blast grids. Loaded only on
   ui-refresh code paths via CommunicateAsset, so the legacy GridView layout
   is never touched. Semantic tokens only — see web/css/ui-refresh/tokens.css.
   ========================================================================= */

/* ── Toolbar (Create buttons / actions above a list) ── */
.ui-refresh .comm-toolbar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: var(--space-2);
    /* Breathing room below the sticky header so the page doesn't feel tight. */
    margin-top: var(--space-6);
    margin-bottom: var(--space-4);
}

.ui-refresh .comm-toolbar__lead {
    margin-right: auto;
    color: var(--fg-muted);
    font-size: var(--font-size-sm);
}

.ui-refresh .comm-card__badges a.badge.comm-stat:hover,
.ui-refresh .comm-list-totals a.badge.comm-stat:hover {
    filter: brightness(0.96);
    text-decoration: none;
}

.ui-refresh .comm-stat .comm-stat__num {
    font-weight: var(--font-weight-semibold);
}

.ui-refresh a.badge.comm-stat .comm-stat__num {
    color: inherit;
}

/* Totals row beneath the Invitation Lists cards */
.ui-refresh .comm-list-totals {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-3);
    padding: var(--space-3) var(--space-5);
}

.ui-refresh .comm-list-totals__label {
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--fg-muted);
}

/* ── Card list ── */
.ui-refresh .comm-card-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.ui-refresh .comm-card-list:last-child {
    margin-bottom: var(--space-5);
}

.ui-refresh .comm-card {
    position: relative;
    background: var(--card);
    border: 1px solid var(--panel-border);
    /* 10px radius matches the session-row card pattern — sits between
       --radius-lg (8px) and --radius-xl (12px), so it's encoded literally. */
    border-radius: 10px;
    box-shadow: var(--shadow-card);
    transition: border-color var(--transition-fast) ease, box-shadow var(--transition-fast) ease;
}

/* Material Design state layer — a subtle translucent overlay (var(--state-hover),
   which re-resolves to a light tint on dark surfaces) instead of the old left
   accent bar. pointer-events:none so it never blocks the card's controls. */
.ui-refresh .comm-card::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: transparent;
    pointer-events: none;
    transition: background var(--transition-fast) ease;
}

/* Hover: state-layer tint + a gentle elevation lift. No accent bar. */
.ui-refresh .comm-card:hover {
    border-color: var(--line-2);
    box-shadow: var(--shadow-card-hover);
}
.ui-refresh .comm-card:hover::after {
    background: var(--state-hover);
}

.ui-refresh .comm-card--linked {
    cursor: pointer;
}

.ui-refresh .comm-card__main {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4);
}

/* Stacked variant — name on its own line, badges wrap to the next line.
   Used by the Invitation Lists cards. */
.ui-refresh .comm-card--stacked .comm-card__name-row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
}

/* Info column */
.ui-refresh .comm-card__info {
    flex: 1;
    min-width: 0;
}

.ui-refresh .comm-card__name-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.ui-refresh .comm-card__name {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--fg-default);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
}

.ui-refresh .comm-card__subname {
    margin-top: var(--space-1);
    font-size: var(--font-size-sm);
    color: var(--fg-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ui-refresh .comm-card__meta {
    margin-top: var(--space-1);
    font-size: var(--font-size-xs);
    color: var(--fg-subtle);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Badges — keep them inline; the Badge widget wraps each in .badges-container
   (a space-between flex row) which we relax for inline use inside a card. */
.ui-refresh .comm-card__badges {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
}

/* Metrics column (e.g. blast status / counts) */
.ui-refresh .comm-card__metrics {
    display: flex;
    align-items: center;
    gap: var(--space-6);
    flex-shrink: 0;
}

.ui-refresh .comm-card__metric {
    text-align: right;
}

.ui-refresh .comm-card__metric-label {
    font-size: var(--font-size-xs);
    color: var(--fg-subtle);
}

.ui-refresh .comm-card__metric-value {
    margin-top: 2px;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-semibold);
    color: var(--fg-default);
}

/* Action buttons — hidden until the card is hovered/focused, matching the
   .session-row .sr-actions hover-reveal pattern (components.css). Covers the
   Send dropdown (email cards) and/or the .sr-actions icon row together, so
   the whole cluster fades in as one unit. */
.ui-refresh .comm-card__actions {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    flex-shrink: 0;
    opacity: 0;
    transition: opacity var(--transition-fast);
}

.ui-refresh .comm-card:hover .comm-card__actions,
.ui-refresh .comm-card:has(:focus-visible) .comm-card__actions {
    opacity: 1;
}

/* Touch devices have no reliable hover — keep actions visible. */
@media (hover: none), (pointer: coarse) {
    .ui-refresh .comm-card__actions {
        opacity: 1;
    }
}

.ui-refresh .comm-card__actions .dropup,
.ui-refresh .comm-card__actions .btn-group {
    margin: 0;
}

/* Icon action row (replaces the bordered .btn-group). Same 30x30 flat icon
   button as .session-row .sr-actions .lr-icon (components.css), scoped to
   Communicate cards. */
.ui-refresh .comm-card__actions .sr-actions {
    display: flex;
    gap: 1px;
    align-items: center;
}
.ui-refresh .comm-card__actions .sr-actions .lr-icon {
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    border-radius: var(--radius-sm);
    background: transparent;
    border: 0;
    color: var(--muted);
    cursor: pointer;
    transition: background var(--transition-fast), color var(--transition-fast);
    text-decoration: none;
}
.ui-refresh .comm-card__actions .sr-actions .lr-icon:hover {
    background: var(--paper-2);
    color: var(--ink);
}
.ui-refresh .comm-card__actions .sr-actions .lr-icon.danger:hover {
    color: var(--color-error);
    background: var(--color-error-bg);
}
.ui-refresh .comm-card__actions .sr-actions .lr-icon.is-disabled,
.ui-refresh .comm-card__actions .sr-actions .lr-icon.is-disabled:hover {
    background: transparent;
    color: var(--muted);
    cursor: not-allowed;
}
.ui-refresh .comm-card__actions .sr-actions .lr-icon i {
    font-size: 13px;
}
/* Invisible placeholder that holds a skipped action's 30px slot (the palette
   on HTML-editor emails — see email/_card_actions.php) so the metrics column
   lines up across designer and HTML-editor cards. */
.ui-refresh .comm-card__actions .sr-actions .lr-icon--spacer {
    visibility: hidden;
    pointer-events: none;
}

/* Invitation Lists table pane column widths (list-table generic styling
   comes from components.css via UiRefreshAsset). */
.ui-refresh .invitation-list-table .lt-th-inv-name,
.ui-refresh .invitation-list-table .lt-cell-inv-name {
    width: 40%;
}
.ui-refresh .invitation-list-table .lt-th-inv-count,
.ui-refresh .invitation-list-table .lt-cell-inv-count {
    width: 15%;
    text-align: center;
}

/* Full-width extra row (sending progress, inline notes) */
.ui-refresh .comm-card__extra {
    padding: var(--space-3) var(--space-5);
    border-top: 1px solid var(--border-subtle);
    font-size: var(--font-size-xs);
    color: var(--fg-muted);
}

/* Sending-progress: bar (track + fill) on the left, count text on the right —
   kept separate so the count is never clipped by a narrow fill. */
.ui-refresh .comm-progress-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.ui-refresh .comm-progress {
    flex: 1 1 auto;
    height: 6px;
    min-width: 120px;
    border-radius: var(--radius-full);
    background: var(--bg-muted);
    overflow: hidden;
}

.ui-refresh .comm-progress__bar {
    height: 100%;
    border-radius: var(--radius-full);
    background: var(--accent-primary);
    transition: width var(--transition-normal);
}

.ui-refresh .comm-progress__count {
    flex: 0 0 auto;
    font-size: var(--font-size-xs);
    color: var(--fg-muted);
    white-space: nowrap;
}

/* Inline "send to everyone" toggle rendered inside a metric slot. The .checkbox
   wrapper already gets the lavender toggle from components.css; here we just keep
   it compact and right-aligned to sit in the metrics column. */
.ui-refresh .comm-card__metric .comm-card__toggle {
    margin: 0;
}

.ui-refresh .comm-card__metric .comm-card__toggle label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    font-size: var(--font-size-xs);
    color: var(--fg-muted);
    font-weight: var(--font-weight-medium);
}

/* ── List toolbar ──
   Right-aligned chrome row above a card list (search-expand and/or filter-pop),
   the Communicate-tab analog of the Sessions/Speakers tab-row gutter. The chrome
   itself (.search-expand, .filter-toggle, .filter-pop) is styled generically in
   components.css; this only handles placement. Rendered by common/_list-toolbar. */
.ui-refresh .comm-toolbar {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    margin-bottom: var(--space-4);
}

.ui-refresh .comm-toolbar__spacer {
    flex: 1;
}

/* Report filters use native <select>s inside the filter-pop (long option lists
   don't fit the boolean fp-row pattern). Keep them full-width and clickable
   while inheriting the shared .fp-input framing. */
.ui-refresh .filter-pop select.fp-select {
    cursor: pointer;
}

/* Sending-progress bar (Bootstrap Progress widget) inside the card extra row */
.ui-refresh .comm-card__extra .progress {
    height: 8px;
    margin: var(--space-2) 0 0;
    background: var(--bg-muted);
    border-radius: var(--radius-full);
    box-shadow: none;
    overflow: hidden;
}

.ui-refresh .comm-card__extra .progress-bar {
    background: var(--accent-primary);
    box-shadow: none;
}

/* ── Grouped sections (automated emails grouped by trigger) ── */
.ui-refresh .comm-section {
    margin-bottom: var(--space-6);
}

.ui-refresh .comm-section__head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.ui-refresh .comm-section__title {
    font-size: var(--font-size-lg);
    font-weight: var(--font-weight-semibold);
    color: var(--fg-default);
}

.ui-refresh .comm-section__subtitle {
    font-size: var(--font-size-xs);
    color: var(--fg-subtle);
}

.ui-refresh .comm-section__actions {
    margin-left: auto;
}

.ui-refresh .comm-section--disabled {
    opacity: 0.5;
    pointer-events: none;
}

/* =========================================================================
   Communicate report data-tables — restyle the DataTables (v2) chrome to
   mirror session/setup's `.list-table`: natural-case muted headers on a
   --paper-2 band, hairline row dividers (no cell grid, no zebra), quiet filter
   inputs, hover-reveal sort carets, purple select checkboxes, a ghost Actions
   button and pill pagination.

   Scoped to .comm-report so the global .new-grid-theme theme
   (ajax-datatables.css, ~69 grids app-wide) is untouched. Colours are tokens,
   so light/dark surfaces resolve for free; `!important` appears only to beat
   the legacy `!important` / hardcoded-hex rules in ajax-datatables.css.
   ========================================================================= */
/* Scroll lives on `.dt-scroll-body`, on both axes — a framed table like the
   legacy UI's, so its horizontal scrollbar stays on screen however many rows a
   report has.
   DataTables (scrollX + scrollY) splits the grid into `.dt-scroll-head`
   (a cloned header table) and `.dt-scroll-body` (the real table) as siblings
   inside `.dt-scroll`, and binds two things to the BODY box: a `scroll.DT`
   handler that mirrors its `scrollLeft` onto the head (so the header tracks the
   columns with no JS of ours), and FixedColumns' scroller, which flags the
   table `dtfc-scrolling-start` and pins the frozen `<td>`s. The head sits
   outside the body, so it stays in view while the rows scroll beneath it.
   The frame height is `--dt-frame-height`, measured by communicate.js's
   initGridFrames() as whatever space the page leaves once everything above and
   below the table fits on screen. Do NOT replace it with DataTables' own
   `scrollY`: that is a fixed pixel guess (AjaxDataTables.php) at the space
   below the table, and three earlier caps built on it shipped and were reverted
   for leaving an internal scrollbar with page space unused beneath it. Pages
   with more than one grid, and viewports too short to leave a 200px frame, get
   no frame (the variable is unset) and fall back to page scrolling — the case
   the sticky head below exists for.
   `.dt-scroll` must stay `overflow: visible` for that sticky head: a sticky
   element pins against its nearest scroll container, and a clipping
   `.dt-scroll` would capture it. `.dt-layout-cell`'s `min-width: 0` is
   load-bearing for the same reason: it is a flex item of `.dt-layout-row`
   (vendor `display: flex`), so its automatic minimum size is the table's
   min-content width — with `.dt-scroll` not clipping, the cell would blow out
   to the full table width and NOTHING could scroll sideways.
   `min-height: 0` clears datatables.min.css's 200px floor on `.dt-scroll-body`
   (only cleared for the *initial* small-table case in `initDataTableInit`),
   which would otherwise pad out a table later filtered down to a few rows. */
.ui-refresh .comm-report .dt-scroll {
    overflow: visible;
}
/* See the flex-item note above — without this nothing scrolls sideways at all. */
.ui-refresh .comm-report .dt-layout-row.dt-layout-table > .dt-layout-cell {
    min-width: 0;
}
.ui-refresh .comm-report .new-grid-theme .dt-layout-table > div.dt-layout-cell > .dt-scroll > .dt-scroll-head {
    /* `!important` because DataTables writes `position: relative` INLINE on this
       div, which silently beat a plain `position: sticky` — the head was never
       actually sticky on any grid before this. */
    position: sticky !important;
    /* Docks under the sticky `.subpage-head`; the var is kept live on
       #main-content by subpage-head-sticky.js (0 at widths where the head isn't
       sticky) and falls back to 0 on the grids that render no subpage-head
       (see _report-grid-page-open.php). */
    top: var(--subpage-head-height, 0px);
    z-index: 2;
    /* Opaque so scrolling body rows don't show through the pinned head;
       matches the header row's own established background (below). */
    background: var(--paper-2);
    /* The vendor default, and required: the head must clip for DataTables'
       `scrollLeft` mirroring to shift it. The filter row's popouts all escape on
       their own — DateInput appends to body, TimeInput repositions a body-level
       wrapper, native selects aren't clipped. */
    overflow: hidden !important;
    /* Radius lives here now that `.dt-scroll` no longer clips anything. */
    border-top-left-radius: var(--radius-md);
    border-top-right-radius: var(--radius-md);
}
.ui-refresh .comm-report .dt-scroll-body {
    overflow: auto !important;
    min-height: 0 !important;
    /* `!important` beats the inline `max-height` DataTables writes from scrollY. */
    max-height: var(--dt-frame-height, none) !important;
    /* The vendor sizes this box to its content; that is exactly what stopped it
       from ever scrolling, so let it take the cell's width instead. */
    width: auto !important;
    min-width: 0 !important;
}
/* Legacy `.min-height-600` (utility.css) on some grids' Pjax wrappers gave
   filter dropdowns room under a short table. #main-content already fills the
   viewport for that, and the padding would push the page past the frame. */
.ui-refresh .min-height-600:has(.comm-report .dt-scroll-body) {
    min-height: 0;
}
/* Collapse the empty body to the visible width instead of the full
   all-columns-combined width, so an empty grid doesn't scroll sideways over
   nothing. Skipped when the head is keeping its per-column filter-inputs row
   visible (see the `.dt-scroll-head:not(:has(tr.filters))` rule below): the
   head clone mirrors the BODY table's column widths, so collapsing the body
   while the head stays at full width would misalign every filter input from
   the column it filters. A filter-row grid therefore keeps its normal
   geometry when empty, and the card inside it is centered on the visible
   width by communicate.js's initEmptyGridActions(). */
.ui-refresh .comm-report .dt-scroll:not(:has(.dt-scroll-head tr.filters)) .dt-scroll-body:has(.dt-empty) {
    width: 100% !important;
    overflow: hidden !important;
}
/* Reserve real height for the very first "Processing" state, before the grid's
   first AJAX response has ever rendered a row. Without it the body collapses to
   nothing and the indicator has no room to sit in.

   The reservation goes on `.dt-scroll-body`, NOT its `.dt-scroll` parent, and
   that distinction is the whole fix: `.dt-scroll-body` is the box carrying the
   visible bottom border (`div.dt-container div.dt-scroll-body{border-bottom}`,
   vendor CSS), so it — not `.dt-scroll` — is what a viewer reads as "the table
   area". Reserving on `.dt-scroll` grew the outer box while the bordered body
   stayed short, leaving dead, invisible space between the border and
   `.dt-scroll`'s bottom edge; anything centered against that outer box then
   rendered visibly BELOW the border (measured live: 19px low, box overhanging
   the line). Growing the bordered box instead keeps "the space that exists" and
   "the space you can see" the same box.

   Overrides the `min-height: 0 !important` set above (hence matching
   `!important` at higher specificity) — that floor-removal is correct once a
   grid has data and must not pad out a table later filtered to a few rows, but
   during this one pre-first-draw window it is what collapses the body.

   Can't key this off "does the tbody have a `<tr>` yet": Yii's own GridView
   (AjaxDataTables' base class) renders a plain `<tr>` server-side for an empty
   dataProvider (`renderTableBody()`), so a row already exists in the initial
   HTML before DataTables JS — and therefore this "Processing" state — ever
   runs. `.dt-drawn` (communicate.js) is the reliable signal instead: it's
   added to `.dt-container` once the table's first genuine AJAX response has
   rendered (not DataTables' own pre-fetch initial draw — see that code), i.e.
   exactly when this reservation should stop applying. */
.ui-refresh .comm-report .dt-container:not(.dt-drawn) .dt-scroll-body {
    min-height: 120px !important;
}
/* Collapse DataTables' loading placeholder row for that same window. While the
   first request is in flight DataTables renders `<tr><td class="dt-empty">` for
   the `language.loadingRecords` string, which AjaxDataTables.php deliberately
   sets to '' — so the cell is blank, but the generic row styling above still
   paints its `border-bottom` and the `.contact-report` empty-cell rule still
   pads it to ~64px. The result is a stray grey rule floating across an
   otherwise-empty table, plus a second band of dead height under the
   reservation. Nothing is being communicated by an intentionally blank row, so
   take it out of the flow entirely and let the reservation above define this
   window's height on its own. Released by `.dt-drawn` with everything else, so
   the real empty-state row (`emptyTable`, which DOES have content) is never
   affected. */
.ui-refresh .comm-report .dt-container:not(.dt-drawn) .dt-scroll-body table.dataTable > tbody > tr > td {
    height: 0 !important;
    padding: 0 !important;
    border-bottom: 0 !important;
}
/* Vendor CSS vertically centers `.dt-processing` with `margin-top: -26px` — a
   fixed offset that only centers correctly if the box is exactly 52px tall.
   Ours renders shorter than that (the plain icon + text, no vendor panel
   chrome), so that fixed margin overshoots. `transform: translateY(-50%)`
   centers relative to the box's OWN rendered height instead of a hardcoded
   guess, so it stays correct regardless of exactly how tall the icon/text/
   padding ends up being. The `top` it centers on is set per-grid in
   communicate.js (see initProcessingCentering) — a CSS percentage can't express
   it, because 50% of `.dt-scroll` measures from the top of the HEADER, which
   lives inside that same box. */
.ui-refresh .comm-report #dt-processing {
    margin-top: 0 !important;
    transform: translateY(-50%);
    /* Hide the "Processing..." text, keep the dot spinner. The text is a
       direct text node of this element (DataTables' own markup), so
       font-size: 0 collapses it to nothing without touching the spinner —
       its dots are absolutely-positioned, background-colored <div>s sized in
       px (see the vendor CSS's div.dt-processing > div:last-child > div
       rule), not font-relative glyphs, so they're unaffected. */
    font-size: 0;
    gap: 0;
    width: 90px;
}
/* Contacts also shows the global #spinner overlay (App.showSpinner) while an
   Add/Import/Edit drawer save reloads #pjax-grid — that overlay only clears on
   pjax:end, by which point this grid's own first-fetch #dt-processing is
   already showing, so the two loading states briefly stack. Rather than touch
   the shared save/reload path (used well beyond contacts), strip this grid's
   indicator down to the bare ring: same components.css ring-spinner, no card
   chrome and no "Processing" text, so only one loading affordance is ever
   visible at a time. The label is a bare text node (no wrapping element), so
   font-size: 0 is what hides it. */
.ui-refresh .contact-report #dt-processing {
    gap: 0;
    padding: 0;
    background: transparent;
    border: 0;
    box-shadow: none;
    font-size: 0;
}
/* #dt-processing above is the INNER div (AjaxDataTables' custom `language.processing`
   HTML); DataTables wraps it in its OWN container carrying Bootstrap's
   `.panel.panel-default` (datatables.min.js's classes map) — a card with a
   border and drop shadow that frames the ring.
   `background` needs !important like the rest: ajax-datatables.css's
   `div.dt-processing.panel{background-color:transparent}` (0,2,1) is outranked by
   components.css's `.ui-refresh :is(#content-container, #report-content) .panel`
   (1,2,0) — `:is()` takes the specificity of its most specific argument, so that
   selector counts an ID — which repaints the wrapper `--bg-surface`. That is the
   grey block that shows through behind the spinner, and only on the real page:
   the account-hub layout supplies the `#content-container` wrapper the module
   default layout omits. */
.ui-refresh .contact-report div.dt-processing {
    margin-bottom: 0 !important;
    background: transparent !important;
    border: 0 !important;
    border-radius: 0 !important;
    box-shadow: none !important;
}
/* The "Use Existing Contacts" pickers (cfs-contact, speaker) reuse this same
   /contact/_grid partial and so inherit .contact-report, but they're a
   different page from the account-level Contacts list the fix above targets:
   there's no Add/Import drawer save reloading this grid's pjax container
   (the "Add Selected" action posts directly and shows its own confirmation,
   not an AjaxDrawer save), so the spinner-stacking scenario that motivated
   stripping the card away doesn't occur here. Restore the normal boxed
   spinner - `#dt-processing` is the visible white card, `div.dt-processing`
   is DataTables' own transparent Bootstrap-panel shell around it (background
   forced transparent, !important, for the same components.css `.panel`
   repaint reason as the rule above; it needs no border/padding of its own).
   `.list-page--wide` (not used by the real Contacts list, which stays plain
   `.list-page`) is what distinguishes the two without a dedicated marker
   class. */
.ui-refresh .list-page--wide .contact-report #dt-processing {
    gap: normal;
    padding: 10px;
    background: var(--bg-surface);
    border: 1px solid var(--border-default);
    box-shadow: none;
    font-size: 16px;
}
.ui-refresh .list-page--wide .contact-report div.dt-processing {
    background: transparent !important;
}

.ui-refresh .comm-report .dataTables_wrapper {
    color: var(--fg-default);
}

.ui-refresh .comm-report table.dataTable {
    border: 0;
    /* Collapse merges the header cell fills so no faint sub-pixel column seams
       show through the band (matches .list-table's seamless rows). */
    border-collapse: collapse;
    border-spacing: 0;
}
/* Kill the legacy bordered-grid + hardcoded #E5E7EB dividers; rows keep only a
   tokenised bottom rule (which re-resolves on dark surfaces). */
.ui-refresh .comm-report table.dataTable th,
.ui-refresh .comm-report table.dataTable td {
    border-left: 0 !important;
    border-right: 0 !important;
    border-top: 0 !important;
}
/* ajax-datatables.css's `.table-condensed > thead > tr:first-of-type > th`
   rule (higher specificity than the generic th/td reset above, since it
   chains thead/tr/:first-of-type) hardcodes a #E5E7EB border-top !important —
   invisible on light paper but a glaring line on dark surfaces. Match its
   shape at higher specificity to beat it. */
.ui-refresh .comm-report table.dataTable.table-condensed > thead > tr:first-of-type > th {
    border-top: 0 !important;
}

/* ── Header band ── continuous --paper-2 fill on the row, transparent cells. */
.ui-refresh .comm-report table.dataTable > thead > tr {
    background: var(--paper-2);
}
.ui-refresh .comm-report table.dataTable > thead > tr.header-row > th {
    text-align: left;
    /* !important beats ajax-datatables.css `.header-row th` (12px / 5px 16px,
       all !important) so header metrics match .list-table. */
    padding: var(--space-2) var(--space-3) !important;
    background: transparent;
    /* !important beats both the #E5E7EB grid borders and `.has-filter` lavender. */
    border-bottom: 1px solid var(--line) !important;
    /* Natural case — matches .list-table and the design language (no uppercase). */
    font-size: 11px !important;
    font-weight: var(--font-weight-semibold);
    /* --muted re-resolves per surface; beats legacy `.header-row th{color:
       gray-800 !important}` which rendered near-invisible on dark. */
    color: var(--muted) !important;
    white-space: nowrap;
    vertical-align: middle;
}
.ui-refresh .comm-report table.dataTable > thead > tr.header-row > th:first-child {
    border-top-left-radius: var(--radius-md);
}
.ui-refresh .comm-report table.dataTable > thead > tr.header-row > th:last-child {
    border-top-right-radius: var(--radius-md);
}
/* Vertically center the header cell content (label + sort caret + any info
   icon), and left-align it — matching .list-table's `thead th { text-align:
   left }`, the established ui-refresh convention. `text-center` in
   report/_grid.php's headerOptions is the pre-ui-refresh Bootstrap class,
   still used as the non-ui-refresh fallback; it's not what ui-refresh headers
   should follow.
   DataTables' own layout is `justify-content: space-between` with the label
   (`.dt-column-title`) set to `flex-grow: 1` — that stretches the label to
   fill the row and shoves the caret to the far right edge, disconnected from
   the text. Pack label+caret together as one unit (flex-grow: 0 on the label)
   and left-align that unit in the row, so the caret sits tight against the
   text instead of floating at the column's right edge. */
.ui-refresh .comm-report table.dataTable > thead > tr.header-row > th div.dt-column-header {
    align-items: center;
    justify-content: flex-start !important;
    --dt-header-align-items: center;
}
.ui-refresh .comm-report table.dataTable > thead > tr.header-row > th div.dt-column-header span.dt-column-title {
    flex-grow: 0 !important;
}
/* dt-type-date columns default to DataTables' `row-reverse` header layout
   (ajax-datatables.css only fixes the equivalent `row-reverse` for
   dt-type-numeric). Combined with the `flex-start` packing above, a
   row-reversed header packs label+caret at the END of the reversed axis --
   i.e. the column's right edge -- while the row's own td content stays left
   aligned, so a sortable date column (e.g. email-blast's "Scheduled Date &
   Time") visibly detaches its header from its data. Scoped here rather than
   alongside the dt-type-numeric fix in ajax-datatables.css because that file
   is NOT gated behind isUiRefreshEnabled() (legacy grids load it too), and
   this fix only matters where the flex-start override above also applies. */
.ui-refresh .comm-report table.dataTable th.dt-type-date div.dt-column-header {
    flex-direction: row !important;
}
/* Center the select-all checkbox in its (checkbox) column. */
.ui-refresh .comm-report table.dataTable > thead > tr.filters > th:first-child {
    text-align: center;
}

/* ── Sort caret ── replace the always-on FA arrows (and their hardcoded
   `color: blue`) with a unicode caret that mirrors .list-table: hidden at rest
   → faint on header hover → same muted color, just fuller opacity, when the
   column is the active sort. .list-table never switches to the accent color
   for its active sort caret (only opacity changes: 0 → 0.35 hover → 0.9
   active) — match that here instead of accent-tinting it.
   Same glyph and 13px size as .list-table's caret (▾) for both directions —
   asc rotates it 180deg in place instead of swapping to .list-table's
   separate ▴ glyph, which has a slightly different bounding box/baseline and
   visibly shifted position between sort states. Rotating guarantees zero
   movement while still matching .list-table's exact glyph/size/color.

   DataTables 2's own CSS gives this span `position:relative; width:12px;
   height:20px` and absolutely-positions a native ::before (a faint, always-
   visible ▲) plus its own ::after at `left:0` — neither aligned the way we
   want, and the fixed 12px box left dead space between the label and the
   glyph. Kill the native ::before and let the span size to its content (no
   fixed width) so it sits tight against the label — same as .list-table,
   which relies on the flex row's own `gap` for spacing rather than padding
   out a fixed-size icon box. */
.ui-refresh .comm-report table.dataTable thead > tr.header-row > th span.dt-column-order {
    font-family: var(--font-family) !important;
    font-weight: 400 !important;
    position: static !important;
    width: auto !important;
    height: auto !important;
    display: inline-flex !important;
    align-items: center !important;
}
.ui-refresh .comm-report table.dataTable thead > tr.header-row > th span.dt-column-order::before {
    display: none !important;
}
.ui-refresh .comm-report table.dataTable thead > tr.header-row > th:not([data-orderable="false"]) span.dt-column-order::after {
    content: "\25BE" !important; /* ▾ */
    display: inline-block !important;
    position: static !important;
    font-size: 13px !important;
    line-height: 1 !important;
    color: var(--muted) !important;
    opacity: 0 !important;
    transform: rotate(0deg);
    transition: opacity var(--transition-fast);
}
.ui-refresh .comm-report table.dataTable thead > tr.header-row > th:not([data-orderable="false"]):hover span.dt-column-order::after {
    opacity: 0.35 !important;
}
.ui-refresh .comm-report table.dataTable thead > tr.header-row > th.dt-ordering-asc span.dt-column-order::after,
.ui-refresh .comm-report table.dataTable thead > tr.header-row > th.dt-ordering-desc span.dt-column-order::after {
    color: var(--muted) !important;
    opacity: 0.9 !important;
}
.ui-refresh .comm-report table.dataTable thead > tr.header-row > th.dt-ordering-asc span.dt-column-order::after {
    transform: rotate(180deg) !important;
}

/* ── Body rows ── airy cells, hairline bottom rule, no zebra, subtle hover. */
.ui-refresh .comm-report table.dataTable > tbody > tr > td {
    /* !important beats ajax-datatables.css `> tbody tr td{padding:8px 16px
       !important}` + the #E5E7EB bottom border. */
    padding: var(--space-3) !important;
    border-bottom: 1px solid var(--line) !important;
    color: var(--ink);
    font-size: 13px;
    vertical-align: middle;
    background: transparent;
}
.ui-refresh .comm-report table.dataTable > tbody > tr > td a {
    color: var(--ink);
}
/* Numeric columns (report/_grid.php's # Fields / # Filters) — content reads
   left-aligned like the other columns' content (Category, report name have
   no center override); only the header stays centered, matching every other
   column's header. `.text-num` is an additive marker class alongside the
   legacy `.text-center` (kept as the non-ui-refresh fallback on both header
   and content), so this is the only place the visual change applies. */
.ui-refresh .comm-report table.dataTable > tbody > tr > td.text-num {
    text-align: left !important;
}
/* Primary (name) column reads a touch heavier, like .lt-cell-name. */
.ui-refresh .comm-report table.dataTable > tbody > tr > td.data-type-full-name {
    color: var(--ink);
    font-size: 13px;
    font-weight: var(--font-weight-medium);
    line-height: 1.4;
}
/* De-zebra — DataTables 2 paints the stripe as an inset box-shadow on the CELLS
   (`> * { box-shadow: inset 0 0 0 9999px rgba(--dt-row-stripe,.05) }`), NOT a
   background, and that 9999px inset also occludes our hover fill. Kill it so
   rows are flat like .list-table (selected rows are re-toned below). */
.ui-refresh .comm-report table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * {
    box-shadow: none !important;
}
/* Empty-state cell height (scoped to .comm-report). */
.ui-refresh .comm-report table.dataTable.table-condensed > tbody > tr > td[class]:not(.select-checkbox):not(.action-col).dt-empty {
    height: 40px;
}
.ui-refresh .comm-report table.dataTable.table-condensed > tbody > tr > td[class]:not(.select-checkbox):not(.action-col).dt-empty .no-data-message {
    transform: translate(-50%, -50%);
    color: var(--muted);
}
/* A report's emptyTableHtml (AjaxDataTables::$emptyTableHtml) can swap the plain
   one-line message above for a real `.list-empty` card (icon/heading/body) —
   Contacts and the Swoogo Admin accounts list both do. The reasoning below is
   written from the contacts case, which is the wider/worse one; the accounts
   grid opts in with `.account-report` for the same reasons.
   The plain message's own positioning doesn't fit it: `.no-data-message` is
   `position: absolute; left: 50%` (ajax-datatables.css) resolving against
   `.dt-scroll-body`. That box used to be sized `width: max-content` — i.e. as
   wide as ALL columns combined, so `left: 50%` landed at half the *table's*
   width and the message only scrolled into view around the horizontal
   midpoint. It is now the horizontal scrollport itself (`width: auto`, see the
   .dt-scroll block above), so `left: 50%` is half the VISIBLE width; the card
   treatment below is kept regardless, being the better empty state. `.list-empty` is a self-contained centered card (border,
   padding, its own text-align) — let it lay out normally instead of patching
   the absolute-position/containing-block chain. That also means the 40px
   cell height above no longer applies (a card with `--space-10` padding on
   its own needs far more than 40px), so give the cell room to size to
   content.

   Centering `.list-empty` with `margin: 0 auto` looked like the obvious next
   step, but it centers on the *table's* width — the empty `<td>` carries a
   `colspan` matching the full column count, so it's exactly as wide as the
   table, which contacts routinely widens well past the viewport — same bug
   as the `left: 50%` it replaced, just moved. `position: sticky; left: 0;
   right: 0` on top of that (to clamp it back into `.dt-scroll`'s visible
   viewport) tests as a no-op here in practice: sticky doesn't engage inside
   this auto-layout table/colspan-cell nesting, so the card stayed at its
   centered-on-the-table position and could still render off both edges of
   the scrollport depending on scroll position — confirmed with a live
   Playwright reproduction of this exact markup, not just reasoned about.

   Flush against column 1 (no margin, no sticky) instead: always visible the
   instant the grid empties, since a freshly-drawn table is scrolled to 0 by
   default — no positioning trick needs to hold up under scroll at all. The
   one case this doesn't cover on its own — the user had already scrolled the
   grid right before a filter emptied it — is handled in communicate.js's
   `initEmptyGridActions()`, which snaps `.dt-scroll` back to `scrollLeft: 0`
   on the same draw. */
.ui-refresh .comm-report table.dataTable.table-condensed > tbody > tr > td.dt-empty:has(.list-empty),
.ui-refresh .account-report table.dataTable.table-condensed > tbody > tr > td.dt-empty,
.ui-refresh .speaker-report table.dataTable.table-condensed > tbody > tr > td.dt-empty {
    height: auto !important;
    padding: var(--space-8) 0 !important;
}
.ui-refresh .comm-report .no-data-message:has(.list-empty),
.ui-refresh .account-report .no-data-message,
.ui-refresh .speaker-report .no-data-message {
    /* !important: a higher-specificity .comm-report rule above sets `transform:
       translate(-50%, -50%)` on this same element (`.dt-empty .no-data-message`,
       chained through `table.dataTable.table-condensed > tbody > tr`); plain
       specificity alone doesn't beat it here. */
    position: static !important;
    transform: none !important;
    white-space: normal !important;
    left: auto !important;
}
/* The card spans the full visible width of the grid, flush left — no cap, no
   centering margin. The width itself has to be set in JS (communicate.js's
   sizeEmptyStateCard()): the empty `<td>` carries a colspan matching every
   column, so it is as wide as the whole TABLE, and a CSS `width: 100%` would
   run the card off the right edge of any report wider than the viewport.
   `box-sizing` is pinned here so that JS width includes the card's own padding
   and border rather than overflowing by them. */
.ui-refresh .comm-report .no-data-message .list-empty,
.ui-refresh .account-report .no-data-message .list-empty,
.ui-refresh .speaker-report .no-data-message .list-empty {
    box-sizing: border-box;
    max-width: none;
    margin-left: 0;
}
/* registrant/_grid.php (and any other view that adopts the same pattern)
   skips the AjaxDataTables widget entirely when the initial count is zero,
   rendering `.list-empty` as a plain sibling of the toolbar instead of a
   descendant of the table. Without a `<table>`, communicate.js's
   mergeReportToolbar() never runs, so `.breadcrumb-btn` never loses the
   Bootstrap `pull-right` float that CSS elsewhere only clears via that JS
   (see the `.comm-report-toolbar .breadcrumb-btn` rule below) — left
   floating with nothing to clear it, it overlaps the card that follows it
   in the DOM. Clear it here directly; harmless no-op when there's no float
   to clear (e.g. the merged-toolbar path, or the live-filtered-to-zero
   path where `.list-empty` sits inside the table instead). */
.ui-refresh .comm-report .list-empty {
    clear: both;
}
/* Grids using the rich `.list-empty` card go further than just fixing its
   position: with zero rows, hide the rest of the DataTables chrome too —
   the column-label row (`.dt-scroll-head`) and the pagination/page-length
   row — so only the toolbar above the grid (merged into the top info row by
   communicate.js's mergeReportToolbar(), or its own `.breadcrumb-btn` bar
   on grids that don't merge) and the empty card are visible. Scoped to
   `:has(.list-empty)` specifically, not just `:has(td.dt-empty)`, so grids
   still using the plain "No data available" one-liner keep their current
   look untouched.

   EXCEPT when the head carries the inline per-column filter-inputs row.
   Those inputs are the ONLY way to edit or clear a column filter, and they
   live in `.dt-scroll-head` alongside the labels — hiding the head the
   moment a filter empties the grid strands the user in a permanently empty
   report with no reachable control to undo what emptied it. `tr.filters` is
   the exact signal: AjaxDataTables::renderTable() emits that row only when
   `enableFilters` is on AND at least one column is filterable (it strips the
   row wholesale otherwise), and DataTables' scroll-head clone preserves it —
   ajax-datatables.js's initFilters() binds its change handlers through
   `.dt-scroll-head thead tr.filters th`, and those bindings survive redraws,
   so a head kept visible here stays fully interactive. Grids with no filter
   row (the sync/queue reports) are unaffected and keep the clean card-only
   look. */
.ui-refresh .comm-report .new-grid-theme:has(.list-empty) .dt-scroll-head:not(:has(tr.filters)) {
    display: none;
}
/* Large Email Blasts is the opposite case from the "sync/queue reports" above:
   it's normally empty (nothing pending approval) rather than occasionally
   empty, so hiding its columns every time there's nothing to approve reads as
   broken rather than clean. #email-blast-grid (an id, so it wins over the
   class-only rule above without !important) keeps just the header row; the
   pagination-row/border collapsing below is unaffected. */
.ui-refresh .comm-report #email-blast-grid.new-grid-theme:has(.list-empty) .dt-scroll-head {
    display: block;
}
.ui-refresh .comm-report .new-grid-theme:has(.list-empty) .dt-layout-row:last-child {
    display: none;
}
/* ajax-datatables.css puts an `!important` left+bottom border on every
   tbody `td` and datatables.min.css puts a bottom border on the whole
   `.dt-scroll-body` — both read as a stray boxed line under the card once
   the header row above it is gone. Only applies when the head IS gone; a
   filter-row grid keeps its head, so its normal table edge should stay. */
.ui-refresh .comm-report .new-grid-theme:has(.list-empty):not(:has(.dt-scroll-head tr.filters)) .dt-scroll-body {
    border-bottom: none;
}
.ui-refresh .comm-report .new-grid-theme:has(.list-empty) td.dt-empty {
    border: none !important;
}
/* Row hover — flat --paper-2 fill (the stripe shadow is gone, so a plain
   background reads correctly on both odd and even rows). */
.ui-refresh .comm-report table.dataTable > tbody > tr:hover > td {
    background: var(--paper-2);
}
/* Selected rows — ajax-datatables.css paints the <tr> AND its cells legacy blue
   (#0A69FA !important) with white text. Re-tone to the accent tint. On the dark
   surface and custom accents --primary-tint is translucent, so layer it over an
   opaque base (same idiom as the frozen-cell rules further down) or the legacy
   blue bleeds through. The <tr> fill is cleared too so no cell can leak it. */
.ui-refresh .comm-report table.dataTable > tbody > tr.selected {
    background-color: transparent !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr.selected > td {
    background: linear-gradient(var(--primary-tint), var(--primary-tint)), var(--grid-sticky-bg, var(--bg-page)) !important;
    color: var(--ink) !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr.selected > td a {
    color: var(--ink) !important;
}

/* ── Filter row (kept) ── in-table per-column filters. Cells are vertically
   centered; controls read as clear, real input fields (same clarity as the
   multiselect) — a defined border + white surface on the --paper-2 band. */
.ui-refresh .comm-report table.dataTable > thead > tr.filters > th,
.ui-refresh .comm-report table.dataTable > thead > tr.filters > td {
    padding: var(--space-2) var(--space-3) !important;
    vertical-align: middle !important;
    background: transparent;
    border-bottom: 1px solid var(--line) !important;
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .form-control,
.ui-refresh .comm-report table.dataTable > thead > tr.filters .value-input,
.ui-refresh .comm-report table.dataTable > thead > tr.filters .multiselect.dropdown-toggle {
    height: 32px;
    min-height: 32px;
    padding: 0 var(--space-2);
    /* --input-border reads clearly on the header band; --line was too faint. */
    border: 1px solid var(--input-border);
    border-radius: var(--radius-sm);
    background: var(--card);
    color: var(--ink);
    font-size: 13px;
    font-weight: var(--font-weight-normal);
    box-shadow: none;
    vertical-align: middle;
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .form-control::placeholder,
.ui-refresh .comm-report table.dataTable > thead > tr.filters .value-input::placeholder {
    color: var(--muted);
}
/* min-width keeps narrow (auto-sized) columns from squeezing this control so
   tight that the caret icon overlaps the selected-text label. The reserved
   right padding gives the caret its own room regardless of column width, and
   the text itself truncates instead of running under it. */
.ui-refresh .comm-report table.dataTable > thead > tr.filters .multiselect.dropdown-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    min-width: 110px;
    padding-right: 28px;
    text-align: left;
    color: var(--ink);
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .multiselect.dropdown-toggle .multiselect-selected-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .form-control:hover,
.ui-refresh .comm-report table.dataTable > thead > tr.filters .value-input:hover,
.ui-refresh .comm-report table.dataTable > thead > tr.filters .multiselect.dropdown-toggle:hover {
    border-color: var(--muted-2);
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .form-control:focus,
.ui-refresh .comm-report table.dataTable > thead > tr.filters .value-input:focus,
.ui-refresh .comm-report table.dataTable > thead > tr.filters .multiselect.dropdown-toggle:focus {
    border-color: var(--accent-primary);
    box-shadow: var(--focus-ring);
    outline: none;
}
/* Column-filter multiselect dropdown.
   Two things make plain `.comm-report .multiselect-container` selectors
   unreliable here, confirmed live via the actual DOM while debugging why
   this never visibly applied:
   1. `ajax-datatables.js`'s `onDropdownShow` unconditionally moves this
      panel to a direct child of `<body>` (`self.$ul.appendTo('body')`, to
      escape `.dt-scroll`'s clipping) the instant it opens — every time,
      not just for edge-of-viewport cases. A selector requiring a
      `.comm-report` ancestor stops matching at that exact moment, so it
      can look right on hover/inspect-while-closed and then silently fail
      the moment a real user actually opens it. `.dt-filter-multiselect`
      (added by that same onDropdownShow, removed on hide) is a stable
      hook that doesn't depend on current DOM position.
   2. Even with that fixed, `components.css`'s
      `.multiselect-container .checkbox input[type="checkbox"]` rule
      (a deliberate reset back to native OS checkbox styling for
      multiselects elsewhere in the app) has equal specificity to a plain
      class-for-class port of this selector and can win the cascade tie
      depending on load order. `!important` on the checkbox's visual
      properties makes the win explicit rather than relying on asset
      dependency order to always favor this file.
   The plugin also defaults the panel to min-width:100%, which resolves
   against the scroll-head and balloons to the full table width — clamp it
   to a readable panel that hugs its options. */
.ui-refresh .multiselect-container.dt-filter-multiselect.dropdown-menu {
    min-width: 200px;
    max-width: 320px;
    width: max-content;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    box-shadow: var(--sh-md);
}
.ui-refresh .multiselect-container.dt-filter-multiselect > li > a {
    padding: var(--space-2) var(--space-3);
}
.ui-refresh .multiselect-container.dt-filter-multiselect > li > a:hover,
.ui-refresh .multiselect-container.dt-filter-multiselect > li > a:focus {
    background: var(--paper-2);
}
.ui-refresh .multiselect-container.dt-filter-multiselect label.checkbox {
    display: flex;
    align-items: center;
    margin: 0;
    color: var(--fg-default);
    font-weight: normal;
    padding: 0;
}
/* Bootstrap-multiselect renders a bare, unstyled `<input type="checkbox">`
   inside `label.checkbox` — restyled to match `input.select-all` (the
   header "select all" checkbox) so every checkbox in the refreshed report
   grid — row-select, select-all, and these column-filter dropdown options —
   reads as one consistent control. See the "Select checkboxes" rule further
   down this file (search `input.select-all`) for the actual declarations,
   shared with `input.select-all` in one combined rule rather than
   duplicated here — including why `!important` and `display: block` are
   needed (components.css's `.multiselect-container .checkbox
   input[type="checkbox"]` reset). */

/* Filter-row text/time/date inputs (`.filter-input-wrap`, added by
   communicate.js) — a chevron for time inputs (they open a picker, like a
   select would) at rest, replaced by an inline clear (×) once the field has
   a value. Only one of the two ever shows (toggled via `.has-value` on the
   wrapper), so they share one slot on the right. */
.ui-refresh .comm-report table.dataTable > thead > tr.filters .filter-input-wrap {
    position: relative;
    display: inline-flex;
    align-items: center;
    width: 100%;
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .filter-input-wrap .value-input {
    width: 100%;
    padding-right: 26px !important;
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .filter-input-chevron {
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 11px;
    color: var(--muted);
    /* Decorative only — clicks must pass through to the input underneath so
       clicking anywhere in the control still opens the timepicker. */
    pointer-events: none;
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .filter-input-wrap.has-value .filter-input-chevron {
    display: none;
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .filter-input-clear {
    display: none;
    align-items: center;
    justify-content: center;
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--muted);
    cursor: pointer;
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .filter-input-clear:hover {
    color: var(--accent-primary);
}
.ui-refresh .comm-report table.dataTable > thead > tr.filters .filter-input-wrap.has-value .filter-input-clear {
    display: inline-flex;
}

/* On a report table the toolbar row holds only the column-filter toggle, and the
   default margins strand it in open space between the page header and the grid.
   Pull it down so it shares a line with the grid's own "1 - 10 of 50" info row,
   which is what anchors it to the table it filters.

   `position:relative` makes this a stacking context, so everything it hosts —
   the fixed `.filter-pop`, the Report/Export menus — resolves its z-index at the
   toolbar's own depth, however high the child's z-index goes. The toolbar itself
   must therefore clear the sticky `.dt-scroll-head` (z-index:2), which at 1
   painted over both. */
.ui-refresh .comm-report .comm-toolbar {
    position: relative;
    z-index: 3;
    margin-top: 0;
    margin-bottom: calc(var(--space-6) * -2);
}
/* When a report is empty, _grid.php skips the widget entirely and renders
   `.list-empty` as a bare sibling of this toolbar instead of the table — the
   negative margin above (calibrated to align the funnel with the table's own
   info row) just pulls the empty card up into the toolbar's row instead, with
   nothing to compensate for it. Restore the toolbar's normal, non-overlapping
   spacing so the card sits cleanly below it. */
.ui-refresh .comm-report .comm-toolbar:has(+ .list-empty) {
    margin-bottom: var(--space-4);
}

/* Cap runaway column width. `.truncate-cell` (GridHelper::createTruncatableCell,
   via TruncatableColumnTrait::applyAutoTruncation) already ships `white-space:
   nowrap; overflow: hidden; text-overflow: ellipsis` plus a title tooltip and
   click-to-expand (ajax-datatables.js's initTruncatableCells()) — none of it has
   ever fired, because every existing width cap in this stack is a `max-width` on
   a <td>, which browsers ignore under table-layout: auto (CSS 2.1 §17.5.2 leaves
   it undefined there; min-width IS honoured, which is why the 100px floor
   elsewhere is real). `.truncate-cell` is `display: block` — a max-width on a
   block CHILD of a cell does contribute to the column's width, so this is what
   actually revives the existing ellipsis/tooltip/expand machinery, not a new
   feature. 400px pairs with `truncateAt: 55` on the contacts grid (_grid.php) —
   the two must move together, since only cells that were actually wrapped in
   .truncate-cell (i.e. exceeded the char threshold) are affected by this cap. */
.ui-refresh .contact-report .truncate-cell {
    max-width: 400px;
}

/* Firefox-only column misalignment: with scrollX, DataTables clones the real
   header into a hidden header row it INJECTS INTO THE BODY TABLE too,
   specifically so the body table's own column auto-sizing accounts for header
   width. ajax-datatables.css:324-326 hides that row with `visibility:
   collapse` — for a table row, `collapse` is a special case (CSS Tables spec)
   that removes the row from the table's layout entirely, not just from paint.
   Firefox drops the row's cells from column-WIDTH calculation too; Chromium
   still counts them (verified live — this is why the bug never reproduced in
   Chromium-based testing). Once a column has no visible data in every row
   (Created By etc. — no class attribute either, so no min-width floor),
   Firefox's body-table column shrinks to padding-only width; DataTables
   clones that narrow width onto the header table on every draw, and every
   column after it shifts to make room for the label + filter control the
   header actually needs.
   Fix: `visibility: visible` restores the row to normal layout (and Firefox's
   width calculation) — but a normal, in-flow header-styled row is a visible
   ~36px blank bar between the real header and the first data row (confirmed
   live: the row is a clone of `tr.header-row`, so it inherits
   `ajax-datatables.css`'s `.table-condensed thead tr.header-row th { height:
   36px !important }` — a `<tr>`'s own height is only ever a hint, the tallest
   cell wins, so zeroing the row alone did nothing). Override that `th` height
   directly, scoped/specific enough to beat it despite both being
   `!important`. Confirmed live: 0px column deltas across an 18-column report
   with 4 empty columns, AND no visible gap, in both Firefox and Chromium.
   Scoped to Contacts, Meeting Report, Automation Executions and Swoogo
   Admin > Go Attend (its Yes/No columns sit under wide filter dropdowns) and
   Go Attend > White-Label Apps, and the event registrant report. Meeting
   Report's time filters also need their widths included. Other .comm-report
   grids keep the existing rules (the min-width
   floor at :81-83 never matches these cells either — a related but separate
   latent bug, tracked, not fixed here).
   `tr.filters th` needs the same zeroing as `tr.header-row th`: this clone
   also carries a copy of the (now-visible, kept) column-filter row, and its
   32px-tall `.value-input` controls (communicate.css's "Filter row (kept)"
   skin) rendered as a second, empty-looking header band between the real
   header and the first data row once contacts stopped hiding `tr.filters`
   globally.
   `border-bottom-width` has to be zeroed too, alongside `border-top-width` —
   vendor DataTables CSS (`div.dt-scroll-body thead tr th { ... border-top-width:
   0 !important; border-bottom-width: 0 !important }`) already zeroes both, but
   the "Filter row (kept)" skin's `tr.filters > th { border-bottom: 1px solid
   var(--line) !important }` (and `tr.header-row > th`'s own border-bottom in
   the header-band rules above) outrank it. Left unzeroed, each zero-height row
   in this hidden clone still paints its bottom border, so header-row's and
   filters row's hairlines stack directly on top of the real header's own
   border-bottom — reading as one thick/doubled divider above the first data
   row instead of one hairline. */
.ui-refresh :is(.contact-report, .meeting-report, .automation-execution-report, .go-attend-report, .go-attend-whitelabel-report, .event-registrant-report) .dt-scroll-body .table-condensed thead tr {
    visibility: visible;
}
.ui-refresh :is(.contact-report, .meeting-report, .automation-execution-report, .go-attend-report, .go-attend-whitelabel-report, .event-registrant-report) .dt-scroll-body .table-condensed thead tr.header-row th,
.ui-refresh :is(.contact-report, .meeting-report, .automation-execution-report, .go-attend-report, .go-attend-whitelabel-report, .event-registrant-report) .dt-scroll-body .table-condensed thead tr.filters th {
    height: 0 !important;
    padding-top: 0 !important;
    padding-bottom: 0 !important;
    border-top-width: 0 !important;
    border-bottom-width: 0 !important;
    overflow: hidden !important;
}
/* Swoogo Admin › Registrants, extended off the verified contacts fix above for
   the same symptom: the clone's un-zeroed `padding-top` (8px, measured) makes
   the body table lay its columns out to different widths than the real header
   does, so the header labels sit off from the values underneath them. */
.ui-refresh .registrant-report .dt-scroll-body .table-condensed thead tr {
    visibility: visible;
}
.ui-refresh .registrant-report .dt-scroll-body .table-condensed thead tr.header-row th,
.ui-refresh .registrant-report .dt-scroll-body .table-condensed thead tr.filters th {
    height: 0 !important;
    padding-top: 0 !important;
    padding-bottom: 0 !important;
    border-top-width: 0 !important;
    border-bottom-width: 0 !important;
    overflow: hidden !important;
}
/* Swoogo Admin > Registrants, before a search runs: the dashed .list-empty
   prompt is the form's next sibling, and neither the form nor .list-empty
   carries a margin there, so the box butts straight up against the Search
   button. The two states that follow a real search are already separated by
   a .form-section ("Search Results"), hence the adjacency selector rather
   than a blanket margin on .list-empty. */
.ui-refresh .registrant-search > form + .list-empty {
    margin-top: var(--space-6);
}
/* Dismiss button on the Registrants page's own flash banners
   (registrant/_flash-banners.php). The shared `.inline-alert__action .btn`
   styling turns anything in that slot into a 32px bordered button, which reads
   as a call to action rather than a close - this borrows the toast's quieter
   close treatment instead, since it is the same gesture. */
.ui-refresh .registrant-search .inline-alert__dismiss {
    flex-shrink: 0;
    background: none;
    border: none;
    color: var(--fg-subtle);
    cursor: pointer;
    padding: var(--space-1);
    font-size: var(--font-size-xs);
    line-height: 1;
    transition: color var(--transition-fast);
}
.ui-refresh .registrant-search .inline-alert__dismiss:hover,
.ui-refresh .registrant-search .inline-alert__dismiss:focus {
    color: var(--fg-default);
}
/* The banners are the first thing inside .list-page, above the breadcrumb's
   own bottom margin, so they need their own top gap to clear the subpage-head
   bar rather than sitting flush against it. */
.ui-refresh .registrant-search > .inline-alert:first-child {
    margin-top: var(--space-4);
}

/* Automation executions keeps search, count and report actions in one native
   full-width DataTables cell. Allow the controls to wrap without squeezing the
   search field or separating the count into a second Bootstrap column. */
.ui-refresh .automation-execution-report .comm-report-toolbar {
    flex-wrap: wrap;
}
.ui-refresh .automation-execution-report .dt-search {
    position: relative;
    flex: 0 1 48ch;
    min-width: 0;
    max-width: 100%;
}
/* Details contains a responsive summary/expander. Give it enough room to be
   readable and let the report's existing horizontal scroll carry the extra
   table width instead of collapsing the summary into a narrow wrapped cell. */
.ui-refresh .automation-execution-report table.dataTable th.automation-execution-details,
.ui-refresh .automation-execution-report table.dataTable td.automation-execution-details {
    min-width: 280px;
}

.ui-refresh .automation-execution-report .dt-search input {
    width: 100% !important;
    height: var(--input-height) !important;
    margin-left: 0;
}
.ui-refresh .automation-execution-report .dt-search .clear-search {
    position: absolute;
    right: var(--space-3);
    top: 50%;
    transform: translateY(-50%);
}
.ui-refresh .automation-execution-report .breadcrumb-btn,
.ui-refresh .automation-execution-report .breadcrumb-btn > .btn-group {
    display: flex;
    flex-wrap: wrap;
    max-width: 100%;
}
.ui-refresh .automation-execution-report .breadcrumb-btn {
    gap: var(--space-2);
}

/* Report toolbar (Filters / Report / Export) — give the button row clear
   breathing room above and below so it doesn't crowd the description text or
   the grid. Also matches `.list-page--wide .breadcrumb-btn` directly: a
   handful of report-grid pages (scan/exhibitor.php among them) render extra
   page content — an embed, instructional text — between the toolbar and the
   grid, so their toolbar isn't a `.comm-report` descendant even though it's
   still inside the wide report-grid page; `.list-page--wide` is only ever
   present on those pages (added by _report-grid-page-open.php, and by the
   Swoogo Admin accounts list, which hand-rolls the same wrapper),
   so this is scoped just as tightly as the `.comm-report` selector. */
.ui-refresh .comm-report .breadcrumb-btn,
.ui-refresh .list-page--wide .breadcrumb-btn {
    margin-top: var(--space-4) !important;
    margin-bottom: var(--space-5) !important;
}

/* "Show deleted records" toggle in the report toolbar (Swoogo Admin > Accounts).
   mergeReportToolbar() drops the `.breadcrumb-btn` row into the DataTables info
   cell, so this sits opposite "Showing 1 to N of M" and reads as a filter rather
   than as the stray checkbox it used to be under the grid. The switch itself
   comes from the global `.checkbox` toggle rules (components.css); all that is
   needed here is inline placement and label typography, since `.checkbox` is a
   block by default. */
.ui-refresh .breadcrumb-btn .grid-toolbar-toggle-form {
    display: inline-flex;
    align-items: center;
    vertical-align: middle;
    margin-right: var(--space-4);
}
.ui-refresh .grid-toolbar-toggle label {
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--fg-muted);
    white-space: nowrap;
}

/* Report toolbar (Filters / Report / Export) — communicate.js's
   mergeReportToolbar() moves the PHP-rendered `.breadcrumb-btn` row into the
   same `.dt-layout-cell` as DataTables' own "Showing X to X of X" info text,
   so they read as one row: info on the left, buttons on the right. */
.ui-refresh .comm-report .dt-layout-row:first-child .dt-layout-cell.comm-report-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    width: 100%;
}

.ui-refresh .comm-report .comm-report-toolbar .breadcrumb-btn {
    float: none;
    margin: 0 !important;
}
/* Give the merged row (and the grid below it) matching breathing room. */
.ui-refresh .comm-report .dt-layout-row:first-child,
.ui-refresh .comm-report .dataTables_wrapper > .row:first-child {
    padding-top: var(--space-3);
    padding-bottom: 0;
}
/* Hide the merged row entirely when the table has no rows to show. */
.ui-refresh .comm-report .dt-layout-row:first-child.is-empty .dt-info {
    visibility: hidden;
}
/* Grids with no toolbar (GDPR Delete Queue, Email Suppression List, the
   sync/queue reports): the first row holds nothing but the "Showing X to Y of
   Z" line, yet still carries the merged-toolbar padding above plus
   datatables.min.css's `.75em` top/bottom margin, and the table row below adds
   another `.75em` — ~50px of air around one line of xs text, which reads as a
   dead gap under the breadcrumb. Collapse it to a single small gap above the
   table. Keyed off the absence of the two things that can fill the row
   (mergeReportToolbar()'s `.comm-report-toolbar`, AjaxDataTables' topRightHtml
   `.dt-filter-pop`), so toolbar grids keep the spacing above. The
   `div.dt-container` prefix is needed to beat datatables.min.css's own
   `div.dt-container div.dt-layout-row` (0,2,2) margin rule. */
.ui-refresh .comm-report div.dt-container > div.dt-layout-row:first-child:not(:has(.comm-report-toolbar, .dt-filter-pop)) {
    margin-top: 0;
    margin-bottom: var(--space-2);
    padding-top: 0;
}
.ui-refresh .comm-report div.dt-container > div.dt-layout-row:first-child:not(:has(.comm-report-toolbar, .dt-filter-pop)) + div.dt-layout-row {
    margin-top: 0;
}

/* DataTables' layout cells are Bootstrap columns carrying `padding: 0 15px`, but
   .dt-layout-row is a flex container with no negative side margins to cancel it.
   That insets the table, the row count and the pager 15px inside .comm-report
   while the page header and toolbar sit flush at its edge — so the breadcrumb
   overhangs the table on the left and the action buttons overhang it on the
   right. Drop the gutter so the grid spans the same box as everything above it.

   The selector mirrors datatables.min.css's own
   `div.dt-container div.dt-layout-row div.dt-layout-cell` (0,3,3) and adds the
   .comm-report scope, because anything shorter loses to it on specificity. */
.ui-refresh .comm-report div.dt-container div.dt-layout-row div.dt-layout-cell {
    padding-left: 0;
    padding-right: 0;
}
/* ajax-datatables.css nudges the row count 10px right; with the gutter gone that
   is the only thing left breaking the left edge. */
.ui-refresh .comm-report .new-grid-theme .dt-info {
    margin-left: 0;
}
.ui-refresh .automation-execution-report .new-grid-theme .dt-info {
    margin-left: auto;
    margin-right: 0;
    white-space: normal;
}

/* `.dt-filter-pop` is the wrapper contacts' topRightHtml (AjaxDataTables)
   renders into, via DataTables' `div` layout feature — see
   AjaxDataTables::getClientOptions(). Its cell has zero right padding
   (the .dt-layout-cell rule above), so canceling the funnel's own 4px side
   margin (components.css .filter-toggle) leaves it with a literal zero
   right inset — flush with .dt-scroll's right edge, i.e. exactly where a
   horizontal scrollbar's track ends and where the last column gets
   clipped. Nothing is usually painted at that x, so the flush edge reads
   as fine — until a scrollbar appears there and makes the missing gap
   visible ("glued"). Give it back a real inset instead. */
.ui-refresh .comm-report .dt-filter-pop .filter-toggle {
    margin-right: var(--space-2);
}

/* Stop the flash of the un-initialised (bordered/striped) table: keep the raw
   table hidden until DataTables adds its `.dataTable` class, and show a spinner
   in the meantime. The spinner is hidden once the table is initialised. */
.ui-refresh .comm-report {
    position: relative;
    min-height: 120px;
}
.ui-refresh .comm-report table:not(.dataTable) {
    opacity: 0;
}
.ui-refresh .comm-report table.dataTable {
    opacity: 1;
    transition: opacity var(--transition-normal) ease;
}
.ui-refresh .comm-report::before {
    content: '';
    position: absolute;
    top: 56px;
    left: 50%;
    width: 28px;
    height: 28px;
    margin-left: -14px;
    border: 3px solid var(--line);
    border-top-color: var(--accent-primary);
    border-radius: 50%;
    animation: comm-report-spin 0.7s linear infinite;
    pointer-events: none;
}
.ui-refresh .comm-report:has(table.dataTable)::before,
.ui-refresh .comm-report:has(.list-empty)::before {
    /* registrant/_grid.php's server-side count check skips the
       AjaxDataTables widget entirely on an empty report — no `<table>`
       ever renders, so `table.dataTable` never appears and the spinner
       above never turns off. `.list-empty` is the other sign the report
       has actually finished loading (whether from that PHP short-circuit
       or the live-filtered-to-zero case where it's nested inside the
       table), so it stops the loader too. */
    display: none;
}
@keyframes comm-report-spin {
    to { transform: rotate(360deg); }
}

/* ── Controls (DataTables v2 hooks) ── search box, record info, pagination.
   (Page-length `.dt-length > select` is tokenised in the FixedColumns block
   below.) The app runs DataTables 2, which emits .dt-search / .dt-info /
   .dt-paging — the legacy .dataTables_* classes never render, so target v2. */
.ui-refresh .comm-report .dt-search input {
    height: var(--input-height);
    border: 1px solid var(--input-border) !important;
    border-radius: var(--input-radius) !important;
    background-color: var(--bg-surface);
    color: var(--fg-default);
    font-size: var(--font-size-sm);
}

.ui-refresh .comm-report .dt-search input:focus {
    border-color: var(--accent-primary) !important;
    box-shadow: var(--focus-ring);
    outline: none;
}

.ui-refresh .comm-report .dt-info,
.ui-refresh .comm-report .dt-info .of-text {
    color: var(--muted);
    font-size: var(--font-size-xs);
}

/* Pill pagination — matches session .pgn: muted at rest, subtle hover, tinted
   accent for the current page. */
.ui-refresh .comm-report .dt-paging .dt-paging-button {
    min-width: 32px;
    height: 32px;
    padding: 0 var(--space-2);
    margin-left: var(--space-1);
    border: 1px solid transparent !important;
    border-radius: var(--radius-md) !important;
    color: var(--muted) !important;
    background: transparent !important;
    box-shadow: none;
}

.ui-refresh .comm-report .dt-paging .dt-paging-button:hover {
    background: var(--paper-2) !important;
    color: var(--ink) !important;
}

/* Current page — the Bootstrap integration marks the li `active` (vanilla
   DataTables uses `current`); cover both. */
.ui-refresh .comm-report .dt-paging .dt-paging-button.current,
.ui-refresh .comm-report .dt-paging .dt-paging-button.current:hover,
.ui-refresh .comm-report .dt-paging .dt-paging-button.active,
.ui-refresh .comm-report .dt-paging .dt-paging-button.active:hover {
    background: var(--primary-tint) !important;
    color: var(--accent-primary) !important;
    border-color: transparent !important;
    font-weight: var(--font-weight-semibold);
}

/* The DataTables Bootstrap integration nests an anchor inside each paging li.
   Bootstrap's .pagination anchor chrome (its own background/border) plus
   ajax-datatables.css's `.pagination > li:hover a` border ring double-paint
   INSIDE the pill — flatten the inner element so only the li pill shows. */
.ui-refresh .comm-report .dt-paging .dt-paging-button > a,
.ui-refresh .comm-report .dt-paging .dt-paging-button > span,
.ui-refresh .comm-report .dt-paging .dt-paging-button > button {
    background: transparent !important;
    border: 0 !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    padding: 0 !important;
    margin: 0 !important;
    color: inherit !important;
    font: inherit;
}
.ui-refresh .comm-report .dt-paging .dt-paging-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* Center pagination under the table, on the same line as "Items per page".
   Confirmed from the live DOM: this row is Bootstrap's `.row` with two
   `.col-sm-6` halves — pageLength in the left half, pagination in the right
   half. `justify-content: center` on the pagination cell alone only centers
   it within its OWN half (landing ~75% across, never true row-center).
   An equal-thirds flex approach (pageLength / pagination / an invisible
   `::after` spacer, all `flex: 1 1 0`) was tried first and looked right on
   wide tables, but broke on narrower ones: flex's `flex-basis: 0` ignores
   content size for the *initial* share, so on a table narrow enough that
   1/3 of its width is less than the pagination control's own natural width,
   the pagination `<ul>` (itself `display: flex; flex-wrap: wrap`, from base
   DataTables/Bootstrap styling, unrelated to this fix) gets squeezed below
   its content's needed width and wraps the last button onto its own line.
   Grid fixes this because `minmax(max-content, 1fr)` on the *middle* track
   only is content-aware: that track never shrinks below what pagination
   needs, while still sharing extra space via its `1fr` upper bound like the
   other two. The two outer tracks explicitly use `minmax(0, 1fr)` — plain
   `1fr` alone has an implicit `auto` (content-based) minimum too, which
   would otherwise let the (usually wider) pageLength control push its own
   track wider than intended, unbalancing the centering. `min-width: 0` on
   the cells themselves matters for the same reason: a grid item's default
   automatic minimum size is its own min-content width, which overrides an
   explicit `minmax(0, …)` on the *track* unless the *item* also opts out.
   With only 2 real cells against 3 defined tracks, the 3rd (trailing) one
   is simply never filled — no `::after` spacer markup needed, unlike flex.
   `:has(.dt-paging)` scopes this to the row that actually contains
   pagination, leaving the info+search row above untouched. No `!important`
   on `display` — communicate.js hides this same row (plain `.hide()`, a
   non-important inline `display: none`) for small/empty tables, and an
   `!important` here would win over that inline style and silently keep the
   row visible. Nothing else sets `display` on this selector with higher
   specificity, so plain `grid` is enough.
   This mirrors reports.css's equivalent fix for the All Reports index (a
   flex version, not yet updated to this more robust grid one — that page's
   tables have historically been wide enough not to hit the wrap bug, but
   the same fix would apply there too) — that file only loads on that one
   page, so every other `.comm-report` grid needed its own copy here.
   Two more non-obvious fixes were needed to get true centering, both
   confirmed live via getComputedStyle before writing here:
   1. This row's class is `row dt-layout-row` — Bootstrap 3's `.row`
      clearfix (`.row:before, .row:after { content: " "; display: table; }`)
      gives it two pseudo-elements with real, non-empty `content`. Once the
      row becomes a grid container those pseudo-elements are genuine grid
      items too, silently occupying the first track ahead of the two real
      children and shifting both one column to the right. Must neutralize
      `content`/`display` on both, scoped to this row only.
   2. `width: auto` on a grid item does NOT reliably stretch it to fill its
      track when the item is itself a flex container (`.dt-layout-end`) —
      it was observed shrink-wrapping to its content's width instead, which
      left `justify-content: center` centering within a too-narrow box
      (visually flush against the track's trailing edge). `width: 100%`
      forces the fill explicitly. */
.ui-refresh .comm-report div.dt-container div.dt-layout-row:has(.dt-paging) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(max-content, 1fr) minmax(0, 1fr);
    align-items: center;
}
.ui-refresh .comm-report div.dt-container div.dt-layout-row:has(.dt-paging)::before,
.ui-refresh .comm-report div.dt-container div.dt-layout-row:has(.dt-paging)::after {
    content: none !important;
    display: none !important;
}
.ui-refresh .comm-report div.dt-container div.dt-layout-row:has(.dt-paging) > div.dt-layout-cell {
    float: none !important;
    width: 100% !important;
    min-width: 0;
}
.ui-refresh .comm-report div.dt-container div.dt-layout-row:has(.dt-paging) > div.dt-layout-end {
    display: flex !important;
    justify-content: center !important;
}
/* AjaxDataTables::bottomLeftHtml (contacts' "With Checked Records") replaces the
   default two-cell bottomStart/bottomEnd split with one `bottom` full-width
   cell holding all three controls, so this row has a single `.dt-layout-full`
   child instead of start+end. Without this it only fills the row's first 1fr
   track (one third of the width) — span all three. Its own layout then comes
   free from the vendor default (`div.dt-layout-cell { display:flex;
   justify-content:space-between }`, datatables.min.css) — button left,
   pagination and length share the remaining space, no extra grid needed. */
.ui-refresh .comm-report div.dt-container div.dt-layout-row:has(.dt-paging) > div.dt-layout-full {
    grid-column: 1 / -1;
}

/* Prev/next arrows — the Bootstrap integration puts previous/next on the
   INNER anchor, not the li. Ink (which flips per surface, so it holds in
   light and dark) at a larger glyph size for real contrast; the disabled
   state dims via the li's opacity but stays legible. */
.ui-refresh .comm-report .dt-paging .page-link.previous,
.ui-refresh .comm-report .dt-paging .page-link.next {
    color: var(--ink) !important;
    font-size: 17px;
    line-height: 1;
    font-weight: var(--font-weight-semibold);
}

.ui-refresh .comm-report .dt-paging .dt-paging-button.disabled {
    opacity: 0.55;
}

/* ── FixedColumns (freeze columns) — surface-tone fix ──
   DataTables FixedColumns hardcodes `background-color: white` on the frozen
   column clones, blockers, and the page-length select (datatables.min.css /
   ajax-datatables.css). Its dark variant is gated on `html.dark`, a hook this
   app never sets — it themes via body[data-surface] — so the frozen
   Actions/checkbox columns, the header-overlap blockers, and the "Items per
   page" select all rendered white on dark surfaces. Re-point them to the same
   surface tokens the rest of the .comm-report table already uses so the frozen
   columns occlude scrolled content seamlessly in every surface tone (tokens
   re-resolve per surface, so no data-surface rules are needed).
   FixedColumns 5 emits both `.dtfc-fixed-start/-end` and the positional aliases
   `.dtfc-fixed-left/-right`, so the body rule below matches on `td`/`th` to
   cover whichever the clone carries. */
.ui-refresh .comm-report table.dataTable > tbody > tr > td.dtfc-fixed-start,
.ui-refresh .comm-report table.dataTable > tbody > tr > td.dtfc-fixed-end,
.ui-refresh .comm-report table.dataTable > tbody > tr > td.dtfc-fixed-left,
.ui-refresh .comm-report table.dataTable > tbody > tr > td.dtfc-fixed-right,
/* The `.table-striped` variants are required so the opaque background beats the
   de-zebra rule above — without them the frozen cells on de-striped rows fall
   back to `transparent` and horizontally-scrolled columns bleed through the
   sticky Actions/checkbox column. */
.ui-refresh .comm-report table.dataTable.table-striped > tbody > tr > td.dtfc-fixed-start,
.ui-refresh .comm-report table.dataTable.table-striped > tbody > tr > td.dtfc-fixed-end,
.ui-refresh .comm-report table.dataTable.table-striped > tbody > tr > td.dtfc-fixed-left,
.ui-refresh .comm-report table.dataTable.table-striped > tbody > tr > td.dtfc-fixed-right {
    background-color: var(--grid-sticky-bg, var(--bg-page)) !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr:hover > td.dtfc-fixed-start,
.ui-refresh .comm-report table.dataTable > tbody > tr:hover > td.dtfc-fixed-end,
.ui-refresh .comm-report table.dataTable > tbody > tr:hover > td.dtfc-fixed-left,
.ui-refresh .comm-report table.dataTable > tbody > tr:hover > td.dtfc-fixed-right {
    background-color: var(--paper-2) !important;
}
/* Selected frozen cells keep the row's accent tint so they don't fall back to
   the plain page tone under the sticky column. Some surface/accent combinations
   define --primary-tint with transparency, so layer it over --bg-page here;
   sticky cells must remain opaque while columns scroll underneath them. */
.ui-refresh .comm-report table.dataTable > tbody > tr.selected > td.dtfc-fixed-start,
.ui-refresh .comm-report table.dataTable > tbody > tr.selected > td.dtfc-fixed-end,
.ui-refresh .comm-report table.dataTable > tbody > tr.selected > td.dtfc-fixed-left,
.ui-refresh .comm-report table.dataTable > tbody > tr.selected > td.dtfc-fixed-right {
    background: linear-gradient(var(--primary-tint), var(--primary-tint)), var(--grid-sticky-bg, var(--bg-page)) !important;
}
/* Header + filter-row + footer frozen cells = the header band tone. */
.ui-refresh .comm-report table.dataTable > thead > tr > .dtfc-fixed-start,
.ui-refresh .comm-report table.dataTable > thead > tr > .dtfc-fixed-end,
.ui-refresh .comm-report table.dataTable > thead > tr > .dtfc-fixed-left,
.ui-refresh .comm-report table.dataTable > thead > tr > .dtfc-fixed-right,
.ui-refresh .comm-report table.dataTable > tfoot > tr > .dtfc-fixed-start,
.ui-refresh .comm-report table.dataTable > tfoot > tr > .dtfc-fixed-end,
.ui-refresh .comm-report table.dataTable > tfoot > tr > .dtfc-fixed-left,
.ui-refresh .comm-report table.dataTable > tfoot > tr > .dtfc-fixed-right {
    background-color: var(--paper-2) !important;
}
/* Header/footer overlap blockers (the white strip at the header's right edge). */
.ui-refresh .comm-report .dtfc-top-blocker {
    background-color: var(--paper-2) !important;
}
.ui-refresh .comm-report .dtfc-bottom-blocker {
    background-color: var(--grid-sticky-bg, var(--bg-page)) !important;
}
/* ── FixedColumns — frozen-boundary divider ──
   ajax-datatables.css draws the only boundary cue as an inset shadow gated on
   `table.dataTable.dtfc-scrolling-left`, which reads as a heavy drop shadow
   against this flat skin. Repurpose the same ::after strip the vendor already
   positions inside the last frozen cell as a 1px rule, revealed only while the
   grid is scrolled sideways so an unscrolled grid stays clean. Gated on
   FixedColumns' own `dtfc-scrolling-start`, which it sets from the scroll
   container it owns (.dt-scroll-body) — the box that genuinely scrolls
   horizontally, see the .dt-scroll block above. It's an absolutely positioned overlay, so it adds
   no width to the cell and can't disturb the colgroup widths
   ajax-datatables.js reads back to place the sticky offsets. --border-strong
   sits one step above the --border-default row separators, so the divider reads
   as a boundary rather than another row line. */
.ui-refresh .comm-report table.dataTable tr > .dtfc-fixed-start:has(+ :not(.dtfc-fixed-start))::after {
    box-shadow: none !important;
    width: 1px;
    right: 0;
    background: transparent;
    transition: background var(--transition-fast);
}
.ui-refresh .comm-report table.dataTable.dtfc-scrolling-start tr > .dtfc-fixed-start:has(+ :not(.dtfc-fixed-start))::after {
    background: var(--border-strong);
}
/* "Items per page" select uses DataTables 2's `.dt-length` class (the rule
   higher up targets the legacy `.dataTables_length`); re-point it to the input
   tokens so it matches the surface like the filter inputs. Transparent
   background instead of a filled surface, matching the ghost-style buttons
   elsewhere on the page (e.g. the action column). The select also carries
   Bootstrap's `.form-control` class, and `.ui-refresh .form-control` sets
   `background-color: var(--card) !important` globally (components.css) — our
   own selector is more specific, so matching its `!important` is enough to
   win. */
.ui-refresh .comm-report .dt-length > select {
    height: var(--input-height);
    border: 1px solid var(--input-border);
    border-radius: var(--input-radius);
    background-color: transparent !important;
    color: var(--fg-default);
    font-size: var(--font-size-sm);
}
/* ajax-datatables.css hardcodes `.dt-length > label:not(select)` to
   `--swoogo-gray-600` — a static, non-flipping gray that reads far too dark
   against dark surfaces. Match the regular body text color instead. */
.ui-refresh .comm-report .dt-length > label:not(select) {
    color: var(--fg-default);
}

/* ── Select checkboxes ── match the .row-select purple check. Three
   mechanisms share this look: the header "select all" (a real
   <input.select-all>), the per-row boxes (DataTables Select pseudo
   `td.select-checkbox::before`, below), and the column-filter multiselect
   dropdown's option checkboxes (`.dt-filter-multiselect` — see the comment
   further up this file, where the marker class is explained). The shared
   box/checkmark declarations below cover both `input.select-all` and the
   multiselect checkbox in one rule rather than two near-identical blocks.
   `!important` throughout is only strictly required for the multiselect
   checkbox (to beat components.css's
   `.multiselect-container .checkbox input[type="checkbox"]` reset, which
   pushes multiselect checkboxes back toward native appearance elsewhere in
   the app) — applying it uniformly here keeps this one source of truth
   instead of forking on specificity needs. */
.ui-refresh .comm-report table.dataTable input.select-all,
.ui-refresh .multiselect-container.dt-filter-multiselect input[type="checkbox"] {
    appearance: none !important;
    -webkit-appearance: none !important;
    box-sizing: border-box;
    width: 16px;
    height: 16px;
    border: 1.5px solid var(--line-2) !important;
    border-radius: var(--radius-sm) !important;
    background: var(--card) !important;
    cursor: pointer;
    position: relative;
    transition: background var(--transition-fast), border-color var(--transition-fast);
}
/* DataTables 2 wraps every header cell's content in a flex `.dt-column-header`
   (a `.dt-column-title` label + an empty `.dt-column-order` sort-indicator
   span), including the filters-row cell holding select-all — there's no sort
   caret here (column 0 is `orderable: false`), but the wrapper still renders.
   Centering select-all via `text-align`/`vertical-align` inside that flex
   context drifts a couple of px off true-center (the zero-width order span
   still participates in `justify-content`), and gets worse once FixedColumns
   clones this header cell for the frozen checkbox column. Instead, take the
   checkbox out of flex flow and center it directly against the `<th>` itself
   — the same absolute-position math `td.select-checkbox::before` uses below
   for row checkboxes, so header and rows land pixel-for-pixel aligned. */
.ui-refresh .comm-report table.dataTable > thead > tr.filters > th.select-checkbox {
    position: relative;
}
.ui-refresh .comm-report table.dataTable input.select-all {
    position: absolute;
    top: 50%;
    left: 50%;
    margin: -8px 0 0 -8px;
}
.ui-refresh .multiselect-container.dt-filter-multiselect input[type="checkbox"] {
    flex: 0 0 auto;
    margin: 0 8px 0 0 !important;
}
.ui-refresh .comm-report table.dataTable input.select-all:hover,
.ui-refresh .multiselect-container.dt-filter-multiselect input[type="checkbox"]:hover {
    border-color: var(--muted-2) !important;
}
.ui-refresh .comm-report table.dataTable input.select-all:checked,
.ui-refresh .comm-report table.dataTable input.select-all:indeterminate,
.ui-refresh .multiselect-container.dt-filter-multiselect input[type="checkbox"]:checked {
    background: var(--accent-primary) !important;
    border-color: var(--accent-primary) !important;
}
/* The legacy `input[type=checkbox]:checked:before` in backend.css paints a
   second, Font-Awesome checkmark. That rule is unscoped, so it hits the
   column-filter multiselect option checkboxes as well as select-all — both
   need suppressing so only the ::after glyph we draw shows. These selectors
   out-specify it. */
.ui-refresh .comm-report table.dataTable input.select-all::before,
.ui-refresh .comm-report table.dataTable input.select-all:checked::before,
.ui-refresh .multiselect-container.dt-filter-multiselect input[type="checkbox"]::before,
.ui-refresh .multiselect-container.dt-filter-multiselect input[type="checkbox"]:checked::before {
    content: none;
    display: none;
}
/* `:not(:indeterminate)` matters for select-all: ajax-datatables.js sets
   checked AND indeterminate together for a partial selection, so without it
   this rule's border survives into the dash below and draws an L-shaped stub.
   The multiselect option checkboxes are never set indeterminate, so they stay
   on the plain `:checked`. */
.ui-refresh .comm-report table.dataTable input.select-all:checked:not(:indeterminate)::after,
.ui-refresh .multiselect-container.dt-filter-multiselect input[type="checkbox"]:checked::after {
    content: "";
    display: block !important;
    position: absolute;
    left: 50%;
    top: 50%;
    width: 4px;
    height: 8px;
    border: solid #fff;
    border-width: 0 2px 2px 0;
    transform: translate(-50%, -60%) rotate(45deg);
}
/* select-all's indeterminate (partial-selection) dash — no multiselect
   checkbox equivalent. */
.ui-refresh .comm-report table.dataTable input.select-all:indeterminate::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 8px;
    height: 2px;
    background: #fff;
    transform: translate(-50%, -50%);
}
.ui-refresh .comm-report table.dataTable > tbody > tr > td.select-checkbox::before {
    width: 16px !important;
    height: 16px !important;
    margin-top: -8px !important;
    margin-left: -8px !important;
    border: 1.5px solid var(--line-2) !important;
    border-radius: var(--radius-sm) !important;
    background: var(--card);
    box-sizing: border-box;
    content: " " !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr.selected > td.select-checkbox::before {
    background: var(--accent-primary);
    border-color: var(--accent-primary) !important;
    color: #fff;
    content: "\2713" !important; /* ✓ */
    font-size: 11px !important;
    line-height: 13px !important;
    text-align: center;
}

/* ── Actions cell (kept, ghosted) ── the per-row "Actions ▾" dropdown restyled
   to a quiet, borderless control (mirrors the .lr-icon hover treatment) and
   right-aligned like .lt-cell-actions.
   Width is handled entirely by `initActionColWidth` (communicate.js), NOT a
   fixed CSS floor — DataTables bakes an explicit pixel width into this
   column's <col> element from its OWN autoWidth pass, measured against the
   buttons' pre-shrink Bootstrap default size (~36-40px), before the 28px
   icon-button size below ever applies. That stale, inflated <col> width is
   what actually controls the rendered column width (a `<col>` wins over any
   width/min-width/max-width set here on the `<td>`/`<th>` themselves), which
   is why a flat `min-width` here previously either did nothing (smaller than
   the inflated <col>) or overshot every grid to the same fixed floor
   regardless of its real button count (3 icons and 4 icons both rendered
   184px wide — confirmed live, not assumed). `white-space: nowrap` still
   matters as a backstop against wrapping regardless of what sets the width. */
.ui-refresh .comm-report table.dataTable th.action-col,
.ui-refresh .comm-report table.dataTable td.action-col {
    text-align: right !important;
    white-space: nowrap;
}
/* This grid has one delete icon; keep it under the left-aligned Actions label. */
.ui-refresh .duplicate-attendance-report table.dataTable th.action-col,
.ui-refresh .duplicate-attendance-report table.dataTable td.action-col {
    text-align: left !important;
}
/* `.action-col--menu` (invitation-list-contact/_actionsButton.php) is a leading
   column holding one labeled "Actions ▾" dropdown, not a trailing icon strip —
   same reasoning as the contacts override below. */
.ui-refresh .comm-report table.dataTable th.action-col--menu,
.ui-refresh .comm-report table.dataTable td.action-col--menu {
    text-align: left !important;
}
/* The 28px square below is sized for icon-only buttons; on a labeled toggle it
   shrinks the hit area to the caret while the label overflows it. */
.ui-refresh .comm-report table.dataTable td.action-col--menu .btn.dropdown-toggle {
    width: auto !important;
    height: var(--space-7) !important;
    padding: 0 var(--space-2) !important;
}
/* Contacts puts the action column first (it's $columns[0], pinned by
   FixedColumns) rather than last as .lt-cell-actions and the All Reports grid
   do. Right-aligning a leading column pools the cell's slack on its left, which
   reads as a gap between the select-all checkbox and the first icon. Can't key
   this off :last-child — FixedColumns clones the pinned cells into their own
   table, where the action cell genuinely is last. */
.ui-refresh .contact-report table.dataTable th.action-col,
.ui-refresh .contact-report table.dataTable td.action-col {
    text-align: left !important;
}
/* Shrink the column to its icons. ActionColumn::renderColumn() hard-codes
   `56 + 22 * n` px inline on the cells, sized for the legacy 22px buttons and an
   "Actions" header label; the refresh draws 28px squares and drops the label, so
   the number is stale. `width: 1%` is the shrink-to-fit idiom .lt-cell-actions
   already uses — in auto table layout the browser gives a 1% cell only what its
   content needs. `!important` is what beats the inline style, and the padding is
   pulled back to --space-2 because the generic cell rule adds 12px a side to a
   cell the legacy theme deliberately zeroed. */
.ui-refresh .contact-report table.dataTable th.action-col,
.ui-refresh .contact-report table.dataTable td.action-col {
    width: 1% !important;
    min-width: 0 !important;
    white-space: nowrap;
    padding-left: var(--space-2) !important;
    padding-right: var(--space-2) !important;
}
/* Swoogo Admin › Account › Users: that grid's action column is also a LEADING
   column (ActionColumn is $columns[0], which AjaxDataTables::getClientOptions()
   always freezes), and it lives inside an .rd-card. Right-aligning a leading
   column pools every pixel of slack on its left — that is what pushed the two
   icons ~150px in from the card edge. Same shrink-to-fit idiom as
   .contact-report above; `!important` beats ActionColumn::renderColumn()'s
   inline `56 + 22 * n` px, sized for the legacy 22px buttons and a header
   label the refresh no longer draws. */
.ui-refresh .rd-card__body .comm-report table.dataTable th.action-col,
.ui-refresh .rd-card__body .comm-report table.dataTable td.action-col {
    text-align: left !important;
    width: 1% !important;
    min-width: 0 !important;
    white-space: nowrap;
    padding-left: var(--space-2) !important;
    padding-right: var(--space-2) !important;
}
/* Swoogo Admin › Registrants, › Go Attend › White-Label Apps and › Pro Services Themes: same leading,
   FixedColumns-pinned ActionColumn as .contact-report above, so the same
   shrink-to-fit override applies. */
.ui-refresh :is(.registrant-report, .go-attend-whitelabel-report, .pro-theme-report) table.dataTable th.action-col,
.ui-refresh :is(.registrant-report, .go-attend-whitelabel-report, .pro-theme-report) table.dataTable td.action-col {
    text-align: left !important;
    width: 1% !important;
    min-width: 0 !important;
    white-space: nowrap;
    padding-left: var(--space-2) !important;
    padding-right: var(--space-2) !important;
}
/* ...and release the select-checkbox column, which rendered 100px here where
   Contacts renders 44px, reserving ~56px of dead space before the first icon.
   Cause: ajax-datatables.css's `td[class]:not(.select-checkbox):not(.action-col)
   { min-width: 100px }` floor. It excludes the checkbox column by class — but
   ajax-datatables.js strips `select-checkbox` off any cell holding a
   `div.no-select` (`Registrant::afterFind` marks every NON-deleted row that way,
   since only deleted ones can be restored), so on this grid the exclusion stops
   matching and the checkbox column picks up the 100px floor meant for data
   columns. Contacts keeps its class, which is the whole difference.
   Clearing the floor lets DataTables measure the column normally (~44px) in BOTH
   tables. Do NOT instead pin the `<col>` width: DataTables sizes each table's
   total independently, so the head shrinks and the body does not — measured live,
   head 46px vs body 100px, i.e. a visibly desynced header. The floor is the thing
   to remove, not the measurement to override. Generalisable to `.comm-report`
   once the other checkbox grids with no-select rows are checked. */
.ui-refresh .registrant-report table.dataTable.table-condensed > tbody > tr > td:has(> div.no-select) {
    min-width: 0 !important;
}
/* The Actions column is sticky (FixedColumns) in these reports, so it MUST paint
   an OPAQUE background to occlude horizontally-scrolled content — a transparent
   fill let the scrolled columns bleed through the frozen header/cells. Re-point
   the legacy hardcoded `background: white` to the same surface tokens as the rest
   of the table (header band on thead, page tone on rows, hover/selected tints),
   keyed on `.action-col` so it's robust to FixedColumns' class aliasing
   (dtfc-fixed-start/-end vs -left/-right). The page tone is a fallback: a
   container that paints its own surface (an .rd-card, see components.css)
   declares --grid-sticky-bg so the frozen column matches it instead of the
   page. */
.ui-refresh .comm-report table.dataTable > thead > tr > th.action-col {
    background: var(--paper-2) !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr > td.action-col {
    background: var(--grid-sticky-bg, var(--bg-page)) !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr:hover > td.action-col {
    background: var(--paper-2) !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr.selected > td.action-col {
    background: linear-gradient(var(--primary-tint), var(--primary-tint)), var(--grid-sticky-bg, var(--bg-page)) !important;
}
/* The select-checkbox (first) column is frozen too — keep it opaque so scrolled
   content can't show through its header/cells. Explicit (in addition to the
   FixedColumns rules) in case the checkbox column doesn't carry a dtfc class. */
.ui-refresh .comm-report table.dataTable > thead > tr > th:first-child {
    background: var(--paper-2) !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr > td.select-checkbox {
    background: var(--grid-sticky-bg, var(--bg-page)) !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr:hover > td.select-checkbox {
    background: var(--paper-2) !important;
}
.ui-refresh .comm-report table.dataTable > tbody > tr.selected > td.select-checkbox {
    background: linear-gradient(var(--primary-tint), var(--primary-tint)), var(--grid-sticky-bg, var(--bg-page)) !important;
}
.ui-refresh .comm-report table.dataTable td.action-col .btn,
.ui-refresh .comm-report table.dataTable td.action-col .dropdown-toggle {
    /* !important: Bootstrap's base `.btn-default{background-color:#fff}` was
       winning when the row's own hover tint (`--paper-2` on td.action-col,
       see the tr:hover rule above) made a leftover white button background
       stand out against it — the button should stay fully transparent except
       on its own direct hover/focus below. */
    background: transparent !important;
    border-color: transparent !important;
    box-shadow: none !important;
    /* --ink-2 rather than --muted: at --muted the resting glyphs read as washed
       out against the row. */
    color: var(--ink-2);
    font-weight: var(--font-weight-medium);
    border-radius: var(--radius-sm);
    transition: background var(--transition-fast), color var(--transition-fast);
    /* The global `.ui-refresh .btn:not(.breadcrumb-btn)` rule (components.css)
       gives every button the full 36px height / 16px-each-side padding size —
       fine for a labeled button, but these are icon-only actions inside
       `.btn-group-sm`, which Bootstrap normally compacts on its own. Our
       selector out-specifies that global rule, so it silently never shrinks:
       4 icon buttons at ~48px each (padding + glyph) need ~190-200px, which
       is why even a fairly wide action column still wrapped them onto a
       second line. Force a small square icon-button size instead. */
    height: 28px !important;
    width: 28px !important;
    padding: 0 !important;
}
/* Icon-only actions render as a Bootstrap .btn-group, which overlaps adjacent
   buttons by -1px and rounds only the end corners — once the buttons are
   ghosted, the glyphs collide and the middle one stays square. Lay the group out
   with a real gap and give each button the same box as .list-table's .lr-icon so
   both grids read the same. The dropdown toggle is excluded because it carries a
   text label and must keep its intrinsic width. */
.ui-refresh .comm-report table.dataTable td.action-col .btn-group {
    display: inline-flex;
    gap: var(--space-1);
}
.ui-refresh .comm-report table.dataTable td.action-col .btn-group > .btn + .btn {
    margin-left: 0;
}
.ui-refresh .comm-report table.dataTable td.action-col .btn:not(.dropdown-toggle) {
    display: inline-grid;
    place-items: center;
    width: 28px;
    height: 28px;
    padding: 0;
    /* Bootstrap's :first-child/:last-child corner rules outrank the shared
       radius above, so this has to be forced to round every button. */
    border-radius: var(--radius-sm) !important;
}
.ui-refresh .comm-report table.dataTable td.action-col .btn i {
    font-size: 13px;
}

.ui-refresh .comm-report table.dataTable > tbody > tr:hover > td.action-col .btn-group .disabled {
    opacity: .2;
    cursor: not-allowed;
}
/* Inert actions must look inert. ActionColumn marks update/delete/restore/
   approve/deny/confirm with `disabled-link gray-500` when the row can't take
   them, and backend.css's `a.disabled-link` kills pointer events — so the
   button is unclickable either way. But `gray-500` (0,1,0) loses its greying to
   components.css's `.ui-refresh .new-grid-theme .table-condensed > tbody tr td a
   { color: var(--fg-default) !important }` (0,3,3), leaving a dead icon that
   looks identical to a live one. Restore the affordance with the same treatment
   the refresh gives every other disabled icon action (.lr-icon.is-disabled),
   out-specifying both that rule and the .btn:hover/.action-delete:hover fills
   below so it holds at rest and on hover. */
.ui-refresh .comm-report table.dataTable > tbody > tr > td.action-col .btn.disabled-link,
.ui-refresh .comm-report table.dataTable > tbody > tr > td.action-col .btn.disabled-link:hover,
.ui-refresh .comm-report table.dataTable > tbody > tr > td.action-col .btn.disabled-link:focus {
    background: transparent !important;
    color: var(--muted) !important;
    cursor: not-allowed;
}
/* Hover has to read on every row state: default (--bg-page), hovered
   (--paper-2) and selected (--primary-tint) all sit within a few points of each
   other, so the old --paper-2 fill disappeared on a hovered row. --line-2 is
   the one step darker that separates from all three and, unlike
   --primary-tint-2, is opaque on every surface (the accent tint is rgba on
   dark, which read as a translucent smear rather than a raised chip) — so one
   rule now covers light and dark instead of a color swap per surface. */
.ui-refresh .comm-report table.dataTable td.action-col .btn:hover,
.ui-refresh .comm-report table.dataTable td.action-col .btn:focus,
.ui-refresh .comm-report table.dataTable td.action-col .open > .dropdown-toggle {
    background: var(--line-2) !important;
    color: var(--ink) !important;
    border-color: transparent !important;
}
/* Delete is destructive — flag it on hover with the same color-only treatment
   (no background swap) that .lr-icon.danger uses elsewhere in ui-refresh. */
.ui-refresh .comm-report table.dataTable td.action-col .btn.action-delete:hover,
.ui-refresh .comm-report table.dataTable td.action-col .btn.action-delete:focus {
    color: var(--danger-primary) !important;
}
/* Approve/deny read as opposite, consequential actions (email-blast's approval
   queue, request-to-attend's report) but were identical gray ghost icons —
   extend the same delete treatment so hover carries meaning here too. */
.ui-refresh .comm-report table.dataTable td.action-col .btn.action-approve:hover,
.ui-refresh .comm-report table.dataTable td.action-col .btn.action-approve:focus {
    color: var(--success-primary) !important;
}
.ui-refresh .comm-report table.dataTable td.action-col .btn.action-deny:hover,
.ui-refresh .comm-report table.dataTable td.action-col .btn.action-deny:focus {
    color: var(--danger-primary) !important;
}

/* ── Bulk actions bar ── common/_bulk-bar's custom-actions branch (used here
   instead of its single hardcoded .bulk-bar-delete button) doesn't space its
   own icons from their labels — sessions.css's `.bulk-bar-delete i` margin
   only covers that default button. go-attend-activity-feed.css needed the
   same rule for its own custom actions; contacts reuses it verbatim. */
.ui-refresh #contact-bulk-bar .contact-bulk-action i {
    margin-right: 6px;
}

/* ── Log Request / Audit Log grids — head/body column-width desync (Firefox) ──
   DataTables' scrollX feature only keeps the sticky head clone
   (.dt-scroll-head) and the real body (.dt-scroll-body) visually aligned by
   measuring each column once and writing MATCHING explicit pixel widths onto
   both tables' <col> elements. Both tables still use table-layout: auto,
   where an explicit <col> width is only a preference, not a hard rule — a
   browser is allowed to size a column differently if that table's OWN
   content disagrees. Confirmed live: Chromium honors the written widths
   faithfully; Firefox re-derives each table's column widths independently
   from its own content (short header/filter-row content for the head clone,
   real row data for the body), so the two end up different — spread evenly
   across every column, not concentrated in one, and confirmed to go away
   once table-layout: fixed is forced on both tables with a matching width
   (fixed layout removes the auto-layout renegotiation Firefox was doing,
   so both tables just use the shared <col> values as given).
   This pairs with initReportColumnSync() (communicate.js), which runs
   DataTables' own `columns.adjust()` on these two grids on every init/draw so
   the shared <col> values are (re)measured against the current box.
   These two grids USED to also need `width: fit-content` on `.dt-scroll-head`
   and `.dt-scroll-body`, because the shared rule then sized the body box to
   `max-content`, which has no sane "natural size" to resolve against once the
   table is `table-layout: fixed` (fixed layout has no content-driven preferred
   size the way auto layout does) — both browsers fell back to a degenerate,
   effectively-infinite width (~100000px). That override is GONE, and must not
   come back: the shared rule no longer uses `max-content` (see the .dt-scroll
   block far above — the body box is now `width: auto`, a real horizontal
   scrollport), so the degenerate case it worked around can't arise. Keeping it
   actively broke these pages: sizing the head/body boxes to their CONTENT made
   them wider than the layout cell on any narrow viewport, so they spilled out
   past the breadcrumb bar and — being content-sized — had nothing to scroll,
   while #main-content's `overflow-x: hidden` silently clipped the spill so no
   scrollbar appeared anywhere. `columns.adjust()` recomputes the table widths
   against the constrained box on its own, which is all these grids need.
   Scoped to just these two grids rather than the shared `.comm-report` rule,
   since every other `.comm-report` grid hasn't shown this symptom and
   table-layout: fixed changes how ALL of a table's columns size (content that
   needs more room wraps/clips instead of growing the column) — worth
   confirming it's solid here before considering it for the other ~70 grids.
   .cfs-contact-report (Call for Speakers Users report) added as a third
   verified grid — same symptom, same fix. Known remaining gap: the
   "Sign Up Status" multiselect filter column can overlap its neighbor under
   this fix; tracked separately rather than blocking the base alignment fix.

   NOTE: tried on `.registrant-report` (2026-09-21) and reverted. It did pin
   the columns so a sort stopped re-flowing them, but the page reads worse for
   it: fixed layout clips instead of growing, so emails and names ellipsise on
   an admin search screen whose whole job is showing you those values. */
.ui-refresh .log-request-report .dt-scroll-head table.dataTable,
.ui-refresh .log-request-report .dt-scroll-body table.dataTable,
.ui-refresh .audit-log-report .dt-scroll-head table.dataTable,
.ui-refresh .audit-log-report .dt-scroll-body table.dataTable,
.ui-refresh .cfs-contact-report .dt-scroll-head table.dataTable,
.ui-refresh .cfs-contact-report .dt-scroll-body table.dataTable {
    table-layout: fixed !important;
}

/* Fixed layout (above) takes the checkbox column's DataTables `width: 20px`
   literally, so the generic 12px-a-side header/filter cell padding left
   select-all a 0px content box — it overflowed right and the opaque frozen
   Actions column painted over it. Drop the side padding so the 16px box fits
   and centers over the row checkboxes. */
.ui-refresh .cfs-contact-report table.dataTable > thead > tr > th:first-child {
    padding-left: 0 !important;
    padding-right: 0 !important;
}

/* ── Log Request grid — action column ──
   Shrink the column to its icons, same reasoning as .contact-report's own
   `width: 1%` override above (the legacy `width:15%` inline style, sized for
   labeled buttons, is stale for 3 icon-only actions). This grid's tables are
   `table-layout: fixed` (see the head/body desync fix above), where `width:
   1%` — a shrink-to-fit trick that only works under `table-layout: auto` —
   is instead taken literally as 1% of the table's total width, nowhere near
   enough for 3 icon buttons, breaking identically in both browsers instead
   of just being an auto-layout leniency difference. Fixed layout needs a
   real pixel value instead: 3 × 28px buttons + 2 × 4px (--space-1) gaps
   between them = 92px, plus the generic .comm-report td padding
   (var(--space-3) = 12px a side = 24px), rounded up for breathing room. */
.ui-refresh .log-request-report table.dataTable th.action-col,
.ui-refresh .log-request-report table.dataTable td.action-col {
    width: 120px !important;
    min-width: 120px !important;
}
/* Row actions hidden until hover/focus. Every other .comm-report grid keeps
   its ghost action-col buttons always visible (see above); the Log Request
   grid (audit/log-request) asked to hide them until the row is interacted
   with, matching .list-table's .lr-icon convention — so this is scoped to
   .log-request-report instead of changing the shared .comm-report rule. */
.ui-refresh .log-request-report table.dataTable td.action-col .btn {
    opacity: 0;
    transition: opacity var(--transition-fast), background var(--transition-fast), color var(--transition-fast);
}
.ui-refresh .log-request-report table.dataTable > tbody > tr:hover td.action-col .btn,
.ui-refresh .log-request-report table.dataTable > tbody > tr:has(:focus-visible) td.action-col .btn {
    opacity: 1;
}
/* Touch devices have no reliable hover, so keep row actions visible. */
@media (hover: none), (pointer: coarse) {
    .ui-refresh .log-request-report table.dataTable td.action-col .btn {
        opacity: 1;
    }
}

/* ── Row dropdowns that outgrow the grid ──
   Applied by communicate.js's initGridRowDropdowns(), which writes the top/left.
   `position: fixed` escapes `.dt-scroll`'s clip (both axes, see the top of this
   file) without re-parenting the menu to <body> — it has to stay inside the grid
   for the delete/mass-action handlers delegated from the grid container to fire.
   z-index clears the sticky header band (2) and the toolbar (3). */
.ui-refresh .comm-report .dropdown-menu.dropdown-menu--free {
    position: fixed;
    z-index: 1000;
    display: block;
    margin: 0;
}

/* ── Empty message ── */
.ui-refresh .comm-report .no-data-message {
    color: var(--muted);
    font-size: 13px;
}

/* ── Bulk-action button ("With Checked Records") ──
   Rendered below the report grid (ReportHelper::massGridActionButton, outside
   .comm-report). When rows are selected, ajax-datatables.js adds `.active` to
   the .action-buttons wrapper and ajax-datatables.css:176 paints the button
   `--swoogo-pacifica-100` (legacy navy) !important. That file loads AFTER
   communicate.css, so we re-tone to the ui-refresh accent CTA with higher
   specificity + !important and a legible on-accent (white) label. */

/* Idle / disabled — neutral ui-refresh button. */
.ui-refresh .bottom-row .action-buttons .btn.btn-default {
    background: var(--bg-page);
    border-color: var(--input-border);
    color: var(--fg-default);
    box-shadow: none;
}
/* Enabled (rows selected) — accent call-to-action with a white label. Higher
   specificity (0,6,0) + !important beats the legacy `.action-buttons.active
   button` (0,2,1) regardless of stylesheet order. */
.ui-refresh .bottom-row .action-buttons.active .btn.btn-default,
.ui-refresh .bottom-row .action-buttons.active .btn.btn-default:focus,
.ui-refresh .bottom-row .action-buttons.active.open > .btn.btn-default.dropdown-toggle {
    background: var(--accent-primary) !important;
    border-color: var(--accent-primary) !important;
    color: var(--fg-on-accent) !important;
    box-shadow: none !important;
}
.ui-refresh .bottom-row .action-buttons.active .btn.btn-default:hover {
    background: var(--primary-ink) !important;
    border-color: var(--primary-ink) !important;
    color: var(--fg-on-accent) !important;
}

/* =========================================================================
   Blast view (view.php) — detail + stats tables
   ========================================================================= */

/* Key/value details table (Blast Details section) */
.ui-refresh .comm-view-table {
    width: 100%;
    border-collapse: collapse;
    border-spacing: 0;
    font-size: 13px;
}

.ui-refresh .comm-view-table th,
.ui-refresh .comm-view-table td {
    padding: var(--space-3) var(--space-3);
    border: 0;
    border-bottom: 1px solid var(--line);
    vertical-align: middle;
    color: var(--ink);
    background: transparent;
    text-align: left;
}

.ui-refresh .comm-view-table th {
    width: 25%;
    font-weight: var(--font-weight-medium);
    color: var(--muted);
    white-space: nowrap;
}

.ui-refresh .comm-view-table > tbody > tr:last-child th,
.ui-refresh .comm-view-table > tbody > tr:last-child td {
    border-bottom: 0;
}

/* Stats rows (General Statistics + Clicks) — same table base, clickable rows */
.ui-refresh .comm-stat-table .comm-stat-row {
    cursor: pointer;
    transition: background var(--transition-fast);
}

.ui-refresh .comm-stat-table .comm-stat-row:hover th,
.ui-refresh .comm-stat-table .comm-stat-row:hover td {
    background: var(--paper-2);
}

.ui-refresh .comm-stat-row--total th,
.ui-refresh .comm-stat-row--total td {
    font-weight: var(--font-weight-semibold);
    color: var(--ink);
    border-top: 1px solid var(--line);
    border-bottom: 0;
}

.ui-refresh .comm-stat-row__count {
    width: 80px;
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: var(--font-weight-semibold);
    color: var(--ink);
}

.ui-refresh .comm-stat-row__bar {
    width: 240px;
    padding-left: var(--space-4);
}

/* Inline progress bar (replaces Bootstrap Progress widget in ui-refresh) */
.ui-refresh .comm-stat-bar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.ui-refresh .comm-stat-bar__track {
    flex: 1 1 auto;
    height: 6px;
    border-radius: var(--radius-full);
    background: var(--bg-muted);
    overflow: hidden;
}

.ui-refresh .comm-stat-bar__fill {
    height: 100%;
    border-radius: var(--radius-full);
    background: var(--accent-primary);
    transition: width var(--transition-normal);
}

.ui-refresh .comm-stat-bar__pct {
    flex: 0 0 36px;
    text-align: right;
    font-size: var(--font-size-xs);
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* ── Modal iframe: reset legacy white-background override ──
   The iframe layout sets `body.iframe #modal-pjax-form { background: white }`
   so the modal reads as a surface-correct card. Override it with the paper
   token so dark/bright surfaces both look right. */
body.ui-refresh.iframe #modal-pjax-form {
    background: var(--paper) !important;
}

/* ── Modal form body padding (send-test, order) ──
   Modals that load view partials don't have the .list-page/comm-form wrapper,
   so add consistent internal padding here. */
.ui-refresh .comm-modal-body {
    padding: var(--space-5) var(--space-5) var(--space-2);
}

/* ── Order-templates modal table ──
   Same clean look as .comm-report: natural-case muted headers, bottom-rule rows,
   no grid lines, no stripes. The drag handle column from GridView stays intact. */
.ui-refresh .comm-order-table {
    width: 100%;
    border-collapse: collapse;
    border-spacing: 0;
    margin: 0;
}

.ui-refresh .comm-order-table > thead > tr > th {
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-bottom: 1px solid var(--line);
    font-size: 11px;
    font-weight: var(--font-weight-semibold);
    color: var(--muted);
    background: var(--paper-2);
    white-space: nowrap;
}

.ui-refresh .comm-order-table > tbody > tr > td {
    padding: var(--space-3);
    border: 0;
    border-bottom: 1px solid var(--line);
    color: var(--ink);
    font-size: 13px;
    vertical-align: middle;
    background: transparent;
}

.ui-refresh .comm-order-table > tbody > tr:hover > td {
    background: var(--paper-2);
}

/* Drag handle icon */
.ui-refresh .comm-order-table .drag-icon {
    color: var(--muted);
}

/* ── Grouped email panels (automated / planner-alert) ──
   The trigger heading was indented further than the card list below it (event
   pages give .panel-body only --space-1 of horizontal padding). Pull the heading
   in to line up flush with the cards, and give the per-group "send to everyone in
   the group" checkbox room to breathe below the cards. */
.ui-refresh .comm-group-panel > .panel-heading {
    padding-left: var(--space-1);
    padding-right: var(--space-1);
}

/* The line belongs between sections, not under each trigger title. Drop the
   base panel-heading underline (the #content-container .panel-heading rule).
   ID-scoped so it beats that (1,2,0) base rule. */
.ui-refresh #content-container .comm-group-panel > .panel-heading,
.ui-refresh #content-container .comm-flush-panel > .panel-heading {
    border-bottom: 0;
}

/* Divider between consecutive grouped sections (automated / session / planner /
   meeting). Sits above every section that follows another — never above the
   first — so it reads as a separator, not a heading underline. active-form.css
   zeroes panel borders with `border: none !important` on
   #content-container .panel:not(...), so the divider needs !important too. */
.ui-refresh #content-container .comm-group-panel + .panel.panel-default {
    border-top: 1px solid var(--panel-border) !important;
    padding-top: var(--space-6);
}

/* Drop the base .panel 20px margin-bottom so the divider is centred between
   sections: the section's own body padding (--space-6) above the line matches
   the next section's padding-top below it, instead of 24px + 20px stacking into
   an oversized gap under each trigger. */
.ui-refresh #content-container .comm-group-panel {
    margin-bottom: 0;
}

.ui-refresh .comm-group-panel > .panel-body > .checkbox {
    margin-top: var(--space-5);
}

/* ── Quick-add invitee sidebar ── */

/* Button row in the "existing contact" conflict panel */
.ui-refresh .comm-conflict-actions {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-4);
}

/* Body region between the panel header and the sticky footer in the sidebar */
.ui-refresh .comm-sidebar-body {
    padding: var(--space-4) var(--space-5);
}

/* Bare comm-form sidebar drawers (e.g. the Schedule Blast form) render their
   sections straight into #sidebar-pjax-form with no padded body wrapper, so
   the content sat flush against the surface edge — a visible seam on dark
   between the form's surface and the drawer's page background. Pad the form
   and let the footer bleed back to full width so its top border still spans
   the drawer. Invitee/contact sidebars don't carry `.comm-form`, so they keep
   their existing comm-sidebar-body + sidebar-footer structure untouched.
   #email-blast-form is excluded for the same reason: it wraps its sections in
   a .sidebar-body, so this padding would double up. */
.ui-refresh #sidebar-pjax-form > .sidebar-form.comm-form:not(#email-blast-form) {
    padding: var(--space-4) var(--space-6) 0;
    /* layout.css's base `.sidebar-form { overflow: hidden }` assumes the single-
       panel drawer case below scrolls (or fits) some other way. A comm-form
       drawer can stack several panels (e.g. the Log Request form's Request
       Dates / Export Type / Conditions) whose combined height can exceed the
       drawer on a short viewport — without a scroll container of its own here,
       that overflow was just clipped with no way to reach it. */
    overflow-y: auto;
}
.ui-refresh #sidebar-pjax-form > .sidebar-form.comm-form:not(#email-blast-form) > .sidebar-footer {
    margin: 0 calc(var(--space-5) * -1);
}
/* layout.css's `.sidebar-form > .panel { flex: 1; min-height: 0 }` is tuned
   for a drawer with exactly ONE panel, grown to push the footer to the
   bottom. A comm-form drawer with several stacked panels (see above) isn't
   that case — `flex: 1` split the leftover height evenly across all of them
   and `min-height: 0` let each shrink below its own content's natural size,
   so on a short viewport every panel got squeezed and its fields visibly
   overlapped. Panels here should just stack at their natural height and let
   the scroll container above handle any overflow. */
.ui-refresh #sidebar-pjax-form > .sidebar-form.comm-form:not(#email-blast-form) > .panel {
    flex: none;
    min-height: auto;
}

/* ── Responsive ── */
@media (max-width: 768px) {
    .ui-refresh .comm-card__main {
        flex-wrap: wrap;
    }

    .ui-refresh .comm-card__metrics {
        gap: var(--space-4);
    }
}

/* Switch-card text (title + description) used as the label of a .toggle-group
   toggle — e.g. "Make this event invitation only" on the Invitation Lists page.
   The shared .toggle-group label flex (components.css) lays this text block on
   the left and pushes the toggle pill to the right; here we stack a bold title
   over a muted description. */
.ui-refresh .comm-switch-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.ui-refresh .comm-switch-title {
    font-size: var(--font-size-btn);
    font-weight: var(--font-weight-semibold);
    color: var(--fg-default);
}
.ui-refresh .comm-switch-desc {
    font-size: var(--font-size-label);
    font-weight: var(--font-weight-normal);
    color: var(--fg-muted);
}

/* ── Report grid filter-row column sizing (#3128) ── report columns rendered
   far wider than their content because the per-column filter row lives in the
   <thead>, and DataTables clones the whole thead into a hidden measurement
   table when it sizes columns (`_fnCalculateColumnWidths`, via
   `f.append($(settings.nTHead).clone())`) — whatever the filter controls
   measure becomes a hard floor baked onto each `<col>`, and nothing
   re-measures afterwards. Verified first on Registration Report (2278px of
   columns in a 1534px viewport, one column whose longest value is "$0.00"
   claiming 208px; 1695px after these rules), then generalized here to every
   `.comm-report` grid — the same DataTables measurement quirk applies
   universally, confirmed on Contacts too (which never renders the operator
   select at all: `contact/_grid.php` never sets
   `enableFilterCustomOperators`, so rules #1/#4 below are structurally inert
   there, and `.dt-column-title`/`.value-input` still apply as expected).
   `.log-request-report`/`.audit-log-report` are excluded from the filter-row
   rules (not from the action-column rule below): both run
   `table-layout: fixed` for an unrelated, already-shipped Firefox head/body
   desync fix, and these rules' `flex`/percentage sizing is only "safe"
   (doesn't contribute to the column's measured width) under `table-layout:
   auto` — under `fixed`, the same values would resolve against whatever
   fixed column width DataTables already baked in, instead of participating
   in the auto-layout measurement this fix depends on. `.activity-feed-report`
   needs no exclusion: it hides its entire `tr.filters` row
   (go-attend-activity-feed.css) in favor of a filter popover, so every
   selector below is already a structural no-op there. */

/* The operator select (AjaxDataTables::renderOperatorInput, rendered only when
   the account's "Max Nested Filter Levels" is > 0) is a bootstrap-multiselect
   whose button label is the bare operator SYMBOL — "=", "C", "DNC", at most
   three characters. It inherits the generic filter-control rule above, which is
   sized for the column-VALUE multiselects ("None selected"), so its 110px floor
   turned a three-character button into a 110px one. */
.ui-refresh .comm-report:not(.log-request-report):not(.audit-log-report) table.dataTable > thead > tr.filters .operator-select-group .input-group-btn .multiselect.dropdown-toggle {
    width: auto;
    min-width: 0;
}

/* A PX width, deliberately not `100%` — a percentage can't resolve during the
   intrinsic measurement described above, so the browser substitutes the
   input's own default width (`size=20`, far more than needed) as a floor on
   every filterable column; `flex: 1 1 auto` still grows the box to fill wider
   columns. Keyed on the filter row rather than `.filter-input-wrap`, since
   that wrapper is added by communicate.js on `init.dt` — after the first draw
   has already measured the columns, so a wrapper-scoped rule doesn't exist at
   the moment the widths are decided (confirmed: it had no effect on a fresh
   load). `.range-slider-input` is excluded for the same reason communicate.js
   skips it when wrapping — it's a bootstrap-slider track (the `range` filter),
   not a text box, and a 60px flex basis would collapse it. */
.ui-refresh .comm-report:not(.log-request-report):not(.audit-log-report) table.dataTable > thead > tr.filters .value-input:not(.range-slider-input) {
    flex: 1 1 auto;
    width: 60px;
    min-width: 0;
}
.ui-refresh .comm-report:not(.log-request-report):not(.audit-log-report) table.dataTable > thead > tr.filters .filter-input-wrap {
    min-width: 0;
}

/* With the intrinsic floor gone, the control chain has to be told to STRETCH or
   it renders at that 60px and leaves the rest of the cell empty. Percentages
   are safe here: they fill the column without contributing to the intrinsic
   measurement that sizes it. */
.ui-refresh .comm-report:not(.log-request-report):not(.audit-log-report) table.dataTable > thead > tr.filters .dt-column-title {
    flex: 1 1 auto;
    min-width: 0;
}
.ui-refresh .comm-report:not(.log-request-report):not(.audit-log-report) table.dataTable > thead > tr.filters .operator-select-group {
    width: 100%;
}
/* The operator cell must hug its button. Bootstrap's own `.input-group-btn
   { width: 1% }` shrink-to-fit is overridden by its `.form-inline .input-group
   .input-group-btn { width: auto }` at higher specificity, and DataTables'
   wrapper carries `form-inline`. An `auto` table-cell beside a `100%` sibling
   takes a share of the surplus instead of hugging its content, which left a
   33px gap between the operator dropdown and the input (measured: an 81px cell
   around a 48px button). Restore `1%`. */
.ui-refresh .comm-report:not(.log-request-report):not(.audit-log-report) table.dataTable > thead > tr.filters .operator-select-group > .input-group-btn {
    width: 1%;
    white-space: nowrap;
}

/* A LEADING action column must not be right-aligned: that pools the cell's
   surplus on its left, reading as a gap between the select-all checkbox and
   the first icon. `.contact-report` already had its own equivalent rule for
   this; generalized here (no `table-layout: fixed` risk — text-align doesn't
   depend on layout mode) so every `.comm-report` grid with a leading, frozen
   action column gets it, without needing a per-grid opt-in. Confirmed inert,
   not just harmless, on the two excluded-above grids: log-request's
   ActionColumn is a trailing column (never `$columns[0]`, so
   AjaxDataTables::getClientOptions() never freezes it — no `.dtfc-fixed-start`
   to match), and audit-log has no action column at all. Keyed on
   `.dtfc-fixed-start` (FixedColumns' pinned-column class) rather than
   `:last-child`, since FixedColumns clones pinned cells into their own table
   where the action cell genuinely is last. This only hides the surplus, not
   removes it — the column still measures 148px for a 3-icon group needing
   116px, because ActionColumn::renderColumn()'s inline `56 + 22 * n` px feeds
   DataTables' `<col>` measurement, which outranks any width set on the cells
   (confirmed live: disabling the `<col>`'s min-width collapsed it to 116px,
   while `width: 1% !important` on the cells did nothing) — fixing that is
   tracked separately. */
.ui-refresh .comm-report table.dataTable th.action-col.dtfc-fixed-start,
.ui-refresh .comm-report table.dataTable td.action-col.dtfc-fixed-start {
    text-align: left !important;
}
