/**
 * Single product
 *
 * Gallery on a pale stage left, purchase column right, then WooCommerce's own
 * tabs and related products - all restyled rather than replaced.
 * effects.css supplies .ml-glass and the reveal behaviour.
 */

.single-product .ml-product {
	max-width: var(--ml-container);
	margin: 0 auto;
	padding: 20px 24px 56px;
}

/* ---------- Breadcrumb ---------- */

.ml-product__crumbs {
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
	font-size: 13.5px;
	color: var(--ml-body);
	margin-bottom: 18px;
}

.ml-product__crumbs a { color: var(--ml-cyan); text-decoration: none; }
.ml-product__crumbs a:hover,
.ml-product__crumbs a:focus-visible { text-decoration: underline; }

/* ---------- Layout ---------- */

.ml-product__top {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
	gap: 34px;
	align-items: start;
}

/* ---------- Gallery ---------- */

/*
 * TASK 1: the gallery column sticks while the summary scrolls past it.
 *
 * CSS only - no scroll listener. Three things make it work, and all three are
 * load-bearing:
 *
 *   1. `.ml-product__top` already carries `align-items: start`. Under the grid
 *      default of `stretch` the item would fill the row and have nowhere to
 *      travel, so sticky would silently do nothing.
 *   2. No ancestor may clip. Checked: nothing on the chain sets `overflow`
 *      other than `.ml-product__buy .quantity`, which is not an ancestor.
 *      Adding `overflow: hidden` anywhere above this kills it with no error.
 *   3. It releases on its own when the summary ends, because a sticky element
 *      cannot leave its containing block - the grid area. No JS needed to stop
 *      it.
 *
 * 24px of headroom, not 0: the site has no sticky header to clear, so this is
 * breathing room rather than an offset.
 */
.ml-product__gallery {
	position: sticky;
	top: 24px;
	/* Explicit, so the sticky does not depend on the grid keeping `start`. */
	align-self: start;
}

/*
 * The stage no longer paints anything - the pale frame, its border, its radius
 * and its 28px padding were removed 2026-08-30. What is left is a centring box:
 * it holds the image on a common baseline with the summary column and gives the
 * gallery a floor height so the row does not jump between products.
 *
 * `overflow: hidden` went with the radius. Its only job was clipping content to
 * those rounded corners; with no corners left it would just crop the hover
 * scale against an invisible edge.
 *
 * `position: relative` STAYS, and is not dead weight - .ml-product__thumbs and
 * the reveal both rely on the stage being a normal block, and keeping it a
 * containing block means a future overlay has somewhere to anchor.
 */
.ml-product__stage {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: center;
}

/*
 * The figure fills the gallery column and the image fills the figure.
 *
 * It used to be `width: fit-content`, shrink-wrapping a capped image so the
 * badges could sit on the artwork rather than out in the column. The cap is
 * gone as of 2026-09-03 - the column width drives the size now - so the figure
 * and the image coincide and the wrap has nothing left to do.
 *
 * `position: relative` is the load-bearing line here. The flags and the zoom
 * button are absolutely positioned inside this figure, so it has to stay their
 * containing block; drop it and their 16px inset is measured from the stage
 * instead, which is a different box the moment anything changes.
 */
/*
 * THE WHITE BOX AROUND THE IMAGE WAS THIS, and it is not the stage.
 *
 * `wc_product_class()` puts `ast-article-single` on the product wrapper, and
 * Astra ships this in its inline Customizer CSS:
 *
 *     .ast-article-single figure,
 *     .ast-article-single img:not(figure img) { box-shadow: 0 0 30px 0 rgba(0,0,0,.15) }
 *
 * (0,1,1). It paints a 30px halo on all four sides of this figure - which read
 * as a soft card once the figure went full-width - and the stage had nothing on
 * it to strip: its background, border, radius and shadow all went on
 * 2026-08-30. Looking there again would find nothing again.
 *
 * The second half of that selector also catches the gallery thumbnails, since
 * they sit in a <button> rather than a <figure> and so are not excluded by
 * `:not(figure img)`. They carry their own border and radius, so the halo was
 * doubling up there too; both are cancelled below at (0,2,0).
 *
 * The image's own `filter: drop-shadow()` was removed 2026-09-15 - on the
 * opaque packshots imported that day it traced a box too. See below.
 */
.ml-product .ml-product__main,
.ml-product .ml-product__thumb img {
	box-shadow: none;
}

.ml-product__main {
	position: relative;
	margin: 0;
	line-height: 0;
	width: 100%;
	display: flex;
	align-items: center;
	justify-content: center;
}

/*
 * No cap and no hover, both removed 2026-09-03.
 *
 * `width: 100%` with `height: auto` lets the column decide the size and the
 * image keep its own aspect ratio. The 380px `max-height` that used to bound
 * it is gone, and with it the reason .ml-product__main needed `fit-content`.
 *
 * The scale-on-hover went too - image, transform and transition alike.
 *
 * THE DROP-SHADOW WENT ON 2026-09-15, and multiply came in. The shadow followed
 * the alpha of the old cut-out PNGs; the packshots imported that day are OPAQUE
 * squares with a white ground, so the same filter traced the square and put a
 * grey band under every image - the white box again, from the other side. 23 of
 * the 30 products now have those packshots; the seven with cut-outs lose a soft
 * shadow they barely showed on a white page.
 *
 * Multiply is inert on today's white page (white x white) and is here so the
 * product gallery follows the same rule as every card: if the stage ever gets a
 * colour, a white-ground packshot takes it instead of showing its square. The
 * ground it would blend with has to be painted inside .ml-product__gallery,
 * which is its own group (the reveal's `will-change`).
 */
.ml-product__main img {
	width: 100%;
	height: auto;
	display: block;
	mix-blend-mode: multiply;
}

/*
 * Normal flow in the summary column, not an overlay on the artwork.
 *
 * They sat `position: absolute` inside .ml-product__main until 2026-09-03, which
 * is why the figure had to be their containing block. That constraint is gone -
 * .ml-product__main keeps `position: relative` only for the zoom button now.
 *
 * It is now a flex ITEM of .ml-product__buy, sharing that row with the cart
 * form and the wishlist button and sitting to their right. `flex: 0 1 auto`
 * keeps it at its natural width so the wishlist button is not squeezed; the
 * row wraps to a second line on its own, because .ml-product__buy already
 * carries `flex-wrap: wrap`.
 *
 * `align-items: center` matters: .ml-product__buy is `align-items: stretch`, so
 * without it this list is stretched to the 50px control height and the pills
 * hang from its top edge instead of centring against the buttons.
 */
.ml-product__flags {
	flex: 0 1 auto;
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 8px;
}

.ml-product__flag {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	padding: 7px 13px;
	border-radius: 999px;
	background: var(--ml-white);
	border: 1px solid var(--ml-line);
	color: var(--ml-navy);
	font-size: 12.5px;
	font-weight: 600;
	line-height: 1.2;
	box-shadow: 0 6px 14px -10px rgba(10, 37, 64, 0.6);
}

