/* --- fonts.css --- */
/* ==========================================================================
   TYPEFACES, SELF HOSTED
   Generated by scripts/fetch-fonts.js. Do not edit by hand.
   Latin and latin-ext only. font-display: swap, so text is readable during
   the swap rather than invisible.
   ========================================================================== */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 600 900;
  font-display: swap;
  src: url('/assets/fonts/archivo-600-900-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 600 900;
  font-display: swap;
  src: url('/assets/fonts/archivo-600-900-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/ibm-plex-mono-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/ibm-plex-mono-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/ibm-plex-mono-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/ibm-plex-mono-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/public-sans-400-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Public Sans';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/public-sans-400-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- tokens.css --- */
/* ==========================================================================
   DESIGN TOKENS
   The only file that may contain a literal colour, radius, duration or size.
   Everything else references these. See docs/MASTER.md section 4.
   ========================================================================== */

:root {
  /* --- corporate -------------------------------------------------------- */
  --c-ink:            #12161F;
  --c-navy:           #16233A;
  --c-gold:           #A87818;
  --c-gold-deep:      #8A600D;   /* the gold, dark enough to be small text */
  --c-gold-mid:       #C89A38;
  --c-gold-lite:      #F5C842;
  --c-gold-wash:      #F5EDDC;

  /* --- neutrals: blue-biased, chosen not inherited ----------------------- */
  --c-paper:          #F4F3F0;
  --c-surface:        #FFFFFF;
  --c-surface-2:      #ECEAE5;
  --c-rule:           #D7D4CD;
  --c-rule-soft:      #E6E3DD;
  --c-text:           #12161F;
  --c-text-2:         #43464D;
  /* Was #72767D, which is 4.11:1 on paper and 3.79:1 on the sunken surface:
     under AA for anything set small, and this token carries breadcrumbs,
     captions, meta lines and helper text across the whole site. #63676E is
     4.73:1 at worst and reads the same weight on the page. */
  --c-text-3:         #63676E;

  /* --- brand: TOPRANK --------------------------------------------------- */
  --c-tr-green:       #1F6B27;
  --c-tr-green-deep:  #10340F;
  --c-tr-lime:        #C6D62E;
  --c-tr-red:         #BE2617;

  /* --- brand: VANISH ---------------------------------------------------- */
  --c-vn-blue:        #1B4C9B;
  --c-vn-blue-deep:   #0F2340;
  --c-vn-yellow:      #F5C842;
  --c-vn-red:         #C6172A;

  /* --- semantic: separate from brand accent ----------------------------- */
  /* 4.44:1 on paper as #2E7D32, a whisker under AA for the status line it
     carries. Darkened to clear it. */
  --c-ok:             #27692B;
  --c-warn:           #A87818;
  --c-critical:       #8C2F2A;
  --c-ok-wash:        #E8F1E9;
  --c-critical-wash:  #F6E7E5;

  /* --- role tokens: components use these, never the raw values ---------- */
  --bg:               var(--c-paper);
  --bg-raised:        var(--c-surface);
  --bg-sunken:        var(--c-surface-2);
  --fg:               var(--c-text);
  --fg-muted:         var(--c-text-2);
  --fg-subtle:        var(--c-text-3);
  --border:           var(--c-rule);
  --border-soft:      var(--c-rule-soft);
  --accent:           var(--c-gold);
  /* The brand gold reaches 3.53:1 on paper, which is fine for a fill, a border
     or a heading but fails AA for anything set small. Small accent text uses
     this deeper gold instead: same hue, 5.03:1. Fills and borders keep
     --accent, so the brand colour on screen is unchanged. */
  --accent-text:      var(--c-gold-deep);
  --accent-fg:        var(--c-ink);
  /* The focus ring was the light gold, which is 1.43:1 against paper: a
     keyboard user could not see where they were on any light page. It reads
     at 11.40:1 on ink, which is why it survived review on the dark sections.
     Light surfaces get the deep gold at 5.03:1, dark surfaces keep the light
     one. WCAG 1.4.11 asks for 3:1. */
  --focus:            var(--c-gold-deep);

  /* --- radius: one system, documented ----------------------------------- */
  --r-btn:            4px;
  --r-card:           6px;
  --r-field:          4px;
  --r-pill:           999px;

  /* --- space scale ------------------------------------------------------ */
  --s-1:   4px;   --s-2:   8px;   --s-3:  12px;   --s-4:  16px;
  --s-5:  24px;   --s-6:  32px;   --s-7:  48px;   --s-8:  64px;
  --s-9:  96px;   --s-10:128px;

  /* --- section rhythm: mobile first ------------------------------------- */
  --section-y:        var(--s-7);
  --gutter:           var(--s-4);
  --measure:          68ch;
  --container:        1280px;

  /* --- type ------------------------------------------------------------- */
  --f-display: "Archivo", system-ui, -apple-system, "Segoe UI", sans-serif;
  --f-body:    "Public Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --f-mono:    "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;

  --t-12:  0.75rem;  --t-14: 0.875rem;  --t-16: 1rem;    --t-18: 1.125rem;
  --t-21:  1.3125rem; --t-26: 1.625rem; --t-32: 2rem;    --t-42: 2.625rem;
  --t-56:  3.5rem;   --t-72: 4.5rem;

  --lh-tight:  0.92;
  --lh-snug:   1.15;
  --lh-normal: 1.6;
  --tr-display: -0.025em;
  --tr-label:    0.1em;

  /* --- motion ----------------------------------------------------------- */
  --d-instant: 120ms;
  --d-quick:   180ms;
  --d-base:    240ms;
  --d-slow:    400ms;
  --d-page:    600ms;
  --e-standard: cubic-bezier(.16, 1, .3, 1);
  --e-exit:     cubic-bezier(.4, 0, 1, 1);

  /* --- elevation: tinted to the ground, never pure black ---------------- */
  --sh-1: 0 1px 2px rgba(18, 22, 31, .06), 0 1px 3px rgba(18, 22, 31, .08);
  --sh-2: 0 2px 6px rgba(18, 22, 31, .07), 0 4px 12px rgba(18, 22, 31, .09);
  --sh-3: 0 6px 16px rgba(18, 22, 31, .10), 0 12px 32px rgba(18, 22, 31, .12);
  --sh-4: 0 12px 28px rgba(18, 22, 31, .14), 0 24px 60px rgba(18, 22, 31, .18);

  --z-base: 1; --z-raised: 10; --z-sticky: 100; --z-drawer: 200;
  --z-modal: 300; --z-toast: 400;
}

/* Desktop opens the rhythm up. Type scale stays; only spacing grows. */
@media (min-width: 760px) {
  :root { --section-y: var(--s-9); --gutter: var(--s-6); }
}
@media (min-width: 1024px) {
  :root { --section-y: var(--s-9); }
}

/* --- brand scopes --------------------------------------------------------
   A section marked data-brand re-points the accent role. Components keep
   referencing --accent, so a TOPRANK card and a VANISH card are the same
   component wearing a different brand.
   ------------------------------------------------------------------------ */
[data-brand="toprank"] {
  --accent:      var(--c-tr-green);
  --accent-text: var(--c-tr-green);   /* 5.93:1 on paper, no deeper variant needed */
  --accent-fg:   #FFFFFF;
  --accent-2:    var(--c-tr-lime);
  --accent-deep: var(--c-tr-green-deep);
  --accent-hot:  var(--c-tr-red);
}
[data-brand="vanish"] {
  --accent:      var(--c-vn-blue);
  --accent-text: var(--c-vn-blue);    /* 7.41:1 on paper */
  --accent-fg:   #FFFFFF;
  --accent-2:    var(--c-vn-yellow);
  --accent-deep: var(--c-vn-blue-deep);
  --accent-hot:  var(--c-vn-red);
}
[data-brand="corporate"] {
  --accent:      var(--c-gold);
  --accent-fg:   var(--c-ink);
  --accent-2:    var(--c-gold-lite);
  --accent-deep: var(--c-navy);
  --accent-hot:  var(--c-gold-mid);
  /* Stated here rather than left to :root. A corporate section nested inside a
     branded one, which is what the related-products block on a product page
     is, otherwise kept the outer brand's accent text while taking the gold
     fill: gold buttons with green labels on four pages. Every scope that sets
     --accent has to set --accent-text with it. */
  --accent-text: var(--c-gold-deep);
}

/* Dark surfaces flip the text roles without touching component CSS.
   The colour must be re-applied here, not only redefined: `color` is already
   resolved at body and inherits as a computed value, so redefining --fg on a
   descendant does nothing on its own. */
[data-surface="dark"] {
  color: var(--fg);
  background: var(--bg);
  --bg:          var(--c-ink);
  --bg-raised:   #1A1F2B;
  --bg-sunken:   #0C0F16;
  --fg:          #F2F1EE;
  --fg-muted:    #B7B9BF;
  --fg-subtle:   #85888F;
  --border:      #2B303C;
  --border-soft: #232833;
  /* On ink the deep gold disappears. The mid gold reads at 6.09:1. */
  --accent-text: var(--c-gold-mid);
  --focus:       var(--c-gold-lite);
}

/* A branded dark section takes the light end of its own palette instead, since
   TOPRANK green and VANISH blue are both too dark to read on ink. */
[data-surface="dark"][data-brand="toprank"], [data-surface="dark"] [data-brand="toprank"] { --accent-text: var(--c-tr-lime); }
[data-surface="dark"][data-brand="vanish"],  [data-surface="dark"] [data-brand="vanish"]  { --accent-text: var(--c-vn-yellow); }

/* --- base.css --- */
/* ==========================================================================
   RESET AND BASE
   Every value comes from tokens.css. No literals below this line.
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 92px;   /* clears the sticky header */
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--f-body);
  font-size: var(--t-16);
  line-height: var(--lh-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd { margin: 0; }
/* :where() drops this to zero specificity. As a bare `ul[class]` it outranks
   any single class, so a component that set its own list margin was silently
   losing to the reset. A reset should never win an argument with a component. */
:where(ul[class], ol[class]) { list-style: none; margin: 0; padding: 0; }

/* The UA rule for [hidden] is a bare attribute selector, so any component that
   sets display on a class beats it and the element stays visible while the
   markup says it is hidden. Error messages and collapsed panels both rely on
   this working. */
[hidden] { display: none !important; }

/* The build wraps images in <picture> to offer an AVIF source. That wrapper
   must not become a layout box: .pdp__shot is a grid item placed with
   grid-area, and the moment a <picture> sat between it and the grid the
   placement stopped applying and both pack shots stacked in flow. display:
   contents removes the wrapper from layout and leaves the <img> as the child
   the stylesheet expects. */


h1, h2, h3, h4 {
  font-family: var(--f-display);
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-display);
  text-wrap: balance;
}
h1 { font-size: clamp(var(--t-32), 6vw, var(--t-72)); }
h2 { font-size: clamp(var(--t-26), 4.2vw, var(--t-42)); }
h3 { font-size: clamp(var(--t-21), 2.6vw, var(--t-32)); }
h4 { font-size: var(--t-18); font-weight: 700; line-height: var(--lh-snug); }

p { max-width: var(--measure); }

a { color: inherit; text-decoration: none; }

img, video, svg { display: block; max-width: 100%; }

/* The build wraps images in <picture> to offer an AVIF source, and that
   wrapper must not become a layout box: .pdp__shot is a grid item placed with
   grid-area, and the moment a <picture> sat between it and the grid the
   placement stopped applying and both pack shots fell into flow at their full
   1200px height, covering the buttons beside them. display: contents removes
   the wrapper from layout and leaves the <img> as the child the stylesheet
   expects. It is set here, in the reset, because a separate rule earlier in
   the file lost to `img, picture, video` further down. */
picture { display: contents; }
img { height: auto; }

button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; padding: 0; cursor: pointer; }

:focus { outline: none; }
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--r-btn);
}

::selection { background: var(--accent); color: var(--accent-fg); }

/* --- utilities ----------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* A narrower measure for pages that are mostly reading and forms, so a text
   block does not stretch across the full shell.
   ------------------------------------------------------------------------
   The measure is now capped on the children rather than on the container.
   Overriding the container's own max-width looked equivalent and was not:
   .container carries margin-inline: auto, so a narrower max-width had the
   auto margins centre the whole column. Every heading on /privacy, /terms,
   /accessibility and the contact FAQ began at x=304 while every other
   heading on the site began at 112, and the h1 no longer lined up with the
   logo above it. It was the same fault as the about page, in four more
   places, and it was never a text-align declaration anywhere.

   Keeping the container full width holds the left edge common to every page,
   and capping the children keeps the line length readable. */
.container--narrow > * { max-width: 56rem; }

.section { padding-block: var(--section-y); }

/* Two plain bands in a row meet with a full measure of padding on each side,
   so the boundary reads as one 192px void rather than as a join. The second
   of the pair takes a smaller share.
   A band with a surface of its own keeps the full measure, because there the
   padding sits on a visible colour change and is doing work. This is why the
   home page had a hole between the pest picker and the brand split, and why
   it is fixed for every plain pair on the site rather than only that one. */
.section:not([data-surface]) + .section:not([data-surface]) {
  padding-block-start: var(--s-7);
}

.stack > * + * { margin-top: var(--flow, var(--s-4)); }

.eyebrow {
  font-family: var(--f-display);
  font-size: var(--t-12);
  font-weight: 700;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--accent-text);
}

.lede { font-size: var(--t-18); color: var(--fg-muted); }

/* The label, heading and standfirst relationship, stated once.
   ------------------------------------------------------------------------
   Headings and paragraphs are margin-reset at the top of this file, which is
   the right default: it stops browser margins leaking into grid and flex
   layouts. The cost is that a plain wrapper around an eyebrow and a heading
   stacks them at a literal zero gap, and that is how they shipped in ten
   places, including the two the client photographed. Every one of those
   wrappers was a different class and three of them had no stylesheet rule at
   all, so fixing them one at a time would have left the next one to be found
   by eye.

   Sibling combinators instead, so the rule follows the pattern rather than
   the container and holds wherever the pattern is used again. Anything that
   wants a different figure overrides it locally, as the product hero does. */
.eyebrow + h1, .eyebrow + h2, .eyebrow + h3 { margin-top: var(--s-2); }
h1 + .lede, h2 + .lede, h3 + .lede { margin-top: var(--s-4); }

.mono {
  font-family: var(--f-mono);
  font-size: 0.9em;
  font-variant-numeric: tabular-nums;
}

.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 {
  position: absolute; top: var(--s-2); left: var(--s-2);
  z-index: var(--z-toast);
  padding: var(--s-3) var(--s-4);
  background: var(--c-ink); color: var(--c-paper);
  border-radius: var(--r-btn);
  transform: translateY(-200%);
  transition: transform var(--d-quick) var(--e-standard);
}
.skip-link:focus-visible { transform: translateY(0); }

/* Wide content scrolls inside its own container; the body never does. */
.scroller { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* --- section reveal ------------------------------------------------------
   Resting state is visible. The observer adds .is-in to animate; if JS never
   runs, or the observer never fires, the page is still complete.
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: no-preference) {
  .reveal { opacity: 0; transform: translate3d(0, 20px, 0); }
  .reveal.is-in {
    opacity: 1; transform: none;
    transition: opacity var(--d-slow) var(--e-standard),
                transform var(--d-slow) var(--e-standard);
    transition-delay: var(--reveal-delay, 0ms);
  }
  .no-js .reveal { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- components.css --- */
/* ==========================================================================
   COMPONENTS
   Mobile-first. Desktop rules live in their own @media block per component,
   fully specifying that layout, so a change to one breakpoint cannot leak.
   ========================================================================== */

/* --- button --------------------------------------------------------------
   States: rest, hover, active, focus-visible, loading, disabled.
   ------------------------------------------------------------------------ */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  min-height: 44px;                      /* touch target */
  padding: var(--s-3) var(--s-5);
  border-radius: var(--r-btn);
  font-family: var(--f-display);
  font-size: var(--t-14);
  font-weight: 800;
  letter-spacing: 0.055em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  cursor: pointer;
  transition: transform var(--d-quick) var(--e-standard),
              box-shadow var(--d-base) var(--e-standard),
              background var(--d-base) var(--e-standard);
}

/* The one width where nowrap cannot be honoured.
   ------------------------------------------------------------------------
   A button label belongs on one line, which is what nowrap above is for. On a
   320px phone the longest label on the site, "See the whole pest library" on
   the home page, comes out 323px wide in the 288px the container leaves, and
   a nowrap box that cannot fit does not clip: it pushes the document wider and
   the whole page scrolls sideways. Measured on every route: this is the only
   button it happens to, and it stops happening at 344px.
   So below 360 a label that will not fit is allowed to wrap. The arrow stays
   on the first line beside it, because .btn is a flex container and the arrow
   is its own item; only the text reflows. Nothing else changes, and above 360
   nowrap still holds. */
@media (max-width: 359px) {
  .btn { white-space: normal; text-wrap: balance; }
}

.btn__arrow {
  width: 1.05em; height: 1.05em; flex: none;
  transition: transform var(--d-base) var(--e-standard);
}

/* one sheen sweep on hover, so the control reads as live */
/* An arrow inside a button carries a viewBox and no width or height, and no
   rule ever sized it. So its used size came out of whatever the flex layout
   happened to leave over: about 15px in the header, where it looked correct,
   and exactly 0 inside the brand cards, where the button is shrink-to-fit and
   the arrow was simply invisible. Stated once here, it renders the same in
   every button. 1.05em matches what the header was already showing, so
   nothing that looked right moves. */
.btn svg { width: 1.05em; height: 1.05em; flex: none; }

.btn::after {
  content: ""; position: absolute; inset-block: 0; left: -60%; width: 42%;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.4), transparent);
  transform: skewX(-18deg); opacity: 0; pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {

  .btn:hover::after { animation: btn-sheen var(--d-slow) var(--e-standard); }
  
}
@keyframes btn-sheen {
  0%   { left: -60%; opacity: 0; }
  20%  { opacity: 1; }
  100% { left: 120%; opacity: 0; }
}
.btn:active { transform: translateY(0) scale(.985); }
.btn[aria-disabled="true"], .btn:disabled {
  opacity: .5; pointer-events: none; transform: none;
}
.btn.is-loading { color: transparent; pointer-events: none; }
.btn.is-loading::before {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 1.1em; height: 1.1em; border-radius: var(--r-pill);
  border: 2px solid currentColor; border-top-color: transparent;
  color: var(--accent-fg);
  animation: btn-spin 700ms linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }

.btn--primary {
  background: var(--accent);
  color: var(--accent-fg);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.28), var(--sh-2);
}

.btn--ghost {
  background: transparent;
  color: var(--fg);
  box-shadow: inset 0 0 0 1px var(--border);
}

.btn--block { width: 100%; }

/* --- links ---------------------------------------------------------------
   Underline draws from the left; nav underline grows from the centre.
   ------------------------------------------------------------------------ */
.link {
  position: relative;
  color: inherit;
  /* Drawn at rest, not on hover, so a touch screen can see it; hover only
     thickens a line that is already there.

     text-decoration rather than a background gradient, because a gradient is
     sized against the element box. .section__more is a block and was drawing
     a 358px rule under 65px of text on a phone. This measures glyphs, so it
     stops at the last character at any width, and underlines each line
     separately when a link wraps. */
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-thickness var(--d-base) var(--e-standard),
              color var(--d-quick) var(--e-standard);
}

/* --- header --------------------------------------------------------------
   MOBILE: logo left, burger right, nav and CTA inside the drawer.
   ------------------------------------------------------------------------ */
.site-header {
  position: sticky; top: 0; z-index: var(--z-sticky);
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border-soft);
}
.site-header__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4);
  min-height: 64px;
}
.site-header__brand img { width: auto; height: 34px; }
.site-nav, .site-header__cta { display: none; }

.site-header__toggle {
  display: grid; place-items: center;
  width: 44px; height: 44px; margin-right: calc(var(--s-2) * -1);
  border-radius: var(--r-btn);
}
.site-header__bars { display: grid; gap: 5px; width: 22px; }
.site-header__bars i {
  display: block; height: 2px; background: var(--fg); border-radius: 2px;
  transition: transform var(--d-base) var(--e-standard),
              opacity var(--d-quick) var(--e-standard);
}
[aria-expanded="true"] .site-header__bars i:nth-child(1) { transform: translateY(7px) rotate(45deg); }
[aria-expanded="true"] .site-header__bars i:nth-child(2) { opacity: 0; }
[aria-expanded="true"] .site-header__bars i:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.site-drawer {
  border-top: 1px solid var(--border-soft);
  background: var(--bg-raised);
  overflow: hidden;
}
.site-drawer__nav { padding: var(--s-4) var(--gutter) var(--s-6); }
.site-drawer__nav ul { list-style: none; margin: 0 0 var(--s-4); padding: 0; }
.site-drawer__nav li + li { border-top: 1px solid var(--border-soft); }
/* scoped to the list, or it overrides the CTA button inside the drawer */
.site-drawer__nav ul a {
  display: block; padding: var(--s-4) 0;
  font-family: var(--f-display); font-weight: 700; font-size: var(--t-18);
}
.site-drawer__calls {
  display: grid; gap: var(--s-2); margin-top: var(--s-4);
  font-family: var(--f-mono); font-size: var(--t-14); color: var(--fg-muted);
}

