/**
 * Shop archive
 *
 * Pale header band with breadcrumb, title and category pills, then a
 * sidebar + grid body. Cards, buttons and filter rows all share the same
 * lift-and-shadow hover language.
 */

.ml-shop {
	background: var(--ml-white);
}

/* ---------- Header band ---------- */

.ml-shop__head {
	background: linear-gradient(180deg, var(--ml-stage) 0%, var(--ml-stage-soft) 100%);
	padding: 22px 24px 26px;
}

.ml-shop__head-inner {
	max-width: var(--ml-container);
	margin: 0 auto;
}

/*
 * Breadcrumbs (.ml-shop__crumbs) MOVED TO style.css on 2026-09-14 - they render
 * on the policy, track-order, account and checkout pages too, none of which load
 * this file, and fell through to Astra there.
 */

.ml-shop__title {
	margin: 0 0 14px;
	font-size: clamp(26px, 3.4vw, 38px);
	font-weight: 700;
	letter-spacing: -0.02em;
	color: var(--ml-navy);
}

.ml-shop__intro {
	margin: 0 0 22px;
	max-width: 62ch;
	font-size: 15px;
	line-height: 1.6;
	color: var(--ml-body);
}

/* ---------- Category pills ---------- */

.ml-shop__pills {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
}

.ml-shop__pill {
	display: inline-flex;
	align-items: center;
	padding: 9px 18px;
	border-radius: 999px;
	border: 1px solid var(--ml-line);
	background: var(--ml-white);
	color: var(--ml-navy);
	font-size: 14px;
	font-weight: 500;
	text-decoration: none;
	white-space: nowrap;
	transition: transform var(--ml-t-base) var(--ml-e-standard), box-shadow var(--ml-t-base) var(--ml-e-standard), border-color var(--ml-t-base) var(--ml-e-standard), background-color var(--ml-t-base) var(--ml-e-standard), color var(--ml-t-base) var(--ml-e-standard);
}

/*
 * THE :focus PIN. This pill is an <a>, so Astra's inline
 *
 *     a:focus { color: var(--ast-global-color-1); outline: thin dotted }
 *
 * at (0,1,1) beats the rest rule's (0,1,0). A pointer click sets :focus but
 * NOT :focus-visible, so a clicked pill was left with #0098d1 ink and a 1px
 * dotted outline until focus moved elsewhere. Measured, not assumed:
 * forcePseudoState(['focus']) returned ink rgb(0, 152, 209) and
 * "outline 1px rgb(0, 152, 209) off 0px".
 *
 * ONLY THE TWO PROPERTIES ASTRA SETS ARE PINNED - color and outline. Astra
 * leaves background and border-color alone on an anchor, and the hover rule
 * below keys on :focus-visible rather than :focus, so a mouse click never
 * triggered the lift or the shadow either; pinning transform or box-shadow
 * here would be restating something that was never wrong. CLAUDE.md's rule:
 * pin what Astra sets, and only what the rest rule names.
 *
 * :focus:not(:focus-visible) rather than a bare :focus, matching the pharma
 * tab exactly, so it cannot swallow the keyboard ring no matter where it sits
 * in the file. It is (0,3,0), which clears Astra's (0,1,1).
 *
 * THE ACTIVE PILL NEEDS ITS OWN PIN because of that same (0,3,0): it outranks
 * .ml-shop__pill.is-active at (0,2,0), so without the second rule a clicked
 * selected pill would take navy ink on its cyan fill.
 *
 * KNOWN TRADE, same as pharma: at (0,3,0) the pin also outranks the hover rule
 * at (0,2,0), so a pill that is still under the pointer right after a click
 * shows its rest ink rather than the hover ink. On this surface that window is
 * a page load long - the pill is a real link and clicking it navigates - so it
 * is not worth a :not(:hover) that would make the two divisions' pins differ.
 */
.ml-shop__pill:focus:not(:focus-visible) {
	color: var(--ml-navy);
	outline: none;
}

.ml-shop__pill.is-active:focus:not(:focus-visible) {
	color: var(--ml-white);
}

.ml-shop__pill:hover,
.ml-shop__pill:focus-visible {
	transform: translateY(-2px);
	border-color: var(--ml-cyan);
	color: var(--ml-cyan);
	box-shadow: 0 10px 18px -12px rgba(10, 37, 64, 0.6);
	outline: none;
}

.ml-shop__pill.is-active {
	background: var(--ml-cyan);
	border-color: var(--ml-cyan);
	color: var(--ml-white);
	font-weight: 600;
	box-shadow: 0 10px 20px -12px rgba(0, 175, 240, 0.9);
}

.ml-shop__pill:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
}

/* ---------- Body layout ---------- */

.ml-shop__body {
	padding: 26px 24px 56px;
}

.ml-shop__body-inner {
	max-width: var(--ml-container);
	margin: 0 auto;
	display: grid;
	grid-template-columns: 268px minmax(0, 1fr);
	gap: 28px;
	align-items: start;
}

/* ---------- Sidebar filters ---------- */

/*
 * THE SIDEBAR IS A COLUMN OF CARDS NOW, NOT ONE CARD WITH HAIRLINES.
 *
 * Changed 2026-09-07, when the three checkbox facets became instances of the
 * shared accordion (`assets/css/accordion.css`, the same component the product
 * page uses for its description sections).
 *
 * `.ml-filters` used to BE the card - white fill, 1px border, 12px radius,
 * shadow - with `.ml-filters__group` hairlines dividing it. Leaving that in
 * place would have put a bordered card inside a bordered card for every facet:
 * the "nested double card" this theme already hit once on the logged-out
 * account screen. So the form is now a plain flex column and the CARDS ARE THE
 * SURFACES, exactly as on the product page where `.ml-accordion` sits directly
 * on the page background.
 *
 * THIS BLOCK IS AN INSTANCE. Per the contract in accordion.css it may set
 * spacing, and it does - nothing else. It does not re-declare the card, the
 * trigger, the chevron or the animation; if a `grid-template-rows` or a chevron
 * `rotate` ever appears in this file, the split has failed.
 */
.ml-filters {
	display: flex;
	flex-direction: column;
	gap: 12px;
	background: none;
	border: 0;
	border-radius: 0;
	padding: 0;
	box-shadow: none;
}

/*
 * The component ships `margin-top: 20px` for its place in
 * `.ml-product__summary`. Here the column's `gap` already does that job, and
 * the margin would stack on top of it.
 */
.ml-filters .ml-filters__accordion {
	margin-top: 0;
}

/*
 * `.ml-accordion__item` carries `margin: 0 0 12px` for the product page, where
 * the items are not in a gapped container. They are here, so the last item
 * would leave 12px of dead space above Price Range on top of the gap.
 */
.ml-filters .ml-filters__accordion .ml-accordion__item:last-child {
	margin-bottom: 0;
}

/*
 * PRICE RANGE IS THE ONE `.ml-filters__group` LEFT, and it is deliberately NOT
 * an accordion item.
 *
 * A slider is a control you set, not a list you scan - collapsing it would hide
 * the one filter whose current value is not obvious from the grid. So it keeps
 * the card, drops the trigger, and its padding is built from the component's
 * own two numbers so the two card types cannot drift:
 *
 *     16px 18px   .ml-accordion__trigger padding
 *      0 18px 16px  .ml-accordion__body padding
 *   -> 16px 18px 18px on a card with no trigger
 *
 * The old rule here was `padding: 16px 0; border-bottom` - a hairline divider
 * inside the single card that no longer exists.
 */
.ml-filters .ml-filters__group {
	padding: 16px 18px 18px;
	border: 1px solid var(--ml-line);
	border-radius: 12px;
	background: var(--ml-white);
}

/*
 * Matched to `.ml-accordion__trigger` - 16px/600 navy, not the 15.5px/700 this
 * carried before. The Price Range heading sits directly beside three accordion
 * triggers in the same column; a different size or weight would read as a
 * mistake rather than as a distinction.
 *
 * `margin: 0 0 12px` keeps the slider off the heading; the trigger gets its
 * equivalent from the body's own top edge.
 */
.ml-filters .ml-filters__title {
	margin: 0 0 12px;
	font-size: 16px;
	font-weight: 600;
	line-height: 1.3;
	color: var(--ml-navy);
}


.ml-filters__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.ml-filters__list label {
	display: flex;
	align-items: center;
	gap: 9px;
	padding: 7px 8px;
	margin: 0 -8px;
	border-radius: 7px;
	font-size: 14px;
	color: var(--ml-body);
	cursor: pointer;
	transition: background-color var(--ml-t-base) var(--ml-e-standard), color var(--ml-t-base) var(--ml-e-standard);
}

.ml-filters__list label:hover {
	background: #f1f8fd;
	color: var(--ml-navy);
}

.ml-filters__list input[type="checkbox"] {
	flex: 0 0 auto;
	width: 16px;
	height: 16px;
	accent-color: var(--ml-cyan);
	margin: 0;
}

.ml-filters__label {
	flex: 1 1 auto;
	min-width: 0;
}

.ml-filters__count {
	flex: 0 0 auto;
	font-size: 12.5px;
	color: var(--ml-muted);
	font-variant-numeric: tabular-nums;
}

/* ---------- Price slider ---------- */

.ml-price__slider {
	position: relative;
	height: 26px;
	margin: 6px 4px 2px;
}

.ml-price__track,
.ml-price__fill {
	position: absolute;
	top: 50%;
	height: 4px;
	border-radius: 2px;
	transform: translateY(-50%);
	pointer-events: none;
}

.ml-price__track {
	left: 0;
	right: 0;
	background: var(--ml-line);
}

.ml-price__fill {
	background: var(--ml-cyan);
}

/* Both ranges stack on the same track; only the thumbs stay interactive. */
.ml-price__slider input[type="range"] {
	position: absolute;
	left: 0;
	top: 0;
	width: 100%;
	height: 26px;
	margin: 0;
	background: none;
	-webkit-appearance: none;
	appearance: none;
	pointer-events: none;
}

.ml-price__slider input[type="range"]::-webkit-slider-thumb {
	-webkit-appearance: none;
	appearance: none;
	pointer-events: auto;
	width: 16px;
	height: 16px;
	border-radius: 50%;
	background: var(--ml-cyan);
	border: 2px solid var(--ml-white);
	box-shadow: 0 2px 6px rgba(10, 37, 64, 0.35);
	cursor: pointer;
}

.ml-price__slider input[type="range"]::-moz-range-thumb {
	pointer-events: auto;
	width: 14px;
	height: 14px;
	border-radius: 50%;
	background: var(--ml-cyan);
	border: 2px solid var(--ml-white);
	box-shadow: 0 2px 6px rgba(10, 37, 64, 0.35);
	cursor: pointer;
}

.ml-price__slider input[type="range"]:focus-visible::-webkit-slider-thumb {
	outline: 2px solid var(--ml-navy);
	outline-offset: 2px;
}

/*
 * SIDEBAR CONTROL FOCUS - added 2026-09-07.
 *
 * Astra's inline CSS carries two rules that reach every bare `<input>`:
 *
 *     input:focus { border-color: var(--ast-global-color-0) }          (0,1,1)
 *     input:focus { border-style: dotted; border-color: inherit;
 *                   border-width: thin }                              (0,1,1)
 *     input[type=range]:focus { outline: 0 }                          (0,2,1)
 *
 * The filter checkboxes and the price-slider ranges had NO focus rules of their
 * own, so the dotted border was the only thing a click produced - and on the
 * range Astra additionally removed the browser's own ring, leaving a keyboard
 * user with nothing on the track.
 *
 * Same pattern as every other control in this theme: `:focus` pinned back to
 * the resting appearance, `:focus-visible` carrying a brand outline. (0,4,1)
 * clears both Astra tiers.
 */
.ml-filters .ml-filters__list input[type="checkbox"]:focus,
.ml-filters .ml-price__slider input[type="range"]:focus {
	border-style: none;
	border-color: transparent;
	outline: none;
}

.ml-filters .ml-filters__list input[type="checkbox"]:focus-visible {
	outline: 2px solid var(--ml-navy);
	outline-offset: 2px;
}

/*
 * The range keeps its ring on the THUMB (the rule above this block) because
 * that is the part the user is actually moving; the element box is the full
 * track and an outline round it reads as a error state. This only has to undo
 * Astra's `outline: 0` for the thumb rule to be reachable.
 */
.ml-filters .ml-price__slider input[type="range"]:focus-visible {
	outline: none;
}

.ml-price__readout {
	display: flex;
	justify-content: space-between;
	margin: 8px 0 0;
	font-size: 13px;
	font-weight: 600;
	color: var(--ml-navy);
	font-variant-numeric: tabular-nums;
}

.ml-price__readout .woocommerce-Price-amount {
	color: inherit;
}

/* ---------- Buttons ---------- */

.ml-filters__apply {
	display: block;
	width: 100%;
	min-height: 50px;
	padding: 0 16px;
	border: 0;
	border-radius: 8px;
	background: var(--ml-cyan);
	color: var(--ml-white);
	font-family: var(--ml-font);
	font-size: 15px;
	font-weight: 600;
	text-align: center;
	text-decoration: none;
	cursor: pointer;
	box-shadow: 0 12px 22px -14px rgba(var(--ml-shadow-tint), 0.95);
	transition: transform var(--ml-t-base) var(--ml-e-standard), box-shadow var(--ml-t-base) var(--ml-e-standard);
}

/*
 * `:focus` pinned back to rest — Astra's inline `button:focus` is (0,1,1) and
 * the rest rule above is (0,1,0), so a click left this button painted
 * `--ast-global-color-1` with a white label until focus moved elsewhere.
 *
 * MUST STAY ABOVE the `:hover, :focus-visible` rule below. It is (0,2,0) and so
 * is that one; at equal specificity the later rule wins, which is what keeps
 * hover and keyboard focus working. Move this block down and both die silently.
 */
.ml-filters__apply:focus {
	background: var(--ml-cyan);
	color: var(--ml-white);
	border: 0;
}

.ml-filters__apply:hover,
.ml-filters__apply:focus-visible {
	background: var(--ml-cyan-dark);
	transform: translateY(-2px);
	box-shadow: 0 16px 26px -14px rgba(var(--ml-shadow-tint), 1);
	color: var(--ml-white);
}

.ml-filters__clear {
	display: block;
	text-align: center;
	font-size: 13.5px;
	color: var(--ml-muted);
	text-decoration: none;
}

.ml-filters__clear:hover {
	color: var(--ml-cyan);
	text-decoration: underline;
}

/* ---------- Toolbar ---------- */

.ml-shop__toolbar {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	flex-wrap: wrap;
	margin-bottom: 20px;
}

.ml-shop__count,
.ml-shop__count .woocommerce-result-count {
	margin: 0;
	font-size: 14px;
	color: var(--ml-body);
}

.ml-shop__tools {
	display: flex;
	align-items: center;
	gap: 12px;
}

.ml-shop__view {
	display: flex;
	gap: 4px;
	padding: 4px;
	border: 1px solid var(--ml-line);
	border-radius: 8px;
	background: var(--ml-white);
}

/*
 * `padding: 0` for the same reason .medlife-carousel__arrow needs it: Astra's
 * inline Customizer CSS gives every bare `button` padding 10px 20px at (0,0,1),
 * and a rule only wins the properties it names. At 32px wide that left
 * 32 - 40 = a zero content box, so both view icons were collapsing to nothing.
 * See trending.css for the full reasoning.
 */
.ml-shop__view-btn {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 30px;
	padding: 0;
	border: 0;
	border-radius: 6px;
	background: none;
	color: var(--ml-muted);
	cursor: pointer;
	transition: background-color var(--ml-t-base) var(--ml-e-standard), color var(--ml-t-base) var(--ml-e-standard);
}

/*
 * `:focus` pinned back to rest, and the placement matters twice here.
 *
 * Astra's `button:focus` (0,1,1) beat this button's (0,1,0) rest rule, so
 * clicking either view toggle left it Astra cyan — most obvious on the button
 * that was NOT selected, since its rest state is `background: none`.
 *
 * This sits above `:hover` AND above `.is-active`, both (0,2,0). Equal
 * specificity means the later rule wins, so hover still highlights and the
 * SELECTED toggle still shows its cyan fill after being clicked. Putting this
 * pin below `.is-active` would blank the active button the moment it was used.
 */
.ml-shop__view-btn:focus {
	background: none;
	color: var(--ml-muted);
	border: 0;
}

.ml-shop__view-btn:hover {
	color: var(--ml-cyan);
	background: #f1f8fd;
}

.ml-shop__view-btn.is-active {
	background: var(--ml-cyan);
	color: var(--ml-white);
}

.ml-shop__view-btn:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
}

/* -------------------------------------------------------------------------
 * Sorting control
 *
 * TWO CONTROLS, ONE FORM. WooCommerce's `<select name="orderby">` is always
 * the thing that submits; `assets/js/sort.js` draws a listbox over it on
 * desktop pointers and writes the chosen value back. Everything below styles
 * BOTH, because the native one is what a phone, a keyboard-only browser with
 * JS off, or a thrown exception is left with.
 * ---------------------------------------------------------------------- */

