/* Dense data table — shared component.
   Extracted from components.css (CSS split, CLAUDE.md §12).
   Despite the .admin-table name, this is the app's generic dense-table
   component (viz-system skill §2) — used well beyond /admin/* views:
   shift-distribution.js, roster-editor/modals.js, planning-calendar.js,
   staff-fte.js (FTE & capacity), and every admin-*.js view.
   .coverage-gaps-table (admin-locations.js) reuses the same rules.
   Dark-theme overrides are colocated inline (none needed separately —
   see the file's own comments for why). */
@layer components {

/* ── Info table ────────────────────────────────────────────────────────────
   Shared because the compensation history (features/leave/balances.js)
   renders on Settings, My Leave and Leave Requests' person panel; Settings'
   Profile, Sessions and Passkeys use it too. Two distinct shapes
   share the class: a plain <tr><th>Label</th><td>Value</td></tr> definition
   list (no <thead>) for Profile, and a real <thead> column-header table for
   compensation history — the second rule below only fires for the latter. */
.info-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}
.info-table th, .info-table td {
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: top;
}
.info-table tr:last-child th, .info-table tr:last-child td { border-bottom: none; }
.info-table th {
  width: 38%;
  font-weight: 500;
  color: var(--muted);
  white-space: normal;
  overflow-wrap: anywhere;
}
.info-table thead th {
  width: auto;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--text-eyebrow);
  white-space: normal;
  background: var(--surface-sunk);
}

/* ── Admin table ───────────────────────────────────────────────────────────── */
/* .coverage-gaps-table shares this styling but is a deliberately distinct class
   (not .admin-table) — admin-locations.js already renders three .admin-table
   instances on one page (Locations / Shift Types / Staffing Rules), and E2E
   specs scope selectors like `.admin-table tr, { hasText: 'SomeLocationName' }`
   assuming exactly one relevant match; a fourth .admin-table for coverage gaps
   (which lists location names too) broke that assumption. */
.admin-table, .coverage-gaps-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.admin-table th, .coverage-gaps-table th {
  text-align: left;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-sunk);
  border-bottom: 1px solid var(--border);
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.7rem;
}

.admin-table td, .coverage-gaps-table td {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

.admin-table tr:last-child td, .coverage-gaps-table tr:last-child td { border-bottom: none; }
.admin-table tr.row-inactive td { opacity: var(--opacity-disabled); }

/* Alternating-row shading — a shared convention across every admin table,
   per .claude/skills/viz-system §2. Applied at the <tr> level (not per-td)
   so it composes correctly with cells that already set their own
   background (heat-shaded .num cells, the sticky-first-col cell) — those
   simply paint over it for their own box, no specificity fight needed.
   var(--bg), not var(--surface-sunk)/--surface-sunk (already the header's
   own tone, §2's "num"/sticky rules use --surface/--surface-sunk
   too) — a row that read as loud as the header would defeat the point;
   var(--bg) is the page's own background, one step quieter than the
   table's --surface, giving the gentlest possible alternation.
   :not(:has(td[colspan])) excludes the single-row empty/error state.
   :not(.row-selected) is required, not cosmetic — a selected row landing on
   an even position would otherwise lose its highlight to this rule, since
   :not(:has(...)) makes this selector MORE specific than .row-selected's
   own (confirmed live: a selected row pinned to an even slot rendered with
   the zebra tint instead of the selection tint until this was added). */
.admin-table tbody tr:nth-child(even):not(:has(td[colspan])):not(.row-selected),
.coverage-gaps-table tbody tr:nth-child(even):not(:has(td[colspan])):not(.row-selected) {
  background: var(--bg);
}

/* Row-selected highlight (Shift Distribution's "select users to compare",
   shared shape for reuse elsewhere). The zebra rule above excludes
   .row-selected explicitly (see its own comment) rather than relying on
   specificity/source order between the two rules. */
.admin-table tr.row-selected {
  background: color-mix(in srgb, var(--primary) 10%, transparent);
}

/* The row whose details the side panel shows (components/side-panel.js:
   Shift Distribution, the working-hours report, wish-based absences): a
   primary bar on its first cell, so it combines with .row-selected. */
.admin-table tr[aria-current="true"] > td:first-child { box-shadow: inset 3px 0 0 var(--primary); }

/* A quantity meant to be compared down its column (not just read across the
   row) — right-aligned with tabular-nums so digits actually line up on
   their ones/decimal place, per .claude/skills/viz-system §2. Opt in per
   cell with class="num"; .text-right (base.css) stays for the one existing
   non-numeric use (an action-button cell) that doesn't want tabular-nums. */
.admin-table td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Shared "table scrolls independently of the page" wrapper. Several views
   used to reach for an inline style="padding:0;overflow-x:auto" for this —
   the app's own CSP (default-src 'self', no style-src 'unsafe-inline')
   silently drops every inline style attribute, so none of those ever
   actually applied: the table's overflow fell through to whichever real,
   stylesheet-based scrolling ancestor it found (often <main>, dragging the
   whole page sideways instead of just the table). This must be a real
   class, not inline. */
.table-scroll-card {
  padding: 0;
  overflow-x: auto;
}

/* The People page's CSV-import results dialog (features/users/csv.js) — a potentially long table of
   imported rows, vertically capped so the modal itself doesn't grow past
   the viewport. */
.import-results-scroll { max-height: 360px; overflow-y: auto; overflow-x: auto; }

/* Sticky first column — for a table wide enough that its later columns
   scroll past the viewport edge (Shift Distribution's Counts and Matrix
   tables keep the person in view). Same left:0 sticky treatment already used for the
   roster grid's date column (.rgv2-date-th/.rgv2-date-td below); opt in
   per table with this modifier rather than applying it to every
   .admin-table, since most are narrow enough to never need it. */
.admin-table--sticky-first-col th:first-child,
.admin-table--sticky-first-col td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
}
.admin-table--sticky-first-col th:first-child {
  background: var(--surface-sunk);
  z-index: 2;
}

