/*
Theme Name: Manaline
Theme URI: https://manaline.com
Author: Alrasly Consultancy
Description: Block theme for Manaline. Bilingual (English / Arabic), WooCommerce, built for WPML. Colour and type tokens are generated from setup/palette.json — edit the palette script, not this file.
Version: 0.12.3
Requires at least: 6.5
Tested up to: 6.9
Requires PHP: 8.1
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: manaline
Tags: e-commerce, full-site-editing, rtl-language-support, block-patterns
*/

/* ---------------------------------------------------------------------------
   LOGICAL PROPERTIES ONLY.

   Every rule below uses inline-start/inline-end/block-start/block-end rather
   than left/right/top/bottom. That is the single thing that lets one stylesheet
   serve both the English and the Arabic site. There is no rtl.css and there
   must never be one.

   If you add a rule containing `left` or `right`, the Arabic site breaks.
   --------------------------------------------------------------------------- */

:root {
  --ml-icon-check: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22m5%2012.8%204.2%204.2L19%207.2%22/%3E%3C/svg%3E");
  --ml-rule: 1px solid color-mix(in oklab, var(--wp--preset--color--stone) 30%, transparent);
  --ml-e: cubic-bezier(0.32, 0.72, 0, 1);
  --ml-slow: 0.72s;
  --ml-mid: 0.4s;
  --ml-fast: 0.2s;
  --ml-gut: clamp(16px, 4vw, 44px);
  /* Height of the sticky header. The hero subtracts it so a "full viewport
     height" hero actually ends at the fold rather than 66px past it — the
     header is sticky but still occupies flow, so the hero starts below it.
     Measured at 66px on desktop and 62px on mobile. */
  --ml-header-h: 66px;
}

/* ═══ script-aware typography ═════════════════════════════════════════════
   Arabic needs more leading (deeper descenders, diacritics collide at the
   Latin setting) and zero tracking — letter-spacing breaks the joins between
   Arabic letterforms. */

[lang="ar"],
[dir="rtl"] {
  line-height: 1.85;
  letter-spacing: 0;
}

[dir="rtl"] .wp-block-heading,
[dir="rtl"] .wp-block-button__link,
[dir="rtl"] .ml-eyebrow,
[dir="rtl"] .ml-opt-label {
  letter-spacing: 0;
  text-transform: none;
}

/* Prices and phone numbers stay left-to-right inside Arabic text, or
   "12.500 BHD" renders reversed and "+973 …" loses its plus sign. */
.woocommerce-Price-amount,
.wc-block-formatted-money-amount,
.wc-block-components-formatted-money-amount,
.ml-ltr {
  direction: ltr;
  unicode-bidi: isolate;
  font-variant-numeric: tabular-nums;
}

/* ═══ accessibility floor ═══ */

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--wp--preset--color--cherry);
  outline-offset: 3px;
  border-radius: 2px;
}

.ml-footer :where(a, button):focus-visible {
  outline-color: var(--wp--preset--color--cherry-100);
}

.skip-link:not(:focus) {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .ml-reveal { opacity: 1 !important; transform: none !important; }
}

/* ═══ header ═══ */

.ml-header {
  position: sticky;
  inset-block-start: 0;
  z-index: 60;
  background: color-mix(in oklab, var(--wp--preset--color--paper) 88%, transparent);
  backdrop-filter: blur(14px);
  border-block-end: var(--ml-rule);
  transition: box-shadow var(--ml-mid) var(--ml-e);
}

.ml-header.is-stuck {
  box-shadow: 0 1px 24px color-mix(in oklab, var(--wp--preset--color--ink) 8%, transparent);
}

.ml-header .wp-block-site-title a {
  text-decoration: none;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--wp--preset--color--cherry);
}

.ml-header .wp-block-navigation-item__content {
  padding-block: 6px;
  position: relative;
  text-decoration: none;
}

.ml-header .wp-block-navigation-item__content::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 1px;
  background: var(--wp--preset--color--cherry);
  transform: scaleX(0);
  transform-origin: inline-start;
  transition: transform var(--ml-mid) var(--ml-e);
}

.ml-header .wp-block-navigation-item__content:hover::after,
.ml-header .current-menu-item .wp-block-navigation-item__content::after {
  transform: scaleX(1);
}

/* ═══ header: logo, and the overlay state on the front page ═══════════════

   Two logos are stacked and cross-faded rather than one src being swapped: the
   cherry one stays under the client's control in the Customizer, and opacity
   cannot shift layout the way a changing src can.

   .ml-logo keeps a fixed block-size so the header height never changes between
   states. Only the wordmark scales inside it — the nav and icons beside it do
   not move, which is the actual requirement. 52px is the 260px wordmark at its
   fixed 4.99:1. */

.ml-logo {
  position: relative;
  block-size: 52px;
  align-items: center;
  /* The overlay wordmark is a shortcode, so wpautop wraps it in a <p>. That <p>
     is a zero-width flex item — but it still earns the group's 16px gap, which
     widened the logo to 216px and pushed the bag icon 27px off-screen at 390. */
  gap: 0;
}

.ml-logo .wp-block-site-logo,
.ml-logo .wp-block-site-logo a { display: block; line-height: 0; }

.ml-logo .wp-block-site-logo img,
.ml-logo .ml-logo-white {
  display: block;
  inline-size: 220px;
  block-size: auto;
  transition: opacity var(--ml-mid) var(--ml-e), inline-size var(--ml-mid) var(--ml-e);
}

/* The white wordmark sits exactly over the cherry one. Both SVGs are 1529x306,
   so at equal inline-size they register to the pixel. */
.ml-logo .ml-logo-white {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 0;
  transform: translateY(-50%);
  opacity: 0;
  pointer-events: none;
}

/* Nav and utility icons travel together at the inline end. */
.ml-header-end { gap: clamp(18px, 3vw, 40px); align-items: center; }

/* ── overlay state ──────────────────────────────────────────────────────
   Front page only, and only with JavaScript — see the ml-js note in
   inc/setup.php. The header is fixed rather than sticky so it leaves the flow
   entirely and the hero can start at the very top of the viewport. */

html.ml-js body.home .ml-header {
  position: fixed;
  inset-inline: 0;
  inset-block-start: 0;
  background: transparent;
  backdrop-filter: none;
  border-block-end-color: transparent;
  /* background and colour only — nothing here changes geometry */
  transition: background var(--ml-mid) var(--ml-e),
              border-color var(--ml-mid) var(--ml-e),
              box-shadow var(--ml-mid) var(--ml-e);
}

/* A second scrim, anchored to the header rather than the hero.
   The hero scrim alone was not enough. It fades to nothing by 22% of the HERO,
   so how dark the nav band ends up depends on how tall the hero happens to be —
   and at 768x720 the hero stacks, the photograph spans the full width, and the
   nav sits directly on pale grey. Measured there: mean 4.56:1 but a worst pixel
   of 2.93:1 against paper, which fails 4.5:1 for text.

   Anchoring a scrim to the header makes the nav band's contrast independent of
   hero height, stacking and image content. The two stack multiplicatively where
   they overlap, which is what lifts the worst case clear of 4.5:1.

   Alpha is 0.7, not 0.62. At 0.62 the measured worst pixels were 4.45:1 behind
   the nav and 4.47:1 behind the icons — a hair under the floor rather than over
   it, and "almost 4.5" is not a pass. */
html.ml-js body.home .ml-header:not(.is-stuck)::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-start: 0;
  block-size: 190%;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(rgba(42, 20, 26, 0.7), rgba(42, 20, 26, 0));
}

/* Paper nav, logo and icons while overlaid. Legible only because of the two
   scrims — the header's own, above, and the hero's, in .ml-hero::before. */
html.ml-js body.home .ml-header:not(.is-stuck) :where(.wp-block-navigation-item__content, .ml-account, .ml-bag, .ml-lang-switch a, .wp-block-navigation__responsive-container-open) {
  color: var(--wp--preset--color--paper);
}

