/*
Theme Name:        Dairy Suvidha
Theme URI:         https://dairysuvidha.com/
Description:       Block theme for Dairy Suvidha. Look only — all business logic lives in the ds-core plugin.
Author:            QuickRanks
Version:           0.2.9
Requires at least: 7.0
Tested up to:      7.1
Requires PHP:      8.2
License:           Proprietary
Text Domain:       dairysuvidha
*/

/* Task 2 fills this file. It carries only what theme.json cannot express:
 * pseudo-classes theme.json has no slot for (::placeholder, :focus-visible),
 * a min-height tap target theme.json's element styles cannot set, and small
 * component classes (cards, pills, badges) that are not theme.json elements.
 *
 * WP_Theme_JSON::ELEMENTS on this core (7.1) is:
 * link, heading, h1, h2, h3, h4, h5, h6, button, caption, cite, textInput, select
 * — textInput and select map to plain tag/attribute selectors
 * (`textarea, input:where([type=email],[type=number],[type=password],
 * [type=search],[type=text],[type=tel],[type=url])` and `select`), so they
 * exist and would not be silently dropped. They are still styled here rather
 * than in theme.json because the properties this design needs on them —
 * ::placeholder colour, :focus-visible outline, a uniform min-height tap
 * target across mixed selectors — have no theme.json equivalent; theme.json
 * elements only style base/:hover/:focus states, not ::placeholder or
 * :focus-visible. */

/* ---- Reset-ish essentials ------------------------------------------------ */

*, *::before, *::after { box-sizing: border-box; }

/* ---- Long-form prose gets its own, narrower cap ---------------------------
 * FIX ROUND 1 (Task 12), shell alignment: theme.json's contentSize is now
 * 1200px — right for grids, rails and card rows, the layouts the design was
 * actually drawn at, but a real paragraph of running text at 1200px is an
 * unreadable line length (~150+ characters). `core/post-content` always
 * renders with the class `entry-content` (wp-includes/blocks/post-content.php),
 * regardless of which template calls it — the four templates that carry a
 * plain `<!-- wp:post-content /-->` block (page.html's policy/About copy,
 * single-product.html's "Description" accordion, page-locations.html's own
 * hub copy, single-ds_branch.html's per-branch copy) all reach this one rule
 * without needing a second selector. `:not(.alignwide):not(.alignfull)`
 * excludes checkout.html's own `<!-- wp:post-content {"align":"wide"} /-->` —
 * that one block is the real, two-column Checkout block, not running prose,
 * and needs the full shell width, the same reason WooCommerce's own
 * page-checkout.html template marks it wide too.
 *
 * FIX ROUND 2 (Task 12): capping `.entry-content` ITSELF (the first version of
 * this rule) was wrong, not just unverified. `.entry-content`'s own layout
 * type is "flow" (confirmed: its rendered class list carries `is-layout-flow`,
 * not `is-layout-constrained` — `core/post-content` only gets the constrained
 * treatment when a template explicitly opts in), so nothing here needed to
 * fight core's `!important`. But `.entry-content` ITSELF is a direct child of
 * `.ds-main`, which IS constrained — core's own output sets `max-width: 1200px;
 * margin-left: auto !important; margin-right: auto !important;` on it, the
 * same as this page's own <h1>. Capping `.entry-content`'s max-width to 720px
 * left that `!important` auto/auto pair centring a now-narrower box inside the
 * 1200px shell instead of the full viewport — on `/sample-page/` at 1600px
 * the text measured left:440, its own <h1> (no max-width of its own) measured
 * left:200. A round 1 report claimed this was already verified flush-left; it
 * had not been. The actual fix targets `.entry-content`'s own CHILDREN
 * instead: `.entry-content` itself keeps core's normal 1200px/centred box —
 * already correctly positioned, left edge identical to the heading above it —
 * and each direct child (paragraph, list, heading, quote) is capped to 720px
 * with a plain, non-important `margin: 0`, because core's constrained-layout
 * CSS never reaches grandchildren of `.ds-main`, only its own direct children.
 * No `!important` fight needed once the cap sits one level lower than the
 * problem. Verified with getBoundingClientRect() on `/sample-page/`: at
 * 1600px, `.entry-content` and its own <h1> both measure left:200 (identical
 * boxes, core's normal constrained treatment, untouched) and the first <p>
 * inside it ALSO measures left:200, width:720 — flush with the heading, not
 * centred under it. Re-confirmed on `page.html`, `single-ds_branch.html` and
 * `page-locations.html`: heading and first line of body text share a left
 * edge on all three. */
.entry-content:not(.alignwide):not(.alignfull) > :not(.alignwide):not(.alignfull) {
	max-width: 720px;
	margin: 0;
}

/* WordPress renders buttons as <a> and <button>; theme.json cannot centre them. */
.wp-element-button,
.wp-block-button__link,
button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--wp--preset--spacing--ds-8);
	min-height: var(--wp--custom--tap-target);
	text-decoration: none;
}

/* ---- Tap targets (spec §7.1: >= 44px) ------------------------------------ */

a.wp-block-button__link,
button,
input[type="submit"],
input[type="checkbox"],
input[type="radio"],
summary,
.ds-icon-button {
	min-height: var(--wp--custom--tap-target);
	min-width: var(--wp--custom--tap-target);
}

/* ---- Controls: fix the 1.27:1 boundary and the 3.75:1 placeholder -------- */

input[type="text"],
input[type="search"],
input[type="tel"],
input[type="email"],
input[type="number"],
select,
textarea {
	min-height: var(--wp--custom--tap-target);
	padding: 0 var(--wp--preset--spacing--ds-12);
	border: 1px solid var(--wp--preset--color--ds-control-border);
	border-radius: var(--wp--custom--radius--input);
	background: var(--wp--preset--color--ds-surface);
	color: var(--wp--preset--color--ds-ink);
	font: inherit;
}

::placeholder { color: var(--wp--preset--color--ds-muted); opacity: 1; }

:where(a, button, input, select, textarea, summary):focus-visible {
	outline: 2px solid var(--wp--preset--color--ds-primary);
	outline-offset: 2px;
}

/* Deliberate exception, not covered by the :where() rule above: WordPress
 * core ships `:where(.wp-block-search__button-inside .wp-block-search__inside-wrapper)
 * .wp-block-search__input:focus { outline: none }` for the search block. Its
 * ancestor is :where()'d (0 specificity) but `.wp-block-search__input:focus`
 * itself is not, so it carries (0,2,0) — enough to beat our zero-specificity
 * base rule regardless of load order. This rule matches that same (0,2,0)
 * weight and loads after core's inline block styles in the actual page head,
 * so it wins the tie and the field gets a visible ring back on keyboard focus
 * while staying silent on a mouse click, exactly like every other control. */
.wp-block-search__input:focus-visible {
	outline: 2px solid var(--wp--preset--color--ds-primary);
	outline-offset: 2px;
}

/* ---- Skip link: the first thing a keyboard user meets --------------------
 * TASK 12: this rule used to target `.ds-skip-link`, a class no markup
 * anywhere in this theme has ever emitted — dead CSS, matching nothing,
 * confirmed by grepping the whole theme. The skip link a visitor actually
 * gets is WordPress core's own: get_the_block_template_html()
 * (wp-includes/block-template.php) unconditionally injects
 * `<a class="skip-link screen-reader-text" id="wp-skip-link" href="#...">`
 * before every block-theme page and wp-includes/css/wp-block-template-skip-
 * link.css hides/reveals it — confirmed live (Tab once at
 * http://localhost:8081/, "Skip to content" is the first thing announced) —
 * but core's own default :focus colours are a generic grey (#eee/#444), not
 * this theme's. `#wp-skip-link` is the id core's version always carries; an
 * ID selector (specificity 1,0,0) outranks core's own
 * `.skip-link.screen-reader-text:focus` (0,3,0) regardless of load order, so
 * only the colours are overridden here — core's own off-screen/reveal
 * mechanics (clip-path, not this file's old left:-9999px trick) are left
 * alone, because they already work correctly. */
#wp-skip-link:focus {
	background: var(--wp--preset--color--ds-surface);
	color: var(--wp--preset--color--ds-primary);
}

/* ---- Reduced motion: honour it site-wide, not per-component --------------
 * TASK 12: nothing on this site currently animates beyond WordPress core's own
 * default transitions (a details/summary marker, a focus outline snapping in),
 * but that is exactly why this belongs here rather than waiting for the first
 * animation to need it — a global page-load rule any future pattern inherits
 * for free, never a per-effect opt-in someone has to remember. */
@media (prefers-reduced-motion: reduce) {
	*, *::before, *::after {
		animation-duration: .01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: .01ms !important;
		scroll-behavior: auto !important;
	}
}

/* ---- Cards, tiles, pills ------------------------------------------------- */

.ds-card {
	background: var(--wp--preset--color--ds-surface);
	border: 1px solid var(--wp--preset--color--ds-border);
	border-radius: var(--wp--custom--radius--card);
}