/* DESKTOP: inline nav, CTA visible, drawer and burger removed entirely. */
@media (min-width: 1024px) {
  .site-header__inner { min-height: 76px; }
  .site-header__brand img { height: 40px; }
  .site-header__toggle, .site-drawer { display: none; }
  .site-nav { display: block; margin-left: auto; }
  .site-nav__list { display: flex; align-items: center; gap: var(--s-6); }
  .site-header__cta { display: inline-flex; margin-left: var(--s-6); }

  .nav__link {
    position: relative;
    font-family: var(--f-display); font-weight: 700;
    font-size: var(--t-14); letter-spacing: .02em;
    padding-block: var(--s-2);
  }
  .nav__link::after {
    content: ""; position: absolute; left: 50%; right: 50%; bottom: 0; height: 2px;
    background: var(--accent); border-radius: 2px;
    transition: left var(--d-base) var(--e-standard), right var(--d-base) var(--e-standard);
  }
.nav__link.is-current::after{ left: 0; right: 0; }

  .nav__link:hover::after { left: 0; right: 0; }
  
}

/* --- footer -------------------------------------------------------------- */
.site-footer { background: var(--bg); color: var(--fg); padding-block: var(--s-7) var(--s-5); }
.site-footer__top { display: grid; gap: var(--s-7); }
.site-footer__identity img { width: auto; height: 46px; margin-bottom: var(--s-4); }
.site-footer__slogan {
  font-family: var(--f-display); font-weight: 700; font-size: var(--t-18);
  color: var(--accent-2, var(--c-gold-lite)); margin-bottom: var(--s-4);
  max-width: 22ch;
}
.site-footer__address { font-style: normal; color: var(--fg-muted); font-size: var(--t-14); }
.site-footer__calls { list-style: none; margin: var(--s-4) 0 0; padding: 0; display: grid; gap: var(--s-2); }
.site-footer__calls a {
  display: inline-flex; align-items: center; gap: var(--s-2);
  font-family: var(--f-mono); font-size: var(--t-14); min-height: 32px;
}
.site-footer__calls svg { width: 15px; height: 15px; opacity: .7; }
.site-footer__note { font-size: var(--t-12); color: var(--fg-subtle); margin-top: var(--s-2); }

.site-footer__nav { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-6) var(--s-4); }
.site-footer__title {
  font-family: var(--f-display); font-size: var(--t-12); font-weight: 700;
  letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--fg-subtle); margin-bottom: var(--s-3);
}
.site-footer__nav ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
.site-footer__nav a { font-size: var(--t-14); color: var(--fg-muted); }

.site-footer__legalblock {
  margin-top: var(--s-7); padding-top: var(--s-5);
  border-top: 1px solid var(--border);
}
.site-footer__attribution { font-size: var(--t-12); color: var(--fg-muted); line-height: var(--lh-snug); }

.site-footer__bottom {
  display: grid; gap: var(--s-3);
  margin-top: var(--s-5); padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}
.site-footer__copy { font-size: var(--t-12); color: var(--fg-subtle); }
.site-footer__legal { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s-4); margin: 0; padding: 0; }
.site-footer__legal a { font-size: var(--t-12); color: var(--fg-subtle); }

@media (min-width: 760px) {
  .site-footer { padding-block: var(--s-9) var(--s-6); }
  .site-footer__top { grid-template-columns: 1fr 1.4fr; gap: var(--s-8); }
  .site-footer__nav { grid-template-columns: repeat(4, 1fr); }
  .site-footer__bottom { grid-template-columns: 1fr auto; align-items: center; }
}

/* --- media ---------------------------------------------------------------
   Space is reserved from --ratio before anything loads, so an arriving image
   never pushes the page around. The blurred placeholder is a background on
   the img itself, so it is visible from first paint.
   ------------------------------------------------------------------------ */
.media { margin: 0; }
.media__img,
.media__slot {
  width: 100%;
  aspect-ratio: var(--ratio, 16 / 9);
  border-radius: var(--r-card);
  object-fit: cover;
}
.media__img {
  background-size: cover;
  background-position: center;
  opacity: 0;
  transition: opacity var(--d-slow) var(--e-standard);
}
.media__img.is-loaded { opacity: 1; }
.no-js .media__img { opacity: 1; }

.media__slot {
  display: grid; place-items: center;
  background: var(--bg-sunken);
  border: 1px dashed var(--border);
  color: var(--fg-subtle);
}
.media__slotlabel {
  font-size: var(--t-12);
  text-align: center;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  line-height: 1.8;
}
.media__caption {
  margin-top: var(--s-2);
  font-size: var(--t-12);
  color: var(--fg-subtle);
  max-width: var(--measure);
}

/* --- card ----------------------------------------------------------------
   The image scales inside a fixed frame, so the card's own footprint never
   changes on hover and nothing around it moves.
   ------------------------------------------------------------------------ */
.card {
  position: relative;
  display: flex; flex-direction: column;
  background: var(--bg-raised);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-card);
  overflow: hidden;
  transition: transform var(--d-base) var(--e-standard),
              box-shadow var(--d-base) var(--e-standard),
              border-color var(--d-base) var(--e-standard);
}
.card__frame { overflow: hidden; }
.card__frame .media__img,
.card__frame .media__slot { border-radius: 0; }
.card__frame img { transition: transform var(--d-slow) var(--e-standard); }
.card__body { padding: var(--s-4); display: flex; flex-direction: column; gap: var(--s-2); flex: 1; }
.card__eyebrow {
  font-family: var(--f-display); font-size: var(--t-12); font-weight: 700;
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--accent-text);
}
.card__title { font-family: var(--f-display); font-size: var(--t-21); font-weight: 800; letter-spacing: -.02em; line-height: var(--lh-snug); }
.card__text { font-size: var(--t-14); color: var(--fg-muted); }
.card__foot { margin-top: auto; padding-top: var(--s-3); display: flex; align-items: center; gap: var(--s-2); font-family: var(--f-display); font-weight: 700; font-size: var(--t-14); color: var(--accent-text); }
.card__foot svg { width: 1em; height: 1em; transition: transform var(--d-base) var(--e-standard); }

/* whole card is the target; the link stretches over it */
.card__link::after { content: ""; position: absolute; inset: 0; }
/* The lift is keyed to :focus-visible, not :focus-within.
   ------------------------------------------------------------------------
   Tapping a card's link gives that link focus, and with :focus-within the
   card stayed lifted after the finger came away, which is the same lingering
   state that hovering used to leave on a phone. :focus-visible only matches
   when the browser judges that focus needs showing, which excludes a mouse
   click and a tap and includes every keyboard route, so the keyboard user
   keeps the feedback and nobody else is left with a card stuck up in the air.
   :has() rather than :focus-visible-within, which is not a selector. */
.card:has(:focus-visible) { transform: translateY(-4px); box-shadow: var(--sh-3); border-color: var(--border); }
.card:has(:focus-visible) { outline: 3px solid var(--focus); outline-offset: 3px; }
.card__link:focus-visible { outline: none; }

.card-grid { display: grid; gap: var(--s-4); grid-template-columns: 1fr; }
@media (min-width: 640px)  { .card-grid { grid-template-columns: repeat(2, 1fr); gap: var(--s-5); } }
@media (min-width: 1024px) { .card-grid { grid-template-columns: repeat(var(--cols, 3), 1fr); } }

/* --- chip row ------------------------------------------------------------ */
.chips { display: flex; flex-wrap: wrap; align-items: center; }
.chip { display: flex; align-items: center; gap: var(--s-2); padding-inline: var(--s-4); }
.chip:first-child { padding-left: 0; }
.chip + .chip { border-left: 1px solid var(--border); }
.chip__icon { width: 22px; height: 22px; flex: none; color: var(--accent-text); transition: transform var(--d-base) var(--e-standard); }
.chip__label { font-family: var(--f-display); font-size: var(--t-12); font-weight: 700; text-transform: uppercase; letter-spacing: .04em; line-height: 1.25; }
@media (max-width: 639px) {
  .chip { padding-inline: var(--s-3); }
  .chip__icon { width: 18px; height: 18px; }
  .chip__label { font-size: 0.6875rem; }
}

/* --- accordion ----------------------------------------------------------- */
.accordion { border-top: 1px solid var(--border-soft); }
.accordion__item { border-bottom: 1px solid var(--border-soft); }
.accordion__btn {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-4); padding: var(--s-4) 0; min-height: 56px; text-align: left;
  font-family: var(--f-display); font-weight: 700; font-size: var(--t-18);
  transition: color var(--d-quick) var(--e-standard);
}
.accordion__icon { width: 20px; height: 20px; flex: none; color: var(--accent-text); transition: transform var(--d-base) var(--e-standard); }
.accordion__btn[aria-expanded="true"] .accordion__icon { transform: rotate(180deg); }
.accordion__panel { overflow: hidden; }
.accordion__inner { padding-bottom: var(--s-4); color: var(--fg-muted); }

/* --- form ---------------------------------------------------------------- */
.field { display: grid; gap: var(--s-2); margin-bottom: var(--s-4); }
.field__label { font-family: var(--f-display); font-weight: 700; font-size: var(--t-14); }
.field__req { color: var(--c-critical); }
.field__help { font-size: var(--t-12); color: var(--fg-subtle); }
.field__input, .field__select, .field__textarea {
  width: 100%; min-height: 48px; padding: var(--s-3);
  background: var(--bg-raised); color: var(--fg);
  border: 1px solid var(--border); border-radius: var(--r-field);
  transition: border-color var(--d-quick) var(--e-standard),
              box-shadow var(--d-quick) var(--e-standard);
}
.field__textarea { min-height: 132px; resize: vertical; }
.field__input:focus, .field__select:focus, .field__textarea:focus {
  outline: none; border-color: var(--accent-text);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
.field.is-error .field__input,
.field.is-error .field__textarea { border-color: var(--c-critical); }
.field__error { display: flex; align-items: center; gap: var(--s-2); font-size: var(--t-12); color: var(--c-critical); }
.field__input:disabled, .field__textarea:disabled { opacity: .55; cursor: not-allowed; }

.form__status { padding: var(--s-4); border-radius: var(--r-card); font-size: var(--t-14); }
.form__status--ok { background: var(--c-ok-wash); color: var(--c-ok); }
.form__status--error { background: var(--c-critical-wash); color: var(--c-critical); }

/* --- breadcrumbs --------------------------------------------------------- */
.breadcrumbs { padding-block: var(--s-4); }
.breadcrumbs ol { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); list-style: none; margin: 0; padding: 0; }
.breadcrumbs li { display: flex; align-items: center; gap: var(--s-2); font-size: var(--t-12); color: var(--fg-subtle); }
.breadcrumbs li + li::before { content: "/"; color: var(--border); }
.breadcrumbs [aria-current="page"] { color: var(--fg-muted); }
/* The current page is text and the steps above it are links, and the only
   thing that said so was a hover colour. */
.breadcrumbs a { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }

/* --- modal and lightbox --------------------------------------------------
   Focus is trapped while open, Escape closes, and focus returns to whatever
   opened it. Background scroll is locked without the page jumping.
   ------------------------------------------------------------------------ */
.modal { position: fixed; inset: 0; z-index: var(--z-modal); display: grid; place-items: center; padding: var(--s-4); }
.modal[hidden] { display: none; }
.modal__scrim {
  position: absolute; inset: 0;
  background: rgba(12, 15, 22, .78);
  backdrop-filter: blur(3px);
  opacity: 0; transition: opacity var(--d-base) var(--e-standard);
}
.modal.is-open .modal__scrim { opacity: 1; }
.modal__panel {
  position: relative; z-index: 1;
  width: min(100%, 60rem); max-height: 88vh; overflow: auto;
  background: var(--bg-raised); border-radius: var(--r-card);
  box-shadow: var(--sh-4);
  opacity: 0; transform: translateY(14px) scale(.98);
  transition: opacity var(--d-base) var(--e-standard), transform var(--d-base) var(--e-standard);
}
.modal.is-open .modal__panel { opacity: 1; transform: none; }
.modal__close {
  position: absolute; top: var(--s-3); right: var(--s-3); z-index: 2;
  width: 44px; height: 44px; display: grid; place-items: center;
  border-radius: var(--r-pill); background: var(--bg-raised);
  box-shadow: var(--sh-2); color: var(--fg);
  transition: transform var(--d-quick) var(--e-standard), background var(--d-quick) var(--e-standard);
}
.modal__close svg { width: 20px; height: 20px; }
.modal--lightbox .modal__panel { background: transparent; box-shadow: none; width: min(100%, 72rem); }
.modal--lightbox img { width: 100%; height: auto; border-radius: var(--r-card); }
body.is-locked { overflow: hidden; }

/* --- hover, pointer devices only -----------------------------------------
   Behind a hover query so a touch screen never matches it. Without the
   guard the first tap on anything styled here only applied the hover and
   a second tap was needed to follow the link, which is the double-tap
   this site was reported for. Selector lists were split where they mixed
   :hover with :focus-within or :focus-visible, so keyboard feedback stays
   outside the guard and still works everywhere.
   ------------------------------------------------------------------------ */

  .btn:hover {transform: translateY(-2px); }
  .btn:hover .btn__arrow {transform: translateX(3px); }
  .btn--primary:hover {box-shadow: inset 0 1px 0 rgba(255,255,255,.32), var(--sh-3); }
  .btn--ghost:hover {box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent-text); }
  .link:hover {text-decoration-thickness: 2px; color: var(--accent-text); }
  .site-footer__calls a:hover {color: var(--accent-2, var(--c-gold-lite)); }
  .site-footer__nav a:hover {color: var(--fg); }
  .site-footer__legal a:hover {color: var(--fg); }
  .card:hover {transform: translateY(-4px); box-shadow: var(--sh-3); border-color: var(--border); }
  .card:hover .card__frame img {transform: scale(1.04); }
  .card:hover .card__foot svg {transform: translateX(4px); }
  .chip:hover .chip__icon {transform: scale(1.14) rotate(-6deg); }
  .accordion__btn:hover {color: var(--accent-text); }
  .field__input:hover, .field__select:hover, .field__textarea:hover {border-color: var(--fg-subtle); }
  .breadcrumbs a:hover {color: var(--accent-text); }
  .modal__close:hover {transform: rotate(90deg); background: var(--bg-sunken); }


