/* ==========================================================================
   Quercia viva — AG Grid skin (brand §5.3 tables)
   The theme itself is a JS object built from the --qc-* tokens
   (assets/ag_grid/theme.js, passed as the grid's `theme` option). What is left
   here is what a theme parameter cannot express: cell alignment, the two-line
   clamp, the surrounding chrome (toolbar, wrapper, overlays) and the in-cell
   badges and action buttons.

   Everything is scoped to .qc-grid__grid, the host element the Twig component
   renders. There is no .ag-theme-* class any more, and no specificity hack:
   the Theming API injects its CSS in a layer of its own, so ordinary
   single-class rules win.
   ========================================================================== */

/* The grid contains native scrollbars (the in-cell overflow of a clamped cell,
   the body's horizontal bar). They are painted by the browser, which needs to
   be told which way the surface goes — a theme parameter would be frozen at
   build time, so it is read from the same attribute the rest of the app uses. */
.qc-grid__grid { color-scheme: light; }
[data-bs-theme="dark"] .qc-grid__grid { color-scheme: dark; }

/* Two-line cells (twoLine renderer: <b>primary</b><br><small>secondary</small>).
   Tight leading so both lines sit centered within the taller row. */
.qc-grid__grid .ag-cell { line-height: 1.3; }
.qc-grid__grid .ag-cell b { color: var(--qc-text-primary); font-weight: 600; }
.qc-grid__grid .ag-cell small { color: var(--qc-text-tertiary); font-size: var(--qc-grid-cell-small-size); }
/* The compact form of a two-line cell lives further down, beside the rest of the
   compact rules — see "Compact: one line, and everything the cell holds is on
   it". */
/* Row state (`row_class_field` renders .table-<color>). Bootstrap's table
   variants only paint inside a real <table>: on a grid row they left the
   background untouched and merely forced a near-black text colour — invisible
   in light theme and unreadable in dark. Paint them from the tokens instead:
   a status bar on the leading edge, plus a tint for the states worth catching
   the eye. The plain state keeps the bar alone so a full list does not read as
   one solid block of colour. */
.qc-grid__grid .ag-row.table-success,
.qc-grid__grid .ag-row.table-warning,
.qc-grid__grid .ag-row.table-danger,
.qc-grid__grid .ag-row.table-secondary { color: var(--qc-text-primary); }
.qc-grid__grid .ag-row.table-success { box-shadow: inset 3px 0 0 var(--qc-success); }
.qc-grid__grid .ag-row.table-warning {
  box-shadow: inset 3px 0 0 var(--qc-status-orange);
  background-color: color-mix(in srgb, var(--qc-status-orange) 9%, transparent);
}
.qc-grid__grid .ag-row.table-danger {
  box-shadow: inset 3px 0 0 var(--qc-error);
  background-color: color-mix(in srgb, var(--qc-error) 9%, transparent);
}
/* A row that opens something says so under the pointer, and only that row: a
   list where some rows lead somewhere and others do not (a stamping nobody may
   still edit, a customer with no sheet yet) must not offer the hand on all of
   them. The cells that answer for themselves keep their own cursor — the action
   buttons, the links, the "+N" pill — so the hand appears where the row is what
   is being pressed and nowhere else. The hover tint is the grid's own
   (`rowHoverColor`), unchanged: this adds the pointer, not a second highlight. */
.qc-grid__grid .ag-row.qc-row-open .ag-cell { cursor: pointer; }
/* The row just saved (`highlight_id`): a wash of primary with its leading bar,
   held for a moment and let go. It sits on an overlay so the row's own state
   tint stays underneath and is there again when it fades. Longer than any §7
   duration on purpose — it has to be found by an eye that has just arrived.
   Under reduced motion the global rule would collapse it straight to its
   transparent end, so it stays a still tint instead, taken off by the same
   timer (ag_grid_controller `_flashRow`). */
.qc-grid__grid .ag-row.qc-row-highlight::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  animation: qc-row-highlight calc(var(--qc-dur-emphasized) * 4) var(--qc-ease-standard) both;
}
@keyframes qc-row-highlight {
  0%, 35% {
    background-color: var(--qc-layer-highlight);
    box-shadow: inset 3px 0 0 var(--qc-primary);
  }
  100% { background-color: transparent; box-shadow: inset 3px 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .qc-grid__grid .ag-row.qc-row-highlight::after {
    animation: none;
    background-color: var(--qc-layer-wash);
    box-shadow: inset 3px 0 0 var(--qc-primary);
  }
}
.qc-grid__grid .ag-row.qc-row-open .qc-actions-cell,
.qc-grid__grid .ag-row.qc-row-open .ag-cell[col-id="ag-Grid-SelectionColumn"] { cursor: default; }

/* header labels: uppercase, tracked — quiet and scannable (Attio/Linear feel) */
.qc-grid__grid .ag-header-cell-text {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  font-size: var(--qc-grid-header-font-size);
}

/* Wrapper: a card framing the toolbar + grid.
   width:100% so it fills flex parents (Tabler .row-deck makes .col-* a
   flex-direction:column container, which would otherwise shrink us to content). */
.qc-grid {
  display: flex;
  flex-direction: column;
  gap: .75rem;
  width: 100%;
}
.qc-grid__overlay { width: 100%; }

/* Phone: the table reaches the edges of the card it sits in.
   A card body insets its contents by a comfortable margin, which is right for
   prose and for forms and wrong for a table: at 360px it hands roughly a fifth
   of the screen to whitespace on either side of the one element that has more
   to show than it has room for. The grid is pulled back out over that inset
   (hence `width: auto` — with the inherited 100% the negative margins would push
   it off the right edge instead of widening it).

   The toolbar is put back inside the inset on its own: the search field and the
   column button are chrome, they read as belonging to the card, and flush to the
   screen edge they would look like an error rather than a decision. */
@media (max-width: 575.98px) {
  .card-body .qc-grid {
    width: auto;
    margin-inline: calc(-1 * var(--tblr-card-spacer-x, 1.5rem));
  }
  .card-body .qc-grid > .qc-grid__toolbar {
    padding-inline: var(--tblr-card-spacer-x, 1.5rem);
  }

  /* Except the works table, which reaches the card edges already — its body is
     given `padding: 0` further down, so it has no inset to be pulled out of.
     Pulling anyway moved it a further 1.5rem out on each side, past the card
     and out of sight: the tick boxes went off the left, the action buttons off
     the right. The card sets --tblr-card-spacer-x for its children whether it
     spends it or not, so the variable cannot be asked which cards did. */
  .qc-worktable-card .qc-grid {
    margin-inline: 0;
  }
  .qc-worktable-card .qc-grid > .qc-grid__toolbar {
    padding-inline: 1.25rem;
  }
}

/* Toolbar above the grid: quick search + column settings */
.qc-grid__toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
}
.qc-grid__search {
  position: relative;
  flex: 1 1 240px;
  max-width: 360px;
}
.qc-grid__search input {
  width: 100%;
  height: var(--qc-control-h);
  padding: 0 .75rem 0 2.25rem;
  border-radius: var(--qc-radius-input);
  border: 1px solid var(--qc-border);
  background: var(--qc-field-bg);
  color: var(--qc-text-primary);
  font-family: var(--qc-font-body);
}
.qc-grid__search input:focus {
  outline: none;
  border-color: var(--qc-primary);
  box-shadow: var(--qc-focus-ring);
}
.qc-grid__search .qc-grid__search-icon {
  position: absolute;
  left: .7rem; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; color: var(--qc-text-tertiary);
  pointer-events: none;
}
.qc-grid__toolbar-spacer { flex: 1 1 auto; }

/* Contextual bulk-action bar (lives in the toolbar_start slot; shown only when
   rows are selected). */
.qc-grid__bulkbar {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  margin-right: auto;
  padding: .3rem .45rem .3rem .7rem;
  border-radius: var(--qc-radius-btn);
  background: color-mix(in srgb, var(--qc-primary) 10%, var(--qc-surface));
  border: 1px solid color-mix(in srgb, var(--qc-primary) 25%, transparent);
  box-shadow: var(--qc-shadow-sm);
}
.qc-grid__bulkbar[hidden] { display: none; }
.qc-grid__bulkbar-count { font-size: var(--qc-fs-sm); font-weight: 600; color: var(--qc-text-secondary); white-space: nowrap; }
.qc-grid__bulkbar-count:empty { display: none; }

/* Toolbar buttons — the column chooser and the "Senza data" filter share the
   brand ghost-primary look from the mockup (not the default grey outline). */
.qc-grid__colmenu > button,
.qc-grid__filter-toggle {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  height: 40px;
  padding: 0 .85rem;
  font: 600 var(--qc-fs-md) var(--qc-font-body);
  cursor: pointer;
  color: var(--qc-primary);
  background: var(--qc-layer-wash);
  border: 1px solid transparent;
  border-radius: var(--qc-radius-btn);
  backdrop-filter: blur(10px);
  transition: background var(--qc-dur-fast, .18s) var(--qc-ease-fast, ease),
              border-color var(--qc-dur-fast, .18s) var(--qc-ease-fast, ease);
}
.qc-grid__colmenu > button svg,
.qc-grid__filter-toggle svg { width: 18px; height: 18px; }
/* The same button as a link (a page's own way out, beside "Colonne"). */
a.qc-grid__filter-toggle,
a.qc-grid__filter-toggle:hover { text-decoration: none; }
/* Keep the pointer target on the button itself so its native `title` tooltip
   surfaces reliably even when hovering the icon or label. */