.ds-pill {
	display: inline-flex;
	align-items: center;
	border-radius: var(--wp--custom--radius--pill);
	padding: 2px var(--wp--preset--spacing--ds-8);
	font-size: var(--wp--preset--font-size--ds-sm);
	font-weight: 600;
}
.ds-pill--cod    { background: var(--wp--preset--color--ds-cod-bg);    color: var(--wp--preset--color--ds-cod-text); }
.ds-pill--online { background: var(--wp--preset--color--ds-online-bg); color: var(--wp--preset--color--ds-online-text); }

/* Accent is badges only (spec §7.1). */
.ds-badge {
	background: var(--wp--preset--color--ds-accent);
	color: var(--wp--preset--color--ds-accent-text);
	border-radius: var(--wp--custom--radius--pill);
	padding: 2px var(--wp--preset--spacing--ds-8);
	font-size: var(--wp--preset--font-size--ds-sm);
	font-weight: 600;
}

/* WhatsApp: never white text. */
.ds-whatsapp {
	background: var(--wp--preset--color--ds-whatsapp);
	color: var(--wp--preset--color--ds-whatsapp-text);
	border-radius: var(--wp--custom--radius--button);
	font-weight: 600;
}

/* An action that has no branch yet is announced as unavailable, not broken. */
[aria-disabled="true"] { opacity: .55; pointer-events: none; }

/* ---- Header: top strip ---------------------------------------------------
 * D21/D22 (Task 5): mobile is logo + search + cart, no drawer, no account
 * link, no core/navigation. Below 782px the strip keeps only the branch
 * switcher — the COD line is desktop-only chrome — because a child cannot be
 * un-hidden once its ancestor is display:none, so the strip itself must stay
 * visible and only its other content hides.
 *
 * FIX ROUND 1 (Task 12), shell alignment: .ds-topstrip and .ds-masthead
 * (below) are each now two nested groups, not one. The OUTER group
 * (`layout":{"type":"constrained"}`, unchanged class name) is what makes the
 * client's fix work: WordPress adds the `has-global-padding` class to any
 * root-level group whose own layout type is "constrained" (this theme sets
 * `useRootPaddingAwareAlignments: true`), which pads that group left/right by
 * ds-12 without touching its background — padding never shrinks a
 * border-box's background — so the strip's colour still paints edge to edge.
 * A plain "flex" group (what both used to be, directly) never gets that
 * class or that padding at all, which is the exact bug the client reported:
 * the header ran full-bleed with no gutter while every section below it sat
 * inside the ds-12/contentSize-centred column. The INNER group
 * (`.ds-topstrip__inner` / `.ds-masthead__inner`) carries the flex row
 * arrangement ("type":"flex", justifyContent:"space-between") the content
 * actually needs — "constrained" layout has no flex mode of its own, so the
 * row arrangement has to live one level in. Being the outer group's one
 * direct child, the inner group also inherits `max-width: 1200px;
 * margin-inline: auto` from the outer's own constrained-layout output, the
 * same way `.wp-block-columns` does inside the footer's constrained group —
 * so its content starts and ends on the same vertical line as `.ds-hero`,
 * `.ds-catgrid` and everything else, confirmed with getBoundingClientRect()
 * in the task report. */

.ds-branch-switcher {
	background: transparent;
	border: 0;
	padding: 0 var(--wp--preset--spacing--ds-4);
	color: inherit;
	font: inherit;
	cursor: pointer;
}

/* The global :focus-visible rule rings every control in ds-primary, but this
 * button sits ON ds-primary (the top strip's own background) — the ring and
 * the surface it sits on are the identical colour, so a keyboard user tabbing
 * here sees nothing at all, not just a low-contrast ring. ds-surface (white)
 * is the only other colour this strip carries (via ds-strip-text is closer
 * but ds-surface is closer to true white and safer here). The cod-limit span
 * beside it is not focusable, so it needs no equivalent override. */
.ds-branch-switcher:focus-visible {
	outline-color: var(--wp--preset--color--ds-surface);
}

.ds-topstrip .ds-cod-limit { display: none; }

@media (min-width: 782px) {
	.ds-topstrip .ds-cod-limit { display: inline; }
}

/* ---- Header: masthead (logo, search, cart) ------------------------------- */

/* Was `.ds-masthead { gap: ... }` — moved to the inner flex wrapper (see the
 * top-strip comment above): `.ds-masthead` itself is "constrained" now, not
 * "flex", and `gap` does nothing on a block-layout element. */
.ds-masthead__inner { gap: var(--wp--preset--spacing--ds-12); }

/* The mark ships with the theme (Round 2, concern 3): a plain <img>, not
 * wp:site-logo, which reads the custom_logo theme mod that nobody sets on a
 * fresh install. */
.ds-logo {
	display: block;
	flex: 0 0 auto;
	width: 40px;
	height: 40px;
}

.ds-search.wp-block-search {
	flex: 1 1 auto;
	min-width: 0;
}

/* Desktop-only, per spec §7.2's "logo; search; account; cart". */
.ds-account {
	display: none;
	align-items: center;
	min-height: var(--wp--custom--tap-target);
	padding: 0 var(--wp--preset--spacing--ds-8);
	color: var(--wp--preset--color--ds-ink);
	font-size: var(--wp--preset--font-size--ds-sm);
	font-weight: 600;
	text-decoration: none;
	flex: 0 0 auto;
}

@media (min-width: 782px) {
	.ds-account { display: inline-flex; }
}

.ds-cart {
	position: relative;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: 0 0 auto;
	min-width: var(--wp--custom--tap-target);
	min-height: var(--wp--custom--tap-target);
}

.ds-cart__count {
	position: absolute;
	top: 2px;
	right: 2px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 16px;
	height: 16px;
	padding: 0 4px;
	border-radius: var(--wp--custom--radius--pill);
	background: var(--wp--preset--color--ds-accent);
	color: var(--wp--preset--color--ds-accent-text);
	font-size: 11px;
	font-weight: 700;
	line-height: 1;
}

/* The `hidden` attribute already sets display:none; this rule only exists to
 * win over the display:inline-flex above, which shares its specificity. */
.ds-cart__count[hidden] { display: none; }

/* ---- Icons painted by CSS mask, not duplicated inline markup -------------
 * Mirrors the 'pin' and 'cart' path data in inc/icons.php exactly — if either
 * changes there, change it here too. mask-image (not a <use> reference) keeps
 * the part pure markup: no inline <svg>, no sprite request.
 *
 * TASK 12, decided and recorded (deferred minor from Task 5): each data URI
 * below carries `stroke='%23000'` (#000) — a literal hex, in a file whose rule
 * (see the file banner) is tokens only. Left as-is, on purpose, not replaced:
 * inc/icons.php's inline-<svg> twin can write `stroke="currentColor"` because
 * it is real DOM, painted by the browser's normal colour cascade, but a
 * `mask-image` data URI is a static, isolated image — `currentColor` and CSS
 * custom properties do not reach inside it, so *some* opaque stroke value is
 * required there regardless of which one is chosen. The mask operation then
 * keeps only the path's alpha (opaque pixels become the visible shape,
 * transparent ones are cut away) and throws the colour itself away entirely;
 * the pin and cart glyphs' actual, visible colour is `background-color:
 * currentColor` two rules below, which does read the normal cascade. Swapping
 * '#000' for a `var(--wp--preset--color--*)` token would not be wrong exactly,
 * but it would misstate what this value does — it would read as "this icon is
 * ds-ink-coloured," when changing that token would change nothing about how
 * the icon looks, only the (invisible) alpha shape briefly used to cut the
 * mask. Left as a plain, maximally-opaque hex with this comment instead. */

.ds-branch-switcher__pin,
.ds-cart__icon {
	display: inline-block;
	width: 18px;
	height: 18px;
	background-color: currentColor;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: contain;
	mask-size: contain;
}

.ds-branch-switcher__pin {
	-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21s7-6.2 7-11a7 7 0 1 0-14 0c0 4.8 7 11 7 11Z'/%3E%3Ccircle cx='12' cy='10' r='2.5'/%3E%3C/svg%3E");
	mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 21s7-6.2 7-11a7 7 0 1 0-14 0c0 4.8 7 11 7 11Z'/%3E%3Ccircle cx='12' cy='10' r='2.5'/%3E%3C/svg%3E");
}

.ds-cart__icon {
	-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4h2l2.4 10.2a2 2 0 0 0 2 1.6h7.2a2 2 0 0 0 2-1.5L20 8H6'/%3E%3Ccircle cx='10' cy='19' r='1.4'/%3E%3Ccircle cx='17' cy='19' r='1.4'/%3E%3C/svg%3E");
	mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4h2l2.4 10.2a2 2 0 0 0 2 1.6h7.2a2 2 0 0 0 2-1.5L20 8H6'/%3E%3Ccircle cx='10' cy='19' r='1.4'/%3E%3Ccircle cx='17' cy='19' r='1.4'/%3E%3C/svg%3E");
}

