/*
 * Report table — the shared `.report-table` chassis (ISH-896).
 *
 * The React `DataTable` (`react-app/src/components/DataTable.tsx`) emits this markup, and so do
 * the hand-written report tables in `js/`. Before this file it existed as two inline copies,
 * in `sales-pipeline-report.html` and `uninvoiced-approved-tasks.html`, which had already drifted
 * apart in the direction this file resolves.
 *
 * ── Where the values come from ───────────────────────────────────────────────
 *
 * The look is `analytics.html`'s Unattributed Brokers table (`.analytics-table.unattributed-table`,
 * ISH-871/878/879/882/893), which is also the table `DataTable`'s own comments already cite for the
 * `aria-sort` convention. `sales-pipeline-report.html`'s copy was ALREADY most of the way there —
 * 600 weight, `--text-label`, no header fill — so this is not a new design. It moves the one
 * outlier (`uninvoiced-approved-tasks.html`: 700 weight, `--text-secondary`, an `#f8fafc` header
 * band and a bordered card wrapper) onto what the other two already do.
 *
 * The header recedes and the data carries the page. That is the whole idea, and it is why the
 * header has no fill: a grey band plus a bordered card plus a row of uppercase labels is three
 * devices competing to be the first thing read on a page whose point is the numbers.
 *
 * ── ⚠️ `.table-wrap` is NOT styled bare here ─────────────────────────────────
 *
 * Five pages declare their own `.table-wrap` (`analytics`, `assignments`, `broker`,
 * `sales-pipeline-assignments`, `uninvoiced-approved-tasks`) and they do not all mean the same
 * component. A bare rule here would be safe only for as long as this file stays on one page, which
 * is exactly the assumption that breaks later. The wrapper rule is therefore scoped with `:has()`
 * to a wrapper that actually contains a `.report-table`, so linking this file can never restyle an
 * unrelated `.table-wrap`.
 *
 * ⚠️ The wrapper IS a card, as of ISH-901 — `--card-bg`, a 1px border and `--radius-lg`. This
 * paragraph used to assert the exact opposite, and was left behind when ISH-901 added all three;
 * ISH-906 corrected it. The code is the intent.
 *
 * It still carries no negative margin: the Unattributed table bleeds `-1.25rem` past its host's
 * padding, but that value is a property of that page's gutter, not of the component. A page that
 * wants the bleed declares it itself.
 *
 * ── ⚠️ The sort affordance is a button, not the `<th>` ───────────────────────
 *
 * The reference table makes the whole `<th>` clickable (`th[data-sort] { cursor: pointer }`), which
 * is not keyboard-reachable. `DataTable` puts a real `<button class="th-sort">` inside the `<th>`
 * instead, and this file styles that button. **Do not add a `th[data-sort]` rule here** — a page
 * adopting this file should move its header to a button rather than bring the inaccessible pattern
 * along.
 *
 * `aria-sort` on the `<th>` stays the single source for BOTH the accessible state and the arrow
 * glyph, so there is only one thing to set and it cannot disagree with itself.
 *
 * Depends on the host page's tokens: --text, --text-secondary, --card-bg, --red. Every page in
 * this app declares all of them, and css/tokens.css owns them, so they are uniform.
 *
 * ── ⚠️ The four values this file PINS rather than reads (ISH-929) ────────────
 *
 * `--text-label`, `--border`, `--border-light` and `--radius-lg` are the four css/tokens.css does
 * NOT own — its header lists them as still-drifted — so reading them meant this component rendered
 * differently on every page that linked it. That is exactly ISH-899's finding about the KPI strip,
 * reaching the next shared component by the same route, and it is fixed the same way: the values
 * are pinned on `.table-wrap` as `--rt-*` and a page that must differ overrides one of those.
 *
 * ⚠️ **Reading the page token again is not that escape hatch; it is the drift coming back.**
 *
 * The label colour is also an ACCESSIBILITY fix, not only a consistency one. `.report-table th` is
 * 0.6875rem — squarely non-large text, so the 4.5:1 floor applies:
 *
 *   #94a3b8   2.56:1   what uninvoiced-approved-tasks rendered (slate scale)   fails
 *   #777777   4.48:1   what analytics.html renders                             fails, marginally
 *   #767676   4.54:1   ISH-498's value for this class; .kpi-grid's --kpi-label-color
 *
 * So the header could not be fixed by copying the reference table — that value fails too. #767676
 * is the value the KPI strip directly above this table already uses.
 *
 * ⚠️ The block is declared on the WRAPPER, not on `.report-table`, because the wrapper needs two of
 * them itself and a parent cannot read a property declared on its child. Declaring the defaults in
 * both places would be worse than it looks: a direct declaration beats an inherited one, so a page
 * overriding `--rt-label-color` on `.table-wrap` would have it silently overridden back by the
 * table's own default. One declaration site, which is also the override site — as `.kpi-grid` does.
 * A bare `.report-table` with no wrapper is therefore not a supported shape here; the renderer in
 * js/uninvoiced-report-table.js always emits the wrapper, and the `:has()` scoping below already
 * assumes it.
 *
 * Consumers today: uninvoiced-approved-tasks.html, sales-pipeline-report.html, ar-report.html,
 * my-ar-ap-report.html, broker.html.
 *
 * ⚠️ The "two inline copies" the top of this header claims to resolve was aspirational for a
 * while: ISH-896 factored the file out of both but only migrated uninvoiced-approved-tasks.html,
 * and js/report-table.test.js carried a describe pinning that gap open. sales-pipeline-report.html
 * is migrated now, so the sentence is finally true as written. It overrides none of the `--rt-*`
 * above — its own --text-label/--border/--border-light/--radius-lg already ARE #767676/#e0e0e0/
 * #ebebeb/8px — which is the shape a page adopting this file should aim for.
 */

