/* ==========================================================================
   Sofvera Manufactures — brand stylesheet
   Palette and type sampled directly from the corporate poster artwork.
   ========================================================================== */

/* ------------------------------------------------------------- 1. tokens */
:root {
  color-scheme: light;

  /* ---- palette ------------------------------------------------------------
     The six brand values, written out exactly as supplied and aliased below
     under the brief's own names, so CSS written against the brief drops in
     without translation. tools/verify.py fails if any of the six drifts, and
     fails if either stylesheet hard-codes one of their channel triples inside
     an rgb()/rgba() instead of using the *-rgb tokens at the bottom.

       --navy  #0B2A45    --green #4F8F3A    --green-wash #EEF4E9
       --cream #F8F9F6    --paper #FFFFFF    --ink        #17324D          */

  --navy:        #0B2A45;
  --navy-deep:   #071C2F;   /* footer, overlays, the near end of the CTA sweep */
  --navy-green:  #1B4A33;   /* the far end of it — navy warmed towards the green */
  --navy-line:   rgb(var(--cream-rgb) / .16);

  /* #4F8F3A carrying white text is 3.95:1, under the 4.5:1 floor, so it cannot
     be the background of anything with a word on it. --green-deep is the same
     hue two steps down and clears it everywhere the site puts green on light:
     5.09:1 on --paper, 4.82:1 on --cream, 4.55:1 on --green-wash. The rule is
     green that carries text uses --green-deep; green that is only a rule, a
     wash or a border uses --green. --green-light is for navy backgrounds only:
     7.1:1 there, and 2.05:1 on white, where it must never appear. */
  --green:        #4F8F3A;
  --green-deep:   #447B32;
  --green-press:  #37642A;   /* the pressed/hover step below --green-deep */
  --green-light:  #86C56E;
  --green-wash:   #EEF4E9;

  --cream:       #F8F9F6;
  --paper:       #FFFFFF;
  --panel:       #FBFCF9;   /* inputs and inner panels — a hair off --paper */
  --ink:         #17324D;
  --muted:       #52667D;   /* 5.9:1 on paper, so it is secondary, not faint */
  --rule:        #E2E7DF;
  --rule-strong: #D2DAC9;   /* input borders, which have to be findable */
  /* mount behind product shots — matched to the photographs' own background so a
     contained image reads as one surface rather than a letterboxed rectangle */
  --shot:        #F2F4F0;

  /* Text on navy, three steps, replacing eight near-identical blue-greys that
     were doing this same job at slightly different values. Lightest first;
     all three clear 4.5:1 on both --navy and --navy-deep. */
  --on-navy:       #D6DFE6;
  --on-navy-soft:  #C3CFD7;
  --on-navy-quiet: #A9BAC6;

  /* One warning family, replacing twelve terracottas spread across the two
     sheets. --warn is the only one that carries text on light (4.9:1 on paper),
     --warn-deep is for text on --warn-wash (6.2:1). */
  --warn:        #B4553F;
  --warn-deep:   #8A4B3C;
  --warn-wash:   #FDF6F4;
  --warn-line:   #E6CFC7;

  /* Channels, so a translucent tint of a brand colour does not have to restate
     that colour as three numbers that then drift. A tint is written
     rgb(var(--green-rgb) / .32) rather than rgba(79, 143, 58, .32).
     Space-separated rgb() with slash alpha is 2018 syntax (Chrome 65, Firefox
     52, Safari 12.1); this sheet already relies on aspect-ratio, which is 2021,
     so nothing here raises the floor. */
  --navy-rgb:        11 42 69;
  --navy-deep-rgb:   7 28 47;
  --green-rgb:       79 143 58;
  --green-light-rgb: 134 197 110;
  --cream-rgb:       248 249 246;
  --paper-rgb:       255 255 255;
  --ink-rgb:         23 50 77;

  /* the brief's names for the same six values */
  --primary-navy:  var(--navy);
  --primary-green: var(--green);
  --light-green:   var(--green-wash);
  --off-white:     var(--cream);
  --white:         var(--paper);
  --text-dark:     var(--ink);

  --serif: "Cinzel", "Trajan Pro", "Cormorant Garamond", "Times New Roman", serif;
  --sans:  "Mulish", "Avenir Next", "Avenir", "Segoe UI", system-ui, -apple-system, sans-serif;
  /* Reference numbers and anything else that has to be read out over the phone
     character by character. */
  --mono:  ui-monospace, "SF Mono", "Cascadia Mono", "Roboto Mono", Menlo, Consolas, monospace;

  --wrap: 1200px;
  --radius: 14px;
  --radius-lg: 22px;
  --shadow-sm: 0 2px 10px rgb(var(--ink-rgb) / .07);
  --shadow:    0 14px 38px rgb(var(--ink-rgb) / .12);
  --shadow-lg: 0 26px 70px rgb(var(--ink-rgb) / .18);
  --ease: cubic-bezier(.22, .61, .36, 1);

  /* ---- spacing ------------------------------------------------------------
     Seven fluid steps, each roughly 1.3x the one below it. Every gap, pad and
     margin on the site comes from here; the numbers below are what they resolve
     to, so a step can be judged without evaluating the clamp in your head.

                  375px    768px   1440px
       --s-1       11       13       16     hairline gaps, icon to label
       --s-2       16       19       25     grid gaps, button rows
       --s-3       22       26       34     card padding, nav gaps
       --s-4       27       33       44     panel padding, inner blocks
       --s-5       34       41       54     block separation
       --s-6       44       53       72     sub-section rhythm
       --s-7       56       71      101     section rhythm

     Below ~450px every step is pinned at its floor, on purpose: spacing that
     keeps shrinking with the viewport is what makes a small phone feel cramped
     rather than compact. */
  --s-1: clamp(.65rem, .5rem  + .7vw,  1rem);
  --s-2: clamp(1rem,   .75rem + .9vw,  1.6rem);
  --s-3: clamp(1.35rem, 1rem  + 1.3vw, 2.15rem);
  --s-4: clamp(1.7rem, 1.25rem + 1.7vw, 2.75rem);
  --s-5: clamp(2.15rem, 1.5rem + 2.2vw, 3.4rem);
  --s-6: clamp(2.75rem, 1.85rem + 3.1vw, 4.5rem);
  --s-7: clamp(3.5rem, 2.2rem + 4.6vw, 6.5rem);

  /* The page gutter, one value instead of two. It used to be 1.3rem a side above
     560px and 0.9rem below it — a gutter that got *narrower* as the screen got
     smaller, which is the wrong direction and cost a media query to express. */
  --gutter: clamp(1rem, .5rem + 1.4vw, 1.6rem);

  /* ---- the sticky header -------------------------------------------------
     Its height is needed in two other places — the scroll offset for in-page
     anchors, and the mobile menu panel — and it was previously a number nobody
     could derive, so those places had guesses in them. Composed here from the
     two things that actually set it: the logo's height and .nav's padding.
     64px on a phone, 78px on a wide desktop. */
  --nav-pad: .8rem;
  --logo-h: clamp(38px, 32px + 1.6vw, 52px);
  --header-h: calc(var(--logo-h) + var(--nav-pad) * 2 + 1px);
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;

  /* This was body{overflow-x:hidden}, which is two faults in one line.
     First: overflow on body makes body a scroll container, and .site-header is
     position:sticky inside body — that is the classic reason a sticky header
     quietly stops sticking. On the root the value propagates to the viewport
     instead, and sticky keeps working.
     Second: `hidden` turns overflow into clipping with no scrollbar, which is
     exactly how the 320px grid overflow stayed invisible. `clip` does the same
     job without creating a scroll container. Either way this rule is what stops
     the eye finding overflow, so verify.py section 13b now has to — the hidden
     line is only the fallback for Safari before 16. */
  overflow-x: hidden;
  overflow-x: clip;

  /* Every in-page jump lands below the sticky header rather than under it. That
     includes the skip link on all five pages: without this, a keyboard user who
     activates "Skip to content" arrives with the first heading hidden. */
  scroll-padding-top: calc(var(--header-h) + var(--s-2));
}

body {
  margin: 0;

  /* Nothing in this sheet used to set this, so any string longer than its box
     spilled out of it. break-word is the floor: an overlong word breaks instead
     of overflowing. It deliberately does NOT change min-content sizing, so it
     cannot rescue a flex item or a grid track — the places that need that get
     overflow-wrap: anywhere individually, and the difference between the two
     values is the whole reason both appear in this file. */
  overflow-wrap: break-word;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  font-weight: 400;
  line-height: 1.68;
  -webkit-font-smoothing: antialiased;
}

/* `height: auto` is the load-bearing half of this rule, and it was missing.
   Every <img> on the site carries width and height attributes so the layout reserves
   space before the file decodes — but an attribute height is a presentational hint, and
   it keeps applying until an author rule overrides it. So any rule that set a width and
   not a height left the attribute height standing, and the image rendered stretched:
   .mark-centred set width:min(420px,80%) against height="443" on a 700x443 file and drew
   the stacked logo 420x443, a 1.67x vertical squeeze. Rules that genuinely want a fixed
   height (.brand img, .footer__logo) are more specific and still win. */
img, svg { display: block; max-width: 100%; height: auto; }
a { color: var(--green-deep); text-decoration: none; }
a:hover { color: var(--green-press); }

:focus-visible {
  outline: 3px solid var(--green);
  outline-offset: 3px;
  border-radius: 4px;
}