/* ---- Category nav (Task 7 pattern; styled here so it drops in unstyled) --
 * Mobile: a horizontally scrollable chip row. Desktop: a plain, wrapped,
 * centred row under the masthead.
 *
 * Round 2 (Task 7): the pattern's real markup is `<nav class="ds-catnav">`
 * wrapping a bare `<ul>` — Task 5 wrote these rules on `.ds-catnav` itself,
 * which was never wrong syntactically, only untestable, because the pattern
 * rendered as nothing (spec'd behaviour of an unregistered pattern) until
 * this task. With a real `<ul>` child, `.ds-catnav { display:flex; list-style:
 * none; ... }` styled the <nav> and left the <ul> at its UA-default
 * block/disc styling, so the row rendered as a bulleted list, not a chip
 * row, at every width — caught in a real browser at 375px, not by any test
 * (parse_blocks / do_blocks tests only check that markup and blocks are
 * well-formed, not how the CSS box model resolves them). The flex/list rules
 * now target the <ul>; `.ds-catnav` itself keeps only the <nav>'s own
 * chrome (background, border). */

.ds-catnav {
	background: var(--wp--preset--color--ds-surface);
	border-bottom: 1px solid var(--wp--preset--color--ds-border);
}

.ds-catnav ul {
	display: flex;
	gap: var(--wp--preset--spacing--ds-8);
	overflow-x: auto;
	-webkit-overflow-scrolling: touch;
	list-style: none;
	margin: 0;
	padding: var(--wp--preset--spacing--ds-8) var(--wp--preset--spacing--ds-12);
}

.ds-catnav li { flex: 0 0 auto; }

.ds-catnav a {
	display: inline-flex;
	align-items: center;
	min-height: var(--wp--custom--tap-target);
	padding: 0 var(--wp--preset--spacing--ds-12);
	border-radius: var(--wp--custom--radius--pill);
	background: var(--wp--preset--color--ds-tint);
	color: var(--wp--preset--color--ds-ink);
	white-space: nowrap;
	text-decoration: none;
}

@media (min-width: 782px) {
	/* FIX ROUND 1 (Task 12), shell alignment: was justify-content:center on
	 * the <ul> alone, which centres the row on its OWN total chip width, not
	 * on the shell — with fewer chips than fit one line that put the first
	 * chip well to the right of the grid's own left edge (measured: 86px in,
	 * against the shell's 40px, on a 1280px viewport). .ds-catnav is plain
	 * markup (`wp:html`), not a real "constrained" group, so it gets none of
	 * theme.json's has-global-padding/max-width machinery automatically —
	 * reproduced here by hand, in the same two layers a real constrained
	 * group uses: the OUTER element (`.ds-catnav`, like `.ds-main`) takes the
	 * ds-12 padding, and the INNER element (the `<ul>`, like `.ds-hero`
	 * inside `.ds-main`) takes the 1200px cap and auto margins, so between
	 * 782px and roughly 1200px (padding alone reaches the shell's own edge,
	 * same as every real constrained section at those widths) and above it
	 * (the auto margins take over) both land on the identical left/right
	 * pixel the rest of the page does at every width — confirmed with
	 * getBoundingClientRect() at 1024px and 1280px in the task report. A
	 * single layer (max-width on the <ul> alone, tried first) is wrong here:
	 * below ~1200px it has no padding of its own left to fall back on, so
	 * the row sits flush against the viewport edge instead of matching the
	 * 12px every other section keeps at that width. */
	.ds-catnav {
		background: transparent;
		border-bottom: 0;
		padding-left: var(--wp--preset--spacing--ds-12);
		padding-right: var(--wp--preset--spacing--ds-12);
	}

	.ds-catnav ul {
		overflow-x: visible;
		flex-wrap: wrap;
		justify-content: flex-start;
		max-width: 1200px;
		margin-inline: auto;
		padding-left: 0;
		padding-right: 0;
	}
}

/* ---- Footer --------------------------------------------------------------
 * ds-muted (2.66:1) and ds-primary (2.32:1) both fail WCAG AA on the
 * #1B2A22 footer background; ds-footer-text (9.84:1) is the only body/link
 * colour used inside .ds-footer. Headings and links carry a global colour
 * from theme.json (ds-ink, ds-primary) that would otherwise win over the
 * inherited footer text colour, so both need an explicit local override —
 * plain type selectors here, not :where(), so the specificity beats
 * theme.json's low-specificity element selectors regardless of load order. */

.ds-footer h1,
.ds-footer h2,
.ds-footer h3,
.ds-footer h4,
.ds-footer h5,
.ds-footer h6 {
	color: var(--wp--preset--color--ds-footer-text);
}

.ds-footer a {
	color: var(--wp--preset--color--ds-footer-text);
}

.ds-footer a:hover,
.ds-footer a:focus {
	color: var(--wp--preset--color--ds-surface);
}

/* TASK 12, deferred from Task 6: the global :focus-visible rule (above) rings
 * every link in ds-primary, but ds-primary on this footer's #1B2A22 background
 * measures 2.32:1 — below WCAG's 3:1 minimum for a non-text indicator, and hard
 * to see even by eye when tabbing through. Same fix shape as
 * .ds-branch-switcher:focus-visible above (a control sitting on a colour too
 * close to the default ring to use it): ds-footer-text, the colour this footer
 * already uses for its own body/link text, measures 9.84:1 here and is the
 * obvious candidate the task named. Plain selector, not :where(), so it beats
 * the global rule's zero-specificity :where() regardless of load order. */
.ds-footer a:focus-visible {
	outline-color: var(--wp--preset--color--ds-footer-text);
}

.ds-footer__logo {
	display: block;
	width: 40px;
	height: 40px;
	margin-bottom: var(--wp--preset--spacing--ds-8);
}

.ds-footer .wp-block-list {
	list-style: none;
	margin: 0;
	padding-left: 0;
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--ds-8);
}

.ds-footer__legal { margin-top: var(--wp--preset--spacing--ds-24); }

/* ---- Branch sheet (Task 6) -------------------------------------------------
 * A real dialog: fixed overlay, scrim behind a panel that rises from the
 * bottom on mobile and centres on desktop. `hidden` (the state the server
 * markup ships in) must still win over the flex layout below it, exactly
 * like .ds-cart__count[hidden] above.
 */

.ds-branch-sheet {
	position: fixed;
	inset: 0;
	z-index: 100;
	display: flex;
	align-items: flex-end;
	justify-content: center;
}

.ds-branch-sheet[hidden] { display: none; }

html.ds-sheet-open { overflow: hidden; }

.ds-branch-sheet__scrim {
	position: absolute;
	inset: 0;
	background: rgba(20, 30, 25, .45);
	border: 0;
	padding: 0;
}

.ds-branch-sheet__panel {
	position: relative;
	width: 100%;
	max-width: 480px;
	max-height: 85vh;
	overflow-y: auto;
	background: var(--wp--preset--color--ds-surface);
	border-radius: var(--wp--custom--radius--sheet) var(--wp--custom--radius--sheet) 0 0;
	padding: var(--wp--preset--spacing--ds-24) var(--wp--preset--spacing--ds-16);
}

@media (min-width: 782px) {
	.ds-branch-sheet { align-items: center; }
	.ds-branch-sheet__panel { border-radius: var(--wp--custom--radius--sheet); }
}

.ds-branch-sheet__handle {
	width: 40px;
	height: 4px;
	margin: 0 auto var(--wp--preset--spacing--ds-16);
	border-radius: var(--wp--custom--radius--pill);
	background: var(--wp--preset--color--ds-border);
}

@media (min-width: 782px) {
	.ds-branch-sheet__handle { display: none; }
}

.ds-branch-sheet__title {
	font-size: var(--wp--preset--font-size--ds-lg);
	margin: 0 0 var(--wp--preset--spacing--ds-4);
}

.ds-branch-sheet__sub {
	color: var(--wp--preset--color--ds-muted);
	margin: 0 0 var(--wp--preset--spacing--ds-16);
}

.ds-branch-sheet__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--ds-8);
}

/* FINAL FIX WAVE: city and state are now one label — "Patna, Bihar", the same
 * words the branch cards and the footer use for the same four branches —
 * rather than a city pinned left and a bare state code pinned right. The
 * comma lives in the state span's own text (branch-switcher.js), so the row
 * has to read as continuous prose: flex-start, and no gap between the two
 * spans, or the comma would float 8px away from the city it belongs to. */
.ds-branch-sheet__row {
	width: 100%;
	min-height: var(--wp--custom--tap-target);
	display: flex;
	align-items: center;
	justify-content: flex-start;
	gap: 0;
	padding: 0 var(--wp--preset--spacing--ds-12);
	border: 1px solid var(--wp--preset--color--ds-border);
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--ds-surface);
	color: var(--wp--preset--color--ds-ink);
	font: inherit;
	text-align: left;
	cursor: pointer;
}

.ds-branch-sheet__row.is-selected {
	border-color: var(--wp--preset--color--ds-primary);
	background: var(--wp--preset--color--ds-tint);
}