.ml-shop__tools .woocommerce-ordering {
	margin: 0;
	min-width: 0;
}

/*
 * The native control, styled to match the enhanced one exactly - same 240px,
 * same 44px, same border and radius - so the two are indistinguishable and a
 * device that keeps the native picker does not look like it got a lesser
 * version.
 *
 * The two background images are the same sort glyph and chevron the enhanced
 * button draws as inline SVG. They are data URIs rather than files because
 * they are two-path icons that would otherwise be two HTTP requests for ~200
 * bytes each.
 */
.ml-shop__tools .woocommerce-ordering select {
	width: 240px;
	max-width: 100%;
	height: 44px;
	padding: 0 38px 0 38px;
	border: 1px solid var(--ml-line);
	border-radius: 10px;
	background-color: var(--ml-white);
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b7684' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 7h11M4 12h8M4 17h5'/%3E%3Cpath d='M17.5 9.5V17m0 0 2.5-2.6M17.5 17 15 14.4' stroke-linejoin='round'/%3E%3C/svg%3E"), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%230A2540' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
	background-repeat: no-repeat, no-repeat;
	background-position: left 12px center, right 12px center;
	font-family: var(--ml-font);
	font-size: 14px;
	color: var(--ml-navy);
	cursor: pointer;
	appearance: none;
	-webkit-appearance: none;
	transition: border-color var(--ml-t-base) var(--ml-e-standard), box-shadow var(--ml-t-base) var(--ml-e-standard);
}

.ml-shop__tools .woocommerce-ordering select:hover {
	border-color: var(--ml-cyan);
}

.ml-shop__tools .woocommerce-ordering select:focus-visible {
	border-color: var(--ml-cyan);
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
}

/*
 * ONCE ENHANCED, THE SELECT IS HIDDEN BUT STILL SUBMITS.
 *
 * `display: none` would be wrong here for a reason worth stating: a
 * `display: none` form control IS still submitted, so it would work - but it
 * also stops `select.focus()` and any measurement, and it makes the control
 * invisible to anything that later wants to read it back. Clipping it keeps it
 * a real, laid-out element that simply is not seen; `tabindex="-1"` and
 * `aria-hidden` (both set by the script) keep it out of the tab order and off
 * the accessibility tree so it is not announced twice.
 */
.ml-shop__tools .woocommerce-ordering select.ml-sort__native {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	border: 0;
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	overflow: hidden;
	white-space: nowrap;
	pointer-events: none;
}

.ml-sort {
	position: relative;
	display: block;
	min-width: 0;
}

.ml-sort__button {
	display: flex;
	align-items: center;
	gap: 8px;
	width: 240px;
	max-width: 100%;
	height: 44px;
	padding: 0 12px;
	border: 1px solid var(--ml-line);
	border-radius: 10px;
	background: var(--ml-white);
	font-family: var(--ml-font);
	font-size: 14px;
	font-weight: 500;
	color: var(--ml-navy);
	text-align: left;
	cursor: pointer;
	transition:
		border-color var(--ml-t-base) var(--ml-e-standard),
		background-color var(--ml-t-base) var(--ml-e-standard),
		color var(--ml-t-base) var(--ml-e-standard);
}

/*
 * The `:focus` pin this theme requires on every custom button - Astra's inline
 * `button:focus { background-color: var(--ast-global-color-1); color: #fff }`
 * is (0,1,1) and a mouse click sets `:focus` without `:focus-visible`, so
 * without this the control would sit repainted cyan after every use. It is
 * placed before `:hover` and `:focus-visible` so both still win in their own
 * state.
 */
/*
 * ONE FILL, EVERY STATE: the brand cyan, on the owner's instruction
 * (2026-09-08). The control is a solid `--ml-cyan` button at rest, on hover, on
 * focus, while active and while open - it does not change colour at all any
 * more, only the chevron rotates.
 *
 * `--ml-cyan` IS #00AFF0 - byte-identical to the leaf in the logo, and the same
 * token the nav band and the cart button use. It is not an approximation of the
 * requested value; it is that value, verified as computed
 * `rgb(0, 175, 240)` in all five states.
 *
 * EVERY STATE IS NAMED, and that is the load-bearing part rather than
 * repetition. Astra's inline
 * `button:focus, button:hover, ... { color: #fff; background-color:
 * var(--ast-global-color-1) }` is (0,1,1) and would otherwise repaint this
 * control the moment it is hovered OR clicked - a rest rule at (0,1,0) does not
 * survive that. The pseudo-class branches here are (0,3,0) and the bare one is
 * (0,2,0), so Astra loses in every state without an `!important`.
 *
 * > CONTRAST, STATED PLAINLY: white on #00AFF0 is **2.50:1**. The label is
 * > 14px/500, so the large-text exemption does not apply and this misses WCAG
 * > 1.4.3's 4.5:1. It was #0077B6 (4.87:1) until this change and was moved to
 * > brand cyan deliberately, with the measurement in hand - the same knowing
 * > trade this file already records for the top-bar ink, the search icon and
 * > the Buy It Now label. #0077B6 is the value to return to if it ever has to
 * > pass; no lighter background can rescue it, because brand cyan tops out at
 * > 2.50:1 against pure white.
 */
.ml-sort .ml-sort__button,
.ml-sort .ml-sort__button:hover,
.ml-sort .ml-sort__button:focus,
.ml-sort .ml-sort__button:focus-visible,
.ml-sort .ml-sort__button:active,
.ml-sort.is-open .ml-sort__button {
	background: var(--ml-cyan);
	color: var(--ml-white);
	border-color: var(--ml-cyan);
}

/*
 * Both icons are inline SVGs whose every path is `stroke="currentColor"`, so
 * they take the label's white from the rule above. This states it explicitly at
 * (0,3,0) because the two rest rules further down still set a colour and sit
 * LATER in the file - at equal specificity they would win the tie on order.
 */
.ml-sort .ml-sort__button .ml-sort__icon,
.ml-sort .ml-sort__button .ml-sort__chevron {
	color: var(--ml-white);
}

.ml-sort__button:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
}

/* Colour comes from the state lock above - see the note there. */
.ml-sort__icon {
	flex: 0 0 auto;
}

/* Same clip, same fix - see the note on .ml-sort__option-text. The button's
   label carries descenders too ("Sort by price: low to high"). */
.ml-sort__value {
	flex: 1 1 auto;
	min-width: 0;
	padding: 3px 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.ml-sort__chevron {
	flex: 0 0 auto;
	transition: transform var(--ml-t-panel) var(--ml-e-enter);
}

.ml-sort.is-open .ml-sort__chevron {
	transform: rotate(180deg);
}

/*
 * The popup is a child of <body> and positioned by the script - see the note
 * in sort.js for why it is not inside the toolbar. `position: fixed` is what
 * makes the flip-above and the viewport clamp possible.
 */
.ml-sort__list {
	position: fixed;
	z-index: 120;
	margin: 0;
	padding: 6px;
	list-style: none;
	background: var(--ml-white);
	border: 1px solid var(--ml-line);
	border-radius: 10px;
	box-shadow: 0 12px 32px -12px rgba(10, 37, 64, 0.28);
	overflow-y: auto;
	overscroll-behavior: contain;
}

.ml-sort__list:focus {
	outline: none;
}

.ml-sort__list:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
}

.ml-sort__option {
	display: flex;
	align-items: center;
	gap: 8px;
	min-height: 40px;
	padding: 0 10px;
	border-radius: 8px;
	font-family: var(--ml-font);
	font-size: 14px;
	color: var(--ml-navy);
	cursor: pointer;
}

/*
 * `is-active` is the KEYBOARD position, tracked by `aria-activedescendant`;
 * it is also what the pointer sets on hover, so the two never disagree about
 * which row is highlighted. A pale wash, not the browser's saturated blue.
 */
.ml-sort__option.is-active {
	background: var(--ml-stage-soft);
}

/*
 * The chosen one: pale cyan, navy ink, and the tick made visible.
 *
 * ONE TINT IN BOTH STATES. There used to be a second, deeper rule for
 * `[aria-selected="true"].is-active` at a literal #ddeefb - which is what the
 * row actually rendered, because the popup opens with the caret ON the selected
 * option. It was heavier than the design called for and it was a literal in a
 * file whose rule is that a colour comes from a token.
 *
 * `--ml-stage` (#e9f4fb) is that token. The caret is still legible when it
 * lands here: this row is the only one carrying the tick and the 600 weight,
 * and `aria-activedescendant` states the position for anything listening.
 *
 * The tie with `.ml-sort__option.is-active` above - both (0,2,0) - is resolved
 * by source order, and this rule is deliberately second.
 */
.ml-sort__option[aria-selected="true"] {
	background: var(--ml-stage);
	font-weight: 600;
}

.ml-sort__check {
	flex: 0 0 auto;
	width: 16px;
	color: var(--ml-cyan);
	visibility: hidden;
}

.ml-sort__option[aria-selected="true"] .ml-sort__check {
	visibility: visible;
}

/*
 * `padding: 3px 0` IS THE DESCENDER FIX - do not remove it as stray spacing.
 *
 * These spans carry `overflow: hidden` so a long label ellipsises. That makes
 * each one its own formatting context whose box is exactly the LINE BOX -
 * 23.09px at 14px/1.65 - and `overflow: hidden` clips at that edge.
 *
 * Poppins puts the glyph ink lower than the line box ends. Measured with
 * canvas TextMetrics at this exact font and size:
 *
 *     half-leading   (23.1 - 14) / 2        =  4.55px
 *   + ascent          fontBoundingBoxAscent = 15.00px   -> baseline at 19.55
 *   + ink descent     actualBoundingBoxDescent = 4.00px -> ink ends at 23.55
 *     box height                                        =  23.09
 *     -------------------------------------------------------------
 *     0.46px of every descender was being cut off, flat.
 *
 * ALL SIX LABELS HAVE A DESCENDER - "Default sortin(g)", "popularit(y)",
 * "avera(g)e ratin(g)", "b(y) latest", "(p)rice: low to hi(g)h", "hi(g)h to
 * low" - so it read as a clipped row rather than one odd letter, which is
 * exactly how it was reported.
 *
 * The padding moves the CLIP EDGE (the padding box) 3px below the ink without
 * touching `line-height`, the 40px row, the 44px control or the ellipsis, which
 * only cares about the inline axis. Raising `line-height` instead would have
 * worked too and would have changed the popup's rhythm; this does not.
 */
.ml-sort__option-text {
	flex: 1 1 auto;
	min-width: 0;
	padding: 3px 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
	.ml-sort__chevron,
	.ml-sort__button,
	.ml-shop__tools .woocommerce-ordering select {
		transition: none;
	}

	.ml-sort.is-open .ml-sort__chevron {
		transform: rotate(180deg);
	}
}

/* ---------- Product grid ---------- */

/*
 * ASTRA OWNED THIS GRID AT EVERY WIDTH, AND NOTHING IN THIS FILE WAS RENDERING.
 *
 * Found 2026-09-07 while chasing the loop card's action pair. Astra ships, in
 * its WooCommerce stylesheet:
 *
 *     .woocommerce ul.products:not(.elementor-grid),
 *     .woocommerce-page ul.products:not(.elementor-grid) {
 *         grid-template-columns: repeat(4, 1fr);                    (0,3,1)
 *     }
 *     @media (max-width: 921px) { ... repeat(3, minmax(0, 1fr)) }   (0,3,1)
 *     @media (max-width: 544px) { ... repeat(2, minmax(0, 1fr)) }   (0,3,1)
 *
 * `:not()` contributes the specificity of its argument, so each of those is
 * (0,3,1) - above the (0,2,1) this file used to carry. THE COLUMN COUNT WAS
 * ASTRA'S AT EVERY BREAKPOINT, and the ladder written here was dead code:
 *
 *     width   this file declared   actually rendered
 *     1440    3                    4
 *     1024    2                    4
 *      768    2                    3
 *      360    1                    2   <- 154px cards
 *
 * At 360 that was not just cosmetic: the card fell to 154px while the action
 * pair is a fixed ~172px, so VIEW PRODUCT and the cart button OVERFLOWED THE
 * CARD by ~9px on each side. The pair is content-width by design (`flex: 0 0
 * auto`), so it cannot shrink to fit - the container had to be fixed.
 *
 * Every selector in the ladder is now
 * `.ml-shop__body .ml-shop__body-inner .ml-shop__main ul.products` = (0,4,1),
 * one tier above the (0,3,1) it answers. Not a tie: this project settles
 * cascade fights on specificity, never on which stylesheet happens to load
 * second.
 *
 * TWO THINGS MOVED WITH IT, and both would have broken silently:
 *
 *   1. `.ml-shop__main.is-list ul.products` - the LIST VIEW - was (0,3,1),
 *      which used to beat the (0,2,1) base. Raising the base to (0,4,1) would
 *      have left the list view rendering in three columns. It is (0,5,1) now,
 *      so it still wins from anywhere in this file.
 *   2. The `@media` overrides below carry the same (0,4,1) selector. A media
 *      block adds no specificity, so leaving them at (0,2,1) would have kept
 *      every width pinned to the base's three columns.
 *
 * THE 2-COLUMN BREAKPOINT MOVED FROM 1180px TO 1023px, and that IS a design
 * change rather than a restoration. This file's own ladder put 2 columns at
 * 1024; three was asked for there instead. 1023 is the value that delivers it,
 * because the sidebar stops at 1024 (`.ml-shop__body-inner` goes single-column
 * there), so ≤1024 has the full container to work with:
 *
 *     1440  main 1064px  -> 3 x 341px
 *     1181  main  837px  -> 2 x 408px   (sidebar still present)
 *     1025  main  681px  -> 3 x 214px   <- the tightest cell in the ladder
 *     1024  main  984px  -> 3 x 315px   (sidebar has stacked)
 *      768  main  728px  -> 2 x 354px
 *      360  main  328px  -> 1 x 328px
 *
 * 214px is the number to watch: the action pair needs 172px plus the card's
 * 16px side padding = 204px, so the 1025-1180 band clears it by about 10px.
 * Anything that widens the pair or the card padding breaks that band FIRST -
 * check it before 360.
 */
.ml-shop__body .ml-shop__body-inner .ml-shop__main ul.products {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: 20px;
}

.ml-shop__main ul.products::before,
.ml-shop__main ul.products::after {
	content: none;
}

/*
 * COMPACT LEFT-ALIGNED CARD, 2026-09-07.
 *
 * The card itself carries NO padding: the image tile insets itself by 8px and
 * the text column pads by 16px, so the two edges are independent. Putting the
 * padding back here double-insets the image.
 */
.ml-card {
	position: relative;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	gap: 0;
	margin: 0 !important;
	/*
	 * 16px, 2026-09-18. The card's own inset now, instead of the media's 8px
	 * margin plus the body's 14px padding - those two were doing the same job
	 * with two different numbers, so the image sat 8px from the edge and the
	 * text 14px. One padding here lines them up, and it is what lets the image
	 * box be exactly the card's inner width.
	 *
	 * STATED HERE AND WON BELOW. This declaration is (0,1,0) and LOSES to
	 * WooCommerce's `.woocommerce ul.products li.product { padding: 0 }` at
	 * (0,3,1) - measured, not guessed: the Trending card took the 16px and the
	 * shop card computed 0, and the two differ only in being a `li.product`.
	 * The rule that actually wins is right after this one.
	 */
	padding: 16px;
	width: auto !important;
	float: none !important;
	/*
	 * ASTRA WINS THIS ONE AND IT DOES NOT MATTER - do not add an override.
	 * `.woocommerce ul.products li.product.desktop-align-center` is (0,4,2) and
	 * keeps the LI at `text-align: center`. Every piece of text on the card
	 * lives in `.ml-card__body`, which sets `left` itself, so nothing inherits
	 * the centre. This declaration states the intent and is what applies if the
	 * Customizer alignment class is ever turned off.
	 */
	text-align: left;
	background: var(--ml-white);
	border: 1px solid var(--ml-line);
	border-radius: 12px;
	box-shadow: 0 1px 2px rgba(var(--ml-shadow-tint), 0.05);
	transition:
		border-color var(--ml-t-base) var(--ml-e-standard),
		box-shadow var(--ml-t-base) var(--ml-e-standard),
		transform var(--ml-t-base) var(--ml-e-standard);
}

/*
 * THE PADDING THAT ACTUALLY WINS - 2026-09-18.
 *
 * WooCommerce ships `.woocommerce ul.products li.product,
 * .woocommerce-page ul.products li.product { padding: 0 }` at (0,3,1), which
 * beats the (0,1,0) declaration above. The symptom was not subtle once it was
 * looked for: the Trending card - the same declaration in trending.css - took
 * its 16px, while the shop, category and related cards computed 0 and the
 * image tile and every line of text sat flush against the card's border. The
 * two cards differ only in being a `li.product`.
 *
 * `ul.products li.product.ml-card` is (0,3,2): three classes to WooCommerce's
 * three, and one element more. One tier above the rule it answers, so it does
 * not depend on load order - the discipline this file states for the Astra
 * traps, applied to a WooCommerce one.
 *
 * THE LIST VIEW IS UNAFFECTED, and that is by specificity rather than by luck:
 * `.ml-shop__main.is-list ul.products li.product.ml-card` is (0,4,2) and still
 * wins its own `padding: 12px`, as does the 8px it takes below 640px.
 */
