/*
 * Single product page.
 *
 * Loaded on the product view only — the gallery, the variation form, the quantity
 * control and the details table exist nowhere else (§48).
 *
 * Sections:
 *   1. Lead layout
 *   2. Gallery
 *   3. Full-size view
 *   4. Summary: title, price, availability
 *   5. Purchase form: variations, quantity, add to cart
 *   6. Delivery and unavailable notices
 *   7. Description and details
 *   8. Related products
 *
 * Every value comes from a token defined in base.css, which aliases a theme.json
 * preset. This file introduces no new colour, spacing or type-scale value.
 *
 * A note on the selectors. Many rules carry the full
 * `.woocommerce div.product …` prefix. That is not decoration: WooCommerce's own
 * stylesheets declare the same properties at that specificity, and the project rule
 * is to match the plugin's selector shape rather than to dequeue its stylesheet or
 * reach for !important (ARCHITECTURE.md §15). Each such rule names the declaration
 * it is answering.
 */

/* ==========================================================================
   1. Lead layout
   ========================================================================== */

/*
 * Gallery and summary side by side.
 *
 * WooCommerce floats them at 48% each. Grid replaces that, and the plugin's float
 * and width are reset explicitly below — a leftover float would take the element
 * out of the grid flow and the second column would collapse.
 *
 * The two columns start at 48rem rather than at the navigation breakpoint. Waiting
 * until 62rem would leave tablet portrait with a full-width square image and the
 * price, options and buy button pushed off the screen (§12).
 *
 * The 48rem and 75rem thresholds below are also stated in the main gallery image's
 * `sizes` attribute — see zakatek_product_gallery_sizes(). Keep the two in step.
 */
.zu-product-lead {
	display: grid;
	gap: var(--zu-space-4);
	margin-block-end: var(--zu-space-5);
}

@media (min-width: 48rem) {
	.zu-product-lead {
		grid-template-columns: minmax(0, 45%) minmax(0, 1fr);
		gap: var(--zu-space-5);
		align-items: start;
	}
}

@media (min-width: 75rem) {
	.zu-product-lead {
		grid-template-columns: minmax(0, 48%) minmax(0, 1fr);
	}
}

/* Answers `.woocommerce div.product div.images { float: left; width: 48% }`. */
.woocommerce div.product .zu-product-lead div.images,
.woocommerce div.product .zu-product-lead div.summary {
	float: none;
	width: auto;
	min-width: 0;
	margin: 0;
}

/*
 * The two known children are placed explicitly rather than left to auto-placement.
 *
 * `woocommerce_before_single_product_summary` is one of the most-used hooks on this
 * page, and the theme renders the gallery at priority 20 — so anything a plugin adds
 * at the default 10 would become the FIRST grid item, displacing the gallery into
 * column two and the summary onto a second row. With the columns claimed, an
 * unexpected sibling flows underneath instead of inside them. Raised by an
 * independent review; latent today because no plugins are installed.
 */
@media (min-width: 48rem) {
	.woocommerce div.product .zu-product-lead div.images {
		grid-row: 1;
		grid-column: 1;
	}

	.woocommerce div.product .zu-product-lead div.summary {
		grid-row: 1;
		grid-column: 2;
	}
}

/*
 * The product wrapper is a plain block again.
 *
 * WooCommerce's `div.product` carries `position: relative` for the absolutely
 * positioned sale flash, which this theme does not use. Left alone it is harmless,
 * so it stays — but the float clearing the plugin relies on is gone with the floats.
 */
.woocommerce div.product {
	margin-bottom: 0;
}

/* ==========================================================================
   2. Gallery
   ========================================================================== */

.zu-gallery {
	display: flex;
	flex-direction: column;
	gap: var(--zu-space-2);
}

.zu-gallery__stage {
	position: relative;
}

/*
 * One fixed frame ratio, and the image is CONTAINED inside it, not cropped.
 *
 * This is the opposite decision from the product card, and deliberately so. In the
 * grid, uniform tiles matter more than seeing every pixel, so the card crops with
 * `object-fit: cover`. On the product page the shopper is deciding whether to buy:
 * cropping the product out of its own photograph is the one thing the gallery must
 * not do, and the catalogue has no uniform aspect ratio to rely on — the fixture set
 * alone spans 4:5, 16:9, 1:1, 2:3 and 5:4.
 *
 * The frame stays square so switching between a portrait and a landscape image does
 * not resize the column and shove the buy block up and down the page. Letterboxing
 * against the surface colour is the visible cost, and it is the cheaper one.
 */