/* --- carousel.css --- */
/* ==========================================================================
   HERO CAROUSEL
   Generated by scripts/port-carousel.js from carousel/template.html.
   Do not edit here. Edit the template, re-run the port, rebuild.
   ========================================================================== */

  /* ===================================================================
     C-SUCCESS HERO CAROUSEL

     Three layers per slide:
       1  plate     generated photographic scene
       2  product   approved master, graded to the scene in the pipeline
       3  copy      live HTML

     Desktop and mobile are written as two SEPARATE rule sets, each fully
     specifying its own layout. Nothing positional is shared between them,
     so a change to one cannot leak into the other.

     Per-slide light direction drives the cast shadow, the rim light and
     the reflection, so each product is lit by the room it stands in.
     =================================================================== */

  :root {
    --paper:#F4F3F0; --surface:#FFFFFF; --ink:#15171B; --ink-2:#43464D; --ink-3:#72767D;
    --rule:#D7D4CD; --gold:#A87818; --gold-mid:#C89A38; --gold-lite:#F5C842; --gold-wash:#F5EDDC;
    --toprank:#1F6B27; --vanish:#1B4C9B;
    --r-btn:4px;
  }


  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --paper:#0F1013; --surface:#181A1E; --ink:#EBE9E4; --ink-2:#B4B1AA; --ink-3:#81848B;
      --rule:#2D3036; --gold:#D2A75A; --gold-mid:#D6AC5C; --gold-wash:#2B2416;
      --toprank:#6FBF73; --vanish:#7DA8EA;
    }
  }



  .stage { margin:0 auto; }


  .carousel { position:relative; overflow:hidden; background:#0A0C10; box-shadow:0 26px 70px rgba(0,0,0,.22); }


  .track { display:flex; width:100%; transition:transform .74s cubic-bezier(.16,1,.3,1); }


  .slide { flex:0 0 100%; min-width:0; }



  /* ---------- shared visual language, non-positional ---------- */
  .canvas { position:relative; overflow:hidden; isolation:isolate; --sh:0,0,0; }


  .plate { position:absolute; inset:0; z-index:0; }


  .plate img {
    position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block;
    /* the resting position, for prefers-reduced-motion where drift is off */
    transform:scale(1.04) translate3d(var(--px,0px),0,0);
  }
/* the daylight room reads as glare on a phone; hold it back */
@media (min-width: 761px) { .stage .s-toprank .plate img{ filter:brightness(.93); } }


  .scrim { position:absolute; inset:0; z-index:1; pointer-events:none; }


  .grain {
    position:absolute; inset:0; z-index:6; pointer-events:none; opacity:.13; mix-blend-mode:overlay;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
  }


  .band { position:absolute; left:0; right:0; bottom:0; z-index:2; pointer-events:none; }



  /* ---------- compositing ----------
     --lx    cast shadow x, away from the key light
     --ly    cast shadow y
     --lb    cast shadow blur      --la  cast shadow alpha
     --pool  tight contact pool alpha, where the product meets the surface
     --pool2 broad pool alpha, thrown with the light
     --refl  reflection opacity, 0 where the surface is matte
     --sh    shadow colour as an r,g,b triplet, default 0,0,0

     --sh exists because a black shadow on honey-coloured wood reads as a grey
     smear rather than a shadow: the surface bounces its own colour back into
     the shade, so the shade keeps a trace of it. It is only set where the
     surface is both bright and strongly coloured, which on this site is the
     TOPRANK mobile plate and nothing else. Everywhere else the default stands,
     because a tint on a dark floor is invisible and a variable that changes
     nothing is a variable somebody has to read later.

     --rx and --rim used to be declared here as a rim light. Nothing ever read
     either of them: grep the stylesheet and they appear once each, in the
     declaration. They are gone rather than left sitting in five rule sets
     looking like they do something.

     Every figure below is measured off the round-two plate it belongs to, not
     carried over. The key light moved on all four product slides and the
     rooms are no longer all the same brightness, which the two pool alphas
     now answer: a near-black contact pool reads as a hole punched in a dark
     bedroom and as a correct shadow in a sunlit kitchen, so it cannot be one
     number for all five. */
  /* height:100% gives .prod a definite box, so the percentage heights on the
     product images resolve instead of falling back to natural size */
  /* The resting position, for when the entry animation never runs: no script,
     or prefers-reduced-motion, where the animation is switched off outright. */
  .shot { transform:translate3d(var(--nx,0px),var(--ny,0px),0); }



  .prod { position:relative; height:100%; display:flex; align-items:flex-end; justify-content:center; }


  .prod img.main {
    display:block; width:auto; max-width:100%; object-fit:contain; position:relative; z-index:2;
    filter: drop-shadow(var(--lx) var(--ly) var(--lb) rgba(var(--sh), var(--la)));
  }


  /* tight core where the product meets the surface */
  .prod::before {
    content:""; position:absolute; left:50%; bottom:-0.4%; z-index:1;
    transform:translateX(calc(-50% + var(--lx) * .3));
    width:82%; height:2.8%; border-radius:50%; filter:blur(2px);
    background:radial-gradient(ellipse at center,
      rgba(var(--sh), calc(var(--pool,.55) * 1.6)) 0%,
      rgba(var(--sh), calc(var(--pool,.55) * .84)) 48%,
      rgba(var(--sh), 0) 76%);
  }


  /* broad soft pool, thrown with the light */
  .prod::after {
    content:""; position:absolute; left:50%; bottom:-1.4%; z-index:1;
    transform:translateX(calc(-50% + var(--lx) * .9));
    width:186%; height:8.5%; border-radius:50%; filter:blur(10px);
    background:radial-gradient(ellipse at center,
      rgba(var(--sh), var(--pool2,.30)) 0%,
      rgba(var(--sh), calc(var(--pool2,.30) * .41)) 46%,
      rgba(var(--sh), 0) 76%);
  }


  .prod img.mirror {
    position:absolute; top:100%; left:50%; width:auto; z-index:0;
    transform:translateX(-50%) scaleY(-1); opacity:var(--refl); filter:blur(2px);
    /* `to top`, and the reason is the scaleY(-1) on the line above.
       ------------------------------------------------------------------
       A mask is resolved in the element's own box and then the transform is
       applied to the masked result, so the flip takes the mask with it: the
       gradient's 0% end, written at the local top, is painted at the bottom
       of what you see. So a reflection that is strongest where the product
       touches the surface and fades with distance has to be written opaque at
       the local BOTTOM, which is `to top` from 0%.
       This was `to bottom` for a while, on the reasoning that the flipped
       element's local bottom is the product's top. That much is true of the
       content and says nothing about the mask, and the result was a
       reflection erased exactly where it should have been strongest: at
       --refl .9 the floor under the VANISH cans was still bare. Checked
       against a no-mask render before changing it back. */
    -webkit-mask-image:linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 38%);
    mask-image:linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 38%);
  }



  /* ---------- typography, shared scale ---------- */
  .head {
    font-family:"Archivo",system-ui,sans-serif; font-weight:900;
    text-transform:uppercase; letter-spacing:-.022em; line-height:.92;
    margin:0; text-wrap:balance;
  }


  .head b { font-weight:900; }


  .accentrule { height:3px; width:3.2rem; border-radius:2px; }


  .sub { font-family:"Archivo",sans-serif; font-weight:600; margin:0; }


  .kicker { font-family:"Archivo",sans-serif; font-weight:700; text-transform:uppercase; letter-spacing:.14em; margin:0; }



  .chips { display:flex; align-items:center; flex-wrap:wrap; }


  .chip { display:flex; align-items:center; gap:.45rem; }


  .chip + .chip { border-left:1px solid currentColor; }


  .chip svg { height:1.15em; width:auto; flex:none; opacity:.95; }


  .chip span { font-family:"Archivo",sans-serif; font-weight:700; text-transform:uppercase; letter-spacing:.04em; line-height:1.2; }



  /* The slide is the link.
     ----------------------------------------------------------------------
     There was a button on every slide, twice over: one in .copy for desktop
     and one in .foot for mobile, ten in all. They are gone and one link per
     slide covers the canvas instead, which is what was asked for and also
     fixes something worse. Every one of those buttons pointed at a fragment,
     #toprank, #vanish, #glue-traps, #incense, #about, and not one of those ids
     exists anywhere on the home page. All five went nowhere. They now go to
     the pages: /toprank/, /vanish/, the two product pages and /about/.

     Sits at z-index 7, which is above .copy at 5 and .grain at 6 and below the
     previous and next controls and the dots at 8. So the link takes the whole
     frame except where those sit, and the region the controls own stays theirs
     without any geometry having to be kept in sync between the two.

     It carries the wording the button carried, in a .sr span, so the link has
     the same accessible name it always had and nothing new was written. The
     cost of losing the button is real and worth saying out loud: on a touch
     screen there is no hover, so nothing now signals that the banner is
     tappable. Sighted mouse users get the cursor and the headline treatment
     below; keyboard users get the focus ring. */
  .slidelink {
    position:absolute; inset:0; z-index:7; display:block;
    -webkit-tap-highlight-color:transparent;
  }


  .slidelink:focus-visible { outline:3px solid var(--gold-lite); outline-offset:-5px; }



  /* The only affordance left, so it has to earn its keep: the accent rule
     grows and the headline lifts a pixel. No new elements, nothing that reads
     as a button, and it costs nothing on a slide nobody is pointing at. */
  .canvas:has(.slidelink:hover) .accentrule,
  .canvas:has(.slidelink:focus-visible) .accentrule { width:5.2rem; }


  .canvas:has(.slidelink:hover) .head,
  .canvas:has(.slidelink:focus-visible) .head { transform:translateY(-1px); }


  .head, .accentrule { transition:width .26s ease, transform .26s ease; }



  /* The attribution had a text shadow and nothing else, which was enough over
     the round-one plates because their top right corners were all dark. Two of
     the round-two scenes put a sunlit window and a lit lantern up there, and
     white text on a 0.55 shadow disappeared into both. It now carries its own
     small scrim, so it does not depend on what the scene happens to do behind
     it. */
  .marketed {
    position:absolute; z-index:5; font-family:"Archivo",sans-serif; font-weight:600;
    line-height:1.35; text-align:right; text-shadow:0 1px 6px rgba(0,0,0,.55);
    padding:.45rem .7rem; border-radius:4px;
    background:radial-gradient(140% 170% at 72% 50%, rgba(0,0,0,.34), rgba(0,0,0,0) 72%);
  }



  /* ---------- per-slide light and colour ---------- */
  /* corp carries no product, so its light figures do nothing. Left in place
     so the five rule sets stay the same shape. */
  .s-corp    { --lx:-9px; --ly:13px; --lb:17px; --la:.5; --pool:.5; --pool2:.26; --refl:.2;
               --accent:#F5C842; --btn:#C89A38; --btnink:#100F0C; --sub:#C6C2B9; --chip:#D9D5CC; }



  /* Sunlit room, window hard left, satin light-wood sideboard. The key light
     is at x124 of 1536 and the cans stand near x1100, so it is a long lateral
     throw: the shadow runs well to the right and stays soft. Bright surfaces
     show a contact shadow plainly, so the pools are the strongest of the set.
     Satin wood barely reflects. */
  /* Light intensity down: the throw is shorter and the shadow softer and
     paler than the first pass, which read as a harder light than the room
     actually has. Reflection off, with the contact pool taking over the job
     of grounding the cans. */
  .s-toprank { --lx:10px; --ly:12px; --lb:26px; --la:.30; --pool:.58; --pool2:.26; --refl:0;
               --accent:#C6172A; --btn:#1F6B27; --btnink:#FFFFFF; --sub:#F2EDE0; --chip:#F5F1E6; }



  /* Dark bedroom, one warm bedside lamp. The lamp sits left of the chest of
     drawers and close to it, so the throw is short and to the right: --lx was
     negative, which threw the shadow back toward the lamp. The brightest
     pixels in this plate are the lit bedding on the right, not the lamp, which
     is what made an automatic reading of the light direction wrong here.
     Pools are the softest of the set: a near-black pool on a dark floor reads
     as a hole rather than a shadow. Polished wood, so the reflection stays. */
  /* Reflection off and a real shadow in its place. At .30 on a polished top
     the mirrored can was the loudest thing on the slide and it spilled over
     the front edge of the chest of drawers, which a reflection cannot do. The
     cast shadow and both pools carry the weight now. */
  .s-vanish  { --lx:9px; --ly:13px; --lb:18px; --la:.58; --pool:.54; --pool2:.30; --refl:0;
               --accent:#F5C842; --btn:#F5C842; --btnink:#0D2140; --sub:#C3D6EF; --chip:#DCE8F8; }



  /* Sunlit kitchen, window hard right at x1431, polished marble worktop. The
     throw is to the left and longer than before. Marble reflects, so the
     mirror is up. */
  /* Reflection off here too. Marble is the one surface in the set that could
     carry one, but the same inverted mask made it a pale green haze across
     the worktop rather than a mirror. */
  .s-glue    { --lx:-14px; --ly:12px; --lb:18px; --la:.46; --pool:.56; --pool2:.30; --refl:0;
               --accent:#C3EE78; --btn:#C3EE78; --btnink:#0F2B0D; --sub:#C4D6C0; --chip:#DBEBD6; }



  /* Veranda at dusk, hanging lantern above and to the right. High and close,
     so the throw is left and short. Dark scene, so soft pools; the table has
     a warm sheen rather than a polish. */
  /* Reflection off. On the dusk veranda it rendered as a violet haze over the
     table, the green pack and the cream cup mixed and tinted by the scrim. */
  .s-incense { --lx:-10px; --ly:11px; --lb:16px; --la:.50; --pool:.48; --pool2:.26; --refl:0;
               --accent:#FFD277; --btn:#F5C842; --btnink:#12301F; --sub:#DFE0CA; --chip:#EDEBD4; }



  .head       { color:#fff; }


  .head b     { color:var(--accent); }


  .accentrule { background:var(--accent); }


  .sub        { color:var(--sub); }


  .kicker     { color:var(--accent); }


  .chip       { color:var(--chip); }


  .marketed   { color:rgba(255,255,255,.62); }



  /* TOPRANK sits in daylight, so its copy is dark on a bright plate */
  .s-toprank .head     { color:#10340F; }


  .s-toprank .sub      { color:#33402D; }


  .s-toprank .chip     { color:#2A4224; }


  /* Dark text on the two slides whose corner is bright, white on the three
     whose corner is dark. Measured off the plates rather than guessed: the
     attribution corner reads 200 on TOPRANK and 196 on the glue trap kitchen,
     against 72, 31 and 73 on the other three. A white line needs a ground
     behind it, and a ground on a sunlit wall reads as a grey box. */
  .s-toprank .marketed,
  .s-glue .marketed {
    color:rgba(20,24,18,.82); text-shadow:0 1px 4px rgba(255,255,255,.6);
    background:none;
  }
/* =================================================================
     DESKTOP LAYOUT  -  2:1, copy left, product right
     ================================================================= */
@media (min-width: 761px) { .stage{ max-width:76rem; } }
@media (min-width: 761px) { .stage .canvas{ aspect-ratio:2/1; } }
@media (min-width: 761px) { .stage .scrim{
    background:linear-gradient(92deg, rgba(0,0,0,.72) 0%, rgba(0,0,0,.42) 34%, rgba(0,0,0,.06) 56%, rgba(0,0,0,0) 70%);
  } }
@media (min-width: 761px) { .stage .s-toprank .scrim{
    background:linear-gradient(92deg, rgba(255,253,246,.20) 0%, rgba(255,253,246,.10) 34%, rgba(255,253,246,.02) 58%, rgba(255,253,246,0) 72%);
  } }
/* and the same local wash the mobile version of this slide needs, for the
     same reason and found the same way.
     ----------------------------------------------------------------------
     The frame scrim above is already a white wash, but it runs left to right
     and is down to a quarter by the middle of the copy column, which is where
     the third line reaches. That line ends in TOPRANK. in red, and the red
     landed on the seated woman's hair and the shadow under the sofa: measured
     1.00:1, which is to say the word and the thing behind it have the same
     luminance and it is legible by hue alone. The three dark green lines were
     fine at 3.8 to 5.3:1, so the frame scrim is doing its job and the red is
     simply the one colour in the set that cannot survive a mid grey.

     Lightening the ground rather than darkening the word, because a darker
     red does not help: against a mid grey even #8E1020 only reaches 1.6:1,
     and the colour comes off the pack anyway.

     Behind the headline and not behind the copy column, which is where this
     started. A wash over the column cleared the red at 3.9:1 and bleached the
     left half of the room doing it: the mother and the two children went to
     ghosts, which is the opposite of what these plates were regenerated for.
     Confined to the headline it hazes the part of her hair the third line
     crosses, which is what a headline on a scrim does, and leaves the
     children, the sofa and the lamp alone. */
@media (min-width: 761px) { .stage .s-toprank .head{ position:relative; } }
/* A blurred rounded panel rather than a gradient, because the shape of the
     thing that needs covering is a block of three lines and a radial gradient
     is an ellipse. Sized to the headline, an ellipse wide enough to reach the
     end of the third line was too flat to reach the top of the first, and one
     tall enough for both was wide enough to bleach the room again: measured,
     the far end of TOPRANK. sat at 1.08 of the ellipse's radius and got
     nothing. A blur gives the same soft falloff on all four sides and a flat
     middle, which is what a scrim behind a paragraph actually wants. */
  /* The cans' shadow on this slide, which was not there.
     ----------------------------------------------------------------------
     Same fault the mobile pools had and the same fix, scoped here so the four
     slides nobody asked about keep the shadows they have. The tight pool is
     82% of the product, narrower than the thing standing on it, and at
     bottom -0.4% it sat behind the product with a pixel showing: on a pale
     sideboard that left the cans with no contact at all. Wider than the
     product and dropped below the contact line, both pools now reach the
     surface.

     Warm rather than black, and for the reason the mobile TOPRANK plate needs
     it: this is the one desktop surface that is both bright and strongly
     coloured, so a black pool reads as a grey smear where the wood bounces
     its own colour into the shade. The alphas carry the rest; measured at
     .6/1/.6 as well, which is heavier than a room this bright wants. */
@media (min-width: 761px) { .stage .s-toprank{ --la:.5; --pool:.9; --pool2:.5; --sh:44,26,8; } }
@media (min-width: 761px) { .stage .s-toprank .prod::before{
    width:118%; height:4.4%; bottom:-1.6%; filter:blur(4px);
  } }
@media (min-width: 761px) { .stage .s-toprank .prod::after{
    width:200%; height:9%; bottom:-3.6%; filter:blur(12px);
  } }
@media (min-width: 761px) { .stage .s-toprank .head::before{
    content:""; position:absolute; inset:-22% -18% -30%; z-index:-1; pointer-events:none;
    background:rgba(255,253,246,.30); border-radius:44% / 52%; filter:blur(30px);
  } }
@media (min-width: 761px) { .stage .band{ height:14%;
    background:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.28) 62%, rgba(0,0,0,.5) 100%); } }
/* One top line for all five slides, at 18.75%.
     ----------------------------------------------------------------------
     This block was vertically centred, and because the five slides carry
     different amounts of copy that meant five different starting heights:
     measured on a 1440 canvas, the headline began at y165 on TOPRANK, the glue
     traps and incense, y192 on VANISH and y195 on the corporate slide, and the
     corporate logo lockup opposite it began at y216. Nothing lined up with
     anything.

     Anchored to the top instead, so every slide begins on the same line and
     the block grows downward by however much copy it has. 18.75% is y135 of
     720, which is the highest of those tops less the 30px asked for. A
     percentage rather than 135px because the canvas is 2:1 and its height
     tracks the viewport width, so a pixel would drift off the line on a
     narrower desktop.

     17.36% now, having been raised the 10px asked for: 1.39 points of 720.
     Raised on every slide rather than on the three it was asked for, because
     the shared line is the point of this rule and moving three of five would
     have broken it. */
@media (min-width: 761px) { .stage .copy{
    position:absolute; z-index:5; left:6.6%; top:17.36%;
    width:44%; display:flex; flex-direction:column; align-items:flex-start; gap:.9rem;
  } }
@media (min-width: 761px) { .stage .brandmark{ height:5.2rem; width:auto; filter:drop-shadow(0 4px 14px rgba(0,0,0,.5)); } }
@media (min-width: 761px) { .stage .s-toprank .brandmark{ filter:drop-shadow(0 4px 12px rgba(0,0,0,.28)); } }
@media (min-width: 761px) { .stage .head{ font-size:clamp(2rem,4.1vw,3.9rem); max-width:13ch; } }
@media (min-width: 761px) { .stage .accentrule{ width:3.4rem; } }
@media (min-width: 761px) { .stage .sub{ font-size:clamp(.9rem,1.16vw,1.16rem); max-width:30ch; } }
@media (min-width: 761px) { .stage .chips{ gap:0; margin-top:.1rem; } }
@media (min-width: 761px) { .stage .chip{ padding:0 .95rem; } }
@media (min-width: 761px) { .stage .chip:first-child{ padding-left:0; } }
@media (min-width: 761px) { .stage .chip span{ font-size:clamp(.62rem,.8vw,.78rem); } }
@media (min-width: 761px) { .stage .marketed{ right:3.2%; top:7%; font-size:.72rem; } }
@media (min-width: 761px) { .stage .foot{ display:none; } }
/* mobile-only block */

  /* product stage: bases sit on --base, measured from the top of the canvas */
@media (min-width: 761px) { .stage .shot{
    position:absolute; z-index:4; right:4%; width:50%;
    top:0; height:var(--base); display:flex; align-items:flex-end; justify-content:center; gap:4px;
  } }
@media (min-width: 761px) { .stage .prod img.main{ height:var(--h); } }
@media (min-width: 761px) { .stage .prod.cup{ margin-left:-9%; } }


  .s-incense .prod.pack { z-index:3; }


  .s-incense .prod.cup { z-index:2; }
@media (min-width: 761px) { .stage .s-incense .shot{ right:0.5%; } }
@media (min-width: 761px) { .stage .prod img.mirror{ height:var(--h); } }
/* --base is where the product's feet land, measured from the top of the
     canvas, and it has to be the surface in the plate behind it. Re-measured
     for the round-two scenes: those rooms carry much less foreground than the
     empty ones did, so the surfaces sit at 75 to 76 per cent of the frame
     instead of 88 to 91. Left at the old figures the cans stood on the front
     of the sideboard rather than on top of it. */
  /* Per-slide resting offsets, in px, measured against the round-two scenes.
     The surfaces in these rooms recede to the right, so a product standing at
     the far left of one sits above it rather than on it: each of these moves
     the product along the surface until it meets it. */
@media (min-width: 761px) { .stage .s-toprank .shot{ --nx:50px; --ny:50px; } }
/* The room moves left while the cans move right, which is what opens a gap
     between the cans and the sleeping couple. 20px, inside the 28px of slack
     the plate's own scale provides. */
@media (min-width: 761px) { .stage .s-vanish .plate img{ --px:-20px; } }
@media (min-width: 761px) { .stage .s-vanish  .shot{ --nx:70px; --ny:30px; } }
@media (min-width: 761px) { .stage .s-glue    .shot{ --nx:20px; --ny:35px; } }
@media (min-width: 761px) { .stage .s-incense .shot{ --ny:65px; } }
@media (min-width: 761px) { .stage .s-corp    .shot{ --base:66%; } }
@media (min-width: 761px) { .stage .s-toprank .shot{ --base:75%; } }
@media (min-width: 761px) { .stage .s-vanish  .shot{ --base:75%; } }
@media (min-width: 761px) { .stage .s-glue    .shot{ --base:76%; } }
@media (min-width: 761px) { .stage .s-incense .shot{ --base:76%; } }
/* No translateY(-50%) here, and the same on the mobile rule further down.
     ----------------------------------------------------------------------
     .corpmark carries `animation: lift ... both`, and an animation with that
     fill mode owns the property it animates: the keyframes end at
     translate3d(--nx, --ny, 0), which discards any transform declared here.
     So the centring worked until the entry animation ran and then stopped
     working, and the block dropped by half its own height and stayed there.
     Measured: 11.5 to 48.6% of the canvas before, 30.1 to 67.1% after, on
     every visit. The lower placement is the one the slide was composed
     against, and the higher one is what a visitor with reduced motion or with
     scripting off was getting, with the lockup running up into the headline.
     Folded into `top` so both states agree. Same fault, same fix, as .shot.

     The figure was calc(37% - 50px), which put the lockup at y216 of 720, then
     18.75% to share the start line with the copy block opposite it. It sits
     20px above that line now and 15px further left, both asked for: 2.78
     points of a 720px canvas and 1.04 of a 1440px one. Percentages rather than
     pixels because the canvas is 2:1 and both dimensions track the viewport
     width. Moving left means growing `right`, since that is the edge this
     block is pinned to. */
@media (min-width: 761px) { .stage .corpmark{
    position:absolute; z-index:4; right:6.04%; top:15.97%;
    width:42%; display:flex; flex-direction:column; align-items:center; gap:1.4rem;
  } }
@media (min-width: 761px) { .stage .corpmark .primary{ width:100%; max-width:24rem; filter:drop-shadow(0 10px 30px rgba(0,0,0,.66)); } }
@media (min-width: 761px) { .stage .corpmark .rule{ width:78%; height:1px; background:linear-gradient(90deg,rgba(200,154,56,0),#C89A38,rgba(200,154,56,0)); } }
@media (min-width: 761px) { .stage .corpmark .marks{ display:flex; align-items:center; gap:2.4rem; } }
@media (min-width: 761px) { .stage .corpmark .marks img{ height:7rem; width:auto; filter:drop-shadow(0 6px 18px rgba(0,0,0,.6)); } }
/* =================================================================
     MOBILE LAYOUT  -  4:5, stacked, product centred and dominant
     ================================================================= */
@media (max-width: 760px) { .stage{ max-width:24rem; } }
@media (max-width: 760px) { .stage .canvas{ aspect-ratio:2/3; } }
@media (max-width: 760px) { .stage .scrim{
    background:linear-gradient(180deg, rgba(0,0,0,.74) 0%, rgba(0,0,0,.3) 26%, rgba(0,0,0,.12) 46%, rgba(0,0,0,.5) 72%, rgba(0,0,0,.82) 100%);
  } }
@media (max-width: 760px) { .stage .s-toprank .scrim{
    /* the generated plate runs untouched here: no wash, no filter */
    background:none;
  } }
/* which leaves the headline with nothing behind it, so it carries its own.
     ----------------------------------------------------------------------
     This is the one slide on the site whose text sits on a photograph with no
     scrim, and on a phone the headline lands on the busiest part of the room:
     a framed picture, a curtain in full sun and a palm. Measured off the
     rendered frame, the dark green came out at 1.22:1 against the picture
     frame and the red at 1.00:1 against the fronds, where large text needs
     3:1. Nothing in a stylesheet could have said so, which is why a11y.js
     skips text inside .slide and why this was read off pixels instead.

     A white wash rather than a black one, and behind the copy rather than
     over the frame. Black would have meant white text here and dark text on
     the desktop version of the same slide, and it would have put a dark patch
     in the brightest room in the set; white reads as sun through the window,
     keeps the copy dark the way the plate asks for, and leaves the family and
     the cans in daylight. Takes the two lines to 8.9:1 and 4.1:1, measured by
     scripts/plate-contrast.js.

     Delete this when the mobile re-frame lands, and check the numbers again on
     whatever arrives: the fix belongs to this plate, not to the slide. */
  /* The box is far taller than the glow on purpose. A radial gradient that
     still has alpha left when it meets the element's edge is cut square
     there, and at the first set of figures that put a straight white line
     across the room at 39% of the frame. The box now runs 120% of the copy's
     own height below it and the gradient reaches zero about 95px inside that,
     so the only edge left is the frame's own. */
  /* The chip row is white here rather than the dark green the desktop slide
     uses, and it carries no panel behind it. It had one: a blurred white
     panel that took the row from 1.30:1 to 5.9:1 against the dark wood grain.
     Both were asked to go. Scoped to mobile so the desktop slide keeps its
     dark chips on the same bright room. */
@media (max-width: 760px) { .stage .s-toprank .chip{ color:#fff; } }
@media (max-width: 760px) { .stage .s-toprank .copy::before{
    content:""; position:absolute; inset:-30% -22% -120%; z-index:-1; pointer-events:none;
    background:radial-gradient(64% 42% at 50% 37%,
      rgba(255,255,255,.30) 0%,
      rgba(255,255,255,.26) 46%,
      rgba(255,255,255,.10) 76%,
      rgba(255,255,255,0) 100%);
  } }
@media (max-width: 760px) { .stage .band{ display:none; } }
@media (max-width: 760px) { .stage .copy{
    position:absolute; z-index:5; left:6%; right:6%; top:3.6%;
    display:flex; flex-direction:column; align-items:center; gap:.72rem; text-align:center;
  } }
/* The corporate slide alone starts 15px lower. It is the one mobile slide
     with no brandmark above its headline, so at the shared 3.6% its first line
     sat higher than every other slide's did. 2.6 points of a 585px canvas is
     those 15px. */
@media (max-width: 760px) { .stage .s-corp .copy{ top:6.2%; } }
/* Both up about a tenth. The brandmark was 2.85rem and the headline topped
     out at 1.5rem, which on a 390px phone left the product doing all the work
     and the branding whispering underneath it. */
@media (max-width: 760px) { .stage .brandmark{ height:3.15rem; width:auto; filter:drop-shadow(0 3px 10px rgba(0,0,0,.5)); } }
@media (max-width: 760px) { .stage .head{ font-size:clamp(1.26rem,6.2vw,1.66rem); max-width:16ch; } }
@media (max-width: 760px) { .stage .accentrule{ width:2.6rem; } }
@media (max-width: 760px) { .stage .sub{ display:none; } }
@media (max-width: 760px) { .stage .marketed{ display:none; } }
/* the chips live in .foot on mobile, so the copy-block copy is hidden */
@media (max-width: 760px) { .stage .copy .chips{ display:none; } }
/* bottom:7%, not 4%. This block held the chips and the button stacked, so
     4% put the button on the bottom edge and the chips above it. With the
     button gone the chips fell to where the button was and landed on the dots,
     which sit at .8rem from the bottom. 7% clears them by 26px. */
@media (max-width: 760px) { .stage .foot{
    position:absolute; z-index:5; left:5%; right:5%; bottom:7%;
    display:flex; flex-direction:column; align-items:center; gap:.7rem;
  } }
@media (max-width: 760px) { .stage .chips{ gap:0; justify-content:center; } }
@media (max-width: 760px) { .stage .chip{ padding:0 .5rem; } }
@media (max-width: 760px) { .stage .chip span{ font-size:.53rem; letter-spacing:.02em; } }
@media (max-width: 760px) { .stage .chip svg{ display:none; } }



  /* Small phones, where the longest chip row stops fitting.
     ----------------------------------------------------------------------
     The TOPRANK row is the widest of the five at 310px, and the foot bar gets
     90% of the canvas, so it wraps to two lines below about 345px. The row
     then grows upward rather than downward and the clearance under the cans
     went from 26px to 4px: measured two rows at 320 and 340, one at 360.
     Only the type and the dividers' padding come down, and only here, which
     brings the row to 278px at 320px wide.

     A real media query rather than a [data-view] rule, because this one has
     to key on the viewport: port-carousel.js passes an @media block through
     untouched, so the selectors inside it are written for the site. The cost
     is that the harness's Mobile toggle cannot show it, the toggle being a
     button rather than a narrow window. Anything under 345px is inside the
     mobile breakpoint either way, so the plain .stage scope is safe. */
  @media (max-width: 344px) {
    .stage .chip { padding:0 .32rem; }
    .stage .chip span { font-size:.5rem; }
  }
/* the product gets its own band below the headline; starting at 0 put it
     underneath the copy and the two collided */
@media (max-width: 760px) { .stage .shot{
    position:absolute; z-index:4; left:0; right:0;
    top:var(--mtop,26%); height:calc(var(--base) - var(--mtop,26%));
    display:flex; align-items:flex-end; justify-content:center; gap:4px;
  } }
@media (max-width: 760px) { .stage .prod img.main{ height:var(--hm, var(--h)); } }
@media (max-width: 760px) { .stage .prod.cup{ margin-left:-10%; } }
/* The glue kitchen puts its cook to the left of centre and a centred pack
     stands directly in front of her: the whole point of that plate is the
     person in the room, so the pack steps right far enough to clear her and no
     further. 30px on a 24rem stage leaves her head, shoulder and wrapper
     visible with the pack still reading as the subject. Measured at 48px as
     well, which shows more of her but crowds the pack against the right edge.
     incense carried the same nudge for the same reason and lost it with the
     round-two veranda, the round-one one being empty with nobody to step
     around. It has 20px again now, asked for rather than measured: the lantern
     on that plate sits right of centre and the products read better set
     towards it. */
@media (max-width: 760px) { .stage .s-toprank .shot{ --nx:70px; } }
@media (max-width: 760px) { .stage .s-glue    .shot{ --nx:70px; } }
@media (max-width: 760px) { .stage .s-incense .shot{ --nx:20px; } }
/* VANISH goes the other way, and further. Its round-two mobile bedroom puts
     the two sleepers at 45 to 54% and 68 to 80% of the visible frame, and the
     centred pair of cans covers 35 to 65%: the man was behind them entirely
     and only the woman read. No nudge reveals both, because the gap between
     the two heads is 14% wide and the cans are 30%, so the pair moves off them
     instead. At -60 both heads are clear and the bedside lamp, which is the
     light the shadow direction depends on, is still visible to the left of the
     cans. Measured at -85 as well, which shows more of the man and crowds the
     lamp.

     That was then. The cans now go the other way and sit close to the right
     edge, which was asked for: +103px puts the outer edge of the pair 35px off
     the frame, inside the 30 to 40 wanted. Both sleepers are behind them
     again as a result. --ny drops the pair 10px without resizing it, which is
     what the variable is for; changing --base would have made them taller as
     well as lower. */
@media (max-width: 760px) { .stage .s-vanish  .shot{ --nx:103px; --ny:10px; } }
@media (max-width: 760px) { .stage .prod img.mirror{ height:var(--hm, var(--h)); } }
/* Pool geometry, mobile only, and the reason is scale rather than taste.
     ----------------------------------------------------------------------
     The two pools are sized as a percentage of the product, and the tight one
     was 82% of it: narrower than the thing standing on it, so it was hidden
     behind the product and only its 2px blur halo ever reached the surface.
     Nothing sat under any of the four products on a phone. It reads at 120%,
     which puts the dark core on the surface either side of the contact.

     Both pools also move down past the contact line. A radial gradient's
     alpha falls away fast across a 21px box, so the band that actually carries
     the colour is about ten pixels tall, and at the shared bottom of -1.4% it
     sat behind the product with a pixel showing: raising the alphas did
     nothing at all, which is how the geometry came to be looked at. They are
     taller here too, for the same reason.

     Both blur radii come down as well. Those are absolute pixels while the
     product here is roughly 40% narrower than on desktop: a 10px blur under a
     60px can is a haze rather than a shadow. Same reason the cast shadow's
     --lb and --ly drop on all four slides below. */
@media (max-width: 760px) { .stage .prod::before{ width:120%; height:5%;  bottom:-1.8%; filter:blur(3px); } }
@media (max-width: 760px) { .stage .prod::after{ width:205%; height:10%; bottom:-4.2%; filter:blur(8px); } }
/* Light and material on a phone.
     ----------------------------------------------------------------------
     Three of the four now carry the same room in both views, so they keep the
     desktop light direction and take only the tighter, closer shadow a
     smaller product needs: --ly and --lb are absolute pixels and the product
     here is about 40% narrower.

     VANISH no longer overrides --lx. Its round-two mobile plate puts the
     bedside lamp on the left, which is where the desktop one has it, so the
     shared +9 throws the shadow away from the lamp in both views. The
     override that used to sit here was -4, for a round-one plate that was a
     studio lit from above, and on this room it would have thrown the shadow
     back into the lamp.

     TOPRANK is still the round-one plate and still the exception. Its window
     is hard right, measured 187 against 132, so --lx flips negative; and it
     is the one surface on the site that takes a tinted shadow, a sunlit
     honey-coloured table where a black pool read as a grey smear because the
     wood bounces its own colour into the shade. It needs the most alpha of the
     four as well, being the brightest plate and the only one with no scrim.

     This line survived the mobile re-frame and must keep surviving. It used to
     say to delete it once that landed, on the assumption the new plate would
     match the desktop one and be lit from the left. It is not: the new living
     room has its window on the right, the same side as the plate it replaced,
     so --lx stays negative here and the shadow keeps falling away from it.

     Reflections on VANISH and incense, and nowhere else. Both of those
     surfaces are polished wood with a lamp showing in them, and on a
     near-black surface a black pool does nothing however far it is turned up,
     so the reflection is what makes the product stand. Both fade shorter than
     the shared 38% so they die above the chips row rather than showing through
     it. */
@media (max-width: 760px) { .stage .s-toprank{ --lx:-12px; --ly:7px; --lb:13px; --la:.54; --pool:.86; --pool2:.44; --refl:0; --sh:44,26,8; } }
@media (max-width: 760px) { .stage .s-vanish{ --ly:7px;  --lb:13px; --refl:.18; } }
@media (max-width: 760px) { .stage .s-glue{ --ly:8px;  --lb:13px; } }
@media (max-width: 760px) { .stage .s-incense{ --ly:7px;  --lb:12px; --pool:.80; --pool2:.44; --refl:.18; } }
@media (max-width: 760px) { .stage .s-vanish  .prod img.mirror{
    -webkit-mask-image:linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 22%);
    mask-image:linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 22%);
  } }
/* 14% rather than VANISH's 22%, because the sachet is nearly the full height
     of the shot and a 22% tail on it reached the chips. Measured at --refl .26
     as well, which pushed the sachet's green into the wood harder than the
     scene's own reflections do. */
@media (max-width: 760px) { .stage .s-incense .prod img.mirror{
    -webkit-mask-image:linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 14%);
    mask-image:linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 14%);
  } }
/* All four product slides stand at 85%, and the figure comes from the foot
     bar rather than from the plates. It was 79% until the button came out: the
     chips row began at 84.4% of the canvas and a base any lower put the
     product's feet behind it. Measured then, 26px of visible surface under
     TOPRANK, 14px under VANISH and incense, and minus 9px under the glue trap,
     whose feet were simply behind the chips.

     Losing the button moved that boundary. The foot bar is a single 10px row
     of chips now rather than a row plus a 48px button, so the chips begin at
     91.3% and six points of canvas came free. They go to the product, which is
     what was asked for and also the only thing on the slide that wants to be
     bigger: the four products gain 35 to 40px of height and still clear the
     chips by more than 30px.

     incense was the exception at 76% while it carried the round-two veranda,
     whose table ran from 65.5 to 81% of the re-cropped frame. Back on the
     round-one one it joins the other three: that table's far edge is level all
     the way across and the crop puts it at 75%, so 79% stands the products 4%
     forward of it with table both behind and in front. */
@media (max-width: 760px) { .stage .s-corp    .shot{ --mtop:27%; --base:72%; } }
@media (max-width: 760px) { .stage .s-toprank .shot{ --mtop:30%; --base:85%; } }
@media (max-width: 760px) { .stage .s-vanish  .shot{ --mtop:26%; --base:85%; } }
@media (max-width: 760px) { .stage .s-glue    .shot{ --mtop:28%; --base:85%; } }
@media (max-width: 760px) { .stage .s-incense .shot{ --mtop:23%; --base:85%; } }
/* top:39.7% and no transform: see the note on the desktop rule. Measured
     23.9 to 56.1% before the entry animation and 40 to 72.2% after it, then
     moved down four points with the rest of the mobile layout when the button
     came out of the foot bar, then up by the 25px asked for, and up another
     10px now: 4.3 and 1.7 points of a 585px canvas. Percentages rather than
     pixels because the canvas is 2:3 and its height tracks the viewport
     width. */
@media (max-width: 760px) { .stage .corpmark{
    position:absolute; z-index:4; left:9%; right:9%; top:38%;
    display:flex; flex-direction:column; align-items:center; gap:1rem;
  } }
@media (max-width: 760px) { .stage .corpmark .primary{ width:100%; filter:drop-shadow(0 8px 24px rgba(0,0,0,.7)); } }
@media (max-width: 760px) { .stage .corpmark .rule{ width:70%; height:1px; background:linear-gradient(90deg,rgba(200,154,56,0),#C89A38,rgba(200,154,56,0)); } }
@media (max-width: 760px) { .stage .corpmark .marks{ display:flex; align-items:center; gap:1.5rem; } }
@media (max-width: 760px) { .stage .corpmark .marks img{ height:4.2rem; width:auto; filter:drop-shadow(0 5px 14px rgba(0,0,0,.65)); } }



  /* =================================================================
     CONTROLS
     ================================================================= */
  /* Desktop: bare chevrons, revealed on hover or keyboard focus.
     Mobile: the same two buttons become invisible tap zones over the image,
     stopping short of the footer so the chips and button stay tappable. */
  .ctrl {
    position:absolute; z-index:8; border:0; padding:0; cursor:pointer;
    background:transparent; display:grid; place-items:center;
    font-family:"Archivo",sans-serif; font-weight:500; line-height:1;
  }
@media (min-width: 761px) { .stage .ctrl{
    top:50%; transform:translateY(-50%); width:3rem; height:3rem;
    color:#fff; font-size:2.3rem; text-shadow:0 2px 14px rgba(0,0,0,.75);
    opacity:0; transition:opacity .24s ease, transform .24s ease;
  } }
@media (min-width: 761px) { .stage .s-toprank .ctrl{ color:#12300F; text-shadow:0 2px 14px rgba(255,255,255,.8); } }
@media (min-width: 761px) { .stage .ctrl.prev{ left:1rem; } }
@media (min-width: 761px) { .stage .ctrl.next{ right:1rem; } }
@media (min-width: 761px) { .stage .carousel:hover .ctrl{ opacity:.9; } }
@media (min-width: 761px) { .stage .ctrl:hover{ opacity:1; transform:translateY(-50%) scale(1.14); } }
@media (min-width: 761px) { .stage .ctrl:focus-visible{ opacity:1; outline:2px solid var(--gold-lite); outline-offset:4px; } }
/* 20% a side, not 42%.
     ----------------------------------------------------------------------
     These are invisible tap zones, and at 42% each they owned 84% of the
     width. That was fine while the only link on the slide was a button in the
     foot bar; now that the slide itself is the link it left a 16% strip down
     the middle to tap, which is not a target. At 20% the centre column is 60%
     wide and the zones are still a comfortable 78px on a 390px phone.

     They also run further down than they did, because the button they used to
     stop short of is gone. The dots keep their own region below. */
@media (max-width: 760px) { .stage .ctrl{
    top:0; bottom:12%; width:20%; height:auto; transform:none;
    color:transparent; font-size:0; text-shadow:none; opacity:1;
    -webkit-tap-highlight-color:transparent;
  } }
@media (max-width: 760px) { .stage .ctrl.prev{ left:0; } }
@media (max-width: 760px) { .stage .ctrl.next{ right:0; } }
@media (max-width: 760px) { .stage .ctrl:focus-visible{ outline:2px solid var(--gold-lite); outline-offset:-4px; } }



  /* thin bars rather than a floating pill */
  .dots {
    position:absolute; left:50%; transform:translateX(-50%); bottom:1.05rem; z-index:8;
    display:flex; align-items:center; gap:.4rem;
  }


  .dots button {
    width:1.5rem; height:2px; padding:0; border:0; border-radius:2px; cursor:pointer;
    background:rgba(255,255,255,.45); box-shadow:0 1px 4px rgba(0,0,0,.55);
    transition:width .3s ease, background .3s ease;
  }


  .dots button[aria-current="true"] { width:2.6rem; background:#fff; }


  .dots button:focus-visible { outline:2px solid #fff; outline-offset:5px; }
@media (max-width: 760px) { .stage .dots{ bottom:.8rem; gap:.32rem; } }
@media (max-width: 760px) { .stage .dots button{ width:1.1rem; } }
@media (max-width: 760px) { .stage .dots button[aria-current="true"]{ width:2rem; } }


  .sr { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; }



  /* =================================================================
     MOTION
     ================================================================= */
  @keyframes rise { from { opacity:0; transform:translate3d(0,18px,0); } to { opacity:1; transform:none; } }


  /* --nx and --ny are the per-slide resting offsets, and they are read here as
     well as on .shot itself: the from frame has to start from the resting
     position, or the product slides in from the wrong place. They are no
     longer here because the end frame is the last word on the transform. It
     was, while these animations filled both ways; they fill backwards now and
     .shot's own rule governs once the animation has run. See the note on the
     declarations below. */
  @keyframes lift { from { opacity:0; transform:translate3d(var(--nx,0px),calc(var(--ny,0px) + 30px),0) scale(.96); } to { opacity:1; transform:translate3d(var(--nx,0px),var(--ny,0px),0); } }


  /* --px pans the plate horizontally. It lives in these keyframes for the same
     reason --nx and --ny live in `lift`: this animation runs on .plate img and
     would overwrite any transform set elsewhere.
     The pan is free because the plate is already scaled. At 1.04 on a 1440
     wide canvas the image is 1497px, so there is 28px of slack either side to
     move into without exposing an edge. */
  @keyframes drift {
    0%,100% { transform:scale(1.04) translate3d(var(--px,0px),0,0); }
    50%     { transform:scale(1.075) translate3d(calc(var(--px,0px) - 1%),-.6%,0); }
  }



  /* backwards, not both, on every one of these.
     ----------------------------------------------------------------------
     `both` keeps the end frame in force for as long as the rule applies, and
     the active slide keeps it forever, so the end frame became the last word
     on transform for the whole slide. It was found on the CTA buttons, whose
     own :hover lift and :active press were being discarded and measured as the
     identity matrix at rest and on hover alike. Those buttons have since gone,
     but the fault has not: the headline's hover lift is a transform on an
     animated element and would be discarded the same way.

     `backwards` holds the from frame through the delay, which is the whole
     reason the fill mode is here (without it every element flashes at full
     opacity before its turn), runs the animation, and then hands the property
     back to the stylesheet. Each of these animations ends where the element's
     own rules already put it, which is what makes the swap safe: `rise` ends
     at opacity 1 and no transform, and `lift` ends at the same translate that
     .shot declares statically. Checked both at rest afterwards. */
  .plate img { animation:drift 32s ease-in-out infinite; }


  .carousel.animate .slide[data-active="true"] .brandmark  { animation:rise .5s .04s backwards cubic-bezier(.16,1,.3,1); }


  .carousel.animate .slide[data-active="true"] .head       { animation:rise .6s .1s backwards cubic-bezier(.16,1,.3,1); }


  .carousel.animate .slide[data-active="true"] .accentrule { animation:rise .5s .18s backwards cubic-bezier(.16,1,.3,1); }


  .carousel.animate .slide[data-active="true"] .sub        { animation:rise .6s .22s backwards cubic-bezier(.16,1,.3,1); }


  .carousel.animate .slide[data-active="true"] .foot,
  .carousel.animate .slide[data-active="true"] .chips      { animation:rise .6s .28s backwards cubic-bezier(.16,1,.3,1); }


  .carousel.animate .slide[data-active="true"] .shot,
  .carousel.animate .slide[data-active="true"] .corpmark   { animation:lift .8s .06s backwards cubic-bezier(.16,1,.3,1); }



  @media (prefers-reduced-motion: reduce) {
    .track { transition:none; }
    .plate img { animation:none; }
    .carousel.animate .slide[data-active="true"] .brandmark,
    .carousel.animate .slide[data-active="true"] .head,
    .carousel.animate .slide[data-active="true"] .accentrule,
    .carousel.animate .slide[data-active="true"] .sub,
    .carousel.animate .slide[data-active="true"] .foot,
    .carousel.animate .slide[data-active="true"] .chips,
    .carousel.animate .slide[data-active="true"] .shot,
    .carousel.animate .slide[data-active="true"] .corpmark { animation:none !important; }
  }



  /* ---------- notes ---------- */
  .notes { margin-top:2.5rem; }


  /* >>> plate placeholders, generated by scripts/plate-placeholders.js */
  /* A slide whose plate has not arrived yet shows a blurred stand-in
     rather than the carousel's own near-black, which is what made the
     product look like it had a dark wash over it on every refresh. */
  .plate { background-size:cover; background-position:center; background-repeat:no-repeat; }


  .s-corp .plate { background-image:url("data:image/webp;base64,UklGRloAAABXRUJQVlA4IE4AAADQAwCdASoYAAwAPu1Ct1apoqakGAEwHYlAE6ZQAHFx3XOlk9MQoAAA/uwby43O4SeyfBSybBvW2Dz8DSgpR/FDL8yB67YpzertOp/fwAA="); }


  @media (max-width:760px) { .s-corp .plate { background-image:url("data:image/webp;base64,UklGRowAAABXRUJQVlA4IIAAAAAQBQCdASoYAB4APu1orVAppaQiqAqpMB2JQBOmWn6Ng0kcP332OhQEUvDD27htjNJAAP7r/Iydfr880Uv1rhXKRc8D7Tmm1fIG+u8O+FJSJyzHMrwbZAWsaZXy3gD8+T7R4ghaiqeRtBa8ijBjH8as+/fXR+6sSDK4yxFec8AAAA=="); } }


  .s-toprank .plate { background-image:url("data:image/webp;base64,UklGRmAAAABXRUJQVlA4IFQAAABQBACdASoYAAwAPu1kqk4ppaQiMAgBMB2JQBOmUI7gBKLlpUrhJZm4PHgAAP7n1ik3LobJHqy9yjzP1FOSGpy+4J/kkcjdet5nPykqq+6eNMNgAAA="); }


  @media (max-width:760px) { .s-toprank .plate { background-image:url("data:image/webp;base64,UklGRpgAAABXRUJQVlA4IIwAAABwBQCdASoYAB4APu1wsFKppiSiqAgBMB2JYgCdMoR8L3+BXhJYTyGAB18inwcikKE7xiAQAP6BDFTCQzXeYGGaVAkwOWJlMmo3TbjOTQ9nCy3Vs30nu5saqEgrerqxRtFxq8emhpirCMjqkhw/pCjIT89+59D6iGz0tHtD/COnU4Jwo8BYrppUP2awAA=="); } }


  .s-vanish .plate { background-image:url("data:image/webp;base64,UklGRlwAAABXRUJQVlA4IFAAAADwAwCdASoYAAwAPu1qrU8ppiQiMAgBMB2JQBWABDvkV1U2/Nf6x1uAAP7w0HHDMkFtmif+NmW7CE0M52Fk31fY//JyadyPOQyGPiJtPPcAAA=="); }


  @media (max-width:760px) { .s-vanish .plate { background-image:url("data:image/webp;base64,UklGRo4AAABXRUJQVlA4IIIAAADwBQCdASoYAB4APu1eqk6ppKOiMBgMATAdiWIAnTKEgr/aiYWccuvsIxMVZh+WpwCj7H36GxzPqAD++E16uaIVUPoKlL02wuNfslACBl0Jn7Tl6DTjE8jbqDL2+XjCglXf3UzZ3ZRsix9TqqsSH8e+R1xma0RuA5MQ33Nw/2u1gAAA"); } }


  .s-glue .plate { background-image:url("data:image/webp;base64,UklGRl4AAABXRUJQVlA4IFIAAADQAwCdASoYAAwAPu1mqk2ppaQiMAgBMB2JYwCdACG7wlV5+xj83ZAA/r/utrnCAJs9cauWJpsIQHByYn8GA+2ppVXOS5ahkDT7ljQrT59cAAAA"); }


  @media (max-width:760px) { .s-glue .plate { background-image:url("data:image/webp;base64,UklGRoIAAABXRUJQVlA4IHYAAACwBACdASoYAB4APtFapE2oJaOiN+gBABoJYwCdACQfIT26hjt5TEL7A1uIiVF8AP4Q3m14Nd1CfjuPyDEx2yCyNWGSewHdR8VaeTeXgKTAROGhMMfz1e842CanRVGrPY25tT0t+ufFE8K1lRq+qOFr2bx0QAAA"); } }


  .s-incense .plate { background-image:url("data:image/webp;base64,UklGRlwAAABXRUJQVlA4IFAAAACwAwCdASoYAAwAPu1mqk2ppaQiMAgBMB2JQBadDMHKElflOv2FAAD+/kse4T/s6Ed0w0NrLUk+rrr2uLYJ0jbBy9a7cX/M3Aa0befMCAAAAA=="); }


  @media (max-width:760px) { .s-incense .plate { background-image:url("data:image/webp;base64,UklGRowAAABXRUJQVlA4IIAAAACQBQCdASoYAB4APu1kqU2ppaQiMBgMATAdiWQAnTMOhSH8zSywIKkBVI1q7oSNdVmM+2zegAD+70J4C5nMrVq7IYpQ9MkQkZXOKAZx8MKGZFt3ZOfEsjVat/pX9ZkYJnWgBTSIsHPp+h1R7iXSR/5JWgJIYkYwvfGEHEKFJgIAAA=="); } }
/* <<< plate placeholders */

/* --- site integration ----------------------------------------------------
   The review harness constrained the stage so both compositions could sit on
   one page. On the site the hero runs to the container width instead.
   ------------------------------------------------------------------------ */
/* The harness capped the stage per view. Those caps now sit inside media
   queries at the same specificity, so a plain .stage rule after them wins. */
.stage { max-width: none; }
.hero .carousel { box-shadow: none; }

/* --- product.css --- */
/* ==========================================================================
   PRODUCT PAGES
   ========================================================================== */
.pageheader { max-width: 46rem; margin-bottom: var(--s-6); }
/* The label, heading and standfirst spacing now comes from the sibling rules
   in base.css, which cover h2 as well. The contact page heads its form with
   an h2 and was missing the gap here because these rules named only h1. */
.pdp__h { font-size: clamp(var(--t-21), 3vw, var(--t-32)); margin-bottom: var(--s-4); }
.pdp__note { margin-top: var(--s-4); font-size: var(--t-14); color: var(--fg-subtle); }
.pdp__source { margin-top: var(--s-5); font-size: var(--t-12); color: var(--fg-subtle); }

/* --- hero ---------------------------------------------------------------- */
.pdp-hero { background: linear-gradient(180deg, var(--bg-sunken), var(--bg)); padding-block: var(--s-6) var(--s-7); }
.pdp-hero__inner { display: grid; gap: var(--s-6); }
.pdp-hero__mark { height: 56px; width: auto; margin-bottom: var(--s-4); }
.pdp-hero__title { font-size: clamp(var(--t-32), 6vw, var(--t-56)); }
.pdp-hero__sub {
  font-family: var(--f-display); font-weight: 700; font-size: var(--t-14);
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--accent-text);
  margin-top: var(--s-2);
}
.pdp-hero__tagline { margin-top: var(--s-4); }
.pdp-hero__chips { margin-top: var(--s-5); }
.pdp-hero__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-5); }

.sizes {
  display: inline-flex; gap: var(--s-1); margin-top: var(--s-5); padding: var(--s-1);
  background: var(--bg-sunken); border-radius: var(--r-btn);
}
.sizes__btn {
  min-height: 40px; padding: 0 var(--s-4); border-radius: var(--r-btn);
  font-family: var(--f-display); font-weight: 700; font-size: var(--t-14);
  color: var(--fg-muted);
  transition: background var(--d-quick) var(--e-standard), color var(--d-quick) var(--e-standard);
}
.sizes__btn.is-active { background: var(--accent); color: var(--accent-fg); }
.sizes--single { margin-top: var(--s-5); color: var(--fg-subtle); font-size: var(--t-14); }

.pdp-hero__stage { display: grid; place-items: center; }
.pdp__shots {
  /* Raised from clamp(280px, 46vw, 460px). The 600 ml is the hero of this page
     and it was sitting smaller than the copy beside it.
     The height is held in a custom property so the cans can be sized from it
     directly. A percentage max-height on the image looked equivalent and was
     not: 80% of this box resolved to 404px rather than 416px, because a
     percentage needs a definite basis and a grid row sized by its own content
     is not one. That mistake has now cost this project five separate bugs. */
  --shot-h: clamp(320px, 52vw, 520px);
  position: relative; width: 100%; height: var(--shot-h);
  /* see the per-format override below */
  display: grid; place-items: center;
  /* the row must be pinned to the container height. Left to auto it is sized
     by the image, so the image max-height:100% resolves against itself. */
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
}
.pdp__shot {
  /* --scale is the can's real height against the largest in the range, so the
     300 ml renders shorter than the 600 ml by the amount it actually is.
     Anchored to the bottom of the cell, because two cans of different heights
     stand on the same surface rather than floating about a shared centre. */
  grid-area: 1 / 1; align-self: end;
  height: calc(var(--shot-h) * var(--scale, 1));
  width: auto; max-width: 100%;
  opacity: 0; transform: scale(.97);
  /* Scale from the baseline, not the centre. These cans stand on a surface, so
     the resting state should grow up off it rather than hover above it. */
  transform-origin: bottom center;
  filter: drop-shadow(0 18px 30px rgba(18,22,31,.28));
  transition: opacity var(--d-base) var(--e-standard), transform var(--d-base) var(--e-standard);
}
.pdp__shot.is-active { opacity: 1; transform: none; }

/* A tall thin pack needs a taller box to carry the same visual weight.
   ------------------------------------------------------------------------
   Every product shares one box height, and since the masters are all
   normalised to 1200px the box height is what decides the rendered width. On
   a phone that gave the glue trap 219px of width and the incense pack 54px,
   because the incense pack is a stick: 0.168 as wide as it is tall. At 54px
   nothing printed on it was legible.
   The override is on the format rather than the product id, because it is a
   property of the shape of the pack rather than of that one item. There is
   room for it: the box is 358px wide on a 390px screen and the pack uses a
   fifth of it. */
[data-format="incense"] .pdp__shots { --shot-h: clamp(460px, 120vw, 520px); }

@media (min-width: 900px) {
  .pdp-hero { padding-block: var(--s-7) var(--s-9); }
  .pdp-hero__inner { grid-template-columns: 1fr 1fr; align-items: center; gap: var(--s-8); }
}

/* --- what it is for ------------------------------------------------------- */
.pdp-pests__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }
.pdp-pests__item {
  display: flex; align-items: center; gap: var(--s-3);
  min-height: 64px; padding: var(--s-2) var(--s-4);
  background: var(--bg-raised); border: 1px solid var(--border-soft); border-radius: var(--r-card);
  font-family: var(--f-display); font-weight: 700;
  transition: transform var(--d-base) var(--e-standard), border-color var(--d-base) var(--e-standard);
}
/* The cut-out sits in front of the name, so a reader matching what is in
   their kitchen to a word on a page has the picture beside the word rather
   than a page away from it. justify-content: space-between went with it: with
   three children it spread the picture, the name and the arrow evenly and
   pulled the picture away from the name it belongs to. The name takes the
   slack instead and the arrow is pushed to the end. */
.pdp-pests__mark {
  flex: none; width: 44px; height: 44px;
  display: grid; place-items: center;
  color: var(--accent-text);
}
.pdp-pests__mark img { width: 100%; height: 100%; }
.pdp-pests__mark svg { width: 28px; height: 28px; }
.pdp-pests__label { flex: 1 1 auto; min-width: 0; }
.pdp-pests__go { flex: none; margin-left: auto; }
.pdp-pests__go svg { width: 18px; height: 18px; color: var(--accent-text); transition: transform var(--d-base) var(--e-standard); }
@media (min-width: 640px)  { .pdp-pests__list { grid-template-columns: repeat(2, 1fr); gap: var(--s-3); } }
@media (min-width: 1024px) { .pdp-pests__list { grid-template-columns: repeat(3, 1fr); } }

/* --- usage ---------------------------------------------------------------- */
.pdp-usage__grid { display: grid; gap: var(--s-5); }
.pdp-usage__block h3 {
  font-size: var(--t-14); letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--accent-text); margin-bottom: var(--s-2);
}
.pdp-usage__block p, .pdp-usage__block li { color: var(--fg-muted); }
.pdp__steps { margin: 0; padding-left: var(--s-5); display: grid; gap: var(--s-3); }
.pdp__claims { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s-2); }
.pdp__claims li {
  padding: var(--s-2) var(--s-4); background: var(--bg-sunken);
  border-radius: var(--r-pill); font-family: var(--f-display);
  font-weight: 700; font-size: var(--t-12); text-transform: uppercase; letter-spacing: .04em;
}
@media (min-width: 760px) { .pdp-usage__grid { grid-template-columns: repeat(2, 1fr); gap: var(--s-6); } }

/* --- safety --------------------------------------------------------------- */
.pdp-safety { background: var(--bg); color: var(--fg); }
.pdp-safety__grid { display: grid; gap: var(--s-5); }
.pdp-safety__item h3 {
  font-size: var(--t-14); letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--c-gold-lite); margin-bottom: var(--s-2);
}
.pdp-safety__item p { color: var(--fg-muted); font-size: var(--t-14); }
.pdp-safety__more { margin-top: var(--s-6); }
.pdp-safety .btn--ghost { color: var(--fg); box-shadow: inset 0 0 0 1px var(--border); }
@media (min-width: 760px) { .pdp-safety__grid { grid-template-columns: repeat(2, 1fr); gap: var(--s-6); } }

/* --- specs and attribution ------------------------------------------------- */
.pdp-specs__inner { display: grid; gap: var(--s-6); }
.spec { margin: 0; border-top: 1px solid var(--border-soft); }
.spec__row {
  display: grid; grid-template-columns: 1fr auto; gap: var(--s-4);
  padding: var(--s-3) 0; border-bottom: 1px solid var(--border-soft);
}
.spec__row dt { color: var(--fg-muted); font-size: var(--t-14); }
.spec__row dd { margin: 0; font-weight: 600; text-align: right; }

.pdp-attrib { padding: var(--s-5); background: var(--bg-sunken); border-radius: var(--r-card); }
.pdp-attrib h3 { font-size: var(--t-14); letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--accent-text); margin-bottom: var(--s-3); }
.pdp-attrib__line { font-size: var(--t-14); margin-bottom: var(--s-2); }
.pdp-attrib__note { font-size: var(--t-12); color: var(--fg-subtle); margin-top: var(--s-2); }
.pdp-attrib__std { margin-top: var(--s-5); padding-top: var(--s-4); border-top: 1px solid var(--border); }
.pdp-attrib__std h4 { font-size: var(--t-12); letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--fg-subtle); margin-bottom: var(--s-2); }
@media (min-width: 900px) { .pdp-specs__inner { grid-template-columns: 1.3fr 1fr; gap: var(--s-8); } }

/* --- pack zoom -------------------------------------------------------------
   The dialog behind "View the pack". Its job on a two-size product is the
   comparison, so the packs are side by side at every width rather than
   stacked on a phone and only paired on a desktop, and they stand on one
   baseline rather than floating about a shared centre.

   The height is a custom property for the same reason it is on .pdp__shots:
   the cans are sized from it directly. A max-height on the image cannot work
   here at all, because the masters are all normalised to 1200px tall, so the
   only thing that makes a 300 ml can shorter than a 600 ml one is --scale.

   An aerosol photograph is about 0.22 as wide as it is tall, so two of them
   at 50vh come to roughly 175px across on a 390px phone. Side by side fits
   with room to spare, which is why the mobile rule does not need to stack.
   ------------------------------------------------------------------------ */
.zoom {
  --zoom-h: 50vh;
  display: grid; grid-auto-flow: column;
  justify-content: center; align-items: end;
  gap: var(--s-4); padding: var(--s-4);
}
.zoom__item {
  margin: 0; text-align: center;
  display: grid; align-content: end; gap: var(--s-3);
  min-width: 0;
}
.zoom__item img {
  height: calc(var(--zoom-h) * var(--scale, 1));
  width: auto; max-width: 100%;
  margin-inline: auto;
  filter: drop-shadow(0 14px 26px rgba(0, 0, 0, .45));
}
.zoom__item figcaption { color: #fff; font-size: var(--t-12); }
/* A wide pack, a glue trap or an incense carton, has no partner to compare
   against and would be pinched by a rule written for a tall thin can. */
.zoom:not([data-compare]) .zoom__item img { height: auto; max-height: var(--zoom-h); }
.zoom__note {
  margin-top: var(--s-3); text-align: center;
  font-size: var(--t-12); color: rgba(255, 255, 255, .72);
}

@media (min-width: 760px) {
  .zoom { --zoom-h: 62vh; gap: var(--s-7); padding: var(--s-5); }
}

/* --- hover, pointer devices only -----------------------------------------
   Behind a hover query so a touch screen never matches it. Without the
   guard the first tap on anything styled here only applied the hover and
   a second tap was needed to follow the link, which is the double-tap
   this site was reported for. Selector lists were split where they mixed
   :hover with :focus-within or :focus-visible, so keyboard feedback stays
   outside the guard and still works everywhere.
   ------------------------------------------------------------------------ */

  .sizes__btn:hover {color: var(--fg); }
  .pdp-pests__item:hover {transform: translateX(4px); border-color: var(--accent-text); }
  .pdp-pests__item:hover .pdp-pests__go svg {transform: translateX(4px); }


/* --- brand.css --- */
/* ==========================================================================
   BRAND HUBS
   ========================================================================== */
.brandhero {
  background:
    radial-gradient(120% 100% at 78% 0%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 62%),
    linear-gradient(180deg, var(--bg-sunken), var(--bg));
  padding-block: var(--s-6) var(--s-7);
}
/* The measure is capped on the children, not on the container. As a narrower
   max-width on .container it inherited margin-inline: auto, so the whole
   column was centred and the brand page h1 began at x=304 while every other
   h1 on the site began at 112. Same fault as the about page and the legal
   pages: three different classes, one cause, and not a text-align anywhere. */
.brandhero__inner > * { max-width: 56rem; }
.brandhero__mark { height: 78px; width: auto; margin-bottom: var(--s-5); }
.brandhero__title { font-size: clamp(var(--t-26), 5vw, var(--t-56)); }
.brandhero__intro { margin-top: var(--s-4); }
.brandhero__meta {
  margin-top: var(--s-5); font-size: var(--t-12);
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--accent-text);
}
@media (min-width: 760px) {
  .brandhero { padding-block: var(--s-7) var(--s-9); }
  .brandhero__mark { height: 104px; }
}

/* --- brand story ---------------------------------------------------------
   Dark band, brand accent on the headings. Three equal columns at desktop so
   no single point of the story outweighs the others.
   ------------------------------------------------------------------------ */
.brandstory { background: var(--accent-deep, var(--c-ink)); color: var(--fg); }
.brandstory__grid { display: grid; gap: var(--s-6); }
.brandstory__item h3 {
  font-size: var(--t-18); margin-bottom: var(--s-3);
  color: var(--accent-2, var(--c-gold-lite));
}
.brandstory__item p { color: rgba(255, 255, 255, .78); font-size: var(--t-14); }
@media (min-width: 900px) { .brandstory__grid { grid-template-columns: repeat(3, 1fr); gap: var(--s-7); } }

/* --- name disambiguation --------------------------------------------------
   Both brand names collide with unrelated products sold elsewhere. Saying so
   plainly, in the page rather than only in the structured data, is the honest
   version and the one a person searching actually reads.
   ------------------------------------------------------------------------ */
.section--tight { padding-block: var(--s-6); }
.disambig__h {
  font-size: var(--t-12); letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--fg-subtle); margin-bottom: var(--s-3);
}
.disambig {
  font-size: var(--t-14); color: var(--fg-subtle); line-height: 1.6;
  padding-left: var(--s-4); border-left: 2px solid var(--border);
}

/* --- pest.css --- */
/* ==========================================================================
   PEST LIBRARY
   ========================================================================== */

/* --- hero ---------------------------------------------------------------- */
.pesthero { background: linear-gradient(180deg, var(--bg-sunken), var(--bg)); padding-block: var(--s-6) var(--s-7); }
.pesthero__inner { display: grid; gap: var(--s-5); align-items: center; }
.pesthero__icon { display: block; width: 56px; height: 56px; color: var(--accent-text); margin-bottom: var(--s-4); }
.pesthero__icon svg { width: 100%; height: 100%; }
.pesthero h1 { font-size: clamp(var(--t-32), 6vw, var(--t-56)); }
.pesthero .lede { margin-top: var(--s-3); }
@media (min-width: 900px) {
  .pesthero { padding-block: var(--s-7) var(--s-9); }
  .pesthero__inner { grid-template-columns: 1.1fr .9fr; gap: var(--s-8); }
  .pesthero__icon { width: 68px; height: 68px; }
}

/* --- identify and behaviour ----------------------------------------------- */
.pest-id { display: grid; gap: var(--s-6); }
.pest-id__marks { margin-top: var(--s-4); color: var(--fg-muted); }
.pest-behaviour h3 {
  font-size: var(--t-14); letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--accent-text); margin-bottom: var(--s-3);
}
.pest-behaviour p { color: var(--fg-muted); }
@media (min-width: 900px) { .pest-id { grid-template-columns: 1fr 1fr; gap: var(--s-8); } }

/* --- breeding and risk ---------------------------------------------------- */
.pest-breed { background: var(--bg); color: var(--fg); }
.pest-breed__inner { display: grid; gap: var(--s-6); }
.pest-breed__block p { color: var(--fg-muted); }
.pest-breed__note {
  margin-top: var(--s-3); padding-left: var(--s-3);
  border-left: 2px solid var(--c-gold-lite);
  font-size: var(--t-12); color: var(--fg-subtle);
}
@media (min-width: 900px) { .pest-breed__inner { grid-template-columns: 1fr 1fr; gap: var(--s-8); } }

/* --- prevention ----------------------------------------------------------
   Numbered because the list is genuinely ordered by what to do first, not
   for decoration.
   ------------------------------------------------------------------------ */
.prevention { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); counter-reset: none; }
.prevention__item {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s-4); align-items: start;
  padding: var(--s-4); background: var(--bg-raised);
  border: 1px solid var(--border-soft); border-radius: var(--r-card);
  transition: border-color var(--d-base) var(--e-standard), transform var(--d-base) var(--e-standard);
}
.prevention__n { color: var(--accent-text); font-size: var(--t-14); font-weight: 500; padding-top: 2px; }
.prevention__item p { color: var(--fg-muted); }
@media (min-width: 760px) { .prevention { grid-template-columns: 1fr 1fr; gap: var(--s-4); } }

/* --- index grid ----------------------------------------------------------- */
.pestgrid { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.pestgrid__item {
  display: grid; grid-template-columns: auto 1fr auto; gap: var(--s-4); align-items: center;
  padding: var(--s-4); background: var(--bg-raised);
  border: 1px solid var(--border-soft); border-radius: var(--r-card);
  transition: transform var(--d-base) var(--e-standard),
              box-shadow var(--d-base) var(--e-standard),
              border-color var(--d-base) var(--e-standard);
}
.pestgrid__icon { width: 40px; height: 40px; color: var(--accent-text); flex: none; transition: transform var(--d-base) var(--e-standard); }
.pestgrid__icon svg { width: 100%; height: 100%; }
.pestgrid__name { display: block; font-family: var(--f-display); font-weight: 800; font-size: var(--t-18); }
/* A whole sentence rather than a caption, so it gets a line's worth of room
   from the name above it instead of a caption's 4px. */
.pestgrid__sum { display: block; margin-top: var(--s-2); font-size: var(--t-14); color: var(--fg-muted); }
.pestgrid__go svg { width: 18px; height: 18px; color: var(--accent-text); transition: transform var(--d-base) var(--e-standard); }
@media (min-width: 760px) { .pestgrid { grid-template-columns: 1fr 1fr; gap: var(--s-4); } }

/* Identification values are prose, not short figures, so the right-aligned
   spec layout used on product pages wraps badly here. */
.pest-id .spec__row { grid-template-columns: minmax(4.5rem, auto) 1fr; }
.pest-id .spec__row dd { text-align: left; font-weight: 500; }

/* --- read next -------------------------------------------------------------
   Guides that name this pest. On a dark surface, so it reads as a change of
   register between the prevention steps and the products.
   ------------------------------------------------------------------------ */
.readnext { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.readnext__item {
  /* Title, summary and flag are three separate things, not a value and its
     caption, so 4px was too tight for the pair of them. */
  display: grid; gap: var(--s-2);
  padding: var(--s-4) var(--s-5);
  border: 1px solid var(--border); border-radius: var(--r-card);
  color: var(--fg);
  transition: border-color var(--d-quick) var(--e-standard),
              transform var(--d-quick) var(--e-standard),
              background var(--d-quick) var(--e-standard);
}
.readnext__title { font-family: var(--f-display); font-weight: 700; font-size: var(--t-18); }
/* A bordered box with a title, a summary and sometimes a flag, and nothing
   saying it was a link. Now it ends the way the pest cards do. */
.readnext__go { display: inline-block; margin-left: .3em; }
.readnext__go svg { width: 17px; height: 17px; vertical-align: -2px; color: var(--accent-text); transition: transform var(--d-base) var(--e-standard); }
.readnext__sum { color: var(--fg-muted); font-size: var(--t-14); line-height: 1.55; }
.readnext__flag { font-size: var(--t-12); color: var(--accent-text); }

@media (min-width: 900px) {
  .readnext { grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); }
}

/* --- the pest mark --------------------------------------------------------
   A cut-out photograph where one has been supplied, a line glyph until then.
   The slots are sized for the photograph, because that is the target state
   and a 40px insect is not identifiable. The glyph keeps its own smaller size
   inside the larger box, centred, so the fallback does not become a huge
   drawing on the way to being replaced.
   ------------------------------------------------------------------------ */
.pestmark { width: 100%; height: 100%; }
/* A cut-out keeps its whole silhouette. It arrives already trimmed to the
   subject and padded square by the asset pipeline, so it is centred rather
   than bottom anchored: anchoring a square canvas to the floor of a square
   box does nothing except make a wide subject sit low. */
.pestmark--cut { object-fit: contain; object-position: center; }
/* A square crop of the identification photograph. Rounded into a disc, which
   is what makes a photograph read as a mark in a list rather than as a small
   picture that has been dropped in. The ring keeps a pale insect on a pale
   background from dissolving into the card. */
.pestmark--crop {
  object-fit: cover;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(18, 22, 31, .10),
              0 1px 4px rgba(18, 22, 31, .14);
  background-size: cover; background-position: center;
}

.pestgrid__icon { display: grid; place-items: center; width: 72px; height: 72px; }
.pestgrid__icon svg { width: 40px; height: 40px; }
.pesthero__icon { display: grid; place-items: center; }
.pesthero__icon svg { width: 56px; height: 56px; }
@media (min-width: 760px) {
  .pestgrid__icon { width: 88px; height: 88px; }
  .pesthero__icon svg { width: 68px; height: 68px; }
}

/* --- hover, pointer devices only -----------------------------------------
   Behind a hover query so a touch screen never matches it. Without the
   guard the first tap on anything styled here only applied the hover and
   a second tap was needed to follow the link, which is the double-tap
   this site was reported for. Selector lists were split where they mixed
   :hover with :focus-within or :focus-visible, so keyboard feedback stays
   outside the guard and still works everywhere.
   ------------------------------------------------------------------------ */

  .prevention__item:hover {border-color: var(--accent-text); transform: translateX(3px); }
  .pestgrid__item:hover {transform: translateY(-3px); box-shadow: var(--sh-2); border-color: var(--accent-text); }
  .pestgrid__item:hover .pestgrid__icon {transform: scale(1.1) rotate(-5deg); }
  .pestgrid__item:hover .pestgrid__go svg {transform: translateX(4px); }
  .readnext__item:hover {border-color: var(--accent-text);
  background: rgba(255, 255, 255, .04);
  transform: translateX(3px); }

  .readnext__item:hover .readnext__go svg { transform: translateX(3px); }


/* --- trust.css --- */
/* ==========================================================================
   TRUST LAYER: safety, genuine pack, where to buy
   ========================================================================== */

/* --- first aid -----------------------------------------------------------
   The most important block on the site. Given the dark surface and a
   generous scale so it reads under pressure, not skimmed.
   ------------------------------------------------------------------------ */
.safety__firstaid { background: var(--bg); color: var(--fg); }
.safety__fa { margin: 0; display: grid; gap: 0; border-top: 1px solid var(--border); }
.safety__fa-row {
  display: grid; gap: var(--s-2);
  padding-block: var(--s-4); border-bottom: 1px solid var(--border);
}
.safety__fa-row dt {
  font-family: var(--f-display); font-weight: 800; font-size: var(--t-18);
  color: var(--c-gold-lite);
}
.safety__fa-row dd { margin: 0; color: var(--fg-muted); }
.safety__emergency {
  margin-top: var(--s-5); padding: var(--s-4);
  border-left: 3px solid var(--c-critical);
  background: rgba(140, 47, 42, .14);
  font-size: var(--t-14);
}
@media (min-width: 760px) {
  .safety__fa-row { grid-template-columns: 10rem 1fr; gap: var(--s-5); align-items: baseline; }
}

/* --- by format ------------------------------------------------------------ */
.safety__formats { display: grid; gap: var(--s-5); }
.safety__format {
  padding: var(--s-5); background: var(--bg-raised);
  border: 1px solid var(--border-soft); border-radius: var(--r-card);
}
.safety__format h3 {
  font-size: var(--t-18); margin-bottom: var(--s-4);
  padding-bottom: var(--s-3); border-bottom: 1px solid var(--border-soft);
}
.safety__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.safety__list li {
  position: relative; padding-left: var(--s-5);
  font-size: var(--t-14); color: var(--fg-muted);
}
.safety__list li::before {
  content: ""; position: absolute; left: 0; top: .55em;
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
}
.safety__note { margin-top: var(--s-4); font-size: var(--t-12); color: var(--fg-subtle); }
@media (min-width: 900px) { .safety__formats { grid-template-columns: repeat(3, 1fr); } }

/* --- storage -------------------------------------------------------------- */
.safety__storage { background: var(--bg-sunken); }
.safety__grid { display: grid; gap: var(--s-5); }
.safety__grid h3 {
  font-size: var(--t-14); letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--accent-text); margin-bottom: var(--s-2);
}
.safety__grid p { color: var(--fg-muted); font-size: var(--t-14); }
@media (min-width: 640px)  { .safety__grid { grid-template-columns: 1fr 1fr; gap: var(--s-6); } }
@media (min-width: 1024px) { .safety__grid { grid-template-columns: repeat(4, 1fr); } }

/* --- genuine pack --------------------------------------------------------- */
.genuine__report { background: var(--accent-deep, var(--c-navy)); color: #fff; }
.genuine__report-inner { display: grid; gap: var(--s-5); }
.genuine__report p { color: rgba(255, 255, 255, .82); }
.genuine__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.genuine__report .btn--ghost { color: #fff; box-shadow: inset 0 0 0 1px rgba(255,255,255,.4); }
@media (min-width: 900px) { .genuine__report-inner { grid-template-columns: 1fr auto; align-items: center; gap: var(--s-7); } }

/* --- stockists ------------------------------------------------------------ */
.stockists { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-3); }
.stockists__item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-4);
  min-height: 60px; padding: 0 var(--s-4);
  background: var(--bg-raised); border: 1px solid var(--border-soft); border-radius: var(--r-card);
  font-family: var(--f-display); font-weight: 700;
  transition: transform var(--d-base) var(--e-standard), border-color var(--d-base) var(--e-standard);
}
.stockists__go svg { width: 18px; height: 18px; color: var(--accent-text); }
.stockists__card { padding: var(--s-5); background: var(--bg-raised); border-radius: var(--r-card); }
.stockists__card h3 { font-size: var(--t-21); margin: var(--s-2) 0 var(--s-3); }
.stockists__card address { font-style: normal; color: var(--fg-muted); font-size: var(--t-14); }
.stockists__calls { margin-top: var(--s-4); }
@media (min-width: 760px) { .stockists { grid-template-columns: 1fr 1fr; gap: var(--s-4); } }

/* --- honest empty state ---------------------------------------------------
   Shown when nothing has been verified yet. It says why rather than
   pretending the section does not exist.
   ------------------------------------------------------------------------ */
.emptystate {
  padding: var(--s-6); background: var(--bg-sunken);
  border: 1px dashed var(--border); border-radius: var(--r-card);
  max-width: 44rem;
}
.emptystate p + p { margin-top: var(--s-3); }
.emptystate p { color: var(--fg-muted); }
.emptystate strong { color: var(--fg); }
.emptystate__calls { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-5) !important; }