ul.products li.product.ml-card {
	padding: 16px;
}

/*
 * The only motion on this card: a 2px lift with a border and shadow change.
 * Nothing scales, and the image never moves - see the reset below, which still
 * pins every img state.
 */
.ml-card:hover,
.ml-card:focus-within {
	border-color: var(--ml-cyan);
	box-shadow: 0 4px 12px rgba(var(--ml-shadow-tint), 0.10);
	transform: translateY(-2px);
}

/*
 * THE STRETCHED CARD LINK IS BACK - 2026-09-18, the owner's call, and it
 * REVERSES the prohibition that stood here. The old note read:
 *
 *     "THE STRETCHED CARD LINK IS GONE - do not put it back. The card now
 *      carries two real controls, VIEW PRODUCT and add-to-cart, and a
 *      stretched anchor sits over both and swallows every click aimed at
 *      them."
 *
 * THE OBJECTION WAS CORRECT AND IS ANSWERED, not ignored: the overlay sits at
 * `z-index: 1` and every control that has its own job is lifted to `z-index: 2`
 * below. That is the whole reason this is safe now and was not before.
 *
 * ONE ANCHOR, NO NESTING, NO JAVASCRIPT. The overlay is a pseudo-element on the
 * title's existing link, so the card gains no second destination and the
 * accessibility tree is unchanged - `::after` is not in it. The title is still
 * the one thing a screen reader announces as the link.
 *
 * MEASURED FIRST: every card surface computes `position: relative` on the card,
 * so `inset: 0` anchors to the card and not to something further up.
 *
 * THE TRADE, stated because it is real: a stretched overlay makes the
 * description text hard to select with the mouse. That is inherent to the
 * pattern, not a bug to fix here.
 */
.ml-card__title a::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 1;
}

/*
 * The whole card reads as clickable. `cursor: auto` was measured on the card
 * before this change - the pointer only appeared over the real links.
 */
.ml-card {
	cursor: pointer;
}

/*
 * ABOVE THE OVERLAY. These are `position: static` today - measured, not assumed
 * - and a `z-index` on a static element does nothing at all, so each one needs
 * a stacking context of its own before the layer means anything.
 *
 * `.ml-card__actions` covers VIEW PRODUCT and the cart together. VIEW PRODUCT
 * goes to the same place as the overlay, so a click would land correctly either
 * way - but under the overlay its :hover would never fire, which is a visible
 * regression rather than a harmless one.
 *
 * `.ml-card__wishlist` is NOT in this list: it is already `position: absolute`
 * at `z-index: 3`, so it is above the overlay already, and adding
 * `position: relative` here would break its placement.
 */
.ml-card .ml-card__actions,
.ml-card .ml-card__more,
.ml-card .ml-card__detail-more {
	position: relative;
	z-index: 2;
}

/*
 * THE RING GOES ROUND THE CARD, and `:has()` is deliberate where the brief said
 * `:focus-within`.
 *
 * `:focus-within` is true whenever ANY descendant has focus - including the
 * cart button and the wishlist heart, which carry their own rings. The card
 * would have drawn a second ring around them. `:has(... a:focus-visible)` fires
 * only for the title link, which is the control the overlay belongs to.
 * `:has()` is already used in this theme (see `.medlife-assurance:has(...)`).
 *
 * The title's own 3px ring is gone with this: one focused thing, one ring.
 */
.ml-card:has(.ml-card__title a:focus-visible) {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
	border-radius: 12px;
}

/*
 * No hover state on the card: no lift, no shadow, no border change, no image
 * scale. It is completely still.
 *
 * Same story as the Trending Now card: the grey wash came from Elementor's
 * global kit, `.elementor-kit-28 img:hover { box-shadow: 100px 100px 100px 0
 * rgba(0,0,0,.5) }`, applied to every image on the site. That setting has been
 * removed from the kit; the reset below keeps this card inert if it is ever
 * re-enabled in the Elementor UI.
 *
 * This card is shared by the shop archive, related products and the cart
 * cross-sells, so all three are covered here.
 *
 * The product title still changes colour on hover; that is a link affordance
 * and moves nothing.
 */
/*
 * THE CARD IS NO LONGER IN THIS LIST - only the images are.
 *
 * It used to be, because the card was completely still. It now has a 2px hover
 * lift, and leaving `.ml-card` here would have killed that silently: this rule
 * sits later in the file at equal specificity. The IMAGE resets stay exactly as
 * they were - they are what keeps Elementor's global kit
 * (`img:hover { box-shadow: 100px ... }`) from washing the packshot if anyone
 * re-enables it.
 */
.ml-card img,
.ml-card img:hover,
.ml-card__media img,
.ml-card__media img:hover,
.ml-card__media:hover img {
	box-shadow: none;
	filter: none;
	opacity: 1;
	transform: none;
	transition: none;
}

/*
 * THE REST STATE LOST TO ASTRA AND THE HOVER STATE DID NOT — which is exactly
 * why the box "disappeared on hover" (2026-09-18).
 *
 * On a PRODUCT page Astra wraps the content in `.ast-article-single` and ships:
 *
 *     .ast-article-single figure,
 *     .ast-article-single img:not(figure img) { box-shadow: 0 0 30px 0 rgba(0,0,0,.15) }
 *
 * `:not()` contributes its argument's specificity, so `figure img` makes that
 * (0,1,3). Measured against the rule above:
 *
 *     .ml-card__media img          (0,1,1)  LOSES  -> 30px glow at rest
 *     .ml-card__media img:hover    (0,2,1)  WINS   -> glow gone on hover
 *
 * A pseudo-class counts in the CLASS column, so only the hover halves of that
 * rule were ever beating Astra. The related-products strip is the only card
 * surface inside `.ast-article-single` - shop, category and Trending never
 * were, which is why the box showed on one surface and not the others.
 *
 * This restates ONLY the two properties Astra gets wrong, at (0,2,1), rather
 * than raising the lock above: that rule also pins `transform`/`transition`
 * for the Elementor kit, and moving its specificity would change what it
 * overrides elsewhere for no reason.
 */
.ml-card .ml-card__media img,
.ml-card .ml-card__media img:hover,
.ml-card .ml-card__media:hover img {
	box-shadow: none;
	filter: none;
}

/*
 * TOP-LEFT OF THE IMAGE, not of the card. The card gained 16px of padding on
 * 2026-09-18, so the image now starts at 16px; 24px puts the badge 8px inside
 * the artwork's top-left corner, which is where the reference carries it.
 * These are offsets from the CARD because the badges are the card's children -
 * moving them into the media would be a template change for no gain.
 */
.ml-card__badges {
	position: absolute;
	top: 24px;
	left: 24px;
	/* Above the stretched title link, so badges never swallow the card click. */
	z-index: 3;
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 5px;
}

.ml-card__badge {
	padding: 5px 11px;
	border-radius: 999px;
	font-size: 11.5px;
	font-weight: 700;
	color: var(--ml-white);
	line-height: 1.2;
	white-space: nowrap;
}

