:root{
  --dusk: #EDE7F6;
  --dusk-deep: #E2EAF5;
  /* Page background: a barely-warm white so product photos stay the colour
     event on the page. --paper is pure white for cards sitting on top of it. */
  --cream: #FDFCFA;
  --paper: #FFFFFF;
  --gold: #F7E3AE;
  --thread: #D8735A;
  --sage: #A9C98F;
  --ink: #43392A;
  --ink-soft: #7C7160;
  /* Slightly cooler and stronger than the old cream line, so white cards
     still read as cards now that the page behind them is nearly white. */
  --line: #E8E2D8;
  --mint: #E8F4EF;
  --mint-deep: #8AC4B2;
  --coral-deep: #F0977E;
  --coral-deep-2: #E68268;
  /* Page pastels. NOT button fills - see the --cta-* block below. */
  --whatsapp-green: #8FC9A8;
  --whatsapp-green-deep: #78B594;

  /* Button fills. The pastels above are page colours: white text on them is
     about 2:1, which is why the shop's "Show N products" button read as
     greyed out. These are the same three hues carried far enough that white
     sits crisply on them (4.5:1 or better) - richer, not different. The
     pastels stay exactly as they are everywhere else. */
  --cta-coral: #C25436;
  --cta-coral-2: #A9462C;      /* hover */
  --cta-mint: #38826A;
  --cta-mint-2: #2F6E5A;
  --cta-whatsapp: #21795A;
  --cta-whatsapp-2: #1A6349;

  /* Shared design tokens. The admin portal mirrors these exactly so the two
     surfaces stay visually related. */
  --r-xs: 2px;      /* hairline bars - the burger's own strokes */
  --r-sm: 6px;      /* chips, small controls inside a card */
  --r-md: 9px;      /* inputs, selects, small buttons */
  --r-lg: 12px;     /* cards, media, primary buttons */
  --r-xl: 18px;     /* the bottom sheet's top corners */
  --r-pill: 999px;  /* tags, badges, filter pills */

  /* Spacing. Everything sits on a 4px grid, which is the thing a designer
     checks first: gaps that are 7, 9 and 11px apart read as unresolved even
     when no single one looks wrong. 52 is the one deliberate outlier - the
     wide two-column splits on the product page and Our Story. */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 24px; --sp-7: 28px; --sp-8: 32px; --sp-wide: 52px;

  /* Line-height. Six steps, from solid to long-form reading. */
  --lh-solid: 1;    --lh-display: 1.1;  --lh-heading: 1.25;
  --lh-ui: 1.4;     --lh-body: 1.55;    --lh-article: 1.75;

  /* Type scale. Nine steps and no half-pixels: every size on the site is one
     of these. There were 24 distinct values before, including 13.5 / 14.5 /
     16.5 sitting within half a pixel of each other - individually invisible,
     collectively the reason the page read as nudged rather than designed.
     Headings keep their own clamp() so they scale with the viewport
     (BRAND.md §3); this is everything else. Never write a raw px font-size. */
  --fs-3xs: 11px;    /* badges and the smallest pills */
  --fs-2xs: 12px;    /* counts, captions, meta lines */
  --fs-xs:  13px;    /* hint text, secondary labels */
  --fs-sm:  14px;    /* small UI text, card names, footer links */
  --fs-base:15px;    /* body default */
  --fs-md:  16px;    /* lede and long-form reading - also the floor that stops
                        iOS zooming the page when a field is focused */
  --fs-lg:  18px;    /* small headings, post titles */
  --fs-xl:  22px;    /* sub-headings, the handwritten accent */
  --fs-2xl: 28px;    /* the product price, the wordmark */

  /* Font stacks as tokens, with fallbacks chosen to be CLOSE to the real face
     rather than a bare `sans-serif`. The web fonts arrive from Google with
     `display=swap`, so the fallback is genuinely on screen for a moment on a
     first visit - and falling back to Arial, which is far wider than Karla,
     is what made the type visibly jump and look wrong mid-page. */
  --font-body: 'Karla', 'Segoe UI', system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;
  --font-display: 'Baloo 2', 'Segoe UI', system-ui, -apple-system, 'Trebuchet MS', sans-serif;
  --font-hand: 'Caveat', 'Segoe Script', 'Bradley Hand', cursive;

  /* Discount colour. Overridden at runtime from the admin setting, and kept
     clearly different from the mint used by tags so the two never blur. */
  --sale: #D8735A;

  /* The muted ramp, for anything that is switched off - a sold-out card, a
     skeleton, a disabled control. These were eight loose hexes invented at the
     point of use, which is exactly what a designer means by "undocumented
     greys": nothing said which was the background and which was the text. */
  --muted-bg:    #EFECE7;  /* the panel a sold-out card fades to */
  --muted-bg-2:  #E3DFD8;  /* its deepest edge */
  --muted-fill:  #EBE8E3;  /* a switched-off control */
  --muted-line:  #DFDBD4;  /* its border */
  --muted-ink:   #6E675C;  /* readable text on the ramp - 4.6:1 on --muted-bg */
  --muted-ink-2: #8A8378;  /* secondary text on the ramp */
  --muted-ink-3: #9A9287;  /* the quietest: a struck-through was-price */
  --muted-pill:  #B4ADA3;  /* a badge that has been stood down */
  --muted-on:    #F6F4F0;  /* text ON a muted-ink fill */
  --skeleton-a:  #F1EFEA;  /* the shimmer, dark stop */
  --skeleton-b:  #F9F8F5;  /* the shimmer, light stop */

  /* The four photo tints. A card with no photograph cycles through them so a
     grid never looks broken; the same set tints post covers and the "why"
     medallions, which is what makes those bands feel related. */
  --tint-mint:   #DCEFE8;
  --tint-blush:  #FBE1DA;
  --tint-butter: #FBEFC9;
  --tint-sage:   #E5EFD6;
  --mint-hover:  #C6E3DA;  /* a mint chip under the pointer */
  --gold-deep:   #D2A23C;  /* the burger's third bar */
  /* Instagram, as a pastel of its own rather than the brand gradient: a wash
     for the button, a mid tone for its border, the real magenta for the mark.
     Flat values on purpose - a gradient across a 2px stroke paints every part
     of the logo's squircle a different colour, so the outline stops reading as
     one shape. Ink on the wash is 9:1, and the mark on it 4.0:1, past the 3:1
     a meaningful icon needs. */
  --instagram-tint: #FBE7F1;
  --instagram-soft: #E79BC2;
  --instagram:      #D62976;

  /* The hexes still written literally below this line are deliberate, and a
     designer should read them as fixed rather than loose: the five wordmark
     letters and the five stitch-divider stripes are the logo's own palette
     (BRAND.md §2 - "never recolour these"), the Instagram button carries
     Instagram's brand gradient, and #fff is white. Everything else is a
     token. */

  /* Long-form article text: a touch softer than --ink, because 16px at 1.75
     over many lines is more comfortable slightly lifted off pure ink. */
  --ink-article: #54493A;
  /* Status text. --ok/--err here match the admin's tokens of the same name.
     --ok is #357C64 rather than the lighter #3E8E6E it was: at 13px bold on a
     card, "● In stock" measured 3.96:1, under the 4.5 that normal-size text
     needs. Measured, not eyeballed - scripts/checks/contrast-check.js. */
  --ok: #357C64;
  --err: #B4472C;
  --err-ink: #9C3D22;

  /* --thread is the brand accent, and at 3.14:1 on cream it is a LARGE-text
     and decorative colour only: the handwritten eyebrow, an underline, a
     hover border. Small text in the accent uses --thread-ink, which is the
     lightest value in the same family that clears 4.5:1. Exactly the rule the
     --cta-* fills already follow - a page accent is not a body-text colour. */
  --thread-ink: #BE5136;
}
*{box-sizing:border-box; margin:0; padding:0;}
html{scroll-behavior:smooth;}
/* The header is sticky, so an anchor jump must stop short of it or the top of
   the target section ends up hidden underneath. */
:root{--nav-h:84px;}
[id]{scroll-margin-top:calc(var(--nav-h) + 14px);}
@media (max-width:480px){ :root{--nav-h:70px;} }
/* `clip`, not `hidden`. Both stop sideways scrolling, but `hidden` turns the
   element into a scroll container - and a sticky header inside a scroll
   container that never scrolls just sits there, so `position:sticky` on the
   nav quietly stops working. `clip` does not create one. */
html,body{max-width:100%; overflow-x:clip;}
body{background:var(--cream); color:var(--ink); font-family:var(--font-body); line-height:1.55; -webkit-font-smoothing:antialiased;}
h1,h2,h3{font-family:var(--font-body); font-weight:700; line-height:1.1; letter-spacing:-0.01em;}
a{color:inherit; text-decoration:none;}
button{font:inherit; color:inherit; background:none; border:none; cursor:pointer;}
img{max-width:100%; display:block;}
.hand{font-family:var(--font-hand); font-weight:600;}
.wrap{max-width:1180px; margin:0 auto; padding-inline:28px;}
.eyebrow-hand{font-family:var(--font-hand); font-size:var(--fs-xl); font-weight:600; color:var(--thread); display:inline-block; margin-bottom:6px;}
.visually-hidden{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;}

/* Skip link: invisible until a keyboard reaches it, then a real button that
   jumps past the header, both searches and the nav. */
.skip-link{
  position:absolute; top:-48px; left:16px; z-index:300;
  padding:10px 18px; border-radius:var(--r-md);
  background:var(--ink); color:var(--cream); font-size:var(--fs-sm); font-weight:700;
  transition:top .15s ease;
}
.skip-link:focus-visible{top:10px;}

/* One visible focus ring for everything interactive, in the brand's own mint -
   BRAND.md promises this and only the WhatsApp FAB ever had it. The two search
   pills keep their :focus-within treatment instead (their inputs opt out). */
a:focus-visible, button:focus-visible, select:focus-visible,
input[type="checkbox"]:focus-visible, [role="option"]:focus-visible{
  outline:3px solid var(--mint-deep); outline-offset:2px;
}

/* ---------- Preview bar ----------
   Shown ONLY to a browser holding a preview cookie while the shop is closed.
   Without it, closing the shop and refreshing showed the real site and looked
   exactly like the toggle had failed - the one person who cannot tell the shop
   is closed is the person who closed it. */
.preview-bar{
  display:flex; align-items:center; justify-content:center; gap:8px 16px; flex-wrap:wrap;
  background:var(--gold); color:var(--ink); text-align:center;
  padding:9px 16px; font-size:var(--fs-xs); font-weight:600;
}
.preview-bar a{
  color:var(--thread-ink); font-weight:700;
  text-decoration:underline; text-underline-offset:2px; white-space:nowrap;
}
.preview-bar a:hover{color:var(--ink);}

/* ---------- Announcement bar ---------- */
.announce{display:none; background:var(--ink); color:var(--cream); text-align:center; padding:9px 16px; font-size:var(--fs-xs); font-weight:600;}
.announce.on{display:block;}

