/*
 * apsw-gridwright structural styles.
 *
 * Everything visible is a custom property. Override the variables to theme the grid; override the
 * `gw-*` classes only when you need to change layout. Nothing here sets a font family or a brand
 * colour, so the grid inherits the surrounding design instead of fighting it.
 *
 * Scope a theme by setting the variables on any ancestor:
 *
 *   .my-page { --gw-accent: #7c3aed; --gw-row-height: 44px; }
 */

.gw-root {
    --gw-font-size: 0.875rem;
    --gw-line-height: 1.45;
    --gw-row-height: 40px;
    /* The smallest hit area on a coarse pointer: a finger, not a cursor. */
    --gw-touch-target: 44px;
    /* Space kept between an overlay and the screen edge. */
    --gw-viewport-gutter: 16px;
    /* The width a responsive grid takes in a shrink-to-fit parent, where it has no content width. */
    --gw-responsive-fallback-width: 30rem;
    /* The space between the cards of a stacked table, and inside one. */
    --gw-card-gap: 8px;
    --gw-cell-padding-x: 12px;
    --gw-cell-padding-y: 8px;
    /* The cell padding of the other levels of `density()`. `comfortable` is the two values above. The row
       height of a level is set from script, on the root, because a windowed body places rows by it. */
    --gw-density-compact-padding-x: 8px;
    --gw-density-compact-padding-y: 4px;
    --gw-density-spacious-padding-x: 16px;
    --gw-density-spacious-padding-y: 14px;
    --gw-radius: 8px;
    --gw-gap: 8px;

    --gw-surface: #ffffff;
    --gw-surface-muted: #f8fafc;
    --gw-surface-hover: #f1f5f9;
    --gw-surface-selected: #eff6ff;
    --gw-text: #0f172a;
    --gw-text-muted: #64748b;
    --gw-border: #e2e8f0;
    --gw-border-strong: #cbd5e1;
    --gw-accent: #2563eb;
    --gw-accent-contrast: #ffffff;
    --gw-danger: #b91c1c;
    --gw-focus-ring: 0 0 0 2px var(--gw-surface), 0 0 0 4px var(--gw-accent);
    /* The edge of a pinned column, on the side the scrolling rows pass. */
    --gw-pinned-shadow-start: 6px 0 8px -6px rgb(15 23 42 / 0.35);
    --gw-pinned-shadow-end: -6px 0 8px -6px rgb(15 23 42 / 0.35);

    display: flex;
    flex-direction: column;
    gap: var(--gw-gap);
    color: var(--gw-text);
    font-size: var(--gw-font-size);
    line-height: var(--gw-line-height);
}

@media (prefers-color-scheme: dark) {
    .gw-root:not([data-gw-theme='light']) {
        --gw-surface: #0f172a;
        --gw-surface-muted: #1e293b;
        --gw-surface-hover: #1e293b;
        --gw-surface-selected: #1e3a8a;
        --gw-text: #e2e8f0;
        --gw-text-muted: #94a3b8;
        --gw-border: #334155;
        --gw-border-strong: #475569;
        --gw-accent: #60a5fa;
        --gw-accent-contrast: #0f172a;
        --gw-danger: #fca5a5;
    }
}

.gw-root[data-gw-theme='dark'] {
    --gw-surface: #0f172a;
    --gw-surface-muted: #1e293b;
    --gw-surface-hover: #1e293b;
    --gw-surface-selected: #1e3a8a;
    --gw-text: #e2e8f0;
    --gw-text-muted: #94a3b8;
    --gw-border: #334155;
    --gw-border-strong: #475569;
    --gw-accent: #60a5fa;
    --gw-accent-contrast: #0f172a;
    --gw-danger: #fca5a5;
}

/* --- WCAG 2.2 AA --------------------------------------------------------------------------- */

/* Everything in this section and the forced-colours block at the end is opt-in: it applies under
   `data-gw-wcag="aa"`, which `wcag()` puts on the root, and nowhere else. A grid that does not list the
   add-on is drawn exactly as it was before it existed.

   The default palette misses two WCAG pairs, muted text on a hovered or selected row (1.4.3) and the
   boundary of a control (1.4.11), so three tokens are reassigned, light and dark. A grid themed through
   `muiTheme()` writes its colours inline, which beats these rules on purpose: the colours are then the
   theme's. The smallest pointer target (2.5.8) is 24px. */
.gw-root[data-gw-wcag='aa'] {
    --gw-target-min: 24px;
    --gw-text-muted: #475569;
    --gw-border: #7c8aa0;
    --gw-border-strong: #64748b;
}

@media (prefers-color-scheme: dark) {
    .gw-root[data-gw-wcag='aa']:not([data-gw-theme='light']) {
        --gw-text-muted: #cbd5e1;
        --gw-border: #7c8aa0;
        --gw-border-strong: #94a3b8;
    }
}

.gw-root[data-gw-wcag='aa'][data-gw-theme='dark'] {
    --gw-text-muted: #cbd5e1;
    --gw-border: #7c8aa0;
    --gw-border-strong: #94a3b8;
}

.gw-root[data-gw-wcag='aa'] .gw-checkbox {
    inline-size: var(--gw-target-min);
    block-size: var(--gw-target-min);
    margin: 0;
    cursor: pointer;
}

.gw-root[data-gw-wcag='aa'] .gw-tree-toggle {
    width: var(--gw-target-min);
    height: var(--gw-target-min);
}

.gw-root[data-gw-wcag='aa'] .gw-group-toggle {
    width: var(--gw-target-min);
    height: var(--gw-target-min);
}

.gw-root[data-gw-wcag='aa'] .gw-detail-toggle {
    width: var(--gw-target-min);
    height: var(--gw-target-min);
}

/* --- density ------------------------------------------------------------------------------ */