.ds-branch-sheet__state {
	color: var(--wp--preset--color--ds-muted);
	font-size: var(--wp--preset--font-size--ds-sm);
}

.ds-branch-sheet__empty {
	padding: var(--wp--preset--spacing--ds-12);
	color: var(--wp--preset--color--ds-muted);
	text-align: center;
}

.ds-branch-sheet__divider {
	display: flex;
	align-items: center;
	gap: var(--wp--preset--spacing--ds-8);
	margin: var(--wp--preset--spacing--ds-16) 0;
	color: var(--wp--preset--color--ds-muted);
	font-size: var(--wp--preset--font-size--ds-sm);
}

.ds-branch-sheet__divider::before,
.ds-branch-sheet__divider::after {
	content: "";
	flex: 1 1 auto;
	height: 1px;
	background: var(--wp--preset--color--ds-border);
}

.ds-branch-sheet__pincode {
	display: flex;
	align-items: center;
	gap: var(--wp--preset--spacing--ds-8);
}

/* The form ships `hidden` until ds-core's quote endpoint exists (Plan 4).
 * Without this, the class selector above and the browser's default
 * `[hidden] { display: none }` tie in specificity and the author rule (this
 * file) wins regardless of load order, unhiding a form that cannot work —
 * exactly the bug .ds-cart__count[hidden] above already guards against. */
.ds-branch-sheet__pincode[hidden] { display: none; }

.ds-branch-sheet__pincode input {
	flex: 1 1 auto;
}

.ds-branch-sheet__close {
	width: 100%;
	margin-top: var(--wp--preset--spacing--ds-16);
	background: transparent;
	border: 1px solid var(--wp--preset--color--ds-border);
	border-radius: var(--wp--custom--radius--button);
	color: var(--wp--preset--color--ds-ink);
}

/* ---- Category grid (Task 7 pattern) --------------------------------------
 * The 3-across tile grid: a circular ds-tint plate behind every label, same
 * count on mobile and desktop per the approved mockups (spec §7.3). */

.ds-catgrid__list {
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: var(--wp--preset--spacing--ds-16);
	list-style: none;
	margin: var(--wp--preset--spacing--ds-16) 0 0;
	padding: 0;
}

.ds-catgrid__tile {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--wp--preset--spacing--ds-8);
	text-align: center;
	text-decoration: none;
}

.ds-catgrid__plate {
	display: block;
	width: 64px;
	height: 64px;
	border-radius: var(--wp--custom--radius--pill);
	background: var(--wp--preset--color--ds-tint);
}

.ds-catgrid__label {
	font-size: var(--wp--preset--font-size--ds-sm);
	font-weight: 600;
	color: var(--wp--preset--color--ds-label);
}

/* ---- Product rail (Task 7 pattern) ----------------------------------------
 * core/query + core/post-template, not woocommerce/product-collection (D9):
 * the WooCommerce block pulls the Interactivity runtime and its router, 27.3
 * KB gzipped, onto any page that uses it, and core/query already respects
 * WooCommerce's catalogue visibility on its own. scroll-snap, never a
 * carousel — there is no JS here and none is added for this. */

.ds-rail__track.wp-block-post-template {
	display: flex;
	gap: var(--wp--preset--spacing--ds-12);
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	-webkit-overflow-scrolling: touch;
	list-style: none;
	margin: var(--wp--preset--spacing--ds-8) 0 0;
	padding: var(--wp--preset--spacing--ds-4) 0 var(--wp--preset--spacing--ds-8);
}

.ds-rail__track.wp-block-post-template > li {
	flex: 0 0 124px;
	scroll-snap-align: start;
}

.ds-rail__image {
	display: block;
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--ds-plate);
	overflow: hidden;
}

.ds-rail__image img {
	display: block;
	width: 100%;
	height: auto;
}

.ds-rail__title {
	margin-top: var(--wp--preset--spacing--ds-8);
}

.ds-rail__title a {
	color: var(--wp--preset--color--ds-ink);
	text-decoration: none;
}

/* ---- Trust band (Task 7 pattern) ------------------------------------------
 * D11: the heading is white (has-ds-surface-color, set on the group in the
 * pattern), not accent — the approved accent-on-green mockup measured
 * 3.85:1 and fails AA; white measures 6.47:1. Accent stays a badge-sized
 * element elsewhere, never body copy on this background. 2x2 cells at every
 * width per the approved mockup.
 *
 * FIX ROUND 2 (Task 12), shell alignment: this used to be `.ds-trust__list`,
 * a class on the <ul> itself — removed on purpose, not merely renamed to
 * `.ds-trust ul` for style. `.ds-trust` carries `wp:ds/cod-limit` as a real
 * inner block, nested three tags deep (div > ul > li > b) with no block
 * comment of its own at the group's top level. WordPress's own layout-
 * support code (wp_render_layout_support_flag(), wp-includes/block-supports/
 * layout.php) has a documented heuristic for exactly this shape: when a
 * group's inner block is buried inside raw HTML, it walks the still-open
 * ancestor tags at the point the inner block begins and hands the group's
 * "is-layout-constrained"/"has-global-padding" classes to the DEEPEST one
 * that carries its own class attribute — on the theory that a block author
 * who added a class to an inner wrapper meant it to be the real content
 * boundary. With `class="ds-trust__list"` on the <ul>, that heuristic landed
 * every layout class on the <ul> instead of `.ds-trust`'s own outer <div> —
 * confirmed by rendering the pattern in isolation and reading the actual
 * output, not just by reasoning about it. The practical effect: the <div>
 * (which carries the background colour) never received has-global-padding,
 * so once it was also marked `align:"full"` its content had nothing capping
 * it to the shell — the <ul> measured edge-to-edge (0/1280 at a 1280px
 * viewport) instead of the 40/1240 every other section's content lands on.
 * A class on the <li> or <b> that contains the block would only move the
 * misidentification one level, not fix it — the actual fix is for nothing
 * between the group's own <div> and the inner block to carry a class at
 * all, so the heuristic's stack walk finds none and falls back to its own
 * documented behaviour: "if there is no block wrapper it won't advance at
 * all, in which case the class names will be added to the first and
 * outermost tag of the block" — exactly the outer <div> this rule needs. */

.ds-trust ul {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: var(--wp--preset--spacing--ds-16);
	list-style: none;
	margin: 0;
	padding: 0;
}

.ds-trust ul li {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--ds-4);
}

.ds-trust ul b {
	font-size: var(--wp--preset--font-size--ds-base);
	font-weight: 800;
}

.ds-trust ul span {
	font-size: var(--wp--preset--font-size--ds-sm);
}

/* ---- Branch cards (Task 7 pattern) ---------------------------------------- */

.ds-branches__list {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: var(--wp--preset--spacing--ds-12);
	list-style: none;
	margin: var(--wp--preset--spacing--ds-16) 0 0;
	padding: 0;
}

@media (min-width: 782px) {
	.ds-branches__list { grid-template-columns: repeat(4, 1fr); }
}

.ds-branches__card {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--wp--preset--spacing--ds-4);
	min-height: var(--wp--custom--tap-target);
	padding: var(--wp--preset--spacing--ds-16);
	border: 1px solid var(--wp--preset--color--ds-border);
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--ds-surface);
	text-align: center;
	text-decoration: none;
}

.ds-branches__icon {
	color: var(--wp--preset--color--ds-primary);
}

.ds-branches__city {
	color: var(--wp--preset--color--ds-ink);
	font-weight: 700;
}

.ds-branches__state {
	color: var(--wp--preset--color--ds-muted);
	font-size: var(--wp--preset--font-size--ds-sm);
}

/* ---- WhatsApp band (Task 7 pattern) ---------------------------------------
 * The band itself is #25D366/#0B2E1C via the ds-whatsapp/ds-whatsapp-text
 * tokens (set on the group in the pattern). The CTA is the surface colour on
 * that green, not .ds-whatsapp (which is for a WhatsApp-coloured control on a
 * *different* background, e.g. the product page's sticky bar) — reusing it
 * here would put green text on a green band. */

.ds-wa-band { text-align: center; }

.ds-wa-band__cta {
	display: inline-flex;
	align-items: center;
	gap: var(--wp--preset--spacing--ds-8);
	min-height: var(--wp--custom--tap-target);
	margin-top: var(--wp--preset--spacing--ds-12);
	padding: 0 var(--wp--preset--spacing--ds-16);
	border-radius: var(--wp--custom--radius--button);
	background: var(--wp--preset--color--ds-surface);
	color: var(--wp--preset--color--ds-whatsapp-text);
	font-weight: 700;
	text-decoration: none;
}

/* ---- FAQ (Task 7 pattern) --------------------------------------------------
 * core/details accordions. The global `summary { min-height/min-width:
 * tap-target }` rule (above) already covers the tap target; this only adds
 * the divider and the open/closed affordance. */

.ds-faq .wp-block-details {
	border-bottom: 1px solid var(--wp--preset--color--ds-border);
	padding: var(--wp--preset--spacing--ds-12) 0;
}