/* The burger needs saying explicitly. It is not a navigation-item, so the rule
   above missed it and it stayed ink on the dark scrim — measured invisible at
   390. The overlay dialog keeps ink, because it opens on a paper panel. */
html.ml-js body.home .ml-header:not(.is-stuck) .wp-block-navigation__responsive-container-open svg { fill: currentColor; }

html.ml-js body.home .ml-header:not(.is-stuck) .wp-block-navigation-item__content::after {
  background: var(--wp--preset--color--paper);
}

html.ml-js body.home .ml-header:not(.is-stuck) .ml-lang-switch a {
  border-color: color-mix(in oklab, var(--wp--preset--color--paper) 55%, transparent);
}

/* Cross-fade to the white wordmark, and up to 260px. Never below 200px in
   either state — that is where the counters start closing. */
html.ml-js body.home .ml-header:not(.is-stuck) .ml-logo .wp-block-site-logo img { opacity: 0; }
html.ml-js body.home .ml-header:not(.is-stuck) .ml-logo .ml-logo-white { opacity: 1; }
html.ml-js body.home .ml-header:not(.is-stuck) .ml-logo .wp-block-site-logo img,
html.ml-js body.home .ml-header:not(.is-stuck) .ml-logo .ml-logo-white { inline-size: 260px; }

/* 260px only where there is room for it. Below 1024 the nav is still inline,
   and a 260px wordmark plus four nav items plus the icons overflowed the row —
   at 768 the header wrapped onto two lines with the logo stranded between them.
   220px is the solid-state size and still well clear of the 200px floor. */
@media (max-width: 1023px) {
  html.ml-js body.home .ml-header:not(.is-stuck) .ml-logo .wp-block-site-logo img,
  html.ml-js body.home .ml-header:not(.is-stuck) .ml-logo .ml-logo-white { inline-size: 220px; }
  .ml-logo { block-size: 44px; }
}

/* Phone widths. 220px plus the burger and two icons overran 390 and flex
   shrank the logo box to 165px — under the 200px floor where the counters
   close. 200px with tighter gaps fits, and the logo is pinned against further
   shrinking so the floor cannot be breached by layout pressure again. */
@media (max-width: 600px) {
  .ml-logo { flex: 0 0 auto; block-size: 40px; }
  .ml-logo .wp-block-site-logo img,
  .ml-logo .ml-logo-white,
  html.ml-js body.home .ml-header:not(.is-stuck) .ml-logo .wp-block-site-logo img,
  html.ml-js body.home .ml-header:not(.is-stuck) .ml-logo .ml-logo-white { inline-size: 200px; }
  .ml-header-end { gap: 10px; }
  .ml-utility { gap: 8px; }
  /* Both icons keep their 44px tap target; only the space between them goes. */
  .ml-bag-group { gap: 0; }
}

/* WPML is not installed yet, so the switcher renders empty — and an empty flex
   item still takes the utility cluster's gap. Reappears on its own once WPML
   supplies the links. */
.ml-lang-switch:not(:has(a)) { display: none; }

/* Scrolled: back to the ordinary solid paper header with the cherry wordmark.
   Nothing below is a geometry change. */
html.ml-js body.home .ml-header.is-stuck {
  background: color-mix(in oklab, var(--wp--preset--color--paper) 88%, transparent);
  backdrop-filter: blur(14px);
  border-block-end: var(--ml-rule);
}

/* Language switcher. WPML renders its own markup, so style defensively. */
.ml-lang-switch ul { padding: 0; margin: 0; list-style: none; border: 0; }
.ml-lang-switch a {
  text-decoration: none;
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--wp--preset--color--cherry);
  border: 1px solid var(--wp--preset--color--cherry-300);
  padding: 5px 11px;
  border-radius: 2px;
  transition: background var(--ml-fast), color var(--ml-fast);
}
.ml-lang-switch a:hover {
  background: var(--wp--preset--color--cherry);
  color: var(--wp--preset--color--paper);
}
[dir="rtl"] .ml-lang-switch a { letter-spacing: 0; text-transform: none; }
.ml-lang-switch .wpml-ls-current-language { display: none; }

/* ═══ hero ═══
   Split rather than text over image. Text over photography fails the moment a
   shot is busy or light, and it fails hardest in Arabic where line lengths
   differ. Splitting guarantees contrast without constraining the photo. */

/* No strip of paper between the header border and the hero.
   Measured rather than assumed: the 16px was neither the header nor the hero —
   it was <main>'s own block-start margin, applied by .wp-site-blocks > *
   block-gap. The hero already sat flush inside main. The other two rules stay as
   cheap insurance against the margin reappearing on either element. */
.ml-header { margin-block-end: 0; }
.wp-site-blocks > main { margin-block-start: 0; }
main > :first-child { margin-block-start: 0; }
.ml-hero { margin-block-start: 0 !important; }

/* Full viewport height, by decision.

   svh is declared second on purpose. On mobile Safari and Chrome 100vh is the
   height WITHOUT the browser chrome, so a 100vh hero is taller than the visible
   screen and the CTA hides under the address bar. 100svh is the small-viewport
   height and behaves. Older browsers keep the vh value; modern ones take svh. */
.ml-hero {
  min-block-size: calc(100vh - var(--ml-header-h));
  min-block-size: calc(100svh - var(--ml-header-h));
  position: relative;
}

/* On the front page the header is fixed and no longer occupies flow, so there
   is nothing to subtract. Leaving the 66px in would make the hero fall short of
   the fold by exactly the header height. Scoped to html.ml-js because without
   JavaScript the header stays solid and in flow, and the subtraction is still
   correct there. */
html.ml-js body.home { --ml-header-h: 0px; }

/* Scrim. The nav sits over a split hero — Cherry on one side, pale photography
   on the other — and paper-coloured nav over light grey fails contrast badly.
   The gradient covers the top 22% of the hero across its full width, so it
   darkens both halves rather than only the photograph.
   pointer-events: none so it cannot intercept clicks on the hero CTA. */
html.ml-js body.home .ml-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(rgba(42, 20, 26, 0.5), rgba(42, 20, 26, 0) 22%);
}

/* The media column must be height-capped, not just min-sized.
   Without a cap, the columns row grows to whatever the source image's aspect
   ratio demands — a 2000x3000 portrait at 54% width forced a row over 2000px
   tall, and the Cherry copy panel stretched to match it. That was the cause
   of both the oversized hero and the large empty burgundy area. */
.ml-hero .wp-block-columns { align-items: stretch; margin-block: 0; }

.ml-hero-media {
  position: relative;
  overflow: hidden;
  min-block-size: calc(100vh - var(--ml-header-h));
  min-block-size: calc(100svh - var(--ml-header-h));
  max-block-size: none;
}

.ml-hero-media .wp-block-image,
.ml-hero-media figure { margin: 0; block-size: 100%; }

/* Absolutely positioned so the photograph contributes no intrinsic height.
   With the cap removed for the full-height hero, an in-flow 1600x2133 image at
   54% column width resolved its own aspect ratio to ~991px and dragged the whole
   row past the viewport — the copy panel then stretched to match and the scroll
   cue fell below the fold. Out of flow, the column height comes purely from
   min-block-size. */
.ml-hero .ml-hero-media img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* Bias the crop upward. Fashion shots put the face in the top third, and a
     centred crop on a full-length portrait cuts the head off. */
  object-position: center 22%;
}

/* The copy panel fills the row rather than floating at its centre.
   The pattern sets verticalAlignment on this column, which core renders as
   is-vertically-aligned-center -> align-self: center. That beats the
   align-items: stretch above, so once the media column gained a height cap the
   row grew taller than the panel and left paper above and below it.

   Deliberately an override here rather than only dropping the attribute from
   the pattern: it has to survive someone re-setting vertical alignment in the
   Site Editor, which would write the class straight back. */