.qc-grid__filter-toggle svg { pointer-events: none; }
.qc-grid__colmenu > button:hover,
.qc-grid__filter-toggle:hover {
  background: var(--qc-layer-highlight);
  color: var(--qc-primary);
  border-color: transparent;
}
/* Active ("Senza data" filter on): a solid filled pill, clearly distinct from the
   ghost look of the inactive state so it reads as toggled at a glance. */
.qc-grid__filter-toggle[aria-pressed="true"],
.qc-grid__filter-toggle.active {
  background: var(--qc-primary);
  color: var(--qc-on-primary);
  border-color: var(--qc-primary);
  box-shadow: var(--qc-shadow-sm);
}
.qc-grid__filter-toggle[aria-pressed="true"]:hover,
.qc-grid__filter-toggle.active:hover {
  background: color-mix(in srgb, var(--qc-primary) 88%, #000);
  color: var(--qc-on-primary);
  border-color: var(--qc-primary);
}

/* Bulk-action buttons: compact glass pills matching the mockup. */
.qc-grid__bulkbar .btn {
  height: 34px;
  padding: 0 .8rem;
  border-radius: 10px;
  font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .qc-grid__colmenu > button,
  .qc-grid__filter-toggle { transition: none; }
}

/* Column settings dialog (Community has no Enterprise tool panel).
   A dialog rather than the dropdown it replaces: the works list can carry 25
   columns, and a 220px panel could only scroll past them. Search narrows the
   list, the count says where you stand, and the reset puts the table back to
   what the page declares. */
.qc-grid__colmenu {
  position: relative;
}
.qc-colmodal__search {
  position: relative;
  margin-bottom: .75rem;
}
.qc-colmodal__search input {
  width: 100%;
  height: var(--qc-control-h);
  padding: 0 .75rem 0 2.25rem;
  border-radius: var(--qc-radius-input);
  border: 1px solid var(--qc-border);
  background: var(--qc-field-bg);
  color: var(--qc-text-primary);
  font-family: var(--qc-font-body);
}
.qc-colmodal__search input:focus {
  outline: none;
  border-color: var(--qc-primary);
  box-shadow: var(--qc-focus-ring);
}
.qc-colmodal__search-icon {
  position: absolute;
  left: .7rem; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px;
  color: var(--qc-text-tertiary);
  pointer-events: none;
}
.qc-colmodal__meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  flex-wrap: wrap;
  margin-bottom: .5rem;
  padding-bottom: .5rem;
  border-bottom: 1px solid var(--qc-divider);
}
.qc-colmodal__count {
  font-size: var(--qc-fs-sm);
  font-weight: 600;
  color: var(--qc-text-secondary);
  font-variant-numeric: tabular-nums;
}
.qc-colmodal__bulk { display: inline-flex; gap: .25rem; }
.qc-colmodal__bulk .btn {
  padding: 0 .35rem;
  font-size: var(--qc-fs-sm);
  font-weight: 600;
  color: var(--qc-primary);
  text-decoration: none;
}
.qc-colmodal__bulk .btn:hover { text-decoration: underline; }
.qc-colmodal__hint {
  margin: 0 0 .5rem;
  font-size: var(--qc-fs-sm);
  color: var(--qc-text-tertiary);
}
.qc-colmodal__list { display: flex; flex-direction: column; gap: 1px; }
.qc-colmodal__item {
  display: flex;
  align-items: center;
  gap: .625rem;
  padding: .5rem .625rem;
  border-radius: var(--qc-radius-sm);
  cursor: pointer;
  color: var(--qc-text-primary);
  margin: 0;
}
.qc-colmodal__item:hover {
  background: var(--qc-layer-hover);
}
.qc-colmodal__item[hidden] { display: none; }
.qc-colmodal__name { min-width: 0; overflow-wrap: anywhere; }

/* ---- The grip a column is moved by ----
   Columns are arranged here and nowhere else: the header no longer drags, so
   this is the whole of the gesture and it has to be visible without being
   loud. A button rather than a decorated <span>, because the arrangement is
   reachable with a keyboard too — focused, up and down move the row.
   The row it sits in is a <label>, so the grip must not pass its click on to
   the tick box: `pointer-events` stay on the button and the handler stops it. */
.qc-colmodal__handle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: var(--qc-radius-sm);
  background: transparent;
  color: var(--qc-text-secondary);
  cursor: grab;
  touch-action: none;   /* the drag is ours; the list must not scroll under it */
}
.qc-colmodal__handle:hover { color: var(--qc-primary); background: var(--qc-layer-selected); }
.qc-colmodal__handle:active { cursor: grabbing; }
.qc-colmodal__handle svg { width: 16px; height: 16px; }
/* Sortable's own classes: what is being dragged, and the space it will land in. */
.qc-colmodal__item.qc-colmodal__item--ghost {
  opacity: 1;                       /* never dim — brand §8 */
  background: var(--qc-layer-wash);
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--qc-primary) 45%, transparent);
}
.qc-colmodal__item.qc-colmodal__item--drag {
  background: var(--qc-surface);
  box-shadow: var(--qc-shadow-md);
}
.qc-colmodal__empty {
  margin: 1rem 0 0;
  text-align: center;
  color: var(--qc-text-tertiary);
  font-size: var(--qc-fs-md);
}

/* Room under the rows for a scrollbar that floats.
   Where a browser paints its scrollbars over the content rather than beside it
   — a phone, or macOS — AG Grid takes the horizontal bar out of the flow and
   pins it to the bottom of `.ag-root` (`position: absolute; bottom: 0`, its
   `.ag-scrollbar-invisible` rule). That is right in itself: an overlay bar
   should cost no layout. But these grids lay themselves out to their own
   content (`domLayout: 'autoHeight'`), so the bottom of the grid is the bottom
   of the last row, and the bar had nothing to float over but that row — which
   came out sliced along its second line.
   The 16px is AG Grid's own: it is the height the library gives the bar when it
   finds scrollbars invisible. Absolute offsets are measured from the padding
   box, so the bar still sits flush at the bottom and the rows end above it.
   Note it is not conditional on the table actually scrolling sideways: what
   little is spent on a table that does not is a strip of quiet under it. */
.qc-grid__grid .ag-root.ag-layout-auto-height:has(.ag-body-horizontal-scroll.ag-scrollbar-invisible) {
  padding-bottom: 16px;
}

/* The grid host: card frame + brand radius */
.qc-grid__grid {
  width: 100%;
  border: 1px solid var(--qc-border);
  border-radius: var(--qc-radius-card);
  overflow: hidden;
  position: relative;
}
/* Scroll edges (components.css has the rule's reason). The grid scrolls a
   viewport of its own between the columns that stay put, so the two shades are
   drawn over the host at that viewport's edges — the controller measures them
   into --qc-fade-left / --qc-fade-right and says which side has more. A
   gradient and an opacity: nothing here blurs (the note on row buttons). */
.qc-grid__grid::before,
.qc-grid__grid::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 2;
  width: 1rem;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-grid__grid::before {
  left: var(--qc-fade-left, 0px);
  background: linear-gradient(to right, var(--qc-scroll-shade), transparent);
}
.qc-grid__grid::after {
  right: var(--qc-fade-right, 0px);
  background: linear-gradient(to left, var(--qc-scroll-shade), transparent);
}
.qc-grid__grid[data-qc-fade-start]::before,
.qc-grid__grid[data-qc-fade-end]::after { opacity: 1; }

/* Works table: merge the grid flush with its card frame — no box-in-a-box.
   The card supplies the border/radius, so the grid drops its own and reaches
   the card edges; the toolbar keeps an inset and a hairline separates it from
   the rows. */
.qc-worktable-card > .card-body {
  padding: 0;
}
.qc-worktable-card .qc-grid {
  gap: 0;
}
.qc-worktable-card .qc-grid__toolbar {
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--qc-border);
}
.qc-worktable-card .qc-grid__grid {
  border: 0;
  border-radius: 0 0 var(--qc-radius-card) var(--qc-radius-card);
}
/* AG Grid's own root wrapper carries --ag-wrapper-border-radius, which would
   round the header's top corners even though the grid is flush with the card.
   Square it off; the host's overflow:hidden + bottom radius clip the bottom to
   the card. */
.qc-worktable-card .qc-grid__grid .ag-root-wrapper {
  border-radius: 0;
}

/* ---------------------------------------------------------------- alignment
   Data is aligned by what it is, not uniformly: text reads from the left,
   numbers and amounts line up on the right where their digits can be compared
   down the column, and badges and action buttons sit centred because they are
   objects rather than values.

   Three sources agree on one set of rules:
     - a renderer that knows its column is numeric adds .qc-al-end
       (assets/ag_grid/renderers.js);
     - a column declared `type: 'numericColumn'` in Twig gets AG Grid's own
       .ag-right-aligned-cell, which is honoured here instead of being undone;
     - a template can override any column with `align: 'start'|'center'|'end'`.

   The cell stays the column flexbox it has always been — that is what stacks a
   two-line cell and centres it vertically, and it is also what lets
   autoSizeColumns measure a column by its content: in a row flexbox the value
   element sits on the main axis, so measured unconstrained it collapses and
   every column comes back at its minimum width. Only the cross-axis value
   changes per alignment, with text-align carrying it into wrapped text. */