.ml-product__flag .ml-icon { color: var(--ml-cyan); }
.ml-product__flag.is-stock .ml-icon { color: #23a455; }

.ml-product__zoom {
	position: absolute;
	top: 16px;
	right: 16px;
	z-index: 2;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 38px;
	height: 38px;
	border-radius: 50%;
	background: var(--ml-white);
	border: 1px solid var(--ml-line);
	color: var(--ml-navy);
	box-shadow: 0 6px 14px -10px rgba(10, 37, 64, 0.6);
	transition: transform var(--ml-t-base) var(--ml-e-standard), color var(--ml-t-base) var(--ml-e-standard), border-color var(--ml-t-base) var(--ml-e-standard);
}

.ml-product__zoom:hover,
.ml-product__zoom:focus-visible {
	transform: scale(1.08);
	color: var(--ml-cyan);
	border-color: var(--ml-cyan);
}

.ml-product__thumbs {
	list-style: none;
	margin: 14px 0 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}

.ml-product__thumb {
	display: block;
	width: 84px;
	padding: 6px;
	border: 2px solid var(--ml-line);
	border-radius: 10px;
	background: var(--ml-white);
	cursor: pointer;
	line-height: 0;
	transition: transform var(--ml-t-base) var(--ml-e-standard), border-color var(--ml-t-base) var(--ml-e-standard), box-shadow var(--ml-t-base) var(--ml-e-standard);
}

/* Multiply for parity with every product image (2026-09-15). Inert while the
   thumb stays white in all states (`:focus` pins it white below); if it ever
   gets a tint, white-ground packshots take it. */
.ml-product__thumb img {
	width: 100%;
	height: 58px;
	object-fit: contain;
	display: block;
	mix-blend-mode: multiply;
}

/*
 * `:focus` pinned back to rest. Astra's `button:focus` (0,1,1) beat the (0,1,0)
 * rest rule, so clicking a gallery thumbnail painted a cyan panel behind the
 * image and left it there until focus moved.
 *
 * Above `:hover` and `.is-active`, both (0,2,0), so the later rules still win
 * in their own states — the selected thumbnail keeps its cyan border after a
 * click. `.is-active` only sets `border-color`, which is exactly why this pin
 * has to restate `border-color` rather than a `border` shorthand: a shorthand
 * here would reset the active border on every focused thumbnail.
 */
.ml-product__thumb:focus {
	background: var(--ml-white);
	border-color: var(--ml-line);
}

/*
 * `background` pinned on hover too (2026-09-15). Astra's `button:hover` (0,1,1)
 * paints --ast-global-color-1 (#0098d1) on every button, and the rest rule is
 * only (0,1,0) - the `:focus` pin above never covered hover. It showed as a
 * 6px cyan ring round an opaque packshot; with `mix-blend-mode: multiply` on the
 * thumbnail image it would have tinted the whole white-ground pack cyan. (0,2,0)
 * wins; `.is-active` and `:focus` keep their own rules.
 */
.ml-product__thumb:hover {
	transform: translateY(-2px);
	background: var(--ml-white);
	border-color: #b7dff5;
	box-shadow: 0 10px 18px -12px rgba(10, 37, 64, 0.6);
}

.ml-product__thumb.is-active {
	border-color: var(--ml-cyan);
	box-shadow: 0 8px 16px -10px rgba(0, 175, 240, 0.8);
}

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

/* ---------- Summary ---------- */

.ml-product__summary {
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.ml-product__cat {
	align-self: flex-start;
	font-size: 13.5px;
	font-weight: 600;
	color: var(--ml-cyan);
	text-decoration: none;
}

.ml-product__cat:hover,
.ml-product__cat:focus-visible { text-decoration: underline; }

.ml-product__title {
	margin: 0;
	font-size: clamp(26px, 3.4vw, 38px);
	font-weight: 700;
	letter-spacing: -0.025em;
	line-height: 1.15;
	color: var(--ml-navy);
}

.ml-product__rating {
	display: flex;
	align-items: center;
	gap: 10px;
	font-size: 13.5px;
	color: var(--ml-body);
}

.ml-product__rating-value { font-weight: 700; color: var(--ml-navy); }
.ml-product__rating-count { color: var(--ml-cyan); text-decoration: none; }
.ml-product__rating-count:hover { text-decoration: underline; }

.ml-product__price {
	font-size: clamp(22px, 2.6vw, 30px);
	font-weight: 700;
	color: var(--ml-navy);
	line-height: 1.2;
}

.ml-product__price del {
	font-size: 0.62em;
	font-weight: 500;
	color: var(--ml-muted);
	margin-right: 8px;
}

.ml-product__price ins { text-decoration: none; }

/*
 * Pack size, directly under the price. A quiet line, not a second price - it
 * is a fact about the box. Rendered only when _medlife_pack_size is set.
 */
.ml-product__pack {
	display: flex;
	align-items: baseline;
	gap: 8px;
	margin: -4px 0 0;
	font-size: 14px;
}

.ml-product__pack-label {
	color: var(--ml-muted);
}

.ml-product__pack-value {
	padding: 4px 12px;
	border-radius: 999px;
	background: var(--ml-stage);
	font-weight: 600;
	color: var(--ml-navy);
}

.ml-product__excerpt {
	font-size: 14.5px;
	line-height: 1.7;
	color: var(--ml-body);
	max-width: 58ch;
}

.ml-product__excerpt p { margin: 0 0 8px; }
.ml-product__excerpt p:last-child { margin-bottom: 0; }

/* ---------- Health goal chips ---------- */

.ml-product__chips {
	list-style: none;
	margin: 2px 0 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 9px;
}

.ml-product__chip {
	display: inline-flex;
	align-items: center;
	gap: 7px;
	padding: 8px 14px;
	border: 1px solid var(--ml-line);
	border-radius: 10px;
	background: var(--ml-white);
	font-size: 13px;
	font-weight: 500;
	color: var(--ml-navy);
	transition: transform var(--ml-t-base) var(--ml-e-standard), border-color var(--ml-t-base) var(--ml-e-standard), box-shadow var(--ml-t-base) var(--ml-e-standard);
}

.ml-product__chip:hover {
	transform: translateY(-2px);
	border-color: var(--ml-cyan);
	box-shadow: 0 10px 18px -14px rgba(10, 37, 64, 0.7);
}

.ml-product__chip .ml-icon { color: var(--ml-cyan); }

/* ---------- Buy row ---------- */

.ml-product__buy {
	display: flex;
	align-items: stretch;
	flex-wrap: wrap;
	gap: 12px;
	margin-top: 6px;
}

.ml-product__buy form.cart {
	display: flex;
	align-items: stretch;
	gap: 12px;
	margin: 0;
	flex: 1 1 auto;
	min-width: 0;
}

.ml-product__buy .quantity {
	display: flex;
	align-items: center;
	border: 1px solid var(--ml-line);
	border-radius: 8px;
	overflow: hidden;
	background: var(--ml-white);
}

.ml-product__buy .quantity input.qty {
	width: 62px;
	height: 50px;
	/*
	 * Astra's `input[type=number]` rule supplies 12px 16px otherwise, leaving a
	 * 30px content box inside a 62px field - fine for one digit, clipping at
	 * three. The cart's equivalent has always set 0; the two now match.
	 */
	padding: 0;
	border: 0;
	text-align: center;
	font-family: var(--ml-font);
	font-size: 15px;
	font-weight: 600;
	color: var(--ml-navy);
	background: transparent;
}

/*
 * Position 1 - the wide, filled primary. Buy It Now sits here.
 */
/*
 * `.button` IS PART OF THE SELECTOR ON PURPOSE — it takes this to (0,3,0).
 *
 * The markup is `<button class="button ml-product__buynow">`, and on a product
 * page `<body>` carries `woocommerce`, so Astra's inline
 * `.woocommerce button.button` at **(0,2,1)** reaches it. At (0,2,0) this block
 * lost every property that rule names, and the button rendered as ASTRA's
 * outline button at rest:
 *
 *     background-color: transparent      instead of the cyan fill
 *     color: var(--ast-global-color-0)   #00AFF0, so CYAN text on white
 *     border: 1px solid #00AFF0          instead of border: 0
 *     border-*-radius: 30px              instead of 8px
 *     padding: 10px 20px                 instead of 0 28px
 *     font-size: 16px                    instead of 15px
 *
 * Keying the lock on `.button` means it applies exactly when Astra's rule
 * applies: if that class is ever dropped from the PHP, both rules stop matching
 * together and this cannot silently half-apply.
 *
 * `height: 50px` and `min-width: 190px` were never contested — Astra sets
 * neither — which is why the button stayed the right SIZE while looking wrong.
 */
.ml-product__buy .ml-product__buynow.button {
	flex: 1 1 auto;
	min-width: 190px;
	height: 50px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 9px;
	padding: 0 28px;
	border: 0;
	border-radius: 8px;
	font-family: var(--ml-font);
	font-size: 15px;
	line-height: 1;
	font-weight: 600;
	cursor: pointer;
	box-shadow: 0 14px 26px -16px 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);
}

/*
 * STATE LOCK on the fill, for the same reason the accordion headers needed one.
 *
 * Astra ships `button:focus, button:hover, ... { color: #ffffff;
 * background-color: var(--ast-global-color-1) }` at (0,1,1) in its inline
 * Customizer CSS, and it has to be answered in EVERY state - a clicked button
 * keeps :focus, so naming the fill only at rest leaves it repainted afterwards.
 *
 * BUT (0,1,1) IS NOT THE TIER THAT MATTERS HERE. On a WooCommerce button Astra
 * also ships `.woocommerce button.button:hover` at **(0,3,1)**, which beat the
 * (0,3,0) these selectors used to carry — so the hover was ASTRA's too, not
 * this rule's. It looked correct only because `--ast-global-color-1` is
 * `#0098d1`, byte-identical to `--ml-cyan-dark` since the palette change. A
 * coincidence of two tokens, not a working lock.
 *
 * Adding `.button` makes the rest selector (0,3,0) and the four state selectors
 * (0,4,0), which clear (0,2,1) and (0,3,1) respectively.
 *
 * Flat brand cyan, not the old 135deg gradient to #0790cf.
 */
.ml-product__buy .ml-product__buynow.button,
.ml-product__buy .ml-product__buynow.button:hover,
.ml-product__buy .ml-product__buynow.button:focus,
.ml-product__buy .ml-product__buynow.button:focus-visible,
.ml-product__buy .ml-product__buynow.button:active {
	background: var(--ml-cyan);
	color: var(--ml-white);
	border: 0;
}

/*
 * Hover deepens within the same hue; the label stays white. Geometry is NOT
 * touched here - every size property lives in the (0,3,0) rest rule above,
 * which keeps matching while the button is hovered, so there is no size jump.
 */
.ml-product__buy .ml-product__buynow.button:hover,
.ml-product__buy .ml-product__buynow.button:focus-visible {
	background: var(--ml-cyan-dark);
}

/*
 * ADDED 2026-09-06 — there was nothing here to preserve.
 *
 * Astra's inline `button:focus, button:active { outline: 0 }` is (0,1,1) and
 * this button carried no author outline of its own, so the browser's default
 * focus ring was being suppressed and the control had NO visible focus
 * indicator at all. That is 2.4.7, and it was pre-existing rather than anything
 * the specificity work introduced.
 *
 * (0,4,0), so it clears Astra's `outline: 0` and the (0,3,1) WooCommerce tier
 * alike. Same treatment the loop card's cart button already carries: it paints,
 * it does not move, and it adds no transition.
 */
.ml-product__buy .ml-product__buynow.button:focus-visible {
	outline: 2px solid var(--ml-navy);
	outline-offset: 2px;
}

.ml-product__buy .ml-product__buynow:hover,
.ml-product__buy .ml-product__buynow:focus-visible {
	transform: translateY(-2px);
	box-shadow: 0 20px 32px -16px rgba(var(--ml-shadow-tint), 1);
}

/*
 * Position 2 - the narrow, outlined secondary. Add to cart sits here.
 *
 * Same height, same radius, same type as the primary; outlined rather than
 * filled. `.single_add_to_cart_button` also carries WooCommerce's `button alt`
 * classes, which Astra styles with its own fill - the `background` shorthand
 * below resets that, so keep it a shorthand rather than `background-color`.
 *
 * (0,6,1), AND IT HAS TO BE - corrected 2026-09-14. At (0,2,0) this rule never
 * rendered: Astra's `.woocommerce button.button` (0,2,1) owned the fill, the 30px
 * radius, the 16px/500 type and the colour, and WooCommerce's
 * `.woocommerce-js div.product form.cart .button.single_add_to_cart_button`
 * owned the padding - a transparent pill beside the filled Buy It Now.
 *
 * THAT WOO RULE IS (0,5,2), NOT (0,4,2): `div.product` is a class too. A first
 * pass at (0,5,1) won the fill, radius and type and still lost the padding.
 * `.ml-product.product` is the same <div> and makes this (0,6,1).
 * Every state, responsive and reduced-motion selector below moved with it.
 */
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button {
	flex: 0 1 auto;
	min-width: 150px;
	height: 50px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 9px;
	padding: 0 26px;
	border: 2px solid var(--ml-cyan);
	border-radius: 8px;
	background: var(--ml-white);
	color: var(--ml-cyan);
	font-family: var(--ml-font);
	font-size: 15px;
	font-weight: 600;
	line-height: 1;
	white-space: nowrap;
	cursor: pointer;
	box-shadow: none;
	transition: background-color var(--ml-t-base) var(--ml-e-standard), color var(--ml-t-base) var(--ml-e-standard), transform var(--ml-t-base) var(--ml-e-standard);
}

/*
 * STATE LOCK, matching the one on .ml-product__buynow above.
 *
 * Astra`s inline `button:focus, button:hover { color: #ffffff;
 * background-color: var(--ast-global-color-1) }` is (0,1,1) and repaints any
 * bare button it can reach. The base rule here is (0,6,1) and wins at rest, but
 * naming the fill only at rest is what left the accordion headers looking
 * filled after a click - :focus persists. Every state is spelled out.
 *
 * The fill stays WHITE in all four; this is the outline button of the pair.
 */
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button,
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:hover,
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:focus,
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:focus-visible,
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:active {
	background: var(--ml-white);
}

/* Outline and label, brand cyan at rest. */
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button,
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:focus,
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:active {
	border-color: var(--ml-cyan);
	color: var(--ml-cyan);
}

/*
 * Hover DEEPENS the outline and the label rather than tinting the fill, which
 * is what the filled sibling does too - the pair now move the same way. This
 * replaced a `background: #f1fbff` wash that lightened instead.
 */
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:hover,
.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:focus-visible {
	transform: translateY(-2px);
	border-color: var(--ml-cyan-dark);
	color: var(--ml-cyan-dark);
}

.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:focus-visible {
	outline: 2px solid var(--ml-navy);
	outline-offset: 2px;
}

/* Wishlist button, restyled from the plugin's own markup. */
.ml-product__wishlist .tinvwl_add_to_wishlist_button {
	display: inline-flex !important;
	align-items: center;
	justify-content: center;
	gap: 9px;
	height: 50px;
	margin: 0;
	padding: 0 22px;
	border: 1px solid var(--ml-cyan);
	border-radius: 8px;
	background: var(--ml-white);
	color: var(--ml-cyan);
	font-family: var(--ml-font);
	font-size: 14.5px;
	font-weight: 600;
	transition: background-color var(--ml-t-base) var(--ml-e-standard), transform var(--ml-t-base) var(--ml-e-standard);
}

.ml-product__wishlist .tinvwl_add_to_wishlist_button:hover {
	background: #f1fbff;
	transform: translateY(-2px);
}

/* ---------- Meta + assurance ---------- */

.ml-product__meta {
	margin: 4px 0 0;
	font-size: 13.5px;
	color: var(--ml-body);
}

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

.ml-product__assurance {
	list-style: none;
	margin: 8px 0 0;
	padding: 16px 0 0;
	border-top: 1px solid var(--ml-line);
	display: flex;
	flex-wrap: wrap;
	gap: 10px 22px;
}

.ml-product__assurance li {
	display: flex;
	align-items: center;
	gap: 9px;
	font-size: 13px;
	line-height: 1.4;
	color: var(--ml-body);
	min-width: 0;
}

.ml-product__assurance .medlife-trust__icon {
	flex: 0 0 auto;
	color: var(--ml-cyan);
	width: 22px;
	height: 22px;
}

.ml-product__assurance .woocommerce-Price-amount { font-weight: 700; color: var(--ml-navy); }

/* -------------------------------------------------------------------------
 * Tabs - WooCommerce markup, restyled
 * ---------------------------------------------------------------------- */

.ml-product__tabs { margin-top: 44px; }

.ml-product__tabs .woocommerce-tabs ul.tabs {
	list-style: none;
	margin: 0 0 22px;
	padding: 0 0 0 2px;
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
	border-bottom: 1px solid var(--ml-line);
}

.ml-product__tabs .woocommerce-tabs ul.tabs::before,
.ml-product__tabs .woocommerce-tabs ul.tabs li::before,
.ml-product__tabs .woocommerce-tabs ul.tabs li::after {
	content: none !important;
}

.ml-product__tabs .woocommerce-tabs ul.tabs li {
	margin: 0;
	padding: 0;
	border: 0;
	background: none;
	border-radius: 0;
}

.ml-product__tabs .woocommerce-tabs ul.tabs li a {
	display: block;
	padding: 12px 18px;
	font-family: var(--ml-font);
	font-size: 14.5px;
	font-weight: 600;
	color: var(--ml-body);
	text-decoration: none;
	border-bottom: 3px solid transparent;
	transition: color var(--ml-t-base) var(--ml-e-standard), border-color var(--ml-t-base) var(--ml-e-standard);
}

.ml-product__tabs .woocommerce-tabs ul.tabs li a:hover { color: var(--ml-navy); }

.ml-product__tabs .woocommerce-tabs ul.tabs li.active a {
	color: var(--ml-cyan);
	border-bottom-color: var(--ml-cyan);
}

/*
 * No `padding` here - it lives in the high-specificity rule below. This
 * selector is (0,2,0) and cannot hold padding against Astra; see the note
 * there before moving it back.
 */
.ml-product__tabs .woocommerce-Tabs-panel {
	background: var(--ml-white);
	border: 1px solid var(--ml-line);
	border-radius: 14px;
	font-size: 14.5px;
	line-height: 1.8;
	color: var(--ml-body);
	box-shadow: 0 12px 30px -26px rgba(10, 37, 64, 0.7);
}

.ml-product__tabs .woocommerce-Tabs-panel h2 {
	margin: 0 0 12px;
	font-size: clamp(18px, 2vw, 23px);
	font-weight: 700;
	letter-spacing: -0.015em;
	color: var(--ml-navy);
}

/*
 * Panel padding, and it needs all five classes to survive.
 *
 * Astra's WooCommerce compatibility sheet carries:
 *
 *     .woocommerce-js div.product .woocommerce-tabs .panel {
 *         margin: 0 0 1.2em; padding: 0
 *     }
 *
 * That is (0,4,1). The rule above is (0,2,0), so its padding was discarded
 * outright and every tab's content sat flush against the border. This selector
 * is (0,5,0) - five classes beats four-plus-a-type - so it wins on specificity
 * alone and does not depend on which stylesheet loads last.
 *
 * WATCH OUT when checking this in the page source: `body` ships as
 * `woocommerce-no-js`, and WooCommerce swaps it to `woocommerce-js` from an
 * inline script AFTER load. Read the raw HTML and Astra's rule looks like it
 * cannot match; read the live DOM and it does. Only the browser tells the
 * truth here.
 *
 * All three tabs share the class - description, additional_information and
 * reviews are the same `woocommerce-Tabs-panel ... panel entry-content wc-tab`
 * element - so this one rule covers them.
 *
 * 24/20/16 is the theme's horizontal scale, matching .ml-blog__body and the
 * trending section rather than introducing a fourth value.
 */
.ml-product.product .ml-product__tabs .woocommerce-tabs .woocommerce-Tabs-panel {
	padding: 26px 24px;
}

.ml-product__tabs .woocommerce-Tabs-panel h3 { color: var(--ml-navy); }
.ml-product__tabs .woocommerce-Tabs-panel a { color: var(--ml-cyan); }
.ml-product__tabs .woocommerce-Tabs-panel img { max-width: 100%; height: auto; border-radius: 10px; }

/* -------------------------------------------------------------------------
 * Review form
 *
 * WooCommerce's own markup, which the theme never styled - so it fell through
 * to Astra's `input[type="…"]` rules at (0,1,1): 40px tall, square corners,
 * Astra's grey border, 16px text. It is reachable from every product page, and
 * with zero reviews store-wide it is the first thing under the Reviews tab.
 *
 * Same treatment as the checkout fields: 48px, 14px padding, 9px radius,
 * --ml-line border, 15px type, cyan focus ring.
 * ---------------------------------------------------------------------- */

.ml-product .ml-product__tabs #respond { max-width: 620px; }

.ml-product .ml-product__tabs #respond .comment-reply-title {
	display: block;
	margin-bottom: 6px;
	font-size: 17px;
	font-weight: 700;
	color: var(--ml-navy);
}