.ds-faq .wp-block-details:first-of-type {
	border-top: 1px solid var(--wp--preset--color--ds-border);
	margin-top: var(--wp--preset--spacing--ds-16);
}

.ds-faq summary {
	font-weight: 700;
	cursor: pointer;
}

.ds-faq p {
	margin: var(--wp--preset--spacing--ds-8) 0 0;
	color: var(--wp--preset--color--ds-muted);
}

/* ---- Homepage hero (Task 8) ------------------------------------------------
 * Background colour and type sizes are set via block attributes
 * (has-ds-tint-background-color, has-ds-2-xl-font-size, has-ds-hero-sub-color);
 * this only adds what theme.json has no slot for: the search field's own max
 * width, so it does not stretch edge-to-edge on desktop like a bare input. The
 * gap above it comes from the theme's global blockGap, same as every other
 * section on this page. */

.ds-hero__search.wp-block-search {
	max-width: 480px;
}

/* ---- Category archive: chip row (Task 8; filter chips added Task 10) ------
 * Task 8 shipped Sort only: "COD available" and "Under ₹1,000" (spec §7.3)
 * both needed a price comparison the theme does not own. ds/catalog-chips
 * (Plan 4 Task 10) is that comparison, so both are in the row now.
 *
 * The two chips reuse .ds-pill's tokens (radius, padding, size) rather than
 * introducing a second pill shape, but are their own class: .ds-pill is an
 * indicative STATEMENT (payment-pill says what a page is, unprompted), and a
 * chip is a TOGGLE the visitor clicks — [aria-pressed] is the state that
 * matters here, which .ds-pill has no concept of. */

.ds-chips {
	align-items: center;
	margin: var(--wp--preset--spacing--ds-16) 0;
}

.ds-chips .wp-block-woocommerce-product-results-count {
	color: var(--wp--preset--color--ds-muted);
	font-size: var(--wp--preset--font-size--ds-sm);
}

.ds-catalog-chips {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--ds-8);
}

.ds-chip {
	display: inline-flex;
	align-items: center;
	border: 1px solid var(--wp--preset--color--ds-border);
	border-radius: var(--wp--custom--radius--pill);
	padding: 2px var(--wp--preset--spacing--ds-8);
	font-size: var(--wp--preset--font-size--ds-sm);
	font-weight: 600;
	color: var(--wp--preset--color--ds-ink);
	background: var(--wp--preset--color--ds-surface);
	text-decoration: none;
}

.ds-chip:hover {
	border-color: var(--wp--preset--color--ds-control-border);
}

/* FIX ROUND 1, item 4: this comment overclaimed. .ds-chip already carries a
 * solid 1px border in its unpressed state above; pressed only changes that
 * border's COLOUR, same as the background and text. The checkmark below is
 * the one actual non-colour cue — content that is simply absent unpressed —
 * and it is what keeps the pressed state readable without relying on colour
 * alone, on top of the cod-limit tokens (the same green .ds-pill--cod uses). */
.ds-chip[aria-pressed="true"] {
	background: var(--wp--preset--color--ds-cod-bg);
	color: var(--wp--preset--color--ds-cod-text);
	border-color: var(--wp--preset--color--ds-cod-text);
}

.ds-chip[aria-pressed="true"]::before {
	content: "✓";
	margin-right: var(--wp--preset--spacing--ds-4);
}

.ds-chips__note {
	font-size: var(--wp--preset--font-size--ds-sm);
	color: var(--wp--preset--color--ds-muted);
}

/* ---- Category archive: product grid (Task 8; tablet step added Task 12) ----
 * Spec §7.3: 2-column grid on mobile. core/post-template renders a plain <ul>;
 * nothing in core or WooCommerce grids it for a query with "inherit":true, so
 * the columns are set here, the same way .ds-rail__track sets its own layout
 * for the Task 7 rail patterns.
 *
 * TASK 12 (Part A): this used to jump straight from 2 columns to 4 at 782px.
 * An iPad in portrait (768px) sits just below that line, so it got the
 * 2-column mobile grid stretched across ~690px of content width instead (that
 * figure is from the 720px shell in place when this was found and fixed —
 * see style.css's shell-alignment section, and the smoke record, for the
 * 1200px shell fix round 1 shipped afterward) — two product photos ballooned
 * to roughly 330px square each, an obviously oversized tile for a catalogue
 * that shows 24 per page. 600px and 1024px (not 782px) give it a real third
 * step, 3 columns, sized for the width a tablet actually has rather than a
 * doubled-up phone tile or a squeezed desktop one; 1024px and 1280px both sit
 * inside the same shell cap (1200px as of fix round 1, was 720px when this
 * comment was first written), so 4 columns there is the same desktop layout
 * already tested in Task 8 at two different absolute widths, not two
 * different layouts — see the shell-alignment section below for what
 * actually differs between those two viewports now. */

.ds-grid__track.wp-block-post-template {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: var(--wp--preset--spacing--ds-16);
	list-style: none;
	margin: var(--wp--preset--spacing--ds-16) 0 0;
	padding: 0;
}

@media (min-width: 600px) {
	.ds-grid__track.wp-block-post-template {
		grid-template-columns: repeat(3, 1fr);
	}
}

@media (min-width: 1024px) {
	.ds-grid__track.wp-block-post-template {
		grid-template-columns: repeat(4, 1fr);
	}
}

.ds-grid__image {
	display: block;
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--ds-plate);
	overflow: hidden;
}

.ds-grid__image img {
	display: block;
	width: 100%;
	height: auto;
}

.ds-grid__title {
	margin-top: var(--wp--preset--spacing--ds-8);
}

.ds-grid__title a {
	color: var(--wp--preset--color--ds-ink);
	text-decoration: none;
}

/* ---- The tile price (final fix wave) ---------------------------------------
 * Shared by the two listing grids (.ds-grid__price) and all three rails
 * (.ds-rail__price), which is why it sits here rather than twice.
 *
 * The research's description of the approved tile is "image plate, 2-line
 * title, bold price", and spec §7.3's "Under Rs 1,000" chip has nothing to
 * filter if no price is on screen. The block is WooCommerce's own
 * `woocommerce/product-price` — the theme reads no price and decides nothing
 * (§4.1); single-product.html has shipped exactly that block since Task 9.
 *
 * The margin is conditional because 47 of the 109 catalogue products have no
 * price yet: woocommerce/product-price still emits its two wrapper divs when
 * get_price_html() is empty (confirmed on fixture 10241 — whitespace only
 * inside, so :empty cannot see it), and an unconditional margin-top would
 * push a gap under those titles for nothing. `.woocommerce-Price-amount` is
 * the wrapper WooCommerce puts around a real amount, sale or not, so its
 * presence is a signal we read, not a price we evaluate — the same test
 * `.ds-price` already uses on the product page. */

.ds-grid__price,
.ds-rail__price {
	font-size: var(--wp--preset--font-size--ds-sm);
	font-weight: 700;
	color: var(--wp--preset--color--ds-ink);
}

.ds-grid__price:has(.woocommerce-Price-amount),
.ds-rail__price:has(.woocommerce-Price-amount) {
	margin-top: var(--wp--preset--spacing--ds-4);
}

/* ---- Category empty state (Task 8) ------------------------------------------
 * Not a corner case: 43 of the cutover's 52 redirects (Plan 2) land on a
 * /products/<category>/ URL while the catalogue is entirely drafts, so every
 * one of them shows exactly this. A designed page, not a bare "no results". */

.ds-empty {
	text-align: center;
	padding: var(--wp--preset--spacing--ds-32) 0;
}

.ds-empty__cta {
	display: inline-flex;
	align-items: center;
	min-height: var(--wp--custom--tap-target);
	margin: var(--wp--preset--spacing--ds-12) 0;
	padding: 0 var(--wp--preset--spacing--ds-16);
}

/* ---- Single product (Task 9) -----------------------------------------------
 * Component order is spec §7.3 line 520 as amended by D2: gallery, title,
 * price, payment pill, Talk card, pincode quote, CTA, trust tiles, then the
 * detail accordions and the sticky bar. Those five containers
 * (.ds-payment-pill, .ds-talk-card, .ds-quote, .ds-cta, .ds-sticky) are no
 * longer written here: Plan 4 Task 7 replaced them with ds-core's ds/*
 * render blocks, which emit the same class and the same data-ds-* name on
 * their own wrapper, so every rule below still applies unchanged. */

.ds-product__media .wp-block-woocommerce-product-image-gallery img {
	display: block;
	width: 100%;
	height: auto;
	border-radius: var(--wp--custom--radius--card);
}