/* Scoped, not bare — see the `.table-wrap` warning above.
 *
 * ISH-901: the wrapper carries a container again. ISH-896 left it bare on the reasoning that the
 * reference table is borderless — but that table BLEEDS to its page's gutter, which is what makes
 * it read as a deliberate section rather than a table that lost its box. Without the bleed (which
 * cannot live here; see the header) a bare table sits under this page's bordered KPI strip looking
 * like two unrelated components stacked. Border and radius, but no shadow: it is a surface, not a
 * raised card. */
.table-wrap:has(> .report-table) {
  /* See the header. Override one of these on .table-wrap if a page must differ. */
  --rt-border: #e0e0e0;
  --rt-row-rule: #ebebeb;
  --rt-radius: 8px;
  --rt-label-color: #767676;

  overflow-x: auto;
  background: var(--card-bg);
  border: 1px solid var(--rt-border);
  border-radius: var(--rt-radius);
}

.report-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8125rem;
}

.report-table th,
.report-table td {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--rt-row-rule);
  text-align: left;
}

/* ⚠️ The header is the quietest row in the table and the totals row is the loudest — that ordering
   is load-bearing, and inverting it is the defect ISH-901 fixed. The header takes a faint ground
   and a real rule beneath it so it reads as a boundary; the totals row below takes NO fill and
   carries itself on weight. Giving the totals row a ground heavier than this one puts the darkest
   band at the top of the table, where it reads as the header and demotes the real one to a
   caption. */
.report-table th {
  font-size: 0.6875rem;
  /* ISH-929: 600, the reference table's weight. It was 700 to match `.chart-col-headers` in
     css/mdat-boa-card.css (ISH-906) — but that is the MDAT CHART's header, and the reference for
     this component is and has always been the Unattributed Brokers TABLE, which is 600. Matching
     the chart pulled the one table on the page away from the table it was extracted from. */
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  /* Pinned, not `--text-label` — see the header. This is an AA fix as much as a matching one: the
     page token resolves to #94a3b8 here, which is 2.56:1 on white at this size. */
  color: var(--rt-label-color);
  /* ⚠️ STILL an explicit ground rather than `transparent`, because the header is sticky: rows
     scrolling underneath a transparent header would show through it. #ffffff is fully opaque, so
     taking the reference's own colour costs nothing here — but do NOT "simplify" this to
     `transparent` or drop the line. MDAT's header can go without a fill precisely because it does
     not stick; that half still must not be copied across. */
  background: #ffffff;
  /* ISH-929: ruled beneath only. The upper rule existed to close a FILLED header against the card
     edge so it read as a band; with the fill gone there is no band to close, and the reference
     table rules horizontally under each row and nowhere else. The rule beneath stays — it is what
     makes the header a boundary, which ISH-901 is explicit about. */
  border-bottom: 1px solid var(--rt-border);
  white-space: nowrap;
  position: sticky;
  top: 0;
  z-index: 1;
}

.report-table tbody tr:last-child td {
  border-bottom: none;
}

/* ⚠️ On the ROW, not on its cells (ISH-962). A cell-level `background` here outranks
   `.cell-heat-*` — one class, on the same element — so hovering a row wiped out its aging shading.
   Declared on the `<tr>`, a shaded `<td>` composites over it instead, which is what the tints are
   written for. Every ground in this file follows that rule; see "Section depth" below. */
.report-table tbody tr:hover {
  background: #fafafa;
}

/* ── Alignment and column roles ──────────────────────────────────────────── */

/* ⚠️ Written as `.report-table th.align-*` / `td.align-*`, NOT as bare `.align-*` utilities —
   the chassis could not otherwise win its own alignment rule. The base rule above declares
   `text-align: left` on `.report-table th, .report-table td` at specificity (0,1,1), and a bare
   class answers at (0,1,0), so it lost on every cell. The visible defect was that
   `js/uninvoiced-report-table.js`'s `align` column option did nothing: every column declaring
   `align: "right"` — the Un-invoiced Approved Task Report's money columns, the AR Report's aging
   and past-due columns, and the Rep AR/AP roster's whole right-hand half — rendered left. Do not
   "simplify" these back to bare selectors.

   Do not fix it from the other end either, by lowering the base rule (wrapping its subject in
   `:where()`, say). Every host override of a cell in this component is already written at (0,1,1)
   or (0,2,1) against that base — `.uat-task-table th` in css/uninvoiced-report.css,
   `.report-table td.ar-name` in ar-report.html, `.report-table td.is-sample` in
   my-ar-ap-report.html — and dropping the base would re-resolve all of them, not just alignment.

   The default is untouched: a column that declares no `align` still takes its left from the base
   rule, and these fire only on a cell that carries the class. */
