/* shop.css
   Reconciled onto the site's design system on 2 September 2026, when 29 pages
   designed outside were merged in. Three kinds of rule were removed; the full
   list is in the commit, not here.

     1. Rules that re-styled SHARED shell components. This stylesheet loads
        AFTER palesas.css, so each one silently overrode the site-wide header,
        footer or nav ON THIS PAGE ONLY, while shell.mjs check called the page
        clean because the MARKUP was identical. A markup checker cannot see a
        stylesheet. That went for CLASS selectors and, in a second pass, for
        bare ELEMENT selectors too: one page stylesheet said nav{display:none}
        below 700px and was hiding the site's primary navigation.
     2. Rules for markup that no longer exists: the chrome this page arrived
        with, and its own hero.
     3. Document-level rules that fought palesas.css, which owns them.

   Colours are tokens now. palesas.css owns every shared component.

   Two things have already broken this banner, both worth knowing. It once
   PASTED THE REMOVED SELECTOR LIST INTO ITSELF, and that list held a star next
   to a slash, which closed the comment early and swallowed the next real rule.
   Then a sweep that counted braces without skipping comments read this prose
   as a selector and deleted words out of it. A tool that parses CSS must strip
   comments FIRST, and no generated text belongs inside one.

   Ported into the WordPress theme unchanged from `html site/css/shop.css`
   (project #36, WooCommerce shop archive + category archive). */
.pk-shop-page{background:var(--pk-bg,var(--pk-bg));overflow:hidden}.shop-hero .pk-eyebrow{color:var(--pk-gold);margin:0 0 4px}.shop-benefits{position:relative;margin-top:-1px}.shop-benefits__grid{background:rgba(255,249,249,.97);border:1px solid var(--pk-border);border-radius:var(--pk-r);box-shadow:var(--pk-shadow-sm);display:grid;grid-template-columns:repeat(4,1fr);overflow:hidden;transform:translateY(20px)}.shop-benefits__grid>div{min-height:68px;padding:12px 24px;display:grid;grid-template-columns:32px 1fr;grid-template-rows:auto auto;column-gap:9px;align-content:center;border-right:1px solid var(--pk-border)}.shop-benefits__grid>div:last-child{border-right:0}.shop-benefits__grid span{grid-row:1/3;color:var(--pk-pink-ink);font-size:25px;display:grid;place-items:center}.shop-benefits strong{font:600 13px/1.2 var(--pk-display,Outfit,sans-serif)}.shop-benefits small{font:400 11px/1.3 var(--pk-sans,DM Sans,sans-serif);color:var(--pk-muted)}.shop-categories{background:var(--pk-surface);padding:var(--pk-band) 0}.shop-categories__grid{display:flex;flex-wrap:wrap;justify-content:center;gap:32px 52px;align-items:flex-start}.shop-categories__grid>a,.shop-categories__grid>.is-empty{flex:0 1 200px;min-width:140px}.shop-categories__grid a{text-align:center;text-decoration:none;color:var(--pk-fg)}.shop-categories__grid img{width:132px;height:132px;border-radius:var(--pk-r-xl);object-fit:cover;border:2px solid var(--pk-gold-soft);padding:4px;background:var(--pk-surface);display:block;margin:0 auto 10px;transition:transform .25s var(--pk-ease),box-shadow .25s var(--pk-ease)}.shop-categories__grid a:hover img{transform:translateY(-4px);box-shadow:var(--pk-shadow)}.shop-categories__grid strong{display:block;font:500 13px/1.25 var(--pk-display,Outfit,sans-serif)}.shop-categories__grid span{display:block;color:var(--pk-pink-ink);font:500 11px/1.5 var(--pk-sans,DM Sans,sans-serif);margin-top:4px}.shop-catalogue{padding:var(--pk-band) 0;background:var(--pk-surface)}.shop-catalogue__head{display:grid;grid-template-columns:1fr auto auto;align-items:center;gap:16px;border:1px solid var(--pk-border);border-radius:var(--pk-r) var(--pk-r) 0 0;padding:14px 18px;background:var(--pk-surface)}.shop-catalogue__head>p{margin:0;font:400 12px var(--pk-sans,DM Sans,sans-serif);color:var(--pk-muted)}.shop-sort{display:flex;align-items:center;gap:8px}.shop-sort label{font-size:11px;color:var(--pk-muted)}.shop-sort select{height:36px;min-width:180px;border:1px solid var(--pk-border);border-radius:var(--pk-r);background:var(--pk-surface);padding:0 11px;font:400 12px var(--pk-sans,DM Sans,sans-serif);color:var(--pk-fg)}.view-button, .shop-filter-toggle{height:36px;border:1px solid var(--pk-border);background:var(--pk-surface);border-radius:var(--pk-r);min-width:36px;color:var(--pk-muted)}.view-button.is-selected{background:var(--pk-primary);color:var(--pk-primary-fg);border-color:var(--pk-primary)}.shop-filter-toggle{display:none;padding:0 12px}.shop-layout{display:grid;grid-template-columns:188px 1fr;border:1px solid var(--pk-border);border-top:0;border-radius:0 0 var(--pk-r) var(--pk-r);overflow:hidden}.shop-filters{padding:18px 16px;background:var(--pk-surface);border-right:1px solid var(--pk-border)}.shop-filters__title{display:flex;justify-content:space-between;align-items:center;text-transform:uppercase;letter-spacing:.07em;font:600 10px var(--pk-sans,DM Sans,sans-serif);padding-bottom:12px;border-bottom:1px solid var(--pk-border)}.shop-filters__title button{display:none;border:0;background:none;font-size:22px}.filter-group{padding:15px 0;border-bottom:1px solid var(--pk-border)}.filter-group h3{display:flex;justify-content:space-between;margin:0 0 10px;font:600 10px var(--pk-sans,DM Sans,sans-serif);text-transform:uppercase;letter-spacing:.06em}.filter-group label{display:flex;align-items:flex-start;gap:6px;margin:7px 0;font:400 10px/1.3 var(--pk-sans,DM Sans,sans-serif);color:var(--pk-fg)}.filter-group label b{margin-left:auto;font-weight:400;color:var(--pk-muted)}.filter-group input{accent-color:var(--pk-pink-ink);margin:0}.filter-group--collapsed{cursor:pointer}.price-line{height:2px;background:var(--pk-pink-ink);position:relative;margin:25px 5px 7px}.price-line:before, .price-line:after{content:"";position:absolute;width:12px;height:12px;border:2px solid var(--pk-pink-ink);background:var(--pk-surface);border-radius:50%;top:50%;transform:translateY(-50%)}.price-line:before{left:-6px}.price-line:after{right:-6px}.price-values{display:flex;justify-content:space-between;font:400 9px var(--pk-sans,DM Sans,sans-serif);color:var(--pk-muted)}.shop-products{padding:16px 16px 20px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;background:var(--pk-surface)}.product-card{border:1px solid var(--pk-border);border-radius:var(--pk-r-xl);overflow:hidden;background:var(--pk-surface);min-width:0;transition:transform .2s var(--pk-ease),box-shadow .2s var(--pk-ease)}.product-card:hover{transform:translateY(-3px);box-shadow:var(--pk-shadow)}.product-card[hidden]{display:none}.product-card__media{position:relative;aspect-ratio:1/1;background:var(--pk-cream);overflow:hidden}.product-card__media img{width:100%;height:100%;display:block;object-fit:cover}.product-wish{position:absolute;z-index:2;right:13px;top:13px;width:29px;height:29px;border:1px solid var(--pk-border);background:rgba(255,255,255,.95);border-radius:50%;display:grid;place-items:center;color:var(--pk-muted)}.product-wish svg{width:16px;height:16px}.product-wish.is-saved{color:var(--pk-pink-ink);background:var(--pk-surface)}.product-wish.is-saved svg{fill:currentColor}.product-badge{position:absolute;z-index:2;left:13px;top:13px;width:43px;height:43px;border-radius:50%;display:grid;place-items:center;text-align:center;text-transform:uppercase;background:var(--pk-gold-ink);color:var(--pk-primary-fg);font:700 9px/1.05 var(--pk-sans)}.product-badge--new{background:var(--pk-primary)}.product-badge--sale{background:var(--pk-pink-ink)}.product-card__body{padding:10px 10px 11px}.product-card h2{font:500 12px/1.28 var(--pk-display,Outfit,sans-serif);min-height:31px;margin:0 0 6px;color:var(--pk-fg)}.product-card h2 a{color:inherit;text-decoration:none}.product-price{margin:0 0 9px;color:var(--pk-primary);font:700 15px var(--pk-sans,DM Sans,sans-serif)}.product-price del{font-size:9px;color:var(--pk-muted);margin-left:4px}.add-cart{width:100%;min-height:44px;border:0;border-radius:var(--pk-pill);background:var(--pk-primary);color:var(--pk-primary-fg);letter-spacing:normal;font:500 13px var(--pk-sans);display:flex;align-items:center;justify-content:center;gap:6px;cursor:pointer;text-decoration:none}.add-cart svg{width:14px;height:14px}.add-cart.added{background:var(--pk-gold-ink)}.added_to_cart{display:none}.shop-newsletter{background:var(--pk-primary);color:var(--pk-primary-fg);padding:var(--pk-band) 0}.shop-newsletter__inner{display:flex;align-items:center;justify-content:space-between;gap:40px}.shop-newsletter .pk-eyebrow{color:var(--pk-gold-soft);margin:0 0 5px}.shop-newsletter h2{font:700 clamp(26px,3.4vw,33.6px)/1.08 var(--pk-display);letter-spacing:-.02em;margin:0;color:var(--pk-primary-fg)}.shop-newsletter form{display:flex;max-width:470px;width:100%;border-bottom:1px solid rgba(255,255,255,.45);padding-bottom:14px 0}.shop-newsletter input{flex:1;border:0;outline:0;background:transparent;color:var(--pk-primary-fg);font:400 13px var(--pk-sans,DM Sans,sans-serif);padding:10px 0}.shop-newsletter input::placeholder{color:rgba(255,255,255,.68)}.shop-newsletter button{border:0;background:none;color:var(--pk-primary-fg);font:600 11px var(--pk-sans,DM Sans,sans-serif);display:flex;gap:8px;align-items:center;cursor:pointer}.shop-newsletter button svg{width:17px}