.ml-product .ml-product__tabs #respond .comment-notes {
	margin: 0 0 18px;
	font-size: 13.5px;
	color: var(--ml-muted);
}

.ml-product .ml-product__tabs #respond p { margin: 0 0 16px; }

.ml-product .ml-product__tabs #respond label {
	display: block;
	margin-bottom: 6px;
	font-size: 13.5px;
	font-weight: 600;
	color: var(--ml-navy);
}

.ml-product .ml-product__tabs #respond .required {
	color: #d33a3a;
	text-decoration: none;
}

/*
 * Do NOT anchor the textarea on #comment. Antispam Bee ships a decoy and the
 * real field, then swaps their ids on load - before the script runs #comment
 * is the hidden decoy, after it runs #comment is the real one. Styling every
 * textarea in #respond is stable either way, and the decoy stays hidden
 * because it carries its own inline !important clip/height/width.
 */
.ml-product .ml-product__tabs #respond input[type="text"],
.ml-product .ml-product__tabs #respond input[type="email"],
.ml-product .ml-product__tabs #respond select,
.ml-product .ml-product__tabs #respond textarea {
	width: 100%;
	height: 48px;
	padding: 0 14px;
	border: 1px solid var(--ml-line);
	border-radius: 9px;
	background: var(--ml-white);
	box-shadow: none;
	font-family: var(--ml-font);
	font-size: 15px;
	color: var(--ml-navy);
	transition: border-color var(--ml-t-base) var(--ml-e-standard), box-shadow var(--ml-t-base) var(--ml-e-standard);
}