/* The sticky column's own opaque background above (needed so scrolled-away
   columns don't show through it) fully hides the zebra/row-selected tint
   every other cell in the row shows through automatically — this cell needs
   its own copy of both, mixed into an opaque base rather than composited
   over transparent, since it can't rely on the card surface showing through
   underneath. */
.admin-table--sticky-first-col tbody tr:nth-child(even):not(:has(td[colspan])):not(.row-selected) td:first-child {
  background: var(--bg);
}
.admin-table--sticky-first-col tr.row-selected td:first-child {
  background: color-mix(in srgb, var(--primary) 10%, var(--surface));
}

/* Sticky header row, per .claude/skills/viz-system §2. Opt in per table;
   combines cleanly with .admin-table--sticky-first-col (the top-left header
   cell already gets z-index: 2 from that modifier above, matching this
   rule's z-index so the corner cell stays above both the sticky column and
   the sticky header).

   `position: sticky; top: 0` alone does NOT work inside .table-scroll-card:
   setting only overflow-x forces the computed overflow-y to `auto` too (per
   the CSS Overflow spec's visible→auto coupling when only one axis is
   non-visible) — so .table-scroll-card silently becomes th's "nearest
   scrolling ancestor" instead of the page, but never actually scrolls
   internally (nothing caps its height), so the header never catches onto
   anything and just scrolls away with the page. This is the right fix
   specifically when a bounded, internally-scrolling "frozen pane" is
   actually what's wanted (CSV import results, long lists inside a
   modal-shaped card) — .roster-grid-v2 (below) wants the opposite: the full
   page keeps scrolling, with only its header row pinned, so this modifier
   doesn't apply there. See that rule's own comment for why a `clip`/`hidden`
   variant of this same trick doesn't work either for that shape, and what
   actually does. */
.table-scroll-card:has(.admin-table--sticky-header) {
  max-height: 70vh;
  overflow-y: auto;
}
.admin-table--sticky-header th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface-sunk);
}

/* Generic responsive table→card transform — generalized from
   admin-security.js's original .security-compliance-table treatment (still
   the reference for this pattern) so every other plain admin CRUD table can
   reuse the same mechanism instead of staying horizontal-scroll-only or
   reinventing its own bespoke stacking rules (see admin-audit.js's
   .audit-table-card for the richer, hand-matched-per-column alternative,
   deliberately left as its own separate design rather than folded in here).
   Opt in per table by adding this modifier class *and* a data-label
   attribute (mirroring that column's header text) on every <td> — a table
   still on the plain horizontal-scroll story (shift-distribution.js's
   dynamic per-shift-type columns, given its own dedicated mobile card
   design instead, CLAUDE.md §16) simply never adds either, rather than
   silently rendering with blank labels. */