/* Set by `density()`. Only the padding is reassigned here: every part that reads the cell padding
   follows, and the row height arrives as an inline `--gw-row-height` on the same element. */
.gw-root[data-gw-density='compact'] {
    --gw-cell-padding-x: var(--gw-density-compact-padding-x);
    --gw-cell-padding-y: var(--gw-density-compact-padding-y);
}

.gw-root[data-gw-density='spacious'] {
    --gw-cell-padding-x: var(--gw-density-spacious-padding-x);
    --gw-cell-padding-y: var(--gw-density-spacious-padding-y);
}

.gw-density {
    display: flex;
    align-items: center;
    gap: var(--gw-gap);
}

/* --- toolbar ------------------------------------------------------------------------------ */

.gw-toolbar {
    display: flex;
    align-items: center;
    gap: var(--gw-gap);
    flex-wrap: wrap;
}

.gw-search {
    flex: 1 1 200px;
    min-width: 0;
    padding: var(--gw-cell-padding-y) var(--gw-cell-padding-x);
    border: 1px solid var(--gw-border);
    border-radius: var(--gw-radius);
    background: var(--gw-surface);
    color: inherit;
    font: inherit;
}

.gw-search:focus-visible,
.gw-select:focus-visible,
.gw-button:focus-visible,
.gw-sort-button:focus-visible,
.gw-checkbox:focus-visible {
    outline: none;
    box-shadow: var(--gw-focus-ring);
}

.gw-selected-count {
    color: var(--gw-text-muted);
    white-space: nowrap;
}

/* --- export ------------------------------------------------------------------------------- */

/* The anchor for the menu. Zero-width positioning context, so the toolbar lays out as though the
   menu were not there, because it is not there until it is opened. */
.gw-export {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--gw-gap);
}

.gw-export-trigger {
    padding: 6px 10px;
    border: 1px solid var(--gw-border);
    border-radius: calc(var(--gw-radius) / 2);
    background: var(--gw-surface);
    color: var(--gw-text);
    font: inherit;
    cursor: pointer;
}

.gw-export-trigger:disabled {
    opacity: 0.45;
    cursor: default;
}

.gw-export-trigger:focus-visible,
.gw-export-item:focus-visible {
    outline: none;
    box-shadow: var(--gw-focus-ring);
}

.gw-export-menu {
    position: absolute;
    z-index: 20;
    inset-block-start: calc(100% + 4px);
    inset-inline-start: 0;
    display: flex;
    flex-direction: column;
    min-inline-size: 180px;
    padding: 4px;
    border: 1px solid var(--gw-border-strong);
    border-radius: calc(var(--gw-radius) + 2px);
    background: var(--gw-surface);
    box-shadow:
        0 1px 2px rgb(15 23 42 / 8%),
        0 8px 24px -6px rgb(15 23 42 / 22%);
}

.gw-export-menu[data-align='end'] {
    inset-inline-start: auto;
    inset-inline-end: 0;
}

.gw-export-item {
    padding: 6px 10px;
    border: 0;
    border-radius: calc(var(--gw-radius) / 2);
    background: transparent;
    color: var(--gw-text);
    font: inherit;
    text-align: start;
    cursor: pointer;
}

.gw-export-item:hover {
    background: var(--gw-surface-hover);
}

.gw-export-group {
    display: flex;
    flex-direction: column;
}

.gw-export-heading {
    padding: 4px 10px 2px;
    color: var(--gw-text-muted);
    font-size: 0.75rem;
    font-weight: 600;
}

/* A radio mark drawn from a border, so the checked state is visible without an icon font and
   without a second element for assistive technology to trip over: `aria-checked` carries it. */
.gw-export-radio {
    display: flex;
    align-items: center;
    gap: 8px;
}

.gw-export-radio::before {
    content: '';
    flex: none;
    inline-size: 10px;
    block-size: 10px;
    border: 1px solid var(--gw-border-strong);
    border-radius: 50%;
}

.gw-export-radio[aria-checked='true']::before {
    border: 3px solid var(--gw-accent);
}

.gw-export-radio[aria-disabled='true'] {
    color: var(--gw-text-muted);
    cursor: default;
}

.gw-export-radio[aria-disabled='true']:hover {
    background: transparent;
}

/* Why an item is off, where the reader is looking when they find it off. */
.gw-export-note {
    max-inline-size: 220px;
    padding-block: 0 6px;
    padding-inline: 28px 10px;
    color: var(--gw-text-muted);
    font-size: 0.75rem;
    white-space: normal;
}

.gw-export-separator {
    block-size: 1px;
    margin: 4px 2px;
    background: var(--gw-border);
}

/* An export that produced nothing has to say so on screen. A hidden live region alone would leave
   a sighted reader clicking a button that appears to do nothing at all. */
.gw-export-error {
    margin: 0;
    color: var(--gw-danger);
}

/* --- table -------------------------------------------------------------------------------- */

/* The wrapper scrolls, never the page: a wide grid stays inside its column on a narrow screen. */
.gw-table-wrapper {
    overflow-x: auto;
    border: 1px solid var(--gw-border);
    border-radius: var(--gw-radius);
    background: var(--gw-surface);
}

.gw-table {
    width: 100%;
    border-collapse: collapse;
    text-align: start;
}

.gw-caption {
    padding: var(--gw-cell-padding-y) var(--gw-cell-padding-x);
    color: var(--gw-text-muted);
    text-align: start;
}

.gw-header-cell {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 0;
    background: var(--gw-surface-muted);
    border-bottom: 1px solid var(--gw-border-strong);
    font-weight: 600;
    text-align: start;
    white-space: nowrap;
}

.gw-header-label {
    display: block;
    padding: var(--gw-cell-padding-y) var(--gw-cell-padding-x);
    overflow: hidden;
    text-overflow: ellipsis;
}

.gw-sort-button {
    display: flex;
    align-items: center;
    gap: 4px;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    font-weight: inherit;
    text-align: inherit;
    cursor: pointer;
}