.zu-gallery__frame {
	position: relative;
	aspect-ratio: 1 / 1;
	overflow: hidden;
	border-radius: var(--zu-radius);
	background-color: var(--zu-surface);
}

/*
 * Answers `.woocommerce div.product div.images img { width: 100%; height: auto }`,
 * which would defeat object-fit by letting the image keep its own height.
 */
.woocommerce div.product div.images img.zu-gallery__image,
.zu-gallery__image {
	width: 100%;
	height: 100%;
	object-fit: contain;
}

.zu-gallery__frame--empty {
	display: flex;
	align-items: center;
	justify-content: center;
	background-color: var(--zu-blush);
	color: var(--zu-gold-decor);
}

/*
 * The frame for a product with no image holds two things: the theme's icon, and a
 * real `<img class="wp-post-image">` that WooCommerce's variation script can mutate.
 * Only one of them is ever on screen — until something swaps the image, all it could
 * show is WooCommerce's grey placeholder graphic.
 *
 * Answers `.woocommerce div.product div.images img { display: block }`, which is the
 * same plugin declaration already countered above for `width` and `height`. The
 * simpler selector lost and the placeholder graphic rendered on top of the icon —
 * caught by the suite, which is why it asserts the computed `display` rather than
 * the presence of a class.
 */
.woocommerce div.product div.images .zu-gallery__frame--empty .zu-gallery__image,
.zu-gallery__frame--empty .zu-gallery__image {
	display: none;
}

.woocommerce div.product div.images .zu-gallery__frame--empty.is-swapped .zu-gallery__image,
.zu-gallery__frame--empty.is-swapped .zu-gallery__image {
	display: block;
}

.zu-gallery__frame--empty.is-swapped {
	background-color: var(--zu-surface);
}

.zu-gallery__frame--empty.is-swapped .zu-gallery__placeholder {
	display: none;
}

/* Answers `.woocommerce div.product div.images .woocommerce-product-gallery__image a`. */
.woocommerce div.product div.images .zu-gallery__frame a.zu-gallery__full,
.zu-gallery__full {
	display: block;
	width: 100%;
	height: 100%;
	outline-offset: var(--zu-focus-offset);
}

/* --- Badges --- */

/*
 * Same vocabulary as the card, including `pointer-events: none` — the badge sits
 * over the full-size link, and a click on it would otherwise land nowhere.
 */
.zu-gallery__badges {
	pointer-events: none;
	position: absolute;
	inset-block-start: var(--zu-space-2);
	inset-inline-start: var(--zu-space-2);
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--zu-space-1);
	z-index: 2;
}

/* --- Thumbnails --- */

.zu-gallery__thumbs {
	display: flex;
	flex-wrap: wrap;
	gap: var(--zu-space-1);
	margin: 0;
	padding: 0;
	list-style: none;
}

.zu-gallery__thumb-item {
	margin: 0;
}

/*
 * A real button, so the tab order includes it without a roving-tabindex scheme.
 * 4.5rem is 72 px, above the 44 px touch minimum on every screen.
 */
.zu-gallery__thumb {
	display: block;
	width: 4.5rem;
	height: 4.5rem;
	padding: 0;
	border: 1px solid var(--zu-line);
	border-radius: var(--zu-radius-sm);
	background-color: var(--zu-surface);
	overflow: hidden;
	cursor: pointer;
	transition: border-color var(--zu-transition), opacity var(--zu-transition);
}