/* ---------- Stitch divider ---------- */
.stitch{
  height:2px; opacity:.8;
  background-image: repeating-linear-gradient(to right,
    #8AC4B2 0 8px, transparent 8px 15px,
    #E5947A 15px 23px, transparent 23px 30px,
    #E0BC66 30px 38px, transparent 38px 45px,
    #C2B6E4 45px 53px, transparent 53px 60px,
    #A8CB96 60px 68px, transparent 68px 75px
  );
}

/* ---------- Buttons ----------
   Buttons keep WHITE text and take their fill from the --cta-* tokens, which
   are the brand hues carried deep enough for white to sit crisply on them. The
   page pastels are not button fills: white on --coral-deep is about 2:1, which
   is why the shop's "Show N products" button read as a greyed-out, disabled
   control. A new filled variant needs a --cta-* fill, not a pastel one. */
.tag-btn{
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  padding:14px 26px; font-size:var(--fs-sm); font-weight:700; font-family:var(--font-body);
  border-radius:var(--r-lg); border:1.5px solid transparent; box-sizing:border-box; line-height:1;
  transition:transform .18s ease, background .18s ease, border-color .18s ease, color .18s ease, box-shadow .18s ease;
}
.tag-btn:hover{transform:translateY(-2px);}
/* Pressed state, so the lift on hover reads as a real, physical button. */
.tag-btn:active{transform:translateY(0);}
/* Ink on the coral, not white. White on a pastel is about 2:1 and reads as a
   disabled control - the "Show N products" button in the filter panel looked
   greyed out. Ink on the same coral is 5.3:1 and keeps the brand fill. */