/* TASK 12, Part A (tablet widths): core's own Columns block only ever offers two
 * states — stacked below 782px, side-by-side at or above it (wp-includes/blocks/
 * columns/style.css) — with no tablet step of its own. An iPad in portrait
 * (768px) falls on the stacked side of that line: the gallery photo renders at
 * the page's full ~690px content width and the buy column's copy underneath it
 * (the Talk card's sub-copy, the detail accordions) wraps at that same width —
 * line lengths well past anything comfortable to read, on a device with plenty
 * of width to spare for two columns. 600px is chosen for the same reason the
 * grid breakpoints below use it: nothing this theme tests sits between it and
 * 768px, and the two columns then split evenly the same way core's own ≥782px
 * behaviour already does — this is not a new layout, only an earlier start for
 * the one that was already built and already tested at 1024/1280.
 *
 * Specificity: core's stacking rule is `.wp-block-columns:not(.is-not-stacked-
 * on-mobile) > .wp-block-column { flex-basis: 100% !important }` — three classes
 * deep, !important. `.ds-product .ds-product__columns.wp-block-columns >
 * .wp-block-column` is four classes deep, so it wins outright on specificity;
 * nothing here depends on load order the way the .wp-block-search__input rule
 * above does. */
@media (min-width: 600px) {
	.ds-product .ds-product__columns.wp-block-columns {
		flex-wrap: nowrap !important;
	}

	.ds-product .ds-product__columns.wp-block-columns > .wp-block-column {
		flex-basis: 0;
		flex-grow: 1;
	}
}

/* TASK 11 FIX ROUND 1, finding 1: WooCommerce's own single-product/product-image.php
 * prints the gallery wrapper with an inline `style="opacity: 0; transition: opacity
 * .25s ease-in-out;"` — by design, meant to be cleared to opacity 1 by
 * assets/js/frontend/single-product.js (`this.$target.css('opacity', 1)`) once the
 * gallery script finishes initializing on the client. Task 11 removed all three
 * `wc-product-gallery-zoom/-lightbox/-slider` theme supports (spec §7.1 bans
 * sliders), and ProductImageGallery::enqueue_legacy_assets() only ever enqueues
 * `wc-single-product` when at least one of those three supports is still active —
 * with all three gone, that script (and the opacity clear it performs) never runs,
 * so the gallery wrapper is stuck at opacity 0 forever and every product photo is
 * invisible. Confirmed on fixtures 6823 and 7015 before this rule existed: the
 * image is present in the DOM, permanently transparent. Fixed here in CSS, not by
 * restoring any of the three theme supports back — this project ships no fades
 * anywhere else, and zoom/lightbox/slider are exactly the interactive gallery
 * behaviour the spec bans. `!important` is required because nothing else beats an
 * inline style. DO NOT DELETE THIS AS DEAD-LOOKING CODE: it exists because that
 * inline opacity: 0 exists, and removing it silently makes every product photo
 * disappear again. */
.woocommerce-product-gallery {
	opacity: 1 !important;
}

.ds-price {
	align-items: baseline;
}

/* FIX ROUND 1, finding 4: 47 of 109 products have no price, and
 * woocommerce/product-price still renders its wrapper divs even when
 * get_price_html() is empty (confirmed on fixture 10241's live markup — only
 * whitespace inside .wc-block-components-product-price, so a plain :empty
 * selector cannot see it: whitespace text nodes count as content for
 * :empty). WooCommerce always wraps an actual amount in
 * .woocommerce-Price-amount, sale or not, so its absence is a reliable,
 * non-computed signal that nothing priced rendered — this reacts to what
 * WooCommerce already decided to output, it does not evaluate a price
 * itself. Hides "incl. GST" only in that case, so the page never claims GST
 * inclusion of a price that isn't there. */
.ds-price:not(:has(.woocommerce-Price-amount)) p {
	display: none;
}

/* .ds-payment-pill renders its own sentence server-side now (ds/payment-pill,
 * Plan 4 Task 7) and is always visible. Two containers still ship `hidden`:
 * .ds-quote until Task 8's script binds its form, and .ds-talk-card on a
 * product the COD limit covers — that card is the only branch contact on the
 * page, so it is rendered for every product and revealed by the script when a
 * quote comes back blocked. Neither of those two <div>s carries a competing
 * display rule of its own, and the quote section Task 8 added below keeps it
 * that way deliberately — the layout goes on .ds-quote__form, never on
 * .ds-quote itself.
 *
 * TASK 8 CORRECTION. The sentence that stood here said the browser's own
 * `[hidden] { display: none }` was therefore enough and nothing overrode it.
 * That was true of these two <div>s and false of the set as a whole:
 * buy-flow.js hides and reveals FOUR things — the quote box, the Talk card,
 * the payment pill and the Buy Now button — and Buy Now is a <button>, which
 * this file's own base rule at the top (`.wp-element-button,
 * .wp-block-button__link, button { display: inline-flex }`) gives a display
 * to. An AUTHOR rule beats the browser's whatever the specificity, because
 * author origin outranks user-agent origin. So in the `blocked` state the
 * attribute was set, the accessibility tree was right, and the button was
 * still on screen and still clickable, on an order the quote had just refused.
 * Found by driving that state on fixture 6823, not in review.
 *
 * Every element the script toggles is guarded below, not only the one that was
 * caught: a class-plus-attribute selector outweighs any bare class or type
 * rule in the same origin, so this holds whatever layout a later task gives
 * these containers, and BuyFlowStatesTest keeps one guard per element the
 * script toggles. The Talk card's two actions joined the list in fix round 1,
 * when the script started hiding whichever of them the branch has no number
 * for — and they carry `display: inline-flex` of their own further down, so
 * they are the same defect as Buy Now, not a precaution. Measured with the
 * rule in place: all six compute to display:none and paint no box once
 * `hidden` is set.
 *
 * TASK 9 joins .ds-checkout-notice to the same rule rather than starting a
 * second mechanism. Nothing sets `hidden` on it today — ds-core's
 * checkout-notice/view.js returns null and React unmounts the node instead —
 * but its two modifiers below declare `display: flex`, so it is the sticky
 * bar's case exactly: one `el.hidden = true` away from the defect Buy Now
 * already shipped. Specificity is the ONLY thing carrying this one, and it has
 * to be: the modifiers sit several hundred lines further down this file, so
 * source order is against it. (0,2,0) here against their (0,1,0) settles it.
 * Measured in the browser with the rule in place, on a checkout showing the
 * --cod-blocked row: setting `hidden` took the computed display from flex to
 * none and the box off the page. */
.ds-checkout-notice[hidden],
.ds-quote[hidden],
.ds-talk-card[hidden],
.ds-talk-card__whatsapp[hidden],
.ds-talk-card__call[hidden],
.ds-payment-pill[hidden],
.ds-cta__buy-now[hidden] {
	display: none;
}

.ds-talk-card {
	margin-top: var(--wp--preset--spacing--ds-16);
	padding: var(--wp--preset--spacing--ds-16);
	border: 1px solid var(--wp--preset--color--ds-border);
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--ds-surface);
}

.ds-talk-card__title {
	margin: 0;
	font-weight: 700;
}

.ds-talk-card__sub {
	margin: var(--wp--preset--spacing--ds-4) 0 0;
	color: var(--wp--preset--color--ds-muted);
	font-size: var(--wp--preset--font-size--ds-sm);
}

.ds-talk-card__actions {
	display: flex;
	gap: var(--wp--preset--spacing--ds-8);
	margin: var(--wp--preset--spacing--ds-12) 0 0;
}

/* Both actions ship `role="button" aria-disabled="true"` with no href — the
 * global `[aria-disabled="true"] { opacity: .55; pointer-events: none; }`
 * rule (above) is what actually reads them as unavailable; this only lays
 * them out side by side at equal width so the disabled/live states don't
 * reflow the card. .ds-whatsapp (also above) supplies the WhatsApp action's
 * colour; .ds-talk-card__call is the plain bordered twin for Call. */
.ds-talk-card__whatsapp,
.ds-talk-card__call,
.ds-talk-card__branch {
	flex: 1 1 0;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-height: var(--wp--custom--tap-target);
	padding: 0 var(--wp--preset--spacing--ds-12);
	text-decoration: none;
	font-weight: 600;
	text-align: center;
}

/* .ds-talk-card__branch is the link buy-flow.js puts here in place of the two
 * actions when the quote's branch has published no number at all — a real
 * destination (the branch's own page: address, hours, map) instead of two
 * buttons that cannot be used. Same outline treatment as Call, and never
 * `aria-disabled`, because it always goes somewhere. */
.ds-talk-card__call,
.ds-talk-card__branch {
	border: 1px solid var(--wp--preset--color--ds-border);
	border-radius: var(--wp--custom--radius--button);
	color: var(--wp--preset--color--ds-ink);
}

/* FIX ROUND 1, finding 5: the global [aria-disabled="true"] { opacity: .55 }
 * rule dims a saturated fill (WhatsApp's green) obviously, but this is
 * already a pale outline button on a white card — .55 opacity on near-black
 * text over a light border still reads as an ordinary secondary button, not
 * an unavailable one. Muting its own colours directly (same ds-muted token
 * already used for this card's own secondary text, e.g. .ds-talk-card__sub)
 * makes "unavailable" visible without relying on opacity to carry it alone. */
.ds-talk-card__call[aria-disabled="true"] {
	border-color: var(--wp--preset--color--ds-tint);
	color: var(--wp--preset--color--ds-muted);
}

