/*
 * OSTADEEZ — the single-product page.
 *
 * Enqueued ONLY on `is_product()` (inc/ostadeez-assets.php). It exists so a rule written for the
 * product page cannot reach the archive, the cart or the account page: the shop-wide sheet is a
 * shared surface, and one correct fix there has broken three screens nobody re-opened.
 *
 * Target design: prd/ostadeez/wireframes/pdp-v2/ («کاغذِ گرم», approved 2026-09-20).
 * Layout ownership and hook order: inc/ostadeez-pdp.php.
 *
 * ⚠ NOTHING HERE WRITES A LITERAL COLOUR. Every value is an `--ostz-*` token, per the standard in
 * inc/ostadeez-tokens.php. A hex in this file is a bug.
 */

/* ---------------------------------------------------------------- gallery badges */

/*
 * The badges are drawn INSIDE the gallery element, which is why the gallery needs a positioning
 * context. Before this, the sale flash was a SIBLING of the gallery and its absolute position
 * resolved against the page — measured at y=276 while the image started at y=300, i.e. floating
 * above the picture it was meant to sit on.
 */
/*
 * ⚠ The context is the WRAPPER, not the gallery element. MEASURED: anchored to the gallery the
 * badge landed at y=278 while the photograph started at y=303 — the 25px belong to the zoom
 * trigger, which is a child of the gallery and sits above the image. A badge floating in that gap
 * is the same defect we removed from WooCommerce's own sale flash, just 25px smaller.
 */
.woocommerce div.product div.images .woocommerce-product-gallery__wrapper,
.ostz-pdp .woocommerce-product-gallery__wrapper {
	position: relative;
}

.ostz-pdp-badges {
	position: absolute;
	inset-block-start: var(--ostz-space-3, 12px);
	inset-inline-start: var(--ostz-space-3, 12px);
	z-index: 3;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--ostz-space-2, 8px);
	pointer-events: none;
}

.ostz-pdp-badge {
	border-radius: 999px;
	padding: 5px 12px;
	font-size: var(--ostz-text-xs, 12px);
	font-weight: var(--ostz-weight-black, 900);
	line-height: 1.6;
	white-space: nowrap;
}

.ostz-pdp-badge--sale {
	background: var(--ostz-accent-2);
	color: var(--ostz-accent-ink);
}

.ostz-pdp-badge--out {
	background: var(--ostz-bg-raised-2);
	color: var(--ostz-text-dim);
	border: 1px solid var(--ostz-line-strong);
}

/* ---------------------------------------------------------------- attributes, above the price */

.ostz-attrs {
	margin-block-start: var(--ostz-space-4, 16px);
	border: 1px solid var(--ostz-line);
	border-radius: var(--ostz-radius-lg, 16px);
	background: var(--ostz-bg-sunken);
	overflow: hidden;
}

.ostz-attrs-table {
	width: 100%;
	border-collapse: collapse;
	font-size: var(--ostz-text-sm, 13px);
}

.ostz-attrs-table th,
.ostz-attrs-table td {
	padding: 8px 14px;
	text-align: start;
	border-block-end: 1px solid var(--ostz-line);
	vertical-align: top;
}

.ostz-attrs-table tr:last-child th,
.ostz-attrs-table tr:last-child td {
	border-block-end: 0;
}

.ostz-attrs-table th {
	color: var(--ostz-text-dim);
	font-weight: var(--ostz-weight-bold, 700);
	width: 40%;
	white-space: nowrap;
}

.ostz-attrs-table td {
	color: var(--ostz-text);
	font-weight: var(--ostz-weight-medium, 600);
}

/*
 * The disclosure is a real `<details>`, so the hidden attribute rows are reachable with
 * scripting off. The marker is removed in both spellings — Safari still needs the WebKit
 * pseudo-element, and a stray triangle beside a centred label is the tell that only one was done.
 */
.ostz-attrs-more {
	border-block-start: 1px solid var(--ostz-line);
}

.ostz-attrs-summary {
	list-style: none;
	cursor: pointer;
	padding: 9px 14px;
	min-height: 34px;
	background: var(--ostz-bg-raised);
	color: var(--ostz-accent);
	font-size: var(--ostz-text-xs, 12.5px);
	font-weight: var(--ostz-weight-black, 800);
	text-align: center;
}