.tag-btn-gold{background:var(--cta-coral); color:#fff; border-color:var(--cta-coral);}
.tag-btn-gold:hover{background:var(--cta-coral-2); border-color:var(--cta-coral-2); color:#fff;}
.tag-btn-outline{background:var(--cta-mint); border-color:var(--cta-mint); color:#fff;}
.tag-btn-outline:hover{background:var(--cta-mint-2); border-color:var(--cta-mint-2); color:#fff;}

/* A filled button carries a soft shadow in its own hue rather than a grey one,
   so it lifts off the near-white page without turning into a drop-shadow box.
   Hover deepens it; the press flattens it. Subtle on purpose. */
.tag-btn-gold{box-shadow:0 3px 10px rgba(194,84,54,0.22);}
.tag-btn-gold:hover{box-shadow:0 7px 18px rgba(194,84,54,0.30);}
.tag-btn-gold:active{box-shadow:0 2px 6px rgba(194,84,54,0.22);}
.tag-btn-outline{box-shadow:0 3px 10px rgba(56,130,106,0.22);}
.tag-btn-outline:hover{box-shadow:0 7px 18px rgba(56,130,106,0.30);}
.tag-btn-outline:active{box-shadow:0 2px 6px rgba(56,130,106,0.22);}
/* WhatsApp mark, so every button that opens WhatsApp says so at a glance. */
.wa-icon{width:17px; height:17px; flex-shrink:0;}

/* Floating WhatsApp button. Round and small, on every page, with a label that
   slides out on hover so it is obvious what it does without shouting. */
.wa-fab{
  position:fixed; right:22px; bottom:22px; z-index:90;
  display:flex; align-items:center; gap:0;
  height:54px; width:54px; padding:0;
  border-radius:var(--r-pill); overflow:hidden;
  background:var(--cta-whatsapp); color:#fff;
  box-shadow:0 4px 14px rgba(33,121,90,.26), 0 1px 3px rgba(67,57,42,.12);
  transition:width .26s cubic-bezier(.3,.8,.4,1), background .18s ease, transform .18s ease;
}
.wa-fab .wa-icon{width:26px; height:26px; margin:0 14px; flex-shrink:0;}
.wa-fab-label{
  white-space:nowrap; font-size:var(--fs-base); font-weight:700;
  opacity:0; transition:opacity .18s ease .06s; padding-right:20px;
}
.wa-fab:hover{width:186px; background:var(--cta-whatsapp-2); transform:translateY(-2px); box-shadow:0 8px 22px rgba(33,121,90,.32), 0 1px 3px rgba(67,57,42,.12);}
.wa-fab:hover .wa-fab-label{opacity:1;}
.wa-fab:focus-visible{outline:3px solid var(--mint-deep); outline-offset:3px;}
/* A soft pulse on arrival, once, so it is noticed and then leaves you alone. */
@keyframes wa-pop{0%{transform:scale(0); opacity:0;}60%{transform:scale(1.12);}100%{transform:scale(1); opacity:1;}}
.wa-fab{animation:wa-pop .45s cubic-bezier(.3,1.2,.5,1) .6s both;}
@media (max-width:480px){
  .wa-fab{right:16px; bottom:16px; height:50px; width:50px;}
  .wa-fab .wa-icon{width:24px; height:24px; margin:0 13px;}
  /* No hover on touch, so never expand and cover the page. */
  .wa-fab:hover{width:50px;}
  .wa-fab:hover .wa-fab-label{opacity:0;}
}
@media (prefers-reduced-motion: reduce){
  .wa-fab{animation:none; transition:background .18s ease;}
}
.tag-btn-dark{background:var(--cta-whatsapp); color:#fff; border-color:var(--cta-whatsapp); box-shadow:0 3px 10px rgba(33,121,90,0.24);}
.tag-btn-dark:hover{background:var(--cta-whatsapp-2); border-color:var(--cta-whatsapp-2); box-shadow:0 7px 18px rgba(33,121,90,0.32);}
.tag-btn-dark:active{box-shadow:0 2px 6px rgba(33,121,90,0.24);}

/* ---------- Nav ---------- */
/* The header stays put, at every scroll position. It is sticky and nothing
   hides it - `overflow-x:clip` rather than `hidden` on html/body is what makes
   that actually work (hidden turns body into a scroll container and sticky
   quietly stops sticking). Do not add hide-on-scroll here. */
/* The header is frosted glass ONLY where the browser can actually frost it.
   It used to be 92% opaque with an unprefixed `backdrop-filter`, which Safari
   and Android WebView ignore - leaving a see-through bar with nothing blurring
   what passed behind it. A product photo, or a long-press selection highlight,
   showed straight through the header and looked like it was painted on top.
   So: opaque by default, translucent only where the blur is supported. */
header.nav{
  position:sticky; top:0; z-index:50;
  background:var(--cream);
  border-bottom:1px solid var(--line);
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  header.nav{
    background:rgba(253,252,250,0.92);
    -webkit-backdrop-filter:blur(10px);
    backdrop-filter:blur(10px);
  }
}
.nav-inner{display:flex; align-items:center; justify-content:space-between; padding:16px 28px; max-width:1180px; margin:0 auto;}
.logo{display:flex; align-items:center; gap:12px;}
.logo-mark{width:50px; height:50px; flex-shrink:0;}
/* Wordmark: "jugnu" in the logo's pastel palette, letter for letter.
   The mark above the final u is a heart - the logo has no accent. */
.wordmark{font-family:var(--font-display); font-size:var(--fs-2xl); font-weight:800; letter-spacing:-0.005em; display:inline-flex; line-height:1;}
.wordmark .l1{color:#7FC3B4;}
.wordmark .l2{color:#F0C36B;}
.wordmark .l3{color:#F09080;}
.wordmark .l4{color:#B9A9DC;}
.wordmark .l5{color:#A9C98F; position:relative;}
.wordmark .l5::after{
  content:''; position:absolute; top:-0.30em; right:-0.10em;
  width:0.30em; height:0.30em; pointer-events:none;
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 22'%3E%3Cpath fill='%23F2A196' d='M12 21.4 3.6 12.7A5.6 5.6 0 0 1 12 5.3a5.6 5.6 0 0 1 8.4 7.4z'/%3E%3C/svg%3E") no-repeat center/contain;
}
.nav-links{display:flex; align-items:center; gap:32px; font-size:var(--fs-base); font-weight:600;}

/* ---------- Header search ----------
   Search sits in the header on every page instead of only in the homepage
   hero, so it is one reach away wherever someone is. Deliberately small: it is
   a way in for people who already know what they want, not the loudest thing
   on the page. It has no submit button - Enter runs it, and the magnifier says
   what it is. */
.nav-search{
  display:flex; align-items:center; position:relative; flex-shrink:1; min-width:0;
  width:190px; background:var(--paper);
  border:1.5px solid var(--line); border-radius:var(--r-pill);
  padding:3px 6px 3px 14px; margin-right:4px;
  transition:border-color .16s ease, box-shadow .16s ease, width .18s ease;
}
.nav-search:focus-within{
  border-color:var(--mint-deep); box-shadow:0 0 0 3px var(--mint); width:230px;
}
.nav-search input{
  flex:1; min-width:0; border:none; background:none; font-family:inherit;
  /* 400, not 500: the stylesheet only loads Karla 400/600/700, and asking for
     a weight that was not fetched makes the browser synthesise one - which is
     why the search field looked subtly unlike the rest of the page. */
  font-size:var(--fs-sm); font-weight:400; color:var(--ink); padding:5px 8px 5px 0;
}

/* The magnifier IS the button - one icon, and it is the thing you press. A
   decorative one on the left as well would be two magnifiers saying different
   things: only one of them does anything. */
.search-go{
  flex-shrink:0; display:flex; align-items:center; justify-content:center;
  width:28px; height:28px; border-radius:var(--r-pill);
  background:var(--cta-coral); color:#fff;
  box-shadow:0 2px 6px rgba(194,84,54,0.24);
  transition:background .16s ease, box-shadow .16s ease, transform .16s ease;
}
.search-go svg{width:15px; height:15px; stroke:currentColor; fill:none;}
.search-go:hover{background:var(--cta-coral-2); box-shadow:0 4px 11px rgba(194,84,54,0.32);}
.search-go:active{transform:scale(.94); box-shadow:0 1px 4px rgba(194,84,54,0.24);}
.search-go-lg{width:36px; height:36px;}
.search-go-lg svg{width:18px; height:18px;}
.nav-search input:focus{outline:none;}
.nav-search input::placeholder{color:var(--ink-soft); font-weight:400;}
/* The copy inside the burger menu: full width, and never the growing kind -
   there is no room beside it to grow into. */
.nav-search[hidden]{display:none;}
.nav-search-mobile{display:none; width:100%; margin:14px 0 4px;}
.nav-search-mobile:focus-within{width:100%;}
.nav-search-mobile input{font-size:var(--fs-base); padding:10px 14px 10px 0;}

/* ---------- Search suggestions ----------
   The same panel under every search box. Deliberately plain: one product name
   per row and a last row that runs the full search. No thumbnail, no price, no
   category column - each of those stole width from the name until every
   suggestion ended in an ellipsis, which is the one thing this list must not
   do. The panel is allowed to be wider than the input for the same reason. */
.suggest{
  position:absolute; top:calc(100% + 7px); left:0; z-index:70;
  min-width:100%; width:max-content; max-width:min(340px, 86vw);
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-md);
  box-shadow:0 10px 30px rgba(67,57,42,.13), 0 2px 5px rgba(67,57,42,.07);
  padding:5px;
  /* Once there are more matches than fit, the panel scrolls inside itself
     rather than running off the bottom of the screen. Capped against the
     viewport, not a fixed pixel count, so it behaves on a short phone in
     landscape as well as on a desktop. overscroll-behavior stops the page
     underneath from scrolling on when the list reaches its end. */
  max-height:min(52vh, 330px); overflow-y:auto; overflow-x:hidden;
  overscroll-behavior:contain; -webkit-overflow-scrolling:touch;
}
.suggest::-webkit-scrollbar{width:7px;}
.suggest::-webkit-scrollbar-track{background:transparent;}
.suggest::-webkit-scrollbar-thumb{background:var(--line); border-radius:var(--r-pill);}
.suggest::-webkit-scrollbar-thumb:hover{background:var(--mint-deep);}
.suggest[hidden]{display:none;}
/* No rules between rows - the hover block is enough separation and a stack of
   hairlines was the bulk of what made the panel look heavy. */
.suggest-item{
  display:block; padding:8px 10px; border-radius:var(--r-sm);
  font-size:var(--fs-sm); line-height:1.4; color:var(--ink);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  transition:background .12s ease;
}
/* Hover and the arrow keys share one highlight, so there is only ever one. */
.suggest-item.on{background:var(--mint);}
.suggest-all{color:var(--thread-ink); font-weight:700;}

/* Every search form anchors its own panel. */
.nav-search, .toolbar-search{position:relative;}
.nav-links a{position:relative; padding:4px 0;}
.nav-links a::after{content:''; position:absolute; left:0; bottom:-2px; width:0; height:2px; background:var(--thread); transition:width .22s ease;}
.nav-links a:hover::after{width:100%;}
/* common.js adds .current to the link matching the page being viewed. */
.nav-links a.current{color:var(--thread-ink);}
.nav-links a.current::after{width:100%;}
.mobile-menu a.current{color:var(--thread-ink);}
.burger{display:none; width:40px; height:40px; border-radius:var(--r-lg); background:var(--mint); border:1.5px solid var(--line); align-items:center; justify-content:center; flex-direction:column; gap:4px; transition:transform .15s ease, background .15s ease;}
.burger:active{transform:scale(0.92);}
.burger.open{background:var(--tint-blush);}
.burger .bar{display:block; height:2.5px; border-radius:var(--r-xs); transition:background .15s ease;}
.burger .bar1{width:16px; background:var(--mint-deep);}
.burger .bar2{width:20px; background:var(--thread);}
.burger .bar3{width:12px; background:var(--gold-deep);}
.mobile-menu{display:none; flex-direction:column; padding:0 28px 18px; background:var(--cream);}
.mobile-menu a{padding:12px 4px; border-top:1px solid var(--line); font-weight:600; font-size:var(--fs-base);}
.mobile-menu .nav-search-mobile{display:flex;}
.mobile-menu .nav-search-mobile[hidden]{display:none;}
.mobile-menu.open{display:flex;}

/* ---------- Hero ---------- */
.hero{position:relative; background:radial-gradient(120% 100% at 50% 0%, var(--dusk) 0%, var(--dusk-deep) 100%); color:var(--ink); overflow:hidden; padding:76px 0 64px; text-align:center;}
.firefly-field{position:absolute; inset:0; pointer-events:none;}
.firefly{position:absolute; width:5px; height:5px; border-radius:50%; background:radial-gradient(circle, rgba(240,185,61,0.95), rgba(240,185,61,0)); box-shadow:0 0 10px 3px rgba(240,185,61,0.5); animation:drift 8s ease-in-out infinite, twinkle 3.4s ease-in-out infinite;}
.sparkle{position:absolute; width:4px; height:4px; border-radius:50%; animation:twinkle 2.8s ease-in-out infinite;}
@keyframes drift{0%{transform:translate(0,0);}50%{transform:translate(12px,-20px);}100%{transform:translate(0,0);}}
@keyframes twinkle{0%,100%{opacity:.2;}50%{opacity:1;}}
.hero-inner{position:relative; z-index:2; max-width:660px; margin:0 auto;}
.hero h1{font-size:clamp(32px,5vw,50px); margin:8px 0 16px;}
.hero p.lede{font-size:var(--fs-md); color:rgba(67,57,42,0.72); max-width:460px; margin:0 auto 30px;}
.btn-row{display:flex; align-items:center; gap:16px; flex-wrap:wrap; justify-content:center;}

/* ---------- Promo banner (alternative homepage hero) ---------- */
.hero-promo{background:var(--cream);}
.promo-inner{display:block; max-width:1180px; margin:0 auto; padding-inline:28px;}
.promo-inner img{width:100%; height:auto; border-radius:var(--r-lg); display:block;}
a.promo-inner{transition:transform .2s ease, filter .2s ease;}
a.promo-inner:hover{transform:translateY(-3px); filter:brightness(1.02);}
/* Home for #hero-inner when an offer image has replaced the firefly hero. The
   moved block is itself a .wrap and brings its own gutters, so this element is
   deliberately NOT a .wrap - nesting two would double the padding. The centring
   lives on .hero, which the block has just left, so it is restated here. */
.promo-actions{padding-block:30px 6px; text-align:center;}
.promo-actions[hidden]{display:none;}
@media (max-width:560px){ .promo-actions{padding-block:22px 4px;} }

@media (max-width:920px){ .promo-inner{padding-inline:24px;} }
@media (max-width:480px){ .promo-inner{padding-inline:20px;} .promo-inner img{border-radius:var(--r-md);} }

/* ---------- Sub-page hero (shop, product) ---------- */
.page-hero{position:relative; background:radial-gradient(120% 100% at 50% 0%, var(--dusk) 0%, var(--dusk-deep) 100%); padding:46px 0 40px; overflow:hidden;}
.page-hero .wrap{position:relative; z-index:2;}
.page-hero h1{font-size:clamp(28px,4vw,40px); margin:10px 0 10px;}
.page-hero-lede{font-size:var(--fs-md); color:rgba(67,57,42,0.72); max-width:520px;}
.breadcrumb{font-size:var(--fs-xs); color:var(--ink-soft); display:flex; align-items:center; gap:8px; flex-wrap:wrap;}
.breadcrumb a:hover{color:var(--thread-ink); text-decoration:underline;}
.breadcrumb span{opacity:.5;}
.breadcrumb strong{color:var(--ink); font-weight:700;}

/* ---------- Shop layout ----------
   The filters are behind a button at EVERY width, not just on phones: the
   products get the whole row, and the panel is opened only when someone
   actually wants to narrow things down. Search is the exception - it stays
   out in the toolbar, because it is what people reach for first.
   Desktop opens it as a drawer down the left; phones get a bottom sheet
   (see the 920px block). */
.shop-layout{display:block;}
.shop-layout > *{min-width:0;}
.shop-filters{
  position:fixed; top:0; left:0; bottom:0; z-index:100; width:min(360px, 92vw);
  background:var(--paper); border-right:1px solid var(--line);
  box-shadow:8px 0 40px rgba(67,57,42,.18);
  transform:translateX(-100%);
  /* visibility, not just a transform: an off-screen panel that is merely moved
     stays in the tab order, and a keyboard user had to walk the whole invisible
     panel to reach the first product. The delay lets the slide-out finish
     before the panel blinks away; JS pairs this with `inert`. */
  visibility:hidden;
  transition:transform .26s cubic-bezier(.3,.9,.4,1), visibility 0s linear .26s;
  display:flex; flex-direction:column; gap:0; padding:0;
}
.shop-filters.open{transform:translateX(0); visibility:visible; transition:transform .26s cubic-bezier(.3,.9,.4,1);}
.filter-title{font-family:var(--font-body); font-size:var(--fs-xs); font-weight:700; text-transform:uppercase; letter-spacing:.07em; color:var(--ink-soft); margin-bottom:11px;}
.filter-search{width:100%; padding:10px 13px; border:1.5px solid var(--line); border-radius:var(--r-md); background:var(--paper); font-family:inherit; font-size:var(--fs-base);}
.filter-search:focus{outline:none; border-color:var(--mint-deep);}
/* The panel (.sheet-body) is the ONE scroller inside the filters, and nothing
   in here gets its own overflow. These two used to, and that is what broke the
   wheel: a later rule sets max-height:none, so each became a scroll container
   with nothing to scroll - and `overscroll-behavior:contain` on such a box
   stops the wheel chaining out to the panel underneath it. The category list
   covers most of the panel, so pointing at it killed the scroll. */
.filter-list{display:flex; flex-direction:column; gap:4px;}
.filter-icon{width:17px; height:17px; flex-shrink:0; color:var(--mint-deep); stroke:currentColor; fill:none; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round;}
/* The counts are what the whole "how many if I tick this" design leans on -
   they were at opacity .6 over ink-soft, roughly 2.2:1, the least legible text
   in the panel. Full-strength ink-soft keeps them quiet without hiding them. */
.filter-count{font-size:var(--fs-2xs); font-weight:700; color:var(--ink-soft);}
/* Every facet is now a list of tick boxes, so the row is the hit target: the
   whole thing is a <label>, padded like the old .filter-item it replaces. */
.filter-check{
  display:flex; align-items:center; gap:8px; width:100%;
  padding:9px 11px; border-radius:var(--r-lg);
  font-size:var(--fs-sm); font-weight:600; color:var(--ink-soft); cursor:pointer;
  transition:background .15s, color .15s;
}
.filter-check:hover{background:var(--paper); color:var(--ink);}
.filter-check.on{background:var(--mint); color:var(--ink);}
.filter-check input{width:17px; height:17px; flex-shrink:0; accent-color:var(--cta-mint); cursor:pointer; margin:0;}
.filter-check .filter-label{flex:1; min-width:0;}
.filter-check.on .filter-icon{color:var(--thread);}

/* The facet heading and its Clear, which only appears once something is on. */
.filter-head{display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin-bottom:11px;}
.filter-head .filter-title{margin-bottom:0;}
.filter-clear{font-size:var(--fs-xs); font-weight:700; color:var(--thread-ink); padding:8px 6px; margin:-6px -6px -6px 0;}
.filter-clear:hover{text-decoration:underline;}
.filter-clear[hidden]{display:none;}
.filter-note{font-size:var(--fs-xs); color:var(--ink-soft); margin:-4px 0 9px; line-height:1.4;}
/* The single clear control. It sits at the end of the active-filter chips, so
   it is on screen exactly when there is something to clear, and it is a
   button rather than another chip so it does not read as one more filter. */
.chip-clear-all{
  display:inline-flex; align-items:center; padding:6px 14px; border-radius:var(--r-pill);
  border:1.5px solid var(--line); background:var(--paper); color:var(--thread);
  font-size:var(--fs-xs); font-weight:700; transition:background .15s, border-color .15s;
}
.chip-clear-all:hover{background:var(--cream); border-color:var(--thread);}

/* The panel chrome, at every width. */
.filter-open{
  display:inline-flex; align-items:center; gap:8px; padding:10px 16px;
  border:1.5px solid var(--line); border-radius:var(--r-pill); background:var(--paper);
  font-size:var(--fs-sm); font-weight:700; color:var(--ink); flex-shrink:0;
}
.filter-open svg{width:17px; height:17px;}
.filter-open:hover{border-color:var(--mint-deep);}
/* Setting `display` here beats the browser's own [hidden]{display:none}, so
   the badge has to opt back out or it shows as an empty green dot even with no
   filters on. Any class that sets `display` and is toggled with `hidden`
   needs this line - check:mobile now enforces it. */
.filter-badge[hidden]{display:none;}
.filter-badge{
  display:inline-flex; align-items:center; justify-content:center; min-width:20px; height:20px;
  padding:0 6px; border-radius:var(--r-pill); background:var(--cta-mint); color:#fff;
  font-size:var(--fs-2xs); font-weight:700;
}
.sheet-backdrop{
  display:block; position:fixed; inset:0; background:rgba(67,57,42,.42);
  z-index:99; opacity:0; pointer-events:none; transition:opacity .22s ease;
}
.sheet-backdrop.open{opacity:1; pointer-events:auto;}
.sheet-head{
  display:flex; align-items:center; justify-content:space-between; position:relative;
  padding:18px 20px 14px; border-bottom:1px solid var(--line); flex-shrink:0;
}
.sheet-head h2{font-size:var(--fs-lg);}
.sheet-close{width:34px; height:34px; border-radius:var(--r-pill); font-size:var(--fs-base); color:var(--ink-soft); display:flex; align-items:center; justify-content:center;}
.sheet-close:hover{background:var(--cream); color:var(--ink);}
/* min-height:0 is load-bearing. A flex item defaults to min-height:auto, which
   refuses to shrink below its content - so this grew past the fixed panel and
   overflow-y:auto never had anything to do, and the wheel did nothing. It only
   showed up once the facet list got taller than the drawer. */
.sheet-body{
  flex:1; min-height:0; min-width:0;
  display:flex; flex-direction:column; gap:28px; padding:20px;
  overflow-y:auto; overscroll-behavior:contain;
}
.sheet-body::-webkit-scrollbar{width:8px;}
.sheet-body::-webkit-scrollbar-track{background:transparent;}
.sheet-body::-webkit-scrollbar-thumb{background:var(--line); border-radius:var(--r-pill);}
.sheet-body::-webkit-scrollbar-thumb:hover{background:var(--mint-deep);}
.sheet-foot{
  display:flex; gap:12px; padding:14px 20px calc(14px + env(safe-area-inset-bottom));
  border-top:1px solid var(--line); background:var(--paper); flex-shrink:0;
}
.sheet-foot .tag-btn{flex:1; padding:14px 12px; width:100%; min-width:0;}
/* Clear all is the lesser of the two: it gets the narrower share. */
.sheet-foot #sheet-clear{flex:0 1 38%;}
body.sheet-open{overflow:hidden;}

/* Inside a panel a vertical list beats a cramped horizontal strip. */
.filter-list{flex-direction:column; gap:4px; padding-right:0;}
.tag-cloud{flex-wrap:wrap;}

/* The search box that stays out on the page. */
/* One pill holding the magnifier, the field and the Search button - the same
   shape as the header search, because they are now the same control. */
.toolbar-search{
  display:flex; align-items:center; flex:1 1 260px; min-width:0; position:relative;
  background:var(--paper); border:1.5px solid var(--line); border-radius:var(--r-pill);
  /* Right padding is not "more is better" here. The pill's end is a semicircle
     of radius R (half the height); the button is a circle of radius r. Clearance
     is greatest when their centres coincide, i.e. padding-right = R - r, and
     falls off in BOTH directions - too much padding walks the button back
     towards the narrowing part of the curve. R is ~23.5 and r is 18, so 6px. */
  padding:4px 6px 4px 18px; transition:border-color .16s ease, box-shadow .16s ease;
}
.toolbar-search:focus-within{border-color:var(--mint-deep); box-shadow:0 0 0 3px var(--mint);}
.toolbar-search .filter-search{
  flex:1; min-width:0; width:auto; border:none; background:none;
  border-radius:0; padding:6px 10px 6px 0;
}
.toolbar-search .filter-search:focus{outline:none; border-color:transparent;}

.shop-toolbar{display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; padding-bottom:16px; border-bottom:1px solid var(--line); margin-bottom:20px;}
.result-count{font-size:var(--fs-sm); font-weight:700; color:var(--ink-soft);}
.sort-wrap{display:flex; align-items:center; gap:8px; font-size:var(--fs-sm); font-weight:600; color:var(--ink-soft);}
.sort-wrap select{padding:8px 11px; border:1.5px solid var(--line); border-radius:var(--r-lg); background:var(--paper); font-family:inherit; font-size:var(--fs-sm); font-weight:600; color:var(--ink); cursor:pointer;}
.sort-wrap select:focus{outline:none; border-color:var(--mint-deep);}
.active-chips{display:flex; gap:8px; flex-wrap:wrap; margin-bottom:18px;}
.active-chips:empty{display:none;}
.chip{display:inline-flex; align-items:center; gap:8px; padding:6px 12px; border-radius:var(--r-pill); background:var(--mint); color:var(--ink); font-size:var(--fs-xs); font-weight:700;}
.chip span{opacity:.55; font-size:var(--fs-sm);}
.chip:hover{background:var(--mint-hover);}
.product-grid-3{grid-template-columns:repeat(3,1fr);}

/* ---------- Tags ---------- */
.tag-cloud{display:flex; flex-wrap:wrap; gap:8px;}
.tag-pill{display:inline-flex; align-items:center; gap:8px; padding:8px 13px; border-radius:var(--r-pill); border:1.5px solid var(--line); background:var(--paper); font-size:var(--fs-xs); font-weight:700; color:var(--ink-soft); transition:background .15s, color .15s, border-color .15s;}
.tag-pill span{font-size:var(--fs-3xs); opacity:.85;}
.tag-pill:hover{border-color:var(--mint-deep); color:var(--ink);}
/* Selected state on the CTA mint, not the pastel: white on --mint-deep is
   about 1.9:1, which made the CHOSEN tag the least legible thing on the page.
   Same rule as every filled control - pastels are backgrounds, never fills. */
.tag-pill.on{background:var(--cta-mint); border-color:var(--cta-mint); color:#fff;}
.tag-pill.on span{opacity:.9;}
/* ---------- Size picker on the product page ----------
   Opt-in per product: clothes carry age ranges, other infant-care products can
   carry options like "250 ml", most non-clothing items have none. The chosen
   size travels in the WhatsApp order message. */
.pdp-sizes{margin:18px 0 4px;}
.pdp-sizes-label{display:block; font-family:var(--font-body); font-size:var(--fs-xs); font-weight:700; text-transform:uppercase; letter-spacing:.07em; color:var(--ink-soft); margin-bottom:9px;}
.pdp-size-list{display:flex; flex-wrap:wrap; gap:8px;}
.size-pill{
  display:inline-flex; align-items:center; justify-content:center;
  min-width:44px; min-height:40px; padding:8px 15px;
  border:1.5px solid var(--line); border-radius:var(--r-md); background:var(--paper);
  font-size:var(--fs-sm); font-weight:700; color:var(--ink);
  transition:background .15s ease, border-color .15s ease, color .15s ease;
}
.size-pill:hover{border-color:var(--mint-deep);}
/* Selected on the CTA mint - the same "filled means chosen" rule as everywhere. */
.size-pill.on{background:var(--cta-mint); border-color:var(--cta-mint); color:#fff;}
/* Sold out stays visible - "6-12m is gone" is information - but not pressable. */
.size-pill.out{color:var(--ink-soft); background:var(--cream); border-style:dashed; text-decoration:line-through; text-decoration-thickness:1px; cursor:default;}

.pdp-tags{display:flex; flex-wrap:wrap; gap:8px; margin-bottom:22px;}
/* Deliberately quiet, and last: an exit, not an offer. */
.pdp-back-link{
  display:inline-block; font-size:var(--fs-sm); font-weight:600; color:var(--ink-soft);
  margin-top:26px; text-decoration:underline; text-underline-offset:3px;
  text-decoration-color:var(--line);
}
.pdp-back-link:hover{color:var(--thread-ink); text-decoration-color:currentColor;}
.pdp-wa-btn{width:100%;}
/* Empty for shoppers; only the "no WhatsApp number saved" warning fills it. */
.pdp-wa-note{font-size:var(--fs-xs); color:var(--ink-soft); margin:-6px 0 14px;}
.pdp-wa-note:empty{display:none; margin:0;}

/* ---------- Blog ---------- */
.post-grid{display:grid; grid-template-columns:repeat(3,1fr); gap:28px 24px;}
.post-card{display:flex; flex-direction:column; background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg); overflow:hidden; transition:transform .2s ease, border-color .2s ease;}
.post-card:hover{transform:translateY(-3px); border-color:var(--mint-deep);}
.post-cover{display:block; aspect-ratio:16/10; background:var(--tint-mint); overflow:hidden; display:flex; align-items:center; justify-content:center;}
.post-cover img{width:100%; height:100%; object-fit:cover;}
.post-cover svg{width:32%; opacity:.28; color:var(--ink);}
.post-card:nth-child(4n+2) .post-cover{background:var(--tint-blush);}
.post-card:nth-child(4n+3) .post-cover{background:var(--tint-butter);}
.post-card:nth-child(4n+4) .post-cover{background:var(--tint-sage);}
.post-body{padding:18px 20px 20px; display:flex; flex-direction:column; flex:1;}
.post-tag{display:inline-block; align-self:flex-start; font-size:var(--fs-3xs); font-weight:700; text-transform:uppercase; letter-spacing:.07em; color:var(--thread-ink); background:var(--tint-blush); padding:4px 9px; border-radius:var(--r-pill); margin-bottom:10px;}
.post-title{font-size:var(--fs-lg); line-height:1.25; margin-bottom:8px;}
.post-title a:hover{color:var(--thread-ink);}
.post-excerpt{font-size:var(--fs-sm); color:var(--ink-soft); margin-bottom:14px;}
.post-meta{margin-top:auto; font-size:var(--fs-xs); color:var(--ink-soft); display:flex; align-items:center; gap:8px; flex-wrap:wrap;}
.dot-sep{opacity:.45;}
/* The newest post spans the row with a side-by-side layout. */
.post-card.featured{grid-column:1/-1; flex-direction:row;}
.post-card.featured .post-cover{flex:1 1 52%; aspect-ratio:auto; min-height:280px;}
.post-card.featured .post-body{flex:1 1 48%; justify-content:center; padding:32px 34px;}
.post-card.featured .post-title{font-size:clamp(22px,2.6vw,30px);}
.post-card.featured .post-excerpt{font-size:var(--fs-base);}

/* ---------- Article ---------- */
.article{max-width:720px; margin:0 auto; padding:26px 0 10px;}
.article-head{margin:22px 0 26px;}
.article-head h1{font-size:clamp(27px,4vw,40px); margin-bottom:14px;}
.article-meta{font-size:var(--fs-sm); color:var(--ink-soft); display:flex; align-items:center; gap:8px; flex-wrap:wrap;}
.article-cover{border-radius:var(--r-lg); overflow:hidden; margin-bottom:32px; background:var(--tint-mint);}
.article-cover img{width:100%; height:auto; display:block;}
.article-body{font-size:var(--fs-md); line-height:1.75; color:var(--ink-article);}
.article-body p{margin-bottom:20px;}
.article-body h3{font-size:var(--fs-xl); margin:34px 0 12px;}
.article-body h4{font-size:var(--fs-lg); margin:26px 0 10px;}
.article-body ul,.article-body ol{margin:0 0 20px 22px;}
.article-body li{margin-bottom:8px;}
.article-body a{color:var(--thread-ink); text-decoration:underline; text-underline-offset:2px;}
.article-body strong{font-weight:700; color:var(--ink);}
.article-body blockquote{border-left:3px solid var(--mint-deep); padding:4px 0 4px 18px; margin:0 0 20px; font-style:italic; color:var(--ink-soft);}
.article-foot{display:flex; gap:12px; flex-wrap:wrap; margin-top:38px; padding-top:26px; border-top:1px solid var(--line);}

/* ---------- Product detail page ---------- */
.pdp-wrap{padding-block:26px 20px;}
.pdp{display:grid; grid-template-columns:1.04fr 1fr; gap:52px; margin-top:22px; align-items:start;}
/* A grid item's min-width is `auto`, which means it refuses to shrink below
   its own content: one unbreakable string - a pasted product name with no
   spaces, a description of solid characters - pushes the column wider than its
   share and shoves the whole page sideways. The same rule `.shop-layout > *`
   already carries. Nothing here may out-grow its column. */
.pdp > *{min-width:0;}

/* ---------- Text that people type must always break ----------
   Product names, descriptions, tags and journal copy are typed by hand, and
   sooner or later one of them arrives with no spaces in it: a pasted name, a
   URL, a row of solid characters. Without this the string refuses to wrap,
   pushes past its column, and - because html/body are `overflow-x:clip` so the
   page cannot scroll sideways - is simply CUT OFF with no way to read it.
   Clipped is worse than a scrollbar: the words are gone rather than awkward.

   `anywhere` rather than `break-word`: only `anywhere` lets the element's
   intrinsic size shrink, which is what stops the column growing in the first
   place. */
.pdp-info h1, .pdp-desc, .pdp-note, .pdp-facts dd, .pdp-cat,
.breadcrumb strong, .breadcrumb a,
.p-name, .p-cat, .post-title, .post-excerpt, .article-body,
.grid-empty, .tag-pill, .chip, .suggest-item, .filter-label{
  overflow-wrap:anywhere;
}
/* Headings and the article body get the softer rule as well, so ordinary
   prose still breaks at spaces first and only splits a word as a last resort. */
.pdp-info h1, .article-body, .post-title{word-break:normal;}
/* The picture stays with you while you read the details, instead of scrolling
   away and leaving an empty column behind. */
.pdp-media{position:sticky; top:calc(var(--nav-h) + 18px);}
.pdp-thumbs{display:grid; grid-template-columns:repeat(auto-fill,minmax(66px,1fr)); gap:8px; margin-top:12px;}
.pdp-thumb{aspect-ratio:4/5; border-radius:var(--r-sm); overflow:hidden; border:2px solid transparent; background:var(--cream); padding:0; transition:border-color .16s, opacity .16s; opacity:.72;}
.pdp-thumb img{width:100%; height:100%; object-fit:cover;}
.pdp-thumb:hover{opacity:1;}
.pdp-thumb.on{border-color:var(--mint-deep); opacity:1;}
.pdp-photo{position:relative; aspect-ratio:4/5; background:var(--tint-mint); border-radius:var(--r-lg); overflow:hidden; display:flex; align-items:center; justify-content:center; border:1px solid var(--line);}
.pdp-photo img{width:100%; height:100%; object-fit:cover;}
.pdp-photo svg{width:38%; opacity:.3; color:var(--ink);}
/* The body face, NOT the display face every other h1 uses. This heading is the
   same string the shopper just clicked on a card, where it is set in Karla -
   switching typeface between the click and the page made the product name look
   like a different thing. Continuity wins over consistency with the other
   page headings here. */
.pdp-info h1{
  font-family:var(--font-body); font-weight:700;
  font-size:clamp(25px,3.4vw,34px); margin-bottom:14px; line-height:1.1;
  letter-spacing:-0.005em;
}
.pdp-cat{display:inline-block; font-size:var(--fs-2xs); font-weight:700; text-transform:uppercase; letter-spacing:.07em; color:var(--thread-ink); margin-bottom:10px;}
.pdp-cat:hover{text-decoration:underline;}
.pdp-price{
  display:flex; align-items:center; flex-wrap:wrap; gap:12px;
  font-family:var(--font-body); font-size:var(--fs-2xl); font-weight:700; color:var(--thread);
  margin-bottom:8px; line-height:1.1;
}
.pdp-stock{font-size:var(--fs-xs); font-weight:700; margin-bottom:0; padding-bottom:22px; border-bottom:1px solid var(--line);}
.pdp-stock.in{color:var(--ok);}
.pdp-stock.out{color:var(--err);}
.pdp-desc{font-size:var(--fs-base); line-height:1.55; color:var(--ink-soft); white-space:pre-line; margin:0 0 22px;}
/* A line in the shop's own hand about this one piece. The glow-dot is the same
   firefly mark used on the cards and the points list, so the aside reads as
   the shop speaking rather than as another block of copy. */
.pdp-note{
  position:relative; margin:0 0 22px; padding-left:26px;
  font-family:var(--font-hand); font-weight:600; font-size:var(--fs-xl); line-height:1.4;
  color:var(--thread);
}
.pdp-note::before{
  content:''; position:absolute; left:2px; top:11px; width:9px; height:9px; border-radius:50%;
  background:radial-gradient(circle, rgba(240,185,61,.95), rgba(240,185,61,0));
  box-shadow:0 0 8px 2px rgba(240,185,61,.5);
}
/* Material and care: the two questions asked about anything a baby touches.
   Hairline-separated rows so it reads as a small spec panel, not loose text -
   and no box around it, because the points list below is already a panel and
   two stacked boxes make the column feel boxed in. */
.pdp-facts{display:grid; margin:0 0 24px;}
.pdp-facts > div{
  display:grid; grid-template-columns:minmax(0,92px) minmax(0,1fr); gap:16px;
  align-items:start; padding:11px 0; border-top:1px solid var(--line);
}
.pdp-facts > div:last-child{border-bottom:1px solid var(--line);}
.pdp-facts dt{font-size:var(--fs-2xs); font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-soft); padding-top:2px;}
.pdp-facts dd{font-size:var(--fs-sm); line-height:1.55; color:var(--ink); min-width:0;}
@media (max-width:480px){
  .pdp-facts > div{grid-template-columns:1fr; gap:4px;}
}
/* One button, full width, nothing beside it competing for the same decision. */
.pdp-actions{display:grid; gap:12px; margin:24px 0 14px;}
.pdp-actions .tag-btn{width:100%; padding-block:16px; font-size:var(--fs-base);}
.pdp-points{
  list-style:none; display:flex; flex-direction:column; gap:12px;
  background:var(--cream); border:1px solid var(--line); border-radius:var(--r-lg); padding:18px 20px;
}
.pdp-points li{position:relative; padding-left:24px; font-size:var(--fs-sm); line-height:1.55; color:var(--ink-soft);}
.pdp-points li::before{content:''; position:absolute; left:0; top:7px; width:8px; height:8px; border-radius:50%; background:radial-gradient(circle, rgba(240,185,61,.95), rgba(240,185,61,0)); box-shadow:0 0 7px 2px rgba(240,185,61,.45);}
.pdp-missing{text-align:center; padding:70px 20px;}
.pdp-missing h1{font-size:var(--fs-2xl); margin:8px 0 12px;}
.pdp-missing p{color:var(--ink-soft); margin-bottom:24px;}

/* A stitch used INSIDE a band (product and post pages) rather than between
   bands: it inherits the band's gutters, so no .wrap class, just breathing room. */
.band > .stitch{margin-bottom:38px;}

/* ---------- Section shell ---------- */
/* padding-block only - a `padding` shorthand here would out-specify .wrap
   (0,1,1 vs 0,1,0) and zero out the side gutters on every `band wrap` section. */
section.band{padding-block:60px;}
.band-dusk{background:radial-gradient(120% 100% at 50% 0%, var(--dusk) 0%, var(--dusk-deep) 100%); color:var(--ink);}
.band-gold{background:var(--gold); color:var(--ink);}
.band-mint{background:var(--mint); color:var(--ink);}
.section-head{display:flex; align-items:flex-end; justify-content:space-between; margin-bottom:30px; gap:20px; flex-wrap:wrap;}
.section-head h2{font-size:clamp(23px,3vw,30px);}
/* A heading with a line under it rather than a link beside it, for a section
   whose purpose is not obvious from its title alone. */
.section-head-stack{flex-direction:column; align-items:flex-start; gap:8px;}
.section-sub{font-size:var(--fs-base); color:var(--ink-soft); max-width:560px;}
.view-all{font-size:var(--fs-sm); font-weight:700; color:var(--thread-ink); white-space:nowrap; cursor:pointer;}
.view-all:hover{text-decoration:underline;}

/* ---------- Category hang-tags ----------
   A single row that scrolls sideways, not a grid: at fourteen categories the
   grid became four rows of homepage before the first product. The rail shows
   them all in one band, snaps tile-by-tile, and the half-visible next tile is
   what says "there is more" - on desktop a slim styled scrollbar says it too. */
.cat-grid{
  display:flex; gap:16px; padding:8px 4px 16px;
  overflow-x:auto; overscroll-behavior-x:contain; -webkit-overflow-scrolling:touch;
  scroll-snap-type:x proximity; scroll-padding-inline:4px;
  scrollbar-width:thin; scrollbar-color:var(--line) transparent;
}
.cat-grid::-webkit-scrollbar{height:8px;}
.cat-grid::-webkit-scrollbar-track{background:transparent;}
.cat-grid::-webkit-scrollbar-thumb{background:var(--line); border-radius:var(--r-pill);}
.cat-grid::-webkit-scrollbar-thumb:hover{background:var(--mint-deep);}
.cat-tile{
  /* width + flex:0 0 auto rather than a px flex-basis: the rail never stacks
     into a column, but the basis would size the wrong axis if it ever did,
     and check:mobile rightly refuses to take "never" on trust. */
  flex:0 0 auto; width:148px; scroll-snap-align:start;
  display:flex; flex-direction:column; align-items:center; justify-content:flex-start;
  background:var(--paper); border:1.5px dashed var(--line); border-radius:var(--r-lg);
  padding:24px 12px 16px; text-align:center; position:relative; cursor:pointer;
  transition:transform .22s ease, border-color .22s ease;
}
.cat-tile::before{
  content:''; position:absolute; top:-6px; left:50%; transform:translateX(-50%);
  width:11px; height:11px; border-radius:50%; background:var(--cream); border:1.5px dashed var(--line);
}
.cat-tile:hover,.cat-tile.active{transform:translateY(-4px); border-color:var(--thread);}
/* Lucide icons inherit stroke and colour from the wrapper. display:block, or
   the inline svg sits BESIDE the name on the text baseline instead of above it. */
.cat-icon{display:block; width:34px; height:34px; margin:0 auto 12px; color:var(--mint-deep); stroke:currentColor; fill:none; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round;}
.cat-tile:nth-child(4n+1) .cat-icon{color:#8AC4B2;}
.cat-tile:nth-child(4n+2) .cat-icon{color:#E5947A;}
.cat-tile:nth-child(4n+3) .cat-icon{color:#E0BC66;}
.cat-tile:nth-child(4n+4) .cat-icon{color:#91B87A;}
/* Two clamped lines at a fixed height, so "Gift Sets" and "Swaddles &
   Blankets" produce tiles of the same height in the rail. */
.cat-tile span{
  font-size:var(--fs-sm); font-weight:700; line-height:1.25; min-height:2.5em;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}

/* ---------- Product grid ---------- */
.product-grid{display:grid; grid-template-columns:repeat(4,1fr); gap:24px;}
.p-card{
  background:var(--paper); border:1px solid var(--line); border-radius:var(--r-lg);
  overflow:hidden; position:relative; display:flex; flex-direction:column;
  transition:transform .24s ease, border-color .24s ease, box-shadow .24s ease;
}
/* The same lift the category tiles and post cards already have, so every card
   on the site answers a pointer the same way. */
.p-card:hover{
  transform:translateY(-3px); border-color:var(--mint-deep);
  box-shadow:0 10px 24px rgba(67,57,42,.09);
}
.p-photo{aspect-ratio:4/5; background:var(--cream); display:flex; align-items:center; justify-content:center; position:relative; overflow:hidden; cursor:pointer; width:100%; padding:0; border:0;}
.p-photo svg{width:46%; height:46%; color:var(--ink); opacity:.32;}
/* A slow push into the photo inside its own frame - the card is a fixed size,
   so only the picture moves. Long and eased, because a fast zoom reads as a
   twitch; this should be barely noticed and still felt. */
.p-photo img{
  width:100%; height:100%; object-fit:cover;
  transition:transform .6s cubic-bezier(.22,.61,.24,1);
}
.p-card:hover .p-photo img{transform:scale(1.055);}
.p-card:nth-child(4n+1) .p-photo{background:var(--tint-mint);}
.p-card:nth-child(4n+2) .p-photo{background:var(--tint-blush);}
.p-card:nth-child(4n+3) .p-photo{background:var(--tint-butter);}
.p-card:nth-child(4n+4) .p-photo{background:var(--tint-sage);}
.p-photo .glow-dot{position:absolute; width:8px; height:8px; border-radius:50%; top:14px; right:14px; background:radial-gradient(circle, rgba(240,185,61,.9), rgba(240,185,61,0)); box-shadow:0 0 8px 2px rgba(240,185,61,.5); opacity:0; transition:opacity .25s ease;}
.p-card:hover .glow-dot, .p-card:focus-within .glow-dot{opacity:1;}
/* The firefly is the brand's namesake, and hover does not exist on the phones
   most shoppers use - so on touch, every card quietly carries its glow. */
@media (hover:none){ .p-photo .glow-dot{opacity:.55;} }
.p-badge{position:absolute; top:12px; left:12px; background:var(--ink); color:var(--cream); font-size:var(--fs-3xs); font-weight:700; letter-spacing:.06em; text-transform:uppercase; padding:5px 9px; border-radius:var(--r-sm);}
.p-body{padding:14px 15px 16px; display:flex; flex-direction:column; flex:1;}
/* Fixed rows so every card is exactly the same height: the name always takes
   two lines, the category always one. The button is pinned to the bottom by
   margin-top:auto, so prices and buttons line up straight across a row. */
.p-name{
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; min-height:2.4em;
}
.p-cat{white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-height:1.4em;}
/* Tags are deliberately not shown on cards - they live on the product page
   and drive the Shop filters. */
.p-name{font-weight:700; font-size:var(--fs-sm); margin-bottom:3px; line-height:1.25;}
.p-cat{font-size:var(--fs-2xs); font-weight:400; color:var(--ink-soft); margin-bottom:6px;}
/* The price is what a shopper scans a row of cards for, so it is the loudest
   line on the card - a step up from the name, in the thread colour. */
.p-price{color:var(--thread-ink); font-weight:700; font-size:var(--fs-md); margin-bottom:12px;}
/* Discount pricing: what they pay, what it was, and the saving. */
.p-price.has-offer{display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;}
.price-was{color:var(--ink-soft); font-weight:600; font-size:var(--fs-xs); opacity:.8;}
.price-off{font-size:var(--fs-3xs); font-weight:700; color:#fff; background:var(--sale); padding:3px 8px; border-radius:var(--r-pill); letter-spacing:.01em;}
.p-badge-sale{background:var(--sale);}
/* CTA mint, not the pastel: the ribbon carries white text. */
.p-badge-mint{background:var(--cta-mint);}
.pdp-was{color:var(--ink-soft); font-size:var(--fs-lg); font-weight:600; margin-left:10px; opacity:.75;}
.pdp-off{font-size:var(--fs-xs); font-weight:700; color:#fff; background:var(--sale); padding:5px 12px; border-radius:var(--r-pill); margin-left:10px; vertical-align:middle;}
.pdp-save-note{font-size:var(--fs-sm); color:var(--sale); font-weight:700; margin:0 0 14px;}
.pdp-ribbon{display:inline-block; background:var(--cta-mint); color:#fff; font-size:var(--fs-3xs); font-weight:700; text-transform:uppercase; letter-spacing:.06em; padding:4px 10px; border-radius:var(--r-pill); margin-bottom:10px;}
.p-enquire{display:flex; align-items:center; justify-content:center; gap:8px; width:100%; margin-top:auto; padding:11px 10px; font-size:var(--fs-xs); font-weight:700; background:var(--cream); color:var(--ink); border:1px solid var(--line); border-radius:var(--r-sm); transition:background .18s ease, color .18s ease;}
/* Mint, not WhatsApp green: this link opens the product page. WhatsApp green
   is reserved for buttons that actually open WhatsApp (BRAND.md §5). */
.p-enquire:hover{background:var(--cta-mint); color:#fff; border-color:var(--cta-mint);}
.p-enquire[aria-disabled="true"]{pointer-events:none;}
/* Sold out: the photo keeps its full colour - the piece should still look like
   something worth wanting, and a grey photo just reads as a broken image. The
   grey is a gradient in the panel BELOW it instead: deepest along the bottom
   edge and fading to nothing where it meets the photo, so the state is obvious
   down a row without draining the picture. */
.p-card.is-sold-out .p-body{
  background:linear-gradient(to top, var(--muted-bg-2) 0%, var(--muted-bg) 48%, var(--paper) 100%);
}
/* Muted, but still readable against the pale top of that gradient - the old
   greys were about 2.6:1 on it. */
.p-card.is-sold-out .p-name,
.p-card.is-sold-out .p-price{color:var(--muted-ink);}
.p-card.is-sold-out .p-cat{color:var(--muted-ink-2);}
.p-card.is-sold-out .p-price .price-was{color:var(--muted-ink-3);}
.p-card.is-sold-out .p-price .price-off{background:var(--muted-pill); color:#fff;}
/* Dark enough to actually read: the old grey-on-grey at half opacity made
   "Sold out" the one label on the card a shopper could not see. */
.p-card.is-sold-out .p-enquire{background:var(--muted-fill); color:var(--muted-ink); border-color:var(--muted-line);}
/* The badge was the only thing on a sold-out card still at full strength - the
   loudest element announcing the one thing that cannot be bought. It joins the
   rest of the muted treatment. */
.p-card.is-sold-out .p-badge{background:var(--muted-ink); color:var(--muted-on);}

/* The empty states are written warmly and used to be rendered coldly: grey
   14px in a bare box. Same dashed frame and cream ground as the hang-tag
   category tiles, so "nothing here yet" still looks like part of the shop. */
.grid-empty{
  grid-column:1/-1; text-align:center; padding:44px 22px; font-size:var(--fs-base);
  color:var(--ink-soft); line-height:1.55;
  background:var(--cream); border:1.5px dashed var(--line); border-radius:var(--r-lg);
}
.grid-empty .eyebrow-hand{margin-bottom:2px;}
.grid-empty h3{font-size:var(--fs-lg); color:var(--ink); margin-bottom:7px;}
.grid-empty p{max-width:420px; margin-inline:auto;}
.grid-empty .tag-btn{margin-top:18px;}
/* Loading copy sits in the same frame, so a slow connection sees the shop's
   own shape rather than a bare line of grey text. */
.loading-block{
  padding:56px 22px; text-align:center; color:var(--ink-soft); font-size:var(--fs-base);
  background:var(--cream); border:1.5px dashed var(--line); border-radius:var(--r-lg);
}

/* A failed refresh is a banner over the content, never a replacement for it -
   the server-rendered products below it still work. */
.page-error-note{
  display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap;
  background:var(--tint-blush); border:1px solid var(--line); border-radius:var(--r-md);
  padding:12px 16px; margin-bottom:18px; font-size:var(--fs-sm); font-weight:600; color:var(--ink);
}
.page-error-note .tag-btn{padding:9px 16px; font-size:var(--fs-xs);}
a.p-name{display:block;}
a.p-name:hover{color:var(--thread-ink);}
a.p-enquire{text-decoration:none;}
/* Cards arrive in a short cascade rather than all at once - the first row
   leads, the rest follow together. Capped at .16s: a stagger long enough to
   notice is a stagger long enough to wait for, and the grid re-renders on
   every filter change. Off entirely under reduced motion (below). */
@media (prefers-reduced-motion: no-preference){
  .product-grid .p-card,
  .post-grid .post-card,
  .cat-grid .cat-tile,
  .insta-grid .insta-tile{animation:tile-in .45s cubic-bezier(.22,.7,.3,1) backwards;}
  .product-grid .p-card:nth-child(2),
  .post-grid .post-card:nth-child(2),
  .cat-grid .cat-tile:nth-child(2),
  .insta-grid .insta-tile:nth-child(2){animation-delay:.05s;}
  .product-grid .p-card:nth-child(3),
  .post-grid .post-card:nth-child(3),
  .cat-grid .cat-tile:nth-child(3),
  .insta-grid .insta-tile:nth-child(3){animation-delay:.1s;}
  .product-grid .p-card:nth-child(n+4),
  .post-grid .post-card:nth-child(n+4),
  .cat-grid .cat-tile:nth-child(n+4),
  .insta-grid .insta-tile:nth-child(n+4){animation-delay:.16s;}
}
@keyframes tile-in{from{opacity:0; transform:translateY(10px);} to{opacity:1; transform:none;}}

.skeleton{background:linear-gradient(90deg,var(--skeleton-a) 25%,var(--skeleton-b) 50%,var(--skeleton-a) 75%); background-size:400% 100%; animation:shimmer 1.4s ease infinite; border-radius:var(--r-sm);}
.skeleton-card{height:300px;}
@keyframes shimmer{0%{background-position:100% 0;}100%{background-position:0 0;}}

/* ---------- Story ---------- */
.story-grid{display:grid; grid-template-columns:1fr 1fr; gap:52px; align-items:center;}
.story-art{aspect-ratio:1/1; border-radius:var(--r-lg); border:1.5px dashed rgba(67,57,42,0.25); display:flex; align-items:center; justify-content:center; background:rgba(255,255,255,0.4);}
.story-art svg{width:38%; opacity:.55;}
/* Once a real photo is uploaded the dashed placeholder frame steps aside. */
.story-art.has-photo{border:none; background:none; overflow:hidden; padding:0;}
.story-art.has-photo img{width:100%; height:100%; object-fit:cover; border-radius:var(--r-lg);}
.story-grid h2{color:var(--ink); margin-bottom:14px;}
.story-grid p{color:rgba(67,57,42,0.72); margin-bottom:22px; font-size:var(--fs-base); max-width:440px;}

/* ---------- Why us ---------- */
.why-grid{display:grid; grid-template-columns:repeat(4,1fr); gap:24px;}
.why-item{display:flex; flex-direction:column; gap:12px;}
/* The icon sits in a soft pastel medallion, cycling the photo-tint set, so the
   band reads as four warm stamps rather than four floating outlines. */
.why-icon{
  width:46px; height:46px; padding:11px; border-radius:50%;
  background:var(--tint-mint); color:var(--thread); flex-shrink:0;
}
.why-item:nth-child(4n+2) .why-icon{background:var(--tint-blush);}
.why-item:nth-child(4n+3) .why-icon{background:var(--tint-butter);}
.why-item:nth-child(4n+4) .why-icon{background:var(--tint-sage);}
.why-item h3{font-family:var(--font-body); font-size:var(--fs-base); font-weight:700;}
.why-item p{font-size:var(--fs-xs); color:var(--ink-soft);}

/* ---------- The daily pick, beside the Instagram invitation ----------
   Deliberately lighter than a product card: no price, no button, and the name
   sits over the photo. Six squares that rotate daily, so the homepage is not
   the same picture every visit. */
.insta-grid{display:grid; grid-template-columns:repeat(6,1fr); gap:12px;}
.insta-tile{
  position:relative; display:block; aspect-ratio:1/1; overflow:hidden;
  border-radius:var(--r-lg); background:var(--paper); border:1px solid var(--line);
  transition:transform .2s ease, border-color .2s ease;
}
.insta-tile:hover{transform:translateY(-3px); border-color:var(--mint-deep);}
.insta-tile img{width:100%; height:100%; object-fit:cover;}
.insta-cta{text-align:center; margin-top:24px;}
.insta-icon{width:18px; height:18px; flex-shrink:0;}
/* The Instagram button carries Instagram's own colours - the same rule that
   reserves WhatsApp green for WhatsApp: a button's colour tells you where it
   goes. A filled gradient, not an outline, because an outlined button on a
   near-white page is hard to spot as something pressable.
   The DEEP half of Instagram's gradient (magenta → purple → blue) on purpose:
   the warm yellow/orange end carries white text at about 1.5:1, which would
   break the "white sits at 4.5:1 or better on a filled button" rule that the
   --cta-* tokens exist to keep. Every stop here clears it. This is the one
   sanctioned non-token fill - a third-party brand mark, like the WhatsApp
   green - and it stays on this button alone. */
.tag-btn-instagram{
  background:var(--instagram-tint); color:var(--ink); border-color:var(--instagram-soft);
  box-shadow:0 3px 10px rgba(214,41,118,0.10);
}
.tag-btn-instagram:hover{
  background:#F8DAE9; color:var(--ink); border-color:var(--instagram);
  box-shadow:0 7px 18px rgba(214,41,118,0.18);
}
.tag-btn-instagram:active{box-shadow:0 2px 6px rgba(214,41,118,0.12);}
/* The mark is one solid colour from the shop's own palette - never Instagram's
   gradient. Across a 2px stroke a gradient renders the squircle's outline in a
   different colour at every point, which is what read as an uneven border. */
.tag-btn-instagram .insta-icon{width:20px; height:20px; color:var(--instagram);}
.insta-tile{aspect-ratio:1/1; border-radius:var(--r-lg); background:var(--paper); border:1px solid var(--line); display:flex; align-items:center; justify-content:center; overflow:hidden;}
.insta-tile img{width:100%; height:100%; object-fit:cover;}
.insta-tile svg{width:30%; opacity:.35; color:var(--ink);}

/* ---------- Newsletter ---------- */
.newsletter{text-align:center;}
.newsletter h2{margin-bottom:8px; color:var(--ink);}
.newsletter p{color:rgba(67,57,42,0.72); margin-bottom:22px; font-size:var(--fs-sm);}
.nl-form{display:flex; gap:12px; max-width:400px; margin:0 auto;}
.nl-form input{flex:1; padding:12px 16px; border:1.5px solid rgba(67,57,42,0.25); font-family:inherit; font-size:var(--fs-md); background:var(--paper); border-radius:var(--r-sm);}
.nl-form button{background:var(--ink); color:var(--cream); padding:12px 20px; font-weight:700; font-size:var(--fs-sm); border-radius:var(--r-sm);}
.nl-form button[disabled]{opacity:.6; cursor:default;}
.nl-note{font-size:var(--fs-xs); margin-top:12px; min-height:19px; font-weight:600;}
.nl-note.err{color:var(--err-ink);}
/* The thank-you deserves to look like one: confirmation and error must not be
   the same grey line. */
.nl-note.ok{color:var(--cta-mint-2); font-size:var(--fs-sm);}
.nl-thanks{display:block; margin-top:14px; font-size:var(--fs-xl);}
/* One firefly drifting up out of the gold band on subscribe - a moment, not a
   show. Never created under reduced motion. */
.newsletter{position:relative;}
.nl-firefly{
  position:absolute; left:50%; bottom:8px; width:7px; height:7px; border-radius:50%;
  background:radial-gradient(circle, rgba(240,185,61,.95), rgba(240,185,61,0));
  box-shadow:0 0 10px 3px rgba(240,185,61,.5); pointer-events:none;
  animation:nl-drift 2.4s ease-out forwards;
}
@keyframes nl-drift{
  0%{opacity:0; transform:translate(0,0);}
  12%{opacity:1;}
  100%{opacity:0; transform:translate(34px,-130px);}
}
.hp-field{position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden;}

/* ---------- Footer ---------- */
footer{padding:50px 0 30px;}
.foot-grid{display:grid; grid-template-columns:1.5fr 1fr 1fr 1fr; gap:32px; margin-bottom:32px;}
.foot-grid h4{font-size:var(--fs-2xs); text-transform:uppercase; letter-spacing:.08em; margin-bottom:13px; color:var(--ink-soft);}
.foot-grid a{display:block; font-size:var(--fs-sm); padding:4px 0;}
/* Setting `display` above beats the browser's own [hidden]{display:none}, so
   every footer link the server stands down for want of an address was shown
   anyway - "Email us" pointing at "#" on a shop with no email saved, and the
   same for Instagram. The same trap the Filters badge fell into. */
.foot-grid a[hidden]{display:none;}
.foot-grid a:hover{color:var(--thread-ink);}
/* The WhatsApp row needs to be a flex line, and `.foot-grid a` above sets
   display:block at the SAME specificity (0,1,1) - so the plain `a.foot-wa-row`
   this used to be, sitting 570 lines earlier, quietly lost the tie. The icon
   and the words ended up touching, on the text baseline rather than centred.
   Two classes wins it, and living here means the two rules are read together. */
.foot-grid a.foot-wa-row{
  display:inline-flex; align-items:center; gap:8px; line-height:1.4;
}
.foot-grid a.foot-wa-row .wa-icon{
  width:16px; height:16px; flex-shrink:0; color:var(--cta-whatsapp);
}
.foot-social{display:flex; gap:8px; margin-top:14px;}
.foot-social a{width:40px; height:40px; border-radius:var(--r-sm); background:var(--paper); border:1px solid var(--line); display:flex; align-items:center; justify-content:center; color:var(--ink-soft); transition:color .15s ease, border-color .15s ease;}
.foot-social a:hover{color:var(--thread-ink); border-color:var(--thread);}
.foot-social a svg{width:18px; height:18px;}
.foot-bottom{display:flex; justify-content:space-between; padding-top:20px; border-top:1px solid var(--line); font-size:var(--fs-2xs); color:var(--ink-soft); flex-wrap:wrap; gap:8px;}
/* The sign-off in the brand's own hand: the one line on every page that is a
   person speaking rather than a label. */
.foot-sign{font-family:var(--font-hand); font-weight:600; font-size:var(--fs-lg); color:var(--thread);}

/* The fade-up as a section scrolls into view. `.in-view` is added by
   initReveal() in common.js.

   The failsafe animation is the point of the block, not decoration. Every
   page's real content is rendered into the HTML before it is sent, and
   `opacity:0` with only a JavaScript path back to visible threw that away: with
   scripting off, everything below the homepage hero - the categories, both
   product rows, Our Story, the newsletter, the footer - was in the markup and
   invisible, and on a slow connection every visitor saw that until common.js
   had downloaded and run. The animation reaches opacity:1 on its own, so the
   content can never be waiting on a script. `.in-view` cancels it, which hands
   the reveal back to the transition whenever the script does arrive first. */
.reveal{
  opacity:0; transform:translateY(16px);
  transition:opacity .55s ease, transform .55s ease;
  animation:reveal-failsafe .4s ease 1.4s both;
}
@keyframes reveal-failsafe{from{opacity:0; transform:translateY(16px);} to{opacity:1; transform:none;}}
.reveal.in-view{opacity:1; transform:translateY(0); animation:none;}
/* A browser that will never run the script should not wait 1.4s either. */
@media (scripting: none){
  .reveal{opacity:1; transform:none; animation:none;}
}

/* ---------- Cursor firefly ---------- */
.cf-wrap{position:fixed; top:0; left:0; width:24px; height:24px; pointer-events:none; z-index:200; opacity:0; transform:translate(-50%,-50%);}
.cf-svg{width:100%; height:100%; overflow:visible; display:block; transform:scaleX(-1); filter:drop-shadow(0 0 2px rgba(240,193,94,.45)) drop-shadow(0 0 5px rgba(240,193,94,.25));}
.cf-spark{
  position:fixed; top:0; left:0; width:4px; height:4px; border-radius:50%;
  background:rgba(240,193,94,.8); box-shadow:0 0 6px 2px rgba(240,193,94,.4);
  pointer-events:none; z-index:199; transform:translate(-50%,-50%);
  animation:cf-fade 1s ease-out forwards;
}
@keyframes cf-fade{0%{opacity:.6;} 100%{opacity:0; transform:translate(-50%,-50%) scale(.25);}}

@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto;} .firefly{animation:none; opacity:.6;} .reveal{transition:none; animation:none; opacity:1; transform:none;}
  .skeleton{animation:none;}
  /* Keep the colour shift, drop the lift. */
  .tag-btn:hover, .tag-btn:active{transform:none;}
  /* Every card movement goes: the colour cues stay, nothing travels or scales. */
  .p-card, .p-photo img, .post-card, .cat-tile, .insta-tile, a.promo-inner{transition:border-color .18s ease, background .18s ease;}
  .p-card:hover, .post-card:hover, .cat-tile:hover, .insta-tile:hover, a.promo-inner:hover{transform:none;}
  .p-card:hover .p-photo img{transform:none;}
  .sparkle{animation:none;}
  .shop-filters{transition:none;}
}

/* The 404 page has no JavaScript, so its nav links must not vanish behind a
   burger that cannot open. Two classes out-specify the media rule below. */
.nav-links.nav-links-simple{display:flex; gap:20px;}

/* The nav row is a form plus four links. Below a roomy desktop the gaps close
   up and the search narrows before anything is allowed to collide. */
@media (max-width:1080px){
  .nav-links{gap:24px;}
  .nav-search{width:150px;}
  .nav-search:focus-within{width:190px;}
}

@media (max-width:920px){
  .wrap{padding-inline:24px;}
  .nav-links{display:none;} .burger{display:flex;}
  /* ---- On a phone the filter panel is a bottom sheet, not a left drawer:
     Clear all and Show N sit where a thumb can reach them. Everything else
     about the panel is the same at every width, and lives in the base rules. */
  .shop-filters{
    top:auto; left:0; right:0; bottom:0; width:auto;
    max-height:88vh; border-right:none;
    border-radius:var(--r-xl) 18px 0 0; box-shadow:0 -8px 40px rgba(67,57,42,.22);
    transform:translateY(100%);
  }
  .shop-filters.open{transform:translateY(0);}
  .sheet-head::before{
    content:""; position:absolute; top:7px; left:50%; transform:translateX(-50%);
    width:38px; height:4px; border-radius:var(--r-pill); background:var(--line);
  }
  .shop-toolbar{border-bottom:none; padding-bottom:0; margin-bottom:16px;}
  .filter-block{min-width:0;}
  .filter-item .filter-count{display:inline;}
  .product-grid-3{grid-template-columns:repeat(2,1fr);}
  /* One column below the desktop split. Centred, and no longer sticky: a
     picture pinned above the text it belongs to just gets in the way. The
     old rule capped the photo but left it against the left edge, which is
     what put a dead half-screen beside every product on a tablet. */
  .pdp{grid-template-columns:1fr; gap:28px;}
  .pdp-media{position:static; max-width:440px; margin-inline:auto; width:100%;}
  .pdp-info{max-width:560px; margin-inline:auto; width:100%;}
  .product-grid{grid-template-columns:repeat(2,1fr);}
  .story-grid{grid-template-columns:1fr;}
  .story-art{max-width:260px; margin:0 auto;}
  .why-grid{grid-template-columns:repeat(2,1fr);}
  .insta-grid{grid-template-columns:repeat(3,1fr);}
  .foot-grid{grid-template-columns:1fr 1fr;}
  .nl-form{flex-direction:column;}

  /* ---- One-handed phone use. ----
     16px inputs: iOS Safari zooms the whole page when a focused field is any
     smaller, and every search on a phone was punting the layout sideways.
     The paddings raise the small tap targets towards a 40px minimum. */
  .nav-search input, .nav-search-mobile input, .filter-search, .sort-wrap select{font-size:var(--fs-md);}
  .nav-search-mobile .search-go{width:38px; height:38px;}
  .nav-search-mobile .search-go svg{width:18px; height:18px;}
  .suggest-item{padding:11px 12px;}
  .foot-grid a{padding:9px 0;}
  .sheet-close{width:40px; height:40px;}
  .chip{padding:8px 13px;}
}

@media (max-width:480px){
  .wrap{padding-inline:20px;}
  .logo-mark{width:42px; height:42px;}
  .wordmark{font-size:var(--fs-xl);}
  .nav-inner{padding:13px 20px;}
  .mobile-menu{padding:0 20px 18px;}
  .announce{padding:8px 12px; font-size:var(--fs-2xs);}
  .hero{padding:48px 0 40px;}
  section.band{padding-block:40px;}
  .hero p.lede{font-size:var(--fs-md);}

  .btn-row{flex-direction:column; align-items:stretch; width:100%;}
  .btn-row .tag-btn{justify-content:center; width:100%;}
  .cat-grid{gap:12px;}
  .cat-tile{width:126px; padding:20px 10px 14px;}
  .product-grid{gap:16px;}
  .product-grid-3{grid-template-columns:repeat(2,1fr); gap:16px;}
  .p-body{padding:12px 12px 14px;}
  .page-hero{padding:32px 0 30px;}
  .shop-toolbar{gap:12px;}
  .pdp-wrap{padding-block:18px 10px;}
  .pdp-actions .tag-btn{width:100%; justify-content:center;}
  .why-grid{gap:20px;}
  .insta-grid{grid-template-columns:repeat(2,1fr); gap:8px;}
  .foot-grid{grid-template-columns:1fr; gap:28px;}
  .foot-bottom{flex-direction:column; align-items:flex-start;}
}

/* ---------- Tablet ----------
   Between the desktop layout and the phone one. The blog and product gallery
   were built desktop-first and need explicit rules here. */
@media (max-width:900px){
  .post-grid{grid-template-columns:repeat(2,1fr); gap:24px 20px;}
  /* The featured post stops being a side-by-side split. */
  .post-card.featured{flex-direction:column;}
  .post-card.featured .post-cover{flex:none; aspect-ratio:16/10; min-height:0;}
  .post-card.featured .post-body{flex:none; padding:20px 22px 22px;}
  .post-card.featured .post-title{font-size:var(--fs-xl);}
  .post-card.featured .post-excerpt{font-size:var(--fs-sm);}
  /* A measure, not the full viewport: at 890px wide, uncapping the article ran
     the body copy to ~105 characters a line. 66ch is the comfortable ceiling. */
  .article{max-width:66ch;}
  .article-body{font-size:var(--fs-md);}
  .pdp-thumbs{grid-template-columns:repeat(auto-fill,minmax(58px,1fr)); gap:8px;}
}

/* ---------- Phone ---------- */
@media (max-width:560px){
  .post-grid{grid-template-columns:1fr; gap:20px;}
  .post-card.featured .post-title{font-size:var(--fs-xl);}
  .post-title{font-size:var(--fs-lg);}
  .article-head{margin:16px 0 20px;}
  .article-body{font-size:var(--fs-md); line-height:1.75;}
  .article-body h3{font-size:var(--fs-lg); margin:26px 0 10px;}
  .article-body ul,.article-body ol{margin-left:18px;}
  .article-foot{flex-direction:column;}
  .article-foot .tag-btn{width:100%;}
  /* Thumbnails stay tappable rather than shrinking to nothing. */
  .pdp-thumbs{grid-template-columns:repeat(4,1fr);}
  .pdp-price{font-size:var(--fs-xl);}
  .pdp-was{font-size:var(--fs-md); margin-left:8px;}
  .pdp-off{font-size:var(--fs-2xs); padding:4px 10px; margin-left:8px;}
  /* The one action stays under the thumb while the description and details
     scroll past. sticky, not fixed: it never covers anything, and it stops at
     the end of its own column. */
  .pdp-actions{position:sticky; bottom:14px; z-index:5;}
  /* Sort control drops under the count instead of squeezing beside it. */
  .shop-toolbar{flex-direction:column; align-items:stretch; gap:12px;}
  /* The toolbar is a column now, so `flex:1 1 260px` on the search would set
     its HEIGHT, not its width - it grew into a 260px-tall pill on a phone.
     Stacked, each row is just as tall as its content. */
  .toolbar-search{flex:0 0 auto;}
  .sort-wrap{justify-content:space-between;}
  .sort-wrap select{flex:1;}
  .breadcrumb{font-size:var(--fs-2xs);}
  .page-hero-lede{font-size:var(--fs-base);}
}

/* Very small phones: keep two product columns readable rather than cramped. */
@media (max-width:360px){
  .product-grid,.product-grid-3{grid-template-columns:1fr;}
  .pdp-thumbs{grid-template-columns:repeat(3,1fr);}
}

/* ---------- Shop pager ----------
   Numbered pages under the grid. The number list is already capped at seven
   entries plus gaps by pageNumbers(), so this only has to stay legible as the
   buttons shrink: the arrows keep their words on a desktop and drop to bare
   arrows on a phone, where the row must never be wider than the screen. */
.pager{
  display:flex; align-items:center; justify-content:center; flex-wrap:wrap;
  gap:8px; margin-top:34px;
}
.pager[hidden]{display:none;}
.pager-btn{
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  min-width:40px; height:40px; padding:0 12px;
  font-size:var(--fs-sm); font-weight:700; font-family:var(--font-body); line-height:1;
  color:var(--ink); background:var(--paper);
  border:1.5px solid var(--line); border-radius:var(--r-md);
  transition:background .16s ease, border-color .16s ease, color .16s ease;
}
.pager-btn:hover:not(:disabled){background:var(--cream); border-color:var(--cta-mint);}
/* The current page is a state, not a target: it reads filled and is not a link. */
.pager-btn.on{background:var(--cta-mint); border-color:var(--cta-mint); color:#fff; cursor:default;}
.pager-btn.on:hover{background:var(--cta-mint);}
/* The pager entries are now real <a> links (crawlable, middle-clickable), so
   the "cannot go further" arrows are spans with this class, not :disabled. */
.pager-btn:disabled, .pager-btn.is-disabled{opacity:.4; cursor:default; pointer-events:none;}
.pager-gap{
  min-width:24px; text-align:center; color:var(--ink-soft);
  font-size:var(--fs-sm); font-weight:700; user-select:none;
}

@media (max-width:560px){
  .pager{gap:4px; margin-top:26px;}
  .pager-btn{min-width:36px; height:36px; padding:0 9px; font-size:var(--fs-sm);}
  /* Words off, arrows on: the row has to fit a 390px screen without wrapping
     into a second line that pushes the footer around. */
  .pager-arrow-text{display:none;}
  .pager-arrow{padding:0 10px;}
}
@media (max-width:360px){
  .pager{gap:4px;}
  /* 36px is the floor: 32px numbered targets 4px apart at the bottom of the
     scroll is a precision task for a thumb. flex-wrap absorbs the width. */
  .pager-btn{min-width:36px; height:36px; padding:0 8px; font-size:var(--fs-xs);}
  .pager-gap{min-width:14px;}
}

/* ---------- Gutter guard (keep this last) ----------
   .wrap owns the page gutters. Any component rule that also lands on a .wrap
   element and uses a `padding` shorthand would otherwise win the tie and flush
   content to the screen edge - this has happened twice. Restating .wrap at the
   end of the file makes it win against every equal-specificity rule above.
   Rules with higher specificity (e.g. `section.band`) must still use
   padding-block only. */
.wrap{padding-inline:28px;}
@media (max-width:920px){ .wrap{padding-inline:24px;} }
@media (max-width:480px){ .wrap{padding-inline:20px;} }