@media(max-width:1023px){.shop-products{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(max-width:767px){.shop-categories{display:none}.shop-benefits__grid{grid-template-columns:repeat(2,1fr);transform:none;margin-top:0}.shop-benefits__grid>div:nth-child(2){border-right:0}.shop-benefits__grid>div:nth-child(-n+2){border-bottom:1px solid var(--pk-border)}.shop-layout{grid-template-columns:1fr;border:0}.shop-filters{position:fixed;z-index:50;left:0;top:0;bottom:0;width:min(320px,88vw);transform:translateX(-105%);transition:transform .25s var(--pk-ease);box-shadow:20px 0 50px rgba(0,0,0,.15)}.shop-filters.is-open{transform:translateX(0)}.shop-filters__title button{display:block}.shop-filter-toggle{display:block}.shop-catalogue__head{grid-template-columns:auto 1fr;gap:10px}.shop-catalogue__head>p{justify-self:end}.shop-sort{grid-column:1/-1;justify-content:flex-end}.shop-sort label{display:none}.shop-sort select{flex:0 1 auto;min-width:160px}.shop-products{grid-template-columns:repeat(2,minmax(0,1fr));padding:12px 0}.shop-newsletter__inner{display:block}.shop-newsletter form{margin-top:25px}}
@media(max-width:639px){.shop-benefits__grid>div{padding:11px 13px}.shop-benefits strong{font-size:11px}.shop-benefits small{font-size:9px}.product-card h2{font-size:11px}}

/* The colour ROLES, restored. palesas.css states them in its own comments:
   berry is the action colour, pink is fills and decoration, and gold-ink is
   the value that can carry a small white label. shop.css had made pink the
   primary action on eight buttons, which with a 4px radius and a 9px
   uppercase label made the Add to cart row read as a different retailer. */

/* Three rules stood here until 10 September 2026, for a category archive whose
   sidebar rendered plain <a> links instead of tick boxes. That branch is gone -
   shop-sidebar.php now renders one checkbox tree on every page - so there is no
   <a> left in the sidebar to style. The third rule never worked at all:
   `.shop-filters aside[aria-current]` looked for an <aside> INSIDE .shop-filters,
   but .shop-filters IS the <aside>, and aria-current sat on a <label>. The
   category being viewed was therefore never bolded, for as long as the rule
   existed. Deleted rather than fixed: the tick shows it now. */

/* WooCommerce's own woocommerce_result_count() / woocommerce_catalog_ordering()
   output plain <p>/<form><select> markup with WC's default classes
   (.woocommerce-result-count, .woocommerce-ordering) - these two rules make
   them sit correctly inside the existing .shop-catalogue__head grid instead
   of being invisible layout items with no matching selector. */
.shop-catalogue__head .woocommerce-result-count { margin: 0; font: 400 12px var(--pk-sans,DM Sans,sans-serif); color: var(--pk-muted); }
.shop-catalogue__head .woocommerce-ordering { display: contents; }

/* WooCommerce's real pagination (woocommerce_pagination()) outputs
   .woocommerce-pagination > .page-numbers, not this file's original
   .ec-pager markup - restyled to the same pill language rather than
   left as unstyled browser defaults. */
.woocommerce-pagination { display: flex; gap: 8px; justify-content: center; margin-top: 24px; }
.woocommerce-pagination .page-numbers { list-style: none; display: flex; gap: 8px; padding: 0; margin: 0; }
.woocommerce-pagination .page-numbers li { display: contents; }
.woocommerce-pagination a.page-numbers,
.woocommerce-pagination span.page-numbers {
	display: inline-flex; align-items: center; justify-content: center;
	min-width: 40px; height: 40px; padding: 0 12px;
	border: 1px solid var(--pk-border); border-radius: var(--pk-pill, 999px);
	color: var(--pk-fg); text-decoration: none; font-size: 13px;
}
.woocommerce-pagination span.page-numbers.current { background: var(--pk-primary); color: var(--pk-primary-fg); border-color: var(--pk-primary); }
.woocommerce-pagination a.page-numbers:hover { border-color: var(--pk-primary); color: var(--pk-primary); }

/* ======================================================================
   THE FILTERS, 8 September 2026.

   Most of what makes this work was already in this file and had never had
   any markup behind it: .filter-group input, .filter-group--collapsed,
   .price-line, .price-values and the whole max-width:767px off-canvas
   block. They are in use now. What follows is only what was genuinely
   missing.

   Nothing here touches style.css. Everything is scoped to the shop.
   ====================================================================== */

/* The sidebar is wrapped in a real <form> so it works without JavaScript.
   display:contents keeps .shop-filters itself the grid child, so
   .shop-layout's `188px 1fr` is unchanged by the extra element. */
.shop-filters-form { display: contents; }

/* .shop-filters__scroll wraps everything scrollable inside the mobile
   drawer (title + filter groups), separate from the apply bar. On desktop
   it does nothing - display:contents keeps its children direct visual
   children of .shop-filters, unchanged. It becomes a real box only inside
   the max-width:767px block below. */
.shop-filters__scroll { display: contents; }

/* Same trick, same reason: #shopCount exists so the script always has an
   element to write the result count into (WooCommerce prints nothing at all
   at zero results), and display:contents keeps the <p> inside it a direct
   child of .shop-catalogue__head's three-column grid. */
#shopCount { display: contents; }

/* The empty state is one cell wide in a three-column grid otherwise. */
.shop-products .ec-panel { grid-column: 1 / -1; }
.shop-products { position: relative; }
.shop-products[aria-busy="true"] { opacity: .7; pointer-events: none; transition: opacity .15s var(--pk-ease); }
/* One loading language, reused from the account pages' .ec-skeleton shimmer
   (assets/css/ecommerce.css) rather than a second, competing loading idiom.
   A thin bar rather than a full skeleton grid: cheap (pure CSS, no markup),
   and legible against cards of any size without knowing the column count. */
.shop-products[aria-busy="true"]::before {
	content: ""; position: absolute; z-index: 3; top: 0; left: 0; right: 0; height: 3px;
	background: linear-gradient(90deg, var(--pk-cream) 25%, var(--pk-primary) 37%, var(--pk-cream) 63%);
	background-size: 400% 100%; animation: pk-shimmer 1.1s linear infinite;
	border-radius: var(--pk-pill, 999px);
}

/* A group heading is a real button now - the "-" was decoration. */
.filter-group h3 { margin: 0 0 10px; }
.filter-group__toggle {
	display: flex; justify-content: space-between; align-items: center;
	width: 100%; padding: 0; border: 0; background: none; cursor: pointer;
	font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit;
}
.filter-group--collapsed .filter-group__body { display: none; }

/* The sign is drawn HERE and nowhere else. It was a hardcoded &minus; in
   shop-sidebar.php until 10 September 2026, so it stayed a "−" when the group
   was shut and told the shopper the opposite of the truth. Hanging it off the
   same class the script already toggles means there is one home for it and
   nothing to keep in step. */
.filter-group__sign { font-weight: 400; line-height: 1; }
.filter-group__sign::before { content: "\2212"; }
.filter-group--collapsed .filter-group__sign::before { content: "+"; }

/* ---------------------------------------------------------------------
   THE TWO-LEVEL CATEGORY FILTER

   Moved here from assets/css/cakes.css on 9 September 2026, and that move
   IS the fix. The markup had been two levels since the re-parenting earlier
   that day - a .filter-cat per main category with its ranges in a
   .filter-cat__kids - but the only rules that drew the hierarchy lived in a
   stylesheet enqueued on Contact, Legal and 404 and on no other page. So on
   the shop, all sixteen categories rendered flush left at one weight, as a
   wall. Nothing was broken in the PHP; the CSS simply was not there.

   Tony, 9 September 2026: "they should show them all but they need to be
   collapsible by two main categories and they should be expanded by
   default." Hence a real toggle per main category, aria-expanded="true" in
   the markup so a no-JavaScript visitor gets everything open rather than
   everything shut.

   ⚠ COLLAPSE WITH display:none, NEVER BY REMOVING THE INPUT. This sidebar
   is a real GET form that works with JavaScript off. A display:none checkbox
   still submits its value, so a shopper who ticks three cake ranges, folds
   Cakes shut and presses "Show results" still gets all three. Anything that
   detaches or disables the input silently drops her filter instead.
   --------------------------------------------------------------------- */
.filter-cat + .filter-cat { margin-top: 12px; }
/* The parent row: its checkbox on the left, its fold button on the right,
   sharing one line. The two are separate controls on purpose - a click on
   the chevron must never tick "everything in Cakes". */
.filter-cat__row { display: flex; align-items: center; gap: 4px; }
.filter-cat__row > label { flex: 1; margin: 0; font-weight: 600; }
.filter-cat__toggle {
	flex: none; width: 20px; height: 20px; padding: 0; border: 0;
	background: none; cursor: pointer; color: var(--pk-muted);
	display: grid; place-items: center; line-height: 1; font: inherit;
	border-radius: var(--pk-r, 8px);
}
/* ⚠ GUARDED, because an unguarded :hover LATCHES after a tap on a phone.
   Without the query, tapping a category chevron left a cream patch stuck on
   the white card until something else was tapped - motion-house rule 5, which
   exists for exactly this and for exactly this audience. And the colour half
   was dead inside the drawer anyway: `.shop-filters .filter-cat__toggle`
   carries the same (0,2,0) specificity and sits later in the file, so the
   resting berry won and only the background ever applied. Both halves now say
   berry, at both widths, and only where a real pointer exists. Caught by the
   Reviewer, 14 September 2026. */
@media (hover: hover) and (pointer: fine) {
	/* Desktop keeps its original --pk-pink-ink. Changing it here would be the
	   same unasked-for desktop recolour the block further down just retreated
	   from. */
	.filter-cat__toggle:hover { color: var(--pk-pink-ink); background: var(--pk-cream); }
}
@media (hover: hover) and (pointer: fine) and (max-width: 767px) {
	.shop-filters .filter-cat__toggle:hover { color: var(--pk-primary); background: var(--pk-cream); }
}
.filter-cat__toggle svg { width: 11px; height: 11px; transition: transform .2s var(--pk-ease); }
.filter-cat--collapsed .filter-cat__toggle svg { transform: rotate(-90deg); }
.filter-cat__kids {
	margin: 4px 0 0 8px; padding-left: 11px;
	border-left: 1px solid var(--pk-border);
}
.filter-cat__kids label { font-weight: 400; margin: 6px 0; }
.filter-cat--collapsed .filter-cat__kids { display: none; }

/* An option that would return nothing. Tony's decision, 8 Sept 2026: shown,
   so a shopper sees the full range, but never clickable into a blank page. */
.filter-group label.is-empty,
.shop-categories__grid .is-empty,
.pk-footer .is-empty { opacity: .45; cursor: not-allowed; }
.filter-group label.is-empty input { pointer-events: none; }
.shop-categories__grid .is-empty { display: block; text-align: center; }
.shop-categories__grid .is-empty img { filter: grayscale(1); }
.shop-categories__grid .is-empty span { color: var(--pk-muted); }

/* THE PRICE SLIDER. Two native range inputs stacked on the .price-line
   track. Native because a real <input type="range"> submits with the form
   when JavaScript is off, takes arrow keys and announces itself - none of
   which a div-and-mousemove slider gets without hundreds of lines. The
   track's own ::before/::after dots are dropped here: the real thumbs
   replace them, styled to the same 12px berry-ringed circle. */
.price-range { position: relative; height: 26px; margin: 22px 6px 4px; }
.price-range .price-line { position: absolute; inset: 50% 0 auto; margin: 0; transform: translateY(-50%); }
.price-range .price-line::before,
.price-range .price-line::after { content: none; }
.price-line__fill { position: absolute; top: 0; bottom: 0; background: var(--pk-primary); }
.price-range__input {
	position: absolute; left: 0; right: 0; top: 0; width: 100%; height: 26px;
	margin: 0; padding: 0; background: none; -webkit-appearance: none; appearance: none;
	pointer-events: none;
}
.price-range__input:focus { outline: none; }
.price-range__input::-webkit-slider-thumb {
	-webkit-appearance: none; appearance: none;
	width: 16px; height: 16px; border-radius: 50%;
	border: 2px solid var(--pk-pink-ink); background: var(--pk-surface);
	box-shadow: var(--pk-shadow-sm); cursor: grab; pointer-events: auto;
}
.price-range__input::-moz-range-thumb {
	width: 16px; height: 16px; border-radius: 50%;
	border: 2px solid var(--pk-pink-ink); background: var(--pk-surface);
	box-shadow: var(--pk-shadow-sm); cursor: grab; pointer-events: auto;
}
.price-range__input:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--pk-primary); outline-offset: 2px; }
.price-range__input:focus-visible::-moz-range-thumb { outline: 2px solid var(--pk-primary); outline-offset: 2px; }
.price-range__input::-moz-range-track { background: none; }