.gw-row--selectable {
    /* Only where the row itself is the selection control: selection({ selectOnRowClick: true }). */
    cursor: pointer;
}

.gw-sort-button:hover {
    background: var(--gw-surface-hover);
}

/* --- column filters ----------------------------------------------------------------------- */

/* Drawn only when column filters are on, so a grid without them keeps its header markup exactly. */
.gw-header-content {
    display: flex;
    align-items: stretch;
}

.gw-header-content > .gw-sort-button,
.gw-header-content > .gw-header-label {
    flex: 1 1 auto;
    min-width: 0;
}

.gw-filter-trigger {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 28px;
    margin-inline-end: 4px;
    padding: 0;
    border: 0;
    border-radius: calc(var(--gw-radius) / 2);
    background: none;
    color: var(--gw-text-muted);
    cursor: pointer;
}

.gw-filter-trigger svg {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linejoin: round;
}

.gw-filter-trigger:hover {
    background: var(--gw-surface-hover);
    color: var(--gw-text);
}

.gw-filter-trigger:focus-visible,
.gw-filter-input:focus-visible,
.gw-filter-dialog .gw-button:focus-visible {
    outline: none;
    box-shadow: var(--gw-focus-ring);
}

/* Filled and in the accent colour when the column is filtered. The accessible name says it too,
   because colour alone is not a state a screen reader or a colour-blind reader can see. */
.gw-filter-trigger[data-active='true'] {
    color: var(--gw-accent);
}

.gw-filter-trigger[data-active='true'] svg {
    fill: currentColor;
}

.gw-filter-dialog {
    position: fixed;
    z-index: 30;
    inline-size: 260px;
    max-inline-size: calc(100vw - var(--gw-viewport-gutter));
    padding: 12px;
    border: 1px solid var(--gw-border-strong);
    border-radius: calc(var(--gw-radius) + 2px);
    background: var(--gw-surface);
    color: var(--gw-text);
    box-shadow:
        0 1px 2px rgb(15 23 42 / 8%),
        0 8px 24px -6px rgb(15 23 42 / 22%);
    font-weight: 400;
    white-space: normal;
}

.gw-filter-form {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
}

.gw-filter-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    color: var(--gw-text-muted);
    font-size: 0.75rem;
    font-weight: 600;
}

.gw-filter-input {
    min-width: 0;
    padding: 6px 8px;
    border: 1px solid var(--gw-border);
    border-radius: calc(var(--gw-radius) / 2);
    background: var(--gw-surface);
    color: var(--gw-text);
    font: inherit;
    font-size: var(--gw-font-size);
    font-weight: 400;
    color-scheme: inherit;
}

.gw-filter-range {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
}

.gw-filter-choices {
    display: flex;
    flex-direction: column;
    gap: 4px;
    max-block-size: 220px;
    margin: 0;
    padding: 0;
    overflow-y: auto;
    border: 0;
}

.gw-filter-choices legend {
    margin-block-end: 4px;
    padding: 0;
    color: var(--gw-text-muted);
    font-size: 0.75rem;
    font-weight: 600;
}

.gw-filter-choice {
    display: flex;
    align-items: center;
    gap: 8px;
}

.gw-filter-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
}

/* Two classes, because `.gw-button` is declared further down and would otherwise win. */
.gw-filter-dialog .gw-filter-apply {
    border-color: var(--gw-accent);
    background: var(--gw-accent);
    color: var(--gw-accent-contrast);
}

/* The indicator is decorative; the accessible sort state lives in aria-sort on the cell. */
.gw-sort-indicator {
    flex: none;
    width: 0;
    height: 0;
    margin-inline-end: var(--gw-cell-padding-x);
    border-inline: 4px solid transparent;
}

.gw-sort-indicator[data-direction='asc'] {
    border-bottom: 5px solid var(--gw-accent);
}

.gw-sort-indicator[data-direction='desc'] {
    border-top: 5px solid var(--gw-accent);
}

/* Drawn only while more than one column is sorted. Decorative like the indicator: the priority
   reaches assistive technology through the announcement. It sits after the indicator, which keeps
   the indicator's end margin, so the badge takes that margin over. */
.gw-sort-indicator:has(+ .gw-sort-priority) {
    margin-inline-end: 0;
}