.ml-card__badge--best { background: var(--ml-cyan); }
.ml-card__badge--new  { background: #23a455; }
.ml-card__badge--sale { background: var(--ml-sale); }

.ml-card__wishlist {
	position: absolute;
	/* 24px for the same reason as the badges - 8px inside the image, which now
	   starts at the card's 16px padding. Mirrors the badge on the other side. */
	top: 24px;
	right: 24px;
	/* Above the stretched title link, so the heart stays clickable. */
	z-index: 3;
}

.ml-card__wishlist .tinvwl_add_to_wishlist_button {
	display: flex !important;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	margin: 0;
	padding: 0;
	border: 1px solid var(--ml-line);
	border-radius: 50%;
	background: var(--ml-white);
	color: var(--ml-muted);
	font-size: 14px;
	line-height: 1;
	transition: color var(--ml-t-base) var(--ml-e-standard), border-color var(--ml-t-base) var(--ml-e-standard), transform var(--ml-t-base) var(--ml-e-standard);
}

.ml-card__wishlist .tinvwl_add_to_wishlist_button:hover {
	color: var(--ml-cyan);
	border-color: var(--ml-cyan);
	transform: scale(1.08);
}

/*
 * The label, the plugin's wrapper divs and its tooltip are handled in
 * wishlist.css, shared with the Trending Now card. `.tinvwl-txt` used to be
 * hidden here - that class is not in this plugin's output, so the label was
 * never hidden and spilled out of this 34px circle.
 */

/* The clean white tile the product sits on. */
/*
 * A SQUARE TILE, INSET 8px FROM THE CARD EDGE.
 *
 * `aspect-ratio: 1 / 1` rather than a fixed height, so the tile grows with the
 * column instead of leaving the blank side margins a fixed height produces on a
 * wide card. The ground was `--ml-stage-soft` until 2026-09-18 so that a white
 * packshot read as an object on a tile; it is `--ml-white` now, because inside
 * a white card that tile read as a second surface. See the rule below.
 *
 * `object-fit: contain` plus the 14px padding is what protects the artwork:
 * nothing is cropped or stretched at any card width, and landscape packshots
 * letterbox inside the square.
 */
.ml-card__media {
	/* Containing block for the pack size pill. */
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
	/*
	 * A SQUARE THAT FILLS THE CARD'S INNER WIDTH - 2026-09-18, to the
	 * Herbiotics reference, measured rather than copied: their image box is
	 * 241x241 inside a 253px card, i.e. SQUARE and 95% of the card width, with
	 * the artwork filling it. Ours was 323x220 (1.47:1) with the artwork at
	 * roughly half the box - the "small picture in a wide letterbox" the owner
	 * was comparing against.
	 *
	 * `aspect-ratio` rather than a fixed height, so the box tracks the column:
	 * 307px at 1440, and it can never go back to being width-limited the way
	 * the old fixed 220px was in the 1025-1180 band.
	 *
	 * No margin and no padding: the card's own 16px padding is the inset now.
	 */
	margin: 0;
	padding: 0;
	height: auto;
	aspect-ratio: 1 / 1;
	/*
	 * WHITE, NOT --ml-stage-soft, SINCE 2026-09-18 - the owner's call.
	 * A pale tile inside a white card reads as two nested surfaces: the
	 * "double background". The card's own border and shadow carry its shape,
	 * so the tile does not need to.
	 *
	 * `mix-blend-mode: multiply` on the image STAYS and must not be removed
	 * as now-redundant: multiply over white is a no-op, so it costs nothing
	 * here, and it is the same declaration that makes the packshot sit on any
	 * other ground (the cart and checkout thumbs are still --ml-stage-soft).
	 * Deleting it would break those the day this tile changes colour again.
	 *
	 * BACK TO --ml-stage-soft ON 2026-09-18, by the owner, AND THIS REVERSES
	 * THE WHITE TILE SET EARLIER THE SAME DAY. White was chosen to kill a
	 * "double background"; the reference card carries a tinted stage
	 * (rgb(250,250,250)) and the owner wants that read. It does not reproduce
	 * the old problem in the same way, because the tile now fills the card's
	 * whole inner width instead of floating inside it with an 8px margin - its
	 * edges line up with the text below, so it reads as the image area rather
	 * than as a second card.
	 *
	 * IF IT EVER READS AS A BOX AGAIN, the reference's own value is the answer:
	 * #fafafa is neutral and 2% off white, where --ml-stage-soft (#f5fafd) is
	 * visibly blue. That is the knob to turn - not the geometry.
	 */
	background: var(--ml-stage-soft);
	border-radius: 10px;
	line-height: 0;
}

/*
 * 170px -> 220px, 2026-09-07, AND THE NUMBER IS DERIVED - do not nudge it.
 *
 * The grid repair widened this card from 251px to 341px at 1440, and the image
 * did not move: this box is a FIXED HEIGHT with `object-fit: contain`, so a
 * square packshot paints at min(boxWidth, height) and was pinned to 170 no
 * matter how much width it was given. The card got wider, the artwork did not,
 * and the difference came out as blank space either side.
 *
 * 24 of the 30 products are square 592x592 packshots, so that is the case to
 * size for. The image box is the card less its 1px border and the media's 18px
 * side padding - card - 38:
 *
 *     width   card     img box   painted at 170   at 220
 *      360    328      290       59% of the box   76%
 *      768    354      316       54%              70%
 *     1024    315      277       61%              79%
 *     1440    341      303       56%              73%
 *
 * 220 is the balance point, not a round number:
 *
 *   - it roughly HALVES the blank space (67px a side at 1440 -> 42px);
 *   - it costs 50px of card height (352 -> 402), which at a 341px card is a
 *     1.18 ratio - still a wide card, not a column;
 *   - going further stops paying. 240 buys 7 more points of fill for another
 *     20px of height, and 260 pushes the card past 1.3.
 *
 * WHAT CAPS IT IS THE 1025-1180 BAND, not the wide end. There the shop runs 3
 * columns beside the 268px sidebar and the image box falls to 176px, so the
 * square is WIDTH-limited and any height above ~176 is dead vertical space -
 * 44px of it at 220, 64px at 240. That band is the reason this is 220 and not
 * 260, and it is the first thing to re-measure if the sidebar or the ladder
 * changes.
 *
 * The 641-1023 single- and two-column bands go the other way: at a 640px
 * viewport the card is 608px wide and even 220 fills only 39% of it. That is
 * the pre-existing cost of one very wide column and this change improves it
 * rather than causing it.
 *
 * `contain`, so the whole product is visible - never cropped to fill. The list
 * view keeps its own 76px image and is untouched.
 *
 * Mirrored proportionally, NOT numerically, in trending.css - see the note
 * there for why the carousel gets 170 rather than 220.
 */
/*
 * `height: calc(100% - 24px)` with `align-self: flex-start` RESERVES the bottom
 * strip of the tile for the pack-size pill. At `height: 100%` a portrait
 * packshot paints the full height of the box and the pill lands on top of the
 * carton. The image is still `contain`, so it is never cropped or stretched -
 * it simply has 24px less height to fit into.
 */
/*
 * MULTIPLY, SO A WHITE-GROUND PACKSHOT SITS ON THE TILE INSTEAD OF IN A WHITE
 * SQUARE (2026-09-15).
 *
 * SINCE 2026-09-18 THIS TILE IS WHITE, so multiply is a no-op here and the
 * declaration is kept rather than deleted - it is what lets the same image
 * component sit on any ground, and the cart and checkout thumbs are still
 * --ml-stage-soft. The history below is why it exists at all.
 *
 * The packshots imported on 2026-09-15 have a pure #ffffff ground. On the
 * --ml-stage-soft (#f5fafd) tile that painted as a white square. Multiply turns
 * white into the tile colour and leaves every darker pixel as it was, so the
 * carton keeps its colours and edges - tried side by side against a white
 * stage on the Achfree Plus and Acevit cards (white cartons, the hardest case)
 * before choosing: `stage-trial-shop-card-1440-2x-2026-09-15.png`.
 *
 * It blends against the tile because the tile's background is painted on
 * .ml-card__media, the image's own parent - keep it there. Transparent PNGs
 * are unaffected. Covers the grid card, list view (its rule does not restate
 * this), related products and cart cross-sells, which all use this card.
 */
/*
 * 88% OF THE SQUARE, not 100% - the owner's figure, and it is what leaves the
 * artwork a margin inside its own tile instead of touching the corners.
 *
 * `align-self: flex-start` with a 2% top margin puts the remaining 10% BELOW
 * the artwork, which is the strip the pack pill sits in. Centring it instead
 * would split that space top and bottom and the pill would land on the carton -
 * the same trap the old `calc(100% - 24px)` was written to avoid.
 */
.ml-card__media img {
	width: 88%;
	height: 88%;
	margin-top: 2%;
	align-self: flex-start;
	object-fit: contain;
	display: block;
	mix-blend-mode: multiply;
}

/*
 * Pack size pill, revealed over the image.
 *
 * z-index 2 puts it above the stretched title link (z-index 1) so it is not
 * painted underneath it; `pointer-events: none` means it still cannot swallow
 * the card click. Kept at opacity 0 rather than display:none so screen readers
 * announce it regardless of hover.
 *
 * Rendered only when _medlife_pack_size is set - the template omits the element
 * entirely otherwise, so there is never an empty pill.
 */
.ml-card__pack {
	position: absolute;
	left: 50%;
	bottom: 8px;
	z-index: 2;
	transform: translate(-50%, 6px);
	padding: 4px 10px;
	border-radius: 999px;
	background: var(--ml-white);
	border: 1px solid var(--ml-line);
	box-shadow: 0 8px 20px -12px rgba(10, 37, 64, 0.5);
	font-size: 11px;
	font-weight: 600;
	line-height: 1.3;
	white-space: nowrap;
	color: var(--ml-navy);
	opacity: 0;
	pointer-events: none;
	transition: opacity var(--ml-t-base) var(--ml-e-standard), transform var(--ml-t-base) var(--ml-e-standard);
}

/* focus-within covers the keyboard: tabbing to the title or heart reveals it. */
.ml-card:hover .ml-card__pack,
.ml-card:focus-within .ml-card__pack {
	opacity: 1;
	transform: translate(-50%, 0);
}

/*
 * Touch devices have no hover, so a hover-only pill would be unreachable -
 * there it is simply always on.
 */
@media (hover: none) {
	.ml-card__pack {
		opacity: 1;
		transform: translate(-50%, 0);
	}
}

/*
 * NO `gap`, AND THAT IS THE POINT - 2026-09-18.
 *
 * The card now runs an explicit spacing ladder, and a single `gap` cannot
 * express it:
 *
 *     image  -> category   14px
 *     category -> title     6px
 *     title -> description  8px
 *     description -> price 12px
 *     price -> buttons     16px
 *
 * Each step is a margin on the element that owns it, so changing one does not
 * move the other four. The old 10px `gap` plus `.ml-card__more`'s -6px
 * correction was the symptom of trying to do this with one number.
 *
 * Padding is 0: the card's own 16px is the inset.
 */
.ml-card__body {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: 0;
	flex: 1 1 auto;
	min-width: 0;
	width: 100%;
	padding: 0;
	text-align: left;
}

/*
 * The category label. Subordinate to the title by design - 12px/600 cyan
 * uppercase against 16px/600 navy - which is what answers the 2026-09-07
 * objection that it read as "a second, weaker heading". See the template note.
 *
 * Cyan on white is 2.50:1 and fails 1.4.3 for body text. It is the owner's
 * brand-cyan trade, the same one already recorded for the top-bar ink, the
 * search icon, `--ast-global-color-0` and the card's own "More" link. The
 * information is not carried by this label alone - the product page and the
 * breadcrumb both state the category - so nothing is lost if it is hard to
 * read. `--ml-accent` (#0077B6, 4.87:1) is the value if it ever has to pass.
 */
.ml-card__cat {
	margin: 14px 0 6px;
	font-size: 12px;
	font-weight: 600;
	line-height: 1.3;
	color: var(--ml-cyan);
	text-transform: uppercase;
	letter-spacing: 0.06em;
}

/*
 * 22px/700 desktop, 18px below 640px (see the responsive block).
 *
 * `-webkit-line-clamp: 2` caps a long name at two lines. It does NOT reserve
 * a second line: rows are levelled by `margin-top: auto` on the price, so
 * reserving one only opened a visible hole under every one-line title.
 */
/*
 * 16px/600, TWO LINES RESERVED - 2026-09-18.
 *
 * `min-height: 2.6em` (2 lines at line-height 1.3) is a REVERSAL of the note
 * above, which says reserving a second line "only opened a visible hole under
 * every one-line title". That was true when the price carried `margin-top:
 * auto` and levelled the rows by absorbing slack at ONE point. The card now
 * reserves two lines for the title and two for the description, so every card
 * has the same text block height and the buttons line up without a single
 * elastic gap doing all the work. The hole moved; it did not disappear - it is
 * now distributed as even whitespace rather than collecting under short names.
 */
.ml-card__title {
	margin: 0 0 8px;
	font-size: 16px;
	font-weight: 600;
	line-height: 1.3;
	min-height: 2.6em;
	color: var(--ml-navy);
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

/*
 * THE TENTH INSTANCE OF ASTRA'S `.woocommerce-js h2 { margin-bottom: .7em }`.
 *
 * Measured, not guessed: the title-to-description gap came out at 11.2px
 * against the 8px the ladder asks for, and 0.7em x 16px IS 11.2px. The card
 * title is an `h2`, `.ml-card__title` is (0,1,0), Astra's is (0,1,1), so the
 * `margin: 0 0 8px` in the rule above never rendered - exactly the failure this
 * file already records for nine other headings, including the 4Cs row where the
 * same rule was ADDING to a flex `gap`.
 *
 * The doubled class is (0,2,0) and names the margin alone, so the base rule
 * keeps every other property and no @media font-size rule is disturbed.
 */
.ml-card__title.ml-card__title {
	margin: 0 0 8px;
}

.ml-card__title a {
	color: inherit;
	text-decoration: none;
}

/*
 * THE :focus PIN. Astra's inline
 *
 *     a:hover, a:focus { color: var(--ast-global-color-1) }
 *     a:focus          { outline: thin dotted }
 *
 * is (0,1,1). A pointer click sets :focus but NOT :focus-visible, so without
 * this the control kept a 1px dotted ring until focus moved. Measured with
 * forcePseudoState, not assumed.
 *
 * :focus:not(:focus-visible) rather than a bare :focus - the same idiom the
 * category pill and the pharma tab use - so it can never swallow the keyboard
 * ring whatever the source order. Pinned properties are exactly the two Astra
 * sets here: color and outline.
 */
/*
 * color: inherit is restated rather than left to the rest rule. That rule is
 * (0,1,1) - a TIE with Astra, won only because the child theme's stylesheets
 * load after Astra's inline block. CLAUDE.md's standing warning is not to lean
 * on that order, so the pin states the value and the dependency goes away.
 */
.ml-card__title a:focus:not(:focus-visible) {
	color: inherit;
	outline: none;
}

.ml-card__title a:hover,
.ml-card__title a:focus-visible {
	color: var(--ml-cyan);
}

/*
 * NO RING ON THE TITLE ITSELF - the CARD carries it now (2026-09-18).
 *
 * Measured, not assumed: with the title's own 3px ring removed, keyboard focus
 * drew a `dotted 1px` ring INSIDE the card's 3px one. That inner ring is
 * ASTRA'S `a:focus { outline: thin dotted }` at (0,1,1), not a leftover of
 * ours - the two remaining `.ml-card__title a:focus-visible` rules in this file
 * set `color` and `text-decoration` and never touch `outline`.
 *
 * The existing pin two rules up is `:focus:not(:focus-visible)`, so it only
 * covers POINTER focus; keyboard focus had nothing answering Astra at all.
 * (0,3,1) here clears (0,1,1) on class count, so this does not depend on load
 * order.
 */
.ml-card .ml-card__title a:focus-visible {
	outline: none;
}

/*
 * Rendered only when `get_review_count()` is above zero - the template does not
 * output this block at all otherwise, so there are never empty stars.
 */
.ml-card__rating {
	display: flex;
	align-items: center;
	justify-content: flex-start;
	gap: 6px;
	font-size: 12.5px;
	color: var(--ml-muted);
}

/*
 * WooCommerce paints `.star-rating` with `color`, so the stars take the navy
 * from here. The accessible text WooCommerce puts inside
 * (`<span class="screen-reader-text">Rated N out of 5</span>`) is untouched.
 */
.ml-card__rating .star-rating {
	color: var(--ml-navy);
}

.ml-card__reviews {
	font-size: 12px;
	color: var(--ml-muted);
}

/*
 * Short description - two lines, then a More link to the product itself.
 * The template omits the whole block when the product has no short
 * description, so an empty card shows no gap rather than a blank strip.
 */
/*
 * 14px/400 muted, two lines reserved - the same reasoning as the title.
 * `min-height: 3em` is two lines at line-height 1.5.
 *
 * The template omits the whole block when a product has no short description,
 * so those cards lose the reserved height too and their buttons still line up,
 * because `margin-top: auto` on the action row absorbs the difference.
 */
.ml-card__excerpt {
	margin: 0;
	font-size: 14px;
	font-weight: 400;
	line-height: 1.5;
	min-height: 3em;
	color: var(--ml-muted);
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

/*
 * `margin-top: -6px` against the column's 10px `gap` leaves 4px between the
 * description and its own More link, without breaking the 10px rhythm between
 * every other pair in the card. The link belongs to the paragraph above it, so
 * it should not sit as far away as the price does.
 */
/*
 * INLINE END - the owner's placement. `flex-end` in a column flex container is
 * the inline axis, so this puts "More" at the right-hand end of the
 * description block rather than on a line of its own at the left.
 *
 * The old `margin-top: -6px` is gone with the `gap` it was correcting: it
 * existed to claw 4px back out of a uniform 10px gap, and the ladder now sets
 * each step directly.
 */
.ml-card__more {
	align-self: flex-end;
	margin-top: 0;
	/*
	 * THE LADDER'S 12px STEP LIVES HERE, not on the price - 2026-09-18.
	 *
	 * "More" sits between the description and the price, so it owns that
	 * boundary. Measured before this line existed, description-to-price was
	 * 20.6px and none of it was deliberate: it was simply the link's own line
	 * box filling the slot, with no step either side of it. The 12px is stated
	 * on the element that ends the text block.
	 *
	 * A probe that measures description-to-price still reads ~32px, because it
	 * spans the link. The step itself is 12.
	 */
	margin-bottom: 12px;
	font-size: 12.5px;
	font-weight: 600;
	color: var(--ml-cyan);
	text-decoration: none;
}

/*
 * THE :focus PIN. Astra's inline
 *
 *     a:hover, a:focus { color: var(--ast-global-color-1) }
 *     a:focus          { outline: thin dotted }
 *
 * is (0,1,1). A pointer click sets :focus but NOT :focus-visible, so without
 * this the control kept a 1px dotted ring until focus moved. Measured with
 * forcePseudoState, not assumed.
 *
 * :focus:not(:focus-visible) rather than a bare :focus - the same idiom the
 * category pill and the pharma tab use - so it can never swallow the keyboard
 * ring whatever the source order. Pinned properties are exactly the two Astra
 * sets here: color and outline.
 */
.ml-card__more:focus:not(:focus-visible) {
	color: var(--ml-cyan);
	outline: none;
}

/*
 * color IS NAMED HERE, and that is a fix rather than tidying. This rule used to
 * set text-decoration only, while the rest rule sits at (0,1,0) - below Astra's
 * a:hover/a:focus (0,1,1). So the link rendered #0098d1 (Astra's
 * --ast-global-color-1) on HOVER and on KEYBOARD FOCUS, not the brand cyan the
 * rest rule asks for. Three states were wrong, not just the clicked one.
 *
 * At (0,2,0) this clears (0,1,1) on class count, so no order dependency.
 */
.ml-card__more:hover,
.ml-card__more:focus-visible {
	color: var(--ml-cyan);
	text-decoration: underline;
	text-underline-offset: 3px;
}

.ml-card__more:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
	border-radius: 4px;
}

/*
 * THE SECOND EXCERPT IS HIDDEN EVERYWHERE EXCEPT THE LIST ROW.
 *
 * `content-product.php` emits `.ml-card__detail` on the shop grid, every
 * category archive, the related strip and the list row alike, because it is one
 * template. The grid card was compacted deliberately and must not gain a third
 * text block, so it is display:none here and switched back on by
 * `.ml-shop__main.is-list` further down.
 *
 * Hiding it in the BASE rule rather than hiding it per-surface is what keeps
 * this from becoming a global card override: the class is new, so nothing that
 * renders today changes.
 */
.ml-card__detail,
.ml-card__detail-more {
	display: none;
}

/*
 * THE CATEGORY LABEL IS HIDDEN IN LIST VIEW, and that is how "leave the list
 * view alone" is honoured rather than ignored.
 *
 * `content-product.php` is one template, so the new `.ml-card__cat` element
 * renders in the list row too. The brief for the 2026-09-18 card redesign was
 * explicitly grid-only - the list row keeps its own proportions, its own type
 * scale and its second excerpt. Showing a new label there would have changed
 * the one surface that was asked to stay put.
 *
 * Hidden HERE at (0,3,0) rather than in the list block, so it cannot be missed
 * if the list rules are ever reorganised.
 */
/*
 * SHOWN IN LIST VIEW SINCE 2026-09-18 - the owner asked for the eyebrow here
 * too, which reverses the "grid-only" scoping this rule was written for hours
 * earlier. Same 12/600 cyan uppercase as the grid; only the margin differs,
 * because the image sits beside the text rather than above it.
 */
.ml-shop__main.is-list .ml-card__cat {
	display: block;
	margin: 0 0 8px;
}

/*
 * `margin-top: auto` MOVED FROM HERE TO `.ml-card__actions` on 2026-09-18.
 *
 * It is what levels the rows, and it has to sit on the LAST element for the
 * buttons to be the thing that lines up. While it was on the price, a card
 * with no description pushed the price down and the gap between price and
 * buttons stayed fixed - so the buttons lined up only because the price did.
 * On the action row it absorbs the slack directly and the price stays 12px
 * under the description it belongs to.
 */
.ml-card__price {
	margin-top: 0;
	margin-bottom: 16px;
	padding-top: 0;
	font-size: 20px;
	font-weight: 700;
	color: var(--ml-navy);
	line-height: 1.2;
}

/*
 * Sale pricing. WooCommerce emits <del>regular</del><ins>sale</ins> from
 * get_price_html() only when the product is genuinely on sale, so a discount
 * cannot be faked here - with no sale price there is simply no <del>/<ins>.
 */
/* ---------- List view ---------- */

/*
 * Redesigned 2026-09-07. The old block assumed a card with no action row - its
 * comment literally read "now the action row is gone" - and after the excerpt
 * was removed it left a ~600px empty strip in every row: four short lines
 * floating in an 848px column.
 *
 * The row is now:
 *
 *     140px media | left: category + title | right: price + actions
 *
 * and ~120px tall, which puts 6-7 products on a 1440 screen. Every value is
 * derived rather than tuned:
 *
 *     76px   image
 *   + 20px   .ml-card__media padding (10px top and bottom, below)
 *   = 96px   media column
 *   + 24px   .ml-card padding (12px top and bottom, below)
 *   = 120px  row
 *
 * Change the image height or either padding and the row height moves with it.
 *
 * ONLY `.is-list` RULES LIVE HERE. The grid card is untouched - it keeps its
 * 170px image, 18px media padding, 16px card padding and centred text.
 */

.ml-shop__body .ml-shop__body-inner .ml-shop__main.is-list ul.products {
	grid-template-columns: minmax(0, 1fr);
}

/*
 * RIGHT PADDING SO THE WHATSAPP FLOAT CANNOT COVER THE CART - 2026-09-18.
 *
 * `.wa-float` is FIXED at `right: 24px` and 66px wide (Code Snippet #17, which
 * lives in the database, not in this theme - see CLAUDE.md). A full-width list
 * row put the cart square at the extreme right of the container, directly
 * under it. The action row is content-width and inset 16px now, and this adds
 * clearance on top so the two cannot meet at any scroll position.
 *
 * Desktop only: below 1024 the float moves to `right: 18px` and the row stacks,
 * and 80px of padding on a phone would be most of the screen.
 */
@media (min-width: 1025px) {
	.ml-shop__body .ml-shop__body-inner .ml-shop__main.is-list ul.products {
		padding-right: 80px;
	}
}

/*
 * THE LIST ROW, REBUILT 2026-09-07: a 230px image panel beside a single
 * left-aligned content column.
 *
 * It used to be a two-by-two grid inside `.ml-card__body` - title and excerpt
 * on the left, price and actions right-aligned - against a 140px thumbnail with
 * a 76px image. That put the price at the far right edge, a metre from the
 * words it belongs to, and shrank the packshot to a stamp.
 *
 * EVERY RULE BELOW IS SCOPED TO `.is-list`. The grid card is untouched: it
 * keeps its 220px image band, 18px title, 13px description, 22px price and
 * 42px buttons. If a rule here would help the grid card too, it still does not
 * belong here - put it on the base class or leave it alone.
 */

/*
 * (0,5,2), AND IT HAS TO BE - the obvious
 * `.ml-shop__main.is-list .ml-card` is (0,3,0) and LOSES.
 *
 * Astra ships `.woocommerce ul.products li.product, .woocommerce-page
 * ul.products li.product { display: flex }` at (0,3,1). With that winning, the
 * `display: grid` here never applied - and the failure was invisible in the
 * usual place to look: `grid-template-columns` still computed to
 * `230px minmax(0px, 1fr)`, exactly as authored, because it is a valid value
 * on any element. It was simply inert on a flex container, so the image panel
 * took the full card width and the row stacked. The card's `padding` was lost
 * the same way.
 *
 * THE SYMPTOM LOOKED LIKE A LAYOUT BUG, NOT A CASCADE ONE. Resolve `display`
 * itself, not the property you were trying to set with it.
 *
 * The two responsive overrides below carry the same five classes for the same
 * reason - at (0,3,0) they would lose to this rule as well as to Astra.
 */
/*
 * THE ROW - rebuilt 2026-09-18 to the grid card's quality.
 *
 * A FIXED 200px image column, not `min(25%, 260px)`: the panel used to grow
 * with the viewport while the artwork inside it stayed small, which is how a
 * row ended up 430px tall around a modest picture. 200px is the whole cell, and
 * the image fills it the way the grid card's does.
 *
 * `min-height: 240px` = 200px image + 2 x 16px padding, plus room for a
 * two-line title. Every row is that height unless its own text needs more, so
 * the column scans evenly.
 *
 * `position: relative` is inherited from the base `.ml-card`, and the action
 * row and the wishlist are positioned against it below.
 */
.ml-shop__main.is-list ul.products li.product.ml-card {
	display: grid;
	grid-template-columns: 200px minmax(0, 1fr);
	align-items: stretch;
	gap: 24px;
	padding: 16px;
	min-height: 240px;
	text-align: left;
}

/*
 * THE IMAGE PANEL - 250px, and the grid card's sizing is undone here rather
 * than fought with.
 *
 * The base rule gives the grid card a 220px band with an 8px margin; this row
 * wants a taller panel flush to the card padding. `aspect-ratio: auto` is
 * stated explicitly because the base rule carried `aspect-ratio: 1/1` for one
 * day and the next person to reinstate a square will need this line to still be
 * here.
 */
/*
 * FULL HEIGHT, NOT A FIXED 250px BAND.
 *
 * `height: auto` plus the card's `align-items: stretch` makes the panel as tall
 * as the content column, so its bottom edge lands on the same line as the
 * action row's - which is the whole look. A fixed height left it floating 42px
 * short of the buttons.
 *
 * The 32px bottom padding is the pack pill's strip. In the grid card that space
 * comes off the image's own height; here the panel has height to spare, so it
 * is padding instead and the artwork keeps its full box.
 */
/*
 * 200 x 200, and the same tile as the grid card - 2026-09-18.
 *
 * `--ml-stage-soft` now, not white: the two views show the same product on the
 * same ground, which is the whole point of them being one card. `aspect-ratio`
 * is restated as `auto` because the explicit square is stated here instead.
 */
.ml-shop__main.is-list .ml-card__media {
	grid-column: 1;
	grid-row: 1;
	align-self: start;
	margin: 0;
	padding: 0;
	width: 200px;
	height: 200px;
	min-height: 0;
	aspect-ratio: auto;
	background: var(--ml-stage-soft);
	border-radius: 10px;
}

/*
 * NO HEIGHT OVERRIDE - the image inherits `calc(100% - 24px)` from the base
 * rule, which is what reserves the strip the pack pill sits in. Setting a fixed
 * height here (it was 76px) is what made the packshot a stamp, and it would put
 * the pill back on top of the carton.
 */
/*
 * THE IMAGE IS TAKEN OUT OF FLOW SO THE PANEL CANNOT SET ITS OWN HEIGHT.
 *
 * In flow, a square packshot at a 236px column gave the panel a 236px
 * intrinsic height. When that came out taller than the text beside it, the grid
 * row took the panel's height and the action row finished 40px short of the
 * panel's bottom edge - the one thing this layout is supposed to line up.
 *
 * Absolutely positioned with all four offsets, the image fills the inset area
 * and contributes NOTHING to the panel's height, so the row is driven by the
 * content column and the panel stretches to meet it. The offsets are the
 * padding the panel no longer needs: 12px on three sides, 36px at the bottom
 * for the pack pill's strip.
 *
 * `object-fit: contain` still governs the artwork inside that box, so nothing
 * is cropped; `max-height` caps it on an unusually tall row and `margin: auto`
 * keeps it centred when the cap bites.
 */
/*
 * BACK IN FLOW, AND THE REASON IT WAS TAKEN OUT IS GONE - 2026-09-18.
 *
 * The note below explains that the image was absolutely positioned so a square
 * packshot could not set the panel's height and knock the row's alignment out.
 * The panel is a FIXED 200 x 200 now, so it cannot take its height from the
 * image at all, and the image can simply be the grid card's image: 88% of the
 * box, `contain`, multiply, top-aligned with the bottom strip left for the
 * pack pill. Identical values to `.ml-card__media img`, deliberately.
 *
 * The `calc()` sizes and `max-height` below went with the absolute positioning;
 * the replaced-element trap they document only applies when both `left` and
 * `right` are set, which is no longer the case.
 */
.ml-shop__main.is-list .ml-card__media img {
	position: static;
	width: 88%;
	height: 88%;
	margin: 2% 0 0;
	max-height: none;
	align-self: flex-start;
	object-fit: contain;
}

/*
 * THE OLD ABSOLUTE RULE WAS DELETED, NOT COMMENTED OUT - and the first attempt
 * at this edit DID comment it out, which was a bug worth recording.
 *
 * CSS COMMENTS DO NOT NEST. The old rule carried a slash-star note of its own
 * about replaced elements, so wrapping the block closed the outer comment at
 * the INNER terminator: seven declarations came back as live top-level CSS and
 * the trailing terminator was left as a stray token, in a position to swallow
 * the rule that follows. A naive count of opener and closer tokens reports
 * "balanced" throughout, because it counts tokens and knows nothing about
 * nesting - so the instrument said clean while the file was not.
 *
 * (This paragraph deliberately spells the delimiters out in words. Writing
 * them as literals inside a comment is what made the token count unbalanced
 * the first time it was written, and a backslash does not escape them - CSS
 * has no escape for a comment terminator.)
 *
 * What the deleted rule knew is preserved in the replacement above and in
 * CLAUDE.md: an `<img>` is a REPLACED element, so with both `left` and `right`
 * set, `width: auto` still resolves to the INTRINSIC width and the offsets are
 * ignored. That trap only applies to absolute positioning, which this view no
 * longer uses.
 */

/*
 * The pack pill is ALWAYS VISIBLE in list view - the row is a scanning surface
 * and pack size is one of the few facts worth comparing down a column. It sits
 * in the 24px strip the image gives up, so it never covers the product.
 *
 * Products with no recorded pack size render no element at all, so nothing
 * empty appears.
 */
.ml-shop__main.is-list .ml-card__pack {
	opacity: 1;
	transform: translate(-50%, 0);
}

/* One left-aligned column, in markup order: title, rating, short description,
   full-description excerpt, price, actions. */
/*
 * `gap: 0` AND EXPLICIT MARGINS, deliberately.
 *
 * A single gap cannot give 8px after the title, 18px between the two excerpts
 * and 16px before the actions. Each step is stated on the element it follows.
 */
.ml-shop__main.is-list .ml-card__body {
	grid-column: 2;
	grid-row: 1;
	display: flex;
	flex-direction: column;
	align-items: stretch;
	justify-content: flex-start;
	gap: 0;
	padding: 0;
	text-align: left;
}

/*
 * `margin: 0` is not tidiness - Astra gives every `h2` a bottom margin
 * (15.4px measured here), and the card title is an `h2`. It was adding a whole
 * line of dead space under the title on top of the column's own spacing.
 */
/* 18/600, up to two lines. `min-height: 0` undoes the grid card's reserved
   pair - each row is its own row and levels nothing. */
.ml-shop__main.is-list .ml-card__title {
	margin: 0 0 14px;
	font-size: 18px;
	font-weight: 600;
	line-height: 1.3;
	min-height: 0;
	-webkit-line-clamp: 2;
}

/* Hidden in the compact grid row, shown here - the width is available. */
/*
 * Shown only because the template omits the element entirely when
 * `get_review_count()` is zero - there is no empty row to reserve and no
 * placeholder star anywhere in this file.
 */
.ml-shop__main.is-list .ml-card__rating {
	display: flex;
	margin: 0 0 8px;
}

/*
 * `min-height: 0` UNDOES THE GRID CARD'S RESERVED TWO LINES - 2026-09-18.
 *
 * The base rule gained `min-height: 3em` so every grid card reserves two
 * description lines and the buttons line up across a row. The list row levels
 * nothing (each row is its own row), so a reserved line there is just a hole
 * under a one-line description.
 *
 * This is the ONE property of that pass that the list block did not already
 * restate - the title's `min-height: 0`, the body's `padding`, the card's
 * `padding: 12px`, the media's `aspect-ratio: auto`, the image's `calc()`
 * sizes, `align-self: flex-start` on More and the `margin` shorthands on price
 * and actions all happened to cover theirs. Checked rule by rule rather than
 * trusted: a `margin-top`/`margin-bottom` grep reports zero here because those
 * rules use the shorthand.
 */
/*
 * ONE DESCRIPTION, NOT TWO - 2026-09-18.
 *
 * The row was printing the short excerpt AND the long one, which is the same
 * product described twice, one under the other. The LONG one stays
 * (`.ml-card__detail`, three lines) because the row has the width for it; the
 * short excerpt and its "More" are hidden here, and `.ml-card__detail-more`
 * becomes the row's single link.
 *
 * Both elements stay in the markup - it is one template, and the grid card
 * needs the excerpt. Nothing was removed from the PHP.
 */
.ml-shop__main.is-list .ml-card__excerpt,
.ml-shop__main.is-list .ml-card__more {
	display: none;
}

/*
 * THE LONG EXCERPT IS SHOWN; ITS OWN LINK IS NOT.
 *
 * Two links one under the other, both going to the same product, is noise - the
 * "More" under the short description already leads there. The markup still
 * carries `.ml-card__detail-more` (it is one template, and the grid card hides
 * both), so nothing had to be removed from the PHP and the product page is
 * untouched.
 */
.ml-shop__main.is-list .ml-card__detail {
	display: block;
}

.ml-shop__main.is-list .ml-card__detail {
	margin: 0 0 8px;
	font-size: 14px;
	font-weight: 400;
	line-height: 1.5;
	min-height: 0;
	color: var(--ml-muted);
	display: -webkit-box;
	-webkit-line-clamp: 3;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

/*
 * THE ROW'S SINGLE LINK, at the inline end of the description it belongs to -
 * the grid card's "More" treatment, on the element that actually follows the
 * long excerpt in markup order.
 *
 * ITS LABEL READS "Full description", NOT "More". That is the template's word
 * and it is left alone: changing it is a one-line PHP edit, and the reason it
 * was named differently - two identically named links one above the other -
 * no longer applies now that only one renders here. Say the word and it
 * becomes "More".
 */
.ml-shop__main.is-list .ml-card__detail-more {
	display: block;
	align-self: flex-end;
	width: auto;
	margin: 0 0 22px;
	font-size: 12.5px;
	font-weight: 600;
	color: var(--ml-cyan);
	text-decoration: none;
}

.ml-shop__main.is-list .ml-card__detail-more:hover,
.ml-shop__main.is-list .ml-card__detail-more:focus-visible {
	color: var(--ml-cyan);
	text-decoration: underline;
	text-underline-offset: 3px;
}

.ml-shop__main.is-list .ml-card__detail-more:focus:not(:focus-visible) {
	color: var(--ml-cyan);
	outline: none;
}

.ml-shop__main.is-list .ml-card__detail-more:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
	border-radius: 4px;
}

/* Both "more" links sit 4px under the paragraph they belong to - the -6px is
   against this column's 10px gap, the same trick the grid card uses. */
/* Sits directly under the short excerpt, then opens 18px before the long one. */
.ml-shop__main.is-list .ml-card__more {
	align-self: flex-start;
	width: auto;
	margin: 0 0 18px;
	font-size: 13px;
	font-weight: 600;
	color: var(--ml-cyan);
	text-decoration: none;
}

.ml-shop__main.is-list .ml-card__more:hover,
.ml-shop__main.is-list .ml-card__more:focus-visible {
	text-decoration: underline;
	text-underline-offset: 3px;
}

/*
 * Price sits with the words, not at the outer edge, and NOT on `margin-top:
 * auto`. The grid card pushes it down to level the action rows across a grid
 * row; here every row is its own row, so pushing it only opens a hole between
 * the description and the price.
 */
/*
 * BOTTOM LEFT. `margin-top: auto` pushes it to the foot of the content column;
 * the action row is positioned against the ROW at the same height, so the two
 * read as one bottom line without needing a wrapper element in the markup -
 * and a wrapper would have had to be added to the shared template, which would
 * have changed the grid card. It must not.
 */
.ml-shop__main.is-list .ml-card__price {
	margin: auto 0 0;
	padding-top: 0;
	font-size: 20px;
	font-weight: 700;
	justify-content: flex-start;
}

/*
 * A TWO-TRACK GRID, not a flex row: `minmax(0, 4fr) minmax(64px, 1fr)` gives
 * VIEW PRODUCT roughly 80% and the cart roughly 20%, with the cart never
 * dropping below 64px however narrow the column gets. `margin-top: auto` is
 * NOT used - the column is top-aligned and the panel stretches to meet the
 * buttons, so pushing them down would only open a hole above the price.
 */
/*
 * BOTTOM RIGHT OF THE ROW, CONTENT WIDTH - 2026-09-18.
 *
 * It was a full-width two-track grid, so the cart square landed at the extreme
 * right of a 1064px row - which is exactly where the fixed WhatsApp float sits,
 * and the float covered it. Content width and a 16px inset keep it clear, and
 * the list container also gains right padding at desktop widths (see
 * `.ml-shop__main.is-list ul.products`).
 *
 * Absolute against the row (`.ml-card` is `position: relative`), which is what
 * puts it on the same line as the price without a markup wrapper.
 */
.ml-shop__main.is-list .ml-card__actions {
	position: absolute;
	right: 16px;
	bottom: 16px;
	display: flex;
	align-items: stretch;
	justify-content: flex-end;
	margin: 0;
	padding-top: 0;
	gap: 10px;
	width: auto;
}

/* The list row is wide, so VIEW PRODUCT fills what is left of the column and
   the cart keeps its square - both back to 48px, against the grid's 42px. */
/* 200px fixed, not stretched - the pair is a control group, not a banner.
   44px to match the grid card and the cart beside it. */
.ml-shop__main.is-list .ml-card .ml-card__view {
	flex: 0 0 200px;
	width: 200px;
	min-height: 44px;
	border-radius: 8px;
	font-size: 12px;
}

/* Width comes from the grid track now, so the square is released. */
/*
 * A 44px SQUARE, like the grid card's - 2026-09-18.
 *
 * `width: auto` inside the old full-width two-track grid made this a ~149 x 46
 * slab pinned to the far right of the row. It is the solid half of a matched
 * pair with VIEW PRODUCT and has to share its geometry: 44px, radius 8, 1px
 * border, 10px gap.
 */
.ml-shop__main.is-list .ml-card .ml-card__actions .ml-card__cart {
	flex: 0 0 44px;
	width: 44px;
	min-width: 0;
	min-height: 44px;
	border-radius: 8px;
}

/*
 * The wishlist heart follows the IMAGE PANEL, not the row.
 *
 * It is `position: absolute; top: 12px; right: 12px` against `.ml-card`, which
 * in the grid card is the top-right of the photograph. In a list row the card
 * is ~1000px wide, so the same rule parks it a metre away from the product and
 * over the text. Every term below is a real measurement from the rules above:
 *
 *     12px   .ml-card padding-left
 *   + 230px  image panel
 *   -  32px  the heart button
 *   -   8px  inset from the panel's right edge
 *   = 202px
 *
 * CHANGE THE PANEL WIDTH AND THIS MOVES WITH IT - there is one of these per
 * breakpoint below for exactly that reason.
 */
/*
 * THE HEART IS A GRID ITEM NOW, IN THE PANEL'S OWN CELL.
 *
 * It used to be `position: absolute` against the card with
 * `left: calc(12px + 230px - 32px - 8px)` - a number that had to be restated at
 * every breakpoint and re-derived whenever the column width moved. The column
 * is a percentage now, so that approach had nothing fixed left to measure from.
 *
 * Sharing `grid-area: 1 / 1` with the panel and using `justify-self: end` puts
 * it on the panel's top-right at ANY column width, including the stacked phone
 * layout, with no arithmetic to maintain.
 */
/*
 * THE ROW'S TOP-RIGHT CORNER, not the image's - 2026-09-18.
 *
 * It was placed into the image cell (`grid-column: 1; justify-self: end`), so
 * the heart sat on the packshot. On a wide row the corner is the conventional
 * place for it and it stops competing with the artwork.
 */
.ml-shop__main.is-list .ml-card__wishlist {
	position: absolute;
	grid-column: auto;
	grid-row: auto;
	justify-self: auto;
	align-self: auto;
	top: 14px;
	right: 14px;
	left: auto;
	margin: 0;
}
/* ---------- Pagination ---------- */

/*
 * `.page-numbers` IS ON THE <ul>, NOT ONLY ON THE LINKS - that is the whole bug
 * this block used to have, and the reason every rule below is scoped to
 * `li > .page-numbers` rather than to `.page-numbers` alone.
 *
 * WooCommerce calls paginate_links() with `'type' => 'list'`, which emits
 *
 *     <ul class="page-numbers"><li><a class="page-numbers">…
 *
 * so a descendant selector `.woocommerce-pagination .page-numbers` matches the
 * LIST as well as the links in it. The list was therefore being painted as a
 * page button: 40px tall, 1px border, 8px radius, white fill - and `display:
 * flex` instead of the intended `inline-flex`, which stretched it edge to edge.
 * That is the "wide bordered container" the numbers appeared to overflow: its
 * children were also 40px tall, plus the li borders and margins below, inside a
 * box fixed at 40px. It also defeated the parent's `text-align: center`, since
 * a full-width flex box has nothing to centre.
 *
 * The container served no purpose and is gone rather than being centred: the ul
 * is now a plain flex row and .woocommerce-pagination does the centring.
 */
.ml-shop__main .woocommerce-pagination {
	margin-top: 30px;
	display: flex;
	justify-content: center;
}

/*
 * Reset for the list, which still matches `.page-numbers` - every property the
 * button block sets has to be undone here or it inherits the button's look.
 */
.ml-shop .ml-shop__main .woocommerce-pagination ul.page-numbers {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: 8px;
	list-style: none;
	margin: 0;
	padding: 0;
	border: 0;
	border-radius: 0;
	background: none;
	min-width: 0;
	height: auto;
}

/*
 * The li carries a SECOND border, from Astra's
 * `.woocommerce-js nav.woocommerce-pagination ul li` at (0,2,3) in
 * woocommerce-grid.min.css - `border: 1px solid var(--ast-border-color)` plus
 * `overflow: hidden`, which clipped the 40px link inside a shorter inline-block.
 *
 * The old reset here was `.ml-shop__main .woocommerce-pagination li` at
 * (0,2,1), which LOSES to Astra and never applied. `.ml-shop` on the front
 * makes this (0,3,2) and wins on class count, so it does not depend on
 * shop.css happening to load after Astra's compatibility CSS.
 */
.ml-shop .ml-shop__main .woocommerce-pagination ul li {
	display: flex;
	border: 0;
	margin: 0;
	padding: 0;
	overflow: visible;
}

/*
 * `li >` keeps this off the ul. 44px square is the theme's tap-target floor,
 * the same one search.css states, and it applies at every width rather than
 * only on phones - a 40px control is no easier to hit with a mouse.
 *
 * No `padding: 0` guard is needed here: these are <a> and <span> elements, so
 * Astra's bare `button { padding: 10px 20px }` cannot reach them. Confirmed in
 * the resolved cascade - padding is won by this rule at (0,3,1).
 */
.ml-shop__main .woocommerce-pagination li > .page-numbers {
	display: flex;
	align-items: center;
	justify-content: center;
	min-width: 44px;
	height: 44px;
	padding: 0 12px;
	border: 1px solid var(--ml-line);
	border-radius: 8px;
	background: var(--ml-white);
	color: var(--ml-navy);
	font-family: var(--ml-font);
	font-size: 14px;
	font-weight: 600;
	line-height: 1;
	text-decoration: none;
	transition:
		transform var(--ml-t-base) var(--ml-e-standard),
		box-shadow var(--ml-t-base) var(--ml-e-standard),
		border-color var(--ml-t-base) var(--ml-e-standard),
		background-color var(--ml-t-base) var(--ml-e-standard),
		color var(--ml-t-base) var(--ml-e-standard);
}

/*
 * Hover/focus is scoped to `a.page-numbers`, NOT `.page-numbers`.
 *
 * The current page is a <span>, not a link - WooCommerce's paginate_links()
 * emits `<span class="page-numbers current">` for it - so naming the element
 * makes it structurally impossible for these states to touch it, whatever the
 * specificity ends up being. A span cannot be hovered into cyan if the rule
 * only ever matches an anchor.
 *
 * THE DIGIT USED TO VANISH ON HOVER, AND `background` IS WHY (fixed 2026-09-07).
 *
 * This rule named `border-color` and `color` and NEVER named `background`. Astra
 * fills it instead, from its inline Customizer block:
 *
 *   .woocommerce nav.woocommerce-pagination ul li a:hover,
 *   .woocommerce nav.woocommerce-pagination ul li a:focus {
 *       background: var(--ast-global-color-0);   (0,3,4)
 *       color: #000000;
 *   }
 *
 * Since the 2026-09-04 palette change `--ast-global-color-0` IS `#00AFF0`, i.e.
 * byte-identical to `--ml-cyan`. This rule is (0,4,2) so it wins `color` - and
 * set it to `var(--ml-cyan)`. Cyan text on a cyan fill is **1:1**. The number
 * was still there, painted in its own background.
 *
 * > A RULE ONLY WINS THE PROPERTIES IT NAMES. Outranking Astra on `color` did
 * > nothing about `background`, and the two colours only collided because the
 * > palette moved underneath a rule written when color-0 was `#046bd2`. The
 * > cascade resolve that found it had to match on WHAT THE ELEMENT IS - a scan
 * > filtered on the literal string `page-numbers` would never have seen an
 * > Astra selector reaching it as `ul li a:hover`.
 *
 * The fill is `--ml-cyan-dark`, not `--ml-cyan`, for two reasons: it is the
 * theme's documented "primary hover" token, which every other button here
 * already uses; and it keeps hover visually distinct from the current-page chip
 * below, which is flat `--ml-cyan`. Three readable states, three existing
 * tokens - rest white/navy, hover cyan-dark/NAVY, current cyan/NAVY.
 *
 * THE INK IS NAVY IN ALL THREE, changed 2026-09-07 from white. White on
 * cyan-dark is 3.28:1 and white on cyan 2.50:1 - both under the 4.5:1 this
 * 14px/600 digit is held to. Navy gives 4.74:1 and 6.21:1, so every state of
 * this control now passes AA, which is not true of the four brand-cyan
 * carve-outs recorded in CLAUDE.md and did not need to be true here: nothing
 * about the pagination required white ink.
 *
 * `:focus` IS IN THE LIST AND IS NOT DECORATION. Astra's block above names
 * `a:focus` as well as `a:hover` at the same (0,3,4), and a MOUSE CLICK sets
 * `:focus` without `:focus-visible` - so without it a clicked page link kept
 * Astra's fill with black text until focus moved. Same pin the custom
 * `<button>`s in this theme carry, on an anchor.
 */
.ml-shop__main .woocommerce-pagination li > a.page-numbers:hover,
.ml-shop__main .woocommerce-pagination li > a.page-numbers:focus,
.ml-shop__main .woocommerce-pagination li > a.page-numbers:focus-visible {
	transform: translateY(-2px);
	background: var(--ml-cyan-dark);
	border-color: var(--ml-cyan-dark);
	color: var(--ml-navy);
	box-shadow: 0 12px 22px -14px rgba(var(--ml-shadow-tint), 0.95);
}

/* The theme's focus idiom, used verbatim everywhere else. */
.ml-shop__main .woocommerce-pagination li > a.page-numbers:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
}

/*
 * The current page holds navy-on-cyan in EVERY state, and the extra `.ml-shop`
 * is what makes that a guarantee rather than a coincidence.
 *
 * Before, this rule and the hover rule were both (0,4,1) - an exact tie, decided
 * only by `.current` happening to sit later in the file. Moving either block, or
 * adding any hover rule below them, would have flipped the current page's digit
 * to cyan. `.ml-shop` in front makes this (0,5,1), so it wins on class count and
 * stops depending on source order.
 *
 * It also clears Astra's `.woocommerce nav.woocommerce-pagination ul li
 * span.current` at (0,3,4), which would otherwise paint #046bd2.
 *
 * The states are listed explicitly rather than relying on the base rule: the
 * span is not focusable today, but a plugin that made it so would otherwise
 * inherit the anchor's lift.
 *
 * `font-weight: 700` IS THE ONLY NON-COLOUR CUE SEPARATING THIS FROM HOVER, and
 * it was added when both inks became navy (2026-09-07). The two fills are
 * `--ml-cyan` and `--ml-cyan-dark`, which are **1.31:1 apart** - a real but weak
 * difference, and it was already the ONLY difference before the ink changed,
 * because both states were white-on-cyan then too. So nothing was lost by
 * moving to navy; what was missing all along is a cue that does not depend on
 * telling two adjacent cyans apart.
 *
 * The lift and shadow also separate them - hover has both, this has neither -
 * but `prefers-reduced-motion` suppresses the lift, so weight is the cue that
 * survives every condition. Base and hover are 600.
 */
.ml-shop .ml-shop__main .woocommerce-pagination li > .page-numbers.current,
.ml-shop .ml-shop__main .woocommerce-pagination li > .page-numbers.current:hover,
.ml-shop .ml-shop__main .woocommerce-pagination li > .page-numbers.current:focus-visible {
	background: var(--ml-cyan);
	border-color: var(--ml-cyan);
	color: var(--ml-navy);
	font-weight: 700;
	transform: none;
	box-shadow: none;
}

/*
 * The prev/next chevrons, injected by medlife_pagination_chevrons() in
 * functions.php. `currentColor` is what ties them to the states above - the
 * chevron takes the same navy, cyan-on-hover and white-on-current as the digits.
 */
.ml-shop__main .woocommerce-pagination li > .page-numbers svg {
	display: block;
	width: 18px;
	height: 18px;
}

.ml-shop__empty {
	padding: 40px 20px;
	text-align: center;
	border: 1px dashed var(--ml-line);
	border-radius: 12px;
	color: var(--ml-body);
}

.ml-shop__empty .ml-filters__apply {
	max-width: 220px;
	margin: 16px auto 0;
}

/* -------------------------------------------------------------------------
 * Responsive
 * ---------------------------------------------------------------------- */

@media (max-width: 1023px) {
	.ml-shop__body .ml-shop__body-inner .ml-shop__main ul.products {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (max-width: 1024px) {
	.ml-shop__head,
	.ml-shop__body {
		padding-left: 20px;
		padding-right: 20px;
	}

	/*
	 * The column is a percentage capped at 260px, so it narrows on its own -
	 * only the gutter tightens here. The heart needs nothing: it is a grid item
	 * in the panel's cell and follows whatever width the panel takes.
	 */
	.ml-shop__main.is-list ul.products li.product.ml-card {
		gap: 16px;
	}

	.ml-shop__body-inner {
		grid-template-columns: minmax(0, 1fr);
	}
}

/*
 * THE 599px IMAGE-HEIGHT BREAKPOINT IS GONE - 2026-09-18.
 *
 * It read: `@media (max-width: 599px) { .ml-card__media { height: 190px } }`,
 * and it existed because the box was a FIXED height that had to be trimmed on
 * a phone. The box is `aspect-ratio: 1 / 1` now, so it tracks the column by
 * construction and there is nothing left to trim - and a `height` here would
 * have silently overridden the ratio and brought the letterbox back at 360.
 *
 * This is checklist item 2 in the flesh: change a value and its @media twin
 * has to change with it, or the old one wins at that width only.
 */

@media (max-width: 640px) {
	/*
	 * THE ONE-COLUMN RULE AND THE 16px GUTTERS BOTH LEFT THIS BLOCK -
	 * 2026-09-22.
	 *
	 * The grid is two columns from 768 down now, and the compact-card block at
	 * the end of this file sets both. Leaving the old
	 * `grid-template-columns: minmax(0, 1fr)` here would have been worse than
	 * redundant: it is the SAME (0,4,1) selector, so it lost only because the
	 * newer rule sits later in the file - a dead rule that reads like a live
	 * one and that any reordering would silently switch back on.
	 */

	/*
	 * The 17px title override is gone - 2026-09-18. The card spec is 16px/600
	 * at every width, and a 17px variant here would be the only place the
	 * ladder was not what the spec says. The two-line `min-height` is in `em`,
	 * so it follows the font size on its own.
	 */

	.ml-shop__toolbar {
		gap: 12px;
	}

	/*
	 * THE LIST'S OWN PHONE LAYOUT WAS DELETED FROM THIS BLOCK - 2026-09-18.
	 *
	 * It used to build a third thing here: a stacked variant with its own
	 * 180px image, its own 8px padding, its own 18px title and 13.5px text.
	 * The owner's brief is that below 769px the list IS the grid card and
	 * there is no separate layout, so the hand-back now lives in its own
	 * `max-width: 768px` block after this one. Only the non-list rules above
	 * are still keyed to 640.
	 */
}

/*
 * BELOW 769px THE LIST IS SIMPLY THE GRID CARD - 2026-09-18.
 *
 * Not a stacked list, not a third layout: every desktop `.is-list` override is
 * handed back so the row renders exactly as `.ml-card` does, one per row. The
 * toggle stops changing anything at this width, which is the intent - a 328px
 * card cannot carry a 200px image column beside a text column, and the two
 * previous attempts at a phone list layout both ended up as their own thing to
 * maintain.
 *
 * 768 rather than 640 because that is where the brief puts it, and it is also
 * where the grid itself drops to two columns - so the two views stop diverging
 * at the same width instead of 128px apart.
 *
 * EVERY RULE HERE UNDOES A SPECIFIC DESKTOP LIST RULE. If a list rule is added
 * above, ask whether it needs undoing here; that is checklist item 2 for this
 * view, and it is the step the previous phone layout skipped.
 */
@media (max-width: 768px) {
	.ml-shop__main.is-list ul.products li.product.ml-card {
		display: flex;
		flex-direction: column;
		grid-template-columns: none;
		gap: 0;
		padding: 16px;
		min-height: 0;
	}

	.ml-shop__main.is-list .ml-card__media {
		grid-column: auto;
		grid-row: auto;
		align-self: auto;
		width: auto;
		height: auto;
		aspect-ratio: 1 / 1;
	}

	.ml-shop__main.is-list .ml-card__body {
		grid-column: auto;
		grid-row: auto;
		padding: 0;
	}

	.ml-shop__main.is-list .ml-card__cat {
		margin: 14px 0 6px;
	}

	.ml-shop__main.is-list .ml-card__title {
		margin: 0 0 8px;
		font-size: 16px;
		min-height: 2.6em;
	}

	/* The SHORT excerpt and its More come back; the long one goes - the grid
	   card's arrangement, which is the whole point of this block. */
	.ml-shop__main.is-list .ml-card__excerpt {
		display: -webkit-box;
		margin: 0;
		min-height: 3em;
	}

	.ml-shop__main.is-list .ml-card__more {
		display: block;
		align-self: flex-end;
		margin: 0 0 12px;
	}

	.ml-shop__main.is-list .ml-card__detail,
	.ml-shop__main.is-list .ml-card__detail-more {
		display: none;
	}

	.ml-shop__main.is-list .ml-card__price {
		margin: 0 0 16px;
		font-size: 20px;
	}

	/* Out of the row's bottom-right corner and back into the column, where
	   `margin-top: auto` levels it exactly as it does on the grid card. */
	/*
	 * `relative`, NOT `static` - changed 2026-09-18 with the clickable card.
	 *
	 * `static` was right when it only had to undo the desktop row's
	 * `position: absolute`. It is wrong now: a `z-index` has no effect on a
	 * static element, so the action row lost its layer here and VIEW PRODUCT
	 * fell UNDER the stretched overlay. The cart survived only because it
	 * carries `position: relative` of its own - which is exactly the kind of
	 * accident that hides a bug.
	 *
	 * Caught by the click test reporting `actions=static/2` at this width while
	 * every other surface read `relative/2`.
	 */
	.ml-shop__main.is-list .ml-card__actions {
		position: relative;
		justify-content: stretch;
		margin-top: auto;
		width: 100%;
	}

	.ml-shop__main.is-list .ml-card .ml-card__view {
		flex: 1 1 auto;
		width: auto;
	}

	.ml-shop__main.is-list .ml-card__wishlist {
		top: 24px;
		right: 24px;
		left: auto;
	}
}

@media (prefers-reduced-motion: reduce) {
	.ml-shop__pill,
	.ml-filters__apply,
	.ml-shop__view-btn,
	.ml-card__wishlist .tinvwl_add_to_wishlist_button,
	.ml-shop__main .woocommerce-pagination li > a.page-numbers {
		transition: none;
	}

	/*
	 * .ml-shop__pill:focus-visible IS NAMED HERE ON PURPOSE, and its absence
	 * was a measured bug rather than an oversight of style: the lift lives on
	 * the grouped rule ".ml-shop__pill:hover, .ml-shop__pill:focus-visible",
	 * so suppressing only the :hover half left a KEYBOARD user with reduced
	 * motion still getting the 2px rise on focus. Confirmed by emulating the
	 * media feature and forcing :focus-visible - :hover read transform none,
	 * :focus-visible read matrix(1, 0, 0, 1, 0, -2).
	 *
	 * The lesson is the one this file keeps relearning: a reduced-motion twin
	 * has to name EVERY pseudo-class the rule it suppresses names. A grouped
	 * selector hides that, because the block reads as if it is covered.
	 */
	.ml-shop__pill:hover,
	.ml-shop__pill:focus-visible,
	.ml-filters__apply:hover,
	.ml-shop__main .woocommerce-pagination li > a.page-numbers:hover,
	.ml-shop__main .woocommerce-pagination li > a.page-numbers:focus,
	.ml-shop__main .woocommerce-pagination li > a.page-numbers:focus-visible {
		transform: none;
	}

	.ml-card__wishlist .tinvwl_add_to_wishlist_button:hover {
		transform: none;
	}

	/* No transition and no travel - the pill appears in place, nothing moves. */
	.ml-card__pack,
	.ml-card:hover .ml-card__pack,
	.ml-card:focus-within .ml-card__pack {
		transform: translate(-50%, 0);
		transition: none;
	}
}

/* -------------------------------------------------------------------------
 * Card: description, price order and the action row (2026-09-04)
 *
 * ASTRA TRAPS HANDLED HERE, both recorded in CLAUDE.md:
 *
 *   1. `.button` is in Astra's bare-button selector list, so the add-to-cart
 *      anchor - which MUST carry that class for `wc-add-to-cart.js` to bind -
 *      inherits `padding: 10px 20px`, a 1px border and a 30px radius. An
 *      icon-only control needs `padding: 0` or the 24px viewBox is squeezed
 *      into a ~4px content box and renders as a dot.
 *   2. `.button:hover`/`:focus` repaints to `var(--ast-global-color-1)` at
 *      (0,1,1), and a clicked anchor keeps `:focus`. Both controls are
 *      state-locked across all four states at (0,2,0) or better.
 * ---------------------------------------------------------------------- */

/* Even heights: the card fills its grid cell and the action row is pushed to
   the bottom, so rows stay level however short a description is. */
.ml-card {
	display: flex;
	flex-direction: column;
	height: 100%;
}

.ml-card__body {
	display: flex;
	flex-direction: column;
	flex: 1 1 auto;
}

/*
 * The title's size, weight and line clamp live in `.ml-card__title` above -
 * ONE rule, not two. A second block here is how this file ended up declaring
 * the same font-size at two specificities.
 */
/*
 * No underline at rest - the card title is the card's own heading, and an
 * underline on every one turns the grid into a wall of rules.
 *
 * It comes back on hover and :focus-visible so the title still reads as a link.
 * The offset and thickness stay declared HERE rather than in the state rule,
 * because a `text-decoration` shorthand elsewhere would reset them.
 *
 * These are (0,2,1) and (0,2,2): the base `.ml-card__title a` block already
 * says `text-decoration: none` at (0,1,1), so the rest state is stated twice on
 * purpose - this block is the redesign layer and must not depend on the base
 * rule staying as it is.
 */
.ml-card .ml-card__title a {
	text-decoration: none;
	text-underline-offset: 3px;
	text-decoration-thickness: 1px;
}

.ml-card .ml-card__title a:hover,
.ml-card .ml-card__title a:focus-visible {
	text-decoration: underline;
}

/* Short description - two lines, then the More link. */
/*
 * Sale price first, original struck through after it. Done with `order` so
 * WooCommerce's own <del>/<ins> markup is left exactly as it ships - price
 * ranges and any filter on get_price_html() keep working.
 */
.ml-card__price {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 8px;
}

.ml-card__price ins {
	order: 1;
	background: none;
	text-decoration: none;
	font-weight: 700;
	color: var(--ml-navy);
}

/*
 * The struck original. It only exists when WooCommerce emits it, i.e. on a
 * genuine sale - there is no way to fake a discount from CSS here.
 */
.ml-card__price del {
	order: 2;
	opacity: 1;
	color: var(--ml-muted);
	font-weight: 400;
	font-size: 0.6em;
	text-decoration: line-through;
}

/* ---------- Action row ---------- */

/* `margin-top: auto` is what levels the rows - it absorbs the difference
   between a card with a two-line description and one with none. */
/*
 * OPTION A, 2026-09-07: the action pair is CONTENT WIDTH and CENTRED.
 *
 * VIEW PRODUCT was `flex: 1 1 auto`, so it stretched to whatever the card left
 * over - ~270px at 1440 - and a 44px-tall bordered rectangle that wide is the
 * heaviest thing on a card whose next-largest element is a 16px price. The
 * height was never the problem; the width was.
 *
 * `flex: 0 0 auto` sizes it to its own text instead, and `justify-content:
 * center` centres the pair under the price:
 *
 *     ~92px  "VIEW PRODUCT" at 12px/600, 0.02em
 *   +  28px  padding (0 14px)
 *   +   2px  1px border, both edges
 *   = ~122px button
 *   +   8px  gap
 *   +  44px  cart
 *   = ~174px pair
 *
 * The 44px touch target is untouched - only the horizontal axis changed.
 * KEEP THIS IN SYNC WITH trending.css.
 */
/*
 * VIEW PRODUCT takes the remaining width, the cart is a fixed square beside it.
 * `margin-top: auto` is what pushes the row to the bottom of the card so rows
 * stay level when one product has no description or no reviews.
 */
/*
 * `margin-top: auto` lives here now - see the note on `.ml-card__price`. It is
 * what makes every card in a grid row end on the same line, whatever its title
 * and description do above it. `padding-top: 0` because the 16px below the
 * price is that step in the ladder.
 */
.ml-card__actions {
	justify-content: stretch;
	display: flex;
	align-items: stretch;
	gap: 8px;
	margin-top: auto;
	padding-top: 0;
}

/*
 * VIEW PRODUCT IS A BUTTON AGAIN - the outline half of a matched pair
 * (2026-09-07, replacing the Option B text link of the same day).
 *
 * The history matters, because the box has now been wrong in two opposite
 * directions and the fix is neither of them:
 *
 *   1. It was a bordered navy button the same visual weight as the cyan cart
 *      square. Two equal bordered boxes read as TWO buttons, not one control
 *      group, and the card only has one action worth that weight.
 *   2. So it became a borderless text link. That fixed the weight and lost the
 *      pairing - a bare word beside a 44px filled square reads as two unrelated
 *      things sitting next to each other.
 *
 * WHAT MAKES THEM A PAIR IS SHARED GEOMETRY PLUS DIFFERENT RANK. Every
 * dimension is identical to `__cart` and must stay identical:
 *
 *     height       44px      (min-height here, min-height there)
 *     radius       8px
 *     border       1px
 *     gap          8px, on .__actions
 *
 * and the rank is carried by FILL alone - this one is the outline/secondary,
 * `__cart` is the solid/primary. That is what stops them competing without
 * making them look unrelated. Change any of the four numbers in one file and
 * the pair breaks; change them in both or not at all.
 *
 *   ~92px  "VIEW PRODUCT" at 12px/600 Poppins, 0.02em
 * +  24px  padding (0 12px)
 * +   2px  border
 * = ~118px
 * +   8px  gap
 * +  44px  cart square
 * = ~170px pair, centred by `justify-content: center` on the row
 *
 * `flex: 0 0 auto` is what keeps it content-width. It is NOT decoration and it
 * is not redundant: with `flex: 1 1 auto` the button stretches to the card and
 * the whole "two heavy boxes" problem comes straight back.
 *
 * KEEP THIS IN SYNC WITH trending.css.
 */
.ml-card .ml-card__view,
.ml-card .ml-card__view:hover,
.ml-card .ml-card__view:focus,
.ml-card .ml-card__view:focus-visible,
.ml-card .ml-card__view:active {
	flex: 1 1 auto;
	min-width: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	/* 44px, 2026-09-18 - the spec's button height, and it restores the WCAG
	   2.5.8 target size the 42px compaction had dropped below. Must stay
	   identical to __cart; see the pair note above. */
	min-height: 44px;
	padding: 0 10px;
	border: 1px solid var(--ml-navy);
	border-radius: 8px;
	background: var(--ml-white);
	color: var(--ml-navy);
	font-family: var(--ml-font);
	font-size: 12px;
	font-weight: 700;
	letter-spacing: 0.02em;
	text-decoration: none;
	white-space: nowrap;

	/*
	 * `--ml-t-base` / `--ml-e-standard`, chosen rather than defaulted.
	 *
	 * The token system names three durations by ROLE: `--ml-t-base` 180ms is
	 * "interaction - hovers, lifts, fills, colour states", `--ml-t-panel` 280ms
	 * is disclosure, `--ml-t-enter` 420ms is entrance. This is a hover fill, so
	 * it is `--ml-t-base` by definition; the other two would make a button feel
	 * like a drawer opening. A new literal duration would be drift.
	 *
	 * Only `background-color` and `color` are listed. `border-color` is NOT -
	 * the border is `--ml-navy` in both states, so there is nothing to animate,
	 * and naming a property that never changes just makes the rule lie about
	 * what moves.
	 *
	 * > `__cart` DELIBERATELY HAS NO TRANSITION - it was stripped of all motion
	 * > by request, and that decision still stands. So the two halves of this
	 * > pair match in form and NOT in timing: this one fades over 180ms, the
	 * > cart snaps. Only one can be hovered at a time so they are never seen
	 * > disagreeing, but if the mismatch ever reads wrong, the fix is to drop
	 * > this transition - not to re-animate the cart.
	 */
	transition:
		background-color var(--ml-t-base) var(--ml-e-standard),
		color var(--ml-t-base) var(--ml-e-standard);
}

/*
 * The fill swap: navy border becomes a navy fill, ink goes white. White on
 * `--ml-navy` is 15.54:1 - the strongest pair available in the token set, and
 * the same figure as the rest state's navy on white, so the control does not
 * get harder to read at the moment the pointer arrives. (That is not true of
 * the four brand-cyan carve-outs recorded elsewhere in this file; it is true
 * here because the accent is navy, not cyan.)
 *
 * `:focus-visible` is here; PLAIN `:focus` IS NOT, and the split is deliberate.
 * A mouse click sets `:focus` without `:focus-visible`, and a link that stayed
 * filled after being clicked would look stuck. `:focus` is instead named in the
 * rest list above, which pins it to the rest appearance and - the actual point -
 * beats Astra's inline `a:focus { color: var(--ast-global-color-1) }` at
 * (0,1,1), which since the palette change paints brand cyan-dark.
 *
 * Two more Astra anchor rules are answered by the same (0,3,0):
 * `a { color: var(--ast-global-color-0) }` (0,0,1) and
 * `a:hover { text-decoration: none }` (0,1,1) - the second is the sharp one,
 * because a single-class rule would have had its `text-decoration` silently
 * overridden.
 */
.ml-card .ml-card__view:hover,
.ml-card .ml-card__view:focus-visible {
	background: var(--ml-navy);
	color: var(--ml-white);
}

.ml-card .ml-card__view:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
}

.ml-card .ml-card__view:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
}

/*
 * Filled brand-cyan square with a white trolley icon.
 *
 * `--ml-cyan` IS #00AFF0 - the token rather than the literal, so this cannot
 * drift from the brand value the logo was sampled from.
 *
 * `padding: 0` is load-bearing, not tidiness - see trap 1 above. The width and
 * min-height match the VIEW PRODUCT button beside it exactly, so the two read
 * as one control pair rather than two sizes.
 */
/*
 * EVERY SELECTOR HERE IS (0,3,0), AND IT HAS TO BE.
 *
 * Astra's inline Customizer CSS ships `.woocommerce-js a.button` at **(0,2,1)**,
 * which beats the (0,2,0) this block used to carry — so the theme's rest state
 * was never what rendered. What Astra was actually painting:
 *
 *     background-color: transparent;          <- not the cyan fill
 *     color: var(--ast-global-color-0);       <- #00AFF0, so a CYAN icon
 *     border-color: var(--ast-global-color-0);
 *     border-*-width: 1px;                    <- not 1.5px
 *     border-*-radius: 30px;                  <- a CIRCLE on a 42px box
 *     padding: 10px 20px;                     <- leaves a 0px content box
 *
 * That is the "circular outline with a cyan icon" that was reported, and the
 * padding is why the icon overflowed its own button. Astra's `:hover` rule,
 * also (0,2,1), then filled it `--ast-global-color-1` with a white icon — which
 * is why the button only looked right while the pointer was on it.
 *
 * `--ast-global-color-0/-1` are `#00AFF0` / `#0098d1` since the palette change,
 * i.e. the same two tokens this block uses. The colours were never the problem;
 * the STATE they were attached to was.
 *
 * The extra class is `.ml-card__actions`, the wrapper the button always sits
 * in. That was chosen over the button's own second class because the markup has
 * TWO variants — `.button` on the AJAX anchor for a simple, purchasable,
 * in-stock product, and `.ml-card__cart--link` on the plain product link for
 * everything else — and the wrapper covers both without naming either.
 *
 * `:is(.button, .ml-card__cart--link)` would also measure (0,3,0), since `:is()`
 * takes the specificity of its most specific argument. It was written that way
 * first and then replaced: **the comma inside `:is()` is not a selector-list
 * comma**, and every naive tool that splits a rule on commas — a minifier, a
 * linter, the cascade resolver used to diagnose this very bug — reads it as
 * `.ml-card .ml-card__cart:is(.button` and reports (0,2,0). The plain three
 * classes are unambiguous to read and to verify.
 *
 *
 * Pairs with `.medlife-card .medlife-card__actions .medlife-card__cart` in
 * trending.css - same button, different surface. Keep the two in sync.
 */
.ml-card .ml-card__actions .ml-card__cart,
.ml-card .ml-card__actions .ml-card__cart:hover,
.ml-card .ml-card__actions .ml-card__cart:focus,
.ml-card .ml-card__actions .ml-card__cart:focus-visible,
.ml-card .ml-card__actions .ml-card__cart:active {
	flex: 0 0 auto;
	display: flex;
	align-items: center;
	justify-content: center;
	/* 44px, matching __view exactly - the pair's shared geometry. */
	width: 44px;
	min-height: 44px;

	/*
	 * Astra sets padding and radius as LONGHANDS at (0,2,1). A shorthand here
	 * still wins — specificity decides, not shorthand-ness — but it must be
	 * stated, or the longhands come through untouched.
	 */
	padding: 0;
	border: 1px solid var(--ml-cyan);
	border-radius: 8px;

	/*
	 * 8px, not a circle: `.ml-card__view` sits directly beside this button with
	 * `border-radius: 8px` and the same 1px border, so the pair reads as one
	 * control group. A round button next to an 8px pill reads as two unrelated
	 * controls. The 30px was Astra's default, not a design decision.
	 */
	background: var(--ml-cyan);
	color: var(--ml-white);
	text-decoration: none;

	/*
	 * STATIC BY REQUEST - no transition, no transform, no keyframes, in any
	 * state. The hover below is a colour swap that lands instantly.
	 *
	 * This is a deliberate exception to the motion token system: the tokens
	 * exist so a gesture is not written two ways, not to force one onto a
	 * control that should not move. Do not "restore" `var(--ml-t-base)` here.
	 */
	transition: none;
	transform: none;
	animation: none;
}

/*
 * THE 8px THAT MADE THE SHOP PAIR 16px TALLER THAN THE TRENDING PAIR.
 *
 * Reported 2026-09-07 as "shop's VIEW PRODUCT is bigger and heavier than
 * trending's". It was, by 16px of height - and NOT ONE DECLARATION ON EITHER
 * BUTTON DIFFERED. Every authored value matched, resolved from the served CSS
 * on both surfaces: 119.8px wide, 12px/600, 0.02em, 1px border, 8px radius,
 * `min-height: 44px`, `padding: 0 12px`.
 *
 * The whole difference was a margin on the CART, from a rule that names
 * neither button and that grepping for either class name cannot find:
 *
 *     .woocommerce-js  ul.products li.product .button,
 *     .woocommerce-page ul.products li.product .button {
 *         margin-top: .5em; margin-bottom: .5em;    <- 8px at 16px
 *     }                                                (0,4,2)
 *
 * `.ml-card` IS the `li.product`, so on `/shop/`, on every category archive
 * and in the related-products strip the cart carried 8px above and below. In
 * the homepage carousel the card is `li.medlife-card` inside
 * `ul.medlife-carousel__track` - no `.products`, no `.product` - so the same
 * shared component was clean there. THE CONTAINER WAS THE ONLY VARIABLE.
 *
 * How 8px of margin became 16px of button:
 *
 *     cart 44px + 8px + 8px = 60px hypothetical cross size
 *  -> the flex line is 60px, not 44px
 *  -> `.ml-card__actions` is `align-items: stretch` and `__view` sets only
 *     `min-height`, so IT STRETCHED to 60px, while the cart kept its own 44px
 *     and sat centred in the line.
 *
 * THE BUTTON THAT LOOKED WRONG WAS NEVER THE ONE WITH THE WRONG VALUE ON IT.
 * Proof, in the browser: giving `__view` `align-self: flex-start` dropped it
 * to 44px on its own, and hiding the CART - not the view - was what took the
 * row from 78px back to 62px.
 *
 * (0,5,2), one tier above the (0,4,2) it answers, per the rule this project
 * follows everywhere: never settle a cascade fight on load order. The cart's
 * own block above is (0,3,0) and could not have carried this.
 *
 * No state variants are needed: the WooCommerce rule is not state-scoped, and
 * a rest rule keeps matching through `:hover` and `:focus`. No `@media` block
 * sets a margin on either control at any width - verified at 360/768/1024/1440.
 * It covers `.ml-card__cart--link` as well as `.button`, because it names the
 * component's own class rather than either variant.
 */
ul.products li.product.ml-card .ml-card__actions .ml-card__cart {
	margin: 0;
}

/*
 * The state lock. The grouped rule above already names all five states, so this
 * is belt and braces - but it is what stops a later single-class rule, or
 * Astra's inline `button:hover` block, reintroducing motion. At (0,3,0) it
 * outranks Astra's (0,1,1) without an `!important`.
 */
.ml-card .ml-card__actions .ml-card__cart:hover,
.ml-card .ml-card__actions .ml-card__cart:focus,
.ml-card .ml-card__actions .ml-card__cart:focus-visible,
.ml-card .ml-card__actions .ml-card__cart:active {
	transition: none;
	transform: none;
	animation: none;
}

/*
 * Hover and focus change TWO COLOURS AND NOTHING ELSE. Every geometry property
 * is already pinned across all five states above, so there is no size jump —
 * which matters here because Astra's `:focus` rule
 * (`.woocommerce a.add_to_cart_button:focus { border-width: thin }`, also
 * (0,2,1)) would otherwise thin the border the moment the button took focus.
 */
.ml-card .ml-card__actions .ml-card__cart:hover,
.ml-card .ml-card__actions .ml-card__cart:focus-visible {
	background: var(--ml-cyan-dark);
	border-color: var(--ml-cyan-dark);
	color: var(--ml-white);
}

/* Kept - a focus indicator is 2.4.7, and it paints rather than moves. */
.ml-card .ml-card__actions .ml-card__cart:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
}

