/* The comparison table's stylesheet — WP-L4, R56-D-2/D-2a.
 *
 * The two rules under "Rules kept from WP-E" predate this package; everything
 * else is WP-L4. The R20 "structure only, no design value" test is retired
 * (A12), so design values arrive here from presets and custom properties — and
 * only from those. No hex, no design px anywhere in this file. The 1px
 * hairlines are the same structural exception theme.json already takes for its
 * border tokens (Token Contract v1 defines no width token), and `80vh` is a
 * scrollport bound rather than a design measurement.
 *
 * Every rule is justified where it sits; the running budget total and the
 * arithmetic against the ~35-line guide are in rere-core/README.md.
 */

/* --- Box model on this plugin's block wrappers (UX-2) -------------------- */

/* The same declaration `blocks/page-nav/style.css` carries, and the same
   reasoning — read it there. It is repeated rather than shared because the two
   stylesheets serve disjoint surfaces: page-nav is on every country and route
   record, this file is on the family hubs, and a hub has no page-nav. On
   /routes/investment/ at 1768px the sourcesOnly citation panel rendered 816
   against a 768 token; with border-box it renders 768. One line in each file is
   cheaper than a third stylesheet loaded on both, and far cheaper than the
   sitewide reset D-2 forbids. */
[class*="wp-block-rere-"] { box-sizing: border-box; }

/* --- The hub table keeps the page gutter (UX-6, audit finding G) --------- */

/* The section is composed `alignfull` (patterns/family-hub.php) and carried no
   padding of its own, so it measured L=0 W=1768 with the table at L=1: the
   sticky first column ran flush against the window edge while the `h1` naming
   it sat in the content column, and so did the 25 filter chips above it. This
   is the combination `main` itself uses — keep `alignfull`, take the root
   padding back — so the table keeps the width full bleed buys it (1720 of 1768,
   1872 of 1920) and stops touching the glass. Measured after: figure and
   controls both L=24, table L=25, no horizontal document scroll at 1920, 1768,
   1440 or 768, and the pane's own scrollWidth still equals its client width.

   At 1440 and below the root padding *is* the page gutter, so the table lands
   on the `h1`'s exact edge and the hub has one left edge. Above ~1648px it does
   not: the chrome edge is where a 100rem box centres (84 at 1768, 160 at 1920)
   and root padding is 24. Both readings were measured and put to Andres, who
   chose this one — capping the section to 100rem would have matched the `h1` at
   every viewport but cost the table 12% more height in its default four-column
   state and 18–35% with all thirteen columns switched on. The extra width is
   worth more here than the shared edge; the reasoning is R56-D-2's, that a
   better reader outcome outranks a tidier rule.

   Scoped to `.alignfull` because this block renders twice on a hub: the second
   instance is `sourcesOnly` and unaligned, already sits at the `h1`'s edge at
   768px wide, and must not be padded again. */
.wp-block-rere-comparison-table.alignfull { padding-inline: var(--wp--style--root--padding-left) var(--wp--style--root--padding-right); }

/* --- The scroll pane and the two sticky axes (deliverable 1) ------------- */

.wp-block-rere-comparison-table .wp-block-table {
	/* Core gives this figure `overflow-x: auto`, and CSS computes `overflow-y`
	   to `auto` alongside it — so the figure, not the page, is the scrollport
	   every sticky offset below resolves against, and a scrollport with no
	   height limit has no vertical travel for a header row to stick through.
	   Bounding the pane is what makes `top: 0` mean anything, and it is the DS
	   ComparisonTable's own construction. 80vh rather than the DS's flat 32rem
	   because our cells carry the recorded note with the figure: one row here
	   can be taller than the whole of the DS's pane. */
	max-height: 80vh;
	/* A pane that clips its own content has to show an edge, or the clip reads
	   as a rendering fault. The DS frames the table for exactly this reason;
	   rere_block_styles()' note that a frame would "frame the scrollbar" was
	   written when the table was unbounded and ran the full page. */
	border: 1px solid var(--wp--custom--border--muted);
	border-radius: var(--wp--custom--radius--default);
}

.wp-block-rere-comparison-table .wp-block-table > table {
	/* Exactly one frame, and it is the pane's. Core's table block.json puts the
	   theme's core/table node on `.wp-block-table > table`, so the table carries
	   a border of its own — which, inside a scroll pane, walks away from three
	   of its four edges and draws a second line inside the first at the fourth.
	   Width only: the colour stays, because the cells read it back through
	   core's `border-color: inherit` for their column rules. */
	border-width: 0;
}