.ostz-attrs-summary::-webkit-details-marker {
	display: none;
}

.ostz-attrs-summary::after {
	content: " ▾";
}

.ostz-attrs-more[open] .ostz-attrs-summary::after {
	content: " ▴";
}

.ostz-attrs-summary:hover {
	background: var(--ostz-bg-raised-2);
}

/* ---------------------------------------------------------------- the price box */

.ostz-pricebox {
	margin-block-start: var(--ostz-space-4, 16px);
	border: 1px solid var(--ostz-line-strong);
	border-radius: var(--ostz-radius-lg, 16px);
	background: var(--ostz-bg-raised);
	padding: 16px 18px;
}

/*
 * `min-width: 0` is not decoration: a flex item refuses to shrink below its content's intrinsic
 * minimum, and a long price with the currency word attached is exactly the token that then pins
 * the whole summary column wider than a phone screen.
 */
.ostz-priceline {
	display: flex;
	align-items: baseline;
	flex-wrap: wrap;
	gap: var(--ostz-space-3, 12px);
	min-width: 0;
}

.ostz-price-now {
	font-size: var(--ostz-text-2xl, 30px);
	font-weight: var(--ostz-weight-black, 900);
	letter-spacing: -.5px;
	color: var(--ostz-text);
	min-width: 0;
}

/*
 * WooCommerce prints the amount inside its own spans; they must not keep the default price
 * sizing inside our box, or the figure renders at body size in a container built for a headline.
 */
.ostz-price-now .woocommerce-Price-amount {
	font-size: inherit;
	font-weight: inherit;
	color: inherit;
}

/* ⚠ MEASURED 2026-09-21: `.52em` of the 22px headline rendered the currency word at 11.44px, under
   the 12px floor — and a price is the last place to go small. It is a step from the reference now,
   which also settles the architecture check's objection to a literal here: a relative size is a
   value nobody can reason about from the type scale. */
.ostz-price-now .woocommerce-Price-currencySymbol {
	font-size: var(--ostz-text-sm);
	font-weight: var(--ostz-weight-bold, 700);
	color: var(--ostz-text-dim);
	margin-inline-start: 6px;
}

/*
 * A variable product falls back to WooCommerce's own price HTML, which may be a `del`/`ins`
 * RANGE. Inside our box it must still read as one line, not as two stacked paragraphs.
 */
.ostz-price-now .price,
.ostz-price-now del,
.ostz-price-now ins {
	display: inline;
	background: none;
	text-decoration: none;
	font-size: inherit;
}

.ostz-price-now del {
	font-size: .55em;
	color: var(--ostz-text-faint);
	text-decoration: line-through;
	margin-inline-end: 8px;
}

.ostz-price-was {
	font-size: var(--ostz-text-md, 16px);
	color: var(--ostz-text-faint);
	text-decoration: line-through;
	white-space: nowrap;
}

.ostz-price-was .woocommerce-Price-amount {
	font-size: inherit;
	color: inherit;
}

.ostz-price-off {
	background: var(--ostz-accent-2);
	color: var(--ostz-accent-ink);
	font-size: var(--ostz-text-sm, 13px);
	font-weight: var(--ostz-weight-black, 900);
	border-radius: 8px;
	padding: 3px 10px;
	white-space: nowrap;
}

.ostz-price-save {
	margin-block-start: 6px;
	font-size: var(--ostz-text-xs, 12.5px);
	font-weight: var(--ostz-weight-bold, 700);
	color: var(--ostz-accent-2);
}

/* ---------------------------------------------------------------- gallery proportion */

/*
 * MEASURED 2026-09-20: a 600x750 photograph rendered 603x603. WooCommerce's cropping settings
 * had squared the container while the file stayed 4:5, so every product image was stretched.
 * Fixing it in CSS rather than by re-cropping keeps the original files intact and works for
 * images already in the library.
 */
.woocommerce div.product div.images img,
.ostz-pdp .woocommerce-product-gallery__image img {
	aspect-ratio: 4 / 5;
	object-fit: cover;
	width: 100%;
	height: auto;
	border-radius: var(--ostz-radius-lg, 16px);
}