.gw-sort-priority {
    flex: none;
    min-width: 1.4em;
    margin-inline-end: var(--gw-cell-padding-x);
    padding: 0 0.3em;
    /* Transparent until forced colours drop the background, and then the badge's outline. */
    border: 1px solid transparent;
    border-radius: 999px;
    background: var(--gw-accent);
    color: var(--gw-accent-contrast);
    font-size: 0.75em;
    font-weight: 600;
    line-height: 1.5;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.gw-cell {
    height: var(--gw-row-height);
    padding: var(--gw-cell-padding-y) var(--gw-cell-padding-x);
    border-bottom: 1px solid var(--gw-border);
    vertical-align: middle;
}

/*
 * The cell the cursor is on, under `cellNavigation()`.
 *
 * An inset ring rather than an outline: a `<td>` in a `table-layout: fixed` grid clips an outline
 * at its edges, and a box-shadow ring also survives a sticky pinned column painting over its
 * neighbour. Nothing about the box changes, so moving the cursor never shifts a row.
 */
.gw-cell--focused {
    outline: none;
    box-shadow: inset var(--gw-focus-ring);
}

.gw-cell--select {
    width: 1%;
    white-space: nowrap;
}

.gw-row--selected .gw-cell {
    background: var(--gw-surface-selected);
}

/* After selection, so the row under the pointer is always the one that looks it: that is the row
   a row menu's action will apply to, selected or not. */
.gw-row:hover .gw-cell {
    background: var(--gw-surface-hover);
}

.gw-tbody[aria-busy='true'] {
    opacity: 0.6;
    transition: opacity 120ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .gw-tbody[aria-busy='true'] {
        transition: none;
    }
}

/* --- column layout ------------------------------------------------------------------------ */

/* Everything below is drawn only under `columnLayout()`: the add-on puts `gw-table--fixed` on the
   table and `gw-cell--pinned` on the cells it pins, so a grid that does not list it keeps exactly
   the table it had. */

/* A width is a suggestion under the automatic algorithm: the browser is free to overrule it from
   the content, and a dragged edge then springs back, which reads as a broken handle. Fixed layout
   makes the declared width the width. `max-content` lets the row grow past the viewport so pinned
   columns have something to stay still against; `min-width: 100%` keeps a narrow grid filling its
   container instead of leaving a gap down the side. */
.gw-table--fixed {
    table-layout: fixed;
    width: max-content;
    min-width: 100%;
}

/* With a fixed width, wrapping would change the row's height as the edge is dragged. */
.gw-table--fixed .gw-cell {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The header cell is the containing block: it is already `position: sticky` for the top edge. */
.gw-resize-handle {
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;
    inline-size: 9px;
    cursor: col-resize;
    /* Without it a touch drag scrolls the grid instead of resizing the column: the browser claims
       the gesture before the first pointermove is delivered. */
    touch-action: none;
    user-select: none;
}

.gw-resize-handle::before {
    content: '';
    position: absolute;
    inset-block: 25%;
    inset-inline-end: 4px;
    inline-size: 1px;
    background: var(--gw-border-strong);
}

.gw-resize-handle:hover::before,
.gw-resize-handle:focus-visible::before {
    inset-block: 0;
    inline-size: 2px;
    background: var(--gw-accent);
}

.gw-resize-handle:focus-visible {
    outline: none;
    box-shadow: var(--gw-focus-ring);
}

/* A sticky cell paints over the rows sliding beneath it, so it needs a ground of its own; without
   one the text of two columns is drawn on top of each other. The hover and selected rules already
   in this sheet are more specific, so a pinned cell still follows its row. */
.gw-cell--pinned {
    background: var(--gw-surface);
}

.gw-header-cell.gw-cell--pinned {
    background: var(--gw-surface-muted);
}

/* The row's state colour, laid over that ground rather than replacing it. A theme is free to make
   hover and selection translucent -- the MUI bridge does, from `action.hover` -- and a sticky cell
   painted in a translucent colour shows the scrolling columns through its own text. Hover after
   selection, as in the rules these override. */
.gw-row--selected .gw-cell--pinned {
    background-color: var(--gw-surface);
    background-image: linear-gradient(var(--gw-surface-selected), var(--gw-surface-selected));
}

.gw-row:hover .gw-cell--pinned {
    background-color: var(--gw-surface);
    background-image: linear-gradient(var(--gw-surface-hover), var(--gw-surface-hover));
}

/* The boundary between what is frozen and what moves. Drawn whether or not the grid is scrolled at
   the moment: it tells the reader which columns will stay before they find out by scrolling. */
.gw-cell--pinned-left-last {
    box-shadow: var(--gw-pinned-shadow-start);
}

.gw-cell--pinned-right-first {
    box-shadow: var(--gw-pinned-shadow-end);
}

/* --- reordering --------------------------------------------------------------------------- */

/* Only a movable header carries the attribute, so a locked one keeps the ordinary cursor and says
   what it is by not inviting the gesture. */
.gw-header-cell[data-movable='true'] {
    cursor: grab;
}

.gw-header-cell[data-dragging='true'] {
    cursor: grabbing;
    opacity: 0.5;
}

/* The edge the column will land against, drawn on the header the pointer is over. An inset shadow
   rather than a border: a border would change the cell's width mid-drag and shift every column
   after it. */
.gw-header-cell[data-drop-target='start'] {
    box-shadow: inset 2px 0 0 0 var(--gw-accent);
}

.gw-header-cell[data-drop-target='end'] {
    box-shadow: inset -2px 0 0 0 var(--gw-accent);
}

/* A pinned boundary already owns `box-shadow`; the drop marker has to win while a drag is in
   flight, or the reader cannot see where they are dropping on the one column that most needs it. */
.gw-header-cell.gw-cell--pinned[data-drop-target='start'] {
    box-shadow: inset 2px 0 0 0 var(--gw-accent), var(--gw-pinned-shadow-start);
}

.gw-header-cell.gw-cell--pinned[data-drop-target='end'] {
    box-shadow: inset -2px 0 0 0 var(--gw-accent), var(--gw-pinned-shadow-end);
}

@media (prefers-reduced-motion: reduce) {
    .gw-header-cell[data-dragging='true'] {
        opacity: 1;
    }
}

/* --- column picker ------------------------------------------------------------------------ */

.gw-column-picker {
    position: relative;
    display: inline-flex;
    align-items: center;
}

.gw-column-picker-trigger {
    padding: 6px 10px;
    border: 1px solid var(--gw-border);
    border-radius: var(--gw-radius);
    background: var(--gw-surface);
    color: inherit;
    font: inherit;
    cursor: pointer;
}

.gw-column-picker-trigger:focus-visible,
.gw-column-picker-item:focus-visible {
    outline: none;
    box-shadow: var(--gw-focus-ring);
}

.gw-column-picker-menu {
    position: absolute;
    z-index: 20;
    inset-block-start: calc(100% + 4px);
    inset-inline-start: 0;
    min-inline-size: 200px;
    max-block-size: 320px;
    overflow-y: auto;
    padding: 4px;
    border: 1px solid var(--gw-border);
    border-radius: var(--gw-radius);
    background: var(--gw-surface);
    box-shadow: 0 8px 24px rgb(15 23 42 / 0.16);
}

.gw-column-picker-menu[data-align='end'] {
    inset-inline-start: auto;
    inset-inline-end: 0;
}

.gw-column-picker-heading {
    display: block;
    padding: 6px 10px 2px;
    color: var(--gw-text-muted);
    font-size: 0.85em;
}

.gw-column-picker-item {
    display: flex;
    align-items: center;
    gap: var(--gw-gap);
    inline-size: 100%;
    padding: 6px 10px;
    border: 0;
    border-radius: calc(var(--gw-radius) / 2);
    background: none;
    color: inherit;
    font: inherit;
    text-align: start;
    cursor: pointer;
}

.gw-column-picker-item:hover {
    background: var(--gw-surface-hover);
}

.gw-column-picker-item[aria-disabled='true'] {
    color: var(--gw-text-muted);
    cursor: default;
}

/* The checkbox, drawn rather than nested: a `menuitemcheckbox` carries its own state, and a real
   input inside it would be a second control the reader has to work out the relationship of. */
.gw-column-picker-column::before {
    content: '';
    flex: none;
    inline-size: 14px;
    block-size: 14px;
    border: 1px solid var(--gw-border-strong);
    border-radius: 3px;
}

.gw-column-picker-column[aria-checked='true']::before {
    border-color: var(--gw-accent);
    background: var(--gw-accent);
    /* The tick: two edges of a square, rotated. No image, no font, nothing to fail to load. */
    box-shadow: inset 0 0 0 2px var(--gw-surface), inset 0 0 0 14px var(--gw-accent);
}

/* The row that holds one column's visibility checkbox and its two pin toggles. */
.gw-column-picker-row {
    display: flex;
    align-items: center;
    gap: 2px;
}

.gw-column-picker-row > .gw-column-picker-item {
    flex: 1 1 auto;
    min-width: 0;
}

.gw-column-picker-pin {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 24px;
    block-size: 24px;
    padding: 0;
    border: 0;
    border-radius: calc(var(--gw-radius) / 2);
    background: none;
    color: var(--gw-text-muted);
    font: inherit;
    cursor: pointer;
}

.gw-column-picker-pin:hover {
    background: var(--gw-surface-hover);
    color: var(--gw-text);
}

.gw-column-picker-pin[aria-checked='true'] {
    background: var(--gw-accent);
    color: var(--gw-accent-contrast);
}

.gw-column-picker-pin:focus-visible {
    outline: none;
    box-shadow: var(--gw-focus-ring);
}

.gw-column-picker-step {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 24px;
    block-size: 24px;
    padding: 0;
    border: 0;
    border-radius: calc(var(--gw-radius) / 2);
    background: none;
    color: var(--gw-text-muted);
    font: inherit;
    cursor: pointer;
}

.gw-column-picker-step:hover:not([aria-disabled='true']) {
    background: var(--gw-surface-hover);
    color: var(--gw-text);
}

.gw-column-picker-step[aria-disabled='true'] {
    opacity: 0.4;
    cursor: default;
}

.gw-column-picker-step:focus-visible {
    outline: none;
    box-shadow: var(--gw-focus-ring);
}

/* The arrows point along the line, so they turn round in a right-to-left page. */
.gw-column-picker-step:dir(rtl) span {
    transform: scaleX(-1);
}

.gw-column-picker-separator {
    block-size: 1px;
    margin: 4px 0;
    background: var(--gw-border);
}

/* --- status rows -------------------------------------------------------------------------- */

.gw-status {
    padding: calc(var(--gw-cell-padding-y) * 3) var(--gw-cell-padding-x);
    color: var(--gw-text-muted);
    text-align: center;
}

.gw-error-title {
    margin: 0 0 4px;
    color: var(--gw-danger);
    font-weight: 600;
}

.gw-error-message {
    margin: 0 0 var(--gw-gap);
}

/* --- the stale-data banner ---------------------------------------------------------------- */

/* Above the table rather than inside it, so warning about the rows does not move them. Every
   colour is a token, so a theme that redefines --gw-danger gets this for free. */
.gw-stale {
    display: flex;
    align-items: center;
    gap: var(--gw-gap);
    padding: calc(var(--gw-cell-padding-y) * 1.5) var(--gw-cell-padding-x);
    border-inline-start: 3px solid var(--gw-danger);
    background: color-mix(in srgb, var(--gw-danger) 10%, transparent);
    color: var(--gw-text);
}

.gw-stale-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    inline-size: 20px;
    block-size: 20px;
    border-radius: 50%;
    background: var(--gw-danger);
    color: var(--gw-surface);
    font-weight: 700;
}

.gw-stale-text {
    display: flex;
    flex-direction: column;
    /* Takes the slack, so the retry button sits at the far end. */
    flex: 1 1 auto;
}

.gw-stale-title {
    color: var(--gw-danger);
    font-weight: 600;
}

.gw-stale-message {
    color: var(--gw-text-muted);
}

/* --- pagination --------------------------------------------------------------------------- */

.gw-pagination {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--gw-gap);
    flex-wrap: wrap;
    color: var(--gw-text-muted);
}