/* --- hover, pointer devices only -----------------------------------------
   Behind a hover query so a touch screen never matches it. Without the
   guard the first tap on anything styled here only applied the hover and
   a second tap was needed to follow the link, which is the double-tap
   this site was reported for. Selector lists were split where they mixed
   :hover with :focus-within or :focus-visible, so keyboard feedback stays
   outside the guard and still works everywhere.
   ------------------------------------------------------------------------ */

  .stockists__item:hover {transform: translateX(4px); border-color: var(--accent-text); }


/* --- about.css --- */
/* ==========================================================================
   ABOUT AND COMMUNITY
   ========================================================================== */

.abouthero {
  background:
    radial-gradient(110% 90% at 82% 0%, color-mix(in srgb, var(--c-gold) 14%, transparent), transparent 60%),
    linear-gradient(180deg, var(--bg-sunken), var(--bg));
  padding-block: var(--s-6) var(--s-7);
}
.abouthero__inner { max-width: 52rem; }
.abouthero h1 { font-size: clamp(var(--t-32), 5.4vw, var(--t-56)); margin-top: var(--s-2); }
.abouthero__slogan {
  margin-top: var(--s-4);
  font-family: var(--f-display); font-weight: 700; font-size: clamp(var(--t-18), 2.4vw, var(--t-26));
  color: var(--accent-text);
  max-width: 22ch;
}
.abouthero__intro { margin-top: var(--s-5); }
@media (min-width: 760px) { .abouthero { padding-block: var(--s-7) var(--s-9); } }