/* --------------------------------------------------------- 2. typography */
h1, h2, h3, h4 {
  margin: 0 0 .6em;
  font-weight: 800;
  line-height: 1.14;
  letter-spacing: -.015em;
  color: var(--ink);
}

h1 { font-size: clamp(2.15rem, 1.35rem + 3.1vw, 3.65rem); }
h2 { font-size: clamp(1.75rem, 1.2rem + 2.1vw, 2.7rem); }
h3 { font-size: clamp(1.16rem, 1.02rem + .55vw, 1.4rem); }

p { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }

.lead { font-size: clamp(1.02rem, .97rem + .3vw, 1.16rem); color: var(--muted); }

/* the Cinzel display voice, borrowed from the wordmark */
.display {
  font-family: var(--serif);
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
}

/* small letterspaced label, as under the logo */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  margin: 0 0 1.1rem;
  font-family: var(--serif);
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--green-deep);
}
.eyebrow::before {
  content: "";
  width: 34px;
  height: 2px;
  background: var(--green);
}
.eyebrow.centred { justify-content: center; }
.eyebrow.centred::after {
  content: "";
  width: 34px;
  height: 2px;
  background: var(--green);
}
.on-navy .eyebrow, .eyebrow.light { color: var(--green-light); }

/* Green as a text colour has exactly two safe forms: --green-deep on light and
   --green-light on navy. Plain --green is 3.95:1 on white, which is fine for a
   rule and not fine for a word, so it is not offered as a text utility here. */
.accent { color: var(--green-deep); }
.accent-light { color: var(--green-light); }

/* ------------------------------------------------------------- 3. layout */
.wrap {
  width: min(100% - var(--gutter) * 2, var(--wrap));
  margin-inline: auto;
}

section { position: relative; }
.section { padding: var(--s-7) 0; }
.section--tight { padding: var(--s-6) 0; }
.section--cream { background: var(--cream); }
.section--wash  { background: var(--green-wash); }

.section--navy {
  background: var(--navy);
  color: var(--on-navy);
}
.section--navy h1, .section--navy h2, .section--navy h3 { color: var(--cream); }
.section--navy .lead, .section--navy p { color: var(--on-navy-soft); }

.head {
  max-width: 640px;
  margin: 0 auto var(--s-5);
  text-align: center;
}
.head p { color: var(--muted); }
.section--navy .head p { color: var(--on-navy-soft); }

.grid { display: grid; gap: var(--s-3); }
/* The min() inside minmax() is not decoration. auto-fit collapses the number of
   tracks but never takes a track below its own minimum, so minmax(300px, 1fr) in a
   291px container — which is what a 320px phone gives you — is 9px of overflow that
   body{overflow-x:hidden} then clips instead of showing. min(300px, 100%) keeps the
   wrap point on wide screens and lets the track shrink when it has to. */
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(272px, 100%), 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(216px, 100%), 1fr)); }

.split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: var(--s-6);
  align-items: center;
}

/* ------------------------------------------------------------ 4. buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .55rem;
  padding: .95rem 1.75rem;
  border: 2px solid transparent;
  border-radius: 999px;
  font-family: var(--sans);
  font-size: .93rem;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform .25s var(--ease), background .25s, color .25s, border-color .25s, box-shadow .25s;
}
.btn:hover { transform: translateY(-2px); }

.btn--primary {
  background: var(--green-deep);
  border-color: var(--green-deep);
  color: var(--paper);
  box-shadow: 0 10px 26px rgb(var(--green-rgb) / .32);
}
.btn--primary:hover { background: var(--green-press); border-color: var(--green-press); color: var(--paper); }

.btn--navy { background: var(--navy); border-color: var(--navy); color: var(--cream); }
.btn--navy:hover { background: var(--navy-deep); border-color: var(--navy-deep); color: var(--paper); }

.btn--ghost { border-color: rgb(var(--cream-rgb) / .5); color: var(--cream); }
.btn--ghost:hover { background: var(--cream); border-color: var(--cream); color: var(--navy); }

.btn--outline { border-color: var(--navy); color: var(--navy); }
.btn--outline:hover { background: var(--navy); color: var(--cream); }

.btn-row { display: flex; flex-wrap: wrap; gap: .85rem; }

/* --------------------------------------------------- 5. topbar + header */
.topbar {
  background: var(--navy-deep);
  color: var(--on-navy-soft);
  font-size: .82rem;
  letter-spacing: .02em;
}
.topbar__in {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .4rem 1.6rem;
  padding: .58rem 0;
}
/* `anywhere`, not `break-word`. These four are all flex items or grid items and
   all of them can hold sofveramanufactures@gmail.com, which has no space and no
   hyphen and therefore no break opportunity at all. Only `anywhere` reduces the
   min-content contribution, and reducing it is the point: with break-word the
   track still reserves the full 253px the word wants and the layout still grows.
   The email is the case in hand; a longer one, or a URL, would behave the same. */
.topbar__meta a,
.footer__col li,
.contact-card p,
.contact-list li a,
.contact-list li span { overflow-wrap: anywhere; }

.topbar a { color: var(--on-navy); }
.topbar a:hover { color: var(--green-light); }
.topbar__meta { display: flex; flex-wrap: wrap; gap: .3rem 1.5rem; }
.topbar__meta span, .topbar__meta a { display: inline-flex; align-items: center; gap: .45rem; }
.topbar__claim {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-family: var(--serif);
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--green-light);
}
@media (max-width: 780px) { .topbar__claim { display: none; } }
@media (max-width: 520px) { .topbar { font-size: .76rem; } }

.site-header {
  position: sticky;
  top: 0;
  z-index: 90;
  background: rgb(var(--paper-rgb) / .94);
  backdrop-filter: saturate(180%) blur(10px);
  border-bottom: 1px solid var(--rule);
  transition: box-shadow .3s;
}
.site-header.is-stuck { box-shadow: var(--shadow-sm); }

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.4rem;
  padding: var(--nav-pad) 0;
  min-width: 0;
}
.brand { display: inline-flex; align-items: center; flex: 0 0 auto; }
/* Height-bound, because --header-h is composed from --logo-h and the header's height
   is what the sticky offset and the mobile panel both quote. That makes this the one
   image on the site whose height is deliberately not auto, so it has to say what
   happens if its box ever ends up the wrong shape: object-fit:contain letterboxes the
   mark instead of stretching it. It is safe today only because .brand is flex:0 0 auto
   and cannot be squeezed — which is a fact about a different rule, and not the sort of
   thing to leave a logo's proportions resting on. */
.brand img { height: var(--logo-h); width: auto; object-fit: contain; }

.nav__links {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
  padding: 0;
  list-style: none;
  min-width: 0;
}
.nav__links a,
.nav__links button,
.nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The exact Request a Quote box: matching height (.78rem vertical padding),
     the wide pill radius, and a min-width so short labels (HOME) still read as a
     wide, equal-sized pill rather than a tight chip. */
  /* Horizontal padding and the equal-pill min-width both hold their full desktop
     value at >=1200px (each clamp reaches its max exactly at 1200px, so the pills
     are pixel-identical to the original there and above) and ease DOWN only across
     the 1025-1199px laptop band, so the four tabs + CTA keep fitting the bar instead
     of overrunning it before the mobile menu takes over at <=1024px. At 1024px each
     is back at its low end (~16px / ~82px). Vertical padding is untouched, so the
     pill height, the 999px radius and the green glow are all unchanged. */
  padding: .78rem clamp(1rem, 3.6vw - 1.3rem, 1.4rem);
  min-width: clamp(80px, 8vw, 96px);
  margin: 0 .2rem;
  background: var(--green-deep);
  /* Thin rim, always 2px, the same green as the fill so the box never grows on
     hover/active and neighbours never shift. The whole nav now matches the green
     Request-a-Quote / private-label button the design leads with. */
  border: 2px solid var(--green-deep);
  border-radius: 999px;
  /* Subtle resting shadow, the green-tinted echo of that button's shadow. */
  box-shadow: 0 4px 14px rgb(var(--green-rgb) / .3);
  font-family: inherit;
  font-size: .82rem;
  font-weight: 800;
  line-height: inherit;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--paper);
  cursor: pointer;
  /* Everything the interaction animates rides one 300ms ease-in-out curve:
     fill, text colour, rim, glow and the 2px lift. */
  transition: background .3s ease-in-out, color .3s ease-in-out,
              border-color .3s ease-in-out, box-shadow .3s ease-in-out,
              transform .3s ease-in-out;
  transform-origin: center;
  text-decoration: none;
}
/* Hover and keyboard focus share one look: the fill lifts to a lighter navy, the
   rim brightens to the running-light blue, a soft navy glow appears and the tab
   rises 2px. All four ride the same 300ms ease-in-out declared on the base rule. */
.nav__links a:hover,
.nav__links button:hover,
.nav__link:hover,
.nav__links a:focus-visible,
.nav__links button:focus-visible,
.nav__link:focus-visible {
  background: var(--green-press);
  border-color: var(--green-press);
  color: var(--paper);
  box-shadow: 0 8px 20px rgb(var(--green-rgb) / .4);
  transform: translateY(-2px);
}
/* Selected tab: the inside flips to white with dark-navy text for clear contrast,
   and it holds that state (server-rendered aria-current) until navigation lands on
   another tab. The white fill still masks the ::before, so only the running light
   shows as a rim. */