.wp-block-rere-comparison-table thead th {
	/* DS ComparisonTable: the header row sticks with the first column. On the
	   cells and not on `thead` or `tr`, because the table is
	   `border-collapse: collapse`, where a row box paints no background of its
	   own and a stuck row would travel without its fill. */
	position: sticky;
	top: 0;
	z-index: 2;
	/* Was an inline style on the header row (WP-E). A sticky cell needs its own
	   opaque paint or the row's background scrolls out from under it, and D-2
	   lets the value live here now. */
	background-color: var(--wp--preset--color--surface-3);
}

.wp-block-rere-comparison-table thead th:first-child {
	/* The corner, where the two sticky axes meet. It has to outrank both or the
	   first body column slides over it on horizontal scroll. */
	left: 0;
	z-index: 3;
}

/* --- Pre-hydration column parity (WP-L6 L6-2, 2026-08-19) ---------------- */

@media (scripting: enabled) {
	.wp-block-rere-comparison-table:has(.rere-ct-controls[hidden]) [data-rere-optional] {
		/* The server renders all thirteen columns and always will — R54-D3 — so
		   a scripted reader painted thirteen and then watched hydration hide
		   nine: `thead` collapsed 90px to 33px, the survivors re-widened, and
		   every visible row moved. 0.3953 CLS at 1920, inside the bounded pane
		   above, because a shift inside a scrollport is still a shift. This
		   hides the same nine at first paint, so the first paint already is the
		   hydrated layout. Measured: 0.72 to 0.29 at 1920, and to 0.01 with the
		   font half (theme.json `font-display: optional`, same package).

		   Three guards, and each one is load-bearing:
		   - `@media (scripting: enabled)` is the no-JS guarantee. With scripting
		     off the query is false, the rule never applies and all thirteen
		     columns render, which is the whole point of rendering them. An
		     engine too old to know the feature drops the rule and gets today's
		     behaviour — the shift, not a broken table.
		   - `:has(.rere-ct-controls[hidden])` is the release. The controls carry
		     a literal `hidden` from the server and lose it at hydration, so this
		     rule stops matching exactly when the cells' own `hidden` bindings
		     take over. Without the release a reader could not switch a column
		     back on.
		   - `display: none`, matching what `[hidden]` resolves to, so nothing
		     moves at the handover.

		   Failure mode, accepted and recorded: if scripting is on but view.js
		   never loads, the controls keep their `hidden`, this rule keeps
		   applying, and the reader sees four columns with no way to reach the
		   other nine — where today they would see thirteen and no controls. The
		   module is same-origin and ships with the document, and every other
		   binding in this block fails the same way. */
		display: none;
	}
}

/* --- Rules kept from WP-E, extended rather than rewritten ---------------- */

.wp-block-rere-comparison-table th[scope="row"] {
	/* A wide hub table scrolls inside core's .wp-block-table overflow; without
	   this the reader loses which country a row belongs to mid-scroll. No token
	   and no core mechanism provide sticky positioning. */
	position: sticky;
	left: 0;
	/* Below the header row and its corner, above every ordinary cell. */
	z-index: 1;
	/* Read back from the theme's own core/table token, so no colour is decided
	   here and none is duplicated. Striped rows override this per cell, from
	   the rule below. */
	background-color: var(--wp--preset--color--surface-1);
}

.wp-block-rere-comparison-table th {
	/* The user agent centres every `th`, so the column headings sat centred over
	   left-aligned data and the first column's country and route name sat
	   centred over nothing. The DS aligns a header with the column it names.
	   `start`, not `left`, so the rule does not assume a writing direction —
	   the records carry Greek and French strings already. */
	text-align: start;
}

.wp-block-rere-comparison-table tbody :is(th, td) {
	/* Cells align to the top of their row, not its middle. A row here is as tall
	   as its longest recorded note — often several hundred words — and centred
	   content left a one-word value floating in the middle of a screen-high row
	   with nothing beside it to read against. Aligned to the top, every value in
	   a route's row starts on the same line, which is the whole point of putting
	   them in a row. */
	vertical-align: top;
}

.wp-block-rere-comparison-table :is(thead, td, th) {
	/* WP-E: row striping now separates the rows, so core's own horizontal cell
	   rules and its 3px thead underline are drawn separation nothing needs.
	   Column rules (the inline edges) are untouched and stay on the theme's
	   border token. Removal only — no value is decided here. */
	border-block-width: 0;
}

/* --- Striping follows the visible rows (deliverable 3, closes R55-D4) ---- */

.wp-block-rere-comparison-table tbody tr:nth-child(even of :not([hidden])) > :is(th, td) {
	/* R55-D4 is closed rather than re-affirmed. The banding used to be emitted
	   inline per row from the server's row number, so hiding countries left two
	   visible rows sharing a band; `of :not([hidden])` counts only the rows the
	   reader can still see, and it counts them in CSS, which is what R55-D4
	   asked for — the colour never travels into JavaScript. Painted on the
	   cells rather than on the row because the first column is sticky and a
	   collapsed table's row box paints nothing behind a stuck cell. An engine
	   without `of S` drops the rule and renders the table unstriped, which is
	   flat, not broken. */
	background-color: var(--wp--preset--color--surface-2);
}