.ml-hero .wp-block-column.ml-hero-copy {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.ml-hero-copy > * {
  opacity: 0;
  transform: translateY(20px);
  animation: ml-rise 0.82s var(--ml-e) forwards;
  /* Flex items now. Keep full width, but let the button keep its own size
     instead of stretching the width of the panel. */
  align-self: stretch;
}
.ml-hero-copy > .wp-block-buttons { align-self: start; }

/* Scroll cue. At full viewport height there is otherwise no evidence anything
   exists below the fold, and on a laptop that is a real exit risk. A genuine
   anchor link, not decoration — it works with JavaScript disabled and is
   reachable by keyboard. */
.ml-hero-cue {
  position: absolute;
  inset-block-end: clamp(16px, 3vh, 32px);
  /* Sits over the Cherry panel rather than centred across the whole hero.
     Centred, it landed on the seam between the two columns — half of it over
     pale photography, where cherry-100 measures far below 3:1 and the label was
     effectively invisible. On Cherry it is 6.82:1, and it groups with the CTA
     directly above it. */
  inset-inline-start: auto;
  inset-inline-end: 0;
  inline-size: 46%;
  z-index: 3;
  text-align: center;
  /* excluded from the copy panel's rise animation, which it is not part of */
  opacity: 1;
}

.ml-hero-cue a {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  min-block-size: 44px;
  padding-inline: 12px;
  text-decoration: none;
  color: var(--wp--preset--color--cherry-100);
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.ml-hero-cue a:hover { color: var(--wp--preset--color--paper); }

.ml-hero-cue svg {
  animation: ml-cue 2.4s var(--ml-e) infinite;
}

@keyframes ml-cue {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(5px); }
}

[dir="rtl"] .ml-hero-cue a { letter-spacing: 0; text-transform: none; }
.ml-hero-copy > *:nth-child(1) { animation-delay: 0.10s; }
.ml-hero-copy > *:nth-child(2) { animation-delay: 0.20s; }
.ml-hero-copy > *:nth-child(3) { animation-delay: 0.30s; }
.ml-hero-copy > *:nth-child(4) { animation-delay: 0.40s; }

@keyframes ml-rise { to { opacity: 1; transform: none; } }

.ml-eyebrow {
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

/* ═══ buttons ═══ */

.wp-element-button {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transition: color var(--ml-mid) var(--ml-e);
}

.wp-element-button::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--wp--preset--color--cherry-900);
  transform: scaleY(0);
  transform-origin: block-end;
  transition: transform var(--ml-mid) var(--ml-e);
  z-index: -1;
}

.wp-element-button:hover::before { transform: scaleY(1); }

/* ═══ product grid ═══
   3:4 portrait. A square crop removes the hem from every full-length garment,
   which is the entire catalogue. */

.wc-block-grid__product-image img,
.wc-block-product-template img,
.woocommerce ul.products li.product img {
  aspect-ratio: 3 / 4;
  object-fit: cover;
  inline-size: 100%;
  block-size: auto;
  transition: transform 1s var(--ml-e);
}

.wc-block-product-template li,
.woocommerce ul.products li.product { position: relative; }

.wc-block-product-template li:hover img,
.woocommerce ul.products li.product:hover img { transform: scale(1.045); }

/* ═══ second-image hover swap ═══
   The front frame crossfades to the next gallery image on hover. The element is
   injected by inc/woocommerce.php and only for products that carry a gallery,
   so a single-image card never gets one and the feature is never half-applied.

   Pointer-gated: a touch device has no hover, and a swap bound to :hover there
   sticks on after the tap with no way to undo it. */

.wc-block-product-template .wc-block-components-product-image { position: relative; }

.ml-card-alt {
  position: absolute;
  inset: 0;
  block-size: 100%;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s var(--ml-e), transform 1s var(--ml-e);
}

@media (hover: hover) and (pointer: fine) {
  .wc-block-product-template li:hover .ml-card-alt { opacity: 1; }
}

.woocommerce ul.products li.product .woocommerce-loop-product__title,
.wc-block-product-template .wp-block-post-title {
  font-size: var(--wp--preset--font-size--sm);
  font-weight: 500;
  margin-block: 12px 2px;
  line-height: 1.4;
}

.woocommerce ul.products li.product .price,
.wc-block-product-template .wc-block-components-product-price {
  font-size: var(--wp--preset--font-size--sm);
  color: var(--wp--preset--color--graphite);
}

.woocommerce span.onsale,
.wc-block-components-product-sale-badge {
  position: absolute;
  inset-block-start: 10px;
  inset-inline-start: 10px;
  inset-inline-end: auto;
  background: var(--wp--preset--color--cherry);
  color: var(--wp--preset--color--paper);
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 4px 9px;
  border-radius: 2px;
  min-block-size: 0;
  min-inline-size: 0;
  line-height: 1.5;
  margin: 0;
}
[dir="rtl"] .woocommerce span.onsale,
[dir="rtl"] .wc-block-components-product-sale-badge {
  letter-spacing: 0;
  text-transform: none;
}

/* Scroll reveal.
   Hidden only when JavaScript is present, because only JavaScript can reveal
   it. Unscoped, a pattern that declares .ml-reveal in its markup — the FAQ
   does — would be invisible for good to anyone with scripts off or blocked.
   The ml-js flag is set inline in <head> (see inc/setup.php), so there is no
   flash of visible content before it applies. */
html.ml-js .ml-reveal { opacity: 0; transform: translateY(24px); }

/* Scoped to html.ml-js as well, and not for symmetry. The hide rule above is
   (0,2,1); a bare .ml-reveal.is-in is (0,2,0) and loses to it, so the class
   would be applied and ignored. Matching the scope puts the reveal at (0,3,1). */
html.ml-js .ml-reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 0.7s var(--ml-e), transform 0.7s var(--ml-e);
}

/* ═══ single product ═══ */

.ml-pdp-gallery { position: sticky; inset-block-start: 90px; }

/* Gallery thumbnails.
   WooCommerce emits .flex-control-thumbs as an <ol>, and the theme only ever
   set padding on the items — so it fell through to browser default list
   rendering and shipped as a numbered list, "1." to "6." down the page with a
   tiny image beside each numeral.

   auto-fill rather than a fixed column count: products carry between three and
   ten frames, so any fixed number leaves either a short row or a squeezed one. */
.woocommerce div.product div.images .flex-control-thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
  gap: 8px;
  list-style: none;
  margin-block: 10px 0;
  margin-inline: 0;
  padding: 0;
}

.woocommerce div.product div.images .flex-control-thumbs li {
  padding: 0;
  margin: 0;
  list-style: none;
}

.woocommerce div.product div.images .flex-control-thumbs img {
  aspect-ratio: 3 / 4;
  object-fit: cover;
  inline-size: 100%;
  opacity: 0.55;
  cursor: pointer;
  transition: opacity var(--ml-fast) var(--ml-e);
}

.woocommerce div.product div.images .flex-control-thumbs img.flex-active,
.woocommerce div.product div.images .flex-control-thumbs img:hover { opacity: 1; }

.woocommerce div.product div.images img { aspect-ratio: 3 / 4; object-fit: cover; }

.ml-opt-label {
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--wp--preset--color--graphite);
  margin-block-end: 10px;
}

.woocommerce div.product form.cart .variations {
  margin-block-end: var(--wp--preset--spacing--40);
}
.woocommerce div.product form.cart .variations td,
.woocommerce div.product form.cart .variations th {
  display: block;
  padding: 0;
  text-align: start;
}

.ml-size {
  min-inline-size: 50px;
  padding: 11px 15px;
  border: 1px solid var(--wp--preset--color--stone);
  border-radius: 2px;
  background: none;
  font: inherit;
  font-size: var(--wp--preset--font-size--sm);
  cursor: pointer;
  transition: all var(--ml-fast) var(--ml-e);
}
.ml-size:hover:not([disabled]) { border-color: var(--wp--preset--color--cherry); }
.ml-size[aria-pressed="true"] {
  background: var(--wp--preset--color--cherry);
  border-color: var(--wp--preset--color--cherry);
  color: var(--wp--preset--color--paper);
}