.nav__links a[aria-current="page"],
.nav__links button[aria-current="page"],
.nav__links a.is-active,
.nav__links button.is-active {
  background: var(--paper);
  border-color: rgb(var(--green-light-rgb) / .6);
  color: var(--navy);
  box-shadow: 0 4px 14px rgb(var(--green-rgb) / .35);
}
/* Hovering or keyboard-focusing the already-selected tab must NOT flip it back to
   navy — it stays white with navy text, only lifting 2px with a brighter rim/glow.
   Higher specificity than the generic :hover so it wins without touching dimensions. */
.nav__links a[aria-current="page"]:hover,
.nav__links button[aria-current="page"]:hover,
.nav__links a[aria-current="page"]:focus-visible,
.nav__links button[aria-current="page"]:focus-visible,
.nav__links a.is-active:hover,
.nav__links button.is-active:hover {
  background: var(--paper);
  color: var(--navy);
  border-color: rgb(var(--green-light-rgb) / .85);
  transform: translateY(-2px);
  box-shadow: 0 8px 20px rgb(var(--green-rgb) / .4), 0 0 16px rgb(var(--green-rgb) / .5);
}
/* Press settles the tab back down for a tactile "click" without resizing it. */
.nav__links a:active,
.nav__links button:active,
.nav__link:active {
  transform: translateY(0) scale(0.98);
  box-shadow: 0 3px 10px rgb(var(--green-rgb) / .35);
}
/* Keep a visible focus ring for keyboard users on top of the hover look above. */
.nav__links a:focus-visible,
.nav__links button:focus-visible,
.nav__link:focus-visible {
  outline: 2px solid var(--green-light);
  outline-offset: 4px;
  border-radius: 999px;
}
.nav__links a::after,
.nav__links button::after,
.nav__link::after { display: none; }

/* ---- active tab: a green light running around the pill ------------------
   Driven purely by [aria-current="page"] (each page ships its own active tab),
   so the light lives on exactly one tab, stays there until navigation lands on
   another page, and defaults to HOME on index. click / Enter / Space all fire a
   <button> click and navigate, so keyboard and pointer behave identically.

   The light is a conic-gradient on a ::before that sits *behind* the pill
   (z-index:-1) and overflows it by 3px. The pill's own opaque fill masks the
   centre, leaving only a 3px animated rim — so the effect never covers the text
   and never changes the button's box. A registered <angle> custom property lets
   the cone rotate smoothly; browsers without @property just show it static. */
@property --nav-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
@keyframes nav-run { to { --nav-angle: 360deg; } }

.nav__links a[aria-current="page"]::before,
.nav__links button[aria-current="page"]::before,
.nav__links a.is-active::before,
.nav__links button.is-active::before {
  content: "";
  position: absolute;
  inset: -3px;
  z-index: -1;                 /* behind the pill's fill → only the rim shows */
  border-radius: inherit;      /* keeps the running light pill-shaped */
  --nav-angle: 0deg;           /* fallback for engines without @property */
  background: conic-gradient(from var(--nav-angle),
    transparent 0deg,
    transparent 250deg,
    rgb(var(--green-rgb) / .55) 300deg,
    var(--green-light) 335deg,
    rgb(var(--green-rgb) / .55) 350deg,
    transparent 360deg);
  animation: nav-run 2s linear infinite;
  pointer-events: none;
}
/* Subtle matching blue glow around the active tab. Same specificity as the
   active rule and placed after it, so this box-shadow wins; the mobile block
   below resets it. */
.nav__links a[aria-current="page"],
.nav__links button[aria-current="page"],
.nav__links a.is-active,
.nav__links button.is-active {
  box-shadow: 0 4px 14px rgb(var(--green-rgb) / .4), 0 0 16px rgb(var(--green-rgb) / .5);
}
/* Reduced motion: hold the highlight still as a full green rim, no rotation. */
@media (prefers-reduced-motion: reduce) {
  .nav__links a[aria-current="page"]::before,
  .nav__links button[aria-current="page"]::before,
  .nav__links a.is-active::before,
  .nav__links button.is-active::before {
    animation: none;
    background: rgb(var(--green-rgb) / .85);
  }
}
/* In the mobile drawer the tabs are plain full-width rows, not pills, so the
   running rim would be meaningless there — drop it. */
@media (max-width: 1024px) {
  .nav__links a[aria-current="page"]::before,
  .nav__links button[aria-current="page"]::before,
  .nav__links a.is-active::before,
  .nav__links button.is-active::before { display: none; }
}

/* Two Request a Quote buttons exist: one inside .nav__links for the mobile drawer,
   one in .nav__cta for the header bar. Show only the one the current layout uses. */
.nav__links .nav__cta-item { display: none; }
.nav__cta { flex: 0 0 auto; }
.nav__cta .btn { padding: .78rem 1.4rem; font-size: .82rem; }
/* The header CTA matches the navy tab family (only here — every other
   Request a Quote across the site keeps its green .btn--primary). Its blue rim
   and hover glow mirror the active tab so the whole bar reads as one set. */
.nav__cta .btn--primary {
  background: #E8A33D;
  border-color: #E8A33D;
  color: var(--navy);
  box-shadow: 0 4px 14px rgb(232 163 61 / .35);
}
.nav__cta .btn--primary:hover,
.nav__cta .btn--primary:focus-visible {
  background: #D8912B;
  border-color: #D8912B;
  color: var(--navy);
  box-shadow: 0 8px 20px rgb(232 163 61 / .45);
}

.burger {
  display: none;
  width: 46px; height: 44px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 10px;
  cursor: pointer;
}
.burger span {
  display: block;
  width: 22px; height: 2px;
  margin: 4px auto;
  background: var(--navy);
  border-radius: 2px;
  transition: transform .3s var(--ease), opacity .2s;
}
.burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 1024px) {
  .burger { display: block; }
  .nav__cta { display: none; }
  .nav__links {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(320px, 86vw);
    flex-direction: column;
    align-items: flex-start;
    gap: .2rem;
    /* Clears the sticky header instead of guessing at it. This was 5.6rem — 89.6px,
       against a header that is 65px on a phone and 74px at this breakpoint, so the gap
       under the bar was somewhere between 16 and 25px depending on the screen, which is
       not a decision anybody made. */
    padding: calc(var(--header-h) + var(--s-2)) var(--s-4) var(--s-4);
    background: var(--navy);
    box-shadow: var(--shadow-lg);
    transform: translateX(105%);
    /* visibility, not just transform. Off-screen via transform is invisible but still
       focusable, so tabbing past the burger used to walk into links nobody could see —
       a focus order fault, not a cosmetic one. visibility animates as a step function:
       going to `visible` it flips at once, so the panel slides in; going to `hidden` it
       holds until the transition ends, so the panel slides out. Both still animate. */
    visibility: hidden;
    transition: transform .38s var(--ease), visibility .38s var(--ease);
    overflow-y: auto;
  }
  .nav__links.is-open { transform: translateX(0); visibility: visible; }
  .nav__links a,
  .nav__links button,
  .nav__link {
    background: transparent;
    border-color: transparent;
    border-radius: 0;
    color: var(--cream);
    font-size: 1rem;
    padding: .8rem .15rem;
    box-shadow: none;
  }
  .nav__links a[aria-current="page"],
  .nav__links button[aria-current="page"],
  .nav__links a.is-active,
  .nav__links button.is-active,
  .nav__links .is-active {
    color: var(--green-light);
    box-shadow: none;
  }
  .nav__links a:hover,
  .nav__links button:hover,
  .nav__link:hover {
    background: transparent;
    border-color: transparent;
    color: var(--green-light);
  }
  .nav__links li { width: 100%; border-bottom: 1px solid var(--navy-line); }
  .nav__links .nav__cta-item { display: block; }
  .nav__links .btn { margin-top: 1.4rem; width: 100%; }
}

.scrim {
  position: fixed;
  inset: 0;
  z-index: 80;
  background: rgb(var(--navy-deep-rgb) / .55);
  opacity: 0;
  visibility: hidden;
  transition: opacity .3s, visibility .3s;
}
.scrim.is-open { opacity: 1; visibility: visible; }

/* --------------------------------------------------------------- 6. hero */
.hero {
  position: relative;
  background: var(--navy);
  color: var(--on-navy);
  overflow: hidden;
}
.hero::before {
  /* soft green glow, echoing the poster's curved sweep */
  content: "";
  position: absolute;
  top: -32%; right: -14%;
  width: 58%; aspect-ratio: 1;
  background: radial-gradient(circle, rgb(var(--green-light-rgb) / .34), transparent 68%);
  pointer-events: none;
}
.hero__in {
  position: relative;
  display: grid;
  /* minmax(0, …) and not a bare `1.08fr .92fr`. `Nfr` means minmax(auto, Nfr),
     and an auto minimum will not go below the item's min-content width — so one
     long unbreakable word inside a column silently widens that column past its
     share and pushes the grid wider than .wrap. Explicit 0 is the only way to
     say "this track really is allowed to be as narrow as its share". */
  grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr);
  gap: var(--s-6);
  align-items: center;
  padding: var(--s-7) 0 var(--s-7);
}
@media (max-width: 900px) { .hero__in { grid-template-columns: minmax(0, 1fr); } }

.hero h1 { color: var(--cream); margin-bottom: .5em; }
.hero h1 .line2 { display: block; color: var(--green-light); }
.hero__text p { color: var(--on-navy-soft); max-width: 46ch; }
.hero__rule {
  width: 96px; height: 3px;
  margin: 0 0 1.6rem;
  background: linear-gradient(90deg, var(--green-light), rgb(var(--green-light-rgb) / 0));
  border: 0;
}
.hero .btn-row { margin-top: 2rem; }