/* --- what we do ----------------------------------------------------------- */
.about-what { display: grid; gap: var(--s-6); }
.about-what p + p { margin-top: var(--s-4); }
.about-what p { color: var(--fg-muted); }
.about-facts { padding: var(--s-5); background: var(--bg-sunken); border-radius: var(--r-card); }
.about-facts h3 {
  font-size: var(--t-12); letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--accent-text); margin-bottom: var(--s-3);
}
.about-facts__addr { font-style: normal; margin-top: var(--s-4); font-size: var(--t-14); color: var(--fg-muted); }
@media (min-width: 900px) { .about-what { grid-template-columns: 1.4fr 1fr; gap: var(--s-8); } }

/* --- brands --------------------------------------------------------------- */
.about-brands__grid { display: grid; gap: var(--s-4); }
.about-brand {
  padding: var(--s-5); border-radius: var(--r-card);
  background: var(--accent-deep, var(--bg-raised));
  transition: transform var(--d-base) var(--e-standard);
}
.about-brand img { height: 56px; width: auto; margin-bottom: var(--s-4); }
.about-brand p { color: rgba(255, 255, 255, .8); font-size: var(--t-14); margin-bottom: var(--s-4); }
.about-brand .link { color: var(--accent-2); font-family: var(--f-display); font-weight: 700; font-size: var(--t-14); }
@media (min-width: 760px) { .about-brands__grid { grid-template-columns: 1fr 1fr; gap: var(--s-5); } }