.ml-product .ml-product__tabs #respond textarea {
	height: auto;
	min-height: 132px;
	padding: 12px 14px;
	line-height: 1.55;
	resize: vertical;
}

.ml-product .ml-product__tabs #respond input[type="text"]:focus,
.ml-product .ml-product__tabs #respond input[type="email"]:focus,
.ml-product .ml-product__tabs #respond select:focus,
.ml-product .ml-product__tabs #respond textarea:focus {
	outline: none;
	border-color: var(--ml-cyan);
	box-shadow: 0 0 0 3px rgba(0, 175, 240, 0.16);
}

/* Checkbox and its label share a line rather than stacking like the fields. */
.ml-product .ml-product__tabs #respond .comment-form-cookies-consent {
	display: flex;
	align-items: flex-start;
	gap: 9px;
}

.ml-product .ml-product__tabs #respond .comment-form-cookies-consent label {
	margin: 0;
	font-size: 13.5px;
	font-weight: 500;
	color: var(--ml-muted);
}

.ml-product .ml-product__tabs #respond .form-submit input[type="submit"] {
	width: auto;
	min-width: 180px;
	height: 48px;
	padding: 0 26px;
	border: 0;
	border-radius: 9px;
	background: var(--ml-cyan);
	color: var(--ml-white);
	font-family: var(--ml-font);
	font-size: 15px;
	font-weight: 600;
	cursor: pointer;
	transition: background-color var(--ml-t-base) var(--ml-e-standard);
}

.ml-product .ml-product__tabs #respond .form-submit input[type="submit"]:hover,
.ml-product .ml-product__tabs #respond .form-submit input[type="submit"]:focus-visible {
	background: var(--ml-cyan-dark);
	color: var(--ml-white);
}

/* -------------------------------------------------------------------------
 * Related / upsells - reuse the shop card look
 * ---------------------------------------------------------------------- */