.qc-grid__grid .ag-cell {
  display: flex;
  flex-direction: column;
  justify-content: center;   /* vertical */
  align-items: flex-start;   /* horizontal — the default is the left */
  text-align: left;
}
.qc-grid__grid .ag-cell.qc-al-end,
.qc-grid__grid .ag-cell.ag-right-aligned-cell {
  align-items: flex-end;
  text-align: right;
}
.qc-grid__grid .ag-cell.qc-al-center {
  align-items: center;
  text-align: center;
}
/* A value that is not stretched across the cell is as wide as its content, and
   one that is centred overflows on *both* sides: a pill longer than its column
   ran under the column before it and under the buttons after it. Capped, the
   pill ends its words in an ellipsis and keeps its icon (`.qc-stpill`).
   max-width and not width, so autoSizeColumns still measures real content. */
.qc-grid__grid .ag-cell.qc-al-center > .ag-cell-value,
.qc-grid__grid .ag-cell.qc-al-center > .ag-cell-wrapper {
  max-width: 100%;
}
/* A left-aligned cell is a column flexbox that sizes its value to the
   content: without a cap one long machine pill ran into the next column. */
.qc-grid__grid .ag-cell > .ag-cell-value,
.qc-grid__grid .ag-cell > .ag-cell-wrapper,
.qc-grid__grid .ag-cell-wrapper > .ag-cell-value {
  max-width: 100%;
  min-width: 0;
}

/* A header the fit pass squeezed wraps (wrapHeaderText) — two short lines,
   not one cut with "…". */
.qc-grid__grid .ag-header-cell-wrap-text .ag-header-cell-text {
  line-height: 1.25;
  overflow-wrap: normal;
}

/* Headers follow their column. AG Grid lays the label out as a flex row and
   gives it `flex: 1 1 auto`, so the label is moved rather than the text. */
.qc-grid__grid .ag-header-cell-label {
  flex-direction: row;
  justify-content: flex-start;
}
.qc-grid__grid .qc-al-end-h .ag-header-cell-label,
.qc-grid__grid .ag-right-aligned-header .ag-header-cell-label {
  justify-content: flex-end;
}
.qc-grid__grid .qc-al-center-h .ag-header-cell-label {
  justify-content: center;
}

/* The sort mark sits at the right edge of every column, whatever the heading
   is aligned to: one place to look for it across the whole header, instead of
   wherever a heading of that length happened to end. The text keeps its own
   alignment in the room left over — at the start, centred, or against the
   mark on a column of figures. */
.qc-grid__grid .ag-header-cell-label .ag-sort-indicator-container {
  order: 2;
  flex: none;
  margin-inline-start: auto;
}
/* The gap rides on the icons, not the container: a column that cannot be
   sorted has an empty one, and a gap there pushed its heading off the edge. */
.qc-grid__grid .ag-header-cell-label .ag-sort-indicator-icon {
  margin-inline-start: .25rem;
}
.qc-grid__grid .qc-al-end-h .ag-header-cell-text,
.qc-grid__grid .ag-right-aligned-header .ag-header-cell-text {
  margin-inline-start: auto;
}
.qc-grid__grid .qc-al-end-h .ag-sort-indicator-container,
.qc-grid__grid .ag-right-aligned-header .ag-sort-indicator-container,
.qc-grid__grid .qc-al-center-h .ag-sort-indicator-container {
  margin-inline-start: 0;
}
.qc-grid__grid .qc-al-center-h .ag-header-cell-text {
  margin-inline: auto;
}

/* The sort affordance is always rendered (unSortIcon in the grid controller),
   so the unsorted state has to read as available-but-inactive without being
   mistaken for an applied sort. Colour, not opacity — dimming with opacity is
   what the brand guideline rules out. */
.qc-grid__grid .ag-header-cell .ag-sort-none-icon {
  color: var(--qc-text-tertiary);
}
.qc-grid__grid .ag-header-cell:hover .ag-sort-none-icon {
  color: var(--qc-text-secondary);
}
.qc-grid__grid .ag-header-cell .ag-sort-ascending-icon,
.qc-grid__grid .ag-header-cell .ag-sort-descending-icon,
.qc-grid__grid .ag-header-cell .ag-sort-order {
  color: var(--qc-primary);
}

/* Wrapped cells (wrapText + autoHeight in the grid controller's defaultColDef).
   A cell shows two lines and scrolls whatever is longer, rather than stretching
   one row out of line with the rest of the table. AG Grid sizes the row as
   "height of the cell's content element + the cell's own padding and border", so
   the two-line box has to be pinned on that content element — `.ag-cell-wrapper`
   or `.ag-cell-value`, depending on the cell — and the padding here is what
   brings the row back to the height the fixed row height gave it before.

   The box is the taller of two measures, not just the typographic one. Lines ×
   leading says how much text a cell shows; --qc-grid-control-h says how tall
   the things that are not text need the row to be — a status pill, the operator
   initials, the selection box, an action button. Compact used to be the first
   measure alone, at 1 × 1.3 × 14px = 18.2px, and all four were cut along their
   bottom edge: a badge cannot shrink into it either, `flex: none` below forbids
   it, and `safe center` gives up and aligns to the top once the content
   overflows, which is what put the cut where it showed.

   The floor is on every auto-height cell rather than only on the ones holding a
   control, for two reasons. A table row is one height across all its columns,
   so deriving it from whichever column happens to carry a pill makes the layout
   depend on the data. And the actions column is `autoHeight: false` — it never
   takes part in the measurement, so it can only inherit whatever the other
   columns decide; the only way to make room for its button is to make room in
   them. */
/* A server-paged grid has no auto-height cells — the infinite model has one row
   height — so its cells are named here beside them and keep the same box, which
   is exactly what `--qc-grid-row-h` is computed from. */
.qc-grid__grid .ag-cell.ag-cell-auto-height,
.qc-grid--paged .qc-grid__grid .ag-cell:not(.qc-cardcell) {
  padding-top: var(--qc-grid-cell-py);
  padding-bottom: var(--qc-grid-cell-py);
}
.qc-grid__grid .ag-cell.ag-cell-auto-height > .ag-cell-wrapper,
.qc-grid__grid .ag-cell.ag-cell-auto-height > .ag-cell-value {
  /* Declared and used on the same element, so the `em` resolves against this
     element's own size — the grid's --qc-grid-data-font-size, inherited from
     .ag-row, with nothing in between resetting it. A custom property carrying an
     `em` is substituted as a token and would re-resolve against a descendant
     that read it; nothing below reads it, which is the only way that could go
     wrong.

     --qc-grid-cell-scroll-h is the third measure, and it is zero everywhere but
     the compact density: a scrollbar the browser reserves room for is drawn
     *inside* this box, so a box sized to exactly one line has one line minus a
     scrollbar left to show it in — which is how the compact cell came to cut
     the very line it had just been taught to scroll. */
  --qc-grid-cell-box-h: max(
    calc(var(--qc-grid-cell-lines) * 1.3em + var(--qc-grid-cell-scroll-h)),
    var(--qc-grid-control-h));
  min-height: var(--qc-grid-cell-box-h);
  max-height: var(--qc-grid-cell-box-h);
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--qc-border) transparent;
}
/* ---- No blank line where a <br> was ----
   Some values carry a second piece: the two-line renderer builds
   <b>primary</b><br><small>secondary</small>, the GPS cell a few <div>s. Where
   the value element is the flex box above, every one of those is a flex item —
   the <br> included, an empty item that spent one of the box's two lines on
   nothing and pushed the secondary out of the box, unseen. A flex column
   already stacks what it holds; the <br> has nothing left to say there.

   Only there, though: nested inside .ag-cell-wrapper the value stays an
   ordinary block with inline content, and in that shape the <br> *is* the line
   break. Hence the child combinator — the compact block below hides it in both
   shapes, because compact has no second line to break to. */
.qc-grid__grid .ag-cell.ag-cell-auto-height > .ag-cell-value > br { display: none; }
/* Same vertical centering the cell itself uses, one level down: the content
   element is taller than a single line, so without it a short value would ride
   the top. `safe` keeps the first line reachable once the content scrolls —
   plain centering pushes it above the scroll origin, out of reach.
   Horizontally the pieces sit where the cell's own alignment puts them, and
   text-align carries the same choice into wrapped text. */
.qc-grid__grid .ag-cell.ag-cell-auto-height > .ag-cell-value {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  align-items: safe center;
  justify-content: flex-start;
  gap: .4em;
}
.qc-grid__grid .ag-cell.qc-al-end.ag-cell-auto-height > .ag-cell-value,
.qc-grid__grid .ag-cell.ag-right-aligned-cell.ag-cell-auto-height > .ag-cell-value {
  justify-content: flex-end;
}
.qc-grid__grid .ag-cell.qc-al-center.ag-cell-auto-height > .ag-cell-value {
  justify-content: center;
}