/* The slogan sits under the standfirst in the shared page hero. It is a
   closing line rather than a subtitle there, so it reads as a kicker. */
.abouthero__slogan {
  margin-top: var(--s-5);
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(var(--t-18), 2.4vw, var(--t-26));
  color: var(--accent-text);
  max-width: 24ch;
}

/* --- how we work ---------------------------------------------------------- */
.about-values { display: grid; gap: var(--s-5); }
.about-values h3 { font-size: var(--t-18); margin-bottom: var(--s-2); color: var(--accent-text); }
.about-values p { color: var(--fg-muted); font-size: var(--t-14); }
@media (min-width: 900px) { .about-values { grid-template-columns: repeat(3, 1fr); gap: var(--s-6); } }

/* ==========================================================================
   COMMUNITY WALL
   Local preview images that load a Facebook player only for a reel, and only
   on click. The previews are the posts' own media, downloaded at build time
   by scripts/fetch-community-previews.js, so the wall shows the pictures and
   still costs Meta nothing until somebody presses one. That is what keeps
   /privacy honest.

   Tile shapes come from the photographs. data-tile is set by the template
   from each picture's real aspect ratio: a vertical reel gets a vertical
   tile, a panorama gets a wide one, and one card in seven is promoted to a
   feature. Mobile and desktop each state their own grid in full rather than
   sharing a rule and patching it, because the column count changes what a
   span means and a shared rule gets that wrong in one of the two.
   ========================================================================== */
.cw { list-style: none; margin: 0; padding: 0; display: grid; }
.cw__item { display: grid; min-width: 0; }

/* --- mobile: two columns ------------------------------------------------- */
.cw {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: 42vw;
  grid-auto-flow: row dense;
  gap: var(--s-2);
}
.cw__item[data-tile="feature"] { grid-column: span 2; grid-row: span 2; }
.cw__item[data-tile="wide"]    { grid-column: span 2; }
.cw__item[data-tile="tall"]    { grid-row: span 2; }

@media (min-width: 640px) {
  .cw { grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: 26vw; gap: var(--s-3); }
  .cw__item[data-tile="feature"] { grid-column: span 2; grid-row: span 2; }
  .cw__item[data-tile="wide"]    { grid-column: span 2; grid-row: span 1; }
  .cw__item[data-tile="tall"]    { grid-column: span 1; grid-row: span 2; }
}

/* --- desktop: four columns on a fixed row, so the wall stops growing with
       the viewport and the tiles stay photograph-sized ------------------- */
@media (min-width: 1024px) {
  .cw { grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: 186px; gap: var(--s-3); }
  .cw__item[data-tile="feature"] { grid-column: span 2; grid-row: span 2; }
  .cw__item[data-tile="wide"]    { grid-column: span 2; grid-row: span 1; }
  .cw__item[data-tile="tall"]    { grid-column: span 1; grid-row: span 2; }
}
@media (min-width: 1240px) {
  .cw { grid-auto-rows: 208px; }
}

/* --- the card ------------------------------------------------------------ */
.cw__card {
  position: relative; display: block; width: 100%; height: 100%;
  padding: 0; border: 0; border-radius: var(--r-card);
  background: var(--bg-sunken);
  box-shadow: inset 0 0 0 1px var(--border-soft);
  font: inherit; color: var(--fg); text-align: left;
  cursor: pointer; overflow: hidden;
  transition: transform var(--d-quick) var(--e-standard),
              box-shadow var(--d-quick) var(--e-standard);
}
.cw__card:active { transform: translateY(-1px) scale(.995); }

.cw__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  background-size: cover; background-position: center;
  opacity: 0; transition: opacity var(--d-base) var(--e-standard),
                          transform var(--d-slow) var(--e-standard);
}
.cw__img.is-loaded { opacity: 1; }

/* A scrim, so the mark and the tag stay legible over a bright photograph and
   the wall keeps one consistent reading rather than depending on each frame. */
.cw__card::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  /* Weighted to the bottom, where the mark sits. The top used to carry a
     third of this so a "Photo" label would read against a bright sky, and
     with those labels gone it was darkening the top of nineteen photographs
     for the sake of the word REEL on six of them. That word has a text
     shadow of its own. */
  background: linear-gradient(180deg, rgba(18,22,31,.16) 0%, rgba(18,22,31,0) 26%,
                                      rgba(18,22,31,.04) 58%, rgba(18,22,31,.50) 100%);
  opacity: .9; transition: opacity var(--d-quick) var(--e-standard);
}

/* --- the play and photo marks -------------------------------------------- */
.cw__mark {
  position: absolute; bottom: var(--s-3); right: var(--s-3); z-index: 2;
  display: grid; place-items: center;
  width: 40px; height: 40px; border-radius: 50%;
  background: rgba(255, 255, 255, .92); color: var(--c-ink);
  box-shadow: 0 2px 10px rgba(18, 22, 31, .28);
  transition: transform var(--d-quick) var(--e-standard),
              background var(--d-quick) var(--e-standard);
}
.cw__mark svg { width: 18px; height: 18px; }

/* A reel is a different kind of thing, so its mark is bigger and centred:
   that is where a play button belongs on a video, and it tells the six reels
   apart from the thirteen photographs at a glance. */
.cw__card[data-kind="reel"] .cw__mark {
  top: 50%; left: 50%; right: auto; bottom: auto;
  width: 54px; height: 54px;
  transform: translate(-50%, -50%);
}
.cw__card[data-kind="reel"] .cw__mark svg { width: 22px; height: 22px; }

.cw__tag {
  position: absolute; top: var(--s-3); left: var(--s-3); z-index: 2;
  font-family: var(--f-display); font-weight: 700; font-size: var(--t-12);
  letter-spacing: var(--tr-label); text-transform: uppercase;
  color: #fff; text-shadow: 0 1px 6px rgba(18, 22, 31, .5);
}
.cw__caption {
  position: absolute; left: var(--s-3); right: 52px; bottom: var(--s-3); z-index: 2;
  font-size: var(--t-14); line-height: 1.3; color: #fff;
  text-shadow: 0 1px 6px rgba(18, 22, 31, .6);
}

/* A card with no preview. cw-008 is in this state: its photograph is not
   among the ones the Facebook plugin returns, so there is nothing honest to
   show. One neutral tile among nineteen photographs reads as an image that
   failed to load, so it is given a word and a border and made to look like
   the link it is rather than like a picture that is missing. */
.cw__item[data-tile="plain"] .cw__card {
  display: grid; place-content: center; gap: var(--s-3);
  padding: var(--s-4);
  background: var(--bg-sunken);
  box-shadow: inset 0 0 0 1px var(--border);
}
.cw__plain {
  font-family: var(--f-display); font-weight: 700; font-size: var(--t-12);
  letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--fg-muted); text-align: center; line-height: 1.4;
}
.cw__item[data-tile="plain"] .cw__card::after { display: none; }
.cw__item[data-tile="plain"] .cw__mark {
  position: static; justify-self: center;
  width: 48px; height: 48px;
  background: var(--accent); color: var(--accent-fg);
  transform: none;
}
.cw__item[data-tile="plain"] .cw__tag {
  position: static; margin-top: var(--s-3); text-align: center;
  color: var(--fg-muted); text-shadow: none;
}

.cw__fallback { display: inline-block; margin-top: var(--s-2); font-size: var(--t-14);
  text-decoration: underline; text-underline-offset: 3px; }

.cw__privacy {
  margin-top: var(--s-5); max-width: 60ch;
  font-size: var(--t-12); color: var(--fg-subtle);
}

/* --- the player ----------------------------------------------------------
   The dialog sizes itself to the media rather than the media being poured
   into a fixed box. It used to be a 60rem panel with a black slot inside it,
   so a portrait photograph sat in the middle of two large black wings with
   the close button floating on one of them, and the footer was a hard white
   strip butted against the black with no margin. Three surfaces meeting at
   once and none of them fitted the picture.

   Now the panel shrinks to its contents, the photograph sits on the panel
   itself with no black behind it, and only a reel keeps a dark surface,
   because a video player is dark and the frame around it should be too.
   ------------------------------------------------------------------------ */
.modal--player .modal__panel {
  /* fit-content so the panel is the size of the picture. min() keeps it
     inside the viewport on a phone, where the media is the full width. */
  width: fit-content;
  max-width: min(100%, 62rem);
  padding: var(--s-3);
  overflow: visible;
}

.cw__player {
  display: grid; place-items: center;
  border-radius: calc(var(--r-card) - 2px);
  overflow: hidden;
  background-size: cover; background-position: center;
}
.cw__player iframe { display: block; width: 100%; border: 0; }