/*
 * CAPPED AT 1064px AND CENTRED, 2026-09-07 - and 1064 is a DERIVED number, not
 * a taste one. Do not round it to 1060 or 1100.
 *
 * It is the width of the shop grid at 1440: three columns of 341.33px plus the
 * two 20px gaps. The shop main column gets 1064px because the 268px sidebar and
 * its 28px gap come out of the 1360px container; this strip is full-bleed inside
 * `.ml-product` and had the whole 1312px, so the SAME three columns were coming
 * out at 424px here against 341px there.
 *
 *     1312 / 3 cols -> 424px cards, square packshot fills 57% of its image box
 *     1064 / 3 cols -> 341px cards, the same 73% the shop and category get
 *
 * That was the last surface where the 170 -> 220 image pass had not landed: the
 * height moved, the box under it kept growing, and the blank space came back.
 * Capping the container is what fixes it, rather than giving this one surface a
 * taller image than the other two - which would have traded a width difference
 * for a height difference.
 *
 * THE CAP GOES ON THE WRAPPER, NOT ON `ul.products`. The heading is a sibling of
 * the grid inside `section.related`, so capping the list alone would centre the
 * cards at x=188 while "Related products" stayed at x=64 - a heading visibly out
 * of line with the row it labels. On the wrapper both move together and the
 * strip reads as one centred block. The wrapper carries no background, border or
 * padding, so there is nothing to leave stranded at full width.
 *
 * `margin-inline`, NOT `margin: 0 auto` - the inline axis only, so the 46px
 * `margin-top` above survives. A shorthand here would silently drop it and pull
 * the strip up against the tabs.
 *
 * BELOW 1064px THE CAP IS INERT BY CONSTRUCTION, which is why it needs no media
 * query: the container is already narrower than the cap, so `max-width` binds
 * nothing and `auto` inline margins resolve to 0 with no free space to absorb.
 * Verified unchanged at 360 / 768 / 1024.
 */
.ml-product__related {
	margin-top: 46px;
	max-width: 1064px;
	margin-inline: auto;
}

.ml-product__related section { margin-bottom: 34px; }

/*
 * THE CHILD COMBINATOR IS LOAD-BEARING - this is the SECTION heading only.
 *
 * It was `.ml-product__related h2`, a descendant selector, which also matched
 * every PRODUCT CARD TITLE in the strip, since `.ml-card__title` is an `<h2>`.
 * At (0,1,1) it beat the card's own `.ml-card__title` (0,1,0), so on
 * 2026-09-07 - the moment the card redesign made the title rule the single
 * source of its own size - related-product titles started rendering at
 * `clamp(20px, 2.3vw, 28px)`: 20px at 360, 28px at 1440, against the 22px the
 * shop and category cards use. Nothing had changed in the card; a
 * higher-specificity rule that was always over-broad simply became visible.
 *
 * Scoping to the direct child fixes it at the source rather than raising the
 * card's specificity to out-shout it.
 */
.ml-product__related section.related > h2 {
	margin: 0 0 22px;
	font-size: clamp(20px, 2.3vw, 28px);
	font-weight: 700;
	letter-spacing: -0.02em;
	color: var(--ml-navy);
	position: relative;
	padding-bottom: 10px;
}

.ml-product__related section.related > h2::after {
	content: "";
	position: absolute;
	left: 0;
	bottom: 0;
	width: 46px;
	height: 3px;
	border-radius: 2px;
	background: var(--ml-cyan);
}

/*
 * (0,4,2) for the same reason shop.css carries (0,4,1) - see the long note
 * there. Astra's `.woocommerce-page ul.products:not(.elementor-grid)` is
 * (0,3,1) and was setting this grid's column count at every width, so the
 * ladder below never rendered: 4 columns at 1440 (which the base asked for at
 * the time), then 4 at 1024, 3 at 768 and 2 at 360, where the 134px card left
 * the ~172px action pair overflowing by ~19px a side.
 *
 * THE BASE WENT 4 -> 3 ON 2026-09-07, so the ladder is now
 * 3 / 2 (<=900) / 1 (<=640).
 *
 * It was 4, and it was the only surface still showing four cards at 1440 once
 * the shop grid was repaired - the shop and the category archives both land on
 * 3 there. A visitor moving from an archive to a product page saw the card size
 * change under them for no reason they could name, so the strip now matches.
 *
 * The redundant `@media (max-width: 1180px) { repeat(3, ...) }` went with it:
 * it restated what the base declares.
 *
 * THE 2-COLUMN BREAKPOINT IS DELIBERATELY STILL 900px, NOT THE SHOP'S 1023px.
 * This strip is full-bleed inside `.ml-product` with no 268px sidebar taking a
 * fifth of the row, so at 950px it has ~294px per card across three columns
 * where the shop, at the same viewport, has already stacked its sidebar and is
 * running two ~450px cards. Matching the number would make this strip WIDER
 * per card than the shop, not more consistent with it. The two ladders agree at
 * every width that gets verified - 360 / 768 / 1024 / 1440 - and differ only
 * across the 901-1023 band, where each is right for its own container.
 */
.ml-product .ml-product__related section.related ul.products {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: 20px;
}

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

/*
 * The cards themselves are .ml-card, styled by shop.css, which is enqueued
 * alongside this file. Only the grid and heading are defined here.
 */

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

/* ---------- Accordions ---------- */

/*
 * TASK 2: the three WooCommerce tabs render as stacked accordion sections.
 *
 * PROGRESSIVE ENHANCEMENT IS THE WHOLE SHAPE OF THIS BLOCK. The panels default
 * to OPEN, and only collapse once accordion.js adds `.is-ready`. With no
 * JavaScript every section is readable and nothing is trapped behind a button
 * that cannot do anything - the failure mode of markup that ships `hidden`.
 *
 * `visibility: hidden` on a closed panel is not decoration either: it is what
 * takes the collapsed content out of the tab order. Height alone would leave
 * links and the review form focusable inside a panel nobody can see.
 */
/*
 * THE ACCORDION LIVES IN `accordion.css` NOW - moved 2026-09-07.
 *
 * Every rule for `.ml-accordion*` was generic (not one was scoped to
 * `.ml-product`), and the shop sidebar's filter groups now use the same
 * component. Keeping it here would have meant a second copy, which is exactly
 * the mistake `search.css` was created to undo.
 *
 * What stays in THIS file is instance styling only: the typography of the
 * product description inside `.ml-product__tabs .ml-accordion__body`, further
 * down. Do not re-add the card, trigger, chevron or animation rules here.
 *
 * PAIRED WITH: assets/css/accordion.css, and the sidebar instance block in
 * shop.css. `medlife-product` depends on `medlife-accordion` in functions.php,
 * so the component always loads first.
 */


@media (max-width: 900px) {
	.ml-product__top { grid-template-columns: minmax(0, 1fr); gap: 26px; }

	/*
	 * Sticky OFF below the single-column breakpoint. With one column the gallery
	 * sits above the summary rather than beside it, so sticking it would pin the
	 * image to the top of the viewport and make the whole summary scroll under
	 * it - the opposite of the intent.
	 */
	.ml-product__gallery {
		position: static;
		top: auto;
	}
	.ml-product .ml-product__related section.related ul.products { grid-template-columns: repeat(2, minmax(0, 1fr)); }
	/* Same five classes - a (0,2,0) override here would lose to Astra exactly
	   as the base rule did. */
	.ml-product.product .ml-product__tabs .woocommerce-tabs .woocommerce-Tabs-panel {
		padding: 24px 20px;
	}
}

@media (max-width: 640px) {
	.single-product .ml-product { padding: 16px 16px 44px; }
	.ml-product__buy form.cart { flex-wrap: wrap; }
	.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button,
	.ml-product__buy .ml-product__buynow { min-width: 0; width: 100%; }
	.ml-product__wishlist,
	.ml-product__wishlist .tinvwl_add_to_wishlist_button { width: 100%; }
	/*
	 * THE ONE-COLUMN RELATED GRID LEFT THIS BLOCK - 2026-09-22. The strip is
	 * two columns from 768 down now (see the block at the end of this file).
	 * Deleted rather than left in place: it is the same (0,4,2) selector, so
	 * it lost only on source order - a dead rule that reads like a live one.
	 */
	.ml-product.product .ml-product__tabs .woocommerce-tabs .woocommerce-Tabs-panel {
		padding: 20px 16px;
	}
}