.woocommerce div.product div.images .flex-control-thumbs img,
.ostz-pdp .flex-control-thumbs img {
	aspect-ratio: 4 / 5;
	border-radius: var(--ostz-radius, 12px);
}

/* ---------------------------------------------------------------- the sticky buy bar */

/*
 * MOVED here from ostadeez-shop.css on 2026-09-20, whole. It is a product-page component and it
 * was being declared in a sheet that also reaches the archive, the cart and the account page.
 *
 * ⚠ It no longer clears the footer navigation bar, because on a product page that bar does not
 * render at all (inc/ostadeez-footerbar.php). The old `inset-block-end: 56px` left a gap once the
 * thing it was avoiding was gone — an offset always has to name what it is avoiding, or it
 * outlives it.
 */
.ostz-buybar {
	position: fixed;
	inset-inline: 0;
	inset-block-end: 0;
	z-index: 100;
	display: none;
	flex-direction: column;
	gap: 6px;
	padding: 8px var(--ostz-space-4, 16px);
	padding-block-end: calc(8px + env(safe-area-inset-bottom, 0px));
	border-block-start: 1px solid var(--ostz-line-strong);
	background: var(--ostz-bg);
	box-shadow: 0 -8px 24px -18px rgba(0, 0, 0, .5);
}

@media (max-width: 800px) {

	.ostz-buybar {
		display: flex;
	}

	/*
	 * The page needs room under the bar, or the last section sits behind it and cannot be read.
	 *
	 * ⚠ The reserve is MEASURED, not guessed: the bar grows with the number of variation axes, so
	 * a product with colour AND size is ~50px taller than a simple one. A fixed value was wrong for
	 * both — 116px against a measured 162px left the footer covered. The script writes the real
	 * height into the custom property; the literal here is only the no-JS fallback, and it is
	 * deliberately generous because being too short hides content while being too long shows a gap.
	 */
	body.single-product {
		padding-block-end: var(--ostz-buybar-h, 180px);
	}
}

.ostz-buybar-row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--ostz-space-3, 12px);
	min-width: 0;
}

.ostz-buybar-info {
	display: flex;
	flex-direction: column;
	min-width: 0;
}

.ostz-buybar-price {
	font-size: var(--ostz-text-md, 16px);
	font-weight: var(--ostz-weight-black, 900);
	unicode-bidi: isolate;
	white-space: nowrap;
}

/* WooCommerce's own price markup lands in here when a variation is chosen. */
.ostz-buybar-price .price,
.ostz-buybar-price del {
	font-size: inherit;
}

/* ⚠ MEASURED 2026-09-22, and it took a seeded discount to see it at all: `.75em` of the buy-bar's
   15px rendered the struck-through OLD price at 11.25px on a phone — under the 12px floor, on the
   one line of the page that tells the customer how much they are saving. It stayed invisible for
   as long as it did because no product on the rig had a sale price, so the `del` never rendered:
   the rule was there, the element was not.
   It is the `xs` step now, which also removes one of the architecture check's literal values. */
.ostz-buybar-price del {
	font-size: var(--ostz-text-xs, 12px);
	color: var(--ostz-text-faint);
	margin-inline-end: 6px;
}