.woocommerce div.product div.images .zu-gallery__thumb img,
.zu-gallery__thumb-image {
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/*
 * The active thumbnail is marked by its border colour, which measures 6.40:1 against
 * the idle border — twice the 3:1 the guidelines ask of a non-text indicator. The
 * dimming of the inactive ones is a supporting cue and little more: an independent
 * review measured the fill difference at 1.09:1 and found the inset ring painted
 * under the image, so the border is doing the work. The machine-readable signal is
 * `aria-current` on the button itself, which is what assistive technology reads.
 */
.zu-gallery__thumb[aria-current='false'] {
	opacity: 0.65;
}

.zu-gallery__thumb:hover {
	opacity: 1;
	border-color: var(--zu-gold-decor);
}

.zu-gallery__thumb[aria-current='true'] {
	opacity: 1;
	border-color: var(--zu-accent-deep);
	box-shadow: inset 0 0 0 1px var(--zu-accent-deep);
}

@media (prefers-reduced-motion: reduce) {
	.zu-gallery__thumb {
		transition: none;
	}
}

/*
 * Four 72 px thumbnails plus three gutters need 312 px; a 320 px screen offers 288
 * inside the page gutters, so the fourth wrapped onto a row of its own. Measured by
 * an independent review. Sixty pixels fits four with room to spare and stays well
 * above the 44 px touch minimum. Five or more images still wrap, which is a second
 * row rather than a defect.
 */
@media (max-width: 22.5rem) {
	.zu-gallery__thumb {
		width: 3.75rem;
		height: 3.75rem;
	}
}

/* ==========================================================================
   3. Full-size view
   ========================================================================== */

/*
 * A native dialog, not a lightbox library.
 *
 * `<dialog>` brings the modal semantics, the focus trap and Escape with it, so the
 * script only has to set a source and call showModal(). When showModal is missing
 * the script does not intercept the click at all and the anchor keeps working as a
 * plain link to the image file — which is also what happens with no JavaScript.
 */
.zu-lightbox {
	max-width: min(96vw, 1200px);
	max-height: 94vh;
	padding: 0;
	border: 0;
	border-radius: var(--zu-radius);
	background-color: var(--zu-surface);
	overflow: hidden;
}

.zu-lightbox::backdrop {
	background-color: color-mix(in srgb, var(--zu-deep) 82%, transparent);
}

.zu-lightbox__image {
	display: block;
	max-width: min(96vw, 1200px);
	max-height: 94vh;
	width: auto;
	height: auto;
	margin-inline: auto;
}

.zu-lightbox__close {
	position: absolute;
	inset-block-start: var(--zu-space-2);
	inset-inline-end: var(--zu-space-2);
	z-index: 1;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 44px;
	height: 44px;
	border: 1px solid var(--zu-line);
	border-radius: var(--zu-radius-pill);
	background-color: var(--zu-surface);
	color: var(--zu-ink);
	cursor: pointer;
}

.zu-lightbox__close:hover {
	border-color: var(--zu-accent-deep);
	color: var(--zu-accent-deep);
}

/* ==========================================================================
   4. Summary: title, price, availability
   ========================================================================== */

.woocommerce div.product .zu-product-lead div.summary {
	display: flex;
	flex-direction: column;
	gap: var(--zu-space-3);
}

/*
 * Product names in this catalogue run past 75 characters, so the title sits one
 * step below the archive H1 on the type scale. A didone at the full display size
 * over four lines in a narrow column is a wall of high-contrast strokes; at this
 * size it still reads as the page's heading (§13 of the stage brief).
 *
 * Answers `.woocommerce div.product .product_title { margin-top: 0; padding: 0 }`.
 */
.woocommerce div.product .zu-product-lead .product_title {
	margin: 0;
	padding: 0;
	font-size: var(--zu-text-h2);
	text-wrap: pretty;
}

/* --- Price --- */

/*
 * Answers two plugin declarations at once: the generic `p.price` sizing and
 * `.woocommerce:where(body:not(.woocommerce-uses-block-theme)) div.product
 * span.price { color: #958e09 }`, which paints the price WooCommerce's olive green.
 * `:where()` adds no specificity, so that rule is (0,3,2) and this one is (0,4,2).
 */
.woocommerce div.product .zu-product-lead p.price,
.woocommerce div.product .zu-product-lead span.price {
	margin: 0;
	color: var(--zu-ink);
	font-size: var(--zu-text-lead);
	font-weight: 500;
	line-height: 1.3;
}

.woocommerce div.product .zu-product-lead .price del {
	margin-inline-end: 0.5rem;
	color: var(--zu-ink-muted);
	font-weight: 400;
	opacity: 1;
}

/*
 * The sale price is not underlined. Browsers underline <ins>, which in a price
 * reads as a link; the strike-through beside it and the badge on the image already
 * carry the meaning.
 */
.woocommerce div.product .zu-product-lead .price ins {
	background: none;
	color: var(--zu-accent-deep);
	font-weight: 500;
	text-decoration: none;
}

/* The price range separator on a variable product before anything is selected. */
.woocommerce div.product .zu-product-lead .price .woocommerce-Price-amount {
	white-space: nowrap;
}

/* --- Availability --- */

/*
 * Availability stays where WooCommerce puts it — immediately above the purchase
 * control — because that is the one position that works for both product types.
 * See the comment at the top of inc/product-page.php.
 *
 * Answers `.woocommerce:where(…) div.product .stock { color: #958e09 }` and
 * `.woocommerce:where(…) div.product .out-of-stock { color: var(--wc-red) }`.
 */
.woocommerce div.product .zu-product-lead .stock {
	margin: 0;
	font-size: var(--zu-text-sm);
	font-weight: 500;
	color: var(--zu-ink-soft);
}

.woocommerce div.product .zu-product-lead .stock.in-stock {
	color: var(--zu-success);
}

.woocommerce div.product .zu-product-lead .stock.out-of-stock,
.woocommerce div.product .zu-product-lead .stock.available-on-backorder {
	color: var(--zu-accent-deep);
}

/* --- Short description --- */

.woocommerce-product-details__short-description {
	color: var(--zu-ink-soft);
}

.woocommerce-product-details__short-description > * + * {
	margin-block-start: var(--zu-space-2);
}

/* ==========================================================================
   5. Purchase form
   ========================================================================== */

/* Answers `.woocommerce div.product form.cart { margin-bottom: 2em }`. */
.woocommerce div.product .zu-product-lead form.cart {
	display: flex;
	flex-direction: column;
	gap: var(--zu-space-3);
	margin: 0;
}

.woocommerce div.product .zu-product-lead form.cart::before,
.woocommerce div.product .zu-product-lead form.cart::after {
	content: none;
}

/* --- Variation selectors --- */

/*
 * WooCommerce renders the selectors as a `<table role="presentation">`. The role
 * means assistive technology does not announce it as a table, so the markup is
 * honest already and there is nothing to gain from replacing it — the layout is a
 * CSS problem. Turning the table parts into blocks gives label-above-control, which
 * is what the rest of the theme's forms do.
 *
 * Answers `.woocommerce div.product form.cart .variations { width: 100% }`,
 * `… .variations th { line-height: 2em; vertical-align: top }`,
 * `… .variations label { font-weight: 700; text-align: left }` and
 * `… .variations td.label { padding-right: 1em }`.
 */
.woocommerce div.product .zu-product-lead form.cart table.variations,
.woocommerce div.product .zu-product-lead form.cart table.variations tbody,
.woocommerce div.product .zu-product-lead form.cart table.variations tr,
.woocommerce div.product .zu-product-lead form.cart table.variations th,
.woocommerce div.product .zu-product-lead form.cart table.variations td {
	display: block;
	width: auto;
	margin: 0;
	padding: 0;
	border: 0;
	line-height: inherit;
	text-align: start;
}

.woocommerce div.product .zu-product-lead form.cart table.variations tr + tr {
	margin-block-start: var(--zu-space-2);
}

.woocommerce div.product .zu-product-lead form.cart table.variations th.label {
	margin-block-end: var(--zu-space-1);
}

.woocommerce div.product .zu-product-lead form.cart table.variations label {
	font-size: var(--zu-text-sm);
	font-weight: 500;
	color: var(--zu-ink-soft);
}

.woocommerce div.product .zu-product-lead form.cart table.variations select {
	width: 100%;
	padding-block: 0.7rem;
}

/*
 * The reset control keeps WooCommerce's `visibility: hidden` start state — the
 * plugin's script reveals it once a selection exists, and it checks the computed
 * visibility to decide whether to animate. Overriding that here would make the
 * control appear before there is anything to reset.
 *
 * Answers `.woocommerce div.product form.cart .reset_variations { font-size: .83em }`.
 */
.woocommerce div.product .zu-product-lead form.cart .reset_variations {
	margin-block-start: var(--zu-space-1);
	padding: 0.25rem 0;
	border: 0;
	background: none;
	color: var(--zu-ink-muted);
	font-size: var(--zu-text-sm);
	text-decoration: underline;
	text-underline-offset: 0.2em;
	cursor: pointer;
}

.woocommerce div.product .zu-product-lead form.cart .reset_variations:hover {
	color: var(--zu-accent-deep);
}

/*
 * A touch target, not just a text link. It measured 58 × 30 px under a coarse
 * pointer — below the 44 px bar this project already raised the header controls to,
 * for the same reason. Padding does the work; the text stays where it is.
 */
@media (pointer: coarse) {
	.woocommerce div.product .zu-product-lead form.cart .reset_variations {
		display: inline-flex;
		align-items: center;
		min-height: 44px;
	}
}

/* --- Variation feedback --- */

/*
 * Empty until something goes wrong, and out of the layout while empty so it does not
 * leave a gap in the form. The message is text, never colour alone.
 *
 * Clipped rather than `display: none`. A live region created or revealed in the same
 * task as its own text is the classic unreliable pattern, and `display: none` keeps
 * the region out of the accessibility tree entirely until it fills. Clipping leaves
 * it present from page load, taking no space, which is what the region needs to be
 * announced dependably. Raised by an independent review; whether real screen readers
 * speak it stays on the NOT VERIFIED list either way.
 */
.zu-variation-message:empty {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	border: 0;
	white-space: nowrap;
}

.zu-variation-message {
	margin: 0;
	padding: var(--zu-space-2);
	border: 1px solid color-mix(in srgb, var(--zu-accent-deep) 25%, var(--zu-line));
	border-inline-start: 3px solid var(--zu-accent-deep);
	border-radius: var(--zu-radius-sm);
	background-color: color-mix(in srgb, var(--zu-accent-deep) 8%, var(--zu-surface));
	color: var(--zu-ink);
	font-size: var(--zu-text-sm);
}

/* WooCommerce's own "choose a different combination" notice, same treatment. */
.woocommerce div.product .zu-product-lead .single_variation [role='alert'],
.woocommerce div.product .zu-product-lead .wc-no-matching-variations {
	display: block;
	margin: 0;
	padding: var(--zu-space-2);
	border: 1px solid color-mix(in srgb, var(--zu-accent-deep) 25%, var(--zu-line));
	border-inline-start: 3px solid var(--zu-accent-deep);
	border-radius: var(--zu-radius-sm);
	background-color: color-mix(in srgb, var(--zu-accent-deep) 8%, var(--zu-surface));
	font-size: var(--zu-text-sm);
}

.woocommerce div.product .zu-product-lead .single_variation {
	display: flex;
	flex-direction: column;
	gap: var(--zu-space-2);
}

.woocommerce div.product .zu-product-lead .single_variation_wrap {
	display: flex;
	flex-direction: column;
	gap: var(--zu-space-3);
}

.woocommerce div.product .zu-product-lead .woocommerce-variation-description p {
	margin: 0;
	color: var(--zu-ink-soft);
	font-size: var(--zu-text-sm);
}

/* --- Quantity --- */

/*
 * Answers `.woocommerce div.product form.cart div.quantity { float: left; margin }`.
 */
.woocommerce div.product .zu-product-lead form.cart .zu-qty div.quantity {
	float: none;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--zu-space-1);
	margin: 0;
}