.hero__art { position: relative; }
/* The video and both stills share the card's shape and chrome. The animated
   reveal fills the card (width/height 100%, object-fit: contain) so it sits
   centred on white and never crops, exactly as the photograph did. */
.hero__frame > img,
.hero__video {
  width: 100%;
  /* 4/3, matching brand-hero-photo.jpg exactly (1200x900 after
     tools/normalise-frames.py). It used to be 4/5 with a 16/11 override below 900px,
     which was right for the cream plate that first sat here and wrong for what
     replaced it: a 1200x803 photograph, r=1.494, letterboxed by 47% of the box height
     against a --cream ground it did not share. The old comment here even predicted it
     — "switch back if a photograph goes back in here" — which is worth remembering as
     a general lesson: a ratio hard-coded to one file is a fault waiting for the next
     file. The box and the file are now the same shape at every width, so `contain`
     never letterboxes and the background below never shows. */
  aspect-ratio: 4 / 3;
  object-fit: contain;
  /* --paper, not --cream: this only shows if a differently-shaped image is dropped in,
     and every plate in this project is built on white. */
  background: var(--paper);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}
/* height: 100% so the video always fills the aspect-ratio box; object-fit:
   contain then centres the frame without cropping. display: block kills the
   inline-baseline gap under replaced elements. */
.hero__video { height: 100%; display: block; }
/* The reduced-motion / fallback still is out of the flow until it is needed —
   the video is the default experience. It does not just get display:none, or it
   could not animate in; it is simply not rendered until the swap below. */
.hero__still { display: none; }
/* Honour prefers-reduced-motion: drop the moving video entirely and show the
   original static logo image in its place. This is the reduced-motion fallback
   the brief asks for; main.js also hard-stops the video for older engines whose
   autoplay ignores the query. */
@media (prefers-reduced-motion: reduce) {
  .hero__video { display: none; }
  .hero__still { display: block; }
}
.hero__art::after {
  content: "";
  position: absolute;
  inset: auto -14px -14px auto;
  width: 62%; height: 58%;
  border: 2px solid rgb(var(--green-light-rgb) / .5);
  border-radius: var(--radius-lg);
  z-index: -1;
}

/* ---------------------------------------------------------------- hero card rim
   A moving conic-gradient border around the hero video card, built the same way
   as the navigation's running-light: an @property <angle> that a keyframe drives
   0->360deg, fed into conic-gradient(from ...). Two pseudo layers on the
   .hero__frame wrapper share one --hero-angle so they rotate in lockstep:
     ::before  the crisp 3px rim. A mask keeps only the band OUTSIDE the card, so
               the gradient reads as a border and can never resize, crop, or cover
               the video whatever the card holds.
     ::after   a blurred, dimmed copy tucked behind the card; only its bloom past
               the edge shows, giving the green-and-gold glow that travels with
               the bright arc.
   The card itself — size, position, white ground, radius, shadow — is untouched. */
@property --hero-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
@keyframes hero-frame-spin { to { --hero-angle: 360deg; } }

.hero__frame {
  position: relative;
  /* Own stacking context: pins the -1 glow layer just behind the card instead of
     letting it fall behind the navy hero section, and stops any bleed reaching
     the header. */
  isolation: isolate;
}
/* Shared paint for both layers. `from var(--hero-angle)` spins the whole wheel;
   navy sits at BOTH 0deg and 360deg so the loop is seamless — no seam, no jump.
   The plain `--hero-angle: 0deg` is the floor for engines that ignore @property:
   they still render a valid static gradient, just without motion. */
.hero__frame::before,
.hero__frame::after,
.art--badge::before,
.art--badge::after,
.card__media::before,
.card__media::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: calc(var(--radius-lg) + 3px);
  --hero-angle: 0deg;
  background: conic-gradient(from var(--hero-angle),
    var(--navy)        0deg,
    var(--green-press) 58deg,
    var(--green-light) 122deg,
    #E8A33D            186deg,
    #EAF7E1            244deg,
    var(--green-press) 300deg,
    var(--navy)        360deg);
  animation: hero-frame-spin 3s linear infinite;
  pointer-events: none;
}
/* The rim: mask = border-box XOR content-box, leaving only the 3px padding band,
   which (with inset:-3px + padding:3px) lands exactly outside the card edge. */
.hero__frame::before,
.art--badge::before,
.card__media::before {
  padding: 3px;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}
/* The glow: same wheel, blurred and dimmed, behind the card so only the bloom
   just past the edge is seen — refined, not neon. */
.hero__frame::after,
.art--badge::after,
.card__media::after {
  z-index: -1;
  filter: blur(9px);
  opacity: .5;
}
@media (prefers-reduced-motion: reduce) {
  /* Freeze both layers at --hero-angle:0deg -> a static green-and-gold gradient
     border, no motion, as the brief requires. */
  .hero__frame::before,
  .hero__frame::after,
  .art--badge::before,
  .art--badge::after,
  .card__media::before,
  .card__media::after { animation: none; }
}
@media (max-width: 900px) {
  /* No aspect-ratio override here any more. It used to become 16/11 on one column,
     which is a third ratio for the same file to be wrong against; one shape at every
     width is both simpler and what "consistent across desktop, tablet and mobile"
     actually means. */
  .hero__frame > img,
  .hero__video { border-radius: var(--radius); }
  /* Keep the animated rim + glow concentric with the smaller mobile card radius. */
  .hero__frame::before,
  .hero__frame::after { border-radius: calc(var(--radius) + 3px); }
}

/* the four circular pillars from the poster */
.pillars {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-2);
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--navy-line);
}
/* repeat(2, minmax(0, 1fr)) to match the base rule. This override used to read
   repeat(2, 1fr) — the guard was in the four-column rule above and this line
   dropped it, which is the worst place to lose one: the rule a reader checks is
   the base rule, and it was correct. */
@media (max-width: 560px) {
  .pillars { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 1.6rem; }
}

.pillar {
  text-align: center;
  /* Pointer-over zoom: the whole card (icon + label) scales up together. Using a
     transform — not font-size/width changes — keeps the fonts and colours untouched,
     and transforms are painted rather than laid out, so enlarging one pillar never
     reflows the grid or nudges its three neighbours. */
  transition: transform .3s cubic-bezier(.2, .7, .3, 1);
  transform-origin: center;
}
/* Each badge sits in an equal-sized circle with a rotating gold rim. Structure
   (the suggested approach): .pillar__fig is the outer circle; its ::before paints
   the animated conic-gradient as a full disc and its ::after paints the same wheel
   blurred as a glow; the <img> is a concentric circle 3px smaller laid ON TOP, so
   the gradient only ever shows as the 3px ring and never covers the artwork. The
   art is a square 512x512 seal, so object-fit fills the 1:1 box with no crop or
   distortion. @property makes the conic angle animatable for a seamless spin (same
   running-light trick as the nav tabs and hero card); engines without @property
   hold a static metallic-gold gradient, which is also the reduced-motion fallback. */
@property --pillar-angle { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
@keyframes pillar-spin { to { --pillar-angle: 360deg; } }

.pillar__fig {
  --pillar-size: 92px;            /* desktop; equal width AND height for all four */
  width: var(--pillar-size);
  height: var(--pillar-size);
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  display: grid;
  place-items: center;            /* centres the inner image circle exactly */
  position: relative;
  isolation: isolate;             /* own stacking context so the layers order cleanly */
  margin: 0 auto var(--s-1);      /* keeps the label centred below at the same rhythm */
}
/* mobile: smaller circles. Placed AFTER the base rule (not up with the .pillars grid
   override) so it wins at equal specificity — otherwise the unconditional 92px above
   would override it and mobile would stay desktop-sized. */
@media (max-width: 560px) {
  .pillar__fig { --pillar-size: 76px; }
}
/* the rotating gold wheel — bronze -> metallic gold -> champagne -> bright gold ->
   bronze (bronze at 0 AND 360 = a seamless loop). ::before is the crisp ring, ::after
   the glow; both share the one moving angle so the halo tracks the bright arc. */
.pillar__fig::before,
.pillar__fig::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  --pillar-angle: 0deg;           /* floor for engines without @property (static rim) */
  background: conic-gradient(from var(--pillar-angle),
    #6A4A1E 0deg, #BD8A2C 90deg, #F2E4BC 180deg, #FFD75E 270deg, #6A4A1E 360deg);
  animation: pillar-spin 3s linear infinite;
  pointer-events: none;
}
.pillar__fig::before { inset: 0; z-index: 1; }
.pillar__fig::after  { inset: -4px; z-index: 0; filter: blur(7px); opacity: .42; }
/* the inner clip circle: 3px smaller than the rim all round, with overflow:hidden so
   the zooming badge is always clipped to the round shape and never spills over the
   golden border. z-index lifts it above the ::before ring. */
.pillar__media {
  position: relative;
  z-index: 2;
  display: block;
  width: calc(100% - 6px);        /* 3px gold ring all round (fig size - 2*3px) */
  height: calc(100% - 6px);
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  overflow: hidden;
}
/* subtle continuous breathing zoom on the badge ONLY — the circle, rim, glow and
   label all stay put. 1 -> 1.08 -> 1 over a slow 4s ease-in-out; the held keyframe
   pairs (0-10%, 45-55%, 90-100%) give a gentle pause at the min and max instead of a
   hard turnaround. object-fit:contain keeps the whole badge, no crop or distortion. */
.pillar__media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  transform: scale(1);
  animation: pillar-zoom 4s ease-in-out infinite;
  will-change: transform;
}
@keyframes pillar-zoom {
  0%, 10%   { transform: scale(1); }
  45%, 55%  { transform: scale(1.08); }
  90%, 100% { transform: scale(1); }
}
/* stagger the four by ~0.3s so they breathe out of phase, not in lockstep */
.pillar:nth-child(1) .pillar__media img { animation-delay: 0s; }
.pillar:nth-child(2) .pillar__media img { animation-delay: .3s; }
.pillar:nth-child(3) .pillar__media img { animation-delay: .6s; }
.pillar:nth-child(4) .pillar__media img { animation-delay: .9s; }
@media (prefers-reduced-motion: reduce) {
  /* freeze the rim at --pillar-angle:0deg (a static metallic-gold border) AND stop the
     badge zoom -> fully static images with a static gold border */
  .pillar__fig::before, .pillar__fig::after, .pillar__media img { animation: none; }
}
/* the label only (last child) — scoped so it does NOT leak onto the .pillar__fig /
   .pillar__media wrapper spans, whose display it would otherwise override */