/* ---- Compact: one line, and everything the cell holds is on it ----
   A compact cell is a single line, so nothing in it may wrap. Scoping that to
   .ag-cell-wrap-text was not enough: the class only says what the column asked
   for, and a value carries markup of its own — a <b>, a <div>, whatever a
   renderer or a summary built on the server put there — which the rule never
   reached. What was meant to be one line came out as two, and the second was
   cut by the box. Every element inside the cell, then, not just the two the
   column happens to own. */
[data-qc-density="compact"] .qc-grid__grid .ag-cell,
[data-qc-density="compact"] .qc-grid__grid .ag-cell * {
  white-space: nowrap;
}
[data-qc-density="compact"] .qc-grid__grid .ag-cell.ag-cell-wrap-text {
  overflow: hidden;
}
/* The pieces lie along the line rather than stacking: the customer and the
   address, the label and the code, the readings of a GPS cell. Wherever the
   value sits — its own box, or nested in the wrapper the selection column adds
   — it is the flex row here, so both shapes read as one line and the <br>
   between the pieces goes away in both.

   Every cell, not only the auto-height ones. The rules above this block are
   scoped to .ag-cell-auto-height because they exist to *measure* the row, and
   only an auto-height cell is measured. That scope was carried down here by
   habit, and it was wrong: a cell AG Grid sizes itself is still a cell showing
   two facts, and on the works list it was the one showing them stacked with the
   second sliced in half along its bottom edge. What a column asked for decides
   who reports the row height; it does not decide whether a compact cell is one
   line. The two exceptions are cells that are not lines at all: the phone card
   and the action buttons, each of which lays itself out.

   What a column expressed as align-items a row expresses as justify-content,
   and there is nothing to say: the value box is only as wide as its content,
   so the cell has already placed it left, right or centre. Vertical centring
   becomes align-items, keeping the `safe` guard for the reason it had it
   before. */
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .ag-cell-value,
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .ag-cell-value > * {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  align-items: safe center;
  /* Restated, not inherited: the column form above says `safe center` on this
     same property, and along a row that would centre the pieces horizontally —
     and, once the value overflows, park the scroll at the middle of itself. */
  justify-content: flex-start;
  gap: .4em;
}
/* ---- One level down, because that is where the pieces are ----
   A cellRenderer that returns a string does not have its markup put into
   .ag-cell-value: AG Grid wraps it in a <span> of its own first, so the real
   shape is
       .ag-cell-value > span > (<b>, <br>, <small>)
   and every rule written against the value's own children was landing on that
   one anonymous span. The value became a flex row of a single item, the item
   stayed a block, and the two lines went on stacking inside it — visibly, on
   every column that shows two facts, in every list in the app.

   So the row is laid on the value *and* on whatever it holds, and the <br> is
   hidden wherever it sits rather than only where it happened to be a child.
   The gap does the separating either way: hidden, the pieces on both sides of
   the <br> become adjacent flex items and the gap falls between them — which
   also covers the renderers whose second piece is bare text with no element to
   hang a margin on (a date over its hours). */
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .ag-cell-value br {
  display: none;
}
/* ---- The second piece that does not come along ----
   Laying the two lines side by side works while the second piece belongs to the
   first — a code beside its label, an hour beside its date. Where it is only
   context, though (the address under a customer's name), the line it makes is
   twice as long as the fact anybody scans the column for, and the name is what
   gets squeezed. Such a column says so itself: `compactHidesSecondary` on the
   twoLine renderer marks its <small> and the compact density drops it. The
   element is still rendered, so the quick search matches it either way, and the
   address has a column of its own for whoever wants to see it here.

   The element selector is not decoration: the flex rules above land on every
   child of the value with five classes of specificity, and this has to outrank
   them wherever the piece sits — directly under the value, or inside the span
   AG Grid wraps a string renderer's markup in. */
[data-qc-density="compact"] .qc-grid__grid .ag-cell .ag-cell-value small.qc-cell__aside {
  display: none;
}
/* ---- A pill at the column's edge (twoLine's `stateAtEnd`) ----
   The value box is as wide as its content, so the pill would sit wherever the
   name happened to end. The cell spans its column first (`qc-cell--ends`, put
   on the cell by the renderer), then the name and its line take what is left
   and end in an ellipsis, and the pill keeps its width at the right edge — the
   same column of marks down every row that has one. */
/* The value is either the cell itself (a cell with no wrapper) or a child of it,
   and AG Grid puts a string renderer's markup in a <span> of its own. */
.qc-grid__grid .ag-cell.qc-cell--ends > .ag-cell-value,
.qc-grid__grid .ag-cell.qc-cell--ends > span,
.qc-grid__grid .ag-cell.qc-cell--ends > .ag-cell-value > span { flex: 1 1 auto; width: 100%; min-width: 0; }
/* That <span> fills the row's height; the name is centred inside it. */
.qc-grid__grid .ag-cell.qc-cell--ends > span,
.qc-grid__grid .ag-cell.qc-cell--ends > .ag-cell-value > span { display: flex; align-items: center; }
.qc-grid__grid .ag-cell.qc-cell--ends .qc-cell__ends {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: .6rem;
  width: 100%;
  min-width: 0;
}
.qc-grid__grid .ag-cell.qc-cell--ends .qc-cell__ends-text {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}
.qc-grid__grid .ag-cell.qc-cell--ends .qc-cell__ends-text > * {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* A name with no second line stays centred in the row. */
.qc-grid__grid .ag-cell.qc-cell--ends .qc-cell__ends-text > small:empty { display: none; }
.qc-grid__grid .ag-cell.qc-cell--ends .qc-cell__ends > .qc-stpill { flex: none; margin-left: auto; }
/* Compact lays the two lines side by side, as everywhere else. */
[data-qc-density="compact"] .qc-grid__grid .ag-cell.qc-cell--ends .qc-cell__ends-text { flex-direction: row; gap: .4em; }

/* The gap is the row's; the margins the utility classes bring (`.mt-1` on a GPS
   line) were written for a stack and here only tilt the line off centre. */
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .ag-cell-value > * {
  margin-top: 0 !important;
  margin-bottom: 0 !important;
}
/* A renderer that builds a little layout of its own stacks it, and the rule
   above only reaches the pieces the value element holds directly. The notes
   preview is the one that does: a column of preview text over a "show all"
   button, both of which fell outside a one-line box. Its own rules live in a
   page <style> (templates/base/_work.hml.twig); this is more specific than any
   of them, so the column becomes a row here without that file having to know
   about the density. Anything else that grows a second dimension inside a cell
   belongs on this list too. */
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .notes-preview-wrap,
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .notes-preview {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: .4em;
  height: auto;
}
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .notes-preview-btn {
  /* `margin-top: auto` pushed the button to the bottom of the column it used to
     sit in; along a row it only drags it off the line. */
  margin-top: 0;
}

/* ---- And what does not fit scrolls, rather than being cut ----
   An ellipsis is honest about a name it shortens, but it can only be honest
   about one piece: the value is a row of them, so each would have to own its
   own ellipsis, and a piece that owns one can no longer say how much room it
   wants. A thin bar under the line says the same thing about the whole value
   and gives all of it back.

   Nothing is squeezed to make that happen. `min-width: auto` on a flex item is
   its min-content width, and nowrap text has no measure smaller than the whole
   string — so every piece keeps its natural width, the row overflows, and the
   overflow is the thing to scroll. Which is why no piece here may be given
   `min-width: 0`: that one declaration turns the scroll back into a squeeze,
   and the second piece comes to sit on top of the first.

   Only the horizontal axis is offered — one line inside a box at least a line
   tall has nothing to scroll vertically — and the cell keeps clipping so a long
   value cannot run under the column beside it. max-width rather than width, so
   the unconstrained layout autoSizeColumns measures still reports real
   content. */
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) > .ag-cell-wrapper {
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
}
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .ag-cell-value {
  max-width: 100%;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  text-overflow: clip;
  scrollbar-width: thin;
  scrollbar-color: var(--qc-border) transparent;
}
/* The bar is chrome, not content: 6px of it, in the divider colour, on a track
   that is not drawn at all. Firefox reads `scrollbar-width: thin` above and
   reserves about ten — which is what --qc-grid-cell-scroll-h makes room for, so
   that the line a bar appears under is not the line the bar then covers. */
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .ag-cell-value::-webkit-scrollbar {
  height: 6px;
}
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .ag-cell-value::-webkit-scrollbar-track {
  background: transparent;
}
[data-qc-density="compact"] .qc-grid__grid .ag-cell:not(.qc-cardcell, .qc-actions-cell) .ag-cell-value::-webkit-scrollbar-thumb {
  background: var(--qc-border);
  border-radius: 3px;
}

/* The wrapper form of the cell holds the selection checkbox beside the value
   (work list), so it keeps the row direction it already has — only the vertical
   alignment needs the same safe centering. */
.qc-grid__grid .ag-cell.ag-cell-auto-height > .ag-cell-wrapper {
  align-items: center;
  align-items: safe center;
}
/* Everything AG Grid puts in that wrapper other than the value element — here,
   the selection tick box — it hands a height derived from its own rowHeight
   parameter. These grids never use that parameter: the row is measured from the
   cell box instead. The two agreed to within a pixel at the comfortable size and
   disagreed by nine at the compact one, which is how the tick box came to be
   shaved along its bottom edge even before the box had a floor. Let the box be
   its own size; the rule above centres it. */