/* Sold-out sizes stay visible and struck through rather than disappearing. It
   tells the customer the size exists and gives a measurable restock signal. */
.ml-size[disabled],
.ml-size.is-out {
  color: var(--wp--preset--color--stone);
  border-color: color-mix(in oklab, var(--wp--preset--color--stone) 40%, transparent);
  text-decoration: line-through;
  cursor: not-allowed;
}

.ml-swatch {
  inline-size: 30px;
  block-size: 30px;
  border-radius: 50%;
  border: 1px solid color-mix(in oklab, var(--wp--preset--color--ink) 16%, transparent);
  cursor: pointer;
  transition: transform var(--ml-fast) var(--ml-e), box-shadow var(--ml-fast);
}
.ml-swatch:hover { transform: scale(1.1); }
.ml-swatch[aria-pressed="true"] {
  box-shadow: 0 0 0 2px var(--wp--preset--color--paper),
              0 0 0 4px var(--wp--preset--color--cherry);
}

/* The size/colour facade. The native <select> stays in the DOM carrying
   variation pricing, stock and validation — it is only hidden, never removed.
   Without JavaScript nothing hides it and the page is stock WooCommerce. */

.ml-optset-controls { display: flex; flex-wrap: wrap; gap: 10px; }
.ml-optset { margin-block-end: var(--wp--preset--spacing--30); }

html.ml-js .ml-optset-native {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

html:not(.ml-js) .ml-optset-controls { display: none; }

.ml-optset-chosen {
  margin-block: 8px 0;
  font-size: var(--wp--preset--font-size--xs);
  color: var(--wp--preset--color--graphite);
}
.ml-optset-chosen:empty { display: none; }

.ml-size { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.ml-size-hint { font-size: var(--wp--preset--font-size--xs); color: var(--wp--preset--color--stone); }
.ml-size[aria-pressed="true"] .ml-size-hint { color: var(--wp--preset--color--cherry-100); }

/* Add-to-bag before a variation is chosen. WooCommerce disables the button,
   which is correct — but the default washed-out pink reads as a broken page
   rather than a waiting one, so the state is explicit and so is the reason. */
.woocommerce div.product form.cart .button.disabled,
.woocommerce div.product form.cart .button:disabled {
  background: var(--wp--preset--color--cherry);
  color: var(--wp--preset--color--paper);
  opacity: 0.45;
  cursor: not-allowed;
}

.ml-choose-hint {
  margin-block: 0 10px;
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.04em;
  color: var(--wp--preset--color--graphite);
}
.ml-choose-hint[hidden] { display: none; }

/* Accordion */
.ml-acc { border-block-start: var(--ml-rule); }
.ml-acc:last-of-type { border-block-end: var(--ml-rule); }
.ml-acc summary {
  list-style: none;
  cursor: pointer;
  padding-block: 16px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--wp--preset--font-size--sm);
  color: var(--wp--preset--color--graphite);
}
.ml-acc summary::-webkit-details-marker { display: none; }
.ml-acc summary::after {
  content: "+";
  color: var(--wp--preset--color--cherry);
  font-size: 17px;
  line-height: 1;
  transition: transform var(--ml-mid) var(--ml-e);
}
.ml-acc[open] summary::after { transform: rotate(45deg); }
.ml-acc-body { padding-block-end: 18px; max-inline-size: 52ch; }

/* ═══ cart drawer ═══ */

.ml-scrim {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: color-mix(in oklab, var(--wp--preset--color--ink) 44%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--ml-mid);
}
.ml-scrim.is-open { opacity: 1; pointer-events: auto; }

.ml-drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  z-index: 100;
  inline-size: min(430px, 100%);
  background: var(--wp--preset--color--paper);
  display: flex;
  flex-direction: column;
  transform: translateX(101%);
  visibility: hidden;
  transition: transform var(--ml-slow) var(--ml-e),
              visibility 0s linear var(--ml-slow);
}
/* Safari has shipped both interpretations of translateX under RTL — set it
   explicitly so the drawer always leaves from the inline-end edge. */
[dir="rtl"] .ml-drawer { transform: translateX(-101%); }
.ml-drawer.is-open {
  transform: none;
  visibility: visible;
  transition-delay: 0s;
}

.ml-drawer-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px var(--ml-gut);
  border-block-end: var(--ml-rule);
}
.ml-drawer-body { flex: 1; overflow-y: auto; padding: var(--ml-gut); }
.ml-drawer-foot {
  padding: var(--ml-gut);
  border-block-start: var(--ml-rule);
  background: var(--wp--preset--color--linen);
}

.ml-ship-bar {
  block-size: 3px;
  background: color-mix(in oklab, var(--wp--preset--color--stone) 25%, transparent);
  border-radius: 3px;
  overflow: hidden;
  margin-block-end: 8px;
}
.ml-ship-bar > i {
  display: block;
  block-size: 100%;
  background: var(--wp--preset--color--cherry);
  transition: inline-size 0.6s var(--ml-e);
}

/* Header utility cluster: language, then account, then bag. Gap only — the
   individual items bring their own styling.
   .ml-bag-group exists because the shortcode block runs through wpautop, which
   wraps the output in a <p>; that <p> would otherwise become a single flex item
   and collapse the gap between the two icons. */
.ml-utility { gap: 14px; align-items: center; }
.ml-utility p { margin: 0; }
.ml-bag-group { display: inline-flex; align-items: center; gap: 6px; }