/* The active-filter chips. The pill itself is the approved .pk-filters
   component from style.css, reused rather than rebuilt; these rules move it
   from the homepage's centred, spaced-out placement into a tight
   left-aligned row above the results.

   ⚠ It must also read as PART of the toolbar/grid card, not a gap in it.
   .shop-catalogue__head above has a border with rounded top corners, and
   .shop-layout below has a border with rounded bottom corners - together
   they are one continuous bordered card. #shopChips sits between them and,
   before this rule, carried no border, background or side padding at all,
   so the rendered card had a literal hole where the pills sit: bordered
   box, then a borderless floating row, then another bordered box. Giving it
   the same side border and the toolbar's own horizontal padding closes that
   gap and turns the trailing whitespace after "Clear all" into the rest of
   an inset panel instead of a stray empty patch. Found on the real filtered
   URL, 8 September 2026 - screenshot before/after in the project's
   comms-log. */
.shop-chips {
	margin: 0; padding: 12px 18px;
	border-left: 1px solid var(--pk-border); border-right: 1px solid var(--pk-border);
	background: var(--pk-surface);
	justify-content: flex-start;
}
.shop-chips:empty { display: none; padding: 0; border: 0; }
.shop-chip {
	flex: none; display: inline-flex; align-items: center; gap: 6px;
	min-height: 32px; padding: 5px 13px; border-radius: var(--pk-pill);
	border: 1px solid var(--pk-border); background: var(--pk-surface);
	font: 500 11px var(--pk-sans, DM Sans, sans-serif); color: var(--pk-fg);
	text-decoration: none;
}
.shop-chip:hover { border-color: var(--pk-primary); color: var(--pk-primary); }
.shop-chip--clear { border-style: dashed; color: var(--pk-muted); }