.ostz-buybar-chosen {
	font-size: var(--ostz-text-xs, 12px);
	color: var(--ostz-text-dim);
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.ostz-buybar-cta {
	flex: 0 0 auto;
	min-height: 44px;
	padding-inline: 18px;
}

/* the mirrored variation pickers */

.ostz-buybar-axis {
	display: flex;
	align-items: center;
	gap: 6px;
	overflow-x: auto;
	padding-block-end: 2px;
	min-width: 0;
}

.ostz-buybar-opt {
	flex: 0 0 auto;
	min-width: 40px;
	min-height: 30px;
	padding-inline: 10px;
	border: 1px solid var(--ostz-line-strong);
	border-radius: var(--ostz-radius, 12px);
	background: var(--ostz-bg-raised);
	color: var(--ostz-text-dim);
	font-family: inherit;
	font-size: var(--ostz-text-sm, 13px);
	font-weight: var(--ostz-weight-bold, 800);
	cursor: pointer;
}

.ostz-buybar-opt[aria-pressed="true"] {
	background: var(--ostz-accent);
	color: var(--ostz-accent-ink);
	border-color: transparent;
}

/*
 * An option the current combination cannot reach is disabled, not removed — «not in this colour»
 * and «we do not sell this size» are different sentences and the shopper must be able to tell
 * them apart. The strike is the non-colour cue, so the state does not depend on colour alone.
 */
.ostz-buybar-opt[disabled] {
	cursor: not-allowed;
	opacity: .45;
	text-decoration: line-through;
}

/* ---------------------------------------------------------------- the cart plugin's float button */

/*
 * MEASURED 2026-09-20 at 375px: the cart drawer's floating button (55x55, z-index 99990) sat ON
 * the buy bar's call to action — the bar is z-index 100, so the bubble was both covering the
 * button and receiving its taps. The primary action of the whole page was partly unreachable on
 * a phone, and nothing errored.
 *
 * ⚠ The PLUGIN is not modified — it ships to other sites and is updated centrally. This is the
 * theme positioning a surface it does not own, scoped to the product page only, which is where
 * the collision exists. The bar is also lifted above the bubble so a tap that lands on the
 * overlap still reaches the button rather than the drawer.
 */
.ostz-buybar {
	z-index: 99991;
}

@media (max-width: 800px) {

	body.single-product .dm-scm-float-btn {
		inset-block-end: calc(var(--ostz-buybar-h, 180px) + 12px);
	}
}

/*
 * ⚠ The sticky CTA is the page's PRIMARY action and must look like one.
 *
 * MEASURED: it was rendering with `--ostz-bg-raised-2` — the theme's SECONDARY button ground —
 * because `.ostz-btn` alone is the quiet variant and the markup never asked for `--primary`.
 * A secondary-looking button in the one place a phone user is meant to press is not a styling
 * nit; it is the page's whole purpose rendered as an afterthought.
 *
 * The declaration here is a safety net for the case where the module sheet's own
 * `.ostz a.button.ostz-btn--primary` selector does not match (this element is not a WooCommerce
 * `.button`), so the state cannot depend on which sheet won.
 */
.ostz-buybar-cta.ostz-btn--primary {
	background: var(--ostz-accent);
	color: var(--ostz-accent-ink);
	border-color: transparent;
}

/* ---------------------------------------------------------------- variation swatches (plugin-rendered) */

/*
 * The swatches are rendered by the variations plugin, in PHP. The PLUGIN IS NOT MODIFIED — it
 * ships to other sites and is updated centrally — so the theme dresses its markup in our tokens
 * here, exactly as it does for every other plugin-drawn surface.
 *
 * Goal: the control keeps the look it already had when the theme was building it itself. What
 * changed is WHO builds it, not what it looks like.
 */
.dm-pv-swatches {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--ostz-space-2, 8px);
	margin-block: 6px 2px;
	min-width: 0;
}

/* colour chips */

.dm-pv-type-color .dm-pv-swatch {
	width: 34px;
	height: 34px;
	border-radius: 50%;
	padding: 0;
	border: 2px solid var(--ostz-line-strong);
	background-clip: padding-box;
	cursor: pointer;
	transition: border-color .18s ease, transform .15s ease, box-shadow .18s ease;
}

.dm-pv-type-color .dm-pv-swatch:hover {
	transform: translateY(-1px);
}

/* text chips (sizes) */

.dm-pv-type-label .dm-pv-swatch,
.dm-pv-type-text .dm-pv-swatch {
	min-width: 48px;
	min-height: 42px;
	padding-inline: 14px;
	border: 1px solid var(--ostz-line-strong);
	border-radius: var(--ostz-radius, 12px);
	background: var(--ostz-bg-raised);
	color: var(--ostz-text-dim);
	font-family: inherit;
	font-size: var(--ostz-text-sm, 14px);
	font-weight: var(--ostz-weight-bold, 800);
	cursor: pointer;
	transition: border-color .18s ease, background-color .18s ease, color .18s ease;
}

.dm-pv-type-label .dm-pv-swatch:hover,
.dm-pv-type-text .dm-pv-swatch:hover {
	border-color: var(--ostz-text-faint);
	color: var(--ostz-text);
}