@media (prefers-reduced-motion: reduce) {
	/* The accordion is covered in accordion.css, with the component itself. */
	.ml-product__thumb,
	.ml-product__zoom,
	.ml-product__chip,
	.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button,
	.ml-product__buy .ml-product__buynow,
	.ml-product__wishlist .tinvwl_add_to_wishlist_button,
	.ml-product__related ul.products li.product,
	.ml-product__related ul.products li.product .button,
	.ml-product .ml-product__tabs #respond input[type="text"],
	.ml-product .ml-product__tabs #respond input[type="email"],
	.ml-product .ml-product__tabs #respond select,
	.ml-product .ml-product__tabs #respond textarea,
	.ml-product .ml-product__tabs #respond .form-submit input[type="submit"] {
		transition: none;
	}

	.ml-product__thumb:hover,
	.ml-product__zoom:hover,
	.ml-product__chip:hover,
	.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:hover,
	.ml-product.product .ml-product__buy form.cart .single_add_to_cart_button.button:focus-visible,
	.ml-product__buy .ml-product__buynow:hover,
	.ml-product__buy .ml-product__buynow:focus-visible,
	.ml-product__wishlist .tinvwl_add_to_wishlist_button:hover,
	.ml-product__related ul.products li.product:hover,
	.ml-product__related ul.products li.product .button:hover {
		transform: none;
	}
}

/* -------------------------------------------------------------------------
 * Gallery lightbox
 *
 * Built by assets/js/lightbox.js and appended to <body>, so it escapes the
 * product grid entirely - nothing in .ml-product__top can clip it.
 *
 * Sized in viewport units throughout, which is what makes it work below the
 * single-column breakpoint without a separate mobile layout: the panel is a
 * column that fills the screen and the image takes whatever height is left
 * after the controls, rather than being given a fixed pixel size.
 * ---------------------------------------------------------------------- */

/* Locks the page behind the overlay. Added to <html> by the script. */
.ml-lightbox-open {
	overflow: hidden;
}

.ml-lightbox {
	position: fixed;
	inset: 0;
	/* Above .wa-float, which sits at 9999 - see CLAUDE.md on that corner. */
	z-index: 10000;
	display: flex;
	align-items: center;
	justify-content: center;
}

.ml-lightbox[hidden] {
	display: none;
}

.ml-lightbox__backdrop {
	position: absolute;
	inset: 0;
	background: rgba(10, 37, 64, 0.88);
}

.ml-lightbox__panel {
	position: relative;
	z-index: 1;
	width: 100%;
	height: 100%;
	/* Top padding clears the 56px toolbar. */
	padding: 72px 16px 16px;
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 14px;
}

/*
 * `min-height: 0` is required. Without it this flex item refuses to shrink
 * below the image's intrinsic height and the thumbnail strip is pushed off the
 * bottom of the viewport on short screens.
 */
.ml-lightbox__stage {
	flex: 1 1 auto;
	min-height: 0;
	width: 100%;
	margin: 0;
	display: flex;
	align-items: center;
	justify-content: center;
}

.ml-lightbox__img {
	max-width: 100%;
	max-height: 100%;
	width: auto;
	height: auto;
	object-fit: contain;
}

.ml-lightbox__counter {
	flex: 0 0 auto;
	margin: 0;
	color: var(--ml-white);
	font-family: var(--ml-font);
	font-size: 13.5px;
	font-weight: 600;
	letter-spacing: 0.04em;
}

/*
 * `padding: 0` on every icon-only button here, per the rule in CLAUDE.md:
 * Astra paints bare `button` with `padding: 10px 20px`, which would leave a
 * 44px control with a 4px content box and shrink these icons to a dot.
 */
/*
 * Shared LOOK only - no positioning. The two share a size and an icon
 * treatment but nothing else: the arrows float over the image, the toolbar
 * buttons are flex children of the bar.
 *
 * `position: absolute` used to live in this shared block, left behind when the
 * close button was renamed to __tool and moved into the bar. That took all six
 * toolbar buttons out of the flex flow, so they stacked at the bar`s start
 * corner instead of laying out - which is what looked like one garbled icon,
 * and what made the focus ring read as a thick white border: six translucent
 * 0.12 backgrounds composite to roughly 54% white.
 */
.ml-lightbox__tool,
.ml-lightbox__arrow {
	width: 44px;
	height: 44px;
	padding: 0;
	border: 0;
	color: var(--ml-white);
	display: flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
	transition: background-color var(--ml-t-base) var(--ml-e-standard), transform var(--ml-t-base) var(--ml-e-standard);
}

/* Floating over the artwork, so it needs a resting fill to be findable. */
.ml-lightbox__arrow {
	position: absolute;
	z-index: 2;
	border-radius: 50%;
	background: rgba(255, 255, 255, 0.12);
}

.ml-lightbox__tool:hover,
.ml-lightbox__arrow:hover,
.ml-lightbox__tool:focus-visible,
.ml-lightbox__arrow:focus-visible {
	background: rgba(255, 255, 255, 0.24);
}

/* Only the arrows lift. A row of six icons nudging about is fidgety. */
.ml-lightbox__arrow:hover,
.ml-lightbox__arrow:focus-visible {
	transform: scale(1.06);
}

/*
 * The ring sits just INSIDE the control rather than 2px outside it.
 *
 * At `outline-offset: 2px` on a 44px button the ring is a 48px rounded square
 * standing clear of the icon, which reads as a border drawn around the button.
 * Pulling it inside ties it to the control, and it still clears the glyph
 * because the icon is 20px in a 44px box.
 */
.ml-lightbox__tool:focus-visible,
.ml-lightbox__arrow:focus-visible,
.ml-lightbox__thumb:focus-visible,
.ml-lightbox__cell:focus-visible {
	outline: 2px solid var(--ml-white);
	outline-offset: -3px;
}

.ml-lightbox__arrow--prev { left: 12px; top: 50%; transform: translateY(-50%); }
.ml-lightbox__arrow--next { right: 12px; top: 50%; transform: translateY(-50%); }

/* The lift has to keep the centring translate or the arrow jumps on hover. */
.ml-lightbox__arrow--prev:hover,
.ml-lightbox__arrow--prev:focus-visible { transform: translateY(-50%) scale(1.06); }
.ml-lightbox__arrow--next:hover,
.ml-lightbox__arrow--next:focus-visible { transform: translateY(-50%) scale(1.06); }

.ml-lightbox__thumbs {
	flex: 0 0 auto;
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 8px;
	max-width: 100%;
}

.ml-lightbox__thumb {
	display: block;
	width: 56px;
	height: 56px;
	padding: 4px;
	border: 2px solid transparent;
	border-radius: 8px;
	background: rgba(255, 255, 255, 0.1);
	cursor: pointer;
	line-height: 0;
	transition: border-color var(--ml-t-base) var(--ml-e-standard), background-color var(--ml-t-base) var(--ml-e-standard);
}

.ml-lightbox__thumb img {
	width: 100%;
	height: 100%;
	object-fit: contain;
	display: block;
}

.ml-lightbox__thumb.is-active {
	border-color: var(--ml-cyan);
	background: rgba(255, 255, 255, 0.2);
}

/* The main image advertises that it opens the overlay. */
.ml-product__main img.is-zoomable {
	cursor: zoom-in;
}

@media (max-width: 640px) {
	.ml-lightbox__panel { padding: 56px 8px 8px; gap: 10px; }
	.ml-lightbox__thumb { width: 46px; height: 46px; }
	/* Arrows move off the image edges, where they would cover the artwork. */
	.ml-lightbox__arrow--prev { left: 4px; }
	.ml-lightbox__arrow--next { right: 4px; }
}

@media (prefers-reduced-motion: reduce) {
	.ml-lightbox__tool,
	.ml-lightbox__arrow,
	.ml-lightbox__thumb,
	.ml-lightbox__cell,
	.ml-lightbox__img {
		transition: none;
	}

	.ml-lightbox__tool:hover,
	.ml-lightbox__tool:focus-visible {
		transform: none;
	}

	.ml-lightbox__arrow--prev:hover,
	.ml-lightbox__arrow--prev:focus-visible,
	.ml-lightbox__arrow--next:hover,
	.ml-lightbox__arrow--next:focus-visible {
		transform: translateY(-50%);
	}
}