/* Load more, in the same pill language as .add-cart. */
.shop-more { display: flex; justify-content: center; margin-top: 22px; }
.shop-more:empty { display: none; }
.shop-more__btn {
	display: inline-flex; align-items: center; justify-content: center; gap: 8px;
	min-height: 44px; padding: 0 28px; border-radius: var(--pk-pill);
	background: var(--pk-primary); color: var(--pk-primary-fg);
	font: 500 13px var(--pk-sans, DM Sans, sans-serif); text-decoration: none; cursor: pointer;
}
.shop-more__btn svg { width: 15px; height: 15px; transform: rotate(90deg); }
.shop-more__btn[aria-disabled="true"] { opacity: .6; pointer-events: none; }

/* The apply button is a phone-only control: on desktop the results update
   as you tick, so a button that says "show them" would be a lie. */
.shop-filters__apply { display: none; }
.shop-filter-toggle__dot {
	display: inline-block; width: 6px; height: 6px; margin-left: 6px;
	border-radius: 50%; background: var(--pk-primary);
}

/* THE MOBILE APPLY BAR, rebuilt 8 September 2026.
   It used to be `position: sticky; bottom: 0` on the LAST child of
   .shop-filters, with .shop-filters reserving 84px of padding-bottom for
   it and the bar carrying `margin-bottom: -84px` to pull itself down into
   that reserved space.

   Both halves of that trick were broken. A sticky element's `bottom: 0`
   sticks to its scroll container's PADDING edge, not its border edge - so
   the 84px of padding-bottom always left an 84px gap below the bar,
   whatever the margin said. And `margin-bottom` on an element with no
   later sibling does not move that element at all (only a NEXT sibling
   would ever be pulled up by it) - so the negative margin cancelled
   nothing. Measured live: the bar sat exactly 84px above the drawer's true
   bottom edge, a 1:1 match with the unclaimed padding-bottom, with the
   grey backdrop showing through beneath it.

   The fix removes the trick instead of re-tuning its numbers: .shop-filters
   is a column flexbox, .shop-filters__scroll (title + filter groups) is the
   ONLY thing that scrolls, and the apply bar is a plain, non-sticky flex
   item pinned at the bottom by the layout itself - nothing to overlap, no
   padding to reserve, no magic number to keep in sync with the button's
   real height. */