/*
 * ⚠ The selected state is matched on EVERY spelling the plugin might use, because which class it
 * sets is the plugin's business and it may change in an update we do not control. A selected
 * swatch that renders identically to an unselected one is a control with no state, and it is the
 * single most likely thing to break the next time this plugin is updated.
 */
.dm-pv-swatch.selected,
.dm-pv-swatch.is-selected,
.dm-pv-swatch.active,
.dm-pv-swatch.is-active,
.dm-pv-swatch[aria-checked="true"],
.dm-pv-swatch[aria-pressed="true"] {
	border-color: var(--ostz-accent);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--ostz-accent) 18%, transparent);
}

.dm-pv-type-label .dm-pv-swatch.selected,
.dm-pv-type-label .dm-pv-swatch.is-selected,
.dm-pv-type-label .dm-pv-swatch.active,
.dm-pv-type-label .dm-pv-swatch.is-active,
.dm-pv-type-label .dm-pv-swatch[aria-checked="true"] {
	background: var(--ostz-accent);
	color: var(--ostz-accent-ink);
	border-color: transparent;
}

/* unavailable — strike as well as fade, so the state is not carried by colour alone */
.dm-pv-swatch.disabled,
.dm-pv-swatch.is-disabled,
.dm-pv-swatch[disabled],
.dm-pv-swatch[aria-disabled="true"] {
	opacity: .45;
	cursor: not-allowed;
	text-decoration: line-through;
}

.dm-pv-swatch:focus-visible {
	outline: 2px solid var(--ostz-accent);
	outline-offset: 3px;
}

/* ---------------------------------------------------------------- out of stock */

/*
 * WooCommerce's own stock line, given the design's voice. It is the ONE sentence in the summary
 * that states availability: the theme's own «تمام شد» pill was retired on 2026-09-20 because the
 * page was saying the same thing three times (badge, this line, and the pill) in three different
 * wordings, which reads as three different facts.
 */
.ostz-pdp .stock.out-of-stock {
	display: flex;
	align-items: center;
	gap: 8px;
	margin-block: var(--ostz-space-4, 16px) 0;
	padding: 10px 14px;
	border: 1px solid var(--ostz-line-strong);
	border-radius: var(--ostz-radius, 12px);
	background: var(--ostz-bg-sunken);
	color: var(--ostz-text-dim);
	font-size: var(--ostz-text-sm, 13px);
	font-weight: var(--ostz-weight-bold, 700);
}

.ostz-pdp .stock.out-of-stock::before {
	content: "";
	flex: 0 0 auto;
	width: 8px;
	height: 8px;
	border-radius: 50%;
	background: var(--ostz-text-faint);
}

.ostz-pdp .stock.in-stock {
	color: var(--ostz-accent);
	font-weight: var(--ostz-weight-bold, 700);
}

/*
 * The «notify me» control, drawn by the engagement plugin.
 *
 * ⚠ THE PLUGIN IS NOT MODIFIED. The theme gives its button the weight the design asks for,
 * because on an out-of-stock product this is the ONLY action on the page — it is that page's
 * primary control, and it was rendering as a small secondary chip.
 */
.ostz-pdp .dm-waitlist-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	width: 100%;
	min-height: 52px;
	margin-block-start: var(--ostz-space-3, 12px);
	padding-inline: 20px;
	border: 0;
	border-radius: var(--ostz-radius, 12px);
	background: var(--ostz-accent);
	color: var(--ostz-accent-ink);
	font-family: inherit;
	font-size: var(--ostz-text-md, 16px);
	font-weight: var(--ostz-weight-black, 900);
	cursor: pointer;
	transition: filter .18s ease, transform .15s ease;
}

.ostz-pdp .dm-waitlist-btn:hover {
	filter: brightness(1.07);
}

.ostz-pdp .dm-waitlist-btn:active {
	transform: scale(.99);
}

/*
 * ⚠ The ALREADY-REGISTERED state must not look like the invitation, or the shopper presses it
 * again and cannot tell whether anything happened. The plugin swaps the label and adds `.active`;
 * the theme makes that difference visible instead of leaving two states one colour.
 */