.gw-page-size {
    display: flex;
    align-items: center;
    gap: 6px;
}

.gw-select,
.gw-button {
    padding: 6px 10px;
    border: 1px solid var(--gw-border);
    border-radius: calc(var(--gw-radius) / 2);
    background: var(--gw-surface);
    color: var(--gw-text);
    font: inherit;
    cursor: pointer;
}

.gw-button:disabled {
    opacity: 0.45;
    cursor: default;
}

.gw-button--icon {
    min-width: 34px;
}

.gw-page-controls {
    display: flex;
    gap: 4px;
}

/* --- utilities ---------------------------------------------------------------------------- */

.gw-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* --- tree --------------------------------------------------------------------------------- */

.gw-tree-cell {
    display: flex;
    align-items: center;
    gap: 4px;
    min-width: 0;
}

/* Indentation is padding on this element rather than nested markup, so the table keeps one cell
   per column and a screen reader still reads a grid. */
.gw-tree-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.gw-tree-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: none;
    color: var(--gw-text-muted);
    cursor: pointer;
}

.gw-tree-toggle:hover {
    background: var(--gw-surface-hover);
    color: var(--gw-text);
}

.gw-tree-toggle--empty {
    cursor: default;
}

.gw-tree-chevron {
    width: 0;
    height: 0;
    border-block: 4px solid transparent;
    /* Logical, so the chevron points the other way in a right-to-left locale without a second rule. */
    border-inline-start: 6px solid currentColor;
    transition: transform 120ms ease-out;
}