@media (max-width: 767px) {
	/* .is-open was never referenced by any JavaScript; PBT.disclosure sets
	   [data-open], and the shop's drawer uses the shared primitive like
	   every other panel in the theme. */
	.shop-filters[data-open] { transform: translateX(0); }

	.shop-filters {
		display: flex; flex-direction: column; padding: 0; overflow: hidden;
	}

	.shop-filters__scroll {
		display: block; flex: 1 1 auto; min-height: 0;
		overflow-y: auto; -webkit-overflow-scrolling: touch;
		padding: 18px 16px;
	}

	.shop-filters__apply {
		display: block; flex: none;
		margin: 0; padding: 12px 16px 16px;
		background: var(--pk-surface); border-top: 1px solid var(--pk-border);
	}
	.shop-filters__apply .pk-btn { width: 100%; }

	/* House drawer motion, copied from .pk-cart-drawer__panel: the panel moves
	   on --dur-panel/--ease-move while its scrim fades on --dur-ui/--ease-ui.
	   The .25s/--pk-ease this replaces predates the house tokens. The two
	   timings differing is the convention, not a mistake - see the scrim's own
	   note below, where assuming otherwise nearly changed the wrong one. */
	.shop-filters { transition: transform var(--dur-panel) var(--ease-move); }
}

/* ======================================================================
   THE DRAWER'S COLOUR, 14 September 2026

   Tony: "the colors are very bad". Most of that was the backdrop bug above
   painting the whole panel 55% near-black. What was left underneath, once
   the tint was gone, was a white-and-grey form with one berry button at the
   bottom and a hot-pink tick - the shop's own palette barely present, and
   two different reds arguing.

   The strategy, in roles rather than swatches:

     head        berry, owning a real region. It names the surface, makes the
                 close control findable, and brackets the list against the
                 berry button at the other end.
     ground      --pk-cream, the token whose own comment in style.css reads
                 "a rosier cream, to sit under the berry". This is the one
                 place in the theme that sentence was never cashed in.
     groups      white, so each filter block reads as a surface on that
                 ground without needing a card, a border or a shadow.
     selection   berry text on a white pill. The ONE state worth colour here
                 is "this is on", because it is what a shopper scrolling
                 sixteen categories cannot otherwise see.
     accent      berry. The tick and the button now agree; --pk-pink-ink is
                 a decoration colour (3.74 as text, per its own comment) and
                 was never the right hue for a control.

   Scoped to the drawer only. Above 767px the sidebar is a grid column inside
   a bordered card and none of this applies - a berry band there would be a
   second header on a page that already has one.
   ====================================================================== */