/* --- The reader controls as DS chips (deliverable 2) --------------------- */

.wp-block-rere-comparison-table .rere-ct-controls {
	/* The two groups stack; the step between them is the in-component 30, and
	   50 separates the whole control strip from the table it acts on. */
	display: grid;
	gap: var(--wp--preset--spacing--30);
	margin-block-end: var(--wp--preset--spacing--50);
}

.wp-block-rere-comparison-table .rere-ct-controls__group {
	/* A group is its label followed by a wrapping row of chips. */
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--wp--preset--spacing--20);
}

/* The group's own label, weighted to the DS field label (2026-08-31, 0.40.0).
   Scoped `> span` so it cannot reach a chip: a chip is a `<label>` or a
   `<button>`, and this span is the only direct-child span the group has. The
   Token Contract defines no font-weight token — `settings.custom` has none and
   `settings.typography` exposes fontSizes only — so neither theme.json nor a
   block style variation can carry this, and an inline style in render.php would
   reverse WP-L4's move of design values out of that file. 600 is the same
   literal `blocks/search-filter/style.css` already spends twice for the DS
   field label, and Inter is a 100-900 variable face here, so it is a real
   semibold. Weight only: no display, no colour, no size — the (scripting: none)
   rule and the L6-2 parity rule both key off this element's ancestors. */
.wp-block-rere-comparison-table .rere-ct-controls__group > span { font-weight: 600; }

.wp-block-rere-comparison-table .rere-ct-chip {
	/* The DS chip: pill radius, muted hairline, quiet ink, one step of padding.
	   `font-family: inherit` is here for the "show all" button, which otherwise
	   takes the user agent's own face and breaks the row. */
	display: inline-flex;
	align-items: center;
	gap: var(--wp--preset--spacing--10);
	padding: var(--wp--preset--spacing--10) var(--wp--preset--spacing--30);
	border: 1px solid var(--wp--custom--border--muted);
	border-radius: var(--wp--custom--radius--pill);
	background-color: var(--wp--preset--color--surface-1);
	color: var(--wp--preset--color--contrast-2);
	font-family: inherit;
	font-size: var(--wp--preset--font-size--sm);
	line-height: var(--wp--custom--line-height--tight);
	cursor: pointer;
	transition: background-color var(--wp--custom--transition--fast), border-color var(--wp--custom--transition--fast);
}

.wp-block-rere-comparison-table .rere-ct-chip input {
	/* The native checkbox stays, visible and unaltered in behaviour: it is what
	   makes the chip keyboard-operable and correctly announced, and replacing it
	   with a painted box would need either an icon this site has not built yet
	   (WP-L5) or a data-URI carrying a hex, which D-2 forbids. `accent-color`
	   tints the user agent's own control from a preset. */
	accent-color: var(--wp--preset--color--primary);
}

.wp-block-rere-comparison-table .rere-ct-chip:has(:checked) {
	/* Active. The fill alone would read the same as hover, so the state is also
	   carried by the border and the ink — and, before either, by the checkbox's
	   own tick. Never colour alone. */
	background-color: var(--wp--preset--color--surface-2);
	border-color: var(--wp--preset--color--contrast-3);
	color: var(--wp--preset--color--contrast);
}

.wp-block-rere-comparison-table .rere-ct-chip:hover {
	background-color: var(--wp--preset--color--surface-2);
}

.wp-block-rere-comparison-table .rere-ct-chip:active {
	/* Press, per the brief's chip spec. Last of the three so it wins the tie
	   with :has(:checked), which has the same specificity. */
	background-color: var(--wp--preset--color--surface-3);
}

.wp-block-rere-comparison-table .rere-ct-chip:focus-visible,
.wp-block-rere-comparison-table .rere-ct-chip:has(:focus-visible) {
	/* The DS focus ring, on the chip rather than on the input inside it, so the
	   ring outlines the thing the reader is operating. Two selectors because the
	   "show all" chip is the focus target itself and the toggles contain theirs.
	   `:focus-visible`, not `:focus`, so a mouse press leaves no ring behind. */
	box-shadow: var(--wp--custom--focus--ring);
}

.wp-block-rere-comparison-table .rere-ct-chip:focus-visible,
.wp-block-rere-comparison-table .rere-ct-chip :focus-visible {
	/* The ring above replaces the user agent's outline; both at once is two
	   rings around one control. Nothing loses a focus indicator here. */
	outline: none;
}