.gw-tree-toggle[aria-expanded='true'] .gw-tree-chevron {
    transform: rotate(90deg);
}

.gw-tree-toggle[data-loading='true'] .gw-tree-chevron {
    border-inline-start-color: var(--gw-accent);
    animation: gw-tree-pulse 900ms ease-in-out infinite;
}

@keyframes gw-tree-pulse {
    50% { opacity: 0.3; }
}

@media (prefers-reduced-motion: reduce) {
    .gw-tree-chevron { transition: none; }
    .gw-tree-toggle[data-loading='true'] .gw-tree-chevron { animation: none; }
}

.gw-tree-note {
    flex: none;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--gw-surface-muted);
    color: var(--gw-text-muted);
    font-size: 11px;
}

.gw-tree-note--error {
    background: color-mix(in srgb, var(--gw-danger) 15%, transparent);
    color: var(--gw-danger);
}

/* --- grouping ------------------------------------------------------------------------------ */

/* The `<td>` itself stays a plain table cell, so table layout stretches it across the full
   colSpan the normal way; a spanned cell given `display: flex` directly does not reliably fill
   that width in every browser's auto table layout. The flex row lives one level in. */
.gw-row--group .gw-group-cell {
    background: var(--gw-surface-muted);
    font-weight: 600;
}

.gw-group-cell-inner {
    display: flex;
    align-items: center;
    gap: 8px;
}

.gw-group-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: none;
    color: var(--gw-text-muted);
    cursor: pointer;
}

.gw-group-toggle:hover {
    background: var(--gw-surface-hover);
    color: var(--gw-text);
}

.gw-group-chevron {
    width: 0;
    height: 0;
    border-block: 4px solid transparent;
    border-inline-start: 6px solid currentColor;
    transition: transform 120ms ease-out;
}

.gw-row--group[aria-expanded='true'] .gw-group-chevron {
    transform: rotate(90deg);
}

@media (prefers-reduced-motion: reduce) {
    .gw-group-chevron { transition: none; }
}

.gw-group-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.gw-group-count {
    color: var(--gw-text-muted);
    font-weight: 400;
}

.gw-group-aggregates {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-inline-start: auto;
    font-weight: 400;
}

.gw-group-aggregate {
    display: flex;
    align-items: baseline;
    gap: 4px;
}

.gw-group-aggregate-label {
    color: var(--gw-text-muted);
    font-size: 0.85em;
}

.gw-tfoot .gw-summary-row {
    border-top: 2px solid var(--gw-border-strong);
    font-weight: 600;
}

/* --- bubble menu -------------------------------------------------------------------------- */

.gw-bubble-anchor {
    position: relative;
    /* Zero height: it is an anchor for absolute positioning, not a row in the layout. */
    height: 0;
}

.gw-bubble {
    position: absolute;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 1px;
    padding: 4px;
    /* Centred on the row it belongs to. The horizontal position is measured in the component,
       because it follows the pointer and has to be clamped inside the grid. */
    transform: translateY(-50%);
    border: 1px solid var(--gw-bubble-border, var(--gw-border-strong));
    border-radius: calc(var(--gw-radius) + 2px);
    background: var(--gw-bubble-surface, var(--gw-surface));
    box-shadow:
        0 1px 2px rgb(15 23 42 / 8%),
        0 8px 24px -6px rgb(15 23 42 / 22%);
    animation: gw-bubble-in 120ms ease-out;
}

/* Hanging under the row: the top edge is already at the row's bottom, so it must not be pulled up
   by half its own height as well. */
.gw-bubble[data-placement='bottom'] {
    transform: translateY(4px);
}

@keyframes gw-bubble-in {
    from {
        opacity: 0;
        transform: translateY(-50%) scale(0.96);
    }
}

@media (prefers-reduced-motion: reduce) {
    .gw-bubble { animation: none; }
}

.gw-bubble[data-pinned='true'] {
    border-color: var(--gw-accent);
    box-shadow:
        0 0 0 1px var(--gw-accent),
        0 8px 24px -6px rgb(15 23 42 / 28%);
}

.gw-bubble-slot {
    display: contents;
}

.gw-bubble-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 10px;
    border: 0;
    border-radius: calc(var(--gw-radius) / 2);
    background: none;
    color: var(--gw-text);
    font: inherit;
    font-size: 13px;
    line-height: 1.2;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color 90ms ease-out, color 90ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .gw-bubble-item { transition: none; }
}

.gw-bubble-item:hover:not(:disabled) {
    background: var(--gw-surface-hover);
}

.gw-bubble-item:focus-visible {
    outline: none;
    background: var(--gw-surface-hover);
    box-shadow: inset 0 0 0 2px var(--gw-accent);
}

.gw-bubble-item:active:not(:disabled) {
    background: color-mix(in srgb, var(--gw-accent) 14%, transparent);
}

.gw-bubble-item:disabled {
    opacity: 0.4;
    cursor: default;
}

.gw-bubble-item--destructive {
    color: var(--gw-danger);
}

.gw-bubble-item--destructive:hover:not(:disabled),
.gw-bubble-item--destructive:focus-visible {
    background: color-mix(in srgb, var(--gw-danger) 12%, transparent);
}

.gw-bubble-separator {
    width: 1px;
    height: 18px;
    margin-inline: 3px;
    background: var(--gw-border);
}