@media (max-width: 767px) {
	.shop-filters { background: var(--pk-cream); }

	.shop-filters__scroll { padding: 0 0 18px; }

	/* The head. Sticky so the shopper always knows which panel they are in
	   and can always reach the close control, however far down a
	   sixteen-category list they have scrolled. */
	.shop-filters .shop-filters__title {
		position: sticky; top: 0; z-index: 2;
		margin: 0; padding: 16px;
		background: var(--pk-primary); color: var(--pk-primary-fg);
		border-bottom: 0;
		font-size: 11px; letter-spacing: .1em;
	}
	.shop-filters .shop-filters__title button {
		width: 32px; height: 32px; margin: -6px -6px -6px 0;
		display: grid; place-items: center;
		border-radius: var(--pk-pill);
		background: rgba(255, 255, 255, .14); color: var(--pk-primary-fg);
		font-size: 19px; line-height: 1;
		transition: background var(--dur-ui) var(--ease-ui),
		            transform var(--dur-tap) var(--ease-ui);
	}
	.shop-filters .shop-filters__title button:active { transform: scale(.92); }

	/* Each group is a white surface on the cream, separated by the gap
	   rather than by a rule. The hairline the desktop sidebar uses would be
	   a second separator doing the same job. */
	.shop-filters .filter-group {
		margin: 10px 12px 0; padding: 14px 14px 12px;
		background: var(--pk-surface); border-bottom: 0;
		border-radius: var(--pk-r);
	}
	.shop-filters .filter-group h3 { color: var(--pk-primary); font-size: 11px; }
	.shop-filters .filter-group__sign { color: var(--pk-primary); font-size: 15px; }

	/* Rows get a touch target and a resting shape, so the ticked pill below
	   has somewhere to land instead of appearing out of nothing. */
	.shop-filters .filter-group label {
		align-items: center; gap: 9px;
		min-height: 34px; margin: 1px 0; padding: 4px 8px;
		border-radius: var(--pk-r);
		font-size: 12px;
		transition: background var(--dur-ui) var(--ease-ui),
		            color var(--dur-ui) var(--ease-ui);
	}
	.shop-filters .filter-group label b { font-size: 11px; }
	.shop-filters .filter-group input { width: 17px; height: 17px; flex: none; }
	.shop-filters .filter-cat__kids { margin-left: 6px; padding-left: 12px; }

	/* WHAT IS ON. :has() only - a class would need the JS to keep it in step
	   with a checkbox the browser already tracks, and this list is patched in
	   place on every request. Browsers without :has() lose the highlight and
	   keep the tick; nothing else depends on it. */
	.shop-filters .filter-group label:has(input:checked) {
		background: var(--pk-cream-deep); color: var(--pk-primary); font-weight: 600;
	}
	.shop-filters .filter-group label:has(input:checked) b { color: var(--pk-primary); }

	.shop-filters .filter-cat__toggle { width: 26px; height: 26px; color: var(--pk-primary); }
	.shop-filters .filter-cat__row > label { font-weight: 600; }

	.shop-filters .price-values { font-size: 11px; }
	.shop-filters .filter-group label.is-empty { opacity: .5; }
}

/* ⚠ MOBILE ONLY, AND THAT IS A DELIBERATE RETREAT.

   These five rules first shipped unscoped, on the argument that "two reds on
   the desktop sidebar is the same mistake at a different size". The argument
   is probably right. It is also not what Tony asked for: the brief was the
   "product page mobile filter", and this silently changed every ticked
   checkbox, the price track, its fill and both thumbs on an approved desktop
   surface from --pk-pink-ink to berry.

   That is the "improve it while I'm in there" instinct, which CLAUDE.md names
   as damage rather than a bonus on work a client has seen. Worse, the evidence
   written down for it said "desktop sidebar visually unchanged" - true of the
   head band, the ground and the apply bar, and false of every control. A wrong
   claim in a decision log is worse than no claim.

   So the recolour stops at the breakpoint the brief stopped at. Taking it to
   desktop is a separate question for Tony, raised rather than assumed.
   Caught by the Reviewer, 14 September 2026. */
@media (max-width: 767px) {
	.shop-filters .filter-group input { accent-color: var(--pk-primary); }
	.shop-filters .price-range__input::-webkit-slider-thumb { border-color: var(--pk-primary); }
	.shop-filters .price-range__input::-moz-range-thumb { border-color: var(--pk-primary); }
	.shop-filters .price-line,
	.shop-filters .price-line::before,
	.shop-filters .price-line::after { background: var(--pk-primary); border-color: var(--pk-primary); }
}

/* ======================================================================
   THE APPLY BUTTON SPEAKS WHILE IT WORKS, 14 September 2026

   Tony: "the show x results button is silent and not animated... the client
   may confuse loading silence to nothing is happening".

   He was reading the code correctly. data-loading was set in exactly one
   place - the moment the drawer opened - and applyLabel() cleared it. Every
   tick AFTER that ran a 250ms debounce plus a network round trip with no
   signal at all, and then the number simply changed. On a phone the results
   are behind the drawer you are standing in, so that number is the only
   thing a shopper can see: silence, then a different digit.

   THE SIGNAL IS THE BUTTON ITSELF, not a marker beside the text. A 14px
   spinner ring on a full-width pill is the weak answer and it was already
   there being ignored; a band of light travelling the whole width of the
   control cannot be missed at arm's length on a bus. So the spinner is kept
   for .shop-more__btn, where a sweep has no room to read, and the apply
   button gets the sweep - one signal each, never both on one control.

   Transform only, on a pseudo-element inside the pill's own overflow. No
   layout property is touched, so it stays on the compositor on a mid-range
   Android, which is the device this ships to.
   ====================================================================== */