/* A reel is vertical video, so the player is tall and dark. The poster frame
   sits behind it as a background while the iframe boots. */
.cw__player[data-kind="reel"] { background: #000; }
.cw__player[data-kind="reel"] iframe {
  aspect-ratio: 9 / 16; height: min(72vh, 680px); margin-inline: auto;
}

/* A photograph is a local file, so it needs no player and no dark ground: it
   sits on the panel at its own shape. */
.cw__player[data-kind="photo"] { background: none; }
.cw__big {
  display: block; width: auto; max-width: 100%;
  max-height: min(70vh, 720px);
  border-radius: calc(var(--r-card) - 2px);
}

.cw__playerfoot {
  margin: 0; padding-top: var(--s-3);
  display: flex; justify-content: center;
}
/* Quieter than a full button. It is the way out of the dialog, not the thing
   the dialog is for. */
.cw__tofb {
  min-height: 36px; padding-inline: var(--s-4);
  font-size: var(--t-12);
  color: var(--fg-muted); box-shadow: inset 0 0 0 1px var(--border-soft);
}
.cw__tofb svg { width: 13px; height: 13px; }

/* Outside the panel, above its top right corner, so it never sits over the
   picture and never needs a surface of its own to stay legible. It falls back
   inside the panel when there is no room beside it. */
.modal--player .modal__close {
  /* Its own height plus a gap, so the whole button clears the panel rather
     than half of it resting on the corner of the picture. */
  top: calc((44px + var(--s-2)) * -1); right: 0;
  background: rgba(255, 255, 255, .92); color: var(--c-ink);
}
@media (max-width: 560px) {
  .modal--player .modal__close { top: var(--s-2); right: var(--s-2); }
}

.cw__how { background: var(--bg); }

/* --- the company in its own place -----------------------------------------
   An about page for a distribution company with no picture of the operation
   is weaker than one with it, and these are the two photographs nobody else
   could have taken.
   ------------------------------------------------------------------------ */
/* A full measure on top. This was zero, from when the band followed a plain
   section and the two ran together as one column of content. It now follows
   the dark brands band, so zero meant the photograph butted straight against
   the dark edge with no gap at all. */
.about-place { padding-block: var(--section-y) var(--s-7); }
.about-place__grid { display: grid; gap: var(--s-4); }
.about-place__shot .media__img,
.about-place__shot .media__slot { border-radius: var(--r-card); }
.about-place__shot figcaption {
  margin-top: var(--s-3); font-size: var(--t-14); color: var(--fg-subtle);
}
/* One photograph here now rather than two, so it is given the width instead
   of being stretched across a column meant for a pair. */
.about-place__grid { max-width: 62rem; }
.about-place__shot figcaption { max-width: 52ch; }

/* --- a row of reels on another page ---------------------------------------
   The genuine pack page shows five of the wall's reels rather than the whole
   wall, so the grid is a single row of portrait tiles instead of the mosaic.
   Mobile and desktop each state their own, because the count per row is the
   whole difference between them: two on a phone, five across on a desktop.
   -------------------------------------------------------------------------- */
.cw--strip { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: auto; }
.cw--strip .cw__item { grid-row: auto; }
.cw--strip .cw__card { aspect-ratio: 9 / 16; height: auto; }

@media (min-width: 640px) {
  .cw--strip { grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: auto; }
  .cw--strip .cw__item { grid-row: auto; }
}
@media (min-width: 1024px) {
  .cw--strip { grid-template-columns: repeat(5, minmax(0, 1fr)); grid-auto-rows: auto; }
  .cw--strip .cw__item { grid-row: auto; }
}

/* --- hover, pointer devices only -----------------------------------------
   Behind a hover query so a touch screen never matches it. Without the
   guard the first tap on anything styled here only applied the hover and
   a second tap was needed to follow the link, which is the double-tap
   this site was reported for. Selector lists were split where they mixed
   :hover with :focus-within or :focus-visible, so keyboard feedback stays
   outside the guard and still works everywhere.
   ------------------------------------------------------------------------ */

  .about-brand:hover {transform: translateY(-3px); }
  .cw__card:hover {transform: translateY(-3px); box-shadow: var(--sh-3); }
  .cw__card:hover .cw__img {transform: scale(1.04); }
  .cw__card:hover::after {opacity: .72; }
  .cw__card:hover .cw__mark {transform: scale(1.1); background: var(--accent); color: var(--accent-fg); }
  .cw__card[data-kind="reel"]:hover .cw__mark {transform: translate(-50%, -50%) scale(1.1); }
  .cw__tofb:hover {color: var(--fg); box-shadow: inset 0 0 0 1px var(--border); }


/* --- article.css --- */
/* ==========================================================================
   ARTICLES
   Long-form reading. Measure is held tight and everything else defers to it.
   ========================================================================== */
.article { padding-block: var(--s-6) var(--s-8); }
/* The measure is capped on the children, the way .article__body below already
   does it. As a max-width on .article__head itself it fought the container's
   margin-inline: auto and lost: the whole heading block was centred, so every
   article opened with its eyebrow, h1, standfirst and byline starting at
   x=384 while the hero and the body beneath them started at 112. Eight
   articles, and the fifth instance of this same pair of rules on the site. */
.article__head > * { max-width: 46rem; }
.article__head h1 { font-size: clamp(var(--t-32), 5.4vw, var(--t-56)); margin-top: var(--s-2); }
.article__head .lede { margin-top: var(--s-4); font-size: clamp(var(--t-18), 2vw, var(--t-21)); }
.article__meta {
  display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-3); align-items: center;
  margin-top: var(--s-5); padding-top: var(--s-4);
  border-top: 1px solid var(--border-soft);
  font-size: var(--t-12); color: var(--fg-subtle);
}

.article__hero { margin-block: var(--s-6); }
.article__hero .media__img, .article__hero .media__slot { border-radius: var(--r-card); }

/* --- body ----------------------------------------------------------------
   The container is full width so figures can breathe, but running text is
   pinned to a readable measure inside it.
   ------------------------------------------------------------------------ */
.article__body > * { max-width: 46rem; }
.article__section + .article__section { margin-top: var(--s-7); }
.article__section h2 {
  font-size: clamp(var(--t-21), 3vw, var(--t-32));
  margin-bottom: var(--s-4);
}
.article__section p { color: var(--fg-muted); font-size: var(--t-18); line-height: 1.65; }
.article__section p + p { margin-top: var(--s-4); }
.article__figure { margin-block: var(--s-5); }

/* --- pull quote ----------------------------------------------------------
   Set apart because it is somebody else's words, and the citation is part of
   the quote rather than an afterthought.
   ------------------------------------------------------------------------ */
.pull {
  margin: var(--s-6) 0; padding: var(--s-5);
  border-left: 3px solid var(--accent);
  background: var(--bg-sunken);
  border-radius: 0 var(--r-card) var(--r-card) 0;
}
.pull p {
  font-family: var(--f-display); font-weight: 600;
  font-size: clamp(var(--t-18), 2.2vw, var(--t-21));
  line-height: 1.4; color: var(--fg);
}
.pull cite { display: block; margin-top: var(--s-3); font-style: normal; }
.ref {
  font-family: var(--f-mono); font-size: var(--t-12); font-weight: 500;
  color: var(--accent-text); padding: var(--s-1) var(--s-2);
  border-radius: var(--r-btn);
  /* A citation was accent-coloured monospace with a rounded box that only
     appeared under the cursor, so the box shape was doing no work. Filling
     it faintly at rest makes the chip read as a chip. */
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  transition: background var(--d-quick) var(--e-standard);
}

/* --- references ----------------------------------------------------------- */
.article__refs { margin-top: var(--s-8); padding-top: var(--s-6); border-top: 1px solid var(--border); }
.article__refs h2 {
  font-size: var(--t-14); letter-spacing: var(--tr-label);
  text-transform: uppercase; color: var(--fg-subtle); margin-bottom: var(--s-4);
}
.refs { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-4); }
.refs li {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s-3);
  font-size: var(--t-14); color: var(--fg-muted);
}
.refs li:target { background: var(--c-gold-wash); border-radius: var(--r-card); padding: var(--s-3); margin: calc(var(--s-3) * -1); }
.refs__n { color: var(--accent-text); }
.refs__body a { word-break: break-word; }
.refs__accessed { display: block; margin-top: var(--s-1); font-size: var(--t-12); color: var(--fg-subtle); }
.refs__back {
  display: inline-block; margin-top: var(--s-2);
  font-size: var(--t-12); color: var(--accent-text);
  border-bottom: 1px solid currentColor;
}

@media (min-width: 900px) {
  .article { padding-block: var(--s-7) var(--s-9); }
  .article__body { display: grid; justify-items: start; }
  .article__figure { max-width: none; width: 100%; }

  /* The reading column is centred on desktop.
     ----------------------------------------------------------------------
     Every left edge inside the article agrees, which is what was wrong
     before: the heading block used to start 272px right of its own body.
     They now agree and the whole column is centred, because 736px of text
     pinned to the left of a 1216px shell leaves 480px of empty paper down
     one side and reads as a fault. The hero keeps the full container width,
     so the opening picture is wider than the text and both are centred on the
     same axis, which is the ordinary editorial arrangement. Section figures
     sit inside their section and so take the measure with them.
     Auto margins beat justify-items on a grid item, so the body's start
     alignment stays in place for anything that is not capped. */
  /* width: 100% as well as the cap, so every child is the same column rather
     than each one being its own width and centred separately. The body is a
     grid with justify-items: start, which makes an item as wide as its
     content: the sections hit the 736px cap and centred at 352, while the
     references list is only 695px of citations and centred at 372. Two blocks
     of the same article, 20px apart, for no reason a reader could see. */
  .article__head > *,
  .article__body > * { width: 100%; max-width: 46rem; margin-inline: auto; }
}

/* --- advice index: topic filter -------------------------------------------
   Hidden without JS, because with no script every card is already visible and
   a set of dead buttons is worse than no buttons.
   ------------------------------------------------------------------------ */
.no-js .topicbar { display: none; }
.topicbar {
  display: flex; flex-wrap: wrap; gap: var(--s-2);
  margin-bottom: var(--s-5);
}
.topicbar__btn {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: 44px; padding: var(--s-2) var(--s-4);
  border: 1px solid var(--border); border-radius: var(--r-pill);
  background: transparent; color: var(--fg-muted);
  font: inherit; font-size: var(--t-14); font-weight: 500;
  text-transform: capitalize; cursor: pointer;
  transition: background var(--d-quick) var(--e-standard),
              color var(--d-quick) var(--e-standard),
              border-color var(--d-quick) var(--e-standard),
              transform var(--d-quick) var(--e-standard);
}
.topicbar__btn:active { transform: scale(0.97); }
.topicbar__btn.is-on {
  background: var(--c-ink); border-color: var(--c-ink); color: #F2F1EE;
}
.topicbar__n {
  font-size: var(--t-12); opacity: 0.6;
  font-variant-numeric: tabular-nums;
}

/* --- mobile reading corrections -------------------------------------------
   The global display leading is 0.92, which is right for a 56px hero line and
   wrong for a 32px headline that wraps to three lines on a phone. Article
   headings are read rather than glanced at, so they get their own leading at
   small sizes and hand back to the display setting once there is room.
   ------------------------------------------------------------------------ */
.article__head h1 { line-height: 1.06; }
.article__section h2 { line-height: 1.12; }

@media (min-width: 900px) {
  .article__head h1 { line-height: var(--lh-tight); }
  .article__section h2 { line-height: 1.02; }
}

/* Below 640px the pull quote loses its side padding to the measure: the left
   rule stays, the inset comes off, so the quote reads as a mark in the margin
   rather than a box competing with the text. */
@media (max-width: 639px) {
  .pull {
    padding: var(--s-4); padding-left: var(--s-4);
    margin-inline: 0;
  }
  .pull p { font-size: var(--t-18); }
  .refs li { grid-template-columns: 1fr; gap: var(--s-1); }
  .refs li:target { padding: var(--s-3); margin-inline: calc(var(--s-3) * -1); }
}

/* The pull block is a summary of published guidance, not a quotation, and it
   says so. Styled as a callout rather than a blockquote so nothing about it
   suggests somebody's exact words. */
.pull__label {
  font-family: var(--f-mono); font-size: var(--t-12);
  letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--accent-text); margin-bottom: var(--s-2);
}
.pull__src {
  margin-top: var(--s-3); font-family: var(--f-body);
  font-size: var(--t-12); font-weight: 400; color: var(--fg-subtle);
}
.pull__src .ref { padding: 2px var(--s-1); }

/* --- medical notice --------------------------------------------------------
   Sits above the article, not at the foot. An article that names malaria or
   Lassa fever has to carry the disclaimer where a person skimming will meet
   it, and the strongest regulatory risk on this site is somebody reading half
   a health article and inferring a claim the product does not make.
   ------------------------------------------------------------------------ */
.medbar {
  max-width: 46rem;
  margin-block: var(--s-5) 0;
  padding: var(--s-4) var(--s-5);
  border: 1px solid var(--c-critical);
  border-left-width: 4px;
  border-radius: 0 var(--r-card) var(--r-card) 0;
  background: var(--c-critical-wash);
}
.medbar p { font-size: var(--t-14); line-height: 1.6; color: var(--fg-muted); }
.medbar strong { color: var(--fg); }

.card__flag {
  display: inline-block; margin-left: var(--s-2);
  font-family: var(--f-mono); font-size: var(--t-12);
  color: var(--c-critical);
}

/* --- hover, pointer devices only -----------------------------------------
   Behind a hover query so a touch screen never matches it. Without the
   guard the first tap on anything styled here only applied the hover and
   a second tap was needed to follow the link, which is the double-tap
   this site was reported for. Selector lists were split where they mixed
   :hover with :focus-within or :focus-visible, so keyboard feedback stays
   outside the guard and still works everywhere.
   ------------------------------------------------------------------------ */

  .ref:hover {background: color-mix(in srgb, var(--accent) 14%, transparent); }
  .topicbar__btn:hover {border-color: var(--accent-text); color: var(--fg); }


/* --- forms.css --- */
/* ==========================================================================
   FORMS, TRADE AND CONTACT
   The field primitives live in components.css. This file is the layout around
   them, the consent block, and the two pages that use both.
   ========================================================================== */

.form { max-width: 46rem; }
.form__grid {
  display: grid; gap: 0 var(--s-4);
  grid-template-columns: 1fr;
}
.form__foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-4);
  margin-top: var(--s-5);
}
.form__required { font-size: var(--t-12); color: var(--fg-subtle); }

/* --- offline notice -------------------------------------------------------
   Stated once, at the top, before anybody starts typing. A form that takes a
   submission it cannot deliver is a worse failure than a form that says so.
   ------------------------------------------------------------------------ */
.form__offline {
  padding: var(--s-4); margin-bottom: var(--s-5);
  border-left: 3px solid var(--accent);
  background: var(--c-gold-wash);
  border-radius: 0 var(--r-card) var(--r-card) 0;
  font-size: var(--t-14); color: var(--fg-muted);
}
.form__offline strong { color: var(--fg); }

/* --- error summary --------------------------------------------------------
   Focused on failed submit, so the first thing announced is what went wrong
   and every entry jumps to the field that caused it.
   ------------------------------------------------------------------------ */
.form__summary {
  padding: var(--s-4); margin-bottom: var(--s-5);
  border: 1px solid var(--c-critical);
  border-radius: var(--r-card);
  background: var(--c-critical-wash);
}
.form__summary:focus-visible { outline: 2px solid var(--c-critical); outline-offset: 2px; }
.form__summary-title {
  font-family: var(--f-display); font-weight: 700;
  font-size: var(--t-14); color: var(--c-critical); margin-bottom: var(--s-2);
}
.form__summary-list { margin: 0; padding-left: var(--s-4); font-size: var(--t-14); }
.form__summary-list li + li { margin-top: var(--s-1); }
.form__summary-list a { color: var(--c-critical); text-decoration: underline; }

/* --- consent --------------------------------------------------------------
   Marketing is its own unticked box with its own basis. Answering an enquiry
   and mailing somebody are different things and NDPA treats them differently.
   ------------------------------------------------------------------------ */
.consent {
  margin-top: var(--s-5); padding: var(--s-5);
  border: 1px solid var(--border-soft); border-radius: var(--r-card);
  background: var(--bg-sunken);
}
.consent__legend {
  font-family: var(--f-display); font-weight: 700;
  font-size: var(--t-14); padding: 0 var(--s-2); margin-left: calc(var(--s-2) * -1);
}
.consent__text { font-size: var(--t-14); color: var(--fg-muted); }
.consent__text + .consent__text { margin-top: var(--s-2); }
.consent__opt {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s-3);
  align-items: start; margin-top: var(--s-4);
  padding: var(--s-3); margin-inline: calc(var(--s-3) * -1);
  border-radius: var(--r-btn); cursor: pointer;
  font-size: var(--t-14);
  transition: background var(--d-quick) var(--e-standard);
}
.consent__opt input {
  width: 20px; height: 20px; margin-top: 1px; accent-color: var(--accent-text);
}
.consent__note { margin-top: var(--s-3); font-size: var(--t-12); color: var(--fg-subtle); }

/* --- lists shared by the trade page --------------------------------------- */
.ticklist, .steplist { margin: 0 0 var(--s-6); padding: 0; list-style: none; }
.ticklist li, .steplist li {
  position: relative; padding-left: var(--s-6);
  color: var(--fg-muted); font-size: var(--t-16); line-height: 1.6;
}
.ticklist li + li, .steplist li + li { margin-top: var(--s-3); }
.ticklist li::before {
  content: ''; position: absolute; left: 0; top: 0.55em;
  width: 10px; height: 10px; border-radius: 50%;
  border: 2px solid var(--accent);
}
.steplist { counter-reset: step; }
.steplist li { counter-increment: step; }
.steplist li::before {
  content: counter(step); position: absolute; left: 0; top: 0.05em;
  width: 22px; height: 22px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent); color: #fff;
  font-family: var(--f-mono); font-size: var(--t-12);
}
.steplist strong { color: var(--fg); }
.h-sub {
  font-size: clamp(var(--t-21), 2.6vw, var(--t-26));
  margin-bottom: var(--s-4);
}

/* --- contact cards -------------------------------------------------------- */
.contact__grid, .trade__grid { display: grid; gap: var(--s-5); }
.contactcard {
  padding: var(--s-5);
  border: 1px solid var(--border-soft); border-radius: var(--r-card);
  background: var(--bg-raised);
}
.contactcard__h {
  font-size: var(--t-12); letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--fg-subtle); margin-bottom: var(--s-4);
}
.contactcard__list { list-style: none; margin: 0; padding: 0; }
.contactcard__list li + li { margin-top: var(--s-2); }
.contactcard__line {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s-3); align-items: center;
  min-height: 48px; padding: var(--s-2); margin-inline: calc(var(--s-2) * -1);
  border-radius: var(--r-btn); color: var(--fg);
  transition: background var(--d-quick) var(--e-standard),
              transform var(--d-quick) var(--e-standard);
}
.contactcard__line:active { transform: translateX(0) scale(.99); }
.contactcard__icon { color: var(--accent-text); display: grid; place-items: center; }
.contactcard__val {
  display: block; font-family: var(--f-display); font-weight: 700;
  font-size: var(--t-18); font-variant-numeric: tabular-nums;
}
/* The caption belongs to the number above it, so it sits close, but it was
   sitting at zero and the two lines touched. */
.contactcard__meta { display: block; margin-top: var(--s-1); font-size: var(--t-12); color: var(--fg-subtle); }
.contactcard__addr {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s-3);
  align-items: start; color: var(--fg-muted); margin-bottom: var(--s-3);
  line-height: 1.55;
}
.contactcard__note { margin-top: var(--s-3); font-size: var(--t-12); color: var(--fg-subtle); }

.doclist { list-style: none; margin: var(--s-4) 0 0; padding: 0; font-size: var(--t-14); }
.doclist li {
  display: flex; justify-content: space-between; gap: var(--s-3);
  padding-block: var(--s-3); border-top: 1px solid var(--border-soft);
  color: var(--fg-muted);
}
.doclist__state {
  flex: none; font-family: var(--f-mono); font-size: var(--t-12);
  color: var(--fg-subtle); text-align: right;
}

.faqgroup + .faqgroup { margin-top: var(--s-6); }
.faqgroup__h {
  font-size: var(--t-12); letter-spacing: var(--tr-label); text-transform: uppercase;
  color: var(--accent-text); margin-bottom: var(--s-3);
}

/* --- from 720px: two fields to a row, sidebar beside the form ------------- */
@media (min-width: 720px) {
  .form__grid { grid-template-columns: 1fr 1fr; }
  .field--wide { grid-column: 1 / -1; }
  .contact__grid { grid-template-columns: repeat(3, 1fr); }
}

@media (min-width: 1024px) {
  .trade__grid { grid-template-columns: minmax(0, 1fr) 22rem; gap: var(--s-7); align-items: start; }
  .trade__side { position: sticky; top: calc(76px + var(--s-5)); display: grid; gap: var(--s-5); }
}

/* --- legal and long-form policy pages -------------------------------------
   Same reading measure as an article, but the sections are numbered by
   heading rather than illustrated, so the rhythm is tighter.
   ------------------------------------------------------------------------ */