.pillar > span:last-child {
  display: block;
  font-size: .74rem;
  font-weight: 800;
  letter-spacing: .13em;
  line-height: 1.45;
  text-transform: uppercase;
  color: var(--on-navy);
}
/* The zoom itself — cursor over the card grows it 20%. z-index lifts the enlarged
   card cleanly above its neighbours. */
.pillar:hover {
  transform: scale(1.2);
  position: relative;
  z-index: 2;
}
/* Motion-sensitive visitors still get the size change, just without the animated
   scaling (the transform snaps instead of easing). Covers the badges and both logos. */
@media (prefers-reduced-motion: reduce) {
  .pillar, .brand, .footer__logo { transition: none; }
}

/* ---------------------------------------------------------- 7. trust bar */
.trust {
  background: var(--cream);
  border-bottom: 1px solid var(--rule);
}
.trust__in {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr));
  gap: var(--s-3);
  padding: var(--s-4) 0;
}
.trust__item {
  display: flex;
  align-items: center;
  gap: 1rem;
  justify-content: center;
}
.trust__item svg { flex: 0 0 auto; width: 40px; height: 40px; }
.trust__item strong {
  display: block;
  font-size: .82rem;
  font-weight: 800;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--navy);
}
.trust__item small { color: var(--muted); font-size: .84rem; }

/* --------------------------------------------------------------- 8. cards */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: transform .32s var(--ease), box-shadow .32s, border-color .32s;
}
.card:hover { transform: translateY(-6px); box-shadow: var(--shadow); border-color: rgb(var(--green-rgb) / .26); }

/* One frame, twelve photographs, no letterboxing — and the padding is what makes that
   possible rather than the ratio.

   `aspect-ratio` applies to the border box, and `box-sizing` is border-box throughout, so
   14px of even padding leaves a *content* box of (W-28) x (0.75W-28). That is not 4:3 —
   at a 276px card it is 1.386 — so a 4:3 photograph inside it is height-limited and
   leaves ~5px of --shot down each side. Small, but it is the same fault the whole pass is
   about, and it scales with the padding.

   The fix is arithmetic, not a magic number. For a content box to keep the border box's
   ratio R, the vertical padding must be the horizontal padding divided by R:

       (W - 2px) / (W/R - 2py) = R   =>   py = px / R

   R here is 4/3, so py = 14 x 3/4 = 10.5px. The same identity gives .art--wide 7.5px and
   .art--tall 12.5px below. tools/verify.py checks it, because the day someone rounds this
   to 14px the grey comes back and nothing errors.

   The shots themselves are now all 4:3 masters (tools/normalise-frames.py), so `contain`
   fills the mount exactly and cropping is impossible by construction. `contain` stays
   declared rather than switched to `cover`: it is what guarantees that a future
   differently-shaped file is shown whole instead of silently trimmed. */
.card__media {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  padding: 10.5px 14px;
  background: var(--shot);
  border-bottom: 1px solid var(--rule);
  overflow: hidden;
  /* Own stacking context so the -1 glow layer sits just above the --shot ground
     (framing the image) instead of falling behind the whole card. */
  isolation: isolate;
}
/* Card images breathe with their OWN keyframe (card-zoom), independent of the pillar
   badges' pillar-zoom — same gentle 1 -> 1.08 -> 1 motion, but decoupled so the cards
   and the pillars are never tied together. */
@keyframes card-zoom {
  0%, 10%   { transform: scale(1); }
  45%, 55%  { transform: scale(1.08); }
  90%, 100% { transform: scale(1); }
}
.card__media img {
  position: relative;
  z-index: 1;                 /* above the glow (::after, z-index:-1), below the rim (::before) */
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  /* Subtle continuous breathing zoom on the image ONLY — the mount, rim and glow stay
     put (card-zoom: 1 -> 1.08 -> 1, 4s ease-in-out, held at the min/max). .card__media's
     overflow:hidden keeps the growth clipped inside. */
  transform: scale(1);
  animation: card-zoom 4s ease-in-out infinite;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .card__media img { animation: none; }   /* static image beside the frozen rim */
}

/* The Sofvera fibre-plate photo is a true 4:3 product shot meant to fill its mount
   edge-to-edge — a full-bleed `cover` rather than the contained default above — with
   object-position:center keeping the centred Sofvera logo clear of the ~0.15% crop.
   Scoped to .media-cover so ONLY the plate cards change; every other product image
   keeps `contain`. The card-zoom breathing (from .card__media img) still applies, and
   the reduced-motion freeze above still matches it (.media-cover is a .card__media img). */