/*
 * The visible caption over the quantity field.
 *
 * WooCommerce's own `<label for="…">` would have been the right element to show,
 * and it cannot be: the plugin declares `.screen-reader-text { position: absolute
 * !important }`, which no selector can outrank without a second !important. See
 * zakatek_quantity_open() for why the caption is aria-hidden instead.
 */
.zu-qty__caption {
	display: block;
	margin-block-end: var(--zu-space-1);
	font-size: var(--zu-text-sm);
	font-weight: 500;
	color: var(--zu-ink-soft);
}

/* Answers `.woocommerce .quantity .qty { width: 3.631em; text-align: center }`. */
.woocommerce .zu-qty div.quantity input.qty {
	width: 4.5rem;
	min-height: 44px;
	padding-inline: 0.5rem;
	text-align: center;
}

.zu-qty__step {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 44px;
	min-height: 44px;
	padding: 0;
	border: 1px solid var(--zu-line);
	border-radius: var(--zu-radius-sm);
	background-color: var(--zu-surface);
	color: var(--zu-ink);
	font-size: var(--zu-text-base);
	line-height: 1;
	cursor: pointer;
}

.zu-qty__step:hover:not([disabled]) {
	border-color: var(--zu-accent-deep);
	color: var(--zu-accent-deep);
}