/* ---------- Lightbox toolbar ---------- */

/*
 * A dark bar across the top, controls right-aligned, sitting above the panel.
 *
 * It is a SIBLING of .ml-lightbox__panel rather than a child, so the panel can
 * keep centring the image without the bar's height entering that calculation -
 * the panel's own top padding is what reserves the space instead.
 */
.ml-lightbox__bar {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	z-index: 3;
	height: 56px;
	padding: 0 10px;
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: flex-end;
	gap: 2px;
	background: rgba(6, 24, 42, 0.92);
}

/*
 * Transparent at rest. With a resting fill the button already reads as a
 * rounded square, and the focus outline around it then looks like a second
 * border rather than a focus indicator.
 */
.ml-lightbox__tool {
	flex: 0 0 auto;
	border-radius: 8px;
	background: none;
}

/* A pressed toggle holds the hover fill so its state is visible at rest. */
.ml-lightbox__tool[aria-pressed="true"] {
	background: rgba(255, 255, 255, 0.24);
}

.ml-lightbox__tool[disabled] {
	opacity: 0.4;
	cursor: default;
}

/*
 * Transient messages - "Image link copied", "Slideshow paused". Visually
 * hidden rather than absent: it is a live region, so it has to stay in the
 * accessibility tree to be announced, and `display: none` would silence it.
 */
.ml-lightbox__status {
	position: absolute;
	left: 16px;
	bottom: 16px;
	z-index: 3;
	margin: 0;
	padding: 8px 14px;
	border-radius: 8px;
	background: rgba(6, 24, 42, 0.92);
	color: var(--ml-white);
	font-family: var(--ml-font);
	font-size: 13px;
}

.ml-lightbox__status:empty {
	display: none;
}

/* ---------- Zoom and pan ---------- */

/*
 * The stage clips, so a panned image cannot spill over the toolbar or the
 * thumbnail strip. Scale lives on the img; the pan translate is set inline by
 * the script, which is why `transform` is not declared here - it would be
 * overwritten on the first drag anyway.
 */
.ml-lightbox__stage.is-zoomed {
	overflow: hidden;
	cursor: grab;
}

.ml-lightbox__img.is-zoomed {
	max-width: none;
	max-height: none;
	width: 200%;
	height: auto;
	cursor: grab;
	touch-action: none;
}

.ml-lightbox__img.is-dragging {
	cursor: grabbing;
}

/* ---------- Grid view ---------- */

/*
 * Sits over the panel's content rather than replacing it, so the single-image
 * view keeps its state and returns unchanged when the grid closes.
 */
.ml-lightbox__grid {
	position: absolute;
	top: 56px;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 2;
	overflow-y: auto;
	padding: 20px;
	box-sizing: border-box;
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
	align-content: start;
	gap: 14px;
	background: rgba(10, 37, 64, 0.96);
}

.ml-lightbox__grid[hidden] {
	display: none;
}

/* With the grid up, the single-image controls are hidden rather than stacked. */
.ml-lightbox__panel.is-grid .ml-lightbox__stage,
.ml-lightbox__panel.is-grid .ml-lightbox__counter,
.ml-lightbox__panel.is-grid .ml-lightbox__thumbs,
.ml-lightbox__panel.is-grid .ml-lightbox__arrow {
	visibility: hidden;
}

.ml-lightbox__cell {
	padding: 6px;
	border: 2px solid transparent;
	border-radius: 10px;
	background: rgba(255, 255, 255, 0.1);
	cursor: pointer;
	line-height: 0;
	aspect-ratio: 1;
	transition: border-color var(--ml-t-base) var(--ml-e-standard), background-color var(--ml-t-base) var(--ml-e-standard);
}

.ml-lightbox__cell img {
	width: 100%;
	height: 100%;
	object-fit: contain;
	display: block;
}

.ml-lightbox__cell:hover {
	border-color: var(--ml-cyan);
	background: rgba(255, 255, 255, 0.2);
}

@media (max-width: 640px) {
	.ml-lightbox__bar { height: 52px; }
	.ml-lightbox__panel { padding-top: 64px; }
	.ml-lightbox__grid { top: 52px; padding: 12px; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 10px; }
}

/* ---------- Share panel ---------- */

/*
 * A third layer over the lightbox, above the grid. Escape backs out one layer
 * at a time - share, then grid, then the lightbox itself - which is why each
 * needs its own z-index rather than sharing one.
 *
 * The wrapper IS the click-outside target: the script closes the panel when the
 * click lands on this element rather than on .ml-share inside it.
 */
.ml-lightbox__share {
	position: absolute;
	inset: 0;
	z-index: 4;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 20px;
	background: rgba(10, 37, 64, 0.6);
}

.ml-lightbox__share[hidden] {
	display: none;
}

.ml-share {
	position: relative;
	width: 100%;
	max-width: 380px;
	padding: 26px 24px 22px;
	box-sizing: border-box;
	border-radius: 14px;
	background: var(--ml-white);
	box-shadow: 0 30px 60px -24px rgba(10, 37, 64, 0.7);
}

.ml-share__title {
	margin: 0 0 18px;
	font-family: var(--ml-font);
	font-size: 18px;
	font-weight: 700;
	color: var(--ml-navy);
}

/* Astra paints bare buttons; this one is icon-only, so it needs padding: 0. */
.ml-share__close {
	position: absolute;
	top: 10px;
	right: 10px;
	width: 36px;
	height: 36px;
	padding: 0;
	border: 0;
	border-radius: 8px;
	background: none;
	color: var(--ml-body);
	display: flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
	transition: background-color var(--ml-t-base) var(--ml-e-standard), color var(--ml-t-base) var(--ml-e-standard);
}

.ml-share__close:hover,
.ml-share__close:focus-visible {
	background: #eef2f6;
	color: var(--ml-navy);
}

.ml-share__nets {
	display: flex;
	flex-direction: column;
	gap: 10px;
}

/*
 * Real links, not buttons - they navigate. Each carries its own brand colour,
 * and white on all three clears 4.5:1:
 *
 *     #3b5998 Facebook   6.84:1
 *     #e60023 Pinterest   4.78:1
 *     #0c7abf Twitter     4.61:1
 */
.ml-share__net {
	display: flex;
	align-items: center;
	gap: 12px;
	min-height: 44px;
	padding: 0 16px;
	border-radius: 8px;
	color: var(--ml-white);
	font-family: var(--ml-font);
	font-size: 14.5px;
	font-weight: 600;
	text-decoration: none;
	transition: filter var(--ml-t-base) var(--ml-e-standard);
}

.ml-share__net:hover,
.ml-share__net:focus-visible {
	filter: brightness(0.92);
	color: var(--ml-white);
}

.ml-share__net:focus-visible,
.ml-share__close:focus-visible,
.ml-share__copy:focus-visible,
.ml-share__input:focus-visible {
	outline: 2px solid var(--ml-navy);
	outline-offset: 2px;
}

