/* Shared list-screen chrome - see TagHelpers/AppGridTagHelper.cs and
   wwwroot/js/app-grid.js.

   BOOTSTRAP VARIABLES ONLY, NO COLOUR LITERALS. The theme flips at runtime via
   data-bs-theme on <html> and there is no reload to re-evaluate a hard-coded
   hex, so a literal here is a colour that survives into the wrong theme. Every
   rule below resolves through var(--bs-*), which Bootstrap redefines under the
   dark selector for us. The one place site.css does use literals for a panel
   (.filter-bar) it pays for it with a whole parallel [data-bs-theme="dark"]
   block; this file deliberately does not take that on. */

.app-grid {
  /* Establishes the containing block for the busy overlay, and the stacking
     context that keeps a sticky header below .act-as-banner's z-index: 1080. */
  position: relative;
}

/* ── Toolbar ─────────────────────────────────────────────────────────────
   Count on the left, chips beside it, rows-per-page pushed right. Wraps to
   two lines on a phone rather than shrinking the controls. */
.app-grid-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  margin-bottom: 0.5rem;
  min-height: 2rem;
}

/* The count is a <p> for the sake of role="status", so it needs its margin
   removed rather than inherited from prose. */
.app-grid-summary {
  margin: 0;
  color: var(--bs-secondary-color);
  font-size: 0.875rem;
  /* Reserves the line so the toolbar does not change height between "No
     events" and "Showing 1-50 of 2,282 events". A toolbar that jumps by a
     line every time you filter is the same layout shift the busy overlay's
     min-height exists to prevent, just smaller. */
  min-height: 1.5rem;
}

.app-grid-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.app-grid-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.125rem 0.25rem 0.125rem 0.5rem;
  font-size: 0.8125rem;
  line-height: 1.4;
  border: var(--bs-border-width) solid var(--bs-border-color);
  border-radius: 10rem;
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-body-color);
}

.app-grid-chip-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.125rem;
  height: 1.125rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--bs-secondary-color);
  line-height: 1;
}

.app-grid-chip-remove:hover,
.app-grid-chip-remove:focus-visible {
  background-color: var(--bs-secondary-bg);
  color: var(--bs-body-color);
}

.app-grid-tools {
  margin-left: auto;
}

.app-grid-page-size-form {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}

.app-grid-page-size-label {
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
  margin: 0;
}

.app-grid-page-size {
  width: auto;
}

/* ── Viewport, busy state ───────────────────────────────────────────────── */

.app-grid-viewport {
  position: relative;
}

/* Dim rather than blank. The rows on screen were true a moment ago and are
   almost always the rows about to come back; replacing them with a spinner
   loses the reader's place for the sake of admitting we are fetching. Held
   above 0.55 because the point is still to be readable - this is "settling",
   not "gone". */
.app-grid-loading .app-grid-scroll {
  opacity: 0.55;
  transition: opacity 120ms ease-out;
  pointer-events: none;
}

.app-grid-busy {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 4rem;
  /* Above the dimmed table, far below .act-as-banner (1080) and Bootstrap's
     modal backdrop (1050). */
  z-index: 2;
  pointer-events: none;
}

.app-grid-busy[hidden] {
  display: none;
}

.app-grid-busy .spinner-border {
  color: var(--bs-secondary-color);
}

/* ── Sticky header ──────────────────────────────────────────────────────── */

/* Opt-in per grid: below about a screenful of rows a pinned header is just a
   row that will not scroll. Needs a background of its own - a transparent
   sticky cell paints the rows sliding underneath it. */
.app-grid-sticky .app-grid-scroll {
  max-height: 70vh;
  overflow-y: auto;
}

.app-grid-sticky thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background-color: var(--bs-body-bg);
  /* box-shadow rather than border-bottom: a sticky cell's own border scrolls
     away with the border-collapse model, which leaves the header floating
     over the rows with nothing separating them. */
  box-shadow: inset 0 -1px 0 var(--bs-border-color);
}

/* ── Sortable headers ───────────────────────────────────────────────────── */

.app-grid-sort-link {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}

.app-grid-sort-link:hover {
  color: var(--bs-link-hover-color);
}

/* The unsorted arrows sit at low opacity so a row of eight headers does not
   read as eight competing controls, then come up on hover and stay up on the
   column actually in force. */
.app-grid-sort-icon {
  font-size: 0.75em;
  opacity: 0.35;
}

.app-grid-sort-link:hover .app-grid-sort-icon {
  opacity: 0.75;
}

.app-grid-sorted .app-grid-sort-icon {
  opacity: 1;
  color: var(--bs-link-color);
}

/* ── Empty state ────────────────────────────────────────────────────────── */

/* A panel, not a <td colspan> row. An empty table with headers still reads as
   a table that is loading; a centred panel with an icon and a sentence reads
   as an answer. It replaces the table rather than sitting under it - the
   tag helper hides one when it shows the other. */
.app-grid-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 2.5rem 1rem;
  text-align: center;
  border: var(--bs-border-width) dashed var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
}

.app-grid-empty[hidden] {
  display: none;
}

.app-grid-empty-icon {
  font-size: 1.75rem;
  opacity: 0.6;
}