.legal__meta { margin-top: var(--s-4); font-size: var(--t-12); color: var(--fg-subtle); }
.legal__section + .legal__section { margin-top: var(--s-6); }
.legal__section h2 {
  font-size: clamp(var(--t-18), 2.2vw, var(--t-21));
  margin-bottom: var(--s-3);
}
.legal__section p { color: var(--fg-muted); line-height: 1.65; }
.legal__section p + p { margin-top: var(--s-3); }
.legal__list { margin: var(--s-3) 0 0; padding: 0; list-style: none; display: grid; gap: var(--s-3); }
.legal__list li {
  position: relative; padding-left: var(--s-5);
  color: var(--fg-muted); line-height: 1.6;
}
.legal__list li::before {
  content: ''; position: absolute; left: 0; top: 0.62em;
  width: 8px; height: 2px; background: var(--accent);
}
.legal__list strong { color: var(--fg); }

/* --- hover, pointer devices only -----------------------------------------
   Behind a hover query so a touch screen never matches it. Without the
   guard the first tap on anything styled here only applied the hover and
   a second tap was needed to follow the link, which is the double-tap
   this site was reported for. Selector lists were split where they mixed
   :hover with :focus-within or :focus-visible, so keyboard feedback stays
   outside the guard and still works everywhere.
   ------------------------------------------------------------------------ */

  .consent__opt:hover {background: color-mix(in srgb, var(--accent) 7%, transparent); }
  .contactcard__line:hover {background: var(--c-gold-wash); transform: translateX(2px); }


/* --- pages.css --- */
/* ==========================================================================
   PAGE-LEVEL STYLES
   ========================================================================== */

/* --- component gallery, internal ---------------------------------------- */
.gal { padding-block: var(--s-7) var(--s-9); }
.gal__head { margin-bottom: var(--s-7); }
.gal__block { padding-block: var(--s-6); border-top: 1px solid var(--border-soft); }
.gal__h { font-size: var(--t-21); margin-bottom: var(--s-2); }
.gal__note { font-size: var(--t-14); color: var(--fg-muted); margin-bottom: var(--s-4); }
.gal__demo { padding: var(--s-5); background: var(--bg-sunken); border-radius: var(--r-card); }
.gal__row { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; }
.gal__row + .gal__row { margin-top: var(--s-3); }
.gal__form { max-width: 34rem; display: grid; gap: var(--s-2); }

/* --- home: section head shared ------------------------------------------- */
/* The gap under a section heading was 24px on mobile and 32px on desktop,
   against a heading that runs to 42px. A heading needs more room beneath it
   than the lines of a paragraph do, or it reads as a label stuck to the top
   of the grid rather than as the title of the band. */
.section__head { display: grid; gap: var(--s-2); margin-bottom: var(--s-6); }
.section__more { font-family: var(--f-display); font-weight: 700; font-size: var(--t-14); color: var(--accent-text); }
@media (min-width: 760px) {
  .section__head { grid-template-columns: 1fr auto; align-items: end; margin-bottom: var(--s-7); }
}

/* --- home: pest picker ---------------------------------------------------
   Six equal targets. Balance here is literal: identical weight, identical
   spacing, so no pest reads as more important than another.
   ------------------------------------------------------------------------ */
.pestpick__head { max-width: 46rem; margin-bottom: var(--s-6); }
.pestpick { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-3); list-style: none; margin: 0; padding: 0; }
.pestpick__item {
  display: flex; flex-direction: column; align-items: center; gap: var(--s-2);
  padding: var(--s-4) var(--s-2); min-height: 116px; justify-content: center;
  background: var(--bg-raised); border: 1px solid var(--border-soft);
  border-radius: var(--r-card); text-align: center;
  transition: transform var(--d-base) var(--e-standard),
              box-shadow var(--d-base) var(--e-standard),
              border-color var(--d-base) var(--e-standard);
}
.pestpick__icon { width: 40px; height: 40px; color: var(--accent-text); transition: transform var(--d-base) var(--e-standard); }
.pestpick__icon svg { width: 100%; height: 100%; }
.pestpick__label { font-family: var(--f-display); font-weight: 700; font-size: var(--t-14); }
/* The same arrow the pest cards carry, trailing the label inline so the tile
   keeps its height and its centring. Inline rather than a fourth row: the
   arrow belongs to the words, and it wraps with them. */
.pestpick__go { display: inline-block; margin-left: .3em; }
.pestpick__go svg { width: 15px; height: 15px; vertical-align: -2px; color: var(--accent-text); transition: transform var(--d-base) var(--e-standard); }
.pestpick__more { margin-top: var(--s-5); }
@media (min-width: 640px)  { .pestpick { grid-template-columns: repeat(3, 1fr); gap: var(--s-4); } }
@media (min-width: 1024px) { .pestpick { grid-template-columns: repeat(6, 1fr); } }

/* --- home: brand split ----------------------------------------------------
   Both wrappers below were in the markup with no rule anywhere, so the
   heading sat on the roof of the cards and the product count was pressed
   between the line above it and the button below. Neither is a taste
   judgement; both were zero.
   ------------------------------------------------------------------------ */
.brands__head { margin-bottom: var(--s-5); }
/* justify-items matters as much as the gap here. Giving this wrapper a grid
   to space its children also made every child a grid item, and a grid item
   stretches: the Explore button, which is a span, went from its own width to
   the full width of the card and the arrow inside it grew to match. The
   children are a heading, two lines of text and a button, none of which want
   to be stretched. */
.brandcard__body { display: grid; gap: var(--s-3); justify-items: start; }
/* The text does want the width, just not the stretch on the button. */
.brandcard__body > p { justify-self: stretch; }
/* The count is a caption for the line above it rather than a separate
   thought, so it sits closer to that line than to the button under it. */
.brandcard__count { margin-top: calc(var(--s-3) * -1 + var(--s-1)); }

.brands__grid { display: grid; gap: var(--s-4); }
.brandcard {
  position: relative; overflow: hidden;
  border-radius: var(--r-card);
  background: var(--accent-deep);
  color: #fff;
  padding: var(--s-6) var(--s-5);
  display: grid; gap: var(--s-4);
  transition: transform var(--d-base) var(--e-standard), box-shadow var(--d-base) var(--e-standard);
}
.brandcard::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(120% 90% at 85% 0%, color-mix(in srgb, var(--accent) 55%, transparent), transparent 62%);
  pointer-events: none;
}
.brandcard > * { position: relative; }
.brandcard__mark img { height: 64px; width: auto; }
.brandcard__title { font-family: var(--f-display); font-size: var(--t-32); font-weight: 900; letter-spacing: -.03em; }
.brandcard__line { color: rgba(255,255,255,.82); font-size: var(--t-14); max-width: 34ch; }
.brandcard__count { color: var(--accent-2); font-size: var(--t-12); letter-spacing: var(--tr-label); text-transform: uppercase; }
.brandcard__cta { align-self: start; }
.brandcard__link { position: absolute; inset: 0; }
/* :focus-visible, not :focus-within, for the same reason as .card: tapping the
   overlay link focuses it, and with :focus-within the outline stayed drawn
   around the card after the finger came away. */
.brandcard:has(:focus-visible) { outline: 3px solid var(--focus); outline-offset: 3px; }

/* The "Explore TOPRANK" control is a <span>, and .brandcard__link is an
   absolutely positioned overlay sitting on top of it. So the span is never the
   element under the pointer and has never matched :hover: this treatment has
   not appeared on a brand card on any device. Reached through the card, which
   does match, so the brand cards now behave like every other button here. */
.brandcard:hover .brandcard__cta {
  box-shadow: inset 0 1px 0 rgba(255,255,255,.32), var(--sh-3);
}
.brandcard:hover .brandcard__cta .btn__arrow { transform: translateX(3px); }
@media (min-width: 760px) { .brands__grid { grid-template-columns: 1fr 1fr; gap: var(--s-5); } .brandcard { padding: var(--s-7) var(--s-6); } }

/* --- home: product cards -------------------------------------------------- */
.card__frame--product {
  /* an explicit height, not aspect-ratio: a percentage max-height on the image
     needs a definite basis to resolve against */
  height: clamp(200px, 21vw, 270px);
  display: flex; align-items: center; justify-content: center;
  padding: var(--s-5) var(--s-4) var(--s-3);
  background: linear-gradient(180deg, var(--bg-sunken), var(--bg-raised));
}
.card__frame--product img {
  max-height: 100%; max-width: 100%; width: auto; height: auto;
  filter: drop-shadow(0 10px 18px rgba(18,22,31,.22));
  transition: transform var(--d-slow) var(--e-standard);
}
/* The glue trap pack, 20% down on the rest of the range.
   ------------------------------------------------------------------------
   Every product card fills the frame, so each pack renders at whichever of
   the frame's two dimensions binds first. That is fair on the three tall
   packs and not on this one: the trap is a wide box at an aspect of 0.683
   against the aerosol's 0.217, so filling the same frame made it read as
   several times the size of a can.

   Both maxima drop together, which is what makes this exactly 20% whichever
   axis is doing the constraining. A transform would have been the other way
   to do it and would have had to fight the hover scale on the same property.

   Keyed on the filename in the src rather than on a class, because five
   templates render this card, all of them with the same markup and the
   product's own image name in it. One rule reaches every one of them, so the
   pack cannot end up 20% smaller in the catalogue and full size on the home
   page. */
.card__frame--product img[src*="toprank-gluetrap"] {
  max-height: 80%; max-width: 80%;
}

/* --- home: trust strip ---------------------------------------------------- */
.trust { background: var(--bg); color: var(--fg); }
.trust__grid { display: grid; gap: var(--s-5); margin: 0; }
.trust__item dt {
  font-family: var(--f-display); font-size: var(--t-12); font-weight: 700;
  letter-spacing: var(--tr-label); text-transform: uppercase; color: var(--accent-2, var(--c-gold-lite));
  margin-bottom: var(--s-2);
}
.trust__item dd { margin: 0 0 var(--s-2); font-family: var(--f-display); font-size: var(--t-21); font-weight: 800; }
.trust__item p { font-size: var(--t-14); color: var(--fg-muted); }
@media (min-width: 640px)  { .trust__grid { grid-template-columns: 1fr 1fr; gap: var(--s-6); } }
@media (min-width: 1024px) { .trust__grid { grid-template-columns: repeat(4, 1fr); } }

/* --- home: where to buy band ---------------------------------------------- */
.buyband { background: var(--accent-deep, var(--c-navy)); color: #fff; padding-block: var(--s-7); }
.buyband__inner { display: grid; gap: var(--s-5); }
.buyband h2 { font-size: clamp(var(--t-26), 3.4vw, var(--t-42)); }
.buyband .lede { color: rgba(255,255,255,.8); }
.buyband__actions { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.buyband .btn--ghost { color: #fff; box-shadow: inset 0 0 0 1px rgba(255,255,255,.42); }
@media (min-width: 900px) {
  .buyband__inner { grid-template-columns: 1fr auto; align-items: center; gap: var(--s-7); }
  .buyband { padding-block: var(--s-8); }
}

/* --- mobile touch targets ------------------------------------------------
   The carousel dots are 2px bars by design and the logo lockup is sized to
   the mark. Both need a larger hit area without changing how they look, so
   the target is expanded with a transparent pseudo-element instead.

   The expansion is downward only. It used to be `inset: -19px -7px`, growing
   in every direction, and on mobile the bar sits directly on the bottom edge
   of the slide's call to action: the last 19px of a 46px button belonged to a
   2px indicator, and a thumb aiming at "Explore TOPRANK" changed slide
   instead. Below the bar there is empty stage until the carousel clips, so
   that is where the target goes.

   The result is about 32px by 23px rather than 44 by 44. WCAG 2.5.8 permits
   this where an equivalent control meets the size requirement, and the
   previous and next controls are 164px wide and the full height of the stage.
   Verified by hit-testing in scripts/sweep.js, which cannot see a
   pseudo-element by measurement and has to tap for it.
   ------------------------------------------------------------------------ */
.dots button { position: relative; }
.dots button::before { content: ""; position: absolute; inset: -19px -7px; }

/* Downward only on mobile, where the bar sits on the call to action. The
   desktop composition puts the copy and its button in the left column, well
   clear of the dots, so there the target can grow both ways.
   Keyed on the viewport, not on data-view. The carousel's own rule sets moved
   to media queries so the first paint stops depending on JavaScript, and
   these two rules were the last things on the site still waiting for the
   attribute to be set before they applied. */
/* Scoped under .stage to match the generated rules. The carousel stylesheet
   states its own mobile dots as `.stage .dots`, two classes, and when these
   rules were keyed on data-view they carried three and won outright. Rewritten
   as a bare `.dots` they dropped to one class and silently lost, which shrank
   the hit area back under the touch target floor. */
@media (max-width: 760px) {
  .stage .dots button::before { inset: 0 -7px -21px; }
}

.site-header__brand {
  display: inline-flex; align-items: center;
  min-height: 44px;
}
/* the carousel clips overflow, so the dots need room inside it for the
   expanded hit area to survive */
@media (max-width: 760px) {
  .stage .dots { bottom: 1.35rem; }
}

/* ==========================================================================
   PAGE HERO
   The band at the top of a section landing page. Replaces a .pageheader that
   sat alone in a .section, where its own bottom padding met the next
   section's top padding and opened every one of these pages with 224px of
   empty ground.

   Mobile and desktop state their own layout in full. They are not variations
   on each other: on a phone the picture follows the copy in one column and is
   held to a shallow band so the heading and the first link stay above the
   fold, and on a desktop it sits beside the copy at the full height of the
   text. A shared rule with an overridden column count gets one of those two
   wrong.
   ========================================================================== */
.pagehero {
  background: linear-gradient(180deg, var(--bg-sunken), var(--bg));
}
/* The band carries its own bottom padding and ends on a colour change, so the
   section after it does not need a full measure on top as well. Together they
   were opening every one of these pages with about 150px of empty ground
   between the heading and the first real content, which was most obvious on
   the community wall, where the content is the point of the page. */
.pagehero + .section { padding-block-start: var(--s-6); }
.pagehero__copy { max-width: 46rem; }

/* --- mobile: copy, then the picture under it ----------------------------- */
.pagehero__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s-5);
  padding-block: var(--s-6) var(--s-7);
}
.pagehero__art { min-width: 0; }
/* contain, not cover.
   ------------------------------------------------------------------------
   The band holds every hero in the same 4:3 box so the pages read as the
   same piece of furniture. Filling that box by cropping was wrong: the about
   page photograph is a 2:1 group shot of the whole team and cover threw away
   a quarter of the people in it. A hero is a photograph somebody chose, so
   the box is a frame rather than a crop, and anything that does not match its
   shape sits inside it whole.
   The shadow is a filter rather than a box-shadow for the same reason: it has
   to hug the picture, not the empty frame around it. */
/* 3:2, matching the photographs, rather than 16:9.
   ------------------------------------------------------------------------
   The frame was 16:9 with the image capped at 46vw, which is 179px on a 390px
   screen. Every one of these heroes is nearer 3:2, and `contain` will not
   crop, so the picture sat 268px wide inside a 358px frame with empty ground
   down both sides and read as a thumbnail rather than a hero. Measured: the
   pest, advice and safety heroes all painted 268x179 in a 358px band.

   At 3:2 the same photograph paints 358x239, filling the width. The two
   outliers still behave: the about page's 2:1 team shot is width-limited
   either way and is untouched, and the 4:3 where-to-buy photograph gains
   height and takes narrow bars at the sides instead of the top.

   The cap is now a ceiling for large phones rather than the thing deciding the
   size. 64vw covers every width up to about 470px without clamping; past that
   300px stops a 759px tablet in portrait from opening with a 485px slab. */
.pagehero__shot { --ratio: 1.5; }
.pagehero__shot .media__img,
.pagehero__shot .media__slot {
  border-radius: var(--r-card);
  max-height: min(64vw, 300px);
  object-fit: contain;
}
.pagehero--upright .pagehero__shot .media__img,
.pagehero--square .pagehero__shot .media__img {
  width: auto; max-width: 100%; margin-inline: auto;
  max-height: none; height: min(58vw, 230px);
}
/* The hero art carries no blur-up placeholder at all, set in pagehero.js.
   Sizing the background to contain very nearly lined it up with the picture
   and the remainder showed as coloured bars down the sides, because the
   20px-wide placeholder does not round to exactly the same box. */

/* A page with no photograph yet keeps the band, at the height the text needs
   and no more, so it reads as a heading rather than as a gap. */
.pagehero--text .pagehero__inner { padding-block: var(--s-6); }

/* --- desktop: picture beside the copy ------------------------------------ */
@media (min-width: 900px) {
  .pagehero__inner {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    /* start, not center. Centring let the heading float by however tall the
       standfirst happened to be, so the h1 sat 48px down on the about page,
       77px down on safety and 124px down on products. The client asked for
       uniformity and the position of the heading is the most visible part of
       it, so both columns begin at the band's top padding. */
    align-items: start;
    gap: var(--s-8);
    /* The section below brings its own 96px of top padding, so a full 64px
       here put 160px of empty ground between the band and the first heading. */
    padding-block: var(--s-7) var(--s-6);
  }
  .pagehero__copy { max-width: 34rem; }
  .pagehero__shot { --ratio: 1.3333; }
  .pagehero__shot .media__img,
  .pagehero__shot .media__slot {
    /* 360px made the band 550px tall against 167px of copy, so the heading
       floated in the middle of a lot of nothing. */
    max-height: 320px;
  }
  /* An upright or square picture is sized by its height instead of the frame,
     and the frame gets out of the way.
     Inside a landscape 4:3 frame a portrait photograph is limited by the
     frame's height, so it came out 240px wide in a 562px column with empty
     bars either side, and read as a thumbnail. Given an explicit height and
     an automatic width it is the frame, at 345 by 460 rather than 240 by 320,
     and there are no bars to leave empty. Costs some band height on the two
     pages whose photographs are shaped this way. */
  .pagehero--upright .pagehero__shot .media__img,
  .pagehero--square .pagehero__shot .media__img {
    width: auto; max-width: 100%; margin-inline: auto;
    max-height: none;
  }
  .pagehero--upright .pagehero__shot .media__img { height: 460px; }
  .pagehero--square .pagehero__shot .media__img { height: 400px; }
  .pagehero__shot .media__img { filter: drop-shadow(0 10px 24px rgba(18, 22, 31, .16)); }
  .pagehero__shot .media__slot { box-shadow: var(--sh-3); }
  .pagehero--text .pagehero__inner {
    grid-template-columns: minmax(0, 1fr);
    padding-block: var(--s-7);
  }
  .pagehero--text .pagehero__copy { max-width: 46rem; }
}

/* The home page picker shares the pest mark, so its slot is sized the same
   way: built for a photograph, with the fallback glyph centred inside it. */
.pestpick__icon { display: grid; place-items: center; width: 72px; height: 72px; }
.pestpick__icon svg { width: 40px; height: 40px; }


/* --- hover, pointer devices only -----------------------------------------
   Behind a hover query so a touch screen never matches it. Without the
   guard the first tap on anything styled here only applied the hover and
   a second tap was needed to follow the link, which is the double-tap
   this site was reported for. Selector lists were split where they mixed
   :hover with :focus-within or :focus-visible, so keyboard feedback stays
   outside the guard and still works everywhere.
   ------------------------------------------------------------------------ */

  .pestpick__item:hover {transform: translateY(-4px); box-shadow: var(--sh-3); border-color: var(--accent-text); }
  .pestpick__item:hover .pestpick__icon {transform: scale(1.12) rotate(-5deg); }
  .brandcard:hover {transform: translateY(-4px); box-shadow: var(--sh-4); }
  .buyband .btn--ghost:hover {box-shadow: inset 0 0 0 1px var(--accent-2); color: var(--accent-2); }


  .pestpick__item:hover .pestpick__go svg { transform: translateX(3px); }


/* --- a11y.css --- */
/* ==========================================================================
   TOUCH TARGETS
   Loaded last so it can raise any target the component styles left small.
   Kept in one file so the mobile audit has a single place to look.
   ========================================================================== */

/* Breadcrumb links sit at caption size by convention, which leaves them well
   under a comfortable tap target. Padding raises the target without changing
   the type size or the visual rhythm of the trail. */
@media (max-width: 759px) {
  .breadcrumbs { padding-block: var(--s-2); }
  .breadcrumbs ol { gap: var(--s-1) var(--s-2); }
  .breadcrumbs a {
    display: inline-flex; align-items: center;
    min-height: 36px; padding-inline: var(--s-1);
  }
  .breadcrumbs li:first-child a { padding-left: 0; }
}

/* Anything that stretches a link over a parent needs the parent to be the
   target, not the text. Documented here so the audit does not flag it again:
   .card__link and .brandcard__link both cover their whole card. */
.card { position: relative; }

/* Section "see all" links sit next to a heading at caption weight. Padding
   raises the target on touch without shifting the heading baseline. */
@media (max-width: 759px) {
  .section__more {
    display: inline-flex; align-items: center;
    min-height: 40px;
  }
}

/* The carousel dot hit area lives in pages.css, where it already was. This
   file briefly carried a second copy of it, which is worth recording: the
   duplicate expanded the target in every direction and took 19px off the
   bottom of the slide's call to action, and the resulting hit-test failure
   then looked like a browser quirk for far too long. One rule, one place. */