@media (max-width: 700px) {
  .admin-table--responsive thead { display: none; }
  .admin-table--responsive,
  .admin-table--responsive tbody,
  .admin-table--responsive tr,
  .admin-table--responsive td { display: block; }
  .admin-table--responsive tr { padding: var(--space-3); border-bottom: 1px solid var(--border); }
  .admin-table--responsive td { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--space-2); padding: var(--space-1) 0; border: 0; }
  .admin-table--responsive td::before { content: attr(data-label); color: var(--muted); font-size: var(--text-sm); }
}

/* ── Sortable column headers ─────────────────────────────────────────────────
   Real, working client-side column sort (data-sort attribute, ⇅/↑/↓
   indicator). Used by shift-distribution.js and
   admin-users.js (People); components/table-sort.js turns the heading into
   a button. */
.sortable-th {
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
.sortable-th:hover { background: rgba(0,0,0,0.04); }
.sort-indicator { color: var(--muted); margin-left: 4px; font-size: 0.7em; }
.sort-indicator.active { color: var(--text); }

/* ── Dark theme ─────────────────────────────────────────────────────────── */
[data-theme="dark"] .sortable-th:hover { background: rgba(255,255,255,0.05); }
[data-theme="dark"] .sort-indicator.active { color: var(--text); }

} /* @layer components */

@layer components {
.admin-actions-cell {
  white-space: nowrap;
}

.admin-order-cell {
  white-space: nowrap;
  display: flex;
  gap: var(--space-1);
}

.admin-order-cell .btn:disabled {
  opacity: 0.3;
  cursor: default;
}

.admin-actions {
  display: flex;
  gap: var(--space-2);
  justify-content: flex-start;
  align-items: center;
}

/* Shared portal surfaces anchored to a trigger (components/action-menu.js):
   the row action menu and the content popover. Fixed positioning lets them
   escape clipped tables while the JS module owns viewport placement
   (--action-menu-left/--action-menu-top) and focus. */
.action-menu-popover,
.action-popover {
  position: fixed;
  top: var(--action-menu-top, 0);
  left: var(--action-menu-left, 0);
  z-index: 450;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-overlay);
}
.action-menu-popover {
  min-width: 190px;
  max-width: min(280px, calc(100vw - 16px));
  padding: var(--space-1);
}
/* A short explanation or a small live list next to its control. */
.action-popover {
  width: min(340px, calc(100vw - 16px));
  max-height: min(420px, calc(100vh - 16px));
  overflow-y: auto;
  padding: var(--space-3);
  font-size: var(--text-sm);
}
.action-popover:focus { outline: none; }
.action-popover:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.action-popover-title { margin: 0 0 var(--space-2); font-size: var(--text-sm); font-weight: 600; }
@media (max-width: 600px) {
  /* As wide as the viewport allows, like the menu's own cap. */
  .action-popover { width: calc(100vw - 16px); }
}

.action-menu-item {
  display: flex;
  width: 100%;
  min-height: 34px;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: var(--text-sm);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.action-menu-item svg { flex: 0 0 auto; color: var(--muted); }
.action-menu-item:hover,
.action-menu-item:focus-visible {
  outline: none;
  background: var(--surface-sunk);
}
.action-menu-item-danger { color: var(--danger); }
.action-menu-item-danger svg { color: currentColor; }
.action-menu-item-separated {
  margin-top: var(--space-1);
  padding-top: calc(var(--space-2) + 1px);
  border-top: 1px solid var(--border);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}
.action-menu-item:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.admin-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: var(--space-6) 0 var(--space-3);
}

.admin-section-header h2 {
  font-size: var(--text-lg);
  font-weight: 600;
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

}

@layer components {
@media (max-width: 700px) {
  .admin-section-header { align-items: stretch; flex-direction: column; gap: var(--space-2); }
  .admin-section-header > .d-flex { align-items: stretch; flex-direction: column; width: 100%; }
  .admin-section-header > .btn,
  .admin-section-header > .d-flex > .btn { width: 100%; }
  .admin-section-header #rules-filter-bar { width: 100%; }
  .admin-section-header #rules-filter-bar .filter-bar { margin-bottom: 0; }
  .admin-section-header #rules-filter-bar .filter-search-wrap { width: 100%; }
  .admin-actions { flex-wrap: wrap; white-space: normal; }
  .admin-actions-cell { min-width: 0; white-space: normal; }
}
}