.ml-account,
.ml-bag {
  color: var(--wp--preset--color--cherry);
  text-decoration: none;
  /* 44px tap target. A mis-tap on the bag icon on a phone held one-handed
     sends the customer to the wrong page mid-shop. */
  min-inline-size: 44px;
  min-block-size: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.ml-account:hover,
.ml-bag:hover { color: var(--wp--preset--color--cherry-900); }

.ml-bag { position: relative; display: inline-flex; align-items: center; gap: 7px; }
.ml-bag-count {
  min-inline-size: 19px;
  block-size: 19px;
  border-radius: 19px;
  background: var(--wp--preset--color--cherry);
  color: var(--wp--preset--color--paper);
  font-size: var(--wp--preset--font-size--xs);
  display: grid;
  place-items: center;
  padding-inline: 5px;
  line-height: 1;
  transform: scale(0);
  transition: transform 0.34s var(--ml-e);
}
.ml-bag-count.has-items { transform: scale(1); }
.ml-bag-count.is-bumping { animation: ml-bump 0.42s var(--ml-e); }
@keyframes ml-bump {
  0% { transform: scale(1); }
  38% { transform: scale(1.42); }
  100% { transform: scale(1); }
}

/* ═══ checkout ═══
   Single column, nothing else on the page. Every extra element on a checkout
   is a reason to leave it. */

.woocommerce-checkout .ml-header .wp-block-navigation,
.woocommerce-checkout .ml-footer .wp-block-columns { display: none; }

.woocommerce form .form-row input.input-text,
.woocommerce form .form-row select,
.wc-block-components-text-input input {
  padding: 13px 15px;
  border: 1px solid var(--wp--preset--color--stone);
  border-radius: 2px;
  font: inherit;
  /* 16px minimum. Anything smaller makes iOS Safari zoom the viewport on
     focus, and the customer is then stuck at 130% mid-checkout. */
  font-size: var(--wp--preset--font-size--md);
  background: var(--wp--preset--color--paper);
}

.woocommerce form .form-row input.input-text:focus {
  border-color: var(--wp--preset--color--cherry);
  outline: 2px solid var(--wp--preset--color--cherry);
  outline-offset: 1px;
}

.woocommerce .woocommerce-error,
.woocommerce .woocommerce-message,
.woocommerce .woocommerce-info {
  border-block-start: 3px solid var(--wp--preset--color--cherry);
  background: var(--wp--preset--color--linen);
  padding: 14px 18px;
  border-radius: 2px;
}
.woocommerce .woocommerce-error {
  border-block-start-color: var(--wp--preset--color--error);
}

/* ═══ footer ═══ */

.ml-footer {
  background: var(--wp--preset--color--cherry);
  color: var(--wp--preset--color--paper);
}
/* Blush on Cherry measures 4.50:1 since the anchor changed — effectively a
   fail once antialiasing is accounted for. cherry-100 gives 6.82:1. */
.ml-footer a { color: var(--wp--preset--color--cherry-100); text-decoration: none; }
.ml-footer a:hover { color: var(--wp--preset--color--paper); }

/* One leading for every link column.
   The Shop column was a vertical navigation block (flex, with block-gap) while
   Help was a paragraph of <br>-separated links, so the two had visibly
   different leading. Every link column is now a navigation block with the gap
   set here, so they cannot drift apart again. */
.ml-footer-links .wp-block-navigation { gap: 10px; }
.ml-footer-links .wp-block-navigation__container { gap: 10px; }
.ml-footer-links h3 { margin-block-end: 14px; }
.ml-footer-brand .wp-block-site-logo { margin-block-end: 16px; }

.ml-colophon {
  font-size: var(--wp--preset--font-size--xs);
  border-block-start: 1px solid color-mix(in oklab, var(--wp--preset--color--cherry-100) 32%, transparent);
  padding-block-start: var(--wp--preset--spacing--40);
}

.ml-pay {
  border: 1px solid color-mix(in oklab, var(--wp--preset--color--cherry-100) 38%, transparent);
  border-radius: 3px;
  padding: 5px 9px;
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.06em;
}

/* ═══ mobile ═══ */

@media (max-width: 781px) {
  /* ── stacked hero ──────────────────────────────────────────────────────
     Below this width the hero columns stack, so a full-height media column
     plus the copy panel beneath it cannot both fit — measured at 768x720 the
     hero ran 376px past the fold and the scroll cue sat 354px below it.

     The whole hero now targets one viewport rather than the image alone: the
     photograph takes a share of the height and the copy sits under it. */
  .ml-hero { min-block-size: auto; }
  .ml-hero-media {
    min-block-size: 46vh;
    min-block-size: 46svh;
  }

  /* The cue is hidden, not repositioned. Pinned to the bottom of a hero that
     is taller than the viewport it can never be seen, and a cue you have to
     scroll to find is worse than none. On a stacked layout the copy panel
     already sits visibly below the photograph, which does the same job. */
  .ml-hero-cue { display: none; }

  /* Two columns, not one. One column feels generous but halves how many pieces
     a customer sees before losing interest, and 3:4 is already tall. */
  .wc-block-product-template,
  .woocommerce ul.products {
    grid-template-columns: 1fr 1fr !important;
    gap: 11px !important;
  }

  .ml-pdp-gallery { position: static; }

  /* Sticky add-to-bag. On mobile the real button scrolls out of view behind
     the fold and the customer never scrolls back up to find it. */
  .ml-sticky-cart {
    display: flex;
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 70;
    background: color-mix(in oklab, var(--wp--preset--color--paper) 96%, transparent);
    backdrop-filter: blur(12px);
    border-block-start: var(--ml-rule);
    padding: 10px var(--ml-gut) max(10px, env(safe-area-inset-bottom));
    display: flex;
    gap: 12px;
    align-items: center;
    transform: translateY(110%);
    transition: transform var(--ml-mid) var(--ml-e);
  }
  .ml-sticky-cart.is-visible { transform: none; }

  /* 44px tap-target floor. Anything smaller gets mis-tapped on a phone held
     one-handed, and a mis-tap on a size selector is a returned order. */
  .ml-size,
  .ml-swatch,
  .woocommerce .quantity input,
  .ml-drawer button,
  .wp-block-navigation-item__content { min-block-size: 44px; }

  .ml-swatch { min-inline-size: 44px; border-radius: 44px; }
}

/* ═══ empty image placeholders ═══
   Before photography exists, the split hero and editorial band render as a
   large blank half — roughly 700px of white on a 1440px screen, which reads
   as a broken page rather than an unfinished one.

   A block-level image with no src gets a tonal panel and a label instead.
   This costs nothing once real images are in: the rule only matches an <img>
   with an empty src attribute, so it stops applying the moment one is set. */

.ml-hero-media img[src=""],
.ml-hero-media img:not([src]),
.wp-block-image img[src=""],
.wp-block-image img:not([src]) {
  display: block;
  inline-size: 100%;
  min-block-size: min(60vh, 480px);
  background:
    linear-gradient(155deg,
      var(--wp--preset--color--cherry-050) 0%,
      var(--wp--preset--color--cherry-100) 38%,
      var(--wp--preset--color--blush) 74%,
      var(--wp--preset--color--cherry-300) 100%);
}

.ml-hero-media figure,
.ml-hero-media { position: relative; }

.ml-hero-media:has(img[src=""])::after,
.ml-hero-media:has(img:not([src]))::after {
  content: "Image not yet uploaded";
  position: absolute;
  inset-block-end: 12px;
  inset-inline-start: 12px;
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--wp--preset--color--graphite);
  background: color-mix(in oklab, var(--wp--preset--color--paper) 92%, transparent);
  padding: 4px 9px;
  border-radius: 2px;
  pointer-events: none;
}
[dir="rtl"] .ml-hero-media::after { letter-spacing: 0; text-transform: none; }

/* ═══ editorial band — same height cap as the hero ═══
   Identical failure mode: the image column set the row height, so a portrait
   shot produced a 2000px band with a mostly-empty linen half beside it. */

/* Matches whether .ml-editorial sits on a wrapper group (as .ml-hero does) or
   directly on the columns block. The descendant-only form silently did nothing
   when the class was on the columns element itself. */
.ml-editorial .wp-block-columns,
.ml-editorial.wp-block-columns { align-items: stretch; margin-block: 0; }

/* Same failure as the hero: the band's copy column carries
   is-vertically-aligned-center, which beats the stretch above now that the
   media column is height-capped. */
.ml-editorial .wp-block-column.ml-editorial-copy {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Full viewport height, matching the hero. svh second for the same reason. */
.ml-editorial,
.ml-editorial-media {
  min-block-size: 100vh;
  min-block-size: 100svh;
}

/* The photograph bleeds to all four edges of its column.
   Two separate causes were leaving linen visible around it: the band group
   carried padding, and .wp-block-image brings a default block-end margin.
   Padding now lives on the copy column only. */
.ml-editorial { padding: 0; }

.ml-editorial-media {
  position: relative;
  overflow: hidden;
  max-block-size: none;
}

.ml-editorial-media .wp-block-image,
.ml-editorial-media figure { margin: 0; block-size: 100%; }

.ml-editorial-media img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: center 25%;
}

/* ═══ trust strip ═══
   Answers the four questions every first-time visitor to an unknown brand
   has, in 60px of height and no photography. */

.ml-trust {
  border-block-end: var(--ml-rule);
  background: var(--wp--preset--color--paper);
}

.ml-trust ul {
  display: grid;
  /* Three cells since the payment column was withdrawn — see
     patterns/trust-strip.php for why it is not coming back yet. */
  grid-template-columns: repeat(3, 1fr);
  list-style: none;
  margin: 0;
  padding: 0;
  max-inline-size: 76rem;
  margin-inline: auto;
}

.ml-trust li {
  padding: 16px var(--wp--preset--spacing--30);
  text-align: center;
  font-size: var(--wp--preset--font-size--xs);
  color: var(--wp--preset--color--graphite);
  border-inline-end: var(--ml-rule);
}
.ml-trust li:last-child { border-inline-end: 0; }
.ml-trust b { color: var(--wp--preset--color--cherry); font-weight: 500; display: block; }

@media (max-width: 781px) {
  /* One per row at three cells: two-up would strand the third alone. */
  .ml-trust ul { grid-template-columns: 1fr; }
  .ml-trust li { border-inline-end: 0; }
  .ml-trust li:not(:last-child) { border-block-end: var(--ml-rule); }
}