/* --- The reader controls, when scripting is off (UX-6, closes L6-6) ------ */

/* The user agent hides a `hidden` element with `display: none` at UA
   specificity and the author `display: grid` above silently beats it, so from
   WP-L4 until now a reader with scripting off was shown 25 checkboxes and a
   button that do nothing — while the thirteen columns they would have filtered
   were all rendered anyway, which is the whole point of rendering them
   (R54-D3). This is the L6-6 the WP-L6 pass found; UX-4 met the same defect in
   rere/search-filter and closed that half on 2026-08-19, and this is the second
   and last block that has it.

   Scoped to `(scripting: none)` and not fixed with a plain
   `[hidden] { display: none }`, for the reason UX-4 measured rather than
   assumed: the unscoped form also hides the strip from a *scripted* reader
   until hydration puts it back, and on the front page that shift took CLS from
   0.0051 to 0.0273 at 1920. This strip is 107px of two wrapping chip rows
   directly above the table, so the same shift here would be larger, and L6-2's
   whole point was to get this block to 0.0000.

   It sits on top of the `(scripting: enabled)` parity rule above and does not
   disturb it: that rule keys off the `hidden` *attribute* through `:has()`, not
   off how the element computes `display`, and the two media queries are
   mutually exclusive. Measured in both directions — scripting off,
   `display: none`, 0 of 25 checkboxes rendered and all 13 columns still there;
   scripting on, `display: grid`, 25 rendered, 4 columns after hydration,
   unchanged. An engine too old to know the feature drops this block and keeps
   the old behaviour: no regression, just no fix.

   Written as one rule on one line, the lever rere-core/README.md has named
   since WP-L4 and UX-5 pulled: the budget metric prices a selector line and a
   closing brace as dearly as a declaration. Nothing is removed to get there. */
@media (scripting: none) {
	.wp-block-rere-comparison-table .rere-ct-controls[hidden] { display: none; }
}

/* --- The stacked table (deliverable 4) ---------------------------------- */

@media (max-width: 37.5rem) {
	.wp-block-rere-comparison-table .wp-block-table {
		/* Nothing to scroll sideways and nothing to bound: below this width the
		   table is a stack of cards in normal flow, and a pane here would be a
		   scroll region inside the page scroll for no gain. */
		max-height: none;
		overflow: visible;
		border: 0;
	}

	.wp-block-rere-comparison-table :is(table, tbody, tr, th, td):not([hidden]) {
		/* The DS ComparisonTable's stacked layout: one card per route, one field
		   per line. Same markup and the same server order — the ARIA roles the
		   renderer emits keep this a table for assistive technology once
		   `display` stops making one. `:not([hidden])` matters: without it this
		   rule would beat the user agent's `[hidden] { display: none }` and every
		   column the reader switched off would come back. */
		display: block;
	}

	.wp-block-rere-comparison-table thead {
		/* Off-screen rather than removed: the column headers stay in the
		   accessibility tree, where `role="columnheader"` still names the cells.
		   The sighted label arrives from the cell's own data-rere-label below. */
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip-path: inset(50%);
	}

	.wp-block-rere-comparison-table tbody tr {
		/* The DS's card, and the in-component 40 between one card and the next. */
		border: 1px solid var(--wp--custom--border--muted);
		border-radius: var(--wp--custom--radius--default);
		margin-block-start: var(--wp--preset--spacing--40);
	}

	.wp-block-rere-comparison-table tbody th[scope="row"] {
		/* It is the card's header now, not a column: nothing to stick to, and
		   the rule under it is what makes it read as one. The alternating band
		   stays — it is what tells one card from the next. */
		position: static;
		border-block-end: 1px solid var(--wp--custom--border--muted);
	}

	.wp-block-rere-comparison-table tbody :is(th, td) {
		/* DS cell padding, and no column rules: a stack has no columns. */
		padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--40);
		border-inline-width: 0;
	}

	.wp-block-rere-comparison-table tbody td + td {
		/* One field from the next, on the soft hairline the DS uses inside a
		   card rather than the muted one that bounds it. */
		border-block-start: 1px solid var(--wp--custom--border--soft);
	}

	.wp-block-rere-comparison-table tbody td::before {
		/* The column label the hidden header row no longer shows, read back from
		   the attribute the renderer emits — which carries the same string the
		   header renders, so no new reader-facing string exists here (hard rule
		   2). The second `content` sets empty alternative text, keeping the
		   label out of the accessibility tree where the columnheader already
		   announces it; an engine without that syntax drops the second
		   declaration and keeps the label from the first. */
		content: attr(data-rere-label);
		content: attr(data-rere-label) / "";
		display: block;
		color: var(--wp--preset--color--contrast-2);
		font-size: var(--wp--preset--font-size--sm);
	}
}