.ostz-pdp .dm-waitlist-btn.active {
	background: var(--ostz-bg-sunken);
	color: var(--ostz-text-dim);
	border: 1px solid var(--ostz-line-strong);
	font-weight: var(--ostz-weight-bold, 700);
}

/* the bar's notify action once the shopper is already on the list */
.ostz-buybar-cta--done {
	background: var(--ostz-bg-sunken);
	color: var(--ostz-text-dim);
	border: 1px solid var(--ostz-line-strong);
	font-weight: var(--ostz-weight-bold, 700);
}

/* ---------------------------------------------------------------- short description */

/*
 * The buyer's bullet list. WooCommerce prints the product's SHORT DESCRIPTION here, whatever
 * shape the owner wrote it in.
 *
 * ⚠ The theme does NOT invent structure. If the owner writes a list, it renders as the design's
 * ticked checklist; if they write a paragraph, it renders as a readable lead. Faking bullets out
 * of a sentence would put words on the page that nobody wrote, and the design's bullets are a
 * CONTENT decision — see the note in the build log for what to tell the owner.
 */
.ostz-pdp .woocommerce-product-details__short-description {
	margin-block-start: var(--ostz-space-4, 16px);
	border: 1px solid var(--ostz-line);
	border-radius: var(--ostz-radius-lg, 16px);
	background: var(--ostz-bg-raised);
	padding: 14px 16px;
	color: var(--ostz-text-dim);
	font-size: var(--ostz-text-sm, 13.5px);
}

.ostz-pdp .woocommerce-product-details__short-description > *:first-child {
	margin-block-start: 0;
}

.ostz-pdp .woocommerce-product-details__short-description > *:last-child {
	margin-block-end: 0;
}

.ostz-pdp .woocommerce-product-details__short-description ul {
	margin: 0;
	padding: 0;
	list-style: none;
}

.ostz-pdp .woocommerce-product-details__short-description li {
	display: flex;
	gap: 10px;
	align-items: flex-start;
	padding-block: 4px;
	min-width: 0;
}

/*
 * The tick is a pseudo-element, so it is decoration and never reaches a screen reader as a
 * word. A literal «✓» typed into each list item would be read aloud on every line.
 */
.ostz-pdp .woocommerce-product-details__short-description li::before {
	content: "✓";
	flex: 0 0 auto;
	color: var(--ostz-accent);
	font-weight: var(--ostz-weight-black, 900);
}

.ostz-pdp .woocommerce-product-details__short-description strong,
.ostz-pdp .woocommerce-product-details__short-description b {
	color: var(--ostz-text);
}

/* ---------------------------------------------------------------- the trust row */

/*
 * Three short promises, side by side on a wide screen and stacked on a phone. It was rendering
 * as a plain stacked list 118px tall; as a row it costs a third of that and reads as a group of
 * facts rather than a paragraph.
 */
.ostz-pdp .ostz-assure {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: var(--ostz-space-2, 10px);
	margin-block-start: var(--ostz-space-3, 12px);
	padding: 0;
	list-style: none;
}

@media (max-width: 720px) {

	.ostz-pdp .ostz-assure {
		grid-template-columns: 1fr;
	}
}

.ostz-pdp .ostz-assure-item {
	display: flex;
	gap: 8px;
	align-items: flex-start;
	min-width: 0;
	margin: 0;
	padding: 10px 12px;
	border: 1px solid var(--ostz-line);
	border-radius: var(--ostz-radius, 12px);
	background: var(--ostz-bg-sunken);
	color: var(--ostz-text-dim);
	font-size: var(--ostz-text-xs, 12.5px);
	line-height: 1.7;
}

.ostz-pdp .ostz-assure-check {
	flex: 0 0 auto;
	color: var(--ostz-accent);
	font-weight: var(--ostz-weight-black, 900);
}

/* ---------------------------------------------------------------- sections (was: tabs) */

.ostz-pdp-section {
	margin-block-start: var(--ostz-space-6, 32px);
	padding-block-start: var(--ostz-space-5, 24px);
	border-block-start: 1px solid var(--ostz-line);
}