/* ═══ category tiles ═══ */

.ml-cats { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(10px, 1.5vw, 20px); }

.ml-cat {
  position: relative;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
}
.ml-cat img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  transition: transform 1.1s var(--ml-e);
}
.ml-cat:hover img { transform: scale(1.05); }
.ml-cat::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
    color-mix(in oklab, var(--wp--preset--color--ink) 62%, transparent),
    transparent 52%);
}
.ml-cat-label {
  position: relative;
  z-index: 2;
  padding: 20px;
  color: var(--wp--preset--color--paper);
  font-size: var(--wp--preset--font-size--lg);
  font-weight: 600;
}

@media (max-width: 781px) {
  .ml-cats { grid-template-columns: 1fr 1fr; }
  .ml-cats > :last-child { grid-column: span 2; aspect-ratio: 16 / 9; }
}

/* ═══ three-column content row ═══
   Real body copy, not icons-and-three-words. This is the block that gives the
   homepage something for a search engine or an AI assistant to actually read
   and quote. */

.ml-pillars { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(20px, 3vw, 44px); }
.ml-pillars h3 {
  font-size: var(--wp--preset--font-size--lg);
  color: var(--wp--preset--color--cherry-900);
  margin-block-end: 10px;
}
.ml-pillars p { color: var(--wp--preset--color--graphite); font-size: var(--wp--preset--font-size--sm); }
.ml-pillars > div { border-block-start: 2px solid var(--wp--preset--color--blush); padding-block-start: 18px; }

/* All three rules on one baseline.
   WordPress gives every child except the first a block-start margin from
   block-gap, and those margins survive on grid items — so columns two and three
   sat 16px lower than the first and their rules did not line up. The grid's own
   gap handles the spacing; the per-item margins are noise here.
   Measured before: 2701 / 2717 / 2717. */
.ml-pillars > * { margin-block-start: 0; margin-block-end: 0; }

@media (max-width: 781px) { .ml-pillars { grid-template-columns: 1fr; } }

/* ═══ FAQ ═══ */

/* The accordion shares the section heading's inline start edge.
   It began 240px to the right of "Common questions", because a 46rem cap plus
   core's auto margins centred it inside the 76rem content column.

   Two approaches were tried and dropped. Raising specificity does not work —
   core sets margin-left: auto with !important. Overriding it with !important of
   our own, or cancelling core's max-width, both pushed the container out to the
   16px root padding instead: 89px to the LEFT of the heading.

   So .ml-faq now declares no inline size at all. It inherits exactly the same
   76rem box and auto margins as the heading, which puts their start edges on the
   same pixel, and the reading measure lives on the rows instead. */
.ml-faq details {
  max-inline-size: 46rem;
  border-block-end: var(--ml-rule);
}
.ml-faq summary {
  list-style: none;
  cursor: pointer;
  padding-block: 18px;
  display: flex;
  justify-content: space-between;
  gap: 20px;
  align-items: center;
  font-weight: 500;
}
.ml-faq summary::-webkit-details-marker { display: none; }
.ml-faq summary::after {
  content: "+";
  color: var(--wp--preset--color--cherry);
  font-size: 18px;
  line-height: 1;
  transition: transform var(--ml-mid) var(--ml-e);
}
.ml-faq details[open] summary::after { transform: rotate(45deg); }
.ml-faq .ml-faq-body { padding-block-end: 20px; color: var(--wp--preset--color--graphite); }

/* ═══ category archive copy ═══
   Three archives shipped as a bare grid with no text. This is the block that
   gives a search engine something to rank and an AI assistant something to
   quote, so it sits above the products rather than below them.

   The archive main is constrained at 76rem for BOTH content and wide size, so
   the H1, the intro, the count/sort bar, the grid and the note all share one
   inline-start edge. Prose is then capped to a readable measure here rather
   than by the layout, which would have centred it and left the copy floating
   17rem inside the grid it belongs to. Same failure the FAQ block had. */

/* Cap the measure on the text, not on the block.
   This is the same trap the FAQ block fell into. Core's constrained layout
   centres every unaligned child with margin-left/right: auto and !important, so
   giving the block itself a max-inline-size makes core centre the narrower box
   and the copy floats 240px inside the grid it belongs to. Overriding the
   centring instead throws it the other way, out to the page edge at x=16.
   Neither is the grid's edge. Letting the block keep the grid's own width and
   capping its children puts the first character exactly on it. */
.ml-cat-intro > *,
.ml-cat-note > * {
  max-inline-size: 46rem;
}

.ml-cat-intro {
  color: var(--wp--preset--color--graphite);
  margin-block: 18px 0;
}

.ml-cat-intro p { margin-block: 0 0.9em; }
.ml-cat-intro p:last-child { margin-block-end: 0; }

/* Count and sort. No filter sidebar — five products need no filtering, and an
   empty filter panel advertises a small catalogue. */

.ml-cat-bar {
  margin-block: clamp(22px, 3vw, 34px) 18px;
  padding-block-end: 14px;
  border-block-end: var(--ml-rule);
  align-items: baseline;
  gap: 16px;
}

.ml-cat-count {
  margin: 0;
  font-size: var(--wp--preset--font-size--xs);
  color: var(--wp--preset--color--stone);
  letter-spacing: 0.04em;
}

.ml-cat-bar .woocommerce-ordering { margin: 0; }

.ml-cat-bar .woocommerce-ordering select {
  font-size: var(--wp--preset--font-size--xs);
  font-family: inherit;
  color: var(--wp--preset--color--graphite);
  background: transparent;
  border: 0;
  border-block-end: var(--ml-rule);
  padding-block: 4px;
  padding-inline: 0 18px;
  cursor: pointer;
}

.ml-cat-empty {
  color: var(--wp--preset--color--graphite);
  max-inline-size: 42rem;
  margin-block: 32px;
}

/* The note, on the far side of the products. Term description can only hold one
   block of text, so this is separate term meta rather than part of the intro. */

.ml-cat-note {
  margin-block-start: clamp(34px, 5vw, 56px);
  padding-block-start: clamp(24px, 3vw, 34px);
  border-block-start: var(--ml-rule);
  color: var(--wp--preset--color--graphite);
}

.ml-cat-note-title {
  font-size: var(--wp--preset--font-size--md);
  font-weight: 600;
  color: var(--wp--preset--color--cherry-900);
  margin-block: 0 10px;
}

.ml-cat-note p { margin-block: 0 0.9em; }
.ml-cat-note p:last-child { margin-block-end: 0; }

/* ═══ more from Manaline ═══
   Shown only on categories holding fewer than three products, where the grid
   would otherwise be a one-item dead end. Stops rendering by itself as stock
   arrives — see MANALINE_THIN_CATEGORY in inc/woocommerce.php. */

.ml-more-from {
  margin-block-start: clamp(40px, 6vw, 72px);
  padding-block-start: clamp(24px, 3vw, 34px);
  border-block-start: var(--ml-rule);
}

.ml-more-from-title {
  font-size: var(--wp--preset--font-size--lg);
  font-weight: 500;
  margin-block: 0 clamp(18px, 2.5vw, 28px);
}

.ml-more-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(12px, 1.6vw, 22px);
  list-style: none;
  margin: 0;
  padding: 0;
}

.ml-more-card a {
  display: block;
  text-decoration: none;
  color: inherit;
}

.ml-more-card img {
  aspect-ratio: 3 / 4;
  object-fit: cover;
  inline-size: 100%;
  block-size: auto;
  transition: transform 1s var(--ml-e);
}

.ml-more-card { overflow: hidden; }
.ml-more-card a:hover img { transform: scale(1.045); }

.ml-more-name {
  display: block;
  font-size: var(--wp--preset--font-size--sm);
  font-weight: 500;
  margin-block: 12px 2px;
  line-height: 1.4;
}