.qc-grid__grid .ag-cell.ag-cell-auto-height > .ag-cell-wrapper > :not(.ag-cell-value) {
  height: auto;
}
/* Selection column. AG Grid gives both the header "select all" box and the row
   boxes a right margin — meant to gap a checkbox from the value beside it, which
   a checkbox-only column has none of — so it just shoves them off centre.
   Note the extra `.ag-header-cell` / `.ag-cell` class: the vendor sheet's
   `.ag-ltr .ag-*` rules load after this one, so we need the higher specificity to
   win — this was why plain equal-specificity rules had no effect. */
.qc-grid__grid .ag-cell .ag-selection-checkbox {
  margin: 0;
}
/* The header box is placed by the header's own layout rather than by its cell:
   the label beside it is `flex: 1 1 auto` and, empty as it is here, still takes
   every spare pixel, pushing the box against one edge. Auto margins cannot win
   that — there is no free space left for them to divide. So it is centred on the
   cell instead, which the header cell can carry because AG Grid positions it.
   The rows centre their box in their cell, so the two now line up by
   construction rather than by both happening to land in the same place.
   It took the padding going to show: 1rem in front of the box had been holding
   it within about three pixels of the rows' centre, close enough to look right. */
.qc-grid__grid .ag-header-cell[col-id="ag-Grid-SelectionColumn"] .ag-header-select-all {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  margin: 0;
  /* Above the label: the label's wrapper (`.ag-header-cell-comp-wrapper`,
     relative, z-index 1) follows the box in the DOM and so won the tie,
     taking every click — the "select all" looked tickable and did nothing.
     The job list never showed it: a server-paged grid has no header box. */
  z-index: 2;
}
/* And the rows do the same, which is the half that was missing: the header box
   was centred on its cell while every row box sat flush against the left edge,
   because a cell is a column flexbox and the app-wide default for its cross
   axis is flex-start. Nothing said otherwise for this column, so the two were
   never lined up — 68 pixels apart at the width the works list gives it.

   align-items rather than a second absolute placement: a cell already puts its
   contents where the column asks, and this column asks for the centre. Both
   boxes now take their position from the same cell edges, so they stay together
   whatever that cell is later resized to. */
.qc-grid__grid .ag-cell[col-id="ag-Grid-SelectionColumn"] {
  align-items: center;
}
/* The column also needs its whole width to sit the box in. The general cell
   padding is 1rem a side, which of a 46px column leaves 14px — and the box is
   20px, so it was clipped on both edges however well it was centred. Every other
   column wants that padding to keep its text off the gridlines; this one holds
   nothing but the box, and has nothing to be kept off anything. */
.qc-grid__grid .ag-header-cell[col-id="ag-Grid-SelectionColumn"],
.qc-grid__grid .ag-cell[col-id="ag-Grid-SelectionColumn"] {
  padding-inline: 0;
}
/* In-page tooltip for in-cell elements (action icons, service/vehicle pills,
   operator avatars). Appended to <body> so it is not clipped by cell overflow. */
.qc-tip {
  position: fixed;
  transform: translate(-50%, calc(-100% - 8px));
  z-index: 10000;
  pointer-events: none;
  padding: .25rem .5rem;
  border-radius: 8px;
  font: 600 var(--qc-fs-xs) var(--qc-font-body);
  line-height: 1.2;
  white-space: nowrap;
  color: var(--qc-surface);
  background: var(--qc-text-primary);
  box-shadow: var(--qc-shadow-lg);
}
.qc-tip::after {
  content: "";
  position: absolute;
  left: 50%; top: 100%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: var(--qc-text-primary);
}

/* A row's warning mark, before its name: what the server found wrong with the
   record. An icon in a tinted disc, never colour alone — the accessible name
   is the count, and the list opens on hover, on focus and on a press. Dark
   amber on the tint keeps the glyph readable on both themes; the tint says
   how pressing. The disc is exactly one line of the cell tall (1.3em, the
   leading the row height is computed from): taller, and the cell's clamp
   cut its top off. */
.qc-rowflag {
  --sev: var(--qc-status-yellow);
  display: inline-grid;
  place-items: center;
  width: 1.3em;
  height: 1.3em;
  margin: 0 .4rem 0 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  vertical-align: top;
  line-height: 1;
  cursor: help;
  color: var(--qc-warning-text);
  background: color-mix(in srgb, var(--sev) 26%, var(--qc-surface));
}
.qc-rowflag .ti { font-size: .85em; }
.qc-rowflag.is-late, .qc-flagpop li.is-late { --sev: var(--qc-status-orange); }
.qc-rowflag.is-note, .qc-flagpop li.is-note { --sev: var(--qc-info); }
.qc-flagpop li.is-soon { --sev: var(--qc-status-yellow); }
.qc-rowflag.is-late { box-shadow: inset 0 0 0 1.5px var(--sev); }
.qc-rowflag:hover,
.qc-rowflag[aria-expanded="true"] { background: color-mix(in srgb, var(--sev) 44%, var(--qc-surface)); }
.qc-rowflag:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: 2px; }

/* Its list. On <body>, like the tips: a cell clips. No blur. */
.qc-flagpop {
  position: fixed;
  z-index: 10000;
  width: max-content;
  max-width: min(22rem, calc(100vw - 16px));
  padding: .5rem;
  border: 1px solid var(--qc-edge);
  border-radius: var(--qc-radius-input);
  background: var(--qc-surface-overlay);
  box-shadow: var(--qc-shadow-lg), var(--qc-shadow-top);
  /* It only says: nothing in it is pressed, so the pointer passes through and
     leaving the mark is leaving the list. */
  pointer-events: none;
}
.qc-flagpop ul { display: grid; gap: .3rem; margin: 0; padding: 0; list-style: none; }
.qc-flagpop li {
  display: grid;
  grid-template-columns: 10px minmax(0, 1fr);
  align-items: start;
  gap: .55rem;
  padding: .4rem .55rem;
  border-radius: 8px;
  font: 500 var(--qc-fs-sm)/1.3 var(--qc-font-body);
  color: var(--qc-text-primary);
  background: color-mix(in srgb, var(--sev) 10%, transparent);
}
.qc-flagpop li > i {
  width: 10px;
  height: 10px;
  margin-top: .25rem;
  border-radius: 50%;
  background: var(--sev);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--sev) 25%, transparent);
}

/* In-cell content: status badges keep their pill look; numeric = tabular.
   Badges never wrap and never shrink below their text: a status label is free
   text somebody types and can be a whole sentence — the accounted one shipped
   as one until it became "Lavori contabilizzati" — and a wrapped pill loses its
   second line to the row height. The column is auto-sized to the widest badge
   (_fitColumns), so the full label is readable. */
.ag-cell .badge {
  font-size: var(--qc-grid-badge-font-size);
  vertical-align: middle;
  white-space: nowrap;
  flex: none;
  max-width: none;
}
.ag-cell.qc-numeric,
.ag-cell .qc-numeric { font-variant-numeric: tabular-nums; }

/* Status badge ("All works"): keep the label on a single line, so a long status
   does not wrap to two rows inside the cell. It used to buy that room by
   dropping to eleven pixels; a status is one of the few words on a row that has
   to be read, so it keeps the floor and the column gives up the width. */
.qc-grid__grid .qc-status-cell .badge {
  white-space: nowrap;
  text-align: center;
  line-height: 1.2;
  font-size: var(--qc-fs-xs);
  padding-inline: .5em;
}

/* Action buttons rendered inside a cell — comfortable hit targets, brand
   outline, visible hover/focus (replaces the cramped default btn-sm look). */
.qc-grid-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--qc-grid-action-gap);
  height: 100%;
  /* Buttons stay on one line; the column is auto-sized to the widest row (see
     _fitColumns) so nothing is ever clipped. */
  flex-wrap: nowrap;
  white-space: nowrap;
}
/* Actions cell: full width so the button group centers with the rest of the
   table, and no ellipsis clipping of the group itself. */
.qc-grid__grid .qc-actions-cell {
  align-items: center;
  text-overflow: clip;
}
.qc-grid-actions .btn {
  --tblr-btn-padding-y: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The same token the cell box takes as its floor, so a row and the button it
     carries can no longer disagree about how tall a row is. The vertical padding
     goes with it: beside a height that moves with the density, a fixed padding is
     the other half of the pair that squashed the button. The line height is
     pinned for the same reason — the button's height is a decision this file
     makes, not one it inherits from Tabler's .btn-sm. */
  min-height: var(--qc-grid-control-h);
  padding: 0 var(--qc-grid-action-px);
  line-height: 1.2;
  border-radius: var(--qc-radius-btn);
  border-color: var(--qc-border);
  color: var(--qc-text-secondary);
  background: var(--qc-surface);
  transition: background var(--qc-dur-fast) var(--qc-ease-fast),
              border-color var(--qc-dur-fast) var(--qc-ease-fast),
              color var(--qc-dur-fast) var(--qc-ease-fast);
}
/* No real blur inside a table. The glass buttons (`.btn-outline-*`,
   components.css) carry a backdrop-filter, and a row repeats them: six per job,
   hundreds of rows, and every one is a compositing layer of its own. Past a few
   hundred the GPU compositor gives up and drops whole tiles — customer names
   and buttons vanished on /work/all while scrolling, with every one of them
   still in the DOM. Over an opaque row the blur shows nothing anyway, and brand
   rule 3 already caps real blur at a handful per view. The tint and the border
   carry the recipe here. */