/* ---- Pincode quote (Task 8) ------------------------------------------------
 * ds/pincode-quote ships this box `hidden` and ds-core's buy-flow.js removes
 * the attribute as it binds the form, so these rules first apply at the moment
 * the box can actually answer. Nothing below gives .ds-quote an unconditional
 * `display` — its only one is the [hidden] guard above the Talk card — so the
 * layout lives on the form. */

.ds-quote {
	margin-top: var(--wp--preset--spacing--ds-16);
}

.ds-quote__form {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--wp--preset--spacing--ds-8);
}

/* Its own full-width line above the controls rather than beside them: the row
 * underneath is already a field plus a button wide on a phone. */
.ds-quote__form label {
	flex: 1 0 100%;
	color: var(--wp--preset--color--ds-label);
	font-size: var(--wp--preset--font-size--ds-sm);
	font-weight: 600;
}

/* Six digits and the field's own padding. It shrinks before the button does,
 * so Check never wraps onto a line of its own. */
.ds-quote__input {
	flex: 1 1 8ch;
	min-width: 0;
}

/* A plain <button> gets no colour from theme.json — that element maps to
 * `.wp-element-button, .wp-block-button__link`, which this one is not — so the
 * whole appearance is set here. Secondary on purpose, the same outline
 * treatment as Buy Now and the Talk card's Call: one page, one primary action,
 * and checking a pincode is not it. */
.ds-quote__form button {
	padding: 0 var(--wp--preset--spacing--ds-16);
	border: 1px solid var(--wp--preset--color--ds-control-border);
	border-radius: var(--wp--custom--radius--button);
	background: var(--wp--preset--color--ds-surface);
	color: var(--wp--preset--color--ds-ink);
	font-weight: 600;
}

/* The one line every answer lands in, and the one rule in this section that is
 * about assistive technology rather than looks.
 *
 * FIX ROUND 1, item 4: this was `:empty { display: none }`, with a comment
 * claiming aria-live worked either way. It does not follow that it does: a
 * live region has to be IN the accessibility tree, empty, before its text
 * arrives, and display:none takes it out of the tree entirely — so the first
 * answer a customer asks for is the one most at risk of never being announced.
 * The claim is withdrawn rather than restated: this has not been tested with a
 * screen reader, and what is done here instead is simply to keep the element
 * rendered at all times and let it take no room while it is empty. An empty
 * block box generates no line box and is zero pixels tall, so only its margin
 * has to go. */
.ds-quote__result {
	margin: var(--wp--preset--spacing--ds-8) 0 0;
	font-size: var(--wp--preset--font-size--ds-sm);
}

.ds-quote__result:empty {
	margin: 0;
}

/* Waiting, and withdrawn: neither is an answer, and both read as the quiet
 * state the line had before anyone asked. */
.ds-quote__result[data-ds-quote-state="checking"],
.ds-quote__result[data-ds-quote-state="stale"] {
	color: var(--wp--preset--color--ds-muted);
}

.ds-quote__result[data-ds-quote-state="quoted"] {
	color: var(--wp--preset--color--ds-label);
}

/* The two answers that change what the customer can do next — the order is
 * blocked for this pincode, or we could not ask at all — get the amber the
 * online-only pill already uses for "there is a condition on this order".
 * A token pair that is already on this page, not a new colour. */
.ds-quote__result[data-ds-quote-state="blocked"],
.ds-quote__result[data-ds-quote-state="error"] {
	padding: var(--wp--preset--spacing--ds-8) var(--wp--preset--spacing--ds-12);
	border-radius: var(--wp--custom--radius--tile);
	background: var(--wp--preset--color--ds-online-bg);
	color: var(--wp--preset--color--ds-online-text);
}

/* ---- CTA: the WooCommerce add-to-cart form ---------------------------------
 * ds/product-cta (ds-core) renders wp:woocommerce/add-to-cart-form, not
 * add-to-cart-with-options — Buy Now is that form's own submission with one
 * extra field (spec §6.2), and the newer block would handle the submit in
 * JavaScript instead. The <form> gets its id from inc/assets.php's
 * render_block filter, so both buttons that live outside it in the DOM —
 * Buy Now here and the sticky bar's — can submit it with
 * `form="ds-add-to-cart"`. */

.ds-cta form.cart {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--ds-12);
	margin-top: var(--wp--preset--spacing--ds-8);
}

.ds-cta .quantity {
	margin: 0;
}

/* Buy Now is the secondary of the two: one page, one primary action, and the
 * one that keeps shopping possible is Add to cart. Same outline treatment as
 * the Talk card's Call action, and `disabled` gets the same "unavailable, not
 * broken" opacity as the sticky button — ds/product-cta ships it disabled
 * when WooCommerce renders no form for it to submit. */
.ds-cta__buy-now {
	margin-top: var(--wp--preset--spacing--ds-8);
	padding: 0 var(--wp--preset--spacing--ds-16);
	border: 1px solid var(--wp--preset--color--ds-primary);
	border-radius: var(--wp--custom--radius--button);
	background: var(--wp--preset--color--ds-surface);
	color: var(--wp--preset--color--ds-primary);
	font-weight: 600;
}

.ds-cta__buy-now:disabled {
	border-color: var(--wp--preset--color--ds-tint);
	color: var(--wp--preset--color--ds-muted);
	opacity: .55;
	cursor: not-allowed;
}

/* The two quoted states (spec §6.2): buy-flow.js relabels this button with the
 * total once a pincode is known, and "Buy Now · Pay on delivery · ₹35,500" is
 * three clauses where the cached page's label is two words. A button is
 * intrinsically sized and will not wrap on its own, so without a max-width it
 * grows straight out of the buy column instead. Measured at a 320px viewport,
 * the narrowest this design targets: the column is 296px and that label is
 * 261px, an eight-digit total 290px — it fits today, with 6px to spare, and a
 * longer cart total or a translated label does not. With these two
 * declarations it wraps to a second line inside the column (measured: 296px
 * wide, 50px tall, no page overflow) rather than overflowing. Scoped to the
 * two quoted states, so the button a cached page serves is exactly the one
 * Task 7 shipped.
 *
 * `blocked` hides this button with the `hidden` attribute, which on a <button>
 * needs the guard rule that sits with the other three in the product-page
 * section above — see the note there for what that costs when it is missing. */
.ds-cta[data-ds-cta-state="cod"] .ds-cta__buy-now,
.ds-cta[data-ds-cta-state="online"] .ds-cta__buy-now {
	max-width: 100%;
	padding-top: var(--wp--preset--spacing--ds-8);
	padding-bottom: var(--wp--preset--spacing--ds-8);
	text-align: center;
	white-space: normal;
}

/* 47 products have no price, so this is the whole CTA for a good part of the
 * catalogue rather than an edge case. */
.ds-cta__no-price {
	margin: var(--wp--preset--spacing--ds-8) 0 0;
	color: var(--wp--preset--color--ds-muted);
}

/* ---- Trust tiles ------------------------------------------------------------ */

.ds-trusttiles p {
	display: inline-flex;
	align-items: center;
	margin: 0;
	padding: var(--wp--preset--spacing--ds-4) var(--wp--preset--spacing--ds-12);
	border-radius: var(--wp--custom--radius--pill);
	background: var(--wp--preset--color--ds-tint);
	color: var(--wp--preset--color--ds-label);
}

/* ---- Product detail accordions: the same treatment as .ds-faq ------------- */

.ds-product__detail .wp-block-details {
	border-bottom: 1px solid var(--wp--preset--color--ds-border);
	padding: var(--wp--preset--spacing--ds-12) 0;
}

.ds-product__detail .wp-block-details:first-of-type {
	border-top: 1px solid var(--wp--preset--color--ds-border);
	margin-top: var(--wp--preset--spacing--ds-16);
}

.ds-product__detail summary {
	font-weight: 700;
	cursor: pointer;
}

.ds-product__detail p {
	margin: var(--wp--preset--spacing--ds-8) 0 0;
	color: var(--wp--preset--color--ds-muted);
}

/* ---- Photo note (inc/images.php) -------------------------------------------
 * Appended to the gallery block's own output when a product carries
 * _ds_needs_photo — a brochure scan standing in for a real product photo. */

.ds-photo-note {
	display: block;
	margin-top: var(--wp--preset--spacing--ds-8);
	color: var(--wp--preset--color--ds-muted);
	font-size: var(--wp--preset--font-size--ds-sm);
	font-style: italic;
}

/* ---- Sticky bar -------------------------------------------------------------
 * Mobile only: below 600px the two-column layout above collapses to one
 * column and the CTA can scroll well out of view, which is exactly what this
 * bar is for. Above 600px the buy column stays on screen beside the gallery,
 * so a second fixed copy of the same button would only be redundant chrome.
 * `.ds-product`'s own bottom padding reserves room for the bar's height so it
 * never sits on top of the page's last section — the same [hidden]-style
 * guard this file uses elsewhere for a fixed/absolute element over real
 * content.
 *
 * TASK 12 (Part A): this threshold must track .ds-product__columns' own
 * breakpoint above exactly — it moved from 782px to 600px in the same task,
 * for the same iPad-portrait reason, and leaving this one behind at 782px
 * would have shown a floating sticky "Add to cart" bar stacked underneath a
 * buy column that is already fully visible on screen at 768px: redundant,
 * confusing chrome the original 782px version of this rule was written
 * specifically to avoid. */