.card__media img.media-cover {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Moving gradient border on the product image. .card__media::before/::after are
   appended to the hero card rim groups above, so each image mount runs the identical
   --hero-angle / hero-frame-spin conic wheel (same colours, 3s linear, 3px rim, blur-9
   glow, reduced-motion freeze). The hero paints inset:-3px OUTSIDE its card; this mount
   clips its content (overflow:hidden) and sits flush to the card's top edge, so the rim
   is pulled to the inner edge (inset:0) and rounded only on top to follow the card. */
.card__media::before,
.card__media::after {
  inset: 0;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.card__media::before { z-index: 2; }   /* crisp rim, above the zooming image */
.card__media::after  { opacity: .42; } /* glow eased back a touch over the light --shot */

/* ---- Category-filtered view (any tab EXCEPT All Products) --------------------
   Reuses the existing .is-active state on the filter buttons — no JS change. When a
   specific category tab is active (Tissue / Rolls / Napkins / Cups & Tableware /
   Hygiene & Wiping), .wrap:has() matches and the product images sit STATIC and flush
   inside the card border: no breathing zoom, no scale/transform, no reflection glow.
   Each 4:3 master fills its 4:3 mount edge-to-edge (cover => no crop, no gap), stays
   centred and scales with the card (responsive). The All Products view is NOT matched,
   so it keeps the original zoom + glow + sizing exactly as-is. */
.wrap:has(.filters button.is-active:not([data-filter="all"])) .card__media {
  padding: 0;                  /* image sits flush to the rounded border — no --shot gap */
}
.wrap:has(.filters button.is-active:not([data-filter="all"])) .card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;           /* fills the bordered area; 4:3-in-4:3 => nothing cropped */
  object-position: center;     /* stays centred */
  transform: none;             /* no scale */
  animation: none;             /* no zoom in / zoom out */
}
.wrap:has(.filters button.is-active:not([data-filter="all"])) .card__media::after {
  content: none;               /* reflection glow off for the filtered categories */
}

.card__tag {
  position: absolute;
  z-index: 3;                 /* above the image and the rim */
  top: .9rem; left: .9rem;
  padding: .3rem .8rem;
  background: rgb(var(--navy-rgb) / .88);
  border-radius: 999px;
  color: var(--cream);
  font-size: .68rem;
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
}

.card__body { padding: 1.5rem 1.5rem 1.7rem; }
.card__body h3 { margin-bottom: .5rem; }
.card__body p { color: var(--muted); font-size: .95rem; margin-bottom: 0; }

.specs {
  margin: 1rem 0 0;
  padding: 1rem 0 0;
  border-top: 1px dashed var(--rule);
  list-style: none;
  display: grid;
  gap: .4rem;
}
.specs li {
  display: flex;
  gap: .6rem;
  font-size: .87rem;
  color: var(--muted);
}
.specs li::before { content: "—"; color: var(--green-deep); flex: 0 0 auto; }

/* feature card, no image */
.feature {
  padding: var(--s-3);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  transition: transform .3s var(--ease), box-shadow .3s;
}
.feature:hover { transform: translateY(-5px); box-shadow: var(--shadow); }
.feature__ico {
  display: grid;
  place-items: center;
  width: 54px; height: 54px;
  margin-bottom: 1.1rem;
  background: var(--green-wash);
  border: 1px solid rgb(var(--green-rgb) / .22);
  border-radius: 50%;
  color: var(--green-deep);
}
.feature__ico svg { width: 26px; height: 26px; }
.feature p { color: var(--muted); font-size: .95rem; }
.feature--navy {
  background: rgb(var(--paper-rgb) / .045);
  border-color: var(--navy-line);
}
.feature--navy .feature__ico {
  background: rgb(var(--green-light-rgb) / .14);
  border-color: rgb(var(--green-light-rgb) / .34);
  color: var(--green-light);
}
.feature--navy h3 { color: var(--cream); }
.feature--navy p { color: var(--on-navy-soft); }

/* numbered value card */
.value { position: relative; padding-left: 3.6rem; }
.value__no {
  position: absolute;
  left: 0; top: -.15rem;
  font-family: var(--serif);
  font-size: 1.45rem;
  font-weight: 700;
  color: var(--green);
}
.section--navy .value__no { color: var(--green-light); }
.value h3 { margin-bottom: .35rem; }
.value p { color: var(--muted); font-size: .95rem; margin: 0; }
.section--navy .value p { color: var(--on-navy-soft); }

/* ---------------------------------------------------------- 9. media art */
/* Framed photographs beside body copy.
   The frame rules match the direct child image only. As plain descendant selectors
   they also hit the logo mark inside .art__badge, which then inherited the frame's
   aspect-ratio and object-fit:cover and rendered as a cropped vertical sliver. */
.art { position: relative; }
.art > img {
  display: block;
  width: 100%;
  /* Horizontal padding only. The vertical half belongs to the ratio variants below,
     because py = px / R and R differs between them — see .card__media for the identity.
     Putting an even `padding: 10px` here is what left ~3.5px of --shot down each side of
     every panel. */
  padding-left: 10px;
  padding-right: 10px;
  background: var(--shot);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  object-fit: contain;
}
/* 4/5, which brand-tall.jpg already is exactly (1000x1250). py = 10 x 5/4. */
.art--tall > img { aspect-ratio: 4 / 5; padding-top: 12.5px; padding-bottom: 12.5px; }
/* 4/3 rather than 3/2, and not for its own sake: product-napkin-pack.jpg is used both as
   a card and as an .art--wide panel, so those two boxes have to agree or that one file
   cannot fit both. Matching them on 4/3 also means the site has exactly two photograph
   ratios instead of four. py = 10 x 3/4. */
.art--wide > img { aspect-ratio: 4 / 3; padding-top: 7.5px; padding-bottom: 7.5px; }
/* Circular brand medallion (about + contact only) — now wearing the SAME moving
   gradient rim as the hero video card. .art--badge is appended to the hero
   ::before/::after selector groups (see the "hero card rim" block above), so it
   reuses the identical gradient colours, 3s speed, direction, 3px thickness, blur-9
   glow and easing via --hero-angle / hero-frame-spin; only the radius is overridden
   to 50% here for a circle. The badge art is a circular lockup on #F5EFE8 paper, so
   object-fit:contain fits the whole thing in a 1/1 box with no crop or distortion,
   and .badge-clip (overflow:hidden + border-radius:50%) keeps the inner breathing
   zoom from ever spilling past the rim. Only the image scales — the circle, rim and
   glow hold still. Width follows the .split column (which collapses to one column on
   narrow screens), so it stays perfectly circular and in proportion on desktop,
   tablet and mobile. Distinct from .art__badge, the caption plaque on index. */
/* Two fixed layers + one animated image:
   .art--badge      = the outer border. A single thin, STATIONARY circle. It never
                      transforms, scales, pulses or animates — no keyframes, no
                      spinning gradient rim, no blurred glow. It just frames.
   .badge-clip      = the inner circular viewport. Clips whatever moves inside it.
   .art--badge img  = the ONLY thing that zooms — a gentle image-only breathing
                      zoom (badge-breathe, 4s, ease-in-out, infinite, 1 -> 1.04). */
.art--badge {
  aspect-ratio: 1 / 1;                /* mathematically square -> the medallion is a true circle */
  border-radius: 50%;
  box-sizing: border-box;             /* the 4px gap lives INSIDE the square, so w == h stays exact */
  width: 100%;                        /* fill the grid track up to the cap below, then stop growing */
  max-width: 420px;                   /* desktop: a balanced medallion, not the ~548px column-wide circle it was */
  align-self: center;                 /* never let a stretching grid/flex parent pull it into an oval */
  justify-self: center;               /* centre the capped circle inside its (wider) grid track */
  margin-inline: auto;                /* same centring if this box is ever placed in a flex/block parent */
  padding: 4px;                       /* unchanged breathing room around the artwork (kept as-is) */
  overflow: hidden;                   /* keep the clipped image within the circle */
  /* The website's own thin gold frame (was: border:2px solid #C9A24B) is removed here on
     request, so the medallion shows ONLY the logo image's native rings — its outer/inner
     gold and green rings stay exactly as drawn in the artwork. Everything else (size,
     centring, spacing, the image-only breathing zoom) is left untouched.
     deliberately NO transform / NO animation / NO will-change: only the image breathes. */
}
/* Tablet + large phone: a smaller cap so the medallion never dominates. On real phones
   the single collapsed column is already narrower than this, so the circle just follows
   the column (~85-90% wide) and this cap only bites in the tablet / large-phone band. */
@media (max-width: 1024px) {
  .art--badge { max-width: min(340px, 88vw); }
}
/* Kill the shared spinning-gradient rim + blurred glow on the badge ONLY — that
   animated halo was what made the edge look like it moved and wobbled off-round.
   .hero__frame and .card__media keep their rim untouched. */
.art--badge::before,
.art--badge::after { display: none; content: none; }
.badge-clip {
  display: block;
  width: 100%;
  height: 100%;                       /* fills the padding box exactly -> stays a circle */
  border-radius: 50%;
  overflow: hidden;                   /* the viewport that clips the zooming image to the circle */
  background: #F5EFE8;                /* the badge's own paper — the mat, unchanged */
  position: relative;
}
.art--badge img {
  display: block;
  width: 100%;
  height: auto;                       /* let the square source keep its own ratio — no forced stretch */
  aspect-ratio: 1 / 1;                /* brand-badge.jpg is 1000x1000, so this just guarantees the square */
  object-fit: contain;                /* show the WHOLE lockup — never crop it, not even at the zoom peak */
  object-position: center;           /* plaque is centred in the source, so 50% 50% keeps it aligned */
  border-radius: 50%;
  transform-origin: center center;    /* zoom grows from the centre, staying inside the clip */
  animation: badge-breathe 4s ease-in-out infinite;   /* gentle 1 -> 1.04 breathing, scoped to the badge only */
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .art--badge img { animation: none; transform: none; }   /* only the image animates, so only it is stilled */
}
/* A gentler, badge-only breathing zoom. Kept separate from the shared pillar-zoom
   (still used at 1 -> 1.08 by the pillar icons and the All-Products tab) so tuning the
   medallion never touches those. 1 -> 1.04 is deliberately subtle, and object-fit:contain
   plus the circular clip mean even the peak shows the whole lockup with no crop. The
   0-10 / 45-55 / 90-100 pairs give a soft pause at each end instead of a hard turnaround. */
@keyframes badge-breathe {
  0%, 10%   { transform: scale(1); }
  45%, 55%  { transform: scale(1.04); }
  90%, 100% { transform: scale(1); }
}

/* Caption plaque. It used to hang over the photograph's bottom corner, which read
   as one image pasted on top of another rather than as a flourish — and text over a
   photo is only ever as legible as the pixels underneath it. Sitting under the
   frame it covers nothing. */
.art__badge {
  display: flex;
  align-items: center;
  gap: .85rem;
  margin-top: .9rem;
  padding: .9rem 1.15rem;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.art__badge img {
  flex: 0 0 42px;
  width: 42px;
  height: auto;
  padding: 0;
  aspect-ratio: auto;
  background: none;
  border-radius: 0;
  box-shadow: none;
  object-fit: contain;
}
.art__badge b { display: block; font-size: .84rem; letter-spacing: .1em; text-transform: uppercase; color: var(--navy); }
.art__badge small { color: var(--muted); font-size: .8rem; }

/* photo strip lifted from the poster */
.strip {
  /* Both lengths here used to be fixed — a 500px background inside a 150px box —
     which meant that at 320px, where the container is 288px, 42% of the band was
     cropped away. Nothing reveals that: a background has no scrollbar and no
     overflow, so it simply looks like a differently composed photograph.
     min(500px, 100%) is the same idiom the grid tracks use, and for the same
     reason: keep the desktop size, let it collapse when it has to. */
  height: clamp(112px, 6rem + 5vw, 150px);
  background: url("../img/brand-band.jpg") center center / min(500px, 100%) auto no-repeat;
}

/* location map — Google's keyless embed, loaded lazily so it costs a visitor
   nothing until they scroll to it */
.mapbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.4rem;
  margin-bottom: var(--s-3);
}
.mapbar h2 { margin-bottom: .35rem; }
.mapbar p { margin: 0; color: var(--muted); }
.mapframe {
  overflow: hidden;
  background: var(--shot);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  line-height: 0;
}
.mapframe iframe {
  display: block;
  width: 100%;
  height: clamp(300px, 190px + 18vw, 480px);
  border: 0;
  filter: saturate(.94);
}

/* ------------------------------------------------------------- 10. stats */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  gap: var(--s-4);
}
.stat { text-align: center; }
.stat b {
  display: block;
  font-family: var(--serif);
  font-size: clamp(2rem, 1.4rem + 2.2vw, 3rem);
  font-weight: 700;
  line-height: 1;
  color: var(--green-light);
}
.stat span {
  display: block;
  margin-top: .6rem;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--on-navy-soft);
}
.section--cream .stat b { color: var(--green-deep); }
.section--cream .stat span { color: var(--muted); }