.ostz-pdp-section-title,
.ostz-pdp-section-body > h2:first-child {
	margin: 0 0 var(--ostz-space-4, 16px);
	font-size: var(--ostz-text-xl, 21px);
	font-weight: var(--ostz-weight-black, 900);
	color: var(--ostz-text);
}

/*
 * The reading measure applies to PROSE only. It is deliberately NOT on the section wrapper: the
 * reviews section holds a form and a list, and a 68ch cap on those makes a form column on a wide
 * screen that looks broken rather than readable.
 */
.ostz-pdp-section-body > p,
.ostz-pdp-section-body > ul,
.ostz-pdp-section-body > ol {
	max-width: 68ch;
	color: var(--ostz-text-dim);
}

/* ---------------------------------------------------------------- related & up-sell grid

   ⚠ WHAT WAS HERE, because it is the most expensive thing this migration found. This section
   declared a COMPLETE SECOND PRODUCT CARD on `.related .product` / `.upsells .product` — border,
   radius, background, hover — on the `<li>` that already contains `<article class="ostz-card">`
   with its own border and background. Every related and up-sell card on a product page rendered
   as TWO NESTED FRAMES. Its own header said it was scoped «on purpose» to avoid a shared-surface
   mistake, which was true of the intent and false of the result: the surface was already shared,
   the scope just hid the second copy from anyone reading the card's real home.
   Most of it also styled `.woocommerce-loop-product__title`, a class this theme REMOVES, so it
   matched nothing at all. The card is now `modules/ostadeez-m-product-card.css`, and the quiet
   button this page wants is its `--quiet` variant.

   What legitimately belongs to the product page is the GRID, and only the grid.

   ⚠ `auto-fit`, not a fixed count, and the track is CAPPED. Related products are a SHORT list —
   often two or three. A fixed four-column grid left empty columns and the section read as a
   failed load; `auto-fill` creates those tracks anyway and merely declines to fill them
   (measured: five 211px tracks for one up-sell). `auto-fit` collapses them — but then hands ALL
   the freed space to what is left, so one up-sell measured the full 1128px and the card became a
   banner. A `minmax` with a real maximum keeps a card the size of a card whether there are one
   or four, and `justify-content: start` puts them where the reader starts.

   ⚠ This rule existed in `ostadeez-shop.css` and NEVER APPLIED, twice over: this sheet declared
   `repeat(4, …)` for the same selector and prints after it, and the comment above that rule had
   a stray paragraph past its own closing marker, which merged into the selector and killed it.
   (⚠ And writing that marker out as a literal inside THIS comment closed it early and did the
   very same thing — caught on the rig, one minute after the rule failed to apply.)
*/
.ostz.single-product .related ul.products,
.ostz.single-product .upsells ul.products {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(200px, 268px));
	justify-content: start;
	gap: var(--ostz-space-4);
	margin: 0;
	padding: 0;
	list-style: none;
}

@media (max-width: 520px) {
	.ostz.single-product .related ul.products,
	.ostz.single-product .upsells ul.products {
		grid-template-columns: 1fr;
	}
}

/* ---------------------------------------------------------------- the offer countdown */

/*
 * The countdown is drawn by the offers plugin, inside our price box. The plugin ships its own
 * colours; the theme hands its surfaces our tokens, the same way it does for every other
 * plugin-drawn surface. THE PLUGIN IS NOT MODIFIED.
 */
.ostz-offer {
	margin-block-start: var(--ostz-space-3, 14px);
	padding-block-start: var(--ostz-space-3, 14px);
	border-block-start: 1px dashed var(--ostz-line-strong);
}

/* the plugin wraps in a grid column class; it must not force a grid width inside our box */
.ostz-offer .col-12 {
	width: auto;
	max-width: none;
	padding: 0;
	margin: 0;
	flex: none;
}

.ostz-offer .amazing-timer {
	display: flex;
	flex-direction: column;
	gap: var(--ostz-space-2, 10px);
	min-width: 0;
	background: none;
	border: 0;
	padding: 0;
}

.ostz-offer .amazing-progress-bar-container {
	height: 6px;
	border-radius: 999px;
	background: var(--ostz-bg-sunken);
	overflow: hidden;
}