.ml-share__net--fb  { background: #3b5998; }
/*
 * #0c7abf, a darkened Twitter blue - the brand #1da1f2 measures only 2.83:1
 * against the white label and misses 4.5:1. This is real text at 14.5px/600, so
 * it is WCAG 1.4.3 rather than the 3:1 that applies to icons and boundaries.
 * Darkening the fill was chosen over darkening the label because a dark label
 * on light blue stops reading as the brand at all.
 */
.ml-share__net--tw  { background: #0c7abf; }
.ml-share__net--pin { background: #e60023; }

/*
 * A bottom hairline only - no box. The field is a read-only display of the
 * link, not somewhere to type, so a full input border would overstate it.
 */
.ml-share__link {
	display: flex;
	align-items: center;
	gap: 8px;
	margin-top: 20px;
	border-bottom: 1px solid var(--ml-line);
}

/*
 * Written as three classes to clear Astra's `input[type="text"]` at (0,1,1),
 * which otherwise supplies its own border, radius and shadow - the same trap
 * the header search field hit, recorded in CLAUDE.md.
 */
.ml-share .ml-share__link .ml-share__input {
	flex: 1 1 auto;
	min-width: 0;
	width: auto;
	height: 42px;
	margin: 0;
	padding: 0;
	border: 0;
	border-radius: 0;
	background: none;
	box-shadow: none;
	color: var(--ml-body);
	font-family: var(--ml-font);
	font-size: 13.5px;
	text-overflow: ellipsis;
}

.ml-share__copy {
	flex: 0 0 auto;
	width: 40px;
	height: 40px;
	padding: 0;
	border: 0;
	border-radius: 8px;
	background: none;
	color: var(--ml-navy);
	display: flex;
	align-items: center;
	justify-content: center;
	cursor: pointer;
	transition: background-color var(--ml-t-base) var(--ml-e-standard), color var(--ml-t-base) var(--ml-e-standard);
}

.ml-share__copy:hover,
.ml-share__copy:focus-visible {
	background: #eef2f6;
	color: var(--ml-cyan);
}

@media (prefers-reduced-motion: reduce) {
	.ml-share__net,
	.ml-share__close,
	.ml-share__copy {
		transition: none;
	}

	.ml-share__net:hover,
	.ml-share__net:focus-visible {
		filter: none;
	}
}

/* -------------------------------------------------------------------------
 * Gallery image counter
 *
 * Top-left of the main image, mirroring the zoom button's 16px inset at
 * top-right. Rendered only when the product has more than one image, so there
 * is no "1 / 1" state to style for.
 * ---------------------------------------------------------------------- */

.ml-product__counter {
	position: absolute;
	top: 16px;
	left: 16px;
	z-index: 2;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	margin: 0;
	padding: 6px 11px;
	border-radius: 999px;
	background: rgba(10, 37, 64, 0.66);
	color: var(--ml-white);
	font-family: var(--ml-font);
	font-size: 12.5px;
	font-weight: 600;
	line-height: 1;
	letter-spacing: 0.01em;
	white-space: nowrap;

	/*
	 * A HINT, NOT A CONTROL. Without this the pill sits over the top-left of the
	 * artwork and swallows clicks meant for the image - which matters because
	 * the image is the lightbox trigger. There is deliberately no :hover and no
	 * :focus-visible rule anywhere for this element: it is not focusable, it is
	 * aria-hidden, and giving it a state would imply it does something.
	 */
	pointer-events: none;
	user-select: none;
}

.ml-product__counter svg {
	flex: 0 0 auto;
	display: block;
	opacity: 0.9;
}

.ml-product__counter-text {
	font-variant-numeric: tabular-nums;
}

@media (max-width: 640px) {
	.ml-product__counter {
		top: 12px;
		left: 12px;
		padding: 5px 9px;
		font-size: 11.5px;
	}
}

/* -------------------------------------------------------------------------
 * Description panel typography (2026-09-05)
 *
 * WHY THIS EXISTS: the panel had NO heading styling at all. The rules above
 * that look like they cover it - `.ml-product__tabs .woocommerce-Tabs-panel
 * h2/h3` - are DEAD. WooCommerce's tab markup was replaced by the accordion,
 * so `.woocommerce-Tabs-panel` appears nowhere in the DOM; nine rules in this
 * file still target it and match nothing. They are left in place for now
 * because removing them is a separate change, but do not add to them.
 *
 * Every heading therefore fell through to Astra: h2 30px/600, h3 20px/600,
 * h4 20px/600, h5 18px/600, all `#1e293b`. **h3 and h4 rendered identically**,
 * which is exactly why authors picked levels at random and the same section is
 * an h3 on one product and an h4 on the next.
 *
 * An explicit scale, so the levels are visually distinguishable and an author
 * can see which one they are looking at:
 *
 *     h2  23px / 700   the product's opening statement, one per description
 *     h3  19px / 700   major sections - Composition, Dosage, Storage, FAQs
 *     h4  16.5px / 600 subsections - an individual FAQ question
 *
 * h4 gets its own rule specifically so it stops inheriting Astra's `#1e293b`
 * while h2 and h3 carry the theme's navy - two heading colours in one panel.
 * ---------------------------------------------------------------------- */

.ml-product__tabs .ml-accordion__body h2,
.ml-product__tabs .ml-accordion__body h3,
.ml-product__tabs .ml-accordion__body h4 {
	font-family: var(--ml-font);
	color: var(--ml-navy);
	letter-spacing: -0.01em;
}

/*
 * The floor is 20px, not 18px. The previous rule clamped to 18px, which is
 * BELOW h3 - so on any viewport under about 1150px a section heading rendered
 * smaller than its own subsection. The floor must stay above h3's size.
 */
.ml-product__tabs .ml-accordion__body h2 {
	margin: 0 0 14px;
	font-size: clamp(20px, 2vw, 23px);
	font-weight: 700;
	line-height: 1.25;
}

.ml-product__tabs .ml-accordion__body h3 {
	margin: 22px 0 14px;
	font-size: 19px;
	font-weight: 700;
	line-height: 1.3;
}

.ml-product__tabs .ml-accordion__body h4 {
	margin: 22px 0 14px;
	font-size: 16.5px;
	font-weight: 600;
	line-height: 1.35;
}

/*
 * Description paragraphs and lists: 22px after every block - the spacing scale
 * (CLAUDE.md). With no rule of its own this was Astra's em-based
 * `p { margin-bottom: 1.75em }` and `ul { margin: 0 0 1.5em 3em }`: 28 / 24px on
 * desktop, 25.5 / 21.9px on phones. margin-bottom only, so Astra's 3em list
 * indent is left as it was.
 */
.ml-product__tabs .ml-accordion__body p,
.ml-product__tabs .ml-accordion__body ul,
.ml-product__tabs .ml-accordion__body ol {
	margin-bottom: 22px;
}

/* First heading in a panel never needs the section gap above it. */
.ml-product__tabs .ml-accordion__body > h2:first-child,
.ml-product__tabs .ml-accordion__body > h3:first-child,
.ml-product__tabs .ml-accordion__body > h4:first-child {
	margin-top: 0;
}

/*
 * 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.
 */
.ml-product__crumbs a:focus:not(:focus-visible),
a.ml-product__zoom:focus:not(:focus-visible),
a.ml-product__cat:focus:not(:focus-visible),
.ml-product__meta a:focus:not(:focus-visible),
.comment-reply-title a:focus:not(:focus-visible),
p.stars a: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").
 */
.ml-product__crumbs a:focus-visible,
a.ml-product__cat:focus-visible,
.ml-product__meta a:focus-visible,
.comment-reply-title a:focus-visible,
p.stars a:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
	border-radius: 4px;
}
a.ml-product__zoom:focus-visible {
	outline: 3px solid var(--ml-cyan);
	outline-offset: 3px;
	border-radius: 50%;
}

/*
 * 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-product__cat,
.comment-reply-title a {
	transition: color var(--ml-t-base) var(--ml-e-standard);
}
.ml-product__crumbs a,
.ml-product__meta a,
p.stars a {
	transition: none;
}

/*
 * <= 768px: related products follow the shop's two-column phone grid.
 *
 * The same rule is in shop.css and LOSES HERE — both selectors measure
 * (0,4,2), so it is a tie, and on a product page this file is served after
 * shop.css. Measured before this line existed: shop 2 columns, related 1.
 * A tie settled by load order is exactly what this project's own notes say
 * not to lean on, so the rule is restated in the file that owns the strip.
 *
 * 640 -> 768 and the gap 20/14 -> 18/12 on 2026-09-22, with the compact card.
 * MEASURED, NOT ASSUMED: with this block still at 640 the shop grid read
 * `gap: 18px 12px` at 375 while related read `20px 14px` on the same phone,
 * because shop.css's rule reaches both selectors and this one only overrode
 * the second. A restated rule is a second copy, and a second copy drifts the
 * moment the first one moves - which is exactly what happened here within a
 * day. It has to be edited in step, every time.
 */
@media (max-width: 768px) {
	.ml-product .ml-product__related section.related ul.products {
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: 18px 12px;
	}
}