/* --------------------------------------------------------------- 11. cta */
.cta {
  position: relative;
  background: linear-gradient(115deg, var(--navy-deep) 0%, var(--navy) 52%, var(--navy-green) 100%);
  color: var(--cream);
  overflow: hidden;
}
.cta::after {
  content: "";
  position: absolute;
  right: -8%; bottom: -55%;
  width: 46%; aspect-ratio: 1;
  background: radial-gradient(circle, rgb(var(--green-light-rgb) / .38), transparent 65%);
}
.cta__in {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding: var(--s-6) 0;
}
.cta h2 { color: var(--cream); margin-bottom: .35em; }
.cta p { color: var(--on-navy-soft); max-width: 52ch; margin: 0; }

/* -------------------------------------------------------------- 12. form */
.form-card {
  padding: var(--s-4);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr));
  gap: var(--s-2);
}
.field { display: flex; flex-direction: column; gap: .4rem; }
.field--full { grid-column: 1 / -1; }
.field label {
  font-size: .78rem;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--navy);
}
.field .req { color: var(--green-deep); }
.field input, .field select, .field textarea {
  width: 100%;
  padding: .85rem 1rem;
  background: var(--panel);
  border: 1px solid var(--rule-strong);
  border-radius: 10px;
  font-family: var(--sans);
  font-size: .97rem;
  color: var(--ink);
  transition: border-color .2s, box-shadow .2s, background .2s;
}
.field textarea { min-height: 140px; resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none;
  background: var(--paper);
  border-color: var(--green);
  box-shadow: 0 0 0 4px rgb(var(--green-rgb) / .18);
}
.field .err {
  display: none;
  font-size: .8rem;
  font-weight: 600;
  color: var(--warn);
}
.field.is-invalid input, .field.is-invalid select, .field.is-invalid textarea { border-color: var(--warn); }
.field.is-invalid .err { display: block; }

.check-row { display: flex; flex-wrap: wrap; gap: .6rem; }
.check {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .55rem 1rem;
  background: var(--panel);
  border: 1px solid var(--rule-strong);
  border-radius: 999px;
  font-size: .88rem;
  cursor: pointer;
  transition: background .2s, border-color .2s, color .2s;
}
.check input { accent-color: var(--green-deep); margin: 0; }
.check:hover { border-color: var(--green); }
.check:has(input:checked) {
  background: var(--green-wash);
  border-color: var(--green);
  color: var(--green-deep);
  font-weight: 700;
}
.form-note { margin-top: 1rem; font-size: .84rem; color: var(--muted); }

.form-status {
  display: none;
  align-items: flex-start;
  gap: .8rem;
  margin-bottom: 1.4rem;
  padding: 1rem 1.2rem;
  background: var(--green-wash);
  border: 1px solid rgb(var(--green-rgb) / .28);
  border-left: 4px solid var(--green);
  border-radius: 10px;
  font-size: .92rem;
  color: var(--green-deep);
}
.form-status.is-visible { display: flex; }
.form-status svg { flex: 0 0 auto; width: 20px; height: 20px; margin-top: .15rem; }

/* The same box carries the reply from the server, which is not always good news, so it
   has a second face. Only the colour and the glyph change — moving the message somewhere
   else would mean the visitor has to look in two places to find out what happened. */
.form-status.is-problem {
  background: var(--warn-wash);
  border-color: var(--warn-line);
  border-left-color: var(--warn);
  color: var(--warn-deep);
}
.form-status svg[data-ico="bad"] { display: none; }
.form-status.is-problem svg[data-ico="ok"] { display: none; }
.form-status.is-problem svg[data-ico="bad"] { display: block; }
.form-status a { color: inherit; text-decoration: underline; }
.form-status a:hover { text-decoration: none; }
.form-status .ref {
  font-family: var(--mono);
  font-weight: 700;
  letter-spacing: .06em;
  white-space: nowrap;
}

/* While the enquiry is in flight. Pointer events off rather than the disabled attribute:
   a disabled button loses its accessible name in some screen readers mid-announcement. */
.btn[aria-busy="true"] { opacity: .65; pointer-events: none; }

/* contact detail tiles */
.contact-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: var(--s-2);
  margin: 0;
  padding: 0;
  list-style: none;
}
.contact-card {
  padding: var(--s-3);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  transition: transform .4s var(--ease), box-shadow .4s var(--ease), border-color .4s var(--ease);
}
.contact-card:hover {
  transform: translateY(-4px);
  border-color: rgb(var(--green-rgb) / .34);
  box-shadow: 0 18px 34px -22px rgb(var(--navy-rgb) / .32);
}
.contact-card h3 {
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--navy);
  margin: 1rem 0 .55rem;
}
.contact-card p { margin: 0; color: var(--ink); font-size: 1rem; line-height: 1.75; }
.contact-card a { color: var(--ink); }
.contact-card a:hover { color: var(--green-deep); }
.contact-card small {
  display: block;
  margin-top: .75rem;
  font-size: .76rem;
  font-style: italic;
  color: var(--muted);
}
/* legacy list variant */
.contact-list li {
  display: flex;
  gap: 1rem;
  padding: 1.15rem 1.3rem;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.contact-list li svg { flex: 0 0 auto; width: 22px; height: 22px; margin-top: .2rem; color: var(--green-deep); }
.contact-list b {
  display: block;
  font-size: .76rem;
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--navy);
  margin-bottom: .2rem;
}
.contact-list li span, .contact-list li a { color: var(--muted); font-size: .95rem; }
.contact-list li a:hover { color: var(--green-deep); }

/* placeholder markers inside spec lists */
.specs em {
  font-size: .8rem;
  font-style: italic;
  color: var(--green-deep);
  opacity: .85;
}

/* ------------------------------------------------------------ 13. footer */
.site-footer {
  background: var(--navy-deep);
  color: var(--on-navy-quiet);
  font-size: .93rem;
}
.footer__top {
  display: grid;
  /* The one that was actually broken. The 1.3fr column holds the email address,
     29 characters with no break opportunity in it, and at a 1024px viewport that
     track's share is 227px against a min-content width of roughly 253px. A bare
     1.3fr is minmax(auto, 1.3fr), so the track grew to the word instead of the
     word wrapping, and the footer overflowed the page at one of the exact widths
     the brief names. Both halves are fixed: the tracks below, and
     overflow-wrap: anywhere on the column's links. */
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.3fr);
  gap: var(--s-5);
  padding: var(--s-6) 0 var(--s-5);
}
@media (max-width: 900px) { .footer__top { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .footer__top { grid-template-columns: minmax(0, 1fr); } }

/* The footer logo is the full stacked SOFVERA lockup (1163x777, full colour). Its navy
   wordmark and droplet can't read on --navy-deep, so — keeping the artwork's own colours
   untouched — it rides a cream "paper" chip (apt for a tissue brand). Width-bound, not
   height-bound: box-sizing is border-box so this width includes the chip padding, and the
   image shrinks with the track instead of distorting (the same fix .pillar img got). */
.footer__logo {
  width: min(230px, 100%);
  height: auto;
  object-fit: contain;
  margin-bottom: 1.2rem;
  /* Matching hover-zoom on the footer lockup (same S emblem), anchored left. */
  transition: transform .3s cubic-bezier(.2, .7, .3, 1);
  transform-origin: left center;
}
.footer__logo:hover { transform: scale(1.2); }
.footer p { color: var(--on-navy-quiet); }
.footer__col h2 {
  margin-bottom: 1.1rem;
  color: var(--cream);
  font-size: .8rem;
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
}
.footer__col ul { margin: 0; padding: 0; list-style: none; display: grid; gap: .6rem; }
.footer__col a { color: var(--on-navy-quiet); }
.footer__col a:hover { color: var(--green-light); }

.footer__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .6rem 1.4rem;
  padding: 1.2rem 0;
  border-top: 1px solid var(--navy-line);
  font-size: .84rem;
}
.footer__claim {
  font-family: var(--serif);
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--green-light);
}

/* country of origin — two possible marks, and tools/origin-mark.py is the only
   thing that chooses between them. See that script for why.

   .origin        the Government of India campaign lion. Dark artwork, so it needs
                  its own light plaque rather than sitting on the navy.
   .origin--text  a typographic "Made in India". This is the default. A statement
                  of fact about where the goods are made cannot be mistaken for a
                  government device, which is the whole point of preferring it. */
.origin {
  display: inline-block;
  margin-top: 1.6rem;
  padding: .7rem .9rem;
  background: var(--paper);
  border-radius: 10px;
  box-shadow: 0 8px 22px -14px rgb(var(--navy-deep-rgb) / .5);
}
.origin img { display: block; width: 136px; height: auto; object-fit: contain; }
.origin--sm img { width: 112px; }
.origin--lg img { width: 168px; }

/* Why 136px and not larger. The brief for this mark says it must be clearly
   visible but not larger than the Sofvera logo, and the footer logo is the
   thing to measure against: 54px tall, and logo-light.png is 600x166, so it
   occupies about 195x54 = 10,500 sq px. The lion is 467x213, so 136px wide
   makes it 136x62 = 8,400 sq px — about four fifths of the brand mark, and
   narrower as well as only slightly taller. It reads as a secondary badge,
   which is exactly what it is. Changing the width here is the one place that
   ratio can drift, so the arithmetic is written down next to the number. */

/* No plaque and no artwork: this one is set, not placed. Sized to sit below the
   54px footer logo so the brand always reads first. */
.origin--text {
  padding: .55rem 0 0;
  background: none;
  border-radius: 0;
  box-shadow: none;
  border-top: 2px solid var(--green);
}
.origin__label {
  display: block;
  color: var(--on-navy-quiet);
  font-size: .64rem;
  font-weight: 700;
  letter-spacing: .18em;
  line-height: 1.6;
  text-transform: uppercase;
}
.origin--text strong {
  display: block;
  color: var(--cream);
  font-family: var(--serif);
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: .1em;
  line-height: 1.35;
  text-transform: uppercase;
}