.app-grid-empty-message {
  margin: 0;
  max-width: 45ch;
}

/* When the empty state is showing there is nothing to page through, so the
   table and the pager both go. Done in CSS rather than JS so the server-
   rendered page is already correct before any script runs. */
.app-grid:has(.app-grid-empty:not([hidden])) .app-grid-scroll,
.app-grid:has(.app-grid-empty:not([hidden])) .pagination {
  display: none;
}

/* ── Row highlight after a save ─────────────────────────────────────────── */

/* The row you just edited, called out for a moment after the grid repaints.
   Without it a modal save closes onto a table that looks identical, and the
   only way to confirm the change landed is to find the row and read it.

   Fades rather than blinks, and only once: a persistent highlight becomes a
   second selection state nobody asked for. */
.app-grid-row-flash > td,
.app-grid-row-flash > th {
  animation: app-grid-row-flash 2.4s ease-out;
}

@keyframes app-grid-row-flash {
  0%, 25% { background-color: var(--bs-warning-bg-subtle); }
  100% { background-color: transparent; }
}

/* Respect the OS setting. Someone who has asked for less motion has usually
   asked because motion is a problem, not a preference - so the highlight
   becomes a static tint that simply disappears when the class is removed. */
@media (prefers-reduced-motion: reduce) {
  .app-grid-row-flash > td,
  .app-grid-row-flash > th {
    animation: none;
    background-color: var(--bs-warning-bg-subtle);
  }

  .app-grid-loading .app-grid-scroll {
    transition: none;
  }
}

/* ── Modal ──────────────────────────────────────────────────────────────── */

.app-modal-loading {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 8rem;
  color: var(--bs-secondary-color);
}

/* While a save is in flight the form stays visible and readable but stops
   accepting input - the same reasoning as the grid's dim. The buttons are
   disabled in JS as well; this covers the rest of the form. */
.app-modal-saving .modal-body {
  opacity: 0.6;
  pointer-events: none;
}

/* ── Toolbar slot ───────────────────────────────────────────────────────── */

/* The screen's own controls, kept together and separated from the component's
   by a hairline rather than by spacing alone - "New award" and "Rows 25" are
   different kinds of control and reading as one group makes both harder to
   find. */
.app-grid-toolbar-slot {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  padding-right: 0.625rem;
  margin-right: 0.25rem;
  border-right: var(--bs-border-width) solid var(--bs-border-color);
}

.app-grid-toolbar-slot:last-child {
  border-right: 0;
  padding-right: 0;
  margin-right: 0;
}

.app-grid-tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
}

/* ── Selection ──────────────────────────────────────────────────────────── */

.app-grid-select-cell {
  width: 1%;
  padding-right: 0;
}

/* The bar that appears once rows are ticked. Accent-tinted rather than plain,
   because it is a mode: things that happen while it is open apply to a subset
   of the table rather than to a row you clicked. */
.app-grid-bulk {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
  padding: 0.5rem 0.75rem;
  border: var(--bs-border-width) solid var(--bs-primary-border-subtle);
  border-radius: var(--bs-border-radius);
  background-color: var(--bs-primary-bg-subtle);
  color: var(--bs-emphasis-color);
}

.app-grid-bulk[hidden] {
  display: none;
}

.app-grid-bulk-count {
  font-size: 0.875rem;
  font-weight: 600;
}

.app-grid-bulk-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
  margin-left: auto;
}

/* ── Totals ─────────────────────────────────────────────────────────────── */

/* A total is a different kind of number from the ones above it, so it gets a
   rule and a weight rather than just sitting at the bottom of the column. */