.zu-qty__step[disabled] {
	opacity: 0.45;
	cursor: not-allowed;
}

/*
 * Nothing to choose, nothing to show. Minimum equals maximum for a product sold
 * individually and for one with a single unit left; WooCommerce renders a hidden
 * input in both cases, and a lone "Ilość" label above nothing is noise.
 */
.zu-qty--fixed {
	display: none;
}

/*
 * A VARIATION THAT CANNOT BE BOUGHT — the MATERIAL finding stage 3 accepted as debt
 * and left for this stage (roadmap 4.11).
 *
 * Choosing a sold-out combination left two things on screen that only make sense for
 * a purchase that can happen: a quantity stepper for an item nobody can take, and a
 * delivery note reading "free delivery over 300 zł" directly beneath "Brak w
 * magazynie". Stage 3 declined to fix either, and the reasoning it gave was that
 * hiding them would mean a JavaScript dependency between an information box and
 * variation state, or reimplementing the plugin's own form rendering.
 *
 * The mechanism turns out to be neither. WooCommerce publishes the state itself:
 * `add-to-cart-variation.js` puts `wc-variation-is-unavailable` on the add-to-cart
 * button for exactly this case, and a different class,
 * `wc-variation-selection-needed`, when nothing has been chosen yet. That distinction
 * is the whole difficulty, and the plugin has already made it. Traced through the
 * real form: nothing chosen gives `selection-needed`, CC / 8 mm gives
 * `is-unavailable`, C / 8 mm leaves the button enabled, and "Wyczyść" returns to
 * `selection-needed` — so a product opens with its delivery note intact.
 *
 * No JavaScript, no template, and nothing of WooCommerce's reimplemented: one CSS
 * rule keyed on a class the plugin maintains.
 *
 * The blocked button, the availability line and the focus move stay as they were.
 * What changes is that the page no longer offers an interaction and a delivery
 * promise for something it has just said cannot be bought.
 */