/* WooCommerce marks the anchor while the AJAX request is in flight. */
.ml-card .ml-card__cart.loading {
	opacity: 0.6;
}

.ml-card .ml-card__cart.added::after {
	content: none;
}

/* The "View cart" link WooCommerce injects after a successful add. */
.ml-card__actions .added_to_cart {
	flex: 0 0 100%;
	order: 3;
	font-family: var(--ml-font);
	font-size: 12.5px;
	font-weight: 600;
	color: var(--ml-cyan-dark);
	text-decoration: underline;
	text-underline-offset: 2px;
}

/*
 * THE VIEW BUTTON ANIMATES AGAIN, SO IT IS COVERED AGAIN.
 *
 * It carried no transition while it was a text link and was correctly dropped
 * from this block then. The 2026-09-07 return to a bordered button gave it a
 * 180ms fill fade, and `transition` is exactly what 2.3.3 asks to be
 * suppressible - so it is named here once more.
 *
 * `.ml-card__cart` is still absent, and still deliberately: it was stripped of all
 * motion by request and has nothing to suppress. That asymmetry is the same one
 * recorded on the button itself - the pair matches in form, not in timing.
 *
 * Mirrors trending.css.
 */
@media (prefers-reduced-motion: reduce) {
	.ml-card .ml-card__view {
		transition: none;
	}

	/* The 2px hover lift added with the 2026-09-07 card redesign. */
	.ml-card,
	.ml-card:hover,
	.ml-card:focus-within {
		transition: none;
		transform: none;
	}
}