.report-table th.align-left,
.report-table td.align-left {
  text-align: left;
}

.report-table th.align-right,
.report-table td.align-right {
  text-align: right;
}

.report-table th.align-center,
.report-table td.align-center {
  text-align: center;
}

/* Tabular figures so numeric columns compare down the page rather than jittering on digit width. */
.col-money {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ISH-929: the vertical rules are gone. The reference table draws hairlines UNDER rows and nowhere
   else, and a table that rules horizontally everywhere and vertically in exactly one place reads as
   having a seam rather than an emphasis. The argument they were added on — "the one column read
   across rather than down" — is still right; it is the TREATMENT that was wrong. The emphasis moves
   into the number instead, which is how the reference marks its own most-read column
   (`.unattributed-table td.uab-fees`: 800 weight in --text, no borders). */
.col-total {
  font-weight: 800;
  color: var(--text);
}

.report-table a {
  color: var(--text);
  text-decoration: none;
  font-weight: 600;
  border-bottom: 1px solid transparent;
}

.report-table a:hover {
  color: var(--red);
  border-bottom-color: currentColor;
}

/* ── Muted cell conventions ───────────────────────────────────────────────────
 *
 * ⚠️ **This file used to say an empty money cell must render "$0.00", because a blank reads as a
 * rendering failure. ISH-901 retired that rule deliberately — do not reinstate it without
 * re-reading why.**
 *
 * It was written for a table with one or two money columns. This report has seven aging buckets,
 * and `.docs/AGENT_GUIDE.md` §CX.4 records that the 0-7 bucket carries ~85% of both the value and
 * the task count — so on a typical row six of them are empty, and "$0.00" six times across a third
 * of the table's width reads as loudly as real money while saying nothing.
 *
 * The sibling island had already reached the same conclusion for these same seven columns
 * (`react-app/src/broker-uninvoiced/App.tsx`), so the two surfaces now agree rather than
 * contradicting each other.
 *
 * What keeps the original concern answered: **an em dash is a rendered value.** A cell that failed
 * to load is empty, and empty still looks like a bug. The caller renders the dash; this file only
 * quiets it.
 *
 * `.is-unassigned` is italic as well as quiet, because "Unassigned" is a real finding — a gap
 * somebody has to close — and it keeps its word rather than becoming a dash.
 */

.is-zero {
  /* Pinned with the header label (ISH-929) rather than left on the page token — same class of
     value, and on the slate scale the page token renders this dash at 2.56:1. */
  color: var(--rt-label-color);
}

.is-unassigned {
  color: var(--text-secondary);
  font-style: italic;
}

/* ── Sortable headers ────────────────────────────────────────────────────── */

.th-sort {
  font: inherit;
  font-size: inherit;
  font-weight: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}

.th-sort:hover {
  color: var(--text);
}

/* Idle sortable columns reserve the arrow's width, so sorting does not shift the header row
   sideways. The glyph is only emitted on the active column. */
.report-table th.is-sortable .th-sort::after {
  content: "";
  display: inline-block;
  width: 0.75em;
  margin-left: 0.15em;
  opacity: 0.45;
}

.report-table th[aria-sort="ascending"] .th-sort::after {
  content: "\25B2";
  opacity: 1;
}

.report-table th[aria-sort="descending"] .th-sort::after {
  content: "\25BC";
  opacity: 1;
}

/* The active column darkens rather than turning red. Red is this app's accent for a selected or
   actionable thing; a sorted column is neither, and on a page where links and the aged-money tile
   already carry red, a red header competes with them for the same meaning. */
.report-table th[aria-sort] .th-sort {
  color: var(--text);
}

/* ── Totals row ───────────────────────────────────────────────────────────────
 *
 * Sits ABOVE the body, as it does in the source spreadsheet.
 *
 * ⚠️ **No fill.** It used to carry `#eef2f7`, which made it the darkest band in the table and put
 * it directly under a header that had just lost its own ground — so it read as the header and the
 * real header read as a caption (ISH-901). Weight and the 2px rule separate it from the body; that
 * is enough, and it keeps the header as the table's only boundary.
 */

/* ⚠️ The 2px is deliberate and survives ISH-929's pass at matching the Unattributed Brokers table.
   That table has no totals row at all, so it offers no opinion here — and this rule is what
   separates the totals from the first data row. Softening it to a hairline "for uniformity" would
   be copying a silence, not a decision. */
.totals-head td {
  border-bottom: 2px solid var(--rt-border);
  padding: 0.4rem 0.75rem;
  /* ⚠️ One step DOWN the scale from the body's 0.8125rem (decision 2026-09-14). The totals are a
     reference line, not the table's loudest row — and ISH-901's rule still holds either way: the
     header above stays the quieter of the two because it is 0.6875rem in the label colour. */
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* The label cell, quieter still — it names the row rather than carrying a figure. */
.totals-head td:first-child + td,
.totals-head td.col-expand + td {
  font-size: 0.5625rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

/* ── Aging heat, opt-in (ISH-924) ────────────────────────────────────────────
   A money cell can be shaded by how much of its row it carries, so a row shows at a glance where
   its own money sits. ⚠️ OPT-IN: nothing here applies unless a column supplies a `cellClass`, so
   linking this file never shades an existing table.

   Tints of --red rather than a separate palette, because on this report a large aged balance is
   the thing to act on and red is already this app's attention colour.

   ⚠️ The darkest step's alpha is set by the WORST case, which is a heat-4 cell on a hovered
   depth-2 row — the deepest ground in the file, not the card. What sits there is --text, and only
   --text: a money figure goes --red through `.is-negative` alone, and in
   js/uninvoiced-approved-tasks-view.js that class and the heat class are derived from the SAME
   cents by complementary conditions (`cellClass` returns "" for `cents <= 0`; `.is-negative`
   applies for `cents < 0`). A red figure therefore never carries a heat class.

   ⚠️ That is a coincidence of two conditions rather than a guarantee, and it is what lets this
   alpha be tuned against --text alone. Shade a negative bucket — by absolute value, say — and
   --red lands on heat-4 at 3.75:1, under the 4.5:1 that applies at this size and weight.
   js/uninvoiced-approved-tasks-render.test.js renders that pairing's absence to hold it.

   Measured on the worst ground at this alpha: --text 9.10:1, well clear of the 7:1 this scale has
   always promised. The shading must never be the reason a number is hard to read.

   ⚠️ This alpha and the depth grounds below are ONE decision. Darken a ground and the --text
   figure has to be re-measured, not assumed — js/report-table.test.js recomputes it from whatever
   alpha and ground are in this file. The 7:1 promise leaves real headroom here (it holds past
   0.40), so this alpha is chosen for the scale's own legibility, not pinned by contrast — it is
   0.28 because that is the step the scale was designed around, and it briefly went to 0.24 to buy
   back --red contrast that no figure was reading. */
.cell-heat-1 {
  background: rgba(166, 38, 38, 0.05);
}

.cell-heat-2 {
  background: rgba(166, 38, 38, 0.1);
}

.cell-heat-3 {
  background: rgba(166, 38, 38, 0.18);
}

.cell-heat-4 {
  background: rgba(166, 38, 38, 0.28);
}

/* ⚠️ A credited task makes a bucket negative, and this report really does carry them. Coloured
   rather than left to a bare minus sign, which is one glyph wide and easy to miss in a grid of
   right-aligned money. */
.is-negative {
  color: var(--red);
  font-weight: 600;
}

/* ── Section depth (ISH-933; bracketed by ISH-962) ───────────────────────────
 *
 * The table sections rather than regroups: a broker row opens onto its client rows, which open
 * onto tasks. Every level is a real `<tr>` sharing the table's own columns, so a figure on a client
 * row lands in the same column as the broker total above it.
 *
 * ⚠️ Depth is marked by a rail and a recessive ground, NEVER by shifting a cell. Indenting the row
 * would move the money columns out from under their headers, which is the one thing this whole
 * design exists to prevent. The indent is drawn inside the identity cell's padding instead.
 *
 * ── ⚠️ Every ground here sits on the `<tr>`, never on its `<td>`s (ISH-962) ──────────────────
 *
 * ISH-933 shipped these fills on the cell, and `.report-table tbody tr[data-depth] td` is (0,2,2)
 * against `.cell-heat-4`'s (0,1,0) — so opening a broker painted over its own aging heat, and all
 * of its clients', in the one state where somebody is reading it closely. New Generation Agg
 * Transport carried 83% of its late money in 8-14 days and drew none of it.
 *
 * Declared on the row, the cascade never arises: a `<td>` background composites over its row's,
 * which is exactly what the `rgba()` tints exist for. **Do not move a ground back onto a cell.**
 *
 * ── The open section is bracketed, not filled ───────────────────────────────
 *
 * An open row keeps the card's own ground and takes its emphasis from weight; a rail runs down the
 * left edge of the block — solid on the open row, 30% beneath it — and one rule closes the group
 * where the next top-level row starts.
 *
 * ⚠️ The children's ground used to be a single step off white, and a single step off white is not
 * a step anybody sees. On a broker with five clients the block read as five more top-level rows:
 * the rail was the only surviving signal, and on the AR reports it is 3px at the far left of a
 * ten-column table. The grounds below are one real step apart each, and they form a LADDER whose
 * ordering is the actual contract — card 255, top-level hover 250, depth-1 246, depth-2 238,
 * depth hover 230. Darken one without the others and the ladder inverts: a hovered nested row
 * would come out lighter than a nested row at rest.
 *
 * ⚠️ The greys are NEUTRAL — equal r/g/b — and that is the point of them, not a rounding of the
 * slate values they replaced. They used to be `#f7f8fb` / `#eef0f5` / `#e7eaf1`, which are cool
 * blue-greys; the AR Report's age ramp is `rgba(71, 85, 105, .045–.195)`, which is ALSO a cool
 * blue-grey. Two grey systems in the same hue family, one on the `tr` and one compositing over it
 * on the `td`, read on screen as a single wash — the depth step and the age step became
 * indistinguishable from each other on exactly the cells where both matter. Neutral greys have no
 * hue to collide with, so the ramp stays legible as a ramp at every depth. Do not "harmonise"
 * these back onto the slate scale; that is the defect.
 *
 * ⚠️ The top-level `tr:hover` above is deliberately NOT part of this ladder and was left where it
 * was. It is a transient state on an un-nested row; sliding it down with the grounds would make
 * the mouse, rather than the structure, the loudest thing on the table.
 *
 * That leaves the heat the only colour in the table, which is the point of the report: a fill
 * heavy enough to mark the open section is also heavy enough to compete with the shading it sits
 * behind.
 */
/* ⚠️ **NO GROUND AT ANY DEPTH.** §KS took the ladder off the slate scale so it stopped colliding
   with the age ramp; this removes the ladder. Four grey steps is more than the eye resolves on the
   AR Report's normal three-level grain, and every one of them lands under figures that are the
   point of the table.

   Depth is DRAWN now — the drawer rails below, and the cut that lifts an open block out of the
   table. The ground is declared as the token rather than omitted, because `.is-expanded` still has
   to have something to beat when an open row is itself a child, which is the ISH-962 failure this
   rule was written for. */
.report-table tbody tr[data-depth] {
  background: var(--card-bg);
}

.report-table tbody tr[data-depth] td {
  font-size: 0.78125rem;
  border-bottom-color: #f2f2f4;
}

/* ── The drawer ──────────────────────────────────────────────────────────────
 * The rail sits on the FIRST cell — the expand column — so it runs down the outside of the block
 * and the toggles sit inside it.
 *
 * ⚠️ **Each level's rail steps IN, and that step is the depth signal now that no ground is.** The
 * cell paints a band of card colour to the rail's left, so a block reads as sitting inside the one
 * above it, and the number of rails on screen is the number of levels you are down. That is
 * countable. A grey is judged, which is why four of them stopped working.
 *
 * ⚠️ **Shadow ORDER is load-bearing.** Inset shadows paint first-on-top, so the card-colour band is
 * declared FIRST and the rail shows only past where the band ends. Reverse them and the band covers
 * the rail completely — and it fails silently, because both rules are still valid CSS.
 *
 * ⚠️ An inset shadow, not a `border-left`: a border in a `border-collapse` table takes real layout
 * width, so the first column would widen the moment anything opened.
 *
 * ⚠️ **A closed row's rail is NEUTRAL; red marks only the row you opened.** `ar-report.html` is
 * explicit that age and lateness must not share an encoding, and red is its past-due colour —
 * spending it on every nested row puts structure into the one colour a reader scans for late money.
 * One red rail per open row is the entire budget. */
.report-table tbody tr[data-depth] td:first-child {
  box-shadow:
    inset 5px 0 0 var(--card-bg),
    inset 8px 0 0 #d4d4d4;
}

.report-table tbody tr[data-depth="2"] td:first-child {
  box-shadow:
    inset 10px 0 0 var(--card-bg),
    inset 13px 0 0 #d4d4d4;
}

.report-table tbody tr[data-depth] .col-broker {
  padding-left: 1.1rem;
}

.report-table tbody tr[data-depth] .col-client {
  padding-left: 1.1rem;
}

.report-table tbody tr[data-depth="2"] .col-client {
  padding-left: 2.2rem;
}

/* The identity span (ISH-962) — a client row's Broker and Client columns as one cell, so a long
   name reads on one line instead of wrapping inside a narrow cell with an empty one beside it.
   ⚠️ It carries `.col-client` as well, so the depth indent above applies to it unchanged and stays
   declared in exactly one place.
   ⚠️ NAME columns only. Every money column keeps its own cell, so a figure still lands under its
   own header — the rule the whole sectioned layout exists to protect. And the run starts at
   Broker, not at Rep: a name spanning from the first column would begin to the LEFT of the broker
   name above it, which reads as a level up rather than a level down. */
.col-identity-span {
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}

/* The LOWER half of the cut. An open block is bounded by page colour above and below, so it reads
   as lifted out of the table rather than shaded inside it — which is what somebody recognises as
   "this is a separate thing" without having to be taught the convention.
   ⚠️ `--page-bg` and 14px, not a border colour and 1px: the point is that the table looks
   INTERRUPTED. A hairline reads as a rule between two rows, which is the opposite claim.
   ⚠️ Keyed off the SIBLING rather than a class on the last child, so it stays correct at every
   grain and asks nothing of the renderer. */
/* ⚠️ `:not(.expanded-row)` is the whole correctness of this rule, and it was MISSING.
   The detail panel is a `<tr>` with no `data-depth` — it is content, not a level — so
   `tr[data-depth] + tr:not([data-depth])` matched it and drew the block's CLOSING cut directly
   above the panel, inside the block it was supposed to close. On the AR Report that put a 14px
   page-grey band between a client row and its own invoices. Measured, not guessed: the panel's
   computed `border-top` was `14px solid rgb(248, 249, 250)`. */
.report-table tbody tr[data-depth] + tr:not([data-depth]):not(.expanded-row) td {
  border-top: 14px solid var(--page-bg);
}

/* ⚠️ The ONLY ground left in the component, and it may be the darkest thing here precisely because
   it is transient: it is gone the moment the mouse is, so it competes with nothing. */
.report-table tbody tr[data-depth]:hover,
.report-table tbody tr.is-expanded:hover {
  background: #f2f2f2;
}

/* A section node's own name, quieter than a top-level row's. */
.uat-node-name {
  color: var(--text-secondary);
  font-weight: 600;
}

/* ⚠️ The expand column keeps its width on every row, including rows with no toggle — a leaf with
   no tasks renders an empty cell there, and letting it collapse would shift that row's columns out
   of line with its siblings.
   ⚠️ **Widened for the drawer.** The deepest rail lands at 13–16px and the toggle is 1.4rem, so at
   the old 2.2rem (35px) the two overlapped. The toggle is right-aligned into the remaining space
   below so it clears every rail at every depth rather than only at the depth that was measured. */
.report-table .col-expand {
  width: 3.4rem;
}

/* ── Row expansion ───────────────────────────────────────────────────────── */

/* ⚠️ **SCOPED, and it has to be.** `.report-table th, .report-table td` declares `text-align: left`
   at (0,1,1); a bare `.col-expand` answers at (0,1,0) and LOSES. This rule shipped bare and the
   alignment never applied — the toggle stayed left, landing on top of the depth-2 rail. The `center`
   that was here before the drawer was inert for the identical reason and had simply never mattered,
   because nothing else was drawn in that cell.

   This is the same defect ISH-1096 found across the `.align-*` utilities, in the same file, against
   the same base rule. See "Alignment and column roles" above — do NOT simplify this back to a bare
   selector, and do not fix it by weakening the base rule.

   ⚠️ And the TEST for it has to resolve the cascade. The first version asserted the declaration said
   "right", which it did, while the rendered cell was left — a rule can be present, correct and
   completely inert. */
.report-table th.col-expand,
.report-table td.col-expand {
  text-align: right;
}

.col-expand {
  width: 3.4rem;
}

.expand-toggle {
  font: inherit;
  font-size: 0.9rem;
  font-weight: 700;
  line-height: 1;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 4px;
  cursor: pointer;
  color: var(--text-secondary);
  background: var(--card-bg);
  border: 1px solid var(--rt-border);
}

.expand-toggle:hover {
  color: var(--red);
  border-color: rgba(166, 38, 38, 0.35);
}

/* ⚠️ NO fill (ISH-962). See "Section depth" above: a ground on these cells outranks the aging
   heat, so the row somebody had just opened was the one row that stopped showing where its late
   money sits. Weight and the rail mark it instead. The ground IS declared rather than omitted,
   because it has to beat the depth ground when an open row is itself a child.

   ⚠️ **WHAT that ground is REVERSED once the greys got darker.** It used to be `--card-bg` at every
   level — an open row popped to the card colour so it read as the head of its block. That works at
   two levels and fails at three, which is the AR Report's normal shape (rep › broker › client).
   With three open, the card colour lands on all three heads in a row and the only thing that steps
   is the leaves — white, white, white, then a **0.129 luminance drop** into a solid grey slab. The
   nest had no gradient; it had a wall at the bottom.

   An open row now takes the ground of the DEPTH IT SITS AT, so opening three levels draws three
   steps instead of one cliff. Measured on the AR Report's own three-level case, the drop into the
   leaf block goes 0.129 → 0.068.

   ⚠️ The head of a block is still marked — by weight, by the solid rail, and by the dropped bottom
   border that joins it to its children. It was never the fill doing that work; ISH-962 removed the
   fill for exactly that reason and this only changes which ground replaces it.

   ⚠️ **Depth 0 is `--card-bg` again, and the `#fdfdfe` that sat here is gone.** The comment it
   replaces said reverting to the token "collapses the distinction" between an open top-level row
   and a closed one. That was true of the FILL and false of the row: a closed top-level row carries
   no `box-shadow` at all, 400-weight cells and a drawn bottom border, while an open one carries a
   solid `--red` rail, 700-weight cells and a transparent bottom border joining it to its children.
   Four signals, none of them the ground. `#fdfdfe` is one part in 255 off white — a step nobody
   has ever seen — so it was never the thing telling them apart, and the paragraph above says as
   much about ISH-962. The ladder now starts at the card colour and steps down from there, which is
   what makes depth-1 the first VISIBLE step rather than the second. */
.report-table tbody tr.is-expanded {
  background: var(--card-bg);
}

/* ⚠️ The per-depth open-row grounds are GONE, and with them the restatement that had to be pinned
   equal to the depth grounds. There is one ground for every row in the component now — the card —
   so there is nothing left to drift. An open row is marked by its red rail, its weight and the cut,
   none of which is a fill. */

.report-table tbody tr.is-expanded td {
  font-weight: 700;
  /* An open row and its children are one block, so nothing is ruled between them. */
  border-bottom-color: transparent;
}

/* Red where a closed row's rail is neutral — the head of the block, and the only red in the body
   outside a past-due figure. Each one sits at its OWN depth's offset, so an open row keeps its
   place in the drawer instead of jumping back to the left edge.
   ⚠️ Declared after the depth rails above, and the two qualified rules below are (0,3,2) against
   this one's (0,2,2), so an open child takes its own offset rather than this one. */
.report-table tbody tr.is-expanded td:first-child {
  box-shadow: inset 3px 0 0 var(--red);
}

.report-table tbody tr[data-depth="1"].is-expanded td:first-child {
  box-shadow:
    inset 5px 0 0 var(--card-bg),
    inset 8px 0 0 var(--red);
}

.report-table tbody tr[data-depth="2"].is-expanded td:first-child {
  box-shadow:
    inset 10px 0 0 var(--card-bg),
    inset 13px 0 0 var(--red);
}

/* The UPPER half of the cut, on the outermost open row only. A nested open row must not take it —
   it would slice the block it belongs to in half. */
.report-table tbody tr.is-expanded:not([data-depth]) td {
  border-top: 14px solid var(--page-bg);
}

/* The task detail hangs inside the same bracket, and its cell spans the table, so there is no heat
   underneath it and any ground would be safe here.

   ⚠️ **It is WHITE, and that is deliberately outside the ladder — the same kind of exception the
   top-level hover is.** A grey here has to be a rung: darker than depth-2, or the deepest thing in
   the nest reads as the shallowest. Three rungs of grey plus a fourth for the panel is more steps
   than the eye resolves, and it is a lot of shading to put under a table whose own age ramp is the
   thing meant to be read.

   The rule that replaces it is legible in one sentence: **a grey ground means a STRUCTURE row, and
   white means actual data.** The head rows of the nest are greys; the invoices, which are the
   records the nest exists to reach, sit on the card colour like the top-level rows do. The panel is
   not "level 4" and no longer pretends to be — it is a different KIND of thing, and it is bracketed
   by the rail and the dropped borders rather than by a fill.

   ⚠️ This is why js/report-table.test.js no longer asserts `panel <= depth-1`. That test encoded
   the ladder reading; it now pins the token instead, so a grey creeping back in fails. */
.expanded-row > td {
  background: var(--card-bg);
  padding: 0 0.75rem 0.9rem 2.75rem;
  font-weight: 400;
  /* ⚠️ NOT a drawer rail — reverted deliberately. The panel took an indented `#d4d4d4` rail with
     the rest of the nest, and on the real report it read as a detached grey hairline: the rows
     above it carry red rails, so the one element furthest inside the block was the only thing in it
     not marked as part of it. It keeps the original tinted rail at the left edge, which reads as
     "this belongs to the open block" and is what it did before. The drawer's step ordering is the
     nest's; the panel is not a level of the nest. */
  box-shadow: inset 3px 0 0 rgba(166, 38, 38, 0.3);
}

/* ── Empty state ─────────────────────────────────────────────────────────── */

/* ⚠️ Scoped as `.report-table td.empty-cell` / `th.empty-cell` for the reason spelled out under
   "Alignment and column roles" above — ISH-1096 found the bare `.align-*` utilities inert and
   scoped them; `.empty-cell` is a bare cell utility in the same file against the same base rule
   and was missed in that pass.

   It was HALF inert, which is why it survived: `color` and `font-size` are not declared on
   `.report-table th, .report-table td`, so those two landed and the empty state looked
   deliberate. `padding` and `text-align` are, at (0,1,1) against this rule's (0,1,0), so every
   empty state in the component rendered left-aligned at the ordinary cell padding instead of
   centred at 2.5rem. Measured on analytics.html before the fix: 8px/12px and `left`, not
   40px/16px and `center`.

   That covered every consumer — `js/uninvoiced-report-table.js`, `js/ar-report.js` and five
   sites in `js/analytics.js`. Do not "simplify" this back to a bare selector, and do not fix it
   by lowering the base rule; see the alignment note for why that is worse than it looks. */
.report-table th.empty-cell,
.report-table td.empty-cell {
  padding: 2.5rem 1rem;
  text-align: center;
  color: var(--text-secondary);
  font-size: 0.9rem;
}

/* ── The report card: header and table in ONE box ────────────────────────────
 *
 * ISH-929 established this shape on `uninvoiced-approved-tasks.html` as `.uat-report-card`, and
 * that file's own header said what to do next: *"Page-local rather than shared — if a second
 * report grows one, move it there."* This is that move.
 *
 * ⚠️ **It was already overdue, and the evidence is in four files.** Before this component existed
 * here, FOUR pages had independently built the same shape:
 *
 *   - `uninvoiced-approved-tasks.html`  — `.uat-report-card`, the original
 *   - `sales-pipeline-report.html`      — links `css/uninvoiced-report.css` to borrow it
 *   - `ar-report.html`                  — own rules, citing `.uat-report-card` in a comment
 *   - `broker-platform-utilization`     — own rules, citing it in a comment as well
 *
 * Two of those cite a class they do not load, which means the comment is the only thing tying
 * them together and nothing keeps the measurements in step. That is precisely the drift
 * `css/filter-bar.css`'s header records for `.filter-bar`, and the pre-ISH-353 stage palette
 * before it.
 *
 * ⚠️ **The four copies above are NOT yet migrated.** This change adds the shared home and adopts
 * it on `my-ar-ap-report.html` so a fifth copy is not written; moving the other four is its own
 * change, because three of them are live reports that would gain risk and no visible difference.
 * Migrate them onto these classes and delete their local rules — do not add a sixth copy.
 *
 * Measurements are `.analytics-card`'s, by way of `.uat-report-card`: copied, not eyeballed.
 */
.report-card {
  /* ⚠️ PINNED, exactly as `.table-wrap` pins `--rt-*` above and for the identical reason:
     `--border` and `--radius-lg` are NOT owned by css/tokens.css — its header lists both as
     still-drifted — so reading them would make this card render differently on every page that
     links this file. `js/report-table.test.js` fails the build for reading either.

     ⚠️ Deliberately its OWN names rather than the wrapper's. The four `--rt-*` above must stay
     declared in exactly one place, or a page overriding one on `.table-wrap` would have it
     overridden back; re-declaring them here would break that. A page that needs a different card
     overrides these two.

     ⚠️ And the radius is 12px, NOT `--rt-radius`'s 8px. A card is not a table wrapper: 12px is
     what both hosts of this shape already render (`--radius-lg: 12px` on
     `uninvoiced-approved-tasks.html` and `my-ar-ap-report.html`). */
  --rt-card-border: #e0e0e0;
  --rt-card-radius: 12px;

  background: var(--card-bg);
  border: 1px solid var(--rt-card-border);
  border-radius: var(--rt-card-radius);
  padding: 1.25rem;
}

/* ⚠️ The table is inside the card now, so it must NOT also draw one. The rule at the top of this
   file gives `.table-wrap` a border, a radius and a ground on the reasoning that a bare table
   would otherwise look like an unrelated component — that reasoning is answered by the card, and
   leaving the wrapper's box on top of it is a card inside a card. The 1.25rem card padding
   replaces the border it gives up; only the horizontal scroll is kept.

   ⚠️ Scoped with `:has()` to a `.table-wrap` that actually contains a `.report-table`, matching
   the base rule above — five pages declare their own unrelated `.table-wrap`. */
.report-card .table-wrap:has(> .report-table) {
  background: none;
  border: 0;
  border-radius: 0;
}

.report-card-head {
  display: flex;
  /* `center`, which is `.analytics-card-header`'s. ISH-929 records that this page once carried
     two competing `align-items` declarations and the losing one was the obvious thing to edit. */
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  padding: 0;
  margin-bottom: 1.25rem;
}

.report-card-headtext {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.report-card-title {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
}

/* ⚠️ `--text-secondary`, NOT `--text-label`. The label token is per-page and reaches #94a3b8 on
   one of this component's hosts, which measures 2.43:1 and fails AA — the same call the filter
   caption and the tab strip already make. */
.report-card-meta {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* ── The card head's segmented control ─────────────────────────────────────────
 * ISH-1138. `.view-bar` / `.view-label` / `.view-btn` come from css/period-controls.css; what
 * lives here is the treatment they take INSIDE a `.report-card-head`, which is the slot
 * `uninvoiced-approved-tasks.html` fills with `Group by` and `my-ar-ap-report.html` fills with
 * `Show`.
 *
 * ⚠️ This is the THIRD page to want these exact measurements and the first to write them where a
 * fourth can reach them. `css/uninvoiced-report.css` states the same block against `.uat-groupby`
 * and explains why: the app has two active-pill languages — `.view-btn.active`'s red tint and
 * `.util-chip.is-active`'s `--nav-bg` fill — and a control in a card header takes the dark one,
 * because that is what the Sales Report's `MDAT & BOA` card does in the same position. When
 * `uninvoiced-approved-tasks.html` migrates to `.report-card` (this file's header lists it first
 * of the four still to move), delete `.uat-groupby .view-btn` and its two siblings there; these
 * rules are their replacement, not a second copy of them.
 *
 * ⚠️ SCOPED to `.report-card-head`, and that is load-bearing rather than tidiness — the same call
 * `.uat-groupby` makes. broker.html renders `.view-btn` from the same stylesheet in a Trends view
 * switcher that is NOT in a card header, and restyling the bare class would move it too.
 */
.report-card-head .view-bar {
  /* `.view-bar` carries a 1rem bottom margin for its usual place above a panel; here it is a
     sibling of the title and needs none. */
  margin-bottom: 0;
}

.report-card-head .view-btn {
  display: inline-flex;
  align-items: center;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
  background: #ffffff;
  /* ⚠️ `--rt-card-border`, not `--border`: this component ships in the same file as `.report-card`
     and must not read a token css/tokens.css does not own — see that card's own note.
     `js/report-table.test.js` fails the build for reading `--border` or `--radius-lg` here. */
  border: 1px solid var(--rt-card-border);
  border-radius: 999px;
  padding: 0.3rem 0.8rem;
}

.report-card-head .view-btn:hover:not(.active) {
  border-color: #999999;
  color: var(--text);
}

.report-card-head .view-btn.active {
  background: var(--nav-bg);
  border-color: var(--nav-bg);
  color: #ffffff;
  font-weight: 700;
}