.qc-grid__grid .btn,
.qc-grid__grid [class*="qc-btn-"],
.qc-rowcard .btn {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.qc-grid-actions .btn:hover {
  background: color-mix(in srgb, var(--qc-primary) 8%, var(--qc-surface));
  border-color: color-mix(in srgb, var(--qc-primary) 40%, transparent);
  color: var(--qc-primary-ink);
}
/* A button: the outline, not the field's halo (components.css, top). */
.qc-grid-actions .btn:focus-visible {
  outline: 2px solid var(--qc-primary);
  outline-offset: 1px;
  border-color: var(--qc-primary);
}
/* Icon-only action button (the actions renderer drops the label whenever a
   button has an icon): square, icon centered. */
.qc-grid-actions__icon {
  /* Square, so it follows the height. 28x28 at the compact density, which keeps
     it above the 24x24 CSS px WCAG 2.2 SC 2.5.8 asks of a pointer target. */
  width: var(--qc-grid-control-h);
  padding-left: 0 !important;
  padding-right: 0 !important;
}
/* pointer-events:none so the hover/tooltip target is the button itself, not the
   inner SVG — otherwise the browser won't surface the button's title tooltip. */
.qc-grid-actions .btn svg {
  display: block;
  width: var(--qc-grid-action-icon);
  height: var(--qc-grid-action-icon);
  pointer-events: none;
}
/* The same size for a font icon (`<i class="ti ti-pencil">`, what most pages
   pass as `iconHtml`) as for the job list's inline SVGs: left to the font it
   drew at the button's small text size, a glyph lost in a 28px square. */
.qc-grid-actions .btn .ti {
  display: block;
  font-size: var(--qc-grid-action-icon);
  line-height: 1;
  pointer-events: none;
}
/* Tabler tucks an icon towards the left of its button and gaps it from the label
   that follows — `margin: 0 padding-x/2 0 padding-x/-4`, six pixels of deliberate
   asymmetry. A button of nothing but an icon has no label to be gapped from, so
   all that margin does is stand the glyph off centre: it was there at 36px too,
   quietly, and became plain the moment the button was 28. The buttons that do
   carry an icon beside a label — the `link` renderer's — keep the tuck, which is
   what it was written for. */
.qc-grid-actions .qc-grid-actions__icon .icon,
.qc-grid-actions .qc-grid-actions__icon .ti,
.qc-grid-actions .qc-grid-actions__icon svg {
  margin: 0;
}
/* Semantic outline buttons keep their intent color on hover. */
.qc-grid-actions .btn-outline-success:hover {
  background: color-mix(in srgb, var(--qc-success) 12%, var(--qc-surface));
  border-color: var(--qc-success); color: var(--qc-success);
}
.qc-grid-actions .btn-outline-danger:hover {
  background: color-mix(in srgb, var(--qc-error) 12%, var(--qc-surface));
  border-color: var(--qc-error); color: var(--qc-error-text);
}
.qc-grid-actions .btn-outline-warning:hover {
  background: color-mix(in srgb, var(--qc-warning) 16%, var(--qc-surface));
  border-color: var(--qc-warning); color: var(--qc-warning);
}
@media (prefers-reduced-motion: reduce) {
  .qc-grid-actions .btn { transition: none; }
}

/* Loader / empty states FLOAT over the grid host.
   The grid element stays in the layout at its measured width for the whole load
   cycle — it is never display:none — so revealing fresh data never makes AG Grid
   re-flow its flex columns (the right-to-left "slide" glitch on work-tab change).
   While busy, `.is-busy` reserves vertical room so the centered loader/empty art
   isn't clipped by the header-only height of a still-empty grid. */
.qc-grid__overlay {
  position: relative;
}
.qc-grid__overlay.is-busy {
  min-height: 320px;
}
.qc-grid__overlay > [data-ag-grid-target="loading"]:not([hidden]),
.qc-grid__overlay > [data-ag-grid-target="empty"]:not([hidden]) {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2rem 0;
  background: var(--qc-surface);
  border-radius: var(--qc-radius-card);
}
/* The rings loader sits ABOVE the grid (and the empty state) and covers it
   opaquely while AG Grid paints + settles the rows underneath; on reveal it
   fades out to uncover the finished table — no layout jitter is ever seen. */
.qc-grid__overlay > [data-ag-grid-target="loading"]:not([hidden]) {
  z-index: 6;
  opacity: 1;
  transition: opacity var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-grid__overlay > [data-ag-grid-target="loading"].is-fading { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .qc-grid__overlay > [data-ag-grid-target="loading"]:not([hidden]) { transition: none; }
}

/* ---- Server-paged grids (the Infinite Row Model) ----
   The grid has its own height (the controller fills the screen with it) and
   scrolls inside itself, so the busy floor an empty auto-height grid needs is
   not wanted. Every row is one height: the card on a phone is clamped to it,
   title to two lines and each field to one. */
.qc-grid--paged .qc-grid__overlay.is-busy,
.qc-grid--fill .qc-grid__overlay.is-busy { min-height: 0; }

/* ---- A grid that fills the box it is put in (`fill_parent`) ----
   For a container with a height of its own — a modal's body: the grid is the
   rest of that height, scrolls inside it, and never measures the window. The
   count line is the container's to say, so it goes. */
.qc-grid--fill-parent { height: 100%; min-height: 0; }
.qc-grid--fill-parent .qc-grid__overlay {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
}
.qc-grid--fill-parent .qc-grid__grid { flex: 1 1 auto; min-height: 0; }
.qc-grid--fill-parent .qc-grid__more { display: none; }
/* The text of a cell stops at `--qc-grid-cell-lines` lines with an ellipsis —
   the row cannot grow to it — and the tooltip carries the whole value. The
   clamp is on the cell's content (the grid gives plain columns a <span> of
   their own in this mode), so the cell keeps centring it — and only on
   content that is text: plain, or the lines of a two-line cell. A row of
   chips (the crew's initials, the service pills) laid out as a clamped box
   stacks one chip per line; badges and the action buttons are left alone.
   The row's warning mark sits in the first line and counts as text: left out
   of this list, it switched the clamp off and the address ran to a third,
   cut line. */
.qc-grid--paged .qc-grid__grid .ag-cell-wrap-text > span:not(.badge):not(.qc-grid-placeholder):not(:has(> :not(b, strong, em, i, br, small, .qc-rowflag))) {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--qc-grid-cell-lines);
  overflow: hidden;
  min-width: 0;
}
/* A two-line value (twoLine: <b>name</b><br><small>address</small>) is two
   lines of one each, not one box of two: clamped as a whole, a name that
   wrapped took both lines, ended in "…" and pushed the address out of sight
   ("Agriturismo Al / Castagno…"). Each line ends in its own ellipsis — an
   e-mail too long for the column included, which was cut mid-word. */
.qc-grid--paged .qc-grid__grid .ag-cell-wrap-text > span:has(> small):not(.badge):not(.qc-grid-placeholder):not(.qc-cell__ends) {
  display: block;
  -webkit-line-clamp: unset;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.qc-grid--paged .qc-rowcard {
  height: 100%;
  overflow: hidden;
}
.qc-grid--paged .qc-rowcard__title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
/* If anything has to give it is the fields, never the buttons: the meta block
   takes what is left and clips, the head and the actions keep their height. */
.qc-grid--paged .qc-rowcard__meta {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  align-content: start;
}
.qc-grid--paged .qc-rowcard__head,
.qc-grid--paged .qc-rowcard__actions { flex: none; }
/* One line a field. A two-line value (a phone over an email) is laid out on
   that one line rather than broken by its <br>. */
.qc-grid--paged .qc-rowcard__pair dd {
  white-space: nowrap;
  overflow: hidden;
  overflow-wrap: normal;
  text-overflow: ellipsis;
}
.qc-grid--paged .qc-rowcard__pair dd br { display: none; }
.qc-grid--paged .qc-rowcard__pair dd > span,
.qc-grid--paged .qc-rowcard__pair dd small { display: inline; }
.qc-grid--paged .qc-rowcard__pair dd small { margin-left: .35em; }
/* A row whose block is still arriving. */
.qc-grid-placeholder {
  display: inline-block;
  width: 70%;
  height: .75rem;
  vertical-align: middle;
}
.qc-rowcard--placeholder .qc-grid-placeholder { display: block; height: .9rem; }
.qc-rowcard--placeholder .qc-grid-placeholder + .qc-grid-placeholder { width: 45%; }

/* The end of the list, where the pagination panel used to be: how far the list
   has got on the left, and the button that asks for more on the right — which
   is also what reaching this line does on its own. The small rings stand in
   for the button while a block is on its way. */
.qc-grid__more {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  min-height: 2.25rem;
  padding: 0 .25rem;
  color: var(--qc-text-secondary);
  font-size: var(--qc-grid-font-size);
}
.qc-grid__more[hidden] { display: none; }
.qc-grid__more-loading { margin-left: auto; }
.qc-grid__more-loading[hidden] { display: none; }
.qc-grid__more-loading .qc-loader__label { color: var(--qc-text-secondary); }

/* ==========================================================================
   Phone: the row becomes a card
   Below 768px the grid draws every row full width (isFullWidthRow in the grid
   controller) and these are the cards it draws. Nothing about the data changes
   — the same renderers paint the same badges, pills and buttons — so what is
   styled here is only the arrangement: an identifying title and its status on
   one line, the fields that matter under it as label/value pairs, the actions
   at the foot. No horizontal scrolling anywhere.
   ========================================================================== */
/* The header is collapsed, not removed. `display: none` on it takes the whole
   body down with it — the rows keep their computed 3300px but every ancestor
   renders at zero — because the header is what the grid lays the body out
   against. Zero height plus overflow:hidden leaves nothing visible and the
   layout intact; headerHeight: 0 (set on the grid in card mode) does the rest. */
/* Nothing structural is taken out of the flow: `display: none` on the header
   collapsed the whole body with it, and setting overflow on one axis made the
   browser compute `auto` on the other, turning a container that grew with its
   content into a scroll box of its own. Both are collapsed to nothing instead.
   headerHeight: 0, set on the grid in card mode, does most of this already. */
.qc-grid__grid--cards .ag-header {
  height: 0;
  min-height: 0;
  overflow: hidden;
  border: 0;
}

/* The card column is the row: no padding, no alignment and no clamp of its
   own. The card inside reports its height to the grid (see _cardRenderer), and
   everything between the cell and the card has to pass that height through. */
.qc-grid__grid--cards .ag-cell.qc-cardcell {
  padding: 0;
  align-items: stretch;
  text-align: left;
  overflow: visible;
  white-space: normal;
}
.qc-grid__grid--cards .ag-cell.qc-cardcell .ag-cell-wrapper,
.qc-grid__grid--cards .ag-cell.qc-cardcell .ag-cell-value {
  height: auto;
  min-height: 0;
  max-height: none;
  overflow: visible;
  display: block;
  white-space: normal;
}
/* A card wraps whatever the density preference says. The compact rule above
   reaches every element inside a cell — it has to, a value brings markup of its
   own — and a card is inside a cell too; but a card is not a row, it has no
   column to run out of, and a title held to one line there would just be cut.
   Same shape of selector, one class more specific, so it wins. */
.qc-grid__grid--cards .ag-cell.qc-cardcell,
.qc-grid__grid--cards .ag-cell.qc-cardcell * {
  white-space: normal;
}

.qc-rowcard {
  /* A card is a touch surface whatever the density preference says: a finger is
     the same size on a compact table as on a comfortable one, and a card has the
     width to spend that a desktop row has not. One token put back and every
     control inside follows — button height, icon-only width — so nothing in this
     block has to restate a rule. The type stays at its compact size: what has to
     come back is the target, not the density. */
  --qc-grid-control-h: var(--qc-grid-control-h-full);
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  width: 100%;
  padding: .875rem 1rem;
  border-bottom: 1px solid var(--qc-divider);
  background: var(--qc-surface);
  font-size: var(--qc-grid-data-font-size);
  line-height: 1.35;
}
/* Row state, as the leading bar it is on a desktop row. */
.qc-rowcard.table-success { box-shadow: inset 3px 0 0 var(--qc-success); }
.qc-rowcard.table-warning { box-shadow: inset 3px 0 0 var(--qc-status-orange); }
.qc-rowcard.table-danger { box-shadow: inset 3px 0 0 var(--qc-error); }
.qc-rowcard.table-secondary { box-shadow: inset 3px 0 0 var(--qc-border); }

.qc-rowcard__head {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
}
.qc-rowcard__title {
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 600;
  color: var(--qc-text-primary);
  overflow-wrap: anywhere;
}
/* The title is often a two-line cell; here the second line is the subtitle. */
.qc-rowcard__title b { font-weight: 600; }
.qc-rowcard__title small {
  display: block;
  font-weight: 400;
  font-size: var(--qc-grid-cell-small-size);
  color: var(--qc-text-tertiary);
}
.qc-rowcard__badge { flex: none; }

.qc-rowcard__meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: .375rem .75rem;
  margin: 0;
}
.qc-rowcard__pair { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.qc-rowcard__pair dt {
  font-size: var(--qc-fs-xs);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--qc-text-tertiary);
}
.qc-rowcard__pair dd {
  margin: 0;
  min-width: 0;
  color: var(--qc-text-secondary);
  overflow-wrap: anywhere;
}
.qc-rowcard__pair dd .qc-numeric { font-variant-numeric: tabular-nums; }

.qc-rowcard__actions {
  display: flex;
  justify-content: flex-end;
  padding-top: .125rem;
}
.qc-rowcard__actions .qc-grid-actions { height: auto; }

/* Sorting, for a layout with no column headers to click. Shown only in card
   mode; the control is built once by the grid controller. */
.qc-grid__cardsort {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
}
.qc-grid__cardsort[hidden] { display: none; }
.qc-grid__cardsort-select {
  height: var(--qc-control-h);
  max-width: 12rem;
  padding: 0 .6rem;
  border-radius: var(--qc-radius-input);
  border: 1px solid var(--qc-border);
  background: var(--qc-field-bg);
  color: var(--qc-text-primary);
  font: 600 var(--qc-fs-md) var(--qc-font-body);
}
.qc-grid__cardsort-select:focus {
  outline: none;
  border-color: var(--qc-primary);
  box-shadow: var(--qc-focus-ring);
}
/* Once Tom Select has taken the select over, the control is its wrapper. */
.qc-grid__cardsort .ts-wrapper {
  width: 12rem;
  max-width: 100%;
}
.qc-grid__cardsort-dir {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--qc-control-h);
  height: var(--qc-control-h);
  flex: none;
  cursor: pointer;
  color: var(--qc-primary);
  background: var(--qc-layer-wash);
  border: 1px solid transparent;
  border-radius: var(--qc-radius-btn);
}
.qc-grid__cardsort-dir:hover { background: var(--qc-layer-highlight); }
.qc-grid__cardsort-dir.is-desc {
  background: var(--qc-primary);
  color: var(--qc-on-primary);
}