/* ⚠ THIS BUTTON OPTS OUT OF THE SITE-WIDE BUSY TREATMENT, and it is the one
   place in the theme that does.

   ecommerce.css owns `.pk-btn[aria-busy="true"]`, which sets
   `color: transparent` and centres a spinner ring in the middle of the pill.
   That is right for Add to cart, where the label never changes and hiding it
   costs nothing. It is wrong here, because on this button the LABEL IS THE
   MESSAGE: "Updating…" is what tells the shopper anything is happening, and
   under prefers-reduced-motion it is the only signal left once the sweep is
   switched off. The site convention would blank the very word this whole
   change exists to show, and drop a second spinner on top of the sweep.

   aria-busy stays on the element because it is correct for a screen reader
   and because pbt-shop-filters.js sets it for that reason. Only the visual
   half is overridden, and only for this one control. Caught by reading the
   computed style back on a real load - the rendered button showed a spinner
   and no text, and a screenshot alone would never have said why. */
.shop-filters__apply .pk-btn[aria-busy="true"] { color: var(--pk-primary-fg); }
.shop-filters__apply .pk-btn[aria-busy="true"]::after { content: none; }

.shop-filters__apply .pk-btn {
	position: relative; overflow: hidden; isolation: isolate;
	transition: transform var(--dur-tap) var(--ease-ui);
}
/* Press feedback. The button had none: nothing acknowledged the tap before
   the results appeared. */
.shop-filters__apply .pk-btn:active { transform: scale(.98); }

.shop-filters__apply-label { display: inline-block; }

/* THE SWAP. A straight cut between "Show 78 results" and "Show 3 results"
   reads as a glitch - two strings where there should be one change - so the
   new text arrives out of focus and resolves.

   ⚠ IT IS A ONE-SHOT ON THE CHANGE, NOT A STATE. The first version hung
   `opacity:.75; filter:blur(1.5px)` off [data-loading], so the label sat
   blurred for the whole request. On a slow connection that is two or three
   seconds of text that simply looks broken, and a reader assumes their screen
   is smudged rather than that the shop is working. Blur bridges a transition;
   held, it is just damage. Caught on a throttled load, 14 September 2026.

   3px, well under the 20px where blur gets expensive in Safari, and on a
   64px label it costs nothing to composite. */
@keyframes pk-apply-swap {
	0%   { opacity: 0; filter: blur(3px); transform: translateY(2px); }
	100% { opacity: 1; filter: blur(0);   transform: translateY(0); }
}
.shop-filters__apply-label.is-swapped {
	animation: pk-apply-swap var(--dur-ui) var(--ease-ui);
}

.shop-filters__apply .pk-btn::before {
	content: ""; position: absolute; z-index: -1; inset: 0;
	background: linear-gradient(
		100deg,
		transparent 20%,
		rgba(255, 255, 255, .22) 45%,
		var(--pk-gold-soft) 50%,
		rgba(255, 255, 255, .22) 55%,
		transparent 80%
	);
	opacity: 0;
	/* ⚠ THE RESTING POSITION IS OFF THE RIGHT, WHERE THE SWEEP ENDS - not off
	   the left, where it begins. Removing [data-loading] removes the animation,
	   and transform then reverts INSTANTLY to whatever the base rule says while
	   opacity fades over --dur-ui. With the base at -100% the gold band jumped
	   back to the left edge of the pill and faded out there, on every single
	   completion. Resting it at +100% means the revert lands exactly where the
	   sweep was travelling to, so the band simply leaves. Caught by the
	   Reviewer, 14 September 2026: a still screenshot cannot see it and a
	   pass-count cannot either. */
	transform: translateX(100%);
	transition: opacity var(--dur-ui) var(--ease-ui);
}
/* 900ms, and deliberately outside the Response budget: this is not feedback
   on a gesture, it is the system reporting that it is still working, and a
   200ms sweep repeating five times a second reads as a fault rather than as
   progress. Linear because constant motion is what "still going" looks
   like - an eased loop pulses, and a pulse implies a rhythm the work does
   not have. */
.shop-filters__apply .pk-btn[data-loading]::before {
	opacity: 1;
	animation: pk-apply-sweep 900ms linear infinite;
}
@keyframes pk-apply-sweep {
	from { transform: translateX(-100%); }
	to   { transform: translateX(100%); }
}

@media (prefers-reduced-motion: reduce) {
	/* "Reduce" means reduce, not remove: strip the movement, keep a real
	   signal. The label still reads "Updating…" and the control still dims,
	   so the state is carried by TEXT and tone - never by motion alone, and
	   never by colour alone either. A shopper with reduced motion on must not
	   be the one person who gets the silence this change exists to fix.

	   The swap keeps a plain opacity fade: a fade is not movement and does
	   not trigger vestibular symptoms, and without it the text would cut. */
	.shop-filters__apply .pk-btn[data-loading]::before { animation: none; opacity: 0; }
	.shop-filters__apply .pk-btn[data-loading] { opacity: .72; }
	.shop-filters__apply-label.is-swapped {
		animation-name: pk-apply-fade;
	}
	.shop-filters__apply .pk-btn:active { transform: none; }
	.shop-filters .shop-filters__title button:active { transform: none; }

	/* ⚠ THE PANEL ITSELF, which the first version missed. A 320px box
	   translating across the screen is precisely the movement this setting
	   exists for; the fading scrim beside it is not. The three sibling drawers
	   in style.css all remember to name their panel here and this one did not,
	   because the reduced-motion evidence was gathered on the BUTTON and the
	   button is not the thing that moves. Caught by the Reviewer, 14 September
	   2026. 1ms rather than 0 to match .pk-wish-drawer and .pk-cart-drawer. */
	.shop-filters { transition-duration: 1ms; }
}
@keyframes pk-apply-fade {
	0%   { opacity: 0; }
	100% { opacity: 1; }
}