/*
 * Pagination was the one animated control in this file with no reduced-motion
 * cover - it lifts on hover with `transform: translateY()`. Colour-only fades
 * elsewhere are left alone deliberately; 2.3.3 is about motion, not a tint.
 */
@media (prefers-reduced-motion: reduce) {
	.ml-shop__main .woocommerce-pagination li > .page-numbers {
		transition: none;
	}

	.ml-shop__main .woocommerce-pagination li > .page-numbers:hover,
	.ml-shop__main .woocommerce-pagination li > .page-numbers:focus-visible {
		transform: none;
	}
}

/*
 * POINTER FOCUS - ASTRA a:focus { outline: thin dotted } (main.min.css L3).
 * A mouse click leaves :focus set, so every anchor that names no outline of its
 * own kept a thin dotted ring until focus moved. Measured 2026-09-13 by the
 * sitewide anchor sweep (CLAUDE.md, "Anchor pointer-focus sweep").
 *
 * :not(:focus-visible) IS LOAD-BEARING: this only ever applies to pointer focus,
 * so the keyboard ring each family already has is untouched. Outline only -
 * nothing moves, so there is no reduced-motion twin.
 */
a.ml-card__detail-more:focus:not(:focus-visible),
a.ml-card__view:focus:not(:focus-visible),
.woocommerce-pagination a.page-numbers:focus:not(:focus-visible) {
	outline: none;
}