/* A badge never leaves its cell. Centred and wider than the column, it used
   to spill over both neighbours and sit on top of the next badge; it now ends
   in an ellipsis, the whole label as its title (renderers.js, `badge`). */
.qc-grid__grid .ag-cell .badge {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: middle;
}

/* ---- A ledger (`variant: 'ledger'`) ----------------------------------------
   A register read down its columns — the customer sheet. Everything here is
   scoped to the variant, so the other tables keep the brand's horizontal-only
   rule and their stripes.

   - The header is one small uppercase line, the way the hand-written <thead>
     it replaced drew it: its headings are labels, not controls.
   - Columns are ruled, by one set of lines. `columnBorder` stays off in the
     theme for every other grid. The heading draws its edge as an inset shadow;
     the body and the totals do not draw theirs cell by cell — a cell is a
     pixel short of its row, so a rule per cell broke at every row and sat a
     pixel off the heading's. Instead the rows are see-through and the lines
     are the background of the rows area and of the totals strip, placed at
     the heading's own edges (ag_grid_controller#_ruleEmpty). One source, one
     position, top to bottom — through the rows and on down the empty band,
     which is ruled into blank rows as well.
   - One surface, the card's own, and no stripes: the ruling is the structure.
     The header and the pinned totals sit in the sunken well (`--qc-field-bg`,
     which is the darker surface in *both* themes — the two themes swap which
     surface is raised, and naming one by surface is how the dark grid ended up
     a box of a different grey inside its card, its odd rows the card's colour
     and its totals row indistinguishable from the ground around it).
   - In the dark theme the hairline is a shade off the surface itself, so it is
     lifted towards the tertiary ink, as the job sheet's tiles do. */
.qc-grid--ledger {
  --qc-grid-header-h: 30px;
  --qc-ledger-bg: var(--qc-card-bg);
  --qc-ledger-well: var(--qc-field-bg);
  --qc-ledger-rule: var(--qc-border);
  --qc-ledger-invoice: color-mix(in srgb, var(--qc-status-teal) 15%, transparent);
  --qc-ledger-commission: color-mix(in srgb, var(--qc-status-purple) 17%, transparent);
}
/* The theme's own parameters, restated on the wrapper: the theme class sets
   them one class deep, this is three. */