/* ------------------------------------------------ 13b/14. social float */
/* The one social location on the site: a fixed group in the former WhatsApp
   corner, holding four brand icons (WhatsApp, Facebook, Instagram, YouTube).
   Each glyph sits in its own equal, transparent, subtly bordered circle; the
   glyphs carry their own official brand colours in the SVGs, so the container
   paints nothing. Hover shifts the border colour and lifts the shadow only —
   no zoom, rotate, bounce or flash. inline-flex + wrap keeps it tidy and
   responsive; on a phone it reflows without overflowing. */
.social-float {
  position: fixed;
  right: clamp(14px, 1.4vw, 26px);
  bottom: clamp(14px, 1.4vw, 26px);
  z-index: 70;
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  max-width: calc(100vw - 28px);
}
.social-float__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  flex: 0 0 auto;
  border: 1px solid #d8ded5;
  border-radius: 50%;
  background: transparent;            /* the container is just the ring; page shows through */
  box-shadow: 0 6px 18px rgba(0, 0, 0, .16);
  transition: border-color .22s var(--ease), box-shadow .22s var(--ease);
}
/* Larger, clearly visible glyph, centred with a comfortable non-touching margin
   inside the 48px circle; object-fit keeps every icon's aspect ratio intact. */
.social-float__btn svg {
  display: block;
  width: 30px;
  height: 30px;
  object-fit: contain;
  /* The glyph is decoration; the <a> takes every pointer event so a click near
     the edge still lands on the link, never on the SVG. */
  pointer-events: none;
}
/* Interaction lives entirely on the clickable <a>. No continuous animation:
   the lift, brightness and brand glow appear only on hover/focus, and the
   press-in only on :active. Background stays transparent in every state. */
.social-float__btn {
  position: relative;
  z-index: 2;
  transition: transform .18s var(--ease), filter .18s var(--ease),
              border-color .18s var(--ease), box-shadow .18s var(--ease),
              opacity .1s var(--ease);
}
.social-float__btn:hover,
.social-float__btn:focus-visible {
  transform: translateY(-3px);
  filter: brightness(1.12);
  background: transparent;
}
/* Immediate press feedback: shrink + fade, faster than the hover transition. */
.social-float__btn:active {
  transform: translateY(0) scale(.9);
  opacity: .75;
  transition-duration: .08s;
}
/* Keyboard focus ring sits clear of the glyph and carries the brand colour. */
.social-float__btn:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
}

/* Per-platform border + glow (brand colours). currentColor drives the focus
   ring too, so each icon focuses in its own colour. */
.social-float__btn--wa { color: #25D366; }
.social-float__btn--fb { color: #1877F2; }
.social-float__btn--ig { color: #d62976; }
.social-float__btn--yt { color: #FF0000; }
.social-float__btn--wa:hover, .social-float__btn--wa:focus-visible {
  border-color: #25D366; box-shadow: 0 10px 24px rgba(37, 211, 102, .45);
}
.social-float__btn--fb:hover, .social-float__btn--fb:focus-visible {
  border-color: #1877F2; box-shadow: 0 10px 24px rgba(24, 119, 242, .45);
}
.social-float__btn--ig:hover, .social-float__btn--ig:focus-visible {
  border-color: #d62976; box-shadow: 0 10px 24px rgba(214, 41, 118, .40), 0 6px 16px rgba(250, 126, 30, .35);
}
.social-float__btn--yt:hover, .social-float__btn--yt:focus-visible {
  border-color: #FF0000; box-shadow: 0 10px 24px rgba(255, 0, 0, .40);
}

/* ----------------------------------------------------------- 15. sweeps */
/* the curved green-over-navy transition used across the poster */
.sweep { display: block; width: 100%; height: clamp(40px, 5vw, 88px); }
.sweep--top { margin-bottom: -1px; }
.sweep--bottom { margin-top: -1px; }
/* Flowing divider (the 5 collection pages that carry the green sweep line). The
   motion lives in the SVG itself — a repeating gradient translated by SMIL, see
   the .sweep markup — so this block owns only the reduced-motion swap: hide the
   two animated .sweep__flow paths (the line and its glow) and reveal the static
   .sweep__static gradient line in their place. .sweep__static is out of the
   paint until then. */
.sweep__static { display: none; }
@media (prefers-reduced-motion: reduce) {
  .sweep__flow { display: none; }
  .sweep__static { display: inline; }
}

/* --------------------------------------------------------- 16. page head */
.pagehead {
  position: relative;
  background: var(--navy);
  color: var(--on-navy-soft);
  overflow: hidden;
}
.pagehead::before {
  content: "";
  position: absolute;
  top: -60%; left: 58%;
  width: 52%; aspect-ratio: 1;
  background: radial-gradient(circle, rgb(var(--green-light-rgb) / .3), transparent 68%);
}
.pagehead__in {
  position: relative;
  max-width: 760px;
  margin-inline: auto;
  padding: var(--s-6) 0 var(--s-6);
}
.pagehead h1 { color: var(--cream); }
.pagehead p { color: var(--on-navy-soft); margin-bottom: 0; }
.crumbs {
  margin: 0 0 1.2rem;
  font-size: .8rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--on-navy-quiet);
}
.crumbs a { color: var(--green-light); }
.crumbs span { margin: 0 .5rem; opacity: .5; }

/* --------------------------------------------------------- 17. filters */
.filters {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: .6rem;
  margin-bottom: var(--s-4);
}
.filters button {
  padding: .6rem 1.25rem;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 999px;
  font-family: var(--sans);
  font-size: .84rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--navy);
  cursor: pointer;
  transition: background .22s, border-color .22s, color .22s;
}
.filters button:hover { border-color: var(--green); color: var(--green-deep); }
.filters button.is-active { background: var(--navy); border-color: var(--navy); color: var(--cream); }

/* Breathing zoom on the All Products tab ONLY — reuses the shared pillar-zoom (scale
   1 -> 1.08 -> 1, 4s ease-in-out). Scoped to [data-filter="all"] so the other category
   tabs stay static. Frozen by the global reduced-motion rule at the end of the file. */
.filters button[data-filter="all"] {
  transform: scale(1);
  animation: pillar-zoom 4s ease-in-out infinite;
  will-change: transform;
}

.is-hidden { display: none !important; }

/* -------------------------------------------------------- 18. reveal fx */
.reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}
.reveal.is-in { opacity: 1; transform: none; }

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

/* ----------------------------------------------------------- 19. helpers */
.center { text-align: center; }
.mt-lg { margin-top: var(--s-5); }

/* utilities that used to be inline style attributes — moved here so the page can
   run under a Content-Security-Policy with no 'unsafe-inline' for styles */
.pillars--flush { border-top: 0; padding-top: 0; margin-top: 0; }
/* height: auto is repeated here rather than left to the base rule, because this rule's
   whole job is to size an image and a reader changing it should see both halves. Without
   it the height="443" attribute on logo-stack.png survived and drew the 700x443 lockup
   at 420x443. */
.mark-centred { width: min(420px, 80%); height: auto; margin: 0 auto 2rem; }
.measure-tight { max-width: 22ch; margin-inline: auto; }
.measure { max-width: 56ch; margin-inline: auto; }
.specs--light { border-top-color: rgb(var(--cream-rgb) / .2); }

/* honeypot — off-canvas rather than display:none, which some bots detect */
.trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.skip {
  position: absolute;
  left: 1rem; top: -60px;
  z-index: 200;
  padding: .7rem 1.2rem;
  background: var(--green-deep);
  border-radius: 0 0 10px 10px;
  color: var(--paper);
  font-weight: 700;
  transition: top .25s;
}
.skip:focus { top: 0; color: var(--paper); }

/* -------------------------------------------------- 20. cookie consent */
/* A bottom bar, injected by assets/js/analytics.js only when a GA4 ID has
   been configured. It matches the navy footer. The global reduced-motion
   rule above already collapses its slide to an instant appearance. */
.consent {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 80;
  background: var(--navy-deep);
  color: var(--on-navy);
  border-top: 3px solid var(--green);
  box-shadow: 0 -14px 38px rgb(var(--ink-rgb) / .3);
  transform: translateY(100%);
  transition: transform .32s var(--ease);
}
.consent.is-in { transform: translateY(0); }
.consent__in {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .9rem var(--s-3);
  padding: 1.1rem 0;
}
.consent__text {
  margin: 0;
  flex: 1 1 420px;
  max-width: 74ch;
  font-size: .92rem;
  color: var(--on-navy-soft);
}
.consent__actions { display: inline-flex; flex: 0 0 auto; gap: .7rem; }
.consent__btn { padding: .7rem 1.5rem; font-size: .82rem; }

/* While the prompt is up, lift the social float clear of it and above it,
   so the icons stay fully visible and tappable. Its markup is untouched. */
body.consent-open .social-float {
  transform: translateY(calc(-1 * clamp(84px, 12vh, 120px)));
  z-index: 90;
}

/* the quiet "Cookie preferences" re-open control in the footer bar. The auto
   margin keeps the existing copyright (left) and claim (right) in place. */
.footer__cookies {
  margin-right: auto;
  border: 0;
  padding: 0;
  background: transparent;
  font: inherit;
  color: var(--on-navy-quiet);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.footer__cookies:hover { color: var(--green-light); }