/*
 * KEYBOARD RINGS - 2026-09-14. These families had NO focus ring of their own:
 * keyboard focus showed only Astra's a:focus-visible 1px dotted in the text
 * colour, or nothing at all where a theme rule said outline: none. Measured by
 * the sitewide anchor sweep (CLAUDE.md, "Keyboard ring pass").
 *
 * Light surface: 3px --ml-cyan offset 3px. Navy surface: 2px. border-radius is
 * RESTATED on every ring - a rule wins only the properties it names, and a
 * shared focus rule has squared a round control on this site before. Text links
 * take 4px corners; boxed controls keep their own radius.
 *
 * The pointer pin (:focus:not(:focus-visible)) above is untouched - the two
 * selectors can never match the same state. Contrast: cyan on white 2.50:1 is
 * the owner's recorded decision (CLAUDE.md, "THE FOCUS-RING RULE").
 */
a.ml-card__detail-more:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
	border-radius: 4px;
}

/*
 * RING FADE - ASTRA'S INLINE `a { transition: all .2s linear }`. "all" includes
 * outline, so these families' keyboard rings slid and faded in over 200ms
 * instead of appearing at once. Each family now names exactly what its hover /
 * focus-visible state really changes, measured 2026-09-14 (CLAUDE.md, "Ring fade
 * pass"): colour where Astra's a:hover repaints the ink, nothing at all where
 * only the (discrete) underline changes or nothing visible changes.
 * Outline is never in the list. Colour-only fades need no reduced-motion twin.
 */