.qc-grid--ledger .qc-grid__grid .ag-root-wrapper {
  --ag-background-color: var(--qc-ledger-bg);
  --ag-odd-row-background-color: transparent;
  --ag-header-background-color: var(--qc-ledger-well);
  --ag-border-color: var(--qc-ledger-rule);
  --ag-row-border: 1px solid var(--qc-ledger-rule);
  background: var(--qc-ledger-bg);
}
.qc-grid--ledger .qc-grid__grid { border-color: var(--qc-ledger-rule); }
.qc-grid--ledger .qc-grid__grid .ag-header {
  border-bottom: 1px solid var(--qc-ledger-rule);
}
.qc-grid--ledger .qc-grid__grid .ag-header-cell-text {
  font-size: var(--qc-fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--qc-text-secondary);
}
.qc-grid--ledger .qc-grid__grid .ag-header-cell:not(.ag-column-last) {
  box-shadow: inset -1px 0 0 var(--qc-ledger-rule);
}
.qc-grid--ledger .qc-grid__grid .ag-row {
  border-color: var(--qc-ledger-rule);
  background-color: transparent;
}
.qc-grid--ledger .qc-grid__grid .ag-row.table-invoice { background-color: var(--qc-ledger-invoice); }
.qc-grid--ledger .qc-grid__grid .ag-row.table-commission { background-color: var(--qc-ledger-commission); }
.qc-grid--ledger .qc-grid__grid .ag-row.qc-row-total { background-color: transparent; }
.qc-grid--ledger .qc-grid__grid .ag-grid-pinned-bottom-rows {
  border-top: 2px solid var(--qc-ledger-rule);
  background: var(--qc-ledger-well);
}
/* Its inner container paints the theme's own colour over the strip, lines
   and all; the strip is what carries both here. */
.qc-grid--ledger .qc-grid__grid .ag-grid-pinned-bottom-rows-container { background: transparent; }
/* The rows area carries the column lines and, under the last row, blank
   ruled rows (ag_grid_controller#_ruleEmpty); `local` so they scroll with the
   cells. */
.qc-grid--ledger .qc-grid__grid .ag-grid-scrolling-rows { background-attachment: local; }

/* The resize grip's own mark sat a pixel or two beside the column's rule, a
   second, crooked line in every heading. The rule is the mark here; the grip
   is still there under the pointer. */
.qc-grid--ledger .qc-grid__grid .ag-header-cell-resize::after { background: transparent; }

/* A ledger closes on its totals: "6 of 6" under them says nothing the rows do
   not, and it was the strip left between the last line and the card's edge. */
.qc-grid--ledger .qc-grid__more { display: none; }

/* ---- The customer sheet (report/_sheet_grid.html.twig) ---------------------
   Three kinds of row in one list. A job is the plain row; an invoice wears the
   teal it wore before, an agreement the violet of its chip. Each is a bar and
   a light wash, like the other row states above — and each also says what it
   is in words, in its own cells. */
.qc-grid__grid .ag-row.table-invoice {
  color: var(--qc-text-primary);
  box-shadow: inset 3px 0 0 var(--qc-status-teal);
  background-color: color-mix(in srgb, var(--qc-status-teal) 8%, transparent);
}
.qc-grid__grid .ag-row.table-commission {
  color: var(--qc-text-primary);
  box-shadow: inset 3px 0 0 var(--qc-status-purple);
  background-color: color-mix(in srgb, var(--qc-status-purple) 9%, transparent);
}
.qc-rowcard.table-invoice { box-shadow: inset 3px 0 0 var(--qc-status-teal); }
.qc-rowcard.table-commission { box-shadow: inset 3px 0 0 var(--qc-status-purple); }

/* The totals held under a list (`pinned_bottom`). */
.qc-grid__grid .ag-row.qc-row-total {
  background-color: var(--qc-surface-light);
  font-weight: 600;
}
.qc-grid__grid .ag-grid-pinned-bottom-rows { border-top: 2px solid var(--qc-border); }

/* An invoice's marks after its number: paid, carrying its XML. Icons with a
   title — the words are the tooltip, and the paid
   mark is also the "Incassato" column beside it. */
.qc-sheetentry { display: inline-flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
.qc-sheetentry__flag { font-size: var(--qc-fs-lg); color: var(--qc-text-secondary); }
.qc-sheetentry__flag--paid { color: var(--qc-success); }

/* "Prev. da avere": the figure, the quotation it came from, the agreement. */
.qc-sheetest { display: inline-flex; align-items: center; justify-content: flex-end; gap: .4rem; flex-wrap: wrap; }
.qc-sheetest__ref {
  display: inline-flex; align-items: center; gap: .15rem;
  font-size: var(--qc-grid-cell-small-size, .75rem);
  color: var(--qc-text-secondary);
  text-decoration: none;
}
.qc-sheetest__ref:hover, .qc-sheetest__ref:focus-visible { color: var(--qc-primary); text-decoration: underline; }

/* ---- A line of words under a row of pills ----
   The agreement list's "Servizi": the services as chips, then the name they
   make as plain text. The default row's box is two lines of text, which is one
   row of pills (22px) and one small line — so the pills never wrap here
   (fitPills gives the rest a "+N") and the caption is one line with an
   ellipsis, its whole text on the hover. */
.qc-cell__captioned {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  width: 100%;
}
.qc-cell__captioned > .catwrap { flex-wrap: wrap; }
.qc-cell__caption {
  display: block;
  color: var(--qc-text-secondary);
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* One line at the compact density, and on a phone's card, where a field is
   one line: the pills first, the name beside them, the name giving way. */
[data-qc-density="compact"] .qc-cell__captioned,
.qc-rowcard__pair .qc-cell__captioned {
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: .4rem;
}
[data-qc-density="compact"] .qc-cell__captioned > .catwrap,
.qc-rowcard__pair .qc-cell__captioned > .catwrap { flex: none; max-width: 60%; }

/* ---- A share of the whole, in a cell ----
   The figure over its bar ("3/12" over a quarter of one). The settings' bar
   recipe: the fill is a token, never a framework colour. */
.qc-cellbar {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: .25rem;
  width: 100%;
  min-width: 0;
}
.qc-cellbar__label { line-height: 1.3; }
.qc-cellbar__bar {
  display: block;
  height: .375rem;
  border-radius: 999px;
  background: var(--qc-layer-selected);
  overflow: hidden;
}
.qc-cellbar__bar::after {
  content: "";
  display: block;
  height: 100%;
  width: var(--qc-bar, 0%);
  border-radius: inherit;
  background: var(--qc-primary);
}
[data-qc-density="compact"] .qc-cellbar,
.qc-rowcard__pair .qc-cellbar {
  flex-direction: row;
  align-items: center;
  gap: .5rem;
}
[data-qc-density="compact"] .qc-cellbar__bar,
.qc-rowcard__pair .qc-cellbar__bar { flex: 1 1 auto; min-width: 2rem; }

/* ---- A fact over the pill that judges it ----
   A window over its planning verdict (`textPill`). The fact is the cell's
   ordinary text — one voice a cell, and here the pill is the one that speaks
   up, so the date is not bold as well. Two lines of text is the row's box and
   a pill is a little taller than a line: the first line is set tight and a
   hair of air kept between them, so the pair is the box and never a scrollbar.
   The pill never leaves the cell: narrower than its words, it ends them in an
   ellipsis and keeps its icon (the whole sentence is its title). At the compact
   density, and on a phone's card, the generic rule lays the two on one line. */
.qc-cell__stack {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 3px;
  min-width: 0;
  width: 100%;
}
.qc-cell__stacktop {
  font-size: var(--qc-fs-md);
  font-weight: 500;
  line-height: 1.15;
  white-space: nowrap;
  color: var(--qc-text-primary);
  font-variant-numeric: tabular-nums;
}
.qc-cell__stack > .qc-stpill {
  align-self: flex-start;
  max-width: 100%;
  min-width: 0;
  line-height: 1.3;
  padding-block: 1px;
}
.qc-cell__stack > .qc-stpill > .qc-stpill__text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.qc-rowcard__pair .qc-cell__stack { flex-direction: row; align-items: center; gap: .4rem; }

/* ---- A filter on the list itself ----
   One chip per subset, each with its count, and "all" to have the whole back:
   the stat cards' gesture (stat_cards_controller drives both) where the page
   has no room, or no reason, for a row of cards over the table. The pressed
   state is `aria-pressed`, drawn as the stat card draws it — a primary border
   — over a faint primary wash. */
.qc-gridfilter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .375rem;
}
.qc-gridfilter__chip {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  padding: .25rem .625rem;
  border: 1px solid var(--qc-border);
  border-radius: var(--qc-radius-pill);
  background: transparent;
  color: var(--qc-text-secondary);
  font: 500 var(--qc-fs-sm)/1.35 var(--qc-font-body);
  white-space: nowrap;
  cursor: pointer;
  transition: border-color var(--qc-dur-fast) var(--qc-ease-fast), background-color var(--qc-dur-fast) var(--qc-ease-fast);
}
.qc-gridfilter__chip:hover { border-color: color-mix(in srgb, var(--qc-primary) 45%, var(--qc-border)); color: var(--qc-text-primary); }
.qc-gridfilter__chip:focus-visible { outline: 2px solid var(--qc-primary); outline-offset: 2px; }
.qc-gridfilter__chip[aria-pressed="true"] {
  border-color: var(--qc-primary);
  background: var(--qc-layer-selected);
  color: var(--qc-text-primary);
  font-weight: 600;
}
.qc-gridfilter__count { color: var(--qc-text-tertiary); }
.qc-gridfilter__chip[aria-pressed="true"] .qc-gridfilter__count { color: var(--qc-text-secondary); }
@media (prefers-reduced-motion: reduce) {
  .qc-gridfilter__chip { transition: none; }
}