/* THE DRAWER BACKDROP. The sidebar/drawer element has never had one; every
   off-canvas panel in this theme relies on its own box-shadow alone. A real
   element (not a ::before on .shop-filters itself) so a tap on it still
   counts as "outside the panel" for hit-testing, and data-filter-backdrop
   is wired to drawer.close() in pbt-shop-filters.js exactly like the
   existing [data-filter-close] button.

   ⚠ IT IS A SIBLING OF .shop-filters, NOT A CHILD, and the long reason is in
   shop-sidebar.php next to the markup. Short version: .shop-filters is
   transformed, a transform is a containing block for fixed descendants, so a
   nested `inset: 0` resolved against the 320px drawer instead of the
   viewport - and `z-index: -1` inside the drawer's stacking context painted
   the scrim ON the drawer's white background rather than behind it. The
   drawer rendered 55% near-black and the page behind was never dimmed.
   Selector below is a sibling combinator for exactly that reason; a
   descendant selector here would be the bug coming back.

   z-index 9998 pairs with the 9999 that style.css gives .pk-scroll-lock
   .shop-filters - the drawer always locks scroll when it opens, so the scrim
   must clear every other panel too while still sitting under the drawer and
   under .pk-toast (also 9999). */
.shop-filters__backdrop { display: none; }
@media (max-width: 767px) {
	.shop-filters__backdrop {
		display: block; position: fixed; inset: 0; z-index: 9998;
		/* ⚠ THE HOUSE SCRIM VALUES, copied from .pk-cart-drawer__scrim and the
		   three other panels in style.css, NOT invented here. A first version
		   used rgba(20,8,10,.55) and --dur-panel, on the reasoning that a
		   berry-tinted scrim suits a berry drawer and that the scrim should
		   match the panel's timing. Both were wrong, and the second one was
		   wrong in an instructive way: the house pattern is deliberately
		   scrim-at---dur-ui, panel-at---dur-panel, so "they were 50ms out of
		   step" described the convention rather than a defect. Four existing
		   drawers cannot all be wrong and this one right. Caught by the
		   Reviewer, 14 September 2026 - the comment in shop-sidebar.php
		   claimed to follow those drawers while matching none of their
		   values, which is FM-8 starting inside the fix for another bug. */
		background: rgba(43, 43, 43, .5); opacity: 0; pointer-events: none;
		transition: opacity var(--dur-ui) var(--ease-ui);
	}
	.shop-filters[data-open] ~ .shop-filters__backdrop { opacity: 1; pointer-events: auto; }
}

/* LOAD MORE: a small CSS-only spinner ring beside the label, sized to the
   pill it sits in.

   ⚠ It used to cover the mobile apply button too. It does not any more: that
   control is full width and gets the sweep instead (see "THE APPLY BUTTON
   SPEAKS WHILE IT WORKS" below). One signal per control - a ring AND a sweep
   on the same pill is two answers to one question, which is the "do not stack
   techniques" rule. Load more keeps the ring because a sweep across a 160px
   pill is not legible. */
.shop-more__btn[aria-disabled="true"]::after {
	content: ""; display: inline-block; width: 14px; height: 14px; margin-left: 8px;
	border: 2px solid rgba(255, 255, 255, .4); border-top-color: var(--pk-primary-fg, #fff);
	border-radius: 50%; animation: pk-spin .7s linear infinite; vertical-align: -2px;
}
@keyframes pk-spin { to { transform: rotate(360deg); } }

/* A facet's count changing after a filter request is the one piece of proof
   a shopper gets that the sidebar actually responded to their click - a
   brief colour pulse, not a redraw. */
@keyframes pk-count-pulse {
	0% { color: var(--pk-primary); transform: scale(1.12); }
	100% { color: inherit; transform: scale(1); }
}
.filter-group label b.is-updated { display: inline-block; animation: pk-count-pulse .5s var(--pk-ease); }

@media (prefers-reduced-motion: reduce) {
	/* The apply button's own reduced-motion treatment is with its rules
	   above, not here: it does more than switch an animation off, because
	   removing the sweep would leave that control with no signal at all. */
	.shop-products[aria-busy="true"]::before,
	.shop-more__btn[aria-disabled="true"]::after {
		animation: none;
	}
	.filter-group label b.is-updated { animation: none; }
	.shop-filters__backdrop { transition-duration: 1ms; }
}

/* The Chaty floating WhatsApp button (a plugin, not this theme) sits fixed
   at the bottom-right on every page and collided with this page's own
   bottom-fixed elements at every breakpoint tested: the mobile drawer's
   sticky "Show results" bar, and the drawer toggle button beneath it.
   Scoped to WooCommerce's own body class so it never touches any other
   page - this file's own header warns against exactly that mistake. */
@media (max-width: 767px) {
	/* !important because Chaty injects its own inline <style id="chaty-widget-0">
	   with an ID selector (#chaty-widget-0 .chaty-widget { bottom: 25px; }),
	   which beats this rule's class selector on specificity regardless of
	   load order. This rule has silently never applied since it was written -
	   it only looked like it worked because the mobile apply bar used to
	   float ~84px above the drawer's true bottom (see "THE MOBILE APPLY BAR"
	   above), which happened to clear Chaty by accident. Fixing that gap
	   moved the bar down onto Chaty's real (25px) position and made this
	   rule's failure visible. Found and fixed together, 8 September 2026. */
	body.woocommerce-shop .chaty-widget { bottom: 100px !important; }
}

/* The homepage's own category tiles, greyed by the same rule. shop.css is
   enqueued on the front page as well (inc/woocommerce-setup.php), which is
   why this can live here rather than in the shell stylesheet. */
.pk-cat.is-empty { opacity: .5; cursor: not-allowed; }
.pk-cat.is-empty:hover { transform: none; box-shadow: none; }
.pk-cat.is-empty img { filter: grayscale(1); }

/* Out of stock card, 25 Sep 2026: every unpriced product lands here. Muted,
   not a button, because there is nothing to press. */
.add-cart--out { background: var(--pk-cream-deep); color: var(--pk-fg); cursor: default; }
