/*
 * Product Grid (TC2-T36) — the homepage "Shop New Products" section.
 *
 * Eyebrow, heading and a link over six WooCommerce product cards on the navy
 * band, with the decorative globe. Six across at desktop; below lg the row is a
 * scroll-snap slider, three across at tablet and two on mobile.
 *
 * Frames: desktop 43369:7978, tablet 43782:19773, mobile 43782:19546
 * (docs/design/product-grid). All three are bound comps — the tablet and mobile
 * frames draw the band, the globe, real product content AND the slider's own
 * controls, so unlike TC2-T40 they are followed directly rather than derived
 * from the written spec.
 *
 * Breakpoints are written at the token values with the token named
 * (sm 30 / md 48 / lg 64 / xl 80rem). 63.9375rem is "below lg", used so the
 * slider stops exactly where the static six-up grid starts.
 *
 * REUSED, NOT REDEFINED: `.tc-eyebrow` (style.css) supplies the bar-and-label
 * micro-kit; this file only re-points the bar's colour for the navy surface.
 *
 * SPECIFICITY. Two separate contests run in this file and both are won on
 * specificity, never on load order:
 *
 *   1. WordPress's flow-layout blockGap, `:root :where(.is-layout-flow) > *`,
 *      lands at (0,1,0) because `:where()` contributes nothing. Every margin
 *      rule here is therefore scoped through `.tc-product-grid` for (0,2,0).
 *      Both the product template <ul> and each product <li> are flow layouts, so
 *      without this every card gains a 24px gap between its parts.
 *   2. WooCommerce's own product-template rules reach (0,2,0)
 *      (`.wc-block-product-template__responsive.columns-6`) and (0,3,0)
 *      (`.is-flex-container.is-flex-container.columns-6 > li`). The doubled-class
 *      selectors below are how this file clears them; WooCommerce uses the same
 *      device on itself (`.wc-block-product-template.wc-block-product-template`),
 *      so the idiom is the neighbours', not an invention.
 *
 * @package tonercable-2026
 */