.ostz-offer .amazing-progress-bar-fill {
	height: 100%;
	border-radius: 999px;
	background: var(--ostz-accent-2);
}

.ostz-offer .dm-amazing-offer-countdown {
	display: flex;
	align-items: center;
	gap: 6px;
	flex-wrap: wrap;
	min-width: 0;
	color: var(--ostz-text-dim);
	font-size: var(--ostz-text-xs, 12.5px);
	font-weight: var(--ostz-weight-bold, 700);
}

.ostz-offer .countdown-segment {
	display: inline-flex;
	flex-direction: column;
	align-items: center;
	gap: 2px;
	min-width: 48px;
	padding: 6px 6px;
	border: 1px solid var(--ostz-line-strong);
	border-radius: var(--ostz-radius-sm, 10px);
	background: var(--ostz-bg-sunken);
}

.ostz-offer .countdown-segment .value {
	font-size: var(--ostz-text-lg, 19px);
	font-weight: var(--ostz-weight-black, 900);
	color: var(--ostz-text);
	line-height: 1.3;
	font-variant-numeric: tabular-nums;
}

.ostz-offer .countdown-segment .label {
	font-size: var(--ostz-text-xs, 11px);
	font-weight: var(--ostz-weight-medium, 600);
	color: var(--ostz-text-faint);
}

/* ---------------------------------------------------------------- the shop's promises */

/*
 * Rows the owner writes in the badge screen and assigns to categories. THE PLUGIN IS NOT
 * MODIFIED — the theme places its `column` layout here and dresses it in our tokens, exactly as
 * it does for the countdown and the notify button.
 */
.ostz-promises {
	margin-block-start: var(--ostz-space-3, 12px);
	border: 1px solid var(--ostz-line);
	border-radius: var(--ostz-radius-lg, 16px);
	background: var(--ostz-bg-sunken);
	padding: 10px 12px;
}

.ostz-promises .dm-badge-inline {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: 2px;
	min-width: 0;
}

/*
 * ⚠ The plugin draws a BADGE — a pill with its own ground and shadow, built to sit on a
 * photograph. In a list of promises that reads as four buttons. Here each row is flattened to a
 * line of text with its icon, which is what the design asks for, and the plugin keeps its own
 * appearance everywhere else it is used.
 */
.ostz-promises .dm-badge-badge {
	display: flex;
	align-items: center;
	gap: 9px;
	width: 100%;
	min-width: 0;
	position: static;
	transform: none;
	box-shadow: none;
	background: none;
	border: 0;
	border-radius: 0;
	padding: 5px 0;
	color: var(--ostz-text-dim);
	font-size: var(--ostz-text-sm, 13px);
	font-weight: var(--ostz-weight-medium, 600);
	line-height: 1.8;
	text-align: start;
}

.ostz-promises .dm-badge-badge-ico {
	flex: 0 0 auto;
	width: 18px;
	height: 18px;
	color: var(--ostz-accent);
}

.ostz-promises .dm-badge-badge-text {
	min-width: 0;
}

/*
 * ⚠ THE THIRD TIME THIS PATTERN HAS BITTEN on this project, so it is written down here.
 *
 * The offers plugin prints its countdown colours INLINE in the page as well as in its stylesheet.
 * An inline <style> block loads after every enqueued sheet, so at EQUAL specificity the plugin
 * always wins — our `.ostz-offer .countdown-segment .value` (0-3-0) lost to its
 * `.amazing-timer .dm-amazing-offer-countdown .value` (0-3-0), and the figures rendered in the
 * plugin's blue #3e68ff, which measures 4.16 on our warm ground. Under the floor, and the numbers
 * are the whole point of a countdown.
 *
 * The lesson, for the next plugin surface: never rely on load order against a plugin — match its
 * selector shape and add one level inside our own scope.
 */
.ostz-pdp .ostz-offer .amazing-timer .dm-amazing-offer-countdown .value {
	/* stylelint-disable-next-line declaration-no-important -- see the note above. */
	color: var(--ostz-text) !important;
}

.ostz-pdp .ostz-offer .amazing-timer .dm-amazing-offer-countdown .label {
	/* stylelint-disable-next-line declaration-no-important -- see the note above. */
	color: var(--ostz-text-dim) !important;
}