.ds-sticky {
	position: fixed;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 40;
	display: flex;
	align-items: center;
	gap: var(--wp--preset--spacing--ds-8);
	padding: var(--wp--preset--spacing--ds-8) var(--wp--preset--spacing--ds-12);
	background: var(--wp--preset--color--ds-surface);
	border-top: 1px solid var(--wp--preset--color--ds-border);
}

/* FIX ROUND 1, item 7. Nothing sets `hidden` on the sticky bar today, which is
 * the only reason the display above has never shipped a bar that would not
 * hide — the identical defect this file already carries a guard for on Buy Now
 * and the Talk card's actions, where it did ship. Measured before this rule:
 * with `hidden` set, .ds-sticky still computed to display:flex and still
 * painted. Guarded now rather than left for whoever first tries to hide it. */
.ds-sticky[hidden] {
	display: none;
}

.ds-sticky__whatsapp {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-height: var(--wp--custom--tap-target);
	padding: 0 var(--wp--preset--spacing--ds-16);
	text-decoration: none;
}

/* D2: only an item the COD limit covers on its own shows a price here. For a
 * machine the number that matters is the one the branch quotes with freight,
 * so ds/sticky-bar renders no price at all rather than one that would read as
 * the total. */
.ds-sticky__price {
	flex: 0 0 auto;
	font-weight: 700;
}

.ds-sticky__buy {
	flex: 1 1 auto;
	background: var(--wp--preset--color--ds-primary);
	color: var(--wp--preset--color--ds-surface);
	border: 0;
	border-radius: var(--wp--custom--radius--button);
	font-weight: 600;
}

/* ds/sticky-bar (ds-core) ships this button `disabled` outright (not
 * aria-disabled — it is a real <button>) when the product has no add-to-cart
 * form to submit, e.g. no price set or out of stock. Same opacity language as
 * [aria-disabled="true"] elsewhere. */
.ds-sticky__buy:disabled {
	opacity: .55;
	cursor: not-allowed;
}

/* FIX ROUND 1, finding 2: this padding was on .ds-product itself, which ends
 * where <main> ends — the footer (its own template part) is below that, so
 * the fixed sticky bar still covered the footer's last line ("Each branch is
 * an independent firm with its own GSTIN…") at full scroll on a 375px
 * viewport.
 *
 * First attempt at this fix used `.ds-product ~ footer`, which turned out
 * not to match at all: WooCommerce wraps a singular product template's
 * <main> in its own extra `div.woocommerce.product` (for its own `.product`-
 * scoped legacy CSS/JS to keep working in a block theme), so <main> and
 * <footer> are NOT siblings the way the raw template source suggests —
 * confirmed via getBoundingClientRect()/parentElement in the browser, where
 * the "fixed" version still measured 0px on the footer and the legal line's
 * rect still sat inside the sticky bar's rect. `:has()` doesn't care about
 * that extra wrapper: it puts the reserved space on <body> itself whenever
 * .ds-sticky exists anywhere inside it, so it survives WooCommerce's own
 * markup shape and still only applies to the one template that renders the
 * sticky bar. Verified by scrolling to the true bottom (window.scrollY +
 * innerHeight === body.scrollHeight) at 375px and reading the GSTIN line
 * fully clear of the bar (task report, fix round 1). */
body:has(.ds-sticky) {
	padding-bottom: calc(var(--wp--custom--tap-target) + var(--wp--preset--spacing--ds-24));
}

@media (min-width: 600px) {
	.ds-sticky { display: none; }
	body:has(.ds-sticky) { padding-bottom: 0; }
}

/* ---- Checkout: the title WooCommerce's own CSS still narrows -------------
 * FIX ROUND 2 (Task 12), item 2: found while verifying the new
 * templates/checkout.html at 1280px, not assumed. WooCommerce's own
 * woocommerce-blocktheme.css (never dequeued on checkout — see
 * dairysuvidha_is_budgeted_request() in inc/assets.php, which excludes
 * is_checkout() from the budget dequeues on purpose, since checkout needs
 * WooCommerce's real assets) ships
 * `.woocommerce-checkout .wp-block-post-title { max-width: 1000px; }` — a
 * rule written for WooCommerce's own distraction-free page-checkout
 * template, at (0,2,0) specificity. Our checkout page-title is otherwise a
 * plain, unaligned wp:post-title, so it also matches core's own
 * `.is-layout-constrained > :where(...)` rule (effectively (0,1,0), the
 * :where() contributing nothing) — WooCommerce's more specific rule won,
 * measured live: the "Checkout" heading sat at max-width:1000px, left:140
 * at a 1280px viewport, while the real Checkout block directly below it
 * (marked align:"wide") correctly measured 1200px at left:40 — the
 * heading and the page's own content no longer shared a left edge, the
 * exact defect this whole task exists to remove, reintroduced by WooCommerce
 * one component at a time. `.woocommerce-checkout .ds-checkout .wp-block-
 * post-title` is three classes deep, (0,3,0), so it wins outright — no
 * reliance on load order.
 *
 * `max-width: 1200px`, not `none`: an unconstrained title measures wider
 * than the shell, not narrower — confirmed by trying `none` first: at
 * 1280px the heading measured left:12, width:1256 (its full available box,
 * `.ds-checkout`'s own content area minus its own 12px padding), while the
 * real Checkout block below it still measured left:40, width:1200 — still
 * two different edges, just the opposite mismatch. Restating the shell's own
 * 1200px explicitly, the same value the block's `align:"wide"` resolves to,
 * lets the inherited `margin: auto !important` (core's own constrained-
 * layout output, untouched by this rule) centre it inside `.ds-checkout`'s
 * available box exactly the way it centres everything else — confirmed
 * afterward at left:40, width:1200, identical to the Checkout block. */
.woocommerce-checkout .ds-checkout .wp-block-post-title {
	max-width: 1200px;
}

/* ---- Checkout: why cash on delivery is not in the list (Task 9) ----------
 * ds-core's ds/checkout-notice draws one of these two rows below the payment
 * radios, inside WooCommerce's own React checkout. Every class here is written
 * by that plugin's blocks/checkout-notice/view.js, never by a template in this
 * theme, so it is a cross-package contract with nothing to fail loudly if it
 * breaks — CheckoutNoticeStylesTest reads that file and fails when the names
 * drift apart.
 *
 * The plate sits on the two modifiers and never on `.ds-checkout-notice`
 * alone. render.php's shell carries the bare class and no content, so a
 * background and a padding on it would paint an empty grey bar. */
/* The no-JavaScript checkout notice (inc/assets.php). A visitor with scripts on
 * never sees this: it is inside <noscript>, so the markup is not even parsed
 * for them. It borrows the checkout notice's plate rather than inventing a
 * second one, because it is the same kind of thing in the same place -- an
 * explanation where a control should have been. */
.ds-noscript-checkout {
	margin-bottom: var(--wp--preset--spacing--ds-12);
	padding: var(--wp--preset--spacing--ds-12);
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--ds-surface);
}

.ds-checkout-notice--cod-blocked,
.ds-checkout-notice--unpurchasable {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--ds-4);
	margin-top: var(--wp--preset--spacing--ds-12);
	padding: var(--wp--preset--spacing--ds-12);
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--ds-surface);
}

/* Greyed, and only greyed: this row is not a control and not an error. It says
 * that a method the customer cannot see was withheld, and the payment options
 * directly above it are still the live ones. */
.ds-checkout-notice--cod-blocked {
	color: var(--wp--preset--color--ds-muted);
}

.ds-checkout-notice__label {
	font-weight: 600;
}

.ds-checkout-notice__sub {
	font-size: var(--wp--preset--font-size--ds-sm);
}

/* The blocking case is a different claim — nothing in this cart can be ordered
 * for this pincode — so it keeps the page's ordinary ink and takes a border,
 * rather than receding the way the COD row does. */
.ds-checkout-notice--unpurchasable {
	border: 1px solid var(--wp--preset--color--ds-border);
	color: var(--wp--preset--color--ds-ink);
}

.ds-checkout-notice__reason {
	margin: 0;
}

/* .ds-whatsapp (far above) supplies the green; this is the same tap target and
 * centring the Talk card's actions get. It ships only when the branch has a
 * number, with a real href — never `aria-disabled`, because it always goes
 * somewhere when it is there at all. */
.ds-checkout-notice__action {
	align-self: flex-start;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-height: var(--wp--custom--tap-target);
	margin-top: var(--wp--preset--spacing--ds-4);
	padding: 0 var(--wp--preset--spacing--ds-12);
	border-radius: var(--wp--custom--radius--button);
	text-decoration: none;
	font-weight: 600;
}