.summary:has( .single_add_to_cart_button.wc-variation-is-unavailable ) .zu-qty,
.summary:has( .single_add_to_cart_button.wc-variation-is-unavailable ) .zu-delivery {
	display: none;
}

/* --- Add to cart --- */

/*
 * Answers `.woocommerce div.product form.cart .button { float: left }`. The button
 * fills the column on phones, where it is the page's main action, and sits at its
 * natural width beside the quantity control once there is room.
 */
.woocommerce div.product .zu-product-lead form.cart .single_add_to_cart_button {
	float: none;
	width: 100%;
	min-height: 48px;
}

.woocommerce div.product .zu-product-lead form.cart .woocommerce-variation-add-to-cart {
	display: flex;
	flex-direction: column;
	gap: var(--zu-space-3);
}

@media (min-width: 30rem) {
	.woocommerce div.product .zu-product-lead form.cart .single_add_to_cart_button {
		width: auto;
		min-width: 14rem;
	}
}

/*
 * The disabled state of the add-to-cart button.
 *
 * WooCommerce marks it with a class only, so the script mirrors that into
 * `aria-disabled` and this rule shows it. Neither the colour nor the cursor is the
 * sole carrier of meaning: the reason is written out above the button, which is what
 * the accessibility contract requires (ARCHITECTURE.md §13).
 *
 * A muted fill with dark ink, NOT the whole button at reduced opacity. Compositing
 * the burgundy pill at 0.55 measured 2.77:1 for its label — and this control is not
 * exempt as an "inactive component", because it has no `disabled` attribute, it is a
 * tab stop, and activating it deliberately does something: the theme keeps it
 * operable precisely so it can explain the refusal. On a variable product with no
 * default variation that state is what the page opens in, so the primary call to
 * action was below the contrast threshold on first paint. Measured by an independent
 * review; this pair measures well past AA and still reads unmistakably as blocked.
 */