.ml-more-price {
  display: block;
  font-size: var(--wp--preset--font-size--sm);
  color: var(--wp--preset--color--graphite);
}

@media (max-width: 781px) {
  .ml-more-grid { grid-template-columns: 1fr 1fr; gap: 11px; }
  .ml-cat-bar { margin-block-start: 20px; }
}

/* ═══ sticky add-to-bag: desktop ═══
   The markup in templates/single-product.html is unconditional, but every rule
   that positioned it lived inside the 781px query — so on desktop it rendered
   as a bare in-flow button sitting above the footer. Hidden by default; the
   mobile query above turns it back on. */

.ml-sticky-cart { display: none; }

@media (max-width: 781px) {
  .ml-sticky-cart { display: flex; }
}

/* ═══ footer wordmark ═══ */

.ml-footer-logo { inline-size: 180px; block-size: auto; display: block; }

/* ═══ category filter row ═══
   Categories left the primary navigation: a menu built from product categories
   breaks every time the range changes. They live beside the products instead. */

.ml-cat-filter {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(14px, 2vw, 26px);
  margin-block: 4px 0;
  padding-block-end: 16px;
}

.ml-cat-filter a {
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--wp--preset--color--graphite);
  padding-block: 4px;
  border-block-end: 1px solid transparent;
  transition: color var(--ml-fast) var(--ml-e), border-color var(--ml-fast) var(--ml-e);
}

.ml-cat-filter a:hover { color: var(--wp--preset--color--cherry); }

.ml-cat-filter a[aria-current="page"] {
  color: var(--wp--preset--color--cherry);
  border-block-end-color: var(--wp--preset--color--cherry);
}

/* ═══ content pages ═══════════════════════════════════════════════════════
   About and the legal set — everything using templates/page.html.

   The heading rhythm is the whole fix. theme.json zeroes heading margins on
   both sides, so a section heading sat 16px below the paragraph above it and
   16px above the one below: no grouping, no hierarchy, just an even ladder.
   On Contact — five headings across ninety-five words — that read as a stack
   of stubs rather than a page.

   Headings also come in at 30px here, close enough to the 44px H1 to compete
   with it. Sections one line long do not need to shout. */

.ml-page :where(h2) {
  font-size: clamp(19px, 2vw, 23px);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.005em;
  color: var(--wp--preset--color--cherry-900);
  /* Space above binds the heading to what follows, not to what precedes it. */
  margin-block-start: clamp(36px, 4.5vw, 56px);
  margin-block-end: 10px;
}

.ml-page :where(h3) {
  font-size: var(--wp--preset--font-size--md);
  margin-block: clamp(24px, 3vw, 32px) 6px;
}

.ml-page :where(p, ul, ol) { margin-block: 0 0.95em; }
.ml-page :where(ul, ol) { padding-inline-start: 1.15em; }
.ml-page :where(li) { margin-block-end: 0.4em; }
.ml-page .entry-content > :last-child { margin-block-end: 0; }

/* The H1 gets a rule so the page has an anchor rather than starting in mid-air. */
.ml-page .wp-block-post-title {
  margin-block-end: clamp(18px, 2.4vw, 26px);
  padding-block-end: clamp(16px, 2.2vw, 24px);
  border-block-end: var(--ml-rule);
}

/* "Last updated", from the post's modified date. */
.ml-legal-updated {
  font-size: var(--wp--preset--font-size--xs);
  letter-spacing: 0.04em;
  color: var(--wp--preset--color--stone);
  margin-block-end: clamp(22px, 3vw, 32px);
}

/* Lawyer-review and consent notes. Deliberately conspicuous — these are the
   two places on the privacy policy that must not be published unreviewed. */
.ml-legal-note {
  border-inline-start: 2px solid var(--wp--preset--color--cherry);
  background: color-mix(in oklab, var(--wp--preset--color--cherry) 7%, transparent);
  padding-block: 14px;
  padding-inline: 16px;
  font-size: var(--wp--preset--font-size--sm);
  color: var(--wp--preset--color--graphite);
}

.ml-legal-table table { border-collapse: collapse; inline-size: 100%; }

/* Core's table block draws a border on every side of every cell, which turned
   these into a boxed grid — and because the last row's block-end border is
   removed below, the side borders carried on past the final row and left an
   open-ended box hanging under the table.

   The class is doubled deliberately. Core's rule is .wp-block-table td, at
   (0,1,1); a reset written as .ml-legal-table :where(th, td) is only (0,1,0)
   because :where() contributes nothing, so it lost and the borders stayed.
   Doubling gets to (0,2,0), which wins on class count. */
.ml-legal-table.ml-legal-table :where(table, thead, tbody, tr, th, td) {
  border: 0;
}

.ml-legal-table.ml-legal-table :where(th, td) {
  text-align: start;
  vertical-align: top;
  font-size: var(--wp--preset--font-size--sm);
  padding-block: 11px;
  padding-inline: 0 18px;
  border-block-end: var(--ml-rule);
}

.ml-legal-table th {
  font-weight: 500;
  color: var(--wp--preset--color--cherry-900);
}

.ml-legal-table :where(th, td):last-child { padding-inline-end: 0; }

/* ── unresolved placeholders ──
   Wrapped at render time by inc/legal.php, never stored in the content. A raw
   【 EMAIL 】 mid-sentence reads as a broken page; this reads as a draft that
   is waiting on something, which is what it is. */