/* --- inline editing ----------------------------------------------------------------------- */

/* A real button, so an editable cell is reachable by Tab and announced as activatable. */
.gw-edit-trigger {
    /* Flex rather than block, because the column's icon is rendered inside the trigger so that
       clicking it starts editing. It still fills the cell, so the whole width is a target. */
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    padding: 2px 4px;
    margin: -2px -4px;
    border: 1px solid transparent;
    border-radius: 4px;
    background: none;
    color: inherit;
    font: inherit;
    text-align: inherit;
    cursor: text;
}

.gw-edit-trigger:hover,
.gw-edit-trigger:focus-visible {
    border-color: var(--gw-border-strong);
    background: var(--gw-surface);
}

.gw-edit-trigger--pending {
    opacity: 0.55;
}

.gw-edit-trigger--error {
    border-color: var(--gw-danger);
    color: var(--gw-danger);
}

.gw-editor {
    width: 100%;
    padding: 2px 4px;
    margin: -2px -4px;
    border: 1px solid var(--gw-accent);
    border-radius: 4px;
    background: var(--gw-surface);
    color: var(--gw-text);
    font: inherit;
}

.gw-editor:focus-visible {
    outline: none;
    box-shadow: var(--gw-focus-ring);
}

/* --- virtualization ----------------------------------------------------------------------- */

/* Spacers carry the height of everything not rendered, so the element stays a real table with
   real rows rather than a stack of absolutely positioned divs. */
.gw-spacer > td {
    padding: 0;
    border: 0;
}

.gw-row--skeleton .gw-cell {
    /* A row whose data has not arrived. Deliberately quiet: it is a placeholder, not content. */
    color: transparent;
}

.gw-skeleton {
    display: block;
    width: min(60%, 320px);
    height: 0.7em;
    border-radius: 999px;
    background: linear-gradient(
        90deg,
        var(--gw-surface-muted) 25%,
        var(--gw-surface-hover) 50%,
        var(--gw-surface-muted) 75%
    );
    background-size: 240% 100%;
    animation: gw-skeleton-sweep 1.4s ease-in-out infinite;
}

@keyframes gw-skeleton-sweep {
    from { background-position: 120% 0; }
    to { background-position: -120% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .gw-skeleton {
        animation: none;
        background: var(--gw-surface-muted);
    }
}

/* --- icons -------------------------------------------------------------------------------- */

.gw-cell-content {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
}

.gw-icon {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 1em;
    height: 1em;
    color: var(--gw-icon, var(--gw-text-muted));
}

.gw-icon svg {
    width: 100%;
    height: 100%;
}

.gw-cell-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --- the hidden attribute wins ------------------------------------------------------------- */

/*
 * A part hidden with `hidden` stays hidden. Every layout rule above uses a class selector, which
 * has the same specificity as a bare `[hidden]` and comes first, so `.gw-pagination` was quietly
 * overriding it and the footer stayed on screen under a virtualized body.
 */
.gw-root [hidden] {
    display: none;
}

/* --- row detail ---------------------------------------------------------------------------- */

/* Drawn only under `rowDetail()`. The panel row is presentational: it is a row of the table and
   deliberately not a row of the grid, so it carries no row height of its own and lets its content
   decide. */

.gw-detail-row {
    background: var(--gw-detail-background, var(--gw-surface-muted));
}

.gw-detail-cell {
    padding: var(--gw-detail-padding, var(--gw-cell-padding-y) var(--gw-cell-padding-x));
    border-bottom: 1px solid var(--gw-border);
}

/* Sticky at the inline start so the panel's content stays where the reader is looking while a wide
   table scrolls sideways, without measuring the scroll container. The wrapper's ref is held by
   `virtualRows()` and `GridTable` keeps only the last one it is handed, so measuring is not
   available to a second add-on anyway. A column pinned by `columnLayout()` still paints over it. */
.gw-detail-panel {
    position: sticky;
    inset-inline-start: 0;
}

.gw-cell--detail {
    width: var(--gw-detail-toggle-width, 40px);
    text-align: center;
}

.gw-detail-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: none;
    color: var(--gw-text-muted);
    cursor: pointer;
}

.gw-detail-toggle:hover:not(:disabled) {
    background: var(--gw-surface-hover);
    color: var(--gw-text);
}

.gw-detail-toggle:disabled {
    cursor: default;
    opacity: 0.4;
}

.gw-detail-chevron {
    width: 0;
    height: 0;
    border-block: 4px solid transparent;
    /* Logical, so the chevron points the other way in a right-to-left locale without a second rule. */
    border-inline-start: 6px solid currentColor;
    transition: transform 120ms ease-out;
}

.gw-detail-toggle[aria-expanded='true'] .gw-detail-chevron {
    transform: rotate(90deg);
}

@media (prefers-reduced-motion: reduce) {
    .gw-detail-chevron { transition: none; }
}

/* --- responsive --------------------------------------------------------------------------- */

/* The default root is deliberately not a size container: `container-type: inline-size` collapses
   a grid in an inline-block, float or flex item with no min-width to zero width. A grid that lists
   responsive() opts in, and the intrinsic size keeps a shrink-to-fit parent from collapsing it. */
.gw-root[data-gw-responsive] {
    container-type: inline-size;
    container-name: gw;
    contain-intrinsic-inline-size: auto var(--gw-responsive-fallback-width);
}

/* Thresholds are constants: a container query cannot read a custom property. */
@container gw (max-width: 30rem) {
    .gw-search {
        flex-basis: 100%;
    }

    .gw-pagination {
        justify-content: space-between;
    }
}

/* A column hidden by its container's width. Display only: the column is still in the data. */
[data-gw-hidden] {
    display: none;
}

/* Pinned columns that take more than half the container leave nothing to scroll. */
.gw-root[data-gw-pins-capped] :is(th, td)[data-pinned] {
    position: static !important;
}