.app-grid-totals td {
  border-top: 2px solid var(--bs-border-color);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Pinned to the bottom of a scrolling grid for the same reason the header is
   pinned to the top: a total you have to scroll to is a total you do not read. */
.app-grid-sticky .app-grid-totals td {
  position: sticky;
  bottom: 0;
  z-index: 1;
  background-color: var(--bs-body-bg);
  box-shadow: inset 0 2px 0 var(--bs-border-color);
  border-top: 0;
}

/* Digits that line up, wherever a column is right-aligned or summed. */
.app-grid td.text-end,
.app-grid th.text-end {
  font-variant-numeric: tabular-nums;
}

/* ── Small screens ──────────────────────────────────────────────────────── */

@media (max-width: 575.98px) {
  /* The rows-per-page control stops being pushed to the right once the
     toolbar has wrapped, otherwise it lands alone on a line of its own. */
  .app-grid-tools {
    margin-left: 0;
  }

  .app-grid-toolbar-slot {
    border-right: 0;
    padding-right: 0;
    margin-right: 0;
  }

  .app-grid-bulk-actions {
    margin-left: 0;
  }

  .app-grid-empty {
    padding: 1.5rem 0.75rem;
  }
}

/* ── Card layout on a phone ─────────────────────────────────────────────────
   A seven-column table on a 375px screen is a table you scroll sideways, and
   the heading you need has always just gone off the left edge. Below the md
   breakpoint each row becomes a card and every cell carries its own heading,
   read from the data-label the component puts there - which it can only do
   because it renders the cells.

   Opt out per grid with responsive="table" when the columns really are meant
   to be compared across rows: a matrix, a report somebody reads in columns. */
@media (max-width: 767.98px) {
  .app-grid-cards thead,
  .app-grid-cards .app-grid-totals {
    /* clip, not display:none - a display:none header is removed from the
       accessibility tree, and the cells below still reference it. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .app-grid-cards,
  .app-grid-cards tbody,
  .app-grid-cards tr,
  .app-grid-cards td {
    display: block;
    width: auto;
  }

  .app-grid-cards tr {
    padding: 0.75rem 0.875rem;
    margin-bottom: 0.625rem;
    border: var(--bs-border-width) solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background-color: var(--bs-body-bg);
  }

  .app-grid-cards td {
    display: flex;
    gap: 0.75rem;
    align-items: baseline;
    justify-content: space-between;
    padding: 0.1875rem 0;
    border: 0;
    /* Alignment is a column idea. Down a card everything reads left. */
    text-align: left !important;
  }

  .app-grid-cards td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 40%;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--bs-secondary-color);
  }

  /* A cell with no heading - the actions column - spans the card instead of
     being squeezed into the value half, and gets a rule above it so the
     buttons read as the end of the card rather than another field. */
  .app-grid-cards td:not([data-label]) {
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-top: 0.5rem;
    padding-top: 0.625rem;
    border-top: var(--bs-border-width) solid var(--bs-border-color);
  }

  /* An empty cell would otherwise render as a lone heading with nothing after
     it, which reads as missing data rather than as "not applicable here". */
  .app-grid-cards td:empty {
    display: none;
  }

  .app-grid-cards .app-grid-select-cell {
    justify-content: flex-start;
    padding-bottom: 0.375rem;
  }

  /* The pinned header has nothing to pin to once rows are cards, and a
     max-height would trap the cards in a short scroller inside the page. */
  .app-grid-sticky .app-grid-scroll {
    max-height: none;
    overflow-y: visible;
  }
}

/* ── Header slot ────────────────────────────────────────────────────────────
   Anything a view writes between the <app-grid> tags that is not a column: a
   band of summary tiles, a filter-aware download link. It sits INSIDE the
   swapped region on purpose, so those totals are re-rendered with the rows
   rather than going stale the moment somebody filters. */
.app-grid-header {
  margin-bottom: 0.75rem;
}

.app-grid-header:empty {
  display: none;
}

/* ── The house table look ───────────────────────────────────────────────────
   One appearance for every list in the app. Before this, each grid wore
   whatever classes its hand-written table had - ten combinations across
   thirty-nine grids - so moving between two admin screens meant re-reading
   what a table looked like.

   Everything here is scoped to .app-grid, so nothing leaks onto the tables
   that are deliberately NOT grids (a confirmation summary, a facts rail). */

/* The table fills its scroll box; the gap below is the pager's own mt-3.
   Bootstrap's .table carries margin-bottom: 1rem, which stacked with that and
   was the reason several screens had reached for mb-0 and mb-4. */
.app-grid table {
  margin-bottom: 0;
}

/* Header: separated by weight and colour rather than by a rule box. Small,
   semibold and secondary-toned so it reads as a label for the column instead
   of competing with the first row of data.

   NOT uppercase. It is the usual move for a data-grid header and it is wrong
   here - these headings are multi-word ("Registration Start", "Advances?"),
   and capitals cost more legibility on those than they buy in formality. */
.app-grid thead th {
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  border-bottom: var(--bs-border-width) solid var(--bs-border-color);
}

/* A sortable header is a control, so it gets the body colour - the muted tone
   above is for labels you cannot act on, and applying it to a link makes the
   whole row of headers look disabled. */
.app-grid thead th.app-grid-sortable .app-grid-sort-link {
  color: var(--bs-body-color);
}

/* Horizontal hairlines only. Bootstrap draws a row border via --bs-table-*;
   this keeps it and makes sure no vertical rule survives a screen that used to
   ask for table-bordered. */
.app-grid table > :not(caption) > * > * {
  border-left: 0;
  border-right: 0;
}

/* Rows breathe a little more than table-sm alone gives, because these cells
   routinely stack a link over a muted sub-line. */
.app-grid tbody td {
  padding-top: 0.45rem;
  padding-bottom: 0.45rem;
}

/* The last row should not draw a line into the gap above the pager - or, on a
   grid with totals, into the totals rule that already separates them. */
.app-grid tbody tr:last-child > td {
  border-bottom: 0;
}

/* Buttons inside a row sit on one line and keep their own spacing without each
   screen adding a wrapper for it. */
.app-grid tbody td .btn + .btn,
.app-grid tbody td .btn + form,
.app-grid tbody td form + form,
.app-grid tbody td form + .btn {
  margin-left: 0.25rem;
}

.app-grid tbody td form.d-inline,
.app-grid tbody td form {
  display: inline-block;
}

/* The sticky header has to paint over the rows sliding under it, so it takes
   the same ground as the header above rather than the body's. */
.app-grid-sticky thead th {
  background-color: var(--bs-tertiary-bg);
}