.tc-product-grid {
	/*
	 * Eighth definition of the arrow in this theme (hero-basic, hero-split,
	 * hero-overlay, media-basic, footer, category-grid, brand-grid, here).
	 *
	 * Two forms, on purpose. The alpha-only ones are painted by
	 * background-color, which is what lets the chevron serve both directions
	 * from one glyph (scaleX(-1) for next) and lets the plus inherit the button's
	 * text colour. The `-navy` one carries its fill baked in, because the link
	 * chip is a WHITE plate with a navy glyph and a mask would cut the plate
	 * rather than colour the glyph.
	 *
	 * Since 2026-09-28 the `-navy` path is the Figma export of
	 * `fi-rs-arrow-right` (42222:4552) — the same path as all nine
	 * `--tc-icon-arrow-right` declarations, only the fill differs. It was
	 * the footer's old filled redraw. docs/reports/icon-audit.md §7.
	 */
	--tc-icon-arrow-right-navy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%230e2e7d'%3E%3Cpath d='M23.124 9.87897L18.538 5.29297L17.124 6.70697L21.386 10.97L0 11V13L21.446 12.97L17.123 17.293L18.537 18.707L23.124 14.121C23.6847 13.5575 23.9995 12.7949 23.9995 12C23.9995 11.205 23.6847 10.4425 23.124 9.87897Z'/%3E%3C/svg%3E");
	--tc-icon-chevron-left: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M15.4 7.4 14 6l-6 6 6 6 1.4-1.4-4.6-4.6z'/%3E%3C/svg%3E");

	/*
	 * The frames bind primary-700 (#0E2E7D, sampled at four points on the
	 * desktop export and identical at tablet and mobile). `.ftm-scheme-primary`
	 * ships primary-800, one step darker, so the scheme class is kept for the
	 * role wiring — white text, muted, accent-text — and only `bg` is re-pointed.
	 * That is the same shape as the header-scoped override at
	 * page-sections-schemes.css:101-103, not a new mechanism. White on #0E2E7D
	 * measures ~12:1.
	 */
	--ftm-scheme-bg: var(--wp--custom--color--primary--700, #0e2e7d);

	position: relative; /* containing block for the globe */
	overflow: hidden;   /* clips the globe's overhang */

	/* The band is inset from the viewport, not full-bleed: 1888 of 1920 and 736
	   of 768 (16 a side), 304 of 320 (8 a side). */
	margin-inline: var(--wp--preset--spacing--xsmall, 0.5rem);
	border-radius: 4px; /* literal — no radius token resolves to 4 (sm 6 / md 16 / pill 1.5em) */

	/* Frame-measured 40 / 48 / 104. Only the first is a token (xlarge); 48 and
	   104 have none — the scale stops at xxlarge 64 and xxxlarge is fluid
	   48→96, which cannot hold 104 flat. Literals, same call the 4px radius
	   above makes. */
	padding-block: var(--wp--preset--spacing--xlarge, 2.5rem);

	/* 16 at mobile, 24 at tablet. At lg the alignwide children cap at 1400 and
	   centre themselves, which lands them 244 inside an 1888 band exactly as the
	   desktop frame draws — so no lg padding-inline value is needed.

	   THE SELECTOR IS DOUBLED, and this is the third specificity contest in this
	   file rather than a stylistic tic. A constrained-layout group gets
	   `has-global-padding` from WordPress, which sets padding-inline from the
	   root padding at (0,1,0) — the SAME weight as a bare `.tc-product-grid`, so
	   it is a tie decided by load order, and global styles win it. Unfixed that
	   paints 40px a side at tablet, taking the content box to 656 where the
	   frame draws 688 and shrinking every card from 224 to 213. Measured, not
	   guessed. The band already sits inside its own 16px viewport inset, so the
	   site's root gutter inside it as well is a double gutter. */

	/* Decorative globe — mobile. Full account in the globe section below. */
	--tc-globe-on: "";
	--tc-globe-size: 491.43px;   /* circle Ø 490 + the asset's bleed */
	--tc-globe-bleed: 339.94px;  /* was clip-path inset(0 339.94px 0 0) */

	/* Anchored LEFT, as the frames draw it (2026-09-25) — the kit's
	   start-anchored form. The bleed values at md and lg follow through. */
	--tc-globe-inset-start: calc( var( --tc-globe-bleed ) * -1 );
	--tc-globe-inset-end: auto;

	/*
	 * COLOUR — read from the frame's DECLARED values, not sampled from a
	 * composited pixel. This globe is WHITE over the navy band, not
	 * secondary-300: Figma node 43369:8346 (svg1827) exports
	 * `stroke="white" stroke-opacity="0.3"` on all six paths, and the bound
	 * variable is `color/white alpha/300` (#ffffff4d — the alpha is IN the
	 * token). The layer then carries `opacity="0.3"` on top of that.
	 *
	 * THE TWO ALPHAS MULTIPLY, AND THAT IS THE DESIGN, NOT A MISTAKE:
	 *   0x4d / 255 = 0.302   (white-alpha-300, the paint)
	 *   x 0.3                (svg1827 layer opacity)
	 *   = 0.0906             effective alpha
	 * Both are declared in the frame, so both are reproduced. Expressed as ONE
	 * declaration via color-mix — the idiom hero-overlay.css already uses for
	 * its gradient stops — so the kit's `--tc-globe-opacity` stays at its
	 * default of 1 and exactly one property carries the alpha. Dropping either
	 * factor is a 3.3x error: white-alpha-300 alone would ship 0.302 where the
	 * frame draws 0.0906.
	 *
	 * Over the band (primary-700 #0e2e7d) a full-coverage stroke pixel measures
	 * rgb(35 64 136); the arithmetic gives 35.8 / 64.9 / 136.8.
	 *
	 * PER-PATTERN, NOT SITE-WIDE. hero-overlay declares secondary-500 and
	 * heading-banner/media-stats declare secondary-300, each chosen against
	 * its own band. Do not normalise these to one value.
	 */
	--tc-globe-paint: color-mix( in srgb, var( --wp--custom--color--white-alpha--300, #ffffff4d ) 30%, transparent );
}

.tc-product-grid.tc-product-grid {
	padding-inline: var(--wp--preset--spacing--normal, 1rem);
}

.tc-product-grid[class*="ftm-scheme-"] {
	/* Nothing global paints the scheme roles; the band paints itself, the same
	   thing every media family member and heading-banner does. */
	background-color: var(--ftm-scheme-bg);
	color: var(--ftm-scheme-text);
}

/* Content sits above the globe. */

.tc-product-grid__header,
.tc-product-grid__slider {
	position: relative;
	z-index: 1;
}

@media (min-width: 48rem) { /* breakpoint/md */

	.tc-product-grid {
		margin-inline: var(--wp--preset--spacing--normal, 1rem);
		padding-block: 3rem; /* 48 — tablet frame */

		--tc-globe-size: 692.02px;   /* circle Ø 690 + bleed */
		--tc-globe-bleed: 336.23px;  /* was clip-path inset(0 336.23px 0 0) */
	}

	.tc-product-grid.tc-product-grid {
		padding-inline: var(--wp--preset--spacing--large, 1.5rem); /* 24 */
	}
}

@media (min-width: 64rem) { /* breakpoint/lg */

	.tc-product-grid {
		padding-block: 6.5rem; /* 104 — desktop frame */

		/*
		 * DECISION A, EXTENDED TO THIS PATTERN 2026-08-31 (lead developer).
		 * Desktop only — the tablet and mobile bindings above stay as the
		 * frames measure them. See the globe section below for why this
		 * breakpoint scales and the other two do not.
		 *
		 * THE RATIO IS OF THE BAND, AND THIS BAND IS NOT THE VIEWPORT. The
		 * section is inset 16px a side, so it is 1888 wide at a 1920 viewport
		 * and the percentage resolves against that — which is why this number
		 * is 54.3665% and media-stats' is 53.4604% for the SAME 1026.44px
		 * square. Same rule, different denominator; do not copy one to the
		 * other.
		 *
		 *   size  = 1026.44 / 1888   = 54.3665% of the band, capped at 1026.44px
		 *   bleed = 202.72 / 1026.44 = 19.75% of the size
		 */
		--tc-globe-size: min( 54.3665%, 1026.44px );
		--tc-globe-bleed: calc( var( --tc-globe-size ) * 0.1975 );
	}
}

/* ─────────────────────────────────────────────────────────────────────────
 * Decorative globe
 *
 * Box mechanics, asset and mask live in the shared globe kit (style.css); this
 * pattern supplies only the positioning contract and the colour.
 *
 * WHERE THE DECLARATIONS ARE. The kit reads its custom properties off the band,
 * and this file already opens a `.tc-product-grid` rule at each breakpoint for
 * the band's own padding — so the globe's values are set THERE rather than in a
 * second rule with the same selector. Search `--tc-globe`: the base rule holds
 * the mobile size, the bleed and the colour; the md and lg blocks re-point the
 * size and bleed.
 *
 * Present at ALL THREE breakpoints here, which is the difference from
 * heading-banner and media-stats — both stop at lg because their tablet and
 * mobile frames drew no globe to derive one from. These frames draw it at every
 * width, so it ships at every width.
 *
 * LEFT EDGE BLEED, AS THE FRAMES DRAW IT — 2026-09-25, reversing the side
 * DECISION B (2026-08-31) chose. The repo owner reported the right-edge globe
 * as wrong; the Figma geometry below is why it was.
 *
 * THE FRAMES' GLOBE GROUP IS ROTATED 180°. In all three boards (43369:8345,
 * 43782:19774, 43782:19547) the group's reported origin equals its own size —
 * (824, 755), (365, 659), (156, 491.67) — the metadata signature of a rotated
 * node. Read un-rotated, the vectors give a left-anchored circle with a hard
 * vertical stop at the window's edge, mid-band: the geometry DECISION B
 * recorded, and which two reviewers rightly called a defect when the build
 * drew it. Read rotated — and the exports on disk settle it — the circle
 * bleeds off the band's LEFT edge and its RIGHT silhouette ends naturally
 * around x 800 of 1888. There was never a mid-band stop in the frame.
 *
 * The globe is symmetric under that rotation (circle, inner ellipse, meridian,
 * equator, and a mirrored pair of latitude arcs), so the frame is exactly the
 * horizontal mirror of what DECISION B shipped. Anchoring to the inline-START
 * with the SAME bleed reproduces it; no size or bleed number changes. The
 * rotated reading of each frame gives the square's overhang past the left
 * edge directly:
 *
 *   breakpoint  circle Ø   asset square   circle left edge   square bleed (left)
 *   desktop     1023.445   1026.44        −201.2             202.72
 *   tablet        690.000    692.02       −335.2             336.23
 *   mobile        490.000    491.43       −339.2             339.94
 *
 * Visible artwork is the square's RIGHTMOST `size − bleed`: 823.72 / 355.79 /
 * 151.49 wide at 1920 / 768 / 320, starting at the band's left edge.
 *
 * CONSEQUENCE, KNOWN AND ACCEPTED: on the left the globe sits behind the
 * section heading again, as every frame draws it. DECISION B had moved it
 * clear of the heading from 1280 up. The header and slider stay above it
 * (z-index 1) and the paint's effective alpha is 0.09.
 *
 * NOT A SITE-WIDE SIDE. heading-banner and media-stats still bleed RIGHT, and
 * that is what THEIR frames draw (media-stats 43657:7012: circle's left
 * silhouette visible, running off the right edge). Each globe matches its own
 * board; do not normalise the side.
 *
 * DECISION A APPLIES AT DESKTOP ONLY, AND THE SPLIT IS THE POINT.
 *
 * ⚠ This block previously said Decision A was NOT applied here, on the literal
 * reading that the decision named only heading-banner and media-stats. The lead
 * developer extended it to this pattern on 2026-08-31 after the drift was
 * measured, so that reading is superseded — but only for the lg binding.
 *
 * WHY DESKTOP SCALES. A flat 1026.44px square in a band that shrinks with the
 * viewport takes an ever-larger share of it. Measured before the extension, the
 * desktop binding ran from 43.63% of the band at 1920 to **83.04% at 1024** —
 * nearly the whole band, and the same class of drift Decision A was written to
 * fix on the other two globes. The lg block now carries the 1920 ratio with a
 * `min()` cap, so coverage holds at 43.63% across the whole desktop range and
 * 1920 renders exactly as it did.
 *
 * WHY TABLET AND MOBILE DO NOT. Those two bindings each govern a NARROW range
 * (48–64rem and below 48rem), and unlike the desktop range each has its OWN
 * bound comp — 692.02 at 736 and 491.43 at 304 — so their sizes are
 * frame-measured at a width inside the range they serve, not extrapolated
 * across a 900px span from a single 1920 board. There is nothing to correct:
 * they are already the frame's answer for the widths they cover. Scaling them
 * would replace a measured value with a derived one.
 *
 * THE CONSEQUENCE, STATED SO IT IS NOT REDISCOVERED AS A BUG: the artwork now
 * STEPS at the 64rem boundary, because a scaled binding meets a flat one there.
 * At 1023 the tablet square shows 355.79 of visible artwork; at 1024 the scaled
 * desktop binding shows 432.81. That step is 77px. It is smaller than the step
 * that was there before this change — the flat desktop square showed 823.72 at
 * 1024, a 468px jump — so the boundary got calmer, not sharper.
 *
 * ASSET: globe-wireframe-full.svg, the one complete globe every consumer
 * uses. The pre-cropped globe-wireframe.svg was retired 2026-09-25 (assets are
 * complete, crops are CSS); do not reintroduce a crop baked into a file.
 *
 * A failed mask URL paints a solid square, so verify this as artwork rather
 * than by presence alone (environment-facts §4).
 * ───────────────────────────────────────────────────────────────────────── */

/* ─────────────────────────────────────────────────────────────────────────
 * Header — eyebrow and heading, with the link beside them at lg
 *
 * Written at (0,2,0) to beat WP's `:root :where()` flow-layout margins at
 * (0,1,0). The frames step 8 / 16 / 16 / 24 / 48 between these parts, which no
 * single blockGap gives.
 * ───────────────────────────────────────────────────────────────────────── */

.tc-product-grid .tc-product-grid__header {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--normal, 1rem); /* 16 — titles -> link, mobile */
	margin-block: 0;
}

.tc-product-grid .tc-product-grid__titles {
	margin-block: 0;
}

.tc-product-grid .tc-eyebrow {
	margin-block: 0;
}

/* The frame draws the indicator in secondary-600 on the navy band, where the
   kit's `currentcolor` would paint it white. Decorative — a 20x4 bar with no
   text role — so the secondary-600/700 contrast rule that governs CTAs does not
   reach it. */

.tc-product-grid .tc-eyebrow::before {
	background-color: var(--wp--custom--color--secondary--600, #057db9);
}

.tc-product-grid .tc-product-grid__heading {
	margin-block: var(--wp--preset--spacing--xsmall, 0.5rem) 0; /* 8 — eyebrow -> heading, mobile */

	/* heading-medium's TOP is 2.5625rem = 41, which is what the desktop frame
	   draws, and the comp's -2 tracking at 41px is -0.82px. `capitalize` in the
	   comp is dropped per family precedent (brand-grid J-note): the seeded copy
	   carries its own capitalisation.

	   font-weight is a stand-in, not decoration: the theme's typeStyles block is
	   inert site-wide (DECISIONS H2), so this heading renders at weight 400
	   without it. Measured at 400 before this line. Fifth family to carry one. */
	font-weight: 700;
	letter-spacing: -0.02em;
	color: var(--ftm-scheme-text);
}

/* F23, third restatement of the heading-medium clamp. The preset is FLUID
   (min 1.4375rem), so heading-medium alone renders 41 / 30.48 / 23 — measured —
   where the frames draw 41 / 41 / 23. The typescale's Desktop mode = Tablet mode,
   so flat-at-tablet is the system's intent for every fluid size (DECISIONS F3/H3).
   23px @320 -> 41px, max reached at breakpoint/md: 23 + 18 * (100vw - 320px) / 448.
   Byte-identical to --tc-heading-medium-size and to brand-grid's restatement;
   !important beats the fluid preset. Hoist to a theme-level custom property —
   F23 asked for that before a third family, and this is the third. */
.tc-product-grid .has-heading-medium-font-size {
	font-size: clamp(1.4375rem, 0.634rem + 4.018vw, 2.5625rem) !important;
	line-height: 1.171; /* 48/41 */
}

.tc-product-grid .tc-product-grid__link {
	margin-block: 0;
}

.tc-product-grid .tc-product-grid__link a {
	display: inline-flex;
	align-items: center;
	gap: var(--wp--preset--spacing--xsmall, 0.5rem); /* 8 */

	/* label-large SIZE on body-large LINE-HEIGHT (15/24), the same mixed binding
	   brand-grid records at J40. Literal because label-large is a fluid preset
	   and the frames draw 15 flat at all three widths. */
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: 0.9375rem;
	font-weight: 500;
	line-height: 1.6;

	/* White on the navy band, not secondary — the frames bind white here and the
	   chip carries the accent instead. */
	color: var(--ftm-scheme-text);
	text-decoration: none;
}

/*
 * Inverted against brand-grid's chip: white plate, navy glyph. A mask cannot do
 * this in one element — masking the plate would cut the plate — so the glyph
 * arrives as a background-image whose fill is baked navy, and the plate keeps
 * background-color. That is why `--tc-icon-arrow-right` is declared twice-over
 * above: the alpha-only form for anything painted by background-color, and this
 * pre-filled form for the one place the plate is the coloured layer.
 */

.tc-product-grid .tc-product-grid__link a::after {
	content: "";
	flex: 0 0 auto;
	inline-size: 1.75rem; /* 28 at mobile */
	block-size: 1.75rem;
	border-radius: 4px;
	background-color: var(--ftm-scheme-text, #fff);
	background-image: var(--tc-icon-arrow-right-navy);
	background-position: center;
	background-repeat: no-repeat;
	background-size: 1rem 1rem; /* 16 glyph */
}

/* G17 (Gary review 2026-09-14): the link had no hover state. It takes the
   featured-brands treatment verbatim — brand-grid.css `.tc-brand-grid__link
   a:hover, a:focus-visible { text-decoration: underline }` — so the two
   section links on the homepage answer the pointer the same way. The arrow
   chip's nudge is motion, not a resting style, and lives in motion.css. */
.tc-product-grid .tc-product-grid__link a:hover,
.tc-product-grid .tc-product-grid__link a:focus-visible {
	text-decoration: underline;
}

@media (min-width: 48rem) { /* breakpoint/md */

	.tc-product-grid .tc-product-grid__header {
		gap: var(--wp--preset--spacing--large, 1.5rem); /* 24 — titles -> link, tablet */
	}

	.tc-product-grid .tc-product-grid__heading {
		margin-block-start: var(--wp--preset--spacing--normal, 1rem); /* 16 */
	}

	.tc-product-grid .tc-product-grid__link a::after {
		inline-size: 2rem; /* 32 from tablet up */
		block-size: 2rem;
	}
}

@media (min-width: 64rem) { /* breakpoint/lg */
	/* Desktop puts the link beside the titles, both sitting on the same
	   baseline-ish bottom edge (the frame bottom-aligns the two blocks at y=84). */

	.tc-product-grid .tc-product-grid__header {
		flex-direction: row;
		align-items: flex-end;
		gap: var(--wp--preset--spacing--normal, 1rem); /* 16 */
	}

	.tc-product-grid .tc-product-grid__titles {
		flex: 1 1 auto;
		min-width: 0;
	}

	.tc-product-grid .tc-product-grid__link {
		flex: 0 0 auto;
	}
}

/* ─────────────────────────────────────────────────────────────────────────
 * Slider viewport and track
 *
 * Below lg the viewport scrolls and each card snaps to its start edge. There is
 * no animation and nothing moves on its own, which is the whole difference from
 * TC2-T40's marquee: that one auto-scrolls and needed a static fallback under
 * prefers-reduced-motion, this one only needs its smooth scroll turned off.
 *
 * At lg the same track becomes a plain six-up grid and the viewport stops
 * scrolling. The controls are not hidden by a breakpoint — the script removes
 * them whenever the track no longer overflows, which at lg it does not. That
 * keeps the breakpoint in ONE place (this file) rather than duplicated into the
 * script the way heading-banner.js had to.
 * ───────────────────────────────────────────────────────────────────────── */

/*
 * The slider wrapper is the positioning context for the controls; the viewport
 * inside it is what scrolls. They cannot be the same element — an absolutely
 * positioned child of a scroll container scrolls away with the content.
 */

.tc-product-grid .tc-product-grid__slider {
	margin-block: var(--wp--preset--spacing--large, 1.5rem) 0; /* 24 — header -> grid, mobile */
}

.tc-product-grid .tc-product-grid__viewport {
	margin-block: 0;
	overflow-x: auto;
	overflow-y: hidden;
	scroll-snap-type: x mandatory;
	scroll-behavior: smooth;

	/* Hidden rather than styled: the controls and native swipe both remain, and
	   WooCommerce's own carousel layout hides it the same way. */
	scrollbar-width: none;
}

.tc-product-grid .tc-product-grid__viewport::-webkit-scrollbar {
	display: none;
}

/* The scroll container is focusable (the script adds tabindex), so it must show
   a focus ring — a keyboard user scrolling it needs to know where they are. */

.tc-product-grid .tc-product-grid__viewport:focus-visible {
	outline: 2px solid var(--ftm-scheme-text, #fff);
	outline-offset: 4px;
}

.tc-product-grid .tc-product-grid__collection {
	margin-block: 0;
}

/*
 * The track. Doubled class clears WooCommerce's own (0,2,0)
 * `.wc-block-product-template__responsive.columns-6` grid without depending on
 * which stylesheet loads last.
 */

.tc-product-grid .wc-block-product-template.wc-block-product-template {
	display: flex;
	flex-direction: row;
	flex-wrap: nowrap;

	/* Stretch, deliberately: the cards are equal-height by design and the CTA
	   pins to the bottom of the tallest. Safe here because every card carries an
	   explicit flex-basis, so no width is ever derived from a height — the trap
	   TC2-T40's tiles hit. */
	align-items: stretch;
	gap: var(--wp--preset--spacing--xsmall, 0.5rem); /* 8 — frame-bound at every breakpoint */
	width: auto;
	max-width: none;
	margin-block: 0;
	padding: 0;
	list-style: none;
}

.tc-product-grid .wc-block-product-template.wc-block-product-template > li {
	margin-block: 0;

	/* 2-up at mobile. The doubled class again — WooCommerce sizes its own li at
	   (0,3,0) inside a min-width:600px query. */
	flex: 0 0 calc((100% - var(--wp--preset--spacing--xsmall, 0.5rem)) / 2);
	scroll-snap-align: start;
}

@media (min-width: 48rem) { /* breakpoint/md */

	.tc-product-grid .tc-product-grid__slider {
		/* 48 at tablet AND desktop; only mobile drops to 24, so one step. */
		margin-block-start: 3rem;
	}

	.tc-product-grid .wc-block-product-template.wc-block-product-template > li {
		/* 3-up at tablet */
		flex: 0 0 calc((100% - 2 * var(--wp--preset--spacing--xsmall, 0.5rem)) / 3);
	}
}

@media (min-width: 64rem) { /* breakpoint/lg */
	/* Static six-up grid — "not for desktop". `1fr` tracks rather than the
	   frame's literal 226.67 so the row fits any width at or above lg, not only
	   the 1400 the desktop frame happens to draw. Same ladder brand-grid and
	   heading-banner use. */

	.tc-product-grid .tc-product-grid__viewport {
		overflow: visible;
		scroll-snap-type: none;
	}

	.tc-product-grid .wc-block-product-template.wc-block-product-template {
		display: grid;
		grid-template-columns: repeat(6, minmax(0, 1fr));
	}

	.tc-product-grid .wc-block-product-template.wc-block-product-template > li {
		flex: none;
		scroll-snap-align: none;
	}
}

/* ─────────────────────────────────────────────────────────────────────────
 * The card
 *
 * WooCommerce's <li> IS the card; nothing wraps it, so the padding, the radius
 * and the column flow all live on the li itself. Its children are a flow layout,
 * which is why the blockGap zeroing below is not optional — without it every
 * card gains 24px between image, category, title, price and button.
 * ───────────────────────────────────────────────────────────────────────── */

/* ═══════════════════════════════════════════════════════════════════════════
 * ⚠ THE CARD IS SHARED FROM HERE VIA `.tc-product-cards`, AND THAT IS THE WHOLE
 * OF THE "card extraction" T-G WAS WAITING ON.
 *
 * product-detail-plan.md §7 lists T-G as depending on "the card extraction", and
 * §10 scopes that as T-CARD. It was never built — and it does not need to be,
 * for a reason worth writing down: THERE IS NO CARD COMPONENT TO EXTRACT.
 *
 * The card is five WooCommerce blocks — product-image, post-terms, post-title,
 * product-price, product-button — carrying `tc-product-card__*` class names this
 * pattern invented. What is shared is the RULES BELOW. And this file is enqueued
 * on EVERY page whether or not the pattern renders, because
 * ftm_enqueue_pattern_styles() globs every patterns/*.css unconditionally
 * (includes/enqueue.php:70). So a consumer needs no new file, no new mechanism
 * and no moved code — only the class.
 *
 * ── THE THREE CONSUMERS ──────────────────────────────────────────────────────
 *
 *   patterns/product-grid/product-grid.php:96     the section wrapper
 *   templates/archive-product.html:125            the product-collection block
 *   patterns/product-detail/product-detail.php:508  ditto (Related Products)
 *
 * The class marks the SCOPE, not a fixed depth: product-grid carries it on its
 * own section wrapper because it has one, and the other two carry it on the
 * collection because they do not. Every rule below is a descendant selector, so
 * the depth does not matter — only that the class is an ancestor of the card.
 *
 * ⚠ SCOPE, DELIBERATELY: the rules ABOVE this comment — the product-TEMPLATE
 * layout, the 6/3/2 flex track and the scroll-snap slider — are NOT shared and
 * key off `.tc-product-grid` alone. They are the product-grid SECTION's layout,
 * not the card. Related Products is a 4/2/1 grid with no slider at all, and the
 * Shop Archive is a filtered 4-up with neither. Two class names, two jobs:
 * `.tc-product-grid` is one section, `.tc-product-cards` is the card kit.
 *
 * ── COLLAPSED IN T-SA-6 PER THIS NOTE ────────────────────────────────────────
 *
 * HISTORY. Until T-SA-6 every rule below read
 * `:is(.tc-product-grid, .tc-product-cards)`, because product-grid was the
 * original owner and Related Products was bolted on as a second consumer
 * without editing the shipped pattern. That note said the collapse to
 * `.tc-product-cards` alone was "the right end state — do it when Shop Archive
 * lands as the THIRD consumer". Shop Archive landed (T-SA-1…T-SA-5), so T-SA-6
 * did it: `tc-product-cards` was added to the product-grid wrapper and the
 * `:is()` was deleted from nineteen rules.
 *
 * ⚠ IT WAS A RENAME, NOT A MOVE, AND THAT IS WHY IT WAS SAFE. The class went on
 * the SAME element the `:is()` was already matching through, so the scope root
 * never moved. Measured rather than asserted: matched-element sets for all 68
 * rules in this file that mention either hook, across six pages, 408 rule/page
 * pairs — identical before and after.
 *
 * SPECIFICITY IS UNCHANGED ON ALL TWENTY RULES, computed both ways. `:is()`
 * takes the highest specificity of its arguments and both arguments were a
 * single class, so dropping it for a single class costs nothing.
 *
 * ⚠ AND THE OLD NOTE'S "every rule below stays (0,2,0)" WAS NEVER TRUE — it is
 * repeated here only so it is not reinstated. Thirteen rules are (0,2,0); the
 * other seven are higher and always were, because they carry a type selector, a
 * pseudo-element or a `:not()` on top of the two classes: `img` and `a` at
 * (0,2,1), the badge and the button at (0,3,0), `::after` at (0,3,1), the two
 * `:hover`/`:focus-visible` pairs at (0,3,1) and (0,4,0). What is true, and is
 * the claim that matters, is that NO rule's specificity CHANGED.
 *
 * The lessons this card carries — the aspectRatio/scale block attributes that
 * beat an inline style, the price-less `:not(:has())` collapse for 409 of 1,171
 * products, the CTA pinned to the card's bottom (retired for the listings in
 * I17 round 2, 2026-10-02; restored on every consumer at the client's request,
 * 2026-10-06), the CTA fill binding, and
 * explicit colours on both title and link — are now had by all three consumers
 * without being written three times.
 * ═══════════════════════════════════════════════════════════════════════════ */

.tc-product-cards .wc-block-product {
	display: flex;
	flex-direction: column;
	border-radius: 4px;
	background-color: var(--wp--custom--color--base--white, #fff);

	/* Mobile draws the image flush to the card edge and insets only the text;
	   tablet and desktop inset everything by 8. */
	padding: 0 0 var(--wp--preset--spacing--xsmall, 0.5rem);
	color: var(--wp--custom--color--primary--800, #0a205b);
}

.tc-product-cards .wc-block-product > * {
	margin-block: 0;
}

.tc-product-cards .wc-block-product > * + * {
	margin-block-start: var(--wp--preset--spacing--xsmall, 0.5rem);
}

/* Everything but the media carries the text inset at mobile. */

.tc-product-cards .wc-block-product > :not(.tc-product-card__media) {
	padding-inline: var(--wp--preset--spacing--xsmall, 0.5rem);
}

.tc-product-cards .tc-product-card__media {
	margin-block: 0;
}

/*
 * The square and the fit are set as BLOCK ATTRIBUTES, not here — WooCommerce
 * writes `aspect-ratio` and `object-fit` inline on the <img>, which no rule in
 * this file could beat without !important. See the pattern. This owns only what
 * is left: the box the image fills, and its radius.
 */

.tc-product-cards .tc-product-card__media img {
	display: block;
	width: 100%;
	border-radius: 2px;

	/*
	 * ⚠ A MISSING IMAGE BLEW THE CARD UP TO 1502px, AND THE INLINE SQUARE DID
	 * NOT STOP IT (QA N1 / C4, fixed QA batch A3 2026-09-28).
	 *
	 * Measured with product images failing to load: 118×1502 on /shop/ at
	 * 390, 3000×3000 on the home carousel at 768, 351×1500 on search. Same
	 * cause build-knowledge records for media-basic and tabbed-timeline (About
	 * page addition, 2026-09-07): WordPress puts `sizes="auto, …"` on lazy
	 * attachment images, the UA sheet gives those `contain: size`, and core
	 * sets `contain-intrinsic-size: 3000px 1500px`. A broken image has no
	 * natural size to fall back on, so the placeholder wins over the inline
	 * `aspect-ratio: 1`.
	 *
	 * `none` removes core's guess rather than replacing it, so the square is
	 * the only thing left to size the box. `aspect-ratio: 1` is restated as a
	 * floor for any card markup that arrives without the block's inline style
	 * (the inline one wins where it exists). The square is every card frame's
	 * and the pattern's `aspectRatio: "1"`. With images failing: 118×118 and
	 * 208×208. Inert where images load — the square they already drew.
	 */
	aspect-ratio: 1;
	contain-intrinsic-size: none;
}

/*
 * ── THE PHOTO'S BORDER — TWO OF THE THREE CONSUMERS, NOT THE KIT ────────────
 *
 * Added 2026-08-31 for the Shop Archive (design review item 4d) and the PDP's
 * Related Products (item 7, the rider — "same card kit"). One rule rather than
 * two, because it is one decision and splitting it across two stylesheets is
 * how the plus glyph went missing in T-SA-7: a card property left behind in a
 * section's file when the card was shared out.
 *
 * ⚠ `.tc-product-grid` IS DELIBERATELY EXCLUDED, and this is the whole reason
 * the rule is scoped rather than hung on `.tc-product-cards`.
 * `docs/design/product-grid/desktop.png` draws its photos on bare white with no
 * frame around them — verified at @2x against the card box measured at 453px
 * wide — and that section's cards sit on a NAVY band where a neutral-200
 * hairline would be a visible box drawn around nothing. The archive and the
 * related rail both sit on white, where the frame reads as a card edge.
 *
 * Neutral-200 `#DEE2E6` is the same hairline the contact form's fields and this
 * archive's own group dividers use; nothing new enters the palette.
 *
 * ⚠ THE COLOUR AND THE RADIUS ARE RESTATED FOR THE PDP RAIL, 2026-09-05, AND
 * ONLY THE ARCHIVE'S ARE STILL UNMEASURED. The neutral-200 above was chosen on
 * the palette argument in the sentence before this one, because — as that
 * build's own report says — the archive card has no exported frame and it
 * "ships on the review's instruction alone". The related rail DOES have one,
 * and all four desktop cards (43324:17464, :17488, :17506, :17524) draw
 * neutral-100 `#E9ECEF` at `rounded-[4px]`. `product-detail.css` restates both
 * at (0,3,1); this rule keeps `box-sizing` and the 1px for both consumers.
 * The archive is left alone deliberately — with no frame, moving it would swap
 * one unmeasured value for another.
 *
 * `box-sizing: border-box` so the 1px comes out of the box rather than growing
 * it — the image is width-constrained by its column and a grown box would
 * overflow the card's 8px inset by exactly 2px.
 */
.tc-shop-archive .tc-product-card__media img,
.tc-pdp__related .tc-product-card__media img {
	box-sizing: border-box;
	border: 1px solid var(--wp--custom--color--neutral--200, #dee2e6);
}

/* ── The "New" badge (T-SA-5) ────────────────────────────────────────────────
 *
 * Injected by includes/product-new-badge.php as the last child of the anchor
 * WooCommerce wraps the photo in. Drawn on 43324:19041 (desktop), 43324:18342
 * (tablet) and 43324:17639 (mobile), where the layer is named "Products Count
 * Container" — a copy-paste artifact from the category tiles, not a count.
 *
 * ⚠ IT LIVES IN THE SHARED `:is()` SCOPE ON PURPOSE, AND THE MARKUP FILTER IS
 * GATED TO MATCH IT EXACTLY. Three of the theme's four `woocommerce/product-
 * image` instances carry `tc-product-card__media` and are reached by these
 * rules; the fourth is the PDP's cross-sell, which is its own scope and owns
 * its own card CSS. An ungated filter would drop a bare "New" text node over
 * that photo — worse than no badge. T-SA-6 widens the scope and the gate in one
 * move; do not widen either alone.
 *
 * ⚠ THE HEIGHT IS STATED, NOT DERIVED, AND IT IS 25 SINCE 2026-09-17 (G19).
 * Until then this note read "25 IS NOT A CSS NUMBER … Do not chase 25": all
 * three boards draw the box 25 tall because Figma trims the label's 16px line
 * box to its 9px cap height, and the same padding and line box give
 * 8 + 16 + 8 = 32 in a browser. That diagnosis was right and still is — the
 * CSS matched the frame's STATED values exactly (12px Inter Bold, 16
 * line-height, 8 padding). What changed is the ruling: Gary raised the 32
 * (G19), the designer confirmed he wants the browser to match the Figma
 * rendering, and docs/reports/chip-badge-lineheight-diagnosis.md worked out
 * the recipe. So `padding-block` is 4.5 (4.5 + 16 + 4.5 = 25), padding-inline
 * stays the frame's 8, and the stated height is 25 to match. THIS ADDS
 * SOMETHING THE FRAME DOES NOT SPECIFY; it does not correct an error, and the
 * ledger says so (DECISIONS 2026-09-17 §4).
 *
 * `text-box-trim` — the property that models the trim literally — was
 * rejected on 2026-09-17 after checking support (86% global; the inline-
 * element sub-feature the PDP badge would need is at 11%): it degrades to 32,
 * which means two badge heights across browsers on a storefront. Do not
 * revisit it without re-checking support. Same recipe on the PDP badge
 * (product-detail.css), the post-card pill kit (style.css), its blog-post
 * restatement and the category-grid count badge — five rules, no shared
 * selector, all moved together.
 *
 * ⚠ THE BADGE IS tertiary-600 WITH WHITE TEXT, WHICH IS WHAT 43324:19041 DRAWS.
 * The fill binds the ramp step, not a hex, so it moves if the ramp moves —
 * which is exactly how it got here. Assertion 5 checks that resolution, not
 * just the painted rgb().
 *
 * ⚠ CORRECTION TO WHAT THIS COMMENT SAID UNTIL 2026-08-19, AND IT SAID THE
 * OPPOSITE IN CAPITALS. It read:
 *
 *   "THE COLOUR IS A RECORDED DEVIATION FROM THE DESKTOP BOARD, DECIDED BEFORE
 *    THE BUILD — DO NOT 'CORRECT' IT BACK. ... Ships primary-100 with black at
 *    EVERY breakpoint — it is what two of the three boards already say."
 *
 * That instruction was right when it was written and is now void. It rested on
 * one fact: white on tertiary-600 measured 3.77:1, because tertiary-600 was
 * #059669, and 12px bold is not "large text" so the 4.5:1 floor applies. On
 * 2026-08-19 Gary re-picked tertiary-600 as #04865D. White on it is 4.59:1 and
 * PASSES. The board never changed — the ramp step it names did — so the only
 * objection to the board's own pairing is gone and the deviation with it.
 *
 * ⚠ THE SECOND HALF OF THAT SENTENCE WAS NEVER SOUND, AND IS WITHDRAWN RATHER
 * THAN JUST OVERTAKEN. "What two of the three boards already say" counted the
 * tablet (43324:18342) and mobile (43324:17639) boards as colour evidence. They
 * are wireframe-tier: their pale blue is placeholder fill inherited from the
 * category tiles, of a piece with the layer being misnamed "Products Count
 * Container" on all three boards, which the note above already identifies as a
 * copy-paste artifact. THE DESKTOP BOARD IS THE AUTHORITY FOR THIS BADGE. A
 * board count is not a substitute for asking which board decides.
 *
 * The AA arithmetic that argument was attached to was correct and still is —
 * 3.77:1 failing, 12.11:1 passing, both recomputed on 2026-08-19. Given the old
 * ramp value, primary-100 with black WAS the right thing to ship. What is
 * withdrawn is the majority-of-boards reasoning, not the outcome it defended at
 * the time. Full account: docs/DECISIONS.md 2026-08-19, and the T-SA-5 report
 * §5.3 which is left as written because it is a dated record.
 *
 * ⚠ SO: DO NOT RESTORE primary-100 WITH BLACK FROM A BOARD READING. Two of the
 * three boards still draw it, and that is now a trap rather than a hint. If
 * tertiary-600 is ever reverted toward #059669 this badge goes illegal again —
 * assertion 5 pins that negatively and is what will say so.
 *
 * ⚠ THE BRIEF CITED `figma-token-translation.md §7` HERE FOR THE PRECEDENT AND
 * THAT FILE DOES NOT EXIST. It is project knowledge, not a repo document, and
 * git has no record of it under any name — established 2026-08-06 by
 * docs/reports/secondary-600-ada.md §8, whose own brief named it three times.
 * The in-repo record of these ratios is docs/DECISIONS.md plus that report.
 * Standing list 2 carries this item and what shipped against it.
 *
 * Type is the Label/Small role at weight 700 where the role is 500, stated as
 * an override rather than as a new type style — exactly the treatment
 * `.tc-product-card__cat` below already uses, and `label-small` is
 * `"fluid": false` so it is a flat 12 at every width.
 *
 * ⚠ NO `position: relative` IS DECLARED HERE AND THAT IS DELIBERATE.
 * WooCommerce's own `.wc-block-components-product-image { position: relative }`
 * already supplies the containing block — measured on the archive, on Related
 * and on the PDP. Restating it would be duplicating a third party's declaration
 * rather than depending on it visibly. The dependency is pinned by an assertion
 * instead, so if a Woo release drops it the spec fails loudly rather than the
 * badge drifting to some far ancestor.
 *
 * ⚠ `pointer-events: none` KEEPS THE PHOTO CLICKABLE. The badge sits inside the
 * product link; without this it would be a dead 43px target over the top-left
 * of every new product's image.
 * ────────────────────────────────────────────────────────────────────────── */

.tc-product-cards
	.tc-product-card__media
	.tc-product-card__badge {
	position: absolute;

	/* 4 at mobile. The md query at the foot of this section raises both to 6. */
	inset-block-start: 0.25rem;
	inset-inline-start: 0.25rem;
	block-size: 1.5625rem; /* 25 — STATED, and the boards' 25. See the note above (G19) */
	border-radius: 0 0 0 0.5rem; /* bottom-left 8 only, all three boards agree */
	background-color: var(--wp--custom--color--tertiary--600, #04865D);
	padding-block: 4.5px; /* 4.5 + 16 + 4.5 = 25 — G19; the frame's 8 is what the trim leaves of 8 */
	padding-inline: var(--wp--preset--spacing--xsmall, 0.5rem); /* 8 — the frame's */
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: var(--wp--preset--font-size--label-small, 0.75rem); /* 12 */
	font-weight: 700;
	line-height: 1.3333; /* 16 */
	letter-spacing: 0.002em; /* 0.2% = 0.024px at 12 */
	color: var(--wp--custom--color--base--white, #fff);
	pointer-events: none;
}

.tc-product-cards .tc-product-card__cat {
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: var(--wp--preset--font-size--label-small, 0.75rem); /* 12 */
	font-weight: 700;
	line-height: 1.3333; /* 16 */
	color: var(--wp--custom--color--primary--800, #0a205b);
}

.tc-product-cards .tc-product-card__cat a {
	color: inherit;
	text-decoration: none;
}

/*
 * The colour is stated on BOTH the heading and its link, and neither is
 * `inherit`. The card is a white box inside a `ftm-scheme-primary` section, so
 * the scheme's heading and link roles both resolve to WHITE here and paint the
 * title white-on-white — invisible, and invisible in a way a passing render
 * check does not catch, because the markup is all present. `inherit` is not
 * enough: it inherits from the heading, which is itself being painted white.
 * The category and price never showed the fault only because they were given
 * explicit colours from the start.
 */

.tc-product-cards .tc-product-card__title {
	/*
	 * The board's 6 between category and title (43324:19045), less the 3.5
	 * the board trims off the category's 12/16 line under its baseline:
	 * recipe B (DECISIONS 2026-09-17 §4), so the gap reads as the frame
	 * renders it. 6 is not a spacing token, so it starts from the literal
	 * (I17 round 2, 2026-10-02).
	 */
	margin-block-start: calc(0.375rem - 3.5px); /* 2.5 */
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: var(--wp--preset--font-size--label-medium, 0.875rem); /* 14 */
	font-weight: 500;
	line-height: 1.4286; /* 20 */
	color: var(--wp--custom--color--primary--800, #0a205b);
}

.tc-product-cards .tc-product-card__title a {
	color: var(--wp--custom--color--primary--800, #0a205b);
	text-decoration: none;
}

.tc-product-cards .tc-product-card__title a:hover,
.tc-product-cards .tc-product-card__title a:focus-visible {
	text-decoration: underline;
}

.tc-product-cards .tc-product-card__price {
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: 0.9375rem; /* 15 — label-large, static across modes */
	font-weight: 700;
	line-height: 1.6; /* 24 */
	color: var(--wp--custom--color--primary--800, #0a205b);
}

/*
 * ── SALE PRICE: the sale price, then "Reg $X" on its own line (I17 round 2) ──
 *
 * WooCommerce renders a sale as `<del>regular</del> <ins>sale</ins>` on ONE
 * line, regular first and struck through. Shop Archive Desktop (43324:19059,
 * all six sale cards) draws the sale price, then the regular price on its own
 * line as "Reg $2,591.00" in neutral-500 Label/Small, no strike-through. Card
 * consumers only (`.tc-product-cards`); the PDP and its Recommended section
 * keep WooCommerce's output.
 *
 * CSS only. Woo's two `screen-reader-text` spans ("Original price was …",
 * "Current price is …") are untouched and still carry the prices to
 * assistive tech; the `del` and `ins` are `aria-hidden` already, so the
 * visual swap (sale first) and the "Reg" prefix never reach a screen reader.
 * The sr spans are absolutely positioned and the separating space is
 * whitespace, so neither takes part in the column.
 *
 * ⚠ "Reg " IS CSS GENERATED CONTENT, not a translatable string. The site is
 * English-only; a translation would need it moved to PHP.
 *
 * Variable products with a sale RANGE keep WooCommerce's default: Woo prints
 * a range there, with no `del`, so none of this matches.
 *
 * The sale price keeps the card's price type. The board draws 16; the card
 * stays 15 by the TC2-T36 frame verification (DECISIONS 2026-07-29, "the card
 * type is flat at 12 / 14 / 15px … matching label-large").
 * ────────────────────────────────────────────────────────────────────────── */

.tc-product-cards .tc-product-card__price .wc-block-components-product-price:has(> del) {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
}

.tc-product-cards .tc-product-card__price ins {
	order: 1;
	text-decoration: none;
}

/*
 * The board's 6 from the price to the Reg line, less the 3.5 trimmed off the
 * top of the Reg text's 12/16 line (recipe B). 6 is not a spacing token.
 */
.tc-product-cards .tc-product-card__price del {
	order: 2;
	margin-block-start: calc(0.375rem - 3.5px); /* 2.5 */
	font-size: var(--wp--preset--font-size--label-small, 0.75rem); /* 12 */
	font-weight: 500;
	line-height: 1.3333; /* 16 */
	letter-spacing: 0.002em;
	color: var(--wp--custom--color--neutral--500, #6c757d);
	text-decoration: none;
}

.tc-product-cards .tc-product-card__price del::before {
	content: "Reg ";
}

/*
 * A product with no price still renders the price BLOCK — an outer div holding
 * an inner div holding whitespace — and 119 of 400 sampled products in this
 * catalogue are priceless. `:empty` does not reach it, because a whitespace text
 * node is content as far as that selector is concerned; the test has to be for
 * the amount itself. Collapsing the box keeps those cards from carrying a
 * phantom 24px line above the CTA, which is what the comp's own priceless card
 * draws.
 */

.tc-product-cards .tc-product-card__price:not(:has(.woocommerce-Price-amount)) {
	display: none;
}

/* ── Stock status ────────────────────────────────────────────────────────────
 *
 * `woocommerce/product-stock-indicator`, placed after the price in
 * `templates/archive-product.html`. Before it, an out-of-stock product was
 * indistinguishable from an in-stock one on the archive: measured on
 * /product-category/headend-electronics-racks-and-cabinets-cabinets-enclosures/,
 * DWR-35-26 and MRK-4031 rendered a full price and a card with **no stock
 * indication of any kind**. The only tell was the CTA reading "Read more"
 * instead of "Add to cart", which is a WooCommerce side effect and not a label
 * anybody reads as "out of stock".
 *
 * ⚠ IT COSTS NOTHING ON THE OTHER 1,163 PRODUCTS. The block returns an EMPTY
 * STRING when `get_availability()` has no text, which is every in-stock product
 * that does not manage stock — so no box, no margin, no collapsed line. That is
 * why this is the block rather than a badge: it is self-gating.
 *
 * A LABEL, NOT A BADGE, AND NOT FOR WANT OF AMBITION. No board draws an
 * out-of-stock card at any breakpoint, so a badge would mean inventing its
 * placement, its fill and its behaviour when it collides with the "New" badge
 * that already owns the media's top-left corner. A label under the price
 * invents nothing and is what WooCommerce's own templates do.
 *
 * TYPE follows the card's other two label roles — `__cat` at 12/700 and
 * `__price` at 15/700 — one step below the price so it reads as subordinate to
 * it. `label-medium` is `"fluid": false`, a flat 14 at every width.
 *
 * COLOUR IS THE DESIGN SYSTEM'S OWN STOCK SEMANTIC, lifted from where it
 * already lives: `design-system/build/woocommerce.css:159-162` paints
 * `.stock.out-of-stock` `error-600`, and that is what the PDP already renders
 * for the same product.
 *
 * ⚠ BACKORDER IS DELIBERATELY LEFT ON THE CARD'S TEXT COLOUR. The design
 * system styles in-stock and out-of-stock and says nothing about
 * `available-on-backorder`; the PDP renders it in the page's navy. A
 * `warning-600` here would be inventing a third semantic in a place with no
 * board and no precedent, and it would disagree with the PDP.
 * ────────────────────────────────────────────────────────────────────────── */

.tc-product-cards .tc-product-card__stock {
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: var(--wp--preset--font-size--label-medium, 0.875rem); /* 14 */
	font-weight: 700;
	line-height: 1.4286; /* 20 */
	color: var(--wp--custom--color--primary--800, #0a205b);

	/*
	 * ⚠ `start` IS LOAD BEARING. WooCommerce centres this element from
	 * `product-collection-style.css` —
	 * `.wp-block-woocommerce-product-collection .wc-block-components-product-stock-indicator
	 * { text-align: center }` — so it shipped centred under a left-aligned
	 * price and title, which read as a mistake rather than as emphasis.
	 * Found by asking the browser which rule matched, not by guessing; there
	 * is nothing about this element that suggests it.
	 */
	text-align: start;
}

.tc-product-cards
	.tc-product-card__stock.wc-block-components-product-stock-indicator--out-of-stock {
	color: var(--wp--custom--color--error--600, #dc2626);
}

/* ── Add to Cart, and the card's flow (I17 round 2, 2026-10-02) ─────────────
 *
 * EVERY CONSUMER PINS THE BUTTON (client request, Sean via Jim, "Align
 * buttons on product page", 2026-10-06). `margin-block-start: auto` below
 * takes a card's spare height and puts it between the last content item and
 * the button, so the button's bottom is the card's and, because every track
 * stretches its cards to the row's height, button tops line up across each
 * row. The padding is the least the button ever sits below the content. What
 * differs by consumer is only that minimum, and the button's width.
 *
 * THE CURATED ROW — the home product grid — is the kit as written: a
 * full-width CTA, 16 of padding above it. This is the rule on main before
 * #309, restored as the kit's default (owner, PR #310 follow-up A), and its
 * own board draws it (TC2-T36: "five of the six desktop cards").
 *
 * THE LISTINGS — Shop Archive (shop, category, tag, brand), search results,
 * the cart's cross-sells and the PDP Related rail — take the listing button
 * ("THE LISTING VARIANT" below: label width, left) and a 12 minimum, Frame
 * 135's gap on all 30 non-sale cards of Shop Archive Desktop 43324:19027;
 * 12.5 after a Reg line (the rule below) and 24 below 48rem on a card with no
 * price ("PHONES KEEP TWO GAPS"). Only the Reg case is here; the 12 lives on
 * the listing variant, in its `:is()` scope.
 *
 * ⚠ THE LISTINGS FLOWED TOP-DOWN FROM 2026-10-02 TO 2026-10-06, AND THE
 * CLIENT REVERSED THAT. Gary withdrew the row alignment in I17 round 2 (#310)
 * and the PDP Related rail followed on 2026-10-06 ("Related Products Grid Has
 * Extra Side Padding"): each button directly under its own content, tops NOT
 * aligned, the spare height below the button — which is also what boards
 * 43324:19027, :17454, :17192 and :16887 draw. The client then asked for the
 * buttons aligned, and that supersedes the row-alignment part of both
 * rulings and nothing else in them (DECISIONS 2026-10-06). The boards were
 * not redrawn; this departs from them on purpose.
 *
 * The PDP's Related rail is reached as `.tc-pdp` in the listing `:is()`:
 * Related is the only `.tc-product-cards` inside the PDP (the Recommended
 * section and the cross-sell deliberately do not carry the class,
 * product-detail.php), and the class sits on the collection itself, so
 * `.tc-pdp__related .tc-product-cards` would match nothing — the cart's case
 * below.
 *
 * ⚠ #309's bottom group stays gone everywhere: no auto margin on stock or
 * price, and no `order` swap, so the visual order is the DOM order on every
 * consumer. Only the space above the button absorbs a row's difference.
 * ────────────────────────────────────────────────────────────────────────── */

.tc-product-cards .tc-product-card__button {
	margin-block-start: auto;
	padding-block-start: var(--wp--preset--spacing--normal, 1rem); /* 16 */
}

/* After a "Reg" line: the board's 16 runs from the trimmed baseline of the
   Reg text, so recipe B takes the 3.5 trim back out (see the Reg rule). */
:is(.tc-shop-archive, .tc-search-results, .wp-block-woocommerce-cart, .tc-pdp)
	.tc-product-cards
	.wc-block-product:has(> .tc-product-card__price del .woocommerce-Price-amount)
	> .tc-product-card__button {
	padding-block-start: calc(var(--wp--preset--spacing--normal, 1rem) - 3.5px); /* 12.5 */
}

/*
 * ⚠ PHONES KEEP TWO GAPS WIDER THAN THE BOARD (I17 round 2, 2026-10-02).
 * Gary redrew only the desktop board; tablet and phone take its gaps sized
 * down as they are (owner's direction), except two steps that the recorded
 * phone tap targets need (QA batch A, 2026-09-27; see "Phone tap targets"
 * below), so these are a deliberate departure from the board below 48rem:
 *
 *   - category → title stays 6, not 2.5: the category link's 24 target
 *     (exception D4) reaches 4 past its 16 line, and at 2.5 the title link
 *     covered it (measured 22).
 *   - a card with no visible price puts 24 above its button, not 12: the
 *     title link's 44 target runs 24 below a one-line title, and at 12 the
 *     button covered it (measured 32 on call-for-price cards). With a price
 *     between them the 12 already clears it (20 + 8 + 24 + 12 = 64).
 */
@media (max-width: 47.9375rem) {
	.tc-product-cards .tc-product-card__title {
		margin-block-start: 0.375rem; /* 6 */
	}

	.tc-product-cards
		.wc-block-product:not(:has(> .tc-product-card__price .woocommerce-Price-amount))
		> .tc-product-card__button {
		padding-block-start: var(--wp--preset--spacing--large, 1.5rem); /* 24 */
	}
}

.tc-product-cards .tc-product-card__button .wc-block-components-product-button__button {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 0.25rem; /* 4 */
	width: 100%;
	border: 0;
	border-radius: 2px;

	/* The comp draws secondary-600 and that is what ships again as of
	   2026-08-06. Flag 4 put this on secondary-700 because white on the OLD 600
	   (#0284C7) measured 4.09:1 and failed WCAG AA. secondary-600 is #057DB9
	   now and white on it is 4.53:1 — AA clear, so the remap is closed here and
	   site-wide (footer F1). */
	background-color: var(--wp--custom--color--secondary--600, #057db9);
	padding: var(--wp--preset--spacing--xsmall, 0.5rem);
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: var(--wp--preset--font-size--label-medium, 0.875rem); /* 14 */
	font-weight: 500; /* G01, corrected to 500 on 2026-09-15 */
	line-height: 1.4286; /* 20 */
	color: var(--wp--custom--color--base--white, #fff);
	text-decoration: none;
}

.tc-product-cards .tc-product-card__button .wc-block-components-product-button__button:hover,
.tc-product-cards .tc-product-card__button .wc-block-components-product-button__button:focus-visible {
	background-color: var(--wp--custom--color--secondary--800, #075985);
}

/* ── THE LISTING VARIANT: the button shrinks to its label and sits LEFT ──────
 *
 * The consumers that list products AS RESULTS — the Shop Archive and the
 * search page — draw the button at its own width, left-aligned under the
 * price. The one that shows products as a CURATED ROW — this pattern's grid —
 * draws it spanning the card. Both are frame positions, not a preference, and
 * the split is the reason this is a variant rather than a change to the kit
 * above. (The PDP's related rail was on the curated side until 2026-10-06;
 * its boards draw the 114-wide left button, see "Add to Cart, and the card's
 * flow" above.)
 *
 * `docs/design/product-grid/desktop.png` binds the full-width form directly:
 * six cards, each button flush to the card's 8px inset with the label centred,
 * including the "Call 215-675-2053" card. Changing `width: 100%` at source
 * would move that away from its own board.
 *
 * ⚠ THERE IS NO EXPORTED FRAME FOR THE LISTING FORM. `docs/design/` has no
 * `shop-archive` directory and no search board at all; T-SA-1/3/4 worked from
 * node ids that cannot be read from here. This ships on a design review's
 * instruction — 2026-08-31 item 4e for the archive, 2026-09-01 for search —
 * recorded as instructed rather than dressed up as measured.
 *
 * ── WHY THIS TAKES TWO DECLARATIONS AND 4e ONLY SHIPPED ONE ─────────────────
 *
 * 4e set `width: fit-content` on the anchor and stopped, on the strength of a
 * claim that is FALSE: that `align-items` on the wrapper "never applied because
 * the wrapper is not a flex container". **The wrapper is a flex container, and
 * WooCommerce makes it one** — `product-button.css`:
 *
 *   .wc-block-components-product-button {
 *     align-items: center; display: flex; flex-direction: column;
 *     gap: 12px; justify-content: center;
 *   }
 *
 * `flex-direction: column` puts the CROSS axis horizontal, so `align-items:
 * center` is what centres the box — and it only became visible once 4e made the
 * box narrower than the card. Measured on /shop/ at 1440 before this rule: card
 * 239 wide, wrapper 239, button 141 sitting at dx 49 with 49 to spare on the
 * right. Perfectly centred, under a price at dx 0.
 *
 * So the shrink alone produced a centred small button rather than a left one.
 * Both halves live here now; the archive-only copy in shop-archive.css is
 * retired to a pointer.
 *
 * `flex-start` IS WOOCOMMERCE'S OWN DEVICE, not an invention — it ships
 * `.wc-block-components-product-button.align-left { align-items: flex-start }`
 * for exactly this. The class is not available to us (the block writes
 * `align-` with nothing after it, measured in the wrapper's className), so the
 * declaration is repeated rather than the class applied.
 *
 * `fit-content` AND NOT `auto`: the anchor is `display: flex` from the kit
 * above, so it is a block-level flex container and `auto` would still fill the
 * line. That reasoning is 4e's and is carried over rather than re-derived.
 *
 * `justify-content: center` on the anchor is LEFT ALONE deliberately. Once the
 * box is the label's own width it centres nothing, and it is the kit's — the
 * home grid still wants it.
 *
 * THE CALL-FOR-PRICE VARIANT IS COVERED WITHOUT NAMING IT.
 * `includes/product-call-for-price.php:211` writes `tc-product-card__button`
 * onto its replacement div (`FTM_CALL_CARD_SCOPE_CLASS`), which is precisely
 * why that card measured 141px on /shop/ alongside the real add-to-carts.
 *
 * SPECIFICITY, NOT LOAD ORDER. `:is()` contributes its most specific argument —
 * one class — so these land at (0,3,0) and (0,4,0) against the kit's (0,2,0)
 * and (0,3,0) and WooCommerce's (0,1,0). They win wherever the sheets land,
 * which matters because `shop-archive.css` and `search-results.css` are
 * enqueued from includes/enqueue.php and this file is glob-loaded.
 *
 * THE CART'S CROSS-SELLS ARE THE THIRD LISTING (D-CC-1, 2026-09-28): the
 * designer asked for the shop's card there, so they take the shop's button.
 * `.wp-block-woocommerce-cart` is the ancestor rather than the collection's own
 * `tc-cart-crosssell`, because that class sits on the SAME element as
 * `.tc-product-cards` and a descendant combinator would never match it. The
 * mini-cart is a different block and is not reached.
 * ────────────────────────────────────────────────────────────────────────── */

:is(.tc-shop-archive, .tc-search-results, .wp-block-woocommerce-cart, .tc-pdp) .tc-product-cards .tc-product-card__button {
	align-items: flex-start;

	/* The kit's `margin-block-start: auto` pins the button; this is only the
	   minimum above it. See "Add to Cart, and the card's flow" above (client
	   request, 2026-10-06). */
	padding-block-start: var(--wp--preset--spacing--small, 0.75rem); /* 12 */
}

:is(.tc-shop-archive, .tc-search-results, .wp-block-woocommerce-cart, .tc-pdp)
	.tc-product-cards
	.tc-product-card__button
	.wc-block-components-product-button__button {
	inline-size: fit-content;
	width: fit-content; /* the kit declares `width`, so beat it in kind */
}

/*
 * The plus glyph. Only on a real add-to-cart: WooCommerce swaps the label to
 * "Read more" on a non-purchasable product, and the frame draws no plus on that
 * card either.
 *
 * ── THE GLYPH IS DECLARED ON THE CARD KIT, NOT ON THE SECTION ───────────────
 *
 * It used to sit in the `.tc-product-grid` custom-property block at the top of
 * this file, beside the arrow and the chevron. Those two are section chrome —
 * the "Shop New Products" link and the slider's prev/next — and belong there.
 * This one is the CARD's, and the card was shared out to `.tc-product-cards` in
 * T-SA-7 while its glyph was left behind.
 *
 * The consequence is worse than a missing icon, and is what a design review
 * reported as "a stray icon right of the label rendering as an empty box". An
 * undefined `var()` makes `mask` invalid, so the mask is dropped — but
 * `background-color: currentcolor` below is still perfectly valid, and paints
 * the whole unmasked 16x16 box. On /shop/ that measured `mask-image: none`,
 * `background-color: rgb(255 255 255)`, 16x16: a solid WHITE SQUARE on the blue
 * button. A missing glyph is invisible; this one is not.
 *
 * Declared here at (0,1,0) so all three consumers of the shared card resolve it
 * — product-grid (whose wrapper carries both classes), the Shop Archive
 * collection in templates/archive-product.html, and the PDP's `tc-pdp__related`.
 * The PDP CROSS-SELL is deliberately unaffected: `.tc-pdp .tc-pdp__crosssell`
 * declares its own plus at (0,2,0) — a different geometry, read off its own
 * board — and beats this on specificity, which is the outcome that file wants.
 */

.tc-product-cards {
	--tc-icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M11.1 11.1V4h1.8v7.1H20v1.8h-7.1V20h-1.8v-7.1H4v-1.8z'/%3E%3C/svg%3E");
}

.tc-product-cards .tc-product-card__button .add_to_cart_button::after {
	content: "";
	flex: 0 0 auto;
	inline-size: 1rem; /* 16 */
	block-size: 1rem;
	background-color: currentcolor;

	/* stylelint-disable-next-line property-no-vendor-prefix -- mask needs the prefix pair */
	-webkit-mask: var(--tc-icon-plus) no-repeat center / contain;
	mask: var(--tc-icon-plus) no-repeat center / contain;
}

/*
 * NO "VIEW CART" LINK ON A CARD (Gary, 2026-10-23).
 *
 * After an AJAX add, `woocommerce/product-button` un-hides a
 * `<span><a class="added_to_cart wc_forward">View cart</a></span>` after the
 * button (ProductButton.php get_view_cart_html()). The span is a flex item in
 * Woo's column wrapper, so it costs its own line plus the wrapper's 12px gap,
 * lifting the "1 in cart" button ~66px above its neighbours' (measured on
 * /shop/ at 1440: 468 against 534). On the home grid it is also near-invisible.
 *
 * It is redundant everywhere this card renders: the header Mini-Cart opens its
 * drawer on add (`addToCartBehaviour: open_drawer`, header.php), and on /cart/
 * — where there is no drawer — the Cart block re-renders its table and totals
 * by itself after a cross-sell add (measured 2026-10-23).
 *
 * The SPAN is hidden, not just the link, so the gap goes too. Scoped to the
 * product-button block inside the shared card, so it reaches every consumer —
 * product-grid, Shop Archive (category, tag, brand), search results, Related,
 * cart cross-sells — and nothing else: the single-product form never prints
 * this span, and Woo's notices are a different element. `wc_forward` with an
 * underscore is the block's class; the notices' is `wc-forward`.
 *
 * The PDP cross-sell is its own scope and carries its own copy,
 * product-detail.css.
 */
.tc-product-cards .tc-product-card__button.wc-block-components-product-button > span:has(> a.added_to_cart.wc_forward) {
	display: none;
}

@media (min-width: 48rem) { /* breakpoint/md */
	/* THE SHARED CARD'S md INSET. Scoped `.tc-product-cards`, so all three
	 * consumers get it — which is what the base rule's own comment has always
	 * promised: "tablet and desktop inset everything by 8".
	 *
	 * WHAT IT DOES, at md (768) and above:
	 *
	 *   product-grid   padding 8px 8px 16px   media inset 8 on all sides
	 *   Related        padding 0              media and text flush — since
	 *                  2026-10-06 (Gary), product-detail.css at (0,3,0)
	 *   Shop Archive   padding 0 0 8px        media flush — BY ITS OWN CHOICE,
	 *                  declared at shop-archive.css:1355 at the same (0,2,0)
	 *                  and winning on source order. Not an omission; see there.
	 *
	 * ── HISTORY: FIXED IN T-SA-7, AFTER T-SA-6 MEASURED AND COSTED IT ──────────
	 *
	 * Until T-SA-7 this rule read `.tc-product-grid` alone, so one of three
	 * consumers got the inset the shared card documented. Both other consumers
	 * carried comments claiming to inherit it — shop-archive.css:1352 and
	 * product-detail.css:1897 — and neither did. T-SA-6 measured that, named it a
	 * defect, and left it: the correct fix crossed its write scope.
	 *
	 * ⚠ THE RESCOPE ALONE WAS NEVER THE FIX, AND THAT IS THE PART WORTH KEEPING.
	 * `product-detail.css:1904` used to set `padding-inline-start: 0` at (0,3,0),
	 * outranking this rule at (0,2,0). Rescoping without deleting it shipped
	 * Related as `8px 8px 16px 0px` — media inset at the top and right, flush at
	 * the left. Measured in the browser at the time: gapTop 8, gapLeft 0. T-SA-7
	 * deleted that declaration in the same change, which is why Related is now
	 * symmetric. If a future edit reinstates any `padding-inline-start` on
	 * `.tc-pdp__related .wc-block-product`, the asymmetry comes back.
	 *
	 * Full measurements: docs/reports/t-sa-7-md-padding.md, and
	 * docs/reports/t-sa-6-card-collapse.md §C4 for the original diagnosis.
	 */

	.tc-product-cards .wc-block-product {
		/* Tablet and desktop inset the media too. */
		padding: var(--wp--preset--spacing--xsmall, 0.5rem) var(--wp--preset--spacing--xsmall, 0.5rem) var(--wp--preset--spacing--normal, 1rem);
	}

	/*
	 * SEARCH RESULTS TAKE THE SHOP ARCHIVE'S CARD (QA batch D, 2026-09-29).
	 * The inset above is drawn for a card that reads as a surface; on the
	 * search page the card is white on a white page, so all that showed was
	 * the image sitting 8 inside the column the H1 and footer use (768-1920).
	 * The Shop Archive puts this same card on the same ground and opts out
	 * (shop-archive.css, "`0 0 8px`, media flush"), so search now matches it:
	 * the media flush to the column, the text inset 8, 8 below. Here rather
	 * than in search-results.css, whose header keeps card styling out of it.
	 * (0,3,0), so it wins over the rule above without relying on order.
	 */
	.tc-search-results__grid.tc-product-cards .wc-block-product {
		padding: 0 0 var(--wp--preset--spacing--xsmall, 0.5rem);
	}

	.tc-search-results__grid.tc-product-cards .wc-block-product > :not(.tc-product-card__media) {
		padding-inline: var(--wp--preset--spacing--xsmall, 0.5rem);
	}

	/*
	 * The badge's inset goes 4 -> 6 at md. The mobile board (43324:17639, 320)
	 * draws 4,4; the tablet (43324:18342, 768) and desktop (43324:19041) boards
	 * both draw 6,6, so md is exactly the boundary.
	 *
	 * ⚠ WRITTEN AS A min-width ADDITION, NOT A below-md OVERRIDE, because THIS
	 * FILE IS MOBILE-FIRST — every one of its eleven queries is `min-width` and
	 * there is no below-md block to add to. Same boundary, same computed result;
	 * the direction follows the file it is in. (shop-archive.css, which does
	 * carry max-width blocks, is a different file with a different convention.)
	 */
	.tc-product-cards
		.tc-product-card__media
		.tc-product-card__badge {
		inset-block-start: 0.375rem; /* 6 */
		inset-inline-start: 0.375rem;
	}
}

/* ─────────────────────────────────────────────────────────────────────────
 * Slider controls
 *
 * Injected by the co-located script, and only while the track actually
 * overflows — so they are absent at lg, absent when fewer cards are chosen than
 * fit, and absent with no JS at all. They are chrome, not content: seeding them
 * as core/buttons would put two deletable blocks in the editor for something an
 * editor must not be able to delete, which is the same call heading-banner made
 * by drawing its globe as a pseudo-element.
 *
 * The frames draw them straddling the band edge, vertically centred on the card
 * row. The mobile frame lets the RIGHT control overhang the band itself while
 * the left stays inside — asymmetric, almost certainly a comp slip; both are
 * placed on the left control's inset here so `overflow: hidden` can keep
 * clipping the globe.
 * ───────────────────────────────────────────────────────────────────────── */

.tc-product-grid .tc-product-grid__control {
	position: absolute;
	z-index: 2;
	inset-block-start: 50%;
	display: flex;
	align-items: center;
	justify-content: center;
	inline-size: 2rem; /* 32 at mobile */
	block-size: 2rem;
	transform: translateY(-50%);
	border: 0;
	border-radius: 50%;
	background-color: var(--wp--custom--color--base--white, #fff);
	padding: 0;
	cursor: pointer;
	box-shadow: 0 2px 8px rgb(5 19 58 / 25%);
	transition: opacity 0.2s var(--wp--custom--easing--standard, ease);
}

.tc-product-grid .tc-product-grid__control::before {
	content: "";
	inline-size: 1.5rem; /* 24 glyph */
	block-size: 1.5rem;
	background-color: var(--wp--custom--color--primary--800, #0a205b);

	/* stylelint-disable-next-line property-no-vendor-prefix -- mask needs the prefix pair */
	-webkit-mask: var(--tc-icon-chevron-left) no-repeat center / contain;
	mask: var(--tc-icon-chevron-left) no-repeat center / contain;
}

.tc-product-grid .tc-product-grid__control--prev {
	inset-inline-start: -0.5rem;
}

.tc-product-grid .tc-product-grid__control--next {
	inset-inline-end: -0.5rem;
}

.tc-product-grid .tc-product-grid__control--next::before {
	transform: scaleX(-1);
}

/*
 * `aria-disabled`, never the `disabled` attribute. A disabled button leaves the
 * tab order and stops being announced, so a keyboard user reaching the end of
 * the track would lose the control entirely rather than be told it is spent.
 * The handler no-ops instead.
 */

.tc-product-grid .tc-product-grid__control[aria-disabled="true"] {
	opacity: 0.4;
	cursor: default;
}

@media (min-width: 48rem) { /* breakpoint/md */

	.tc-product-grid .tc-product-grid__control {
		inline-size: 2.5rem; /* 40 from tablet up */
		block-size: 2.5rem;
	}

	.tc-product-grid .tc-product-grid__control--prev {
		inset-inline-start: -1rem;
	}

	.tc-product-grid .tc-product-grid__control--next {
		inset-inline-end: -1rem;
	}
}

/* ─────────────────────────────────────────────────────────────────────────
 * Reduced motion
 *
 * The whole of it. Nothing here moves on its own — the only motion is the
 * smooth scroll a control press starts, so turning that off is the complete
 * fallback. TC2-T40's marquee needed a static grid instead because it animated
 * unprompted; this one has no animation to fall back FROM.
 * ───────────────────────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
	.tc-product-grid .tc-product-grid__viewport {
		scroll-behavior: auto;
	}

	.tc-product-grid .tc-product-grid__control {
		transition: none;
	}
}

/* ── Phone tap targets (QA batch A, 2026-09-27) ──────────────────────────────
   Measured at 360-430 on every `.tc-product-cards` list (home grid, shop,
   category, PDP related, search): category link 15 tall, title link 20 per
   line, card button 36, carousel arrows 32×32 — against the 44×44 the phone
   brief requires. Every fix below is a transparent pseudo-element or a
   display change that draws nothing, so the frames are untouched.

   - CATEGORY LINK: 24 tall, NOT 44 — a RECORDED EXCEPTION (DECISIONS.md,
     2026-09-27, QA batch A), accepted at the WCAG 2.2 AA 2.5.8 floor. It sits
     6px above the title link and, on home, 8px under the image link, so a 44
     target would overlap both neighbours. 4px up and down keeps a 2px /
     4px clearance.
   - TITLE LINK: made a block (it is the heading's only child, so its line
     boxes do not change) so a hit box can be anchored to it. The ::after
     starts at its top and runs to at least 44: `bottom: calc(100% - 44px)`.
     Below it is the price, which is not a control.
   - CARD BUTTON: 4px above and below its 36 = 44. ::before, because Woo's
     loading skeleton owns ::after on the placeholder state.
   - ARROWS: 6px round a 32 circle = 44. The outer edge lands 14px outside the
     track, inside the 16px gutter, so the section's overflow clip leaves it.
   ────────────────────────────────────────────────────────────────────────── */
@media (max-width: 47.9375rem) {
	.tc-product-cards .tc-product-card__cat a,
	.tc-product-cards .tc-product-card__title a,
	.tc-product-cards .tc-product-card__button .wc-block-components-product-button__button {
		position: relative;
	}

	/*
	 * ONE CATEGORY ON PHONES — the PRIMARY one (QA batch A2, 2026-09-27).
	 *
	 * The card has no category logic of its own: core/post-terms prints every
	 * product_cat term in WordPress's own order, so the card's "first category"
	 * is simply the first link it prints. That is the primary here — no new
	 * rule is invented. On phones the rest, and the ", " separators between
	 * them, are not displayed.
	 *
	 * WHY: when a product's categories wrapped ("Decoders, IRD's, Receivers"
	 * on kyrion DR5000), the links stacked on the 16px line pitch and each got
	 * ~16-20 — below the 24 the recorded exception accepts. One link, one line,
	 * 24. `display: none` also takes the hidden links out of the tab order and
	 * the accessibility tree; every one of them is still on the product page
	 * and in the archive's category filter. 48rem up is unchanged.
	 */
	.tc-product-cards .tc-product-card__cat a ~ a,
	.tc-product-cards .tc-product-card__cat .wp-block-post-terms__separator {
		display: none;
	}

	/* Centred and sized outright rather than inset: an inline box is only as
	   tall as the font's content area (~15 at 12px), not the 16 line, so a
	   fixed -4 came out 23. */
	.tc-product-cards .tc-product-card__cat a::after {
		content: "";
		position: absolute;
		inset-block-start: calc(50% - 12px);
		inset-inline: 0;
		block-size: 24px;
	}

	.tc-product-cards .tc-product-card__title a {
		display: block;
	}

	.tc-product-cards .tc-product-card__title a::after {
		content: "";
		position: absolute;
		inset-block: 0 calc(100% - 44px);
		inset-inline: 0;
	}

	/* Woo clips this button (`overflow: hidden`) for its 0.1s "added" text
	   slide, which would clip the hit box too — measured 36 tall on /shop/.
	   `clip` plus an 8px clip margin (the reach of the hit box below) keeps
	   the clip and lets the target out; at most an 8px sliver of sliding text
	   shows for that 0.1s. The skeleton placeholder keeps Woo's own hidden
	   overflow. */
	.tc-product-cards
		.tc-product-card__button
		.wc-block-components-product-button__button:not(.wc-block-components-product-button__button--placeholder) {
		overflow: clip;
		overflow-clip-margin: 8px;
	}

	/*
	 * The 8 the button needs goes BELOW it, not 4 above and 4 below (QA batch
	 * A2, 2026-09-27). With no price between them — a call-for-price card,
	 * "kyrion DR5000" at 320 — the title sits 24 above the button, so the
	 * title's 44 (20 + 24 below) ended exactly at the button's top edge and the
	 * button's 4 above painted over it: title measured 40. Below the button is
	 * only the card's own bottom padding (24 in listings, 8 on home / related),
	 * so both now reach 44 and meet without overlapping.
	 */
	.tc-product-cards .tc-product-card__button .wc-block-components-product-button__button::before {
		content: "";
		position: absolute;
		inset-block: 0 -8px;
		inset-inline: 0;
	}

	.tc-product-grid .tc-product-grid__control::after {
		content: "";
		position: absolute;
		inset: -6px; /* square: a 44 circle would not hold a 44 square */
	}
}

/*
 * ARROWS BELOW THE TRACK, EVERYWHERE THE CAROUSEL EXISTS (QA batch A2,
 * 2026-09-27; widened to tablet by QA batch A3, 2026-09-28).
 *
 * Centred on the track, the two controls landed ON the first and last
 * visible cards' category row — at 360-430 (A2) and again at 768 and 834
 * (the 2026-09-27 re-review: prev over the start of card 1's category link).
 * Below 64rem they sit in their own row under the track, flush right like the
 * category band's chevrons, so they cannot touch card content at any card
 * height. From 64rem the track is a static grid and the script removes the
 * controls, so this now covers every width that draws them. No frame draws a
 * phone or tablet state (D-QA-1).
 *
 * The script appends both buttons to `.tc-product-grid__slider`, after the
 * viewport, so the slider becomes a wrapping flex row: the viewport takes
 * the whole first line and the pair wraps beneath it. 16 between the pair
 * keeps the two 44 targets 4px apart on phones (32 + 6 + 6) and 12 apart at
 * tablet (40 + 2 + 2); 16 above clears the cards by 10 and 14. No script or
 * markup change.
 *
 * (0,3,0): the base placement rules and core's flow-layout margins are
 * (0,2,0), and the buttons are children of a flow-layout group.
 */
@media (max-width: 63.9375rem) {
	.tc-product-grid .tc-product-grid__slider {
		display: flex;
		flex-wrap: wrap;
		justify-content: flex-end;
		gap: var(--wp--preset--spacing--normal, 1rem) var(--wp--preset--spacing--normal, 1rem); /* 16 */
	}

	.tc-product-grid .tc-product-grid__slider > .tc-product-grid__viewport {
		flex: 1 0 100%;
		min-inline-size: 0;
	}

	.tc-product-grid .tc-product-grid__slider .tc-product-grid__control {
		position: relative;
		inset: auto;
		flex: none;
		margin: 0;
		transform: none;
	}
}

/* Tablet arrow target (QA batch A3): the 40 circle plus 2px all round = 44,
   drawn nothing. Phones get 6 round their 32 in the block above. */
@media (min-width: 48rem) and (max-width: 63.9375rem) {
	.tc-product-grid .tc-product-grid__control::after {
		content: "";
		position: absolute;
		inset: -2px;
	}
}

/* "Shop New Products" (QA batch A3, R4): 28 tall — the 15/24 label beside the
   28 chip. 8px of padding above and below with matching negative margin makes
   it 44 without moving either; the nearest control, the first card's image
   link, is 24px below. 48rem up it is 32 and out of the phone brief. */
@media (max-width: 47.9375rem) {
	.tc-product-grid .tc-product-grid__link a {
		padding-block: 8px;
		margin-block: -8px;
	}
}