.gw-sentinel {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
    pointer-events: none;
}

/* Overlays never wider than the screen they open on. */
.gw-column-picker-menu,
.gw-bubble {
    max-inline-size: calc(100vw - 16px);
}

.gw-row-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 4px;
    border: 0;
    border-radius: calc(var(--gw-radius) / 2);
    background: none;
    color: var(--gw-text-muted);
    cursor: pointer;
}

.gw-row-trigger:focus-visible {
    outline: 2px solid var(--gw-accent);
    outline-offset: 2px;
}

.gw-cell--row-trigger {
    width: 1%;
    white-space: nowrap;
}

@media (pointer: coarse) {
    .gw-sort-button,
    .gw-button,
    .gw-select,
    .gw-bubble-item,
    .gw-column-picker-trigger,
    .gw-column-picker-item,
    .gw-row-trigger {
        min-block-size: var(--gw-touch-target);
    }

    .gw-button--icon,
    .gw-row-trigger {
        min-inline-size: var(--gw-touch-target);
    }
}

/* --- stacked rows ------------------------------------------------------------------------- */

/* Below `stackBelow` every row is a card. The roles are restored in markup (role="row", "gridcell",
   "columnheader"), because display other than table drops table semantics in several browsers. */
.gw-table--stacked {
    display: block;
    width: 100%;
    table-layout: auto;
}

/* The header cells stay in the document, visually hidden, so every value still has its column
   header for assistive technology. The row and group around them are left as table boxes. */
.gw-table--stacked thead th {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.gw-table--stacked tbody {
    display: block;
}

.gw-table--stacked tbody tr {
    display: grid;
    gap: calc(var(--gw-card-gap) / 2);
    margin-block-end: var(--gw-card-gap);
    padding: var(--gw-card-gap);
    border: 1px solid var(--gw-border);
    border-radius: var(--gw-radius);
}

.gw-table--stacked tbody td {
    display: block;
    width: auto !important;
    height: auto;
    padding: calc(var(--gw-cell-padding-y) / 2) var(--gw-cell-padding-x);
    border-bottom: 0;
    /* A column's own alignment is an inline style, for a column of numbers; a card reads from the
       start, so it has to be overridden here. */
    text-align: start !important;
}

/* One line per value: the label, then the cell. The label is generated from the header text, with an
   empty alternative so assistive technology hears the real column header and not this a second time. */
.gw-table--stacked tbody td[data-gw-label] {
    display: grid;
    grid-template-columns: minmax(5rem, 35%) minmax(0, 1fr);
    gap: var(--gw-card-gap);
    align-items: baseline;
}

.gw-table--stacked tbody td[data-gw-label]::before {
    content: attr(data-gw-label) / '';
    color: var(--gw-text-muted);
    font-size: 0.85em;
}

/* The row-menu trigger of a touch device sits in the card's top corner, opposite the checkbox, where a
   thumb finds it, and not after the last value where it reads as an afterthought. */
.gw-table--stacked tbody tr {
    position: relative;
}

.gw-table--stacked tbody td.gw-cell--row-trigger {
    position: absolute;
    inset-block-start: var(--gw-card-gap);
    inset-inline-end: var(--gw-card-gap);
    padding: 0;
}

/* A column hidden by width stays hidden in a card; the rules above would otherwise out-rank it. */
.gw-table--stacked [data-gw-hidden],
.gw-table--stacked tbody td[data-gw-hidden] {
    display: none;
}

/* A pinned column has nothing to stick to in a card. */
.gw-root[data-gw-stacked] :is(th, td)[data-pinned] {
    position: static !important;
}

.gw-stacked-sort {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--gw-gap);
}

.gw-hidden-query-note {
    color: var(--gw-text-muted);
}

/* --- forced colours ----------------------------------------------------------------------- */

/* Windows High Contrast and other forced-colour modes replace every colour with a system one and drop
   box-shadow and background images. The focus ring, the cursor cell, the selected row and the sort
   state are all drawn with those, so each gets a rule that survives. System colours only: nothing here
   is a colour of ours. Opt-in with the rest of the section above: every rule is under `wcag()`'s
   attribute, and every dialog and menu is inside the root, so the scope reaches them. */
@media (forced-colors: active) {
    .gw-root[data-gw-wcag='aa'] [class*='gw-']:focus-visible {
        outline: 2px solid Highlight;
        outline-offset: 2px;
    }

    .gw-root[data-gw-wcag='aa'] .gw-cell--focused {
        outline: 2px solid Highlight;
        outline-offset: -2px;
    }

    .gw-root[data-gw-wcag='aa'] .gw-row--selected .gw-cell {
        border-block: 2px solid Highlight;
    }

    .gw-root[data-gw-wcag='aa'] .gw-sort-indicator[data-direction='asc'] {
        border-bottom-color: CanvasText;
    }

    .gw-root[data-gw-wcag='aa'] .gw-sort-indicator[data-direction='desc'] {
        border-top-color: CanvasText;
    }

    .gw-root[data-gw-wcag='aa'] .gw-sort-priority {
        border-color: CanvasText;
    }

    .gw-root[data-gw-wcag='aa'] .gw-resize-handle::before {
        background: CanvasText;
    }

    .gw-root[data-gw-wcag='aa'] .gw-resize-handle:hover::before,
    .gw-root[data-gw-wcag='aa'] .gw-resize-handle:focus-visible::before {
        background: Highlight;
    }

    .gw-root[data-gw-wcag='aa'] .gw-column-picker-item[aria-disabled='true'],
    .gw-root[data-gw-wcag='aa'] .gw-export-radio[aria-disabled='true'],
    .gw-root[data-gw-wcag='aa'] .gw-button:disabled,
    .gw-root[data-gw-wcag='aa'] .gw-bubble-item:disabled {
        color: GrayText;
    }
}