.woocommerce div.product .zu-product-lead form.cart .single_add_to_cart_button.disabled,
.woocommerce div.product .zu-product-lead form.cart .single_add_to_cart_button[aria-disabled='true'] {
	opacity: 1;
	background-color: var(--zu-line);
	border-color: var(--zu-line);
	color: var(--zu-ink-soft);
	cursor: not-allowed;
}

/* Hover must not suggest the control is available. */
.woocommerce div.product .zu-product-lead form.cart .single_add_to_cart_button.disabled:hover,
.woocommerce div.product .zu-product-lead form.cart .single_add_to_cart_button[aria-disabled='true']:hover {
	background-color: var(--zu-line);
	color: var(--zu-ink-soft);
}

/* ==========================================================================
   6. Delivery and unavailable notices
   ========================================================================== */

.zu-delivery {
	padding: var(--zu-space-2) var(--zu-space-3);
	border: 1px solid var(--zu-line);
	border-radius: var(--zu-radius-sm);
	background-color: var(--zu-blush);
}

.zu-delivery__highlight {
	margin: 0;
	font-family: var(--zu-font-display);
	font-size: var(--zu-text-lead);
	color: var(--zu-ink);
}

.zu-delivery__text {
	margin: 0;
	color: var(--zu-ink-soft);
	font-size: var(--zu-text-sm);
}

.zu-delivery__highlight + .zu-delivery__text {
	margin-block-start: var(--zu-space-1);
}

.zu-product-unavailable {
	padding: var(--zu-space-3);
	border: 1px solid var(--zu-line);
	border-inline-start: 3px solid var(--zu-accent-deep);
	border-radius: var(--zu-radius-sm);
	background-color: color-mix(in srgb, var(--zu-accent-deep) 6%, var(--zu-surface));
}

.zu-product-unavailable__text {
	margin: 0;
	color: var(--zu-ink);
	font-weight: 500;
}

.zu-product-unavailable__actions {
	margin-block-start: var(--zu-space-2);
	margin-block-end: 0;
}

/* ==========================================================================
   7. Description and details
   ========================================================================== */

.zu-product-section {
	max-width: var(--zu-container-text);
	margin-block-start: var(--zu-space-5);
}

.zu-product-section__title {
	margin-block-end: var(--zu-space-3);
}

/*
 * Imported descriptions bring their own headings, and the theme cannot control which
 * level a shop owner used. The section title above is an h2, so anything inside is
 * visually subordinate regardless of its tag — but the levels still have to be
 * distinguishable from each other. An independent review found h3 and h4 rendering
 * at the same size in the didone face, which makes a nested description read as a
 * flat one.
 */
.zu-product-description__body :is(h2, h3) {
	font-size: var(--zu-text-lead);
}

.zu-product-description__body h4 {
	font-family: var(--zu-font-sans);
	font-size: var(--zu-text-base);
	font-weight: 500;
	letter-spacing: 0;
}

.zu-product-description__body :is(figure, img) {
	border-radius: var(--zu-radius-sm);
}

.zu-product-description__body figure {
	margin: 0;
}

.zu-product-description__body figcaption {
	margin-block-start: var(--zu-space-1);
	color: var(--zu-ink-muted);
	font-size: var(--zu-text-sm);
}

.zu-product-description__body blockquote {
	margin: 0;
	padding-inline-start: var(--zu-space-3);
	border-inline-start: 2px solid var(--zu-gold-decor);
	color: var(--zu-ink-soft);
	font-style: normal;
}

/* --- Attributes table --- */

/*
 * Answers the plugin's `table.shop_attributes` block: dotted borders, a 150 px
 * label column, bold labels and — the one that matters — `font-style: italic` on
 * every value, which turns a specification list into a run of italics.
 *
 * The table becomes a two-column grid so a long value wraps under its own label
 * instead of being squeezed into whatever is left of 150 px.
 */
.woocommerce div.product table.shop_attributes,
.woocommerce table.shop_attributes {
	width: 100%;
	margin: 0;
	border: 0;
	border-block-start: 1px solid var(--zu-line);
}