.ml-marker {
  display: inline-block;
  padding-inline: 6px;
  border: 1px dashed color-mix(in oklab, var(--wp--preset--color--cherry) 45%, transparent);
  border-radius: 3px;
  background: color-mix(in oklab, var(--wp--preset--color--cherry) 8%, transparent);
  color: var(--wp--preset--color--cherry-900);
  font-size: 0.88em;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

/* ── contact channels ── */

.ml-contact-card {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(16px, 2.5vw, 30px);
  margin-block: clamp(20px, 3vw, 28px) clamp(8px, 2vw, 16px);
  padding-block: clamp(20px, 2.6vw, 26px);
  border-block: var(--ml-rule);
}

.ml-contact-card p { margin-block: 0; font-size: var(--wp--preset--font-size--sm); }

.ml-contact-card strong {
  display: block;
  font-size: var(--wp--preset--font-size--xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--wp--preset--color--stone);
  margin-block-end: 7px;
}

.ml-contact-card em {
  display: block;
  font-style: normal;
  color: var(--wp--preset--color--graphite);
  font-size: var(--wp--preset--font-size--xs);
  margin-block-start: 5px;
}

@media (max-width: 781px) {
  .ml-contact-card { grid-template-columns: 1fr; gap: 20px; }
}

/* ═══ icons ═══════════════════════════════════════════════════════════════
   Inline SVG as CSS masks rather than markup in the page content. Three
   reasons: the icon takes its colour from the surrounding text, so it works
   on the paper pages and would work on a dark one; nothing has to be added to
   the block content, so a client editing a paragraph cannot delete half an
   <svg> and leave broken markup behind; and there is no icon font and no
   external request, which the CSP and the minimal-plugins rule both want.

   Stroke 1.6 on a 24px box, matching the header bag and account icons. */

.ml-i {
  --ml-icon-size: 17px;
}

:where([class*="ml-i-"])::before {
  content: "";
  display: inline-block;
  inline-size: var(--ml-icon-size, 17px);
  block-size: var(--ml-icon-size, 17px);
  margin-inline-end: 9px;
  vertical-align: -3px;
  background-color: currentColor;
  -webkit-mask: var(--ml-icon) center / contain no-repeat;
  mask: var(--ml-icon) center / contain no-repeat;
  flex: 0 0 auto;
}

.ml-i-mail { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Crect%20x=%223%22%20y=%225%22%20width=%2218%22%20height=%2214%22%20rx=%222%22/%3E%3Cpath%20d=%22m3.5%207.5%208.5%206%208.5-6%22/%3E%3C/svg%3E"); }
.ml-i-chat { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M21%2011.5a8.4%208.4%200%200%201-12.4%207.4L3%2020.5l1.7-5.6A8.4%208.4%200%201%201%2021%2011.5Z%22/%3E%3C/svg%3E"); }
.ml-i-camera { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Crect%20x=%223%22%20y=%223%22%20width=%2218%22%20height=%2218%22%20rx=%225%22/%3E%3Ccircle%20cx=%2212%22%20cy=%2212%22%20r=%224%22/%3E%3Ccircle%20cx=%2217.2%22%20cy=%226.8%22%20r=%221.1%22/%3E%3C/svg%3E"); }
.ml-i-truck { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M3%207h11v9H3z%22/%3E%3Cpath%20d=%22M14%2010.5h3.8L21%2013.6V16h-7z%22/%3E%3Ccircle%20cx=%227%22%20cy=%2218%22%20r=%222%22/%3E%3Ccircle%20cx=%2217.5%22%20cy=%2218%22%20r=%222%22/%3E%3C/svg%3E"); }
.ml-i-clock { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Ccircle%20cx=%2212%22%20cy=%2212%22%20r=%229%22/%3E%3Cpath%20d=%22M12%206.8V12l3.6%202.1%22/%3E%3C/svg%3E"); }
.ml-i-pin { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M12%2021s7-5.6%207-11a7%207%200%201%200-14%200c0%205.4%207%2011%207%2011Z%22/%3E%3Ccircle%20cx=%2212%22%20cy=%2210%22%20r=%222.6%22/%3E%3C/svg%3E"); }
.ml-i-exchange { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M3.5%208.5h13.5l-3.6-3.6%22/%3E%3Cpath%20d=%22M20.5%2015.5H7l3.6%203.6%22/%3E%3C/svg%3E"); }
.ml-i-shield { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M12%203%205%205.9v5.6c0%204.4%202.9%208%207%209.5%204.1-1.5%207-5.1%207-9.5V5.9Z%22/%3E%3Cpath%20d=%22m9.2%2012%202.1%202.1%204.1-4.1%22/%3E%3C/svg%3E"); }
.ml-i-check { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22m5%2012.8%204.2%204.2L19%207.2%22/%3E%3C/svg%3E"); }
.ml-i-building { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M4%2021V6.5L12%203l8%203.5V21%22/%3E%3Cpath%20d=%22M9.5%2021v-5h5v5%22/%3E%3Cpath%20d=%22M8.5%2010h2M13.5%2010h2%22/%3E%3C/svg%3E"); }
.ml-i-note { --ml-icon: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22%23000%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%3E%3Cpath%20d=%22M6%203h8l4%204v14H6z%22/%3E%3Cpath%20d=%22M14%203v4h4%22/%3E%3Cpath%20d=%22M9%2012h6M9%2016h4%22/%3E%3C/svg%3E"); }

/* ═══ iconed section headings ═══
   The icon sits outside the text column so multi-line headings stay aligned. */

.ml-page h2[class*="ml-i-"] {
  display: flex;
  align-items: center;
}

.ml-page h2[class*="ml-i-"]::before {
  --ml-icon-size: 21px;
  color: var(--wp--preset--color--cherry);
  margin-inline-end: 11px;
}

/* ═══ the facts tables ═══
   Two-column key/value on the shipping page, and the side-by-side terms on
   returns. Label column is fixed so rows line up down the page. */

.ml-legal-table { margin-block: clamp(14px, 2vw, 20px) clamp(20px, 3vw, 28px); }
.ml-legal-table table { table-layout: auto; }

.ml-legal-table.ml-legal-table tr:last-child :where(th, td) { border-block-end: 0; }

.ml-legal-table tbody td:first-child {
  color: var(--wp--preset--color--stone);
  inline-size: 34%;
  white-space: nowrap;
}

/* Returns puts two policies side by side. The first column is the label, and
   the two that follow are the change-of-mind terms and the statutory ones —
   which must read as distinct, never as one blended policy. */
.ml-returns-table tbody td:first-child { inline-size: 26%; }

.ml-returns-table thead th:nth-child(3) {
  color: var(--wp--preset--color--cherry);
}

.ml-returns-table tbody td:last-child {
  background: color-mix(in oklab, var(--wp--preset--color--cherry) 5%, transparent);
}

/* ═══ rights list ═══ */

.ml-rights {
  list-style: none;
  padding-inline-start: 0;
  margin-block: clamp(16px, 2.4vw, 22px) 0;
  display: grid;
  gap: 14px;
}

.ml-rights li {
  display: flex;
  align-items: flex-start;
  gap: 0;
  margin-block-end: 0;
  font-size: var(--wp--preset--font-size--sm);
}

.ml-rights li::before {
  --ml-icon: var(--ml-icon-check);
  --ml-icon-size: 18px;
  content: "";
  display: inline-block;
  inline-size: 18px;
  block-size: 18px;
  flex: 0 0 auto;
  margin-inline-end: 11px;
  margin-block-start: 2px;
  background-color: var(--wp--preset--color--cherry);
  -webkit-mask: var(--ml-icon-check) center / contain no-repeat;
  mask: var(--ml-icon-check) center / contain no-repeat;
}

/* ═══ contact channel card ═══ */

/* Icon above the label rather than beside it: the paragraph carries the
   ml-i-* class, and stacking keeps the three cards on one baseline however
   long a label or a placeholder chip turns out to be. */
.ml-contact-card p[class*="ml-i-"]::before {
  --ml-icon-size: 21px;
  display: block;
  margin-inline-end: 0;
  margin-block-end: 12px;
  color: var(--wp--preset--color--cherry);
}

@media (max-width: 600px) {
  .ml-legal-table :where(th, td) { font-size: var(--wp--preset--font-size--xs); }
  .ml-legal-table tbody td:first-child { white-space: normal; }
}

/* ═══ about ═══════════════════════════════════════════════════════════════
   An editorial page rather than a column of paragraphs. The photography is
   existing campaign work of the actual garments — see the note in
   setup/build-legal-pages.php for why nothing depicts a studio or a workroom. */

.ml-about-split { gap: clamp(24px, 4vw, 56px); margin-block-end: clamp(40px, 6vw, 72px); }

.ml-about-img img {
  aspect-ratio: 3 / 4;
  object-fit: cover;
  inline-size: 100%;
  block-size: auto;
  display: block;
}

.ml-about-img { margin-block: 0; }

/* The opening paragraph carries the page; it is set larger than the rest. */
.ml-page .ml-lede {
  font-size: clamp(18px, 1.6vw, 21px);
  line-height: 1.65;
  color: var(--wp--preset--color--cherry-900);
  margin-block-end: 1em;
}

/* Her strongest line, given room to be the pivot of the page. */
.ml-about-quote {
  margin-block: clamp(40px, 6vw, 76px);
  padding-block: clamp(30px, 4.5vw, 52px);
  border-block: var(--ml-rule);
  text-align: center;
}

.ml-page .ml-about-quote p {
  margin-block: 0;
  margin-inline: auto;
  max-inline-size: 34rem;
  font-size: clamp(20px, 2.6vw, 29px);
  line-height: 1.4;
  letter-spacing: -0.015em;
  color: var(--wp--preset--color--cherry);
  text-wrap: balance;
}

.ml-about-grid {
  gap: clamp(10px, 1.6vw, 20px);
  margin-block: clamp(26px, 4vw, 40px) clamp(34px, 5vw, 56px);
}

.ml-page .ml-about-close {
  font-size: clamp(18px, 1.6vw, 21px);
  color: var(--wp--preset--color--cherry-900);
  margin-block-end: clamp(20px, 3vw, 28px);
}

/* Columns collapse below 781px in core; the split reads better with the
   photograph first, which is the source order, so nothing is reversed. */
@media (max-width: 781px) {
  .ml-about-quote { text-align: start; }
  .ml-page .ml-about-quote p { margin-inline: 0; }
  .ml-about-grid { gap: 11px; }
}