a.ml-card__detail-more {
	transition: color var(--ml-t-base) var(--ml-e-standard);
}

/* -------------------------------------------------------------------------
 * <= 768px: the category pills clear 44px (2026-09-21 mobile audit)
 *
 * Measured 122.5 x 43.1 - 0.9px short of WCAG 2.5.8, from `9px 18px` padding
 * on a 14px/1.65 label.
 *
 * `.mlp-pill` ON THE PHARMA PAGE GETS THE SAME LINE IN THE SAME PASS. This
 * file records the two as a deliberate parity pair ("`.mlp-pill` deliberately
 * reproduces `.ml-shop__pill` ... measured identical at 43px tall in every
 * state"), so raising one alone would silently split them. Desktop keeps 43.1
 * on both - this is inside the query.
 * ---------------------------------------------------------------------- */
@media (max-width: 768px) {
	.ml-shop__pills .ml-shop__pill {
		min-height: 44px;
	}
}

/* =========================================================================
 * <= 640px: TWO COLUMNS (2026-09-21 mobile audit)
 *
 * The grid was one column below 640, so a 375px phone showed one ~343px card
 * per screen and the catalogue read as a very long list. Two columns is what
 * the reference (herbiotics.com.pk) does on a phone, and it is what this pass
 * was asked for.
 *
 * THE ARITHMETIC IS WHY THE ACTION ROW CHANGES WITH IT, and it is the same
 * arithmetic this file already records for the 2026-09-07 grid ladder:
 *
 *     375 viewport - 32 body padding            = 343
 *     (343 - 20 gap) / 2                        = 161.5 card
 *     161.5 - 32 card padding                   = 129.5 content
 *     129.5 - 44 cart - 8 gap                   =  77.5 for VIEW PRODUCT
 *     "VIEW PRODUCT" at 12px/600 + 0.02em       = ~92
 *
 * So the pair does not fit, and a control that cannot shrink inside a
 * container someone else is sizing is exactly the failure recorded at 360 in
 * the old ladder. Rather than shrink the label until it is unreadable, the
 * outline half stands down here:
 *
 * VIEW PRODUCT IS HIDDEN AND THE CART GOES FULL WIDTH. The whole card has
 * been a single stretched link since 2026-09-18, so "VIEW PRODUCT" leads
 * exactly where tapping the card already leads - it is the duplicate of the
 * two. The cart is the only action the overlay cannot perform, so it is the
 * one worth the width.
 *
 * The matched-pair note still governs 641px and up, where both render.
 * ====================================================================== */
/* =========================================================================
 * <= 768px: THE COMPACT MOBILE CARD (2026-09-22, owner's brief)
 *
 * Supersedes the <= 640px block this replaces, which arrived a day earlier as
 * part of the mobile audit. It did the columns and left the card's desktop
 * anatomy in place; this does the card.
 *
 * Reference: nutrifactor.com.pk on a phone - image, name, price, one cart
 * control, and nothing else. What the desktop card carries and this one drops
 * is listed below, each with its reason, because every one of them is real
 * data rather than decoration and dropping it is a decision:
 *
 *   category label  - a cyan uppercase line above a 15px name, on a 161px
 *                     card, reads as a competing heading rather than a label.
 *                     It is subordinate BY TYPE SCALE on the desktop card
 *                     (12/600 cyan under 16/600 navy), and at phone sizes
 *                     there is not enough scale left between them for that to
 *                     work. Still in the markup, still on the product page.
 *   short excerpt   - two lines of ~20 characters each. The clamp does not
 *   + "More"          truncate gracefully at that width, and "More" duplicates
 *                     the stretched card link that already covers the tile.
 *   VIEW PRODUCT    - the same destination as tapping the card. The cart is
 *                     the only action the overlay cannot perform.
 *   rating          - INERT TODAY: zero approved reviews store-wide, so the
 *                     template's `get_review_count()` guard has never emitted
 *                     it. Named anyway because the brief says no stars on the
 *                     mobile card, and the day a review lands this is what
 *                     keeps that true.
 *
 * NOT dropped: the pack-size pill and the badges. Both are owner-entered data
 * and both are handled below rather than hidden.
 * ====================================================================== */
@media (max-width: 768px) {
	/*
	 * 16px gutters, per the brief. This tier previously inherited the <= 1024
	 * rule's 20px and only reached 16 at 640.
	 */
	.ml-shop__head,
	.ml-shop__body {
		padding-left: 16px;
		padding-right: 16px;
	}

	.ml-shop__body .ml-shop__body-inner .ml-shop__main ul.products,
	.ml-product .ml-product__related section.related ul.products {
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: 18px 12px;
	}

	/*
	 * 16px -> 10px, AND THIS IS THE WHOLE OF "make the light-blue padding
	 * smaller" - the image's own inset inside the tile is deliberately NOT
	 * touched. See the note under .ml-card__media img: the artwork is 88% tall
	 * at `margin-top: 2%`, which reserves the tile's bottom 10% for the pack
	 * pill. Raising it to 92% on a 145px tile leaves 8.7px for a ~20px pill and
	 * puts it back on the carton - the exact defect that percentage was chosen
	 * to avoid.
	 *
	 * Taking it off the CARD instead grows the tile from 133px to 145px at 375,
	 * so the packshot gains ~9% with the pill's strip intact.
	 */
	ul.products li.product.ml-card,
	.ml-product__related ul.products li.product.ml-card {
		padding: 10px;
	}

	/* Badges and heart were insets of 24px = the old 16px card padding + 8px
	   inside the tile. The card padding moved, so these move with it or they
	   drift 6px into the artwork. Checklist item 2, on a value rather than a
	   breakpoint. */
	.ml-card .ml-card__badges {
		top: 16px;
		left: 16px;
		gap: 4px;
	}

	.ml-card .ml-card__wishlist {
		top: 16px;
		right: 16px;
	}

	.ml-card .ml-card__badge {
		padding: 3px 8px;
		font-size: 10px;
	}

	.ml-card .ml-card__wishlist .tinvwl_add_to_wishlist_button {
		width: 28px;
		height: 28px;
	}

	/* Dropped - see the block comment. */
	.ml-card .ml-card__cat,
	.ml-card .ml-card__excerpt,
	.ml-card .ml-card__more,
	.ml-card .ml-card__detail,
	.ml-card .ml-card__detail-more,
	.ml-card .ml-card__rating,
	.ml-card .ml-card__actions .ml-card__view {
		display: none;
	}

	.ml-card .ml-card__body {
		align-items: center;
		text-align: center;
	}

	.ml-card .ml-card__title {
		text-align: center;
	}

	.ml-card .ml-card__price {
		justify-content: center;
		text-align: center;
	}

	/*
	 * The cart keeps its 44 x 44 square - "chhota cyan cart button" is the
	 * brief, and this is already that. It is centred rather than stretched,
	 * which REVERSES yesterday's `flex: 1 1 auto; width: auto`: with VIEW
	 * PRODUCT hidden, a full-width cyan bar is the loudest thing on the card
	 * and competes with the price directly above it.
	 */
	.ml-card .ml-card__actions {
		justify-content: center;
	}
}

@media (max-width: 640px) {
	/*
	 * Type only. The layout above holds from 768 down; below 640 the card is
	 * ~161px and the two reserved title lines hold about half the characters
	 * they do at 768, so the scale steps once. The `min-height` reservations
	 * are in `em`, so they follow these sizes rather than needing their own
	 * twin.
	 */
	.ml-card .ml-card__title {
		font-size: 15px;
	}

	.ml-card .ml-card__price {
		font-size: 18px;
	}
}

/* <= 768px: the grid/list toggle measured 32 x 30 - a standalone control, and
   the only one on the toolbar that was still under the theme's 44px figure.
   Width and height both, since it is a square icon button. */
@media (max-width: 768px) {
	.ml-shop__tools .ml-shop__view-btn {
		width: 44px;
		height: 44px;
	}
}

/*
 * <= 768px: the toolbar wraps.
 *
 * MEASURED CAUSE, not a guess. Raising the view toggles from 32 to 44px added
 * 24px to a row that was ALREADY 3px over its container — `.ml-shop__toolbar`
 * was 330 inside 327 before this pass and the container absorbed it. At 354
 * it did not, and the 27px propagated all the way up to a 378px document on a
 * 375px screen: the only horizontal overflow this audit ever produced, and it
 * was on the category archive only, because that is where the row is longest.
 *
 * Proven by isolation in the browser: forcing the grid back to one column
 * left `scrollWidth` at 378, and forcing the buttons back to 32x30 took it to
 * 375. The grid was never involved.
 *
 * Wrapping is the fix rather than shrinking the buttons back, because the row
 * holds a 240px sort control plus two 44px toggles and cannot fit 327px on one
 * line at any honest size.
 */
@media (max-width: 768px) {
	/*
	 * .ml-shop__toolbar ALREADY carries flex-wrap: wrap (line 480), and the
	 * first attempt at this fix targeted it - a no-op that re-measured at
	 * exactly the same 354. The row that does NOT wrap is .ml-shop__tools,
	 * which holds a 240px sort control beside a 100px view group inside a
	 * 327px column.
	 */
	.ml-shop__toolbar .ml-shop__tools {
		flex-wrap: wrap;
		row-gap: 12px;
		justify-content: space-between;
		width: 100%;
	}
}

/* =========================================================================
 * <= 768px: THE CATEGORY ARCHIVE'S CARDS WERE NARROWER THAN THE SHOP'S
 * (2026-09-22)
 *
 * Reported as "category cards 157.6px against the shop's 165.5px at 375".
 * NOTHING IN THIS THEME CAUSED IT, and nothing in this theme could see it: the
 * two surfaces render the SAME template with the SAME 16px gutters, and
 * `.ml-shop__body` measured 359.3px on one and 375px on the other.
 *
 * MEASURED UP THE ANCESTOR CHAIN, not guessed. The whole difference is one box
 * the template does not own:
 *
 *   .ast-container   shop 0px/0px      category 7.87968px/7.87968px
 *
 * and the two pages carry different Astra LAYOUT classes, which is why one is
 * padded and the other is not:
 *
 *   shop      body.ast-page-builder-template .ast-no-sidebar
 *   category  body.ast-separate-container .ast-two-container .ast-left-sidebar
 *
 * The shop page has Astra page meta (`site-content-layout`, recorded in
 * CLAUDE.md); a TERM has no postmeta, so a `product_cat` archive falls through
 * to Astra's global default and gets the separate-container treatment.
 *
 * THE WINNING RULE WAS RESOLVED FROM THE SERVED CSS, because it is Astra's and
 * a grep of this theme can never find it:
 *
 *   @media (max-width: 544px)
 *   .ast-separate-container #content .ast-container { padding: 0.54em }   (1,2,0)
 *   .ast-container, .ast-container-fluid { padding: 20px }                (0,1,0)
 *
 * 0.54em resolves to 7.87968px here. Above 544 the base 20px takes over, which
 * is why the gap is 7.9px at 360-544 and 20px from 545 up.
 *
 * (1,3,0) IS THE FLOOR, AND THE DOUBLED CLASS IS WHY IT IS NOT (1,2,0).
 * `#content .ast-container:has(> .ml-shop)` measures (1,2,0) - a TIE with
 * Astra's rule, won only because this stylesheet loads later, which this
 * project's notes say not to lean on. Doubling `.ast-container` clears it
 * outright without naming `ast-separate-container`, so the fix does not break
 * the day Astra's layout class for a term archive changes.
 *
 * `:has(> .ml-shop)` KEEPS THE BLAST RADIUS AT THE SHOP WRAPPER. `.ast-container`
 * wraps every Astra-rendered page; only the shop archive and the category
 * archives put `.ml-shop` directly inside it.
 *
 * ⚑ THE SAME CAUSE RUNS ON ABOVE 768 AND IS DELIBERATELY NOT TOUCHED, because
 * the brief was phone-only and desktop had to stay byte-identical: the category
 * card is still 20px narrower than the shop's at 769-1023 and 2.6px at 1440.
 * Fixing that is the same one-line rule without the media query - a desktop
 * change, and therefore the owner's call.
 * ====================================================================== */
@media (max-width: 768px) {
	#content .ast-container.ast-container:has(> .ml-shop) {
		padding-left: 0;
		padding-right: 0;
	}
}