.woocommerce div.product table.shop_attributes th,
.woocommerce table.shop_attributes th {
	width: 12rem;
	padding: var(--zu-space-2) var(--zu-space-2) var(--zu-space-2) 0;
	border: 0;
	border-block-end: 1px solid var(--zu-line);
	color: var(--zu-ink-soft);
	font-size: var(--zu-text-sm);
	font-weight: 500;
	line-height: 1.5;
	text-align: start;
	vertical-align: top;
}

.woocommerce div.product table.shop_attributes td,
.woocommerce table.shop_attributes td {
	padding: var(--zu-space-2) 0;
	border: 0;
	border-block-end: 1px solid var(--zu-line);
	color: var(--zu-ink);
	font-size: var(--zu-text-sm);
	font-style: normal;
	line-height: 1.5;
	vertical-align: top;
}

.woocommerce div.product table.shop_attributes td p,
.woocommerce table.shop_attributes td p {
	margin: 0;
	padding: 0;
}

/* The zebra striping from the plugin fights the theme's quiet surfaces. */
.woocommerce div.product table.shop_attributes tr:nth-child(2n) td,
.woocommerce div.product table.shop_attributes tr:nth-child(2n) th {
	background: none;
}

/*
 * Below 30rem a 12rem label column leaves too little for the value, so label and
 * value stack. The table keeps its semantics; only the visual arrangement changes.
 */
@media (max-width: 30rem) {
	.woocommerce div.product table.shop_attributes th,
	.woocommerce div.product table.shop_attributes td {
		display: block;
		width: auto;
	}

	.woocommerce div.product table.shop_attributes th {
		padding-block-end: 0;
		border-block-end: 0;
	}
}

/* --- SKU and tags --- */

.zu-product-meta {
	display: grid;
	grid-template-columns: 12rem minmax(0, 1fr);
	gap: 0;
	margin-block-start: 0;
}

.zu-product-meta dt {
	padding: var(--zu-space-2) var(--zu-space-2) var(--zu-space-2) 0;
	border-block-end: 1px solid var(--zu-line);
	color: var(--zu-ink-soft);
	font-size: var(--zu-text-sm);
	font-weight: 500;
}

.zu-product-meta dd {
	margin: 0;
	padding: var(--zu-space-2) 0;
	border-block-end: 1px solid var(--zu-line);
	font-size: var(--zu-text-sm);
}

@media (max-width: 30rem) {
	.zu-product-meta {
		grid-template-columns: minmax(0, 1fr);
	}

	.zu-product-meta dt {
		padding-block-end: 0;
		border-block-end: 0;
	}
}

/* ==========================================================================
   8. Related products
   ========================================================================== */

/*
 * The section reuses the storefront grid and card from shop.css, so there is
 * nothing to style but the spacing and the heading.
 */
.woocommerce div.product .related,
.woocommerce div.product .up-sells {
	margin-block-start: var(--zu-space-6);
	clear: both;
}

.woocommerce div.product .related > h2,
.woocommerce div.product .up-sells > h2 {
	margin-block-end: var(--zu-space-3);
	font-size: var(--zu-text-h2);
}

/*
 * The product option migrated from Shoper — "Wybierz kolor" on three products.
 *
 * Only layout lives here. The field's appearance comes from base.css, where `input`
 * carries its border, so the same values have no second source of truth
 * (ARCHITECTURE §4).
 *
 * There is no error state to style. The owner made the field optional, so it cannot
 * refuse an add and never carries `aria-invalid`. Four rules and a comment describing
 * a required field outlived that decision here — the comment even named the exact
 * sentence the decision removed — and the stage 6 frontend review caught them.
 */
.zu-product-options {
	margin-block-end: var(--zu-space-3);
}

.zu-product-options__field {
	margin-block: 0;
}

.zu-product-options__label {
	display: block;
	margin-block-end: var(--zu-space-1);
	font-size: var(--zu-text-sm);
	font-weight: 500;
	color: var(--zu-ink-soft);
}

.zu-product-options__input {
	width: 100%;
	max-width: 22rem;
	min-height: 44px;
}

.zu-product-options__hint {
	display: block;
	margin-block-start: var(--zu-space-1);
	font-size: var(--zu-text-sm);
	color: var(--zu-ink-muted);
}
