/* ==========================================================================
   FFS Foods — brand layer
   --------------------------------------------------------------------------
   Loaded AFTER agriox.css. Everything brand-specific lives here so the
   template stylesheet stays untouched and upgradable.

   agriox.css has been pre-processed so all 286 previously-hardcoded brand
   hexes now resolve through the four variables below — redefining them here
   re-skins the entire site.

   Two sources, and they cover different ground:
     · Colour comes from the Brand Colour Integration brief (blue / gold /
       orange / gray). It SUPERSEDES the palette in FFS_Brand.pdf.
     · Type still comes from FFS_Brand.pdf — Sora display, Inter body. The
       brief does not touch typography and nothing here has changed it.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Palette

   Source of truth: the Brand Colour Integration brief. This SUPERSEDES the
   five-colour scheme in FFS_Brand.pdf — the navy/mid-blue/pale-tint system
   is gone, and so is the red accent (#C62828) that predated it. Do not
   reintroduce either.

     Primary Blue    #2E3192  headers, footers, key typography
     Secondary Gold  #F2A900  secondary actions, iconography, decorative accents
     Tertiary Orange #E86100  Shopee header badge only (see below)
     Neutral Gray    #CBCFD0  subtle borders, background variations
     White           #FFFFFF  dominant background

   Mr Magic Green #8C9600 was added 2026-09-07, on top of the brief above
   rather than as part of it: the client asked to move the high-priority CTA
   — "Enquire Now", "Explore products", every Shopee button in the body of a
   page — from Tertiary Orange to "the same green as Mr Magic" (Mr Magic
   being the client's own retail counter brand). Sampled from a lit photo of
   the actual Mr Magic signage (assets/images/hero/mr-magic-cafe.jpg): the
   fascia reads anywhere from a bright, near-yellow highlight (~#DEE00A)
   under direct light to a deeper olive (~#8C9600–#9C9600) in its own
   ambient shade, confirmed NOT a colour-cast artefact — neutral whites
   elsewhere in the same photo read as neutral grey, so the camera's white
   balance is accurate and the material genuinely shifts that much under
   mixed spot lighting. #8C9600 was chosen from within that range for a
   reason the brief's own gold already illustrates: a swatch has to survive
   sitting on white AND on the blue hero as a 3:1 non-text graphic (WCAG
   1.4.11) to work as a button fill at all, and the brighter, more
   "chartreuse" end of the sampled range (anything past roughly #B0BE00)
   is too close in luminance to white to clear that floor — it would not
   read as a button, only as a paler patch of the page.

   SUPERSEDED 2026-09-08: the client replaced #8C9600 outright with #73C92D
   — their own colour, not a further sample off the signage photo — while
   keeping the label white. #73C92D does NOT clear the 3:1 boundary/large-
   text floor #8C9600 was picked to satisfy (2.07:1, see the contrast table
   below); it was supplied as a fixed value with an explicit "white text"
   instruction, not chosen from a range with that floor in mind, and it is
   applied as given. The sampling paragraph above is kept for the record of
   how the CTA colour got to #8C9600 — #73C92D is not derived from it.

   Tertiary Orange is not retired: it still lives as-is on the header
   utility-bar Shopee badge (--ffs-shopee, section 8), a 14px pill that
   predates and is unrelated to the CTA-button decision above.

   Two things shape the mapping below:

     Gold on white is 2.01:1. That is under the 3:1 that WCAG 1.4.11 asks of
     even a meaningful *graphic*, so gold is not merely bad for text on white
     — it cannot carry an icon there either. Gold is at its best on the blue
     (5.30:1), which is where the iconography and decorative accents live:
     the top bar, the hero, the blue panels, the mobile drawer, the footer.
     Where an icon must sit on white, use a blue chip with a gold glyph.

     Green with a WHITE label tops out under 4:1 across the whole sampled
     #8C9600-family range — #8C9600 itself managed 3.24:1 at rest, clearing
     the 3:1 large-text floor Tertiary Orange's 3.42:1 white label also
     depended on, just with less margin. Neither colour ever cleared 4.5:1
     normal-text with a white label. For a few hours on 2026-09-07 this
     shipped with a near-black label instead (--ffs-on-brand, 5.74:1/8.01:1),
     which is the more comfortable fix and the one this stylesheet uses
     everywhere else a light fill needs a label — gold, WhatsApp, the
     Shopee badge. The client asked for white text back the same day, and
     then on 2026-09-08 replaced the fill itself with #73C92D — a colour
     that does not clear even the 3:1 large-text floor with white (2.07:1),
     which #8C9600 was specifically chosen to satisfy. That is a genuine AA
     FAIL, applied as instructed rather than adjusted to pass; see the
     --ffs-cta token comment below for the full note on it. Hover still
     DEEPENS (white gets more legible as a fill darkens, the opposite of a
     near-black label) — #569621, 3.63:1 — because deepening is still the
     right direction even though rest state already fails; it does not
     make hover legal on its own.

   How the brand colours map onto the template's tokens:

     --agriox-primary      Primary Blue. Dark surfaces, headings, nav.
     --agriox-base         Primary Blue again, deliberately. This is the
                           template's accent token and it feeds 86 `color:`
                           declarations — it has to hold something that is
                           safe as text on white, and of the four only the
                           blue is (10.66:1). Gold and orange are therefore
                           NOT wired in globally; they are applied by hand
                           below, on grounds chosen to clear AA.
     --agriox-base-dark    Blue darkened, for hover on blue fills. Since
                           base and primary are now the same value, any
                           template hover that went base→primary is a no-op;
                           the FFS components use this instead.
     --agriox-secondary    Gold. In agriox.css this token only reaches parts
                           the FFS pages do not render (blog, testimonials,
                           team, checkout), so it is safe to hand it a
                           white-hostile colour. Treat it as decorative.
     --agriox-base-on-dark Gold, for accent text and icons on the blue
                           surfaces (5.30:1). NEVER use on white.
     --ffs-cta             #73C92D, the client's own CTA green (2026-09-08).
                           The high-priority CTA fill — .ffs-btn--primary
                           AND .ffs-btn--shopee share it, by the same "one
                           CTA colour" instruction that originally put
                           Shopee on Tertiary Orange here; it is the loudest
                           thing on the page and stops being loud the moment
                           it is used for anything else.

   Contrast, measured (WCAG 2.1, relative luminance per the sRGB formula):
     #FFFFFF on #2E3192 .... 10.66:1  AAA  — text on the blue header/footer
     #2E3192 on #FFFFFF .... 10.66:1  AAA  — headings, nav, links on white
     #0B141A on #F2A900 ....  9.26:1  AAA  — label on a gold fill
     #2E3192 on #EDEEEF ....  9.18:1  AAA  — headings on the neutral band
     #4A4A55 on #FFFFFF ....  8.74:1  AAA  — body copy
     #4A4A55 on #EDEEEF ....  7.53:1  AAA  — body copy on cards
     #2E3192 on #CBCFD0 ....  6.79:1  AA   — text on a full-strength gray
     #F2A900 on #2E3192 ....  5.30:1  AA   — gold text and icons on the blue
     #73C92D on #2E3192 ....  5.14:1  AA   — non-text graphic: the CTA fill
                                             sitting on the blue hero (this
                                             one reading improved over
                                             #8C9600's 3.29 — the trade-off
                                             is entirely on white grounds)
     #A57300 on #FFFFFF ....  4.16:1  ——   — gold, darkened, on light grounds
     #FFFFFF on #569621 ....  3.63:1  ——   — CTA label, hover state — still
                                             short of normal-text AA, better
                                             than rest
     #A57300 on #EDEEEF ....  3.58:1  ——   — gold, darkened, on the neutral
                                             band
     #73C92D on #FFFFFF ....  2.07:1  FAIL — two readings at once: the CTA
                                             fill as a control boundary on
                                             white, AND (same numbers) white
                                             CTA label, rest state. Below the
                                             3:1 large-text floor the 19px/700
                                             size rule exists to guarantee —
                                             see the --ffs-cta token comment
                                             for why this shipped anyway
     #F2A900 on #FFFFFF ....  2.01:1  FAIL — never text, never a meaningful
                                             icon; blue grounds only
     #CBCFD0 on #FFFFFF ....  1.57:1  FAIL — borders and background
                                             variations only

   Tertiary Orange's own contrast figures (white label 3.42:1 rest / 4.65:1
   hover, both large-text-only) are unchanged and still apply wherever it
   still appears — the header Shopee badge and the values under --ffs-orange
   / --ffs-shopee below — just no longer to the CTA button.
   -------------------------------------------------------------------------- */
:root {
  /* Primary Blue. Headers, footers, key typography, dark surfaces. Feeds
     171 pre-processed hexes in agriox.css; changing it re-skins the whole
     template, so measure before touching it. */
  --agriox-primary: #2e3192;
  --agriox-primary-rgb: 46, 49, 146;

  /* Blue, deepened. NOT the brand gold, despite this being the template's
     "secondary" slot — the token is a background in template chrome the FFS
     pages do render, and the template hardcodes #ffffff on top of it. The
     footer bottom bar is the proof: gold here rendered a white copyright
     line and white social icons at 2.01:1. Gold reaches the page through
     --agriox-base-on-dark and --ffs-gold instead, both of which are only
     ever placed on grounds this stylesheet controls. */
  --agriox-secondary: #242672;
  --agriox-secondary-rgb: 36, 38, 114;

  /* Page base. Declared rather than left to the UA default so nothing
     downstream can quietly reintroduce the template's boxed cream. White
     stays the dominant background. */
  --ffs-page: #ffffff;

  /* The template's accent token. Blue, not gold — it lands on 86 text
     declarations and gold would take every one of them to 2.01:1. */
  --agriox-base: #2e3192;
  --agriox-base-rgb: 46, 49, 146;

  /* Blue darkened ~22%. Hover/active for blue-filled surfaces; 13.16:1 on
     white, so it is also safe as text. */
  --agriox-base-dark: #242672;

  /* Gold — accent text and icons on the blue surfaces (5.30:1). The
     on-white counterpart is --ffs-gold-dark. */
  --agriox-base-on-dark: #f2a900;

  /* Body text — warm neutral, not pure grey. The brief does not specify a
     body colour; this measures 8.74:1 on white and 7.53:1 on the band. */
  --agriox-color-1: #4a4a55;
  --agriox-color-1-rgb: 74, 74, 85;

  /* The four brand colours under their own names, for the component work
     further down. */
  --ffs-blue: #2e3192;
  --ffs-gold: #f2a900;
  --ffs-orange: #e86100;
  --ffs-gray: #cbcfd0;

  /* High-priority CTA — #73C92D as of 2026-09-08, the client's own explicit
     replacement for the sampled #8C9600 Mr Magic Green this stylesheet
     landed on the day before (see the section 1 palette note for that
     sampling and reasoning, now superseded). #73C92D is a brighter,
     "flatter" green — closer to a stock/brand-kit swatch than a colour
     lifted from a lit photo — and the client was explicit that the label
     stays white.

     FLAG, not a blocker: white on #73C92D is 2.07:1 — below even the 3:1
     WCAG large-text floor the previous green (3.24:1) and the orange before
     it (3.42:1) both cleared, both at the same 19px/700. This is a real AA
     FAIL, not a tight pass; see the palette note's contrast table. Applied
     as instructed regardless — this is the client's explicit colour AND
     label choice, not a default this file picked. If it needs to clear AA
     later, the fix is either a near-black label again (--ffs-on-brand,
     which would pass comfortably on this fill) or a darker green with white
     retained; it is not a CSS bug to "fix" unilaterally.

     --ffs-cta-hover deepens for the same reason it deepened under the old
     green: a white label gains contrast as the fill gets darker, so hover
     should move that direction even though rest state already fails —
     3.63:1 on hover, still short of 4.5 but closer, and better than resting
     state either way. */
  --ffs-cta: #73c92d;
  --ffs-cta-hover: #569621;

  /* Gold darkened until it clears 3:1 on white AND on the neutral band
     (4.16:1 / 3.58:1). For gold-toned borders and any gold graphic that has
     to sit on a light ground. Still not body text.

     --ffs-gold-hover is the shallower darkening used for gold FILLS. It is
     visibly deeper than the rest state while keeping the near-black label at
     7.29:1; --ffs-gold-dark itself would take that label to 4.48:1, which is
     just under AA for a 16px bold button label. */
  --ffs-gold-dark: #a57300;
  --ffs-gold-hover: #d69600;

  /* Typography, per FFS_Brand.pdf: Sora for display, Inter for everything
     functional. Both are Google Fonts. The template's third font
     (Shadows Into Light) stays dropped; --agriox-font-3 is aliased to the
     display face so any stray reference still resolves. */
  --agriox-font: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --agriox-font-2: 'Sora', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --agriox-font-3: 'Sora', system-ui, sans-serif;

  /* Supporting tokens used by the FFS components below.

     --ffs-surface is the brief's "background variation": the neutral gray
     at 35% over white. Full-strength #CBCFD0 is too heavy for a section
     band behind body copy, but it is exactly right as a border, which is
     what --ffs-border takes it for. */
  --ffs-ink: #2e3192;
  --ffs-surface: #edeeef;
  --ffs-border: #cbcfd0;
  --ffs-radius: 10px;
  --ffs-btn-gap: 16px;
  --ffs-shadow: 0 2px 4px rgba(46, 49, 146, .05), 0 8px 24px rgba(46, 49, 146, .08);

  /* Third-party brand fills. Both are light, and both were carrying white
     labels that measured far under AA — WhatsApp green 2.0:1, Shopee orange
     3.4:1. Rather than dulling two recognisable brand colours, the label
     flipped to near-black (9.4:1 and 5.4:1). Same pattern Spotify uses for
     black-on-green. Hovers LIGHTEN these two, because dark text on a
     darkened fill goes the wrong way.

     Shopee now takes the brand's Tertiary Orange rather than Shopee's own
     #EE4D2D, which the brief asks for and which the two colours are close
     enough to survive: the badge still reads as Shopee without importing a
     fifth orange into the palette.

     The Shopee badge carries a WHITE label — originally chosen to match the
     CTA buttons, back when .ffs-btn--primary was white-on-orange too. The
     CTA has moved fills and label colours several times since (Mr Magic
     Green with a near-black label, then white; #73C92D with white — see the
     section 1 palette note for the full sequence), so this is now a
     coincidence rather than a design link either way. The badge itself is
     unaffected by any of it: it is a 14px pill, not a 19px button, so it
     could never lean on the large-text exemption the CTA sizing gives, and
     it therefore sits on a deepened shade of its own Tertiary Orange rather
     than whatever --ffs-cta currently holds: white measures 4.79:1 on
     #C24E00 and 6.21:1 on the hover. Same colour family, legal at badge
     size, independent of the CTA's own state.

     This pair is now the HEADER BADGE ONLY. The Shopee buttons in the body
     of the page take --ffs-cta flat (#73C92D as of 2026-09-08) — see
     .ffs-btn--shopee in section 4, which currently fails AA on that fill
     even at 19px/700 (2.07:1) and would fail worse at 14px. Do not point
     the badge at --ffs-cta regardless of what it holds next — that is
     exactly the failure mode the deepened shade exists to avoid. */
  --ffs-whatsapp: #25d366;
  --ffs-whatsapp-hover: #2fe072;
  --ffs-shopee: #c24e00;
  --ffs-shopee-hover: #a54200;

  /* Label colour for every light brand fill — gold, orange, WhatsApp,
     Shopee. Near-black rather than pure #000 so it reads as ink, not as a
     hole in the fill. */
  --ffs-on-brand: #0b141a;

  /* Review stars. A star row rendered in the brand blue reads as decoration
     rather than a rating, and it is the only rating a sighted user sees (the
     text equivalent is for screen readers) — so it needs its own colour, and
     WCAG 1.4.11 asks 3:1 of it as a meaningful non-text graphic. This is the
     brand gold darkened until it clears that on both grounds it appears on:
     4.16:1 on white, 3.58:1 on the neutral band. Brand gold itself is 2.01:1
     and would fail both. Same value as --ffs-gold-dark; kept under its own
     name so the rating can be retuned without moving every gold border. */
  --ffs-star: var(--ffs-gold-dark);
}

/* --------------------------------------------------------------------------
   1b. Font Awesome — give bare `.fa` the solid face
   --------------------------------------------------------------------------
   Font Awesome 5 splits what v4 called `.fa` into three families, and it sets
   the weight on the NEW classes only:

     .fas  font-family: "Font Awesome 5 Free"    font-weight: 900   (1001 icons)
     .far  font-family: "Font Awesome 5 Free"    font-weight: 400   ( 151 icons)
     .fab  font-family: "Font Awesome 5 Brands"                     ( 457 icons)
     .fa   font-family: "Font Awesome 5 Free"    -- no weight at all

   Bare `.fa` is a v4 leftover: it picks the family but leaves the weight to be
   INHERITED from whatever the icon happens to sit inside. The two faces are
   400 and 900, so per CSS font matching an inherited 400 or 500 selects the
   400 face — which holds only 151 of the 1001 Free glyphs. Every solid-only
   icon in that position has no glyph to draw and renders as nothing at all.

   That is what was emptying the boxes on the homepage. `.ffs-cert__mark` sets
   no weight, so its icons inherited body's 400: fa-flask, fa-cogs, fa-box-open,
   fa-store in "What we do", and fa-shield-alt, fa-check, fa-balance-scale in
   "Why it matters", were all blank. 14 distinct icons sitewide. The ones that
   did survive (fa-check-circle, fa-clock, fa-envelope, fa-file-alt) only did so
   by happening to exist in the 151 — and they were drawing as outlines while
   their neighbours were solid.

   Setting the weight here fixes all 26 bare-`fa` icons at once, which is why
   this is a stylesheet rule and not 35 edits across the PHP. All 26 were
   checked against the solid face's codepoint table first; every one is present,
   so none of them can regress into the same blank state from this direction.

   `.fab` and `.far` are untouched: brands carry their own family, and the
   review stars deliberately use `.far` for the empty state. Neither element
   carries the `fa` class, so neither matches this selector.
   -------------------------------------------------------------------------- */
.fa {
  font-weight: 900;
}

/* --------------------------------------------------------------------------
   2. Typography scale

   The template ships 18px/34px body copy, which is oversized for reading on
   a phone and forces awkward line lengths. Reset to a fluid scale.
   -------------------------------------------------------------------------- */
body {
  font-size: 16px;
  line-height: 1.65;
  color: var(--agriox-color-1);
  background-color: var(--ffs-page);
  -webkit-font-smoothing: antialiased;
}

/* The template ships a boxed layout on a cream ground (#f6f4ec). No FFS page
   sets .boxed-wrapper, but the rule outranks the element selector above if one
   ever does, so it is neutralised here rather than left as a trapdoor. */
body.boxed-wrapper { background-color: var(--ffs-page); }

@media (min-width: 768px) {
  body {
    font-size: 17px;
    line-height: 1.7;
  }
}

h1, h2, h3, h4, h5, h6,
.section-title__title,
.main-slider__title {
  font-family: var(--agriox-font-2);
  color: var(--ffs-ink);
  font-weight: 700;
  line-height: 1.18;
  letter-spacing: -0.01em;
}

h1 { font-size: clamp(2rem, 1.4rem + 3vw, 3.5rem); }
h2 { font-size: clamp(1.6rem, 1.2rem + 2vw, 2.5rem); }
h3 { font-size: clamp(1.25rem, 1.05rem + 1vw, 1.6rem); }

/* Sora is geometric and sits wider than the serif it replaces, so optical
   tracking has to tighten as the size grows — a flat -0.01em reads loose at
   3.5rem and cramped at 1.25rem. Small headings go back to neutral. */
h1 { letter-spacing: -0.025em; }
h2 { letter-spacing: -0.02em; }
h4, h5, h6 {
  font-weight: 600;
  letter-spacing: 0;
}

/* Long-form copy should not run edge to edge on a wide screen. */
.ffs-prose p { max-width: 68ch; }

/* Bulleted/numbered lists inside prose copy — added 2026-08-29 for the Halal
   and HACCP policy statements on accreditation.php, the first place this site
   has needed a list inside body copy rather than a run of <p>. Same max-width
   as the paragraphs above, so a policy's list items wrap at the same measure
   as its lead-in sentence. */
.ffs-prose ul,
.ffs-prose ol {
  max-width: 68ch;
  margin: 0 0 1em;
  padding-left: 1.25em;
}
.ffs-prose li + li { margin-top: .5em; }

.ffs-skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 1000;
  padding: 12px 20px;
  background: var(--agriox-primary);
  color: #fff;
  font-weight: 700;
}
.ffs-skip-link:focus {
  left: 8px;
  top: 8px;
}

/* Visible keyboard focus — the template removes outlines in several places. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 3px solid var(--agriox-base);
  outline-offset: 2px;
}

/* The blue focus ring is invisible on the blue grounds — 1:1, since it is
   the same colour. Anything sitting on blue takes the gold instead, which
   measures 5.30:1, well past the 3:1 a focus indicator needs. */
.ffs-hero a:focus-visible,
.ffs-hero button:focus-visible,
.ffs-section--panel a:focus-visible,
.ffs-section--panel button:focus-visible,
.main-header--one__top a:focus-visible {
  outline-color: var(--agriox-base-on-dark);
}

/* --------------------------------------------------------------------------
   3. Hero — single static banner replacing the 3-slide Swiper carousel
   -------------------------------------------------------------------------- */
.ffs-hero {
  position: relative;
  display: flex;
  align-items: flex-start;
  min-height: 460px;
  padding: 80px 0;
  background: var(--agriox-primary);
  overflow: hidden;
}

.ffs-hero > .auto-container {
  width: 100%;
}

@media (min-width: 992px) {
  .ffs-hero { min-height: 620px; }
}

.ffs-hero__media {
  position: absolute;
  inset: 0;
}
.ffs-hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ffs-hero__media::after {
  content: '';
  position: absolute;
  inset: 0;
  /* Directional scrim: keeps text legible without flattening the photo. */
  background: linear-gradient(100deg,
      rgba(var(--agriox-primary-rgb), .94) 0%,
      rgba(var(--agriox-primary-rgb), .82) 45%,
      rgba(var(--agriox-primary-rgb), .45) 100%);
}

.ffs-hero__inner {
  position: relative;
  z-index: 2;
  max-width: 720px;
  color: #fff;
  text-align: left;
}

.ffs-hero__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 18px;
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  /* Sits on the blue hero scrim — gold, not the raw accent blue. */
  color: var(--agriox-base-on-dark);
}

.ffs-hero__title {
  color: #fff;
  margin-bottom: 20px;
}

.ffs-hero__text {
  font-size: 1.0625rem;
  color: rgba(255, 255, 255, .88);
  margin-bottom: 32px;
  max-width: 52ch;
}
/* The 32px above is the gap between the copy and the CTA row, not between
   sentences. Where a hero runs to two paragraphs (home, products) that same
   gap would split one passage into two loose blocks, so tighten everything
   but the last — which still has the buttons under it to clear. */
.ffs-hero__text:not(:last-of-type) {
  margin-bottom: 18px;
}

/* Layout only — display/gap come from the shared button-group rule in §4 so
   the hero pair is spaced identically to every other pair. */
.ffs-hero__actions {
  align-items: center;
  justify-content: flex-start;
}

/* The .ffs-hero__trust checklist that lived here — a tick-listed row of short
   claims under the hero CTAs — was removed in August 2026 at the client's
   request, from index.php and business-solutions.php together. Those were its
   only two instances, so the rules went with the markup rather than being left
   behind as dead weight in an already long file.

   Both pages keep the markup they lost in a comment at the point of removal, so
   restoring it means pasting that back and re-adding a flex row here. Nothing
   else referenced the class. */

/* White certification marks in hero banners. Used on index.php, about.php and
   accreditation.php; only index.php takes the --flush modifier below.

   Sized as a headline element rather than a footnote: 80px on a phone, 96px on
   a tablet, 112px from lg up. The source PNGs are 300x300 with generous internal
   padding, so the glyph inside reads roughly two-thirds of the box height —
   which is why these numbers look large next to a typical logo strip and do not
   render large on screen.

   Nothing here may take a max-height. .ffs-hero does set overflow:hidden — that
   is there to crop the background photograph — but it cannot clip this row,
   because the hero has a min-height rather than a fixed height and grows to fit
   its content. If a logo ever looks cropped, something has given an ancestor a
   fixed height; fix that rather than shrinking these values back down. */
.ffs-hero__certs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 28px 40px;
  margin-top: 40px;
  padding-top: 32px;
  border-top: 1px solid rgba(255, 255, 255, .2);
}
.ffs-hero__certs img {
  width: auto;
  height: 80px;
  max-height: none;
  object-fit: contain;
  opacity: 1;
  /* Lifts the white vectors off the blue scrim. The hero gradient runs from
     near-solid blue to 45% at the right edge, so a logo landing on the lighter
     end needs the shadow to keep an edge. */
  filter: drop-shadow(0 4px 10px rgba(0, 0, 0, .35));
  transition: transform .18s ease, filter .18s ease;
}
.ffs-hero__certs img:hover {
  transform: scale(1.04);
  filter: drop-shadow(0 6px 14px rgba(0, 0, 0, .45));
}

@media (min-width: 576px) {
  .ffs-hero__certs {
    gap: 32px 48px;
  }
  .ffs-hero__certs img { height: 96px; }
}

@media (min-width: 992px) {
  .ffs-hero__certs img { height: 112px; }
}

/* --flush: the certs row when nothing sits between it and the CTAs.

   index.php used to run CTAs → checklist → logos, with a hairline rule opening
   the checklist and a second one opening the logo row. The checklist and both
   rules were removed in August 2026, and a lone rule left under the buttons read
   as a stray underline rather than as a divider between two things, so this
   modifier drops the border and the padding that went with it.

   It is a modifier and not a change to the base rule because about.php and
   accreditation.php also use .ffs-hero__certs, and there the border still
   separates the logos from the body copy above them. Those two keep it.

   Margin note: top only. This is the last child of .ffs-hero__inner and the hero
   already carries 80px of bottom padding, so a bottom margin here would deepen a
   gap that is generous already rather than balance anything. */
.ffs-hero__certs--flush {
  border-top: 0;
  padding-top: 0;
  margin-top: 32px;
}

/* ---------------------------------------------------------------------------
   Single-line mobile behaviour for the certification logo row.

   Written August 2026 for FOUR marks (HACCP, Halal, ISO, Buatan Malaysia):
   justify-content:space-between plus a width:calc(...)/4 so the row filled
   edge to edge exactly, at every width down to the 320px floor. ISO and
   Buatan Malaysia were dropped from every page using this row 2026-08-29
   (client instruction — see the removal notes on index.php, about.php and
   accreditation.php), leaving two marks: HACCP and Halal, everywhere
   .ffs-hero__certs appears. Nobody revisited this block when that happened,
   so space-between kept doing exactly what it says with two flex items —
   push the first to the left edge and the last to the right — which reads
   as the two logos abandoned at opposite corners rather than as one group.
   Fixed 2026-09-09.

   Two marks at 290px of content width (the 320px floor less the 30px
   .auto-container padding — .ffs-hero itself is padding:80px 0, no
   horizontal padding of its own to subtract) need none of the old row-
   filling math: the base (non-media-query) rule already sizes each mark at
   height:80px / width:auto, and two 80px squares plus a clean gap come to
   under 200px — comfortably centered with room either side at any width
   this site supports, no shrinking required. So the fix is two
   declarations, not a new sizing system: centre the row, and gap it
   generously instead of the old 8px (which existed only to let four boxes
   ration a tight row — with two, and no more row to fill, the tighter gap
   just reads as stingy).
   -------------------------------------------------------------------------- */
@media (max-width: 575.98px) {
  .ffs-hero__certs {
    flex-wrap: nowrap;
    justify-content: center;
    gap: 20px;
    margin-top: 28px;
    padding-top: 24px;
  }

  /* Must follow the .ffs-hero__certs rule above, not precede it. Both are
     single-class selectors, so specificity is equal and source order decides —
     placed first, the padding-top just above would win back the space this
     modifier exists to remove. */
  .ffs-hero__certs--flush {
    padding-top: 0;
    margin-top: 28px;
  }
}

/* Vitaplex B+ badge — a sticker sitting half on, half off the top-right corner
   of the card.

   The bleed is the whole point of this component and it is in direct conflict
   with .ffs-card's overflow:hidden, which exists so a square photograph does not
   square off the card's rounded corners. Both are needed, so the clip MOVES
   rather than being deleted: --badged hands overflow:hidden down to the media
   box (which is the only child that actually needs clipping) and gives it the
   two top corner radii the card used to impose. Net visual change to the card
   itself: none.

   Consequence to keep in mind — the badge now overhangs its grid column. The
   row gutter is 30px against a 12px overhang, so it clears its neighbour, but a
   larger negative offset or a tighter gutter would put it over the next card.

   The positioning context comes from .ffs-card itself, which is relative for the
   category pill's sake; this modifier only lifts the clip.

   The doubled class is deliberate and load-bearing. This block sits ~530 lines
   ABOVE the .ffs-card base rule, and `.ffs-card--badged` alone has exactly the
   same specificity as `.ffs-card` (0,0,1,0), so the later overflow:hidden won
   the cascade and clipped the badge no matter what this rule said. Writing it as
   .ffs-card.ffs-card--badged (0,0,2,0) makes it win on specificity instead of
   position, so it stays correct if either block moves. Do not simplify it. */
.ffs-card.ffs-card--badged {
  overflow: visible;
}
.ffs-card--badged .ffs-card__media {
  border-top-left-radius: var(--ffs-radius);
  border-top-right-radius: var(--ffs-radius);
}

.ffs-vitaplex-badge {
  position: absolute;
  top: -12px;
  right: -12px;
  z-index: 20;
  transition: transform .2s ease;
}
.ffs-vitaplex-badge img {
  height: 48px;
  width: auto;
  object-fit: contain;
  display: block;
  /* On the badge image rather than the wrapper: a drop-shadow filter traces the
     PNG's alpha, so the shadow follows the logo's silhouette instead of drawing
     a rectangle behind a transparent box. */
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, .18));
}
.ffs-card--badged:hover .ffs-vitaplex-badge,
.ffs-vitaplex-badge:hover {
  transform: scale(1.1);
}

/* The lift is decorative; anyone who has asked the OS to stop moving things
   keeps the badge and loses the animation. */
@media (prefers-reduced-motion: reduce) {
  .ffs-vitaplex-badge { transition: none; }
  .ffs-card--badged:hover .ffs-vitaplex-badge,
  .ffs-vitaplex-badge:hover { transform: none; }
}

/* The same sticker on product-detail.php, where it hangs off the main product
   image. The wrapper exists only to give the badge a positioning context that
   is NOT clipped: both media branches on that page hide their overflow (the
   photo one inline, the icon one via .ffs-detail-media), so a badge nested
   inside either would be cut off at the corner it is meant to overhang. */
.ffs-detail-media-wrap {
  position: relative;
}
.ffs-vitaplex-badge--detail {
  top: -12px;
  right: -12px;
}
.ffs-vitaplex-badge--detail img {
  height: 56px;
  /* Deeper than the card badge's shadow: this sits on a large pale media box
     rather than a small one, and the lighter shadow disappeared against it. */
  filter: drop-shadow(0 5px 12px rgba(0, 0, 0, .3));
}
.ffs-detail-media-wrap:hover .ffs-vitaplex-badge--detail {
  transform: scale(1.06);
}
@media (prefers-reduced-motion: reduce) {
  .ffs-detail-media-wrap:hover .ffs-vitaplex-badge--detail { transform: none; }
}

/* Certification strip under the product media on product-detail.php.

   Light panel now, paired with the blue-artwork logo variants in
   certified-logos-blue/. The panel and the artwork are one decision, not two:
   the blue marks are only legible on a light ground, exactly as the white
   silhouettes were only legible on the old blue ground. If this ever goes dark
   again, the <img> srcs have to move back to certified-logos-white/ in the same
   change — a blue-on-blue logo is invisible.

   No filter touches any logo. The three FFS marks are flat brand blue and the
   two Moody marks are the certifying body's full-colour artwork (UKAS crown,
   red Standards Malaysia oval, registration numbers); a contrast/invert filter
   that "unified" them would destroy the second group. They render native. */
.ffs-detail-certs {
  margin-top: 24px;
  padding: 16px 18px;
  border-radius: 14px;
  background: #fff;
  border: 1px solid var(--ffs-border);
}
.ffs-detail-certs__label {
  margin: 0 0 14px;
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  /* Dark ink on the light panel, tied to the brand blue rather than a neutral
     slate so the label belongs to the marks below it. */
  color: var(--ffs-ink);
}
/* Single row, all marks at one box height. flex-wrap so a narrow column drops
   to a second line instead of forcing a scrollbar; at the col-lg-6 width the
   five 40px marks and their gaps come to ~330px and stay on one line. */
.ffs-detail-certs__row {
  display: flex;
  flex-flow: row wrap;
  align-items: center;
  justify-content: flex-start;
  gap: 18px 24px;
}
.ffs-detail-certs__row img {
  /* 52px = the requested h-10 (40px) plus 30%. Equal BOX height, not equal ink:
     these five canvases are all 300x300 but the mark inside fills between 62%
     (Moody HACCP) and 92% (Halal) of it, so the flatter marks read a little
     smaller. That is the source artwork, and forcing per-logo heights to
     equalise the ink would be fragile the moment a file is reissued. */
  height: 52px;
  width: auto;
  object-fit: contain;
}
/* .ffs-detail-certs__item / __code — the one-off wrapper that put the Halal
   registration code under its logo — was removed 2026-09-07 along with the
   markup that used it. See the comment on the accreditation-standards row
   in accreditation.php. */
.ffs-detail-certs__note {
  margin: 16px 0 0;
  font-size: .8125rem;
  color: var(--agriox-color-1);
}
.ffs-detail-certs__note a {
  color: var(--ffs-ink);
  text-decoration: underline;
}
.ffs-detail-certs__note a:hover,
.ffs-detail-certs__note a:focus {
  color: var(--agriox-base-dark);
}

@media (max-width: 575.98px) {
  .ffs-detail-certs__row {
    gap: 14px 18px;
  }
  .ffs-detail-certs__row img { height: 44px; }
}

/* Specification list on product-detail.php. Machinery only for now — see the
   comment on the block in that file for why the figures are not labelled.

   Deliberately quieter than the "What FFS supplies" box directly beneath it:
   that box is the offer and should hold the eye, this is reference the reader
   scans once for whether the machine fits the counter. Hence a rule and a gold
   marker rather than a second bordered card, which would set two competing
   panels side by side. */
.ffs-specs {
  margin-top: 32px;
  padding-top: 24px;
  border-top: 1px solid var(--ffs-border);
}
.ffs-specs__title {
  font-size: 1.0625rem;
  margin: 0 0 12px;
}
.ffs-specs__list {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: .9375rem;
  line-height: 1.55;
}
.ffs-specs__list li {
  position: relative;
  padding-left: 18px;
}
.ffs-specs__list li + li { margin-top: 6px; }
.ffs-specs__list li::before {
  content: "";
  position: absolute;
  left: 0;
  /* Optically centred on the first line of text rather than the box: a spec
     that wraps to two lines keeps its marker beside line one. */
  top: .6em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ffs-gold);
}

/* Left column of the Regulation block on accreditation.php — the intro and the
   accreditation block, with that block pinned to the column's bottom edge so it
   finishes on the same line as the third law card in the column beside it
   ("Food Hygiene Regulations 2009").

   How the alignment holds, because it looks more fragile than it is:
   Bootstrap columns are flex items under align-items:stretch, so at lg+ both
   columns are already the full row height. The right column's <ul> carries
   margin:0 from .ffs-certs, so its bottom edge IS the third card's bottom edge,
   and therefore the row's. space-between then drops this column's last child
   onto that same line. Nothing here measures or hard-codes a height, so the
   cards can grow their copy freely and the two edges stay together.

   The catch, and why .ffs-accred-laws below is not optional: stretch makes both
   columns as tall as the ROW, and the row is as tall as whichever column has
   more content. Measured at 1400px that was the LEFT one — the intro plus the
   logo block ran 41px past the three law cards — so space-between alone had
   nothing to distribute and the logos finished 41px low. The fix is to let the
   law list fill its column too: .ffs-certs--stacked is a grid with
   grid-auto-rows:1fr, so given a definite height it hands the extra space to
   the three rows equally and the third card's bottom lands on the row's bottom.
   Both edges are then the row's, whichever column happens to be the tall one at
   a given width, so this holds as the copy changes rather than at 1400px only.
   That matters here: the logo block has been flat, then carded, then flat again
   inside a week, and each of those changes its height by the 32px of padding.
   The alignment survives all three because nothing in it is a measurement.

   Two conditions it does depend on, both cheap to break by accident:
   the left column must have exactly two children (see the note in
   accreditation.php), and .ffs-certs must keep margin:0 — give the <ul> a
   bottom margin and the logos will sit that far below the third card.

   Gated at lg because below it the columns stack, each takes its own content
   height, and there is no free space to distribute — space-between would then
   do nothing on a good day and stretch a short column oddly on a bad one. */
@media (min-width: 992px) {
  .ffs-accred-col {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }
  .ffs-accred-laws {
    display: flex;
  }
  .ffs-accred-laws > .ffs-certs--stacked {
    flex: 1;
  }
}

/* Accreditation page logo row (accreditation.php, under "The law we
   manufacture under"). Same logo row as the product detail banner — the
   component is reused deliberately so the two pages cannot drift apart — but it
   lives in a narrow col-lg-5 column rather than a half-width one, and five
   marks at the detail banner's 52px will not fit on one line there. h-9 (36px)
   does; flex-wrap stays as the graceful fallback for anything narrower than the
   column itself.

   UNLIKE the product-detail instance, this one is flat: the white ground, the
   border, the radius, the shadow and the padding are all cancelled so the label
   and marks sit straight on the section tint. Every one of those values is
   inherited from .ffs-detail-certs, which product-detail.php shares, so the
   cancelling happens here in the modifier and the base rule is left alone.

   This block has now gone flat → carded → flat inside a week, all three at the
   client's request, so the switch is worth spelling out. Card: delete the five
   declarations below and add `padding:16px; border-radius:16px; box-shadow:
   var(--ffs-shadow)`. Flat: what is written here. Either way the bottom-edge
   alignment above holds by itself — it does not know or care how tall this
   block is.

   Flat is safe as far as the artwork goes: all five PNGs are
   transparent-background (checked, 69-81% alpha, all four corners clear), so
   none of them brings a white rectangle of its own onto the tint. If a mark is
   ever reissued with a baked-in white ground, this is where it will show, and
   the fix is to fix the asset rather than to restore the card. */
.ffs-accred-logos {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.ffs-accred-logos .ffs-detail-certs__label {
  /* 14px in the base rule, where the card's own padding was doing part of the
     work of holding the label off the marks. Without it the label needs to keep
     that distance itself. */
  margin-bottom: 12px;
}
/* A tight left-aligned cluster, not a row spread across the column.

   This briefly ran justify-content:space-between to make the row's right edge
   line up with the card stack opposite. It bought that edge at the cost of a
   ~130px hole between each mark, and five marks strung out that far stop
   reading as one group — which is the whole job of the label above them. Fixed
   gap wins; the ragged right edge is the correct trade (August 2026, client
   request). Both are one line, so neither affects the bottom alignment.

   16px is the client's gap-4 rather than the base rule's 24px: the marks are
   36px here against 52px there, and the base gap around smaller marks was what
   made them read as separate in the first place. */
.ffs-accred-logos .ffs-detail-certs__row {
  justify-content: flex-start;
  gap: 16px;
}
.ffs-accred-logos .ffs-detail-certs__row img {
  height: 36px;
}

/* Best Seller badge — bottom-left of the media, orange gradient pill. Inset
   12px from both edges rather than flush to the corner, which is why the shape
   is a full pill: a corner-notch radius only reads as intentional when the
   badge actually sits in the corner. */
.ffs-best-seller-badge {
  position: absolute;
  bottom: 12px;
  left: 12px;
  z-index: 10;
  background: linear-gradient(90deg, #e86100 0%, #f2a900 100%);
  color: #fff;
  font-family: var(--agriox-font);
  font-size: .6875rem;
  font-weight: 700;
  /* Same reason as .ffs-card__tag: body's 34px line-height is a length, not a
     ratio, so it inherits intact and inflates the banner to ~46px. */
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: .08em;
  padding: 6px 10px;
  border-radius: 999px;
  box-shadow: 0 2px 6px rgba(232, 97, 0, .35);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   4. Buttons
   -------------------------------------------------------------------------- */
/* ONE button size, everywhere. Every variant below inherits this box and
   changes colour only — no variant may re-declare font-size, padding, gap or
   min-height, and no page may override them locally. If a button needs to look
   different, it needs a new colour variant, not a new size.

   Two sanctioned exceptions, and only two:

   1. .ffs-btn--flush, below, zeroes the LEFT padding so its label lines up with
      the body copy above it. It changes one side only, and it is opt-in per
      button.

   2. .ffs-vbanner .ffs-btn (section 12f) scales the box UP — 20px/700 in a
      taller target. That banner sets its headline at 48px and its bullets at
      20px, and a 19px CTA under 20px body copy reads as a de-emphasised
      control, which is the opposite of what a primary CTA should do. Added
      August 2026 at the client's request.

      Note the asymmetry: scaling this box UP is always safe, scaling it DOWN
      is not something to do without re-checking first. Any future local
      override may only increase it.

   Anything beyond these belongs in a colour variant, not a new size.

   19px/700 was what made a white label legal on the CTA fill, for two
   colours running: Tertiary Orange (3.42:1, passed large-text at this size)
   and #8C9600 "Mr Magic Green" (3.24:1, same). It no longer performs that
   job as of 2026-09-08: white on the current fill, #73C92D, is 2.07:1 —
   short of the 3:1 large-text floor at ANY size, so there is no size this
   button could be that would make its current colour-and-label pairing
   legal. The rule stays at 19px/700 anyway, both because shrinking it
   would only make a real problem look smaller, and for the non-AA reason
   the .ffs-vbanner bullet above gives (a shrunk CTA reads as de-emphasised
   beside larger body copy). See the --ffs-cta token comment and the
   section 1 palette note for the full history and the fix if this ever
   needs to pass AA — it is not something changing this rule can solve.

   (This button's label has flipped direction twice in as many days, all
   2026-09-07–08: near-black landed first with the green fill, comfortably
   legal at any size; the client asked for white back within hours, legal
   only at large-text on that fill; the fill itself then changed to
   #73C92D, at which point white stopped being legal at all. Each change is
   documented where it was made — this note describes today's design, not
   the sequence that reached it.) */
.ffs-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 56px;
  padding: 15px 32px;
  border: 2px solid transparent;
  border-radius: var(--ffs-radius);
  font-family: var(--agriox-font);
  font-size: 1.1875rem;   /* 19px — see note above before changing */
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  cursor: pointer;
  transition: background-color .18s ease, color .18s ease, border-color .18s ease;
}

/* Buttons sitting side by side get one gap value too, so a pair of CTAs is
   spaced identically on the hero, in a panel and in the footer. Every group
   wrapper below reads --ffs-btn-gap; nothing sets its own.

   This is a flex gap rather than a `.ffs-btn + .ffs-btn` margin on purpose —
   a sibling margin would stack on top of the gap inside .ffs-hero__actions and
   make the hero pair wider-spaced than every other pair on the site, which is
   exactly the inconsistency this is meant to remove. */
.ffs-btn-group,
.ffs-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ffs-btn-gap);
}

/* The group also owns the space between itself and the copy above it, for the
   same reason it owns the gap: contact.php, product-detail.php and the footer
   each used to set their own (24px, 28px, and a <p> margin from the template),
   which is how a Shopee button ended up sitting a different distance from its
   heading on all three pages. 24px matches the `mt-4` every other section CTA
   on the site is spaced by. Override it locally only with a real reason. */
.ffs-btn-group { margin: 24px 0 0; }

/* Centres the buttons within the group. The group is a flex row, so this is
   justify-content rather than text-align — and it is a modifier rather than a
   change to the base rule because most groups on the site sit under
   left-aligned copy and are meant to line up with it. */
.ffs-btn-group--center { justify-content: center; }

/* Product detail: buttons sit side-by-side on desktop, stack on mobile.
   Both buttons share equal width above the mobile breakpoint. */
.ffs-btn-group--detail {
  justify-content: center;
}
.ffs-btn-group--detail .ffs-btn--equal {
  flex: 1 1 0;
  min-width: 0; /* allows flex shrink below content width */
}

@media (max-width: 575.98px) {
  .ffs-btn-group--detail .ffs-btn--equal {
    flex: 1 1 100%;
  }
}

/* Phones: every button in a group goes full-width and shares one edge, so a
   pair reads as a stack of equal bars rather than two boxes of whatever width
   their labels happened to shrink-wrap to. At 390px that mismatch was at its
   worst next to the Shopee CTAs, whose labels are the longest on the site —
   "Buy retail packs on Shopee" set a box roughly twice the width of the "Get
   directions" button above it.

   The 32px side padding goes to 20px here for the same labels: at 390px, minus
   the grid gutter, 32px a side pushed that label onto a third line. Padding is
   the only part of the box this touches — the 19px/700 and the 56px floor are
   what keep a white label legal on the orange fills (see the base rule), and
   they are deliberately left alone. This is also why the rule is scoped to
   grouped buttons: a lone .ffs-btn in a sentence keeps its natural width. */
@media (max-width: 575.98px) {
  .ffs-btn-group .ffs-btn,
  .ffs-hero__actions .ffs-btn {
    flex: 1 1 100%;
    padding-left: 20px;
    padding-right: 20px;
  }
  /* --flush is opt-in flush-left against body copy; full-width must not give
     it a left inset back. */
  .ffs-btn-group .ffs-btn--flush,
  .ffs-hero__actions .ffs-btn--flush {
    padding-left: 0;
  }
}

/* Flush-left label. Drops the left padding to 0 so the button's TEXT — not its
   fill — starts on the same vertical line as the paragraph above it. Without
   this the label sits 32px in, and against a left-aligned block of copy that
   reads as an indent.

   The base rule centres the label with justify-content, which would fight this
   the moment the button is ever given a width, so the alignment is restated
   here rather than left to depend on the box shrink-wrapping its text.

   Two things this deliberately does not do. It keeps the right padding, so the
   fill still has somewhere to breathe. And it keeps the 2px transparent
   border, which offsets the label by 2px: removing it would buy exact optical
   alignment at the cost of making this variant a different height from every
   other button, which is the one thing items 7/11 asked me not to do.

   Use sparingly, and only on a button that directly follows body copy. */
.ffs-btn--flush {
  padding-left: 0;
  justify-content: flex-start;
  text-align: left;
}

/* The high-priority CTA — "Enquire now", "Explore products", "Contact us".
   #73C92D as of 2026-09-08 (was #8C9600 "Mr Magic Green", 2026-09-07; was
   Tertiary Orange before that), and the loudest thing on any page it
   appears on, which is why nothing else in this stylesheet is allowed to
   use it.

   White label on #73C92D: 2.07:1 at rest — a WCAG AA FAIL, below even the
   3:1 large-text floor the base rule's 19px/700 exists to guarantee, and
   the first colour in this token's history that fails it. This is the
   client's own explicit fill-and-label pair, supplied together on
   2026-09-08 as a fixed instruction rather than picked from a range with
   AA in mind — the two previous greens (#8C9600 white 3.24:1, near-black
   5.74:1) were both chosen partly to clear that floor; this one was not
   checked against it before being handed over, and is implemented as given
   rather than adjusted. See the section 1 palette note for the full
   contrast table and the fix if this ever needs to pass: either a near-
   black label (--ffs-on-brand — passes comfortably on this fill) or a
   darker green with white kept.

   Hover DEEPENS the fill to #569621 (white label, 3.63:1 — still short of
   normal-text AA, and still short of even large-text AA, but less wrong
   than resting state) rather than lightening it — under a white label,
   lighter is always the wrong direction, independent of whether the rest
   state passes anything. */
.ffs-btn--primary {
  background: var(--ffs-cta);
  color: #fff;
}
.ffs-btn--primary:hover,
.ffs-btn--primary:focus {
  background: var(--ffs-cta-hover);
  color: #fff;
}

/* Secondary action — Secondary Gold. Near-black label at 9.26:1.

   Gold on white is 2.01:1, so unlike the orange CTA this fill does NOT clear
   the 3:1 boundary contrast on its own; the darkened-gold border supplies it
   (4.16:1). Drop the border and the button dissolves into the page for anyone
   with low contrast sensitivity. */
.ffs-btn--gold {
  background: var(--ffs-gold);
  border-color: var(--ffs-gold-dark);
  color: var(--ffs-on-brand);
}
.ffs-btn--gold:hover,
.ffs-btn--gold:focus {
  background: var(--ffs-gold-hover);
  border-color: var(--ffs-gold-dark);
  color: var(--ffs-on-brand);
}

.ffs-btn--ghost {
  background: transparent;
  border-color: rgba(255, 255, 255, .55);
  color: #fff;
}
.ffs-btn--ghost:hover,
.ffs-btn--ghost:focus {
  background: #fff;
  border-color: #fff;
  color: var(--agriox-primary);
}

/* Blue-filled, white label at 10.66:1. Hover deepens to --agriox-base-dark
   (13.16:1) — NOT to --agriox-base, which now holds the same value as
   --agriox-primary and would make the hover a no-op. */
.ffs-btn--solid {
  background: var(--agriox-primary);
  color: #fff;
}
.ffs-btn--solid:hover,
.ffs-btn--solid:focus {
  background: var(--agriox-base-dark);
  color: #fff;
}

/* Tertiary action on a LIGHT background. --ghost is the dark-background
   equivalent; using it here gives a white border on white.

   The border is blue rather than --ffs-border: the neutral gray is 1.57:1 on
   white, fine as a decorative rule between cards but not as the boundary of
   an interactive control, which WCAG 1.4.11 wants at 3:1. */
.ffs-btn--outline {
  background: transparent;
  border-color: var(--agriox-primary);
  color: var(--agriox-primary);
}
.ffs-btn--outline:hover,
.ffs-btn--outline:focus {
  border-color: var(--agriox-primary);
  background: var(--agriox-primary);
  color: #fff;
}

/* Shopee action. This is the storefront CTA everywhere it appears in the body
   of a page — product detail, contact, the reviews section, the footer — and
   it is a colour variant precisely so it stops being a different size from
   the button it stands next to. It was a 14px pill; on product-detail.php
   that put it shoulder to shoulder with a 19px/56px .ffs-btn--primary in the
   same row, which is the mismatch items 7/11 were about.

   The ONE Shopee link that is not this variant is the badge in the header's
   utility bar (see .ffs-shopee, section 8). That bar is 40-odd pixels tall
   and a 56px button would not fit in it, let alone belong there — it is
   chrome sitting beside the phone number and opening hours, not a call to
   action. Standardising it into a button would break the header rather than
   tidy it.

   Colour is --ffs-cta — the same fill --primary carries, by instruction:
   every Shopee CTA on the site is one colour, full stop, and that colour is
   whatever --ffs-cta currently holds (#73C92D as of 2026-09-08; #8C9600
   "Mr Magic Green" for one day before that; Tertiary Orange #E86100 before
   both). This variant is therefore a semantic alias, not a different
   colour; it stays a separate class so the storefront CTAs remain
   greppable, and this is the "repainted in one place later" this comment
   used to promise — two swaps in two days proved it: one token edit each
   time, no change needed here.

   Two consequences to know about before touching this.

   White label on #73C92D: 2.07:1 at rest, 3.63:1 on hover — same numbers,
   same AA FAIL and same size dependency as .ffs-btn--primary, since it is
   the same fill and the same 19px/700 base rule. See that rule's comment
   and the section 1 palette note for the full history and the fix if this
   ever needs to pass AA. Do not use this class on anything below 18.66px
   bold — the fail is already real at that size; smaller would only widen it.

   On product-detail.php "Enquire about <product>" and "Buy on Shopee" sit
   side by side in identical fills, same as before this colour changed — the
   icon is still what distinguishes them, not the fill.

   --ffs-shopee / --ffs-shopee-hover are NOT unused after this: they still
   carry the header utility-bar badge (section 8), which stays Tertiary
   Orange regardless of what --ffs-cta holds — see the section 1 palette
   note and the badge's own comment above --ffs-shopee. */
.ffs-btn--shopee {
  background: var(--ffs-cta);
  color: #fff;
}
.ffs-btn--shopee:hover,
.ffs-btn--shopee:focus {
  background: var(--ffs-cta-hover);
  color: #fff;
}

/* Sized in em, not px, so the glyph tracks the label rather than pinning a
   second size into a variant. A 1em bag next to 19px bold caps reads heavy —
   the glyph fills its em box where the letters do not. */
.ffs-btn--shopee i { font-size: .9em; }

/* --------------------------------------------------------------------------
   5. Product grid + category filter
   -------------------------------------------------------------------------- */

/* MOQ notice — products.php only, directly under the page hero. Added
   2026-08-29 per the client's feedback spreadsheet: a standing notice that
   the catalogue's minimum order is 1 pallet (40 cartons), with a Shopee link
   for anyone ordering less.

   Background is --ffs-surface with --agriox-primary text: that pair measures
   9.18:1 (heading) and 7.53:1 (body) per the palette note in section 1, so it
   reads as a calm, permanent fixture rather than a loud warning banner. The
   left border is the same blue rather than the brand gold — gold on this
   ground would fail the 3:1 a meaningful border/icon needs (2.01:1 on white,
   and --ffs-surface is lighter still). */
.ffs-moq-notice {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  margin-bottom: 40px;
  padding: 18px 22px;
  border-left: 4px solid var(--agriox-primary);
  border-radius: 4px;
  background: var(--ffs-surface);
}
.ffs-moq-notice i {
  flex-shrink: 0;
  margin-top: 3px;
  font-size: 1.125rem;
  color: var(--agriox-primary);
}
.ffs-moq-notice p {
  margin: 0;
  color: var(--agriox-color-1);
  font-size: .9375rem;
  line-height: 1.6;
}
.ffs-moq-notice strong {
  color: var(--agriox-primary);
}
.ffs-moq-notice a {
  color: var(--agriox-primary);
  font-weight: 700;
  text-decoration: underline;
}
.ffs-moq-notice a:hover,
.ffs-moq-notice a:focus {
  text-decoration: none;
}

.ffs-filter {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-bottom: 44px;
  padding: 0;
  list-style: none;
}

.ffs-filter button {
  min-height: 44px;
  padding: 10px 22px;
  border: 1px solid var(--ffs-border);
  border-radius: 999px;
  background: #fff;
  color: var(--agriox-color-1);
  font-size: .9375rem;
  font-weight: 500;
  cursor: pointer;
  transition: all .18s ease;
}
.ffs-filter button:hover {
  border-color: var(--agriox-primary);
  color: var(--agriox-primary);
}
.ffs-filter button[aria-pressed="true"] {
  background: var(--agriox-primary);
  border-color: var(--agriox-primary);
  color: #fff;
}

/* Category tab bar (products.php). A horizontal pill rail that scrolls sideways
   on narrow screens rather than wrapping — eight tabs would wrap to three rows
   on a phone, which reads as a menu, not a filter.

   The rail carries <button>s: products.php filters the 41 product cards in
   place rather than navigating, and initProductFilter() in ffs.js drives it off
   button[data-cat] + aria-pressed. Anchors are still styled identically because
   the same rail is useful as pure navigation elsewhere, and because a rail of
   links is the correct no-JS fallback if one is ever needed.

   Both states are matched below — aria-current for the anchor form, aria-pressed
   for the button form — so the two spellings cannot drift apart visually.

   scroll-snap keeps a tapped pill from landing half-clipped at the rail edge,
   and the negative margin lets the rail bleed to the container edge so a
   mid-scroll pill is visibly cut off (the cue that there is more to scroll to)
   instead of ending flush with the text column. */
.ffs-tabs {
  display: flex;
  gap: 10px;
  margin: 0 -20px 40px;
  padding: 4px 20px 14px;
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
.ffs-tabs::-webkit-scrollbar {
  height: 6px;
}
.ffs-tabs::-webkit-scrollbar-thumb {
  border-radius: 999px;
  background: var(--ffs-border);
}
.ffs-tabs li {
  scroll-snap-align: start;
}
.ffs-tabs a,
.ffs-tabs button {
  display: inline-block;
  white-space: nowrap;
  min-height: 44px;
  padding: 10px 22px;
  border: 1px solid var(--ffs-border);
  border-radius: 999px;
  background: #fff;
  color: var(--agriox-color-1);
  font-size: .9375rem;
  font-weight: 500;
  font-family: inherit;
  line-height: 1.4;
  text-decoration: none;
  cursor: pointer;
  transition: all .18s ease;
}
.ffs-tabs a:hover,
.ffs-tabs button:hover {
  border-color: var(--agriox-primary);
  color: var(--agriox-primary);
}
.ffs-tabs a[aria-current="true"],
.ffs-tabs button[aria-pressed="true"] {
  background: var(--agriox-primary);
  border-color: var(--agriox-primary);
  color: #fff;
}

/* The visible count under the heading, kept in step by ffs.js. It is an
   aria-live region, so it announces "8 products" after a filter without moving
   focus off the pill the user just pressed. */
.ffs-filter-count {
  margin: 0 0 28px;
  font-size: .875rem;
  font-weight: 500;
  color: var(--agriox-color-1);
}

/* Anchor targets sit under the sticky header once it engages, so a jump to
   #seasoning would land the card top behind the bar. Offset the landing by
   roughly the header's height. Applies to the id'd category cards and the
   #all row the "All" tab returns to. */
.ffs-card[id],
#all {
  scroll-margin-top: 120px;
}

.ffs-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 26px;
}

.ffs-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  /* Load-bearing, not decorative: .ffs-card__tag is absolutely positioned and
     nothing else in its ancestor chain is positioned (.ffs-card__body and
     .auto-container are both static), so without this the category pill escapes
     all the way to .page-wrapper and every unbadged card's tag piles up in the
     top-left corner of the document. It only looked correct on the three
     Vitaplex cards because --badged happened to set position:relative, and on
     hover, where the translateY transform creates a containing block of its own. */
  position: relative;
  background: #fff;
  border: 1px solid var(--ffs-border);
  border-radius: var(--ffs-radius);
  overflow: hidden;
  transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
}
.ffs-card:hover {
  transform: translateY(-4px);
  border-color: transparent;
  box-shadow: var(--ffs-shadow);
}

/* 3:2, not square. The product photographs are landscape crops out of the
   original flyers (roughly 1.6:1 and 2.7:1); a square box zoomed the wider
   family so far in that the product left the frame. */
.ffs-card__media {
  position: relative;
  aspect-ratio: 3 / 2;
  background: var(--ffs-surface);
  overflow: hidden;
}
.ffs-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --------------------------------------------------------------------------
   Category cards

   These carry the seven supply categories while product photography is
   outstanding. Two variants live here:

     .ffs-card__media--placeholder  Restores the top media strip on cards that
       need it (the home page's three category cards), with a diagonal tint so
       an empty slot reads as "image pending" rather than as a failed image.

     .ffs-cat-card--featured / .ffs-cat-card__icon / __flag / __main
       An icon-led lockup used on the products.php directory grid and any
       featured/full-width card the home page might promote in future.

   Sizing, the hover lift and the bottom-aligned link all come from .ffs-card
   above — the rules below add only what those base cards do not cover.
   -------------------------------------------------------------------------- */

/* Placeholder photograph. The tint is a shade off the surface fill so the box
   reads as pending rather than broken, and the panel keeps the 3:2 box above
   the body so the grid does not reflow when the real shots land.

   The modifier is applied by PHP only when the category has no image, rather
   than by :has(img) here — a selector list containing an unsupported :has()
   drops the whole rule in older engines, and this needs to degrade to plain
   surface fill at worst, never to a rule that silently stops applying. */
.ffs-card__media--placeholder {
  background:
    repeating-linear-gradient(
      135deg,
      var(--ffs-surface) 0,
      var(--ffs-surface) 10px,
      rgba(var(--agriox-primary-rgb), .05) 10px,
      rgba(var(--agriox-primary-rgb), .05) 20px
    );
}

/* Icon tile, used wherever a product has no photograph of its own — which is
   currently all 41 lines. The category glyph sits centred on the surface fill
   at a size that reads as a deliberate illustration rather than as a missing
   asset.

   Distinct from --placeholder above: that one signals "photo pending" with a
   diagonal hatch, and suits a category card whose real content is the text
   below it. A product card is the product, so a blank hatched box reads as
   broken there. The glyph gives it a subject.

   Swapping any product's 'image' from null to a path in data/products.php
   replaces this with the real photograph, no CSS change needed. */
.ffs-card__media--icon {
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    linear-gradient(
      160deg,
      var(--ffs-surface) 0%,
      rgba(var(--agriox-primary-rgb), .07) 100%
    );
}
.ffs-card__media--icon i {
  font-size: 2.75rem;
  line-height: 1;
  color: var(--agriox-primary);
  opacity: .45;
  transition: opacity .22s ease, transform .22s ease;
}
/* The card already lifts on hover; bringing the glyph up with it stops the
   tile reading as a static image pasted onto a moving card. */
.ffs-card:hover .ffs-card__media--icon i {
  opacity: .7;
  transform: scale(1.06);
}

/* The same tile at detail-page scale, where it stands in for the product
   photograph beside the description. Taller than the 3:2 card box because it
   sits next to a column of running text and has to hold that height. */
.ffs-detail-media {
  border-radius: var(--ffs-radius);
  overflow: hidden;
  background: var(--ffs-surface);
}
.ffs-detail-media--icon {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 320px;
  background:
    linear-gradient(
      160deg,
      var(--ffs-surface) 0%,
      rgba(var(--agriox-primary-rgb), .07) 100%
    );
}
.ffs-detail-media--icon i {
  font-size: 5rem;
  line-height: 1;
  color: var(--agriox-primary);
  opacity: .4;
}

/* CTA card — the fourth slot on the home page grid. It carries no category:
   it just points at products.php. The visual is a bordered dashed panel with
   the label centred vertically, so it reads as "and there is more" without
   pretending to be a category itself. */
.ffs-cta-card {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-height: 200px;
  padding: 32px 24px;
  border: 1.5px dashed rgba(var(--agriox-primary-rgb), .35);
  border-radius: var(--ffs-radius);
  background: rgba(var(--agriox-primary-rgb), .04);
  color: var(--agriox-primary);
  font-weight: 700;
  text-align: center;
  text-decoration: none;
  transition: background-color .2s ease, border-color .2s ease, transform .2s ease;
}
.ffs-cta-card:hover,
.ffs-cta-card:focus-visible {
  background: rgba(var(--agriox-primary-rgb), .08);
  border-color: var(--agriox-primary);
  color: var(--agriox-primary);
  transform: translateY(-4px);
  text-decoration: none;
}
.ffs-cta-card__label {
  font-size: 1.0625rem;
}
.ffs-cta-card i {
  margin-left: 8px;
  transition: transform .18s ease;
}
.ffs-cta-card:hover i { transform: translateX(4px); }

.ffs-cat-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* .ffs-card__body is a flex column, whose default align-items:stretch would
     pull this to the full card width. flex:0 0 auto only pins the main axis. */
  align-self: flex-start;
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  margin-bottom: 16px;
  border-radius: 14px;
  background: rgba(var(--agriox-primary-rgb), .08);
  color: var(--agriox-primary);
  font-size: 1.375rem;
  transition: background-color .2s ease, color .2s ease;
}

/* Inverts with the card lift, so the icon travels with the panel instead of
   sitting on it like a flat sticker. */
.ffs-card:hover .ffs-cat-card__icon {
  background: var(--agriox-primary);
  color: #fff;
}

/* Flagship marker on the featured card. Same pill as .ffs-card__tag but tinted
   rather than solid — it labels the card, it does not overlay a photograph. */
.ffs-cat-card__flag {
  display: inline-block;
  margin-bottom: 10px;
  padding: 4px 11px;
  border-radius: 999px;
  background: rgba(var(--agriox-primary-rgb), .1);
  color: var(--agriox-primary);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.ffs-cat-card--featured .ffs-card__body {
  padding: 28px 30px;
}

/* Below 768px the featured card stays a normal stacked card — the horizontal
   lockup would leave the text in a column barely wider than the icon. */
@media (min-width: 768px) {
  .ffs-cat-card--featured .ffs-card__body {
    flex-direction: row;
    align-items: center;
    gap: 28px;
  }
  .ffs-cat-card--featured .ffs-cat-card__icon {
    width: 68px;
    height: 68px;
    margin-bottom: 0;
    font-size: 1.75rem;
  }
  .ffs-cat-card--featured .ffs-cat-card__main {
    flex: 1;
  }
  .ffs-cat-card--featured .ffs-card__text {
    margin-bottom: 0;
  }
  /* margin-top:auto on .ffs-card__link pushes to the bottom of a *column*.
     On this row-axis card that does nothing useful, so it is reset and the
     link becomes the trailing item in the row instead. */
  .ffs-cat-card--featured .ffs-card__link {
    margin-top: 0;
    white-space: nowrap;
  }
}

/* Category pill, overlaid on the top-left of the media box. It is written inside
   .ffs-card__body but positions against .ffs-card, which is the only positioned
   ancestor — see the note on .ffs-card. */
.ffs-card__tag {
  position: absolute;
  top: 12px;
  left: 12px;
  padding: 5px 12px;
  border-radius: 999px;
  background: rgba(var(--agriox-primary-rgb), .92);
  color: #fff;
  font-size: .6875rem;
  font-weight: 700;
  /* body sets line-height in px, not a ratio, so an 11px label would otherwise
     inherit a 34px line box and sit in a pill three times taller than its text. */
  line-height: 1;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.ffs-card__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 20px 22px 24px;
}

.ffs-card__title {
  margin: 0 0 8px;
  font-size: 1.125rem;
}
.ffs-card__title a { color: var(--ffs-ink); }
/* --ffs-ink and --agriox-primary now hold the same blue, so the old colour
   hover was a no-op. Deepen it and underline: the underline is what actually
   carries the state for anyone who cannot separate the two blues. */
.ffs-card__title a:hover,
.ffs-card__title a:focus-visible {
  color: var(--agriox-base-dark);
  text-decoration: underline;
}

.ffs-card__text {
  margin: 0 0 18px;
  font-size: .9375rem;
  line-height: 1.55;
}

/* Specification subtitle on a product card: pack format on a premix,
   dimensions and power on a machine. Sits between the title and the
   description. The title already carries 8px beneath it, so this only sets its
   own 9px below: title, spec, description should read as one block rather than
   three evenly spaced paragraphs.

   Flex rather than a pipe-joined string so a spec that will not fit wraps as a
   whole figure — "L 447 x W 570 x H 270 mm" never breaks across two lines, and
   the separator never ends up stranded at the right margin. The separator is a
   pseudo-element on every item but the first, so it survives reordering and
   there is no trailing divider to strip. */
.ffs-card__specs {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 0;
  margin: 0 0 9px;
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .01em;
  color: var(--agriox-color-1);
}
.ffs-card__specs span {
  white-space: nowrap;
}
.ffs-card__specs span + span::before {
  content: "|";
  margin: 0 8px;
  color: var(--ffs-border);
}

.ffs-card__link {
  margin-top: auto;
  font-weight: 700;
  font-size: .9375rem;
  color: var(--agriox-primary);
}
.ffs-card__link i {
  margin-left: 6px;
  transition: transform .18s ease;
}
.ffs-card__link:hover i { transform: translateX(4px); }

.ffs-grid__empty {
  grid-column: 1 / -1;
  padding: 60px 20px;
  text-align: center;
  color: var(--agriox-color-1);
}

/* The filter hides cards with the `hidden` attribute. Its UA default is only
   `display: none`, which any later `display` declaration outranks — and the
   grid columns are flex items inside .row, exactly the situation where a
   stray display rule would leave a "hidden" card on screen. Making it
   !important means a filtered-out card cannot come back through a specificity
   accident in the template CSS below it. */
[hidden] {
  display: none !important;
}

/* --------------------------------------------------------------------------
   6. WhatsApp floating button
   -------------------------------------------------------------------------- */
.ffs-wa {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 99;
  display: flex;
  align-items: center;
  gap: 10px;
  height: 56px;
  padding: 0 18px;
  border-radius: 999px;
  background: var(--ffs-whatsapp);
  color: var(--ffs-on-brand);
  font-weight: 700;
  font-size: .9375rem;
  box-shadow: 0 6px 20px rgba(37, 211, 102, .38);
  transition: background-color .18s ease, transform .18s ease, box-shadow .18s ease;
}
.ffs-wa:hover,
.ffs-wa:focus {
  background: var(--ffs-whatsapp-hover);
  color: var(--ffs-on-brand);
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(37, 211, 102, .46);
}
.ffs-wa i { font-size: 1.5rem; }

/* On phones collapse to a circle so it never covers form fields.
   Sits left of the template's scroll-to-top button, which is bottom-right. */
@media (max-width: 575.98px) {
  .ffs-wa {
    width: 56px;
    padding: 0;
    justify-content: center;
    right: 16px;
    bottom: 78px;
  }
  .ffs-wa__label { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .ffs-wa,
  .ffs-card,
  .ffs-card__link i { transition: none; }
  .ffs-wa:hover,
  .ffs-card:hover { transform: none; }
}

/* Speech bubble above the FAB.
   Ships with the HTML hidden attribute (not just opacity), so it does not
   appear when JS is blocked or still loading. ffs.js flips the attribute
   after the ten-second delay. role="status" is inert — it keeps the bubble
   out of the tab order and lets a screen reader hear it in passing without
   yanking the user out of whatever they were reading. */
.ffs-wa-bubble {
  position: fixed;
  right: 18px;
  bottom: 86px; /* 18px floor + 56px FAB + 12px gap */
  z-index: 99;
  max-width: 280px;
  padding: 14px 32px 14px 16px;
  border-radius: var(--ffs-radius);
  background: #fff;
  color: var(--agriox-color-1);
  font-size: .9375rem;
  line-height: 1.5;
  box-shadow: var(--ffs-shadow), 0 6px 20px rgba(0, 0, 0, .12);
  transform: translateY(8px);
  opacity: 0;
  transition: transform .35s ease, opacity .35s ease;
}
/* Arrow tail — a 10px right-pointing triangle rotated 45° and positioned
   just above the right-hand third of the FAB so it points straight at it. */
.ffs-wa-bubble::after {
  content: '';
  position: absolute;
  bottom: -6px;
  right: 26px;
  width: 12px;
  height: 12px;
  background: inherit;
  border-radius: 0 0 3px 0;
  transform: rotate(45deg);
  box-shadow: 3px 3px 4px rgba(0, 0, 0, .06);
}
.ffs-wa-bubble.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.ffs-wa-bubble__close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 24px;
  height: 24px;
  border: 0;
  padding: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--agriox-color-1);
  font-size: 13px;
  line-height: 24px;
  text-align: center;
  cursor: pointer;
  transition: background .15s ease;
}
.ffs-wa-bubble__close:hover,
.ffs-wa-bubble__close:focus-visible {
  background: var(--ffs-surface);
}

.ffs-wa-bubble__link {
  display: block;
  color: inherit;
  text-decoration: none;
}
.ffs-wa-bubble__link:hover { text-decoration: underline; }

@media (max-width: 575.98px) {
  .ffs-wa-bubble {
    right: 16px;
    bottom: 146px; /* 78px mobile FAB + 56px circle + 12px gap */
    max-width: calc(100vw - 48px);
    padding: 12px 28px 12px 14px;
    font-size: .875rem;
  }
  .ffs-wa-bubble::after { right: 20px; }
}

@media (prefers-reduced-motion: reduce) {
  .ffs-wa-bubble { transition: none; }
}

@media print {
  .ffs-wa-bubble { display: none !important; }
}

/* --------------------------------------------------------------------------
   7. Map facade — static image swapped for the real iframe on click
   -------------------------------------------------------------------------- */
.ffs-map {
  position: relative;
  aspect-ratio: 16 / 9;
  min-height: 320px;
  border-radius: var(--ffs-radius);
  overflow: hidden;
  background: var(--ffs-surface);
}
.ffs-map iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.ffs-map__facade {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  width: 100%;
  padding: 24px;
  border: 0;
  background: var(--ffs-surface);
  background-size: cover;
  background-position: center;
  cursor: pointer;
  text-align: center;
}
.ffs-map__facade::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(var(--agriox-primary-rgb), .12);
}
.ffs-map__facade-inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 22px 26px;
  border-radius: var(--ffs-radius);
  background: rgba(255, 255, 255, .95);
  box-shadow: var(--ffs-shadow);
  max-width: 420px;
}
.ffs-map__facade-icon {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--agriox-primary);
  color: #fff;
  font-size: 1.25rem;
}
.ffs-map__facade-note {
  font-size: .8125rem;
  color: var(--agriox-color-1);
  margin: 0;
}

/* --------------------------------------------------------------------------
   8. Shopee badge — header utility bar only

   Every other Shopee link on the site is .ffs-btn--shopee, a normal-sized
   button (section 4). This class is what is left over: the small pill in the
   blue utility bar at the top of every page, sitting in a 40px-tall strip
   beside the phone number and the opening hours. It is chrome, not a call to
   action, and it is the one place a 56px button cannot go.

   Because it is a 14px pill it cannot use WCAG's large-text exemption the way
   the 19px buttons can, which is the whole reason --ffs-shopee is a deepened
   orange rather than the brand's Tertiary Orange: white measures 4.79:1 on it
   and clears AA for normal text.
   -------------------------------------------------------------------------- */
.ffs-shopee {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 15px;
  border-radius: 999px;
  background: var(--ffs-shopee);
  color: #fff;
  font-size: .875rem;
  font-weight: 700;
  line-height: 1.2;
  transition: background-color .18s ease;
}
.ffs-shopee:hover,
.ffs-shopee:focus {
  background: var(--ffs-shopee-hover);
  color: #fff;
}
.ffs-shopee i { font-size: 1rem; }

/* `.main-header--one__top a` (0,1,1) outranks `.ffs-shopee` (0,1,0) and was
   repainting the label to white at 90% opacity. The label is white now, but
   90% white is not — it measured 3.4:1 on the old fill. These two carry the
   same declarations at 0,2,0 purely to win that. Do not fold them back into
   the base rule.

   The matching `.footer-one .ffs-shopee` pair that used to sit here is gone
   with the badge it guarded: the footer's Shopee link is a .ffs-btn--shopee
   now, and .ffs-btn variants set their own colour at the same weight the
   template's footer rules do not reach. */
.main-header--one__top .ffs-shopee {
  color: #fff;
}
.main-header--one__top .ffs-shopee:hover,
.main-header--one__top .ffs-shopee:focus {
  color: #fff;
}

/* The footer's focus ring is the blue one from section 2, which is the same
   blue as the footer ground — 1:1, i.e. no ring at all for a keyboard user.
   Same fix already applied to the hero and the panel sections: gold on the
   footer blue measures 5.30:1, well past the 3:1 an indicator needs.

   Scoped to the whole footer rather than to the button, because the contact
   links, the explore list and the social icons all share the problem and all
   share the ground. */
.footer-one a:focus-visible,
.footer-one button:focus-visible {
  outline-color: var(--agriox-base-on-dark);
}

/* --------------------------------------------------------------------------
   8b. Shopee customer reviews — scroll-snap carousel

   Rendered by partials/reviews.php from data/reviews.php.

   This is a CSS carousel, not a slider library. The track is one overflowing
   flex row with scroll snapping, so it works on touch and trackpad with no
   JavaScript at all; assets/js/ffs.js only adds the arrow buttons on top and
   reveals them once it has measured an actually-overflowing track. Nothing
   here depends on that script running — the failure mode is a plain
   swipeable strip, not a broken one.

   Cards are sized in thirds of the VIEWPORT rather than of the track. The
   track is a block box inside the scroll container, so its width is the
   visible width and the items overflow it; a percentage flex-basis therefore
   resolves against what the reader can see, which is what "3 across" means.

   Note the deliberate ugliness of .ffs-review--sample: placeholder rows are
   meant to be impossible to miss in a client review, so nobody signs off a
   page carrying invented testimonials. Delete nothing there when the samples
   go — the rules simply stop matching. As of the real Shopee reviews landing
   in data/reviews.php, they no longer do.
   -------------------------------------------------------------------------- */
.ffs-reviews__head {
  max-width: 720px;
  margin-bottom: 36px;
}

.ffs-reviews__aggregate {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  margin: 14px 0 16px;
  font-size: 1.0625rem;
}
.ffs-reviews__aggregate strong { color: var(--ffs-ink); }
.ffs-reviews__count {
  font-size: .9375rem;
  color: var(--agriox-color-1);
}

/* These represent a third-party (Shopee) rating. --ffs-star is the brand
   gold darkened to 4.16:1 on white and 3.58:1 on the neutral band, so the
   rating clears the 3:1 that meaningful non-text graphics need on both
   grounds it appears on. Brand gold as shipped is 2.01:1 and would not. */
.ffs-stars {
  display: inline-flex;
  gap: 2px;
  color: var(--ffs-star);
  font-size: .9375rem;
  line-height: 1;
}

.ffs-reviews__carousel {
  --ffs-review-gap: 24px;
  /* Card width is explicit rather than a share of the container: the track is
     `width: max-content` for the marquee, so a percentage flex-basis would be
     resolving against a width the cards themselves define. 368px puts three
     across the 1170px container with both gaps, matching the old layout. */
  --ffs-review-w: 368px;
  position: relative;
}

/* Hides everything outside the strip and clips the animation. Overflow is
   `hidden` rather than `auto` because the animation and a scroll position
   fight each other — under prefers-reduced-motion, where the animation is
   off, it goes back to `auto` and the strip is a plain scroller again. */
.ffs-reviews__viewport {
  overflow: hidden;
  /* Headroom above and below the cards so their drop shadow is not sheared
     off: clipping horizontally clips vertically too, shadow included. */
  padding: 8px 0 20px;
}

.ffs-reviews__viewport:focus-visible {
  outline: 3px solid var(--agriox-base);
  outline-offset: 2px;
}

.ffs-reviews {
  display: flex;
  width: max-content;
  gap: var(--ffs-review-gap);
  margin: 0;
  padding: 0;
  list-style: none;
  animation: ffs-review-marquee var(--ffs-marquee-duration, 60s) linear infinite;
}

/* Pause so a reader can finish a card. :focus-within covers the keyboard —
   without it, tabbing to the strip would leave it sliding out from under the
   focus ring. */
.ffs-reviews__carousel:hover .ffs-reviews,
.ffs-reviews__carousel:focus-within .ffs-reviews {
  animation-play-state: paused;
}

/* The list is rendered twice, so translating by exactly one set width lands
   the duplicate's first card where the original's was and the reset is
   invisible. That distance is half the track plus half a gap: the track's
   total width carries one more gap than the two sets do between them, since
   the seam between set one and set two is itself a gap. */
@keyframes ffs-review-marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(calc(-50% - var(--ffs-review-gap) / 2)); }
}

/* No animation, and the strip becomes a normal horizontal scroller — the
   cards are still all reachable, they just wait to be pushed. The duplicate
   set is left in place: it reads as more cards rather than as a glitch, and
   it is aria-hidden so it never doubles up for a screen reader. */
@media (prefers-reduced-motion: reduce) {
  .ffs-reviews {
    animation: none;
  }
  .ffs-reviews__viewport {
    overflow-x: auto;
    /* Stops a horizontal flick from chaining into the browser's back-swipe
       once the strip hits its end. */
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--ffs-border) transparent;
  }
}

.ffs-review {
  flex: 0 0 var(--ffs-review-w);
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 28px 26px;
  border-radius: 14px;
  background: #fff;
  box-shadow: var(--ffs-shadow);
  /* A hairline at 6% rather than the usual --ffs-border. These are white
     cards that sit on a white section, and the shadow alone leaves the top
     edge indistinct; this defines it without reading as a drawn border. It
     is decoration, not a control boundary, so the 3:1 of 1.4.11 does not
     apply to it. */
  border: 1px solid rgba(46, 49, 146, .06);
}

/* Viewport units, not percentages, for the same reason the base width is a
   fixed value: the track is `width: max-content`, so a percentage basis would
   be resolving against a width the cards are themselves defining. */
@media (max-width: 991.98px) {
  .ffs-reviews__carousel { --ffs-review-w: calc(50vw - 32px); }
}
@media (max-width: 767.98px) {
  .ffs-reviews__carousel {
    --ffs-review-gap: 16px;
    /* Not the full width: on a phone the sliver of the next card is what
       tells a reader the strip keeps going. */
    --ffs-review-w: 78vw;
  }
}

.ffs-review__rating {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
}
/* Slightly larger than the aggregate row in the section head — in a card the
   star row is the first thing read, and 15px reads as a footnote. */
.ffs-review .ffs-stars {
  gap: 3px;
  font-size: 1.0625rem;
}
/* The glyph row is aria-hidden, so the rating is also stated in text —
   visually hidden, but read aloud and available to search. */
.ffs-review__rating-text {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* `flex: 1 1 auto` is what pushes the attribution to the bottom of the card.
   Cards in a flex row stretch to match the tallest, so without it a short
   review would leave its author floating in the middle of the box while its
   neighbour's sat on the floor. */
.ffs-review__body {
  flex: 1 1 auto;
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 1rem;
  line-height: 1.65;
  color: var(--agriox-color-1);
}
.ffs-review__body p { margin: 0; }
.ffs-review__body p::before { content: '\201C'; }
.ffs-review__body p::after  { content: '\201D'; }

/* Attribution stacks: name first, the product it refers to directly beneath.
   A review of the 1kg commercial mix and a review of the retail pack say
   different things to a trade buyer, so the product is not decoration. */
.ffs-review__meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  margin: 0;
  font-size: .875rem;
  color: var(--agriox-color-1);
}
.ffs-review__author {
  font-weight: 700;
  font-size: .9375rem;
  color: var(--ffs-ink);
}
.ffs-review__product,
.ffs-review__date {
  font-size: .8125rem;
}

/* --- carousel arrows ------------------------------------------------------
   Centred under the track. Both are the same 52px circle; only the fill
   differs, so the pair reads as one control rather than two widgets.

   Shipped with the `hidden` attribute and revealed by ffs.js, so the arrows
   never appear when they would do nothing — script blocked, or a track that
   already fits. Giving `display` only to the :not([hidden]) case means the
   UA's `display: none` for [hidden] still wins by default, without needing
   an !important to fight a base display value. */
/* The prev/next arrows and their .ffs-reviews__nav container are gone: the
   strip now loops on its own, so there is nothing left for a reader to page
   through. Their rules were removed with them rather than left orphaned. */

/* Spacing and width now come from .ffs-btn-group, which the element carries
   alongside this class — the 24px it used to set here was the same 24px the
   group sets, and keeping both meant two places to change it. The class stays
   as the section's hook for this CTA; it just no longer owns the box. */

/* ---- placeholder state — must never survive to production ---- */
.ffs-reviews__warning {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 0 0 26px;
  padding: 16px 20px;
  border: 2px dashed #b7791f;
  border-radius: var(--ffs-radius);
  background: #fffbeb;
  color: #7a4f0a;
  font-size: .9375rem;
}
.ffs-reviews__warning i {
  margin-top: 3px;
  font-size: 1.125rem;
}

.ffs-review--sample {
  border: 2px dashed #b7791f;
  background: repeating-linear-gradient(
      135deg,
      #fffbeb 0 12px,
      #fef6dc 12px 24px);
}
.ffs-review__flag {
  align-self: flex-start;
  padding: 3px 10px;
  border-radius: 999px;
  background: #b7791f;
  color: #fff;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

/* --------------------------------------------------------------------------
   9. Certification badges
   -------------------------------------------------------------------------- */
.ffs-certs {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  padding: 0;
  margin: 0;
  list-style: none;
}
.ffs-cert {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 20px;
  border: 1px solid var(--ffs-border);
  border-radius: var(--ffs-radius);
  background: #fff;
}

/* Stacked variant — one box per row instead of a wrapped horizontal strip.

   Grid rather than `flex-direction: column`, and specifically for
   `grid-auto-rows: 1fr`: it sizes every row to the tallest one, so the three
   boxes come out genuinely equal even though "Regulatory compliance" carries
   three times the caption text of "Halal". A flex column would let each box
   shrink to its own content, which is the uneven result this replaces.

   The rule overrides display/gap from .ffs-certs above, so it must stay after
   it in the file. */
.ffs-certs--stacked {
  display: grid;
  grid-template-columns: 1fr;
  grid-auto-rows: 1fr;
  gap: 16px;
}
.ffs-certs--stacked .ffs-cert {
  width: 100%;
  padding: 20px 24px;
}
.ffs-cert__mark {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  flex: 0 0 42px;
  border-radius: 50%;
  background: rgba(var(--agriox-primary-rgb), .08);
  color: var(--agriox-primary);
  font-size: 1.125rem;
}
.ffs-cert__name {
  display: block;
  font-weight: 700;
  color: var(--ffs-ink);
  line-height: 1.3;
}
.ffs-cert__meta {
  display: block;
  font-size: .8125rem;
  color: var(--agriox-color-1);
}

/* --------------------------------------------------------------------------
   9b. Certificate flip cards — accreditation.php
   --------------------------------------------------------------------------
   The HACCP and Halal cards, with the copy on the front and the certificate
   PDF on the back.

   Two things drive the structure:

   - The front face is in normal flow and the back is absolutely positioned
     over it, so the card's height comes from the prose rather than from a
     magic number that would clip the moment the client edits a sentence. The
     inner then carries a min-height purely to guarantee the iframe on the back
     has room to be worth looking at.

   - Visibility, not just backface-visibility, hides the face that is turned
     away. backface-visibility only stops it painting: the "Open full PDF" link
     on the back would still be in the tab order behind the front of the card,
     which is a focus trap you cannot see. `visibility: hidden` takes it out of
     the tab order too, and is delayed to the midpoint of the turn so the face
     does not vanish before it has rotated away.

   Hover is behind `(hover: hover)`. On a touchscreen a :hover rule latches on
   first tap and stays stuck until you tap elsewhere, which is exactly the
   behaviour the click toggle exists to avoid. */
.ffs-flip {
  height: 100%;
  perspective: 1000px;
}

.ffs-flip__inner {
  position: relative;
  height: 100%;
  min-height: 520px;
  transform-style: preserve-3d;
  transition: transform .6s cubic-bezier(.2, .7, .3, 1);
}

@media (hover: hover) {
  .ffs-flip:hover .ffs-flip__inner { transform: rotateY(180deg); }
}
.ffs-flip.is-flipped .ffs-flip__inner { transform: rotateY(180deg); }

.ffs-flip__face {
  display: flex;
  flex-direction: column;
  padding: 32px;
  border: 1px solid var(--ffs-border);
  border-radius: var(--ffs-radius);
  background: #fff;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  /* Delayed to the midpoint of the 600ms turn: the face is edge-on by then,
     so the swap is invisible. Going the other way it must be instant, or the
     face being turned towards the reader spends 300ms unfocusable. */
  transition: visibility 0s linear .3s;
}

.ffs-flip__front { height: 100%; }

.ffs-flip__back {
  position: absolute;
  inset: 0;
  transform: rotateY(180deg);
  visibility: hidden;
}

@media (hover: hover) {
  .ffs-flip:hover .ffs-flip__front { visibility: hidden; }
  .ffs-flip:hover .ffs-flip__back  { visibility: visible; }
}
.ffs-flip.is-flipped .ffs-flip__front { visibility: hidden; }
.ffs-flip.is-flipped .ffs-flip__back  { visibility: visible; }

.ffs-flip__title {
  font-size: 1.375rem;
  margin: 0 0 6px;
}
.ffs-flip__meta {
  font-size: .875rem;
  color: var(--agriox-color-1);
  margin: 0 0 18px;
}

/* Pushed to the bottom of the front face so it sits in the same place on both
   cards regardless of how much copy each carries. */
.ffs-flip__toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  align-self: flex-start;
  margin-top: auto;
  padding: 10px 0 0;
  border: 0;
  background: none;
  font-size: .875rem;
  font-weight: 700;
  color: var(--agriox-primary);
  cursor: pointer;
}
.ffs-flip__toggle:hover span,
.ffs-flip__toggle:focus-visible span { text-decoration: underline; }
.ffs-flip__toggle i {
  transition: transform .3s ease;
}
.ffs-flip__toggle:hover i { transform: rotate(180deg); }

/* The preview takes the slack in the back face and the actions sit under it,
   so a tall card gives the certificate more room rather than leaving a gap. */
.ffs-flip__preview {
  flex: 1 1 auto;
  min-height: 0;
  margin-bottom: 18px;
  border: 1px solid var(--ffs-border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--ffs-surface);
}
.ffs-flip__preview iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.ffs-flip__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}
/* The two links in here are .ffs-btn, which goes full-width below 768px via
   .ffs-btn-group — this is not a btn group, so they keep their own width and
   only the wrap handles a narrow card. */
.ffs-flip__actions .ffs-btn {
  flex: 0 1 auto;
  min-height: 48px;
  padding: 12px 22px;
  font-size: .9375rem;
}

.ffs-flip__back-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  padding: 8px 4px;
  border: 0;
  background: none;
  font-size: .875rem;
  font-weight: 700;
  color: var(--agriox-primary);
  cursor: pointer;
}
.ffs-flip__back-btn:hover span,
.ffs-flip__back-btn:focus-visible span { text-decoration: underline; }

@media (max-width: 767.98px) {
  .ffs-flip__inner { min-height: 460px; }
  .ffs-flip__face { padding: 24px; }
}

/* A 600ms rotation is exactly the kind of movement the setting is asking about,
   so the card swaps faces instantly instead. Everything still works — the
   toggle, the preview and the links are unaffected. */
@media (prefers-reduced-motion: reduce) {
  .ffs-flip__inner { transition: none; }
  .ffs-flip__face { transition: none; }
  .ffs-flip__toggle i { transition: none; }
}

/* --------------------------------------------------------------------------
   10. Proposition pillars
   -------------------------------------------------------------------------- */
/* The four "why fun food works" cards on business-solutions.php. These used to
   borrow .ffs-cert with a style attribute overriding its flex direction; they
   are their own component now, because a cert badge is a flat trust marker
   while these are cards that lift on hover and carry a top accent rule.

   .ffs-cert__mark is deliberately still reused for the icon badge — it is the
   site's shared icon-badge token, already used standalone on the package cards
   here and on accreditation.php, not a strict BEM child of .ffs-cert. */
.ffs-pillar {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  height: 100%;
  padding: 28px 24px 26px;
  background: #fff;
  border: 1px solid var(--ffs-border);
  border-radius: var(--ffs-radius);
  overflow: hidden;
  transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
}

/* Accent drawn as a pseudo-element rather than a real border-top: a border
   would add to the box on one side only, so the 1px outline would read uneven
   where the radius curves at the two top corners. */
.ffs-pillar::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 3px;
  background: var(--agriox-primary);
}

.ffs-pillar:hover {
  transform: translateY(-4px);
  border-color: transparent;
  box-shadow: var(--ffs-shadow);
}

/* Sora at 700 on --ffs-ink (10.4:1 on white) against the body copy's
   --agriox-color-1 (8.6:1) — the weight and family carry the hierarchy, and
   both sides clear AA comfortably. */
.ffs-pillar__name {
  display: block;
  margin-bottom: 6px;
  font-family: var(--agriox-font-2);
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.3;
  color: var(--ffs-ink);
}

.ffs-pillar__text {
  display: block;
  font-size: .9375rem;
  line-height: 1.6;
  color: var(--agriox-color-1);
}

@media (prefers-reduced-motion: reduce) {
  .ffs-pillar { transition: none; }
  .ffs-pillar:hover { transform: none; }
}

/* --------------------------------------------------------------------------
   11. Outlets
   -------------------------------------------------------------------------- */
.ffs-outlets {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
  padding: 0;
  list-style: none;
}
/* Padding moved from .ffs-outlet itself onto .ffs-outlet__body 2026-09-07,
   when a storefront photo (.ffs-outlet__media) was added above the text —
   same restructure .ffs-card went through for its own media box: the OUTER
   element clips to the border-radius via overflow:hidden and carries no
   padding of its own, so the image can sit flush against the card's rounded
   top corners instead of getting boxed in by an inset it would need
   negative margins to escape. An outlet with no photo yet (see the null
   check in business-solutions.php) simply has no __media element — the
   __body's own padding still gives it the original card look. */
.ffs-outlet {
  border: 1px solid var(--ffs-border);
  border-radius: var(--ffs-radius);
  background: #fff;
  overflow: hidden;
}
.ffs-outlet__media {
  aspect-ratio: 3 / 2;
  background: var(--ffs-surface);
  overflow: hidden;
}
.ffs-outlet__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ffs-outlet__body {
  padding: 22px 24px;
}
.ffs-outlet h3 {
  font-size: 1.0625rem;
  margin: 0 0 6px;
}
.ffs-outlet p {
  margin: 0 0 14px;
  font-size: .9375rem;
}
.ffs-outlet a {
  font-weight: 700;
  font-size: .9375rem;
  color: var(--agriox-primary);
}

/* --------------------------------------------------------------------------
   12. Forms
   -------------------------------------------------------------------------- */
.ffs-field { margin-bottom: 20px; }

.ffs-field label {
  display: block;
  margin-bottom: 7px;
  font-size: .9375rem;
  font-weight: 500;
  color: var(--ffs-ink);
}
.ffs-field .req { color: var(--agriox-base); }

.ffs-field input,
.ffs-field select,
.ffs-field textarea {
  width: 100%;
  min-height: 50px;
  padding: 13px 16px;
  border: 1px solid var(--ffs-border);
  border-radius: var(--ffs-radius);
  background: #fff;
  font-family: var(--agriox-font);
  font-size: 1rem;
  color: var(--ffs-ink);
}
.ffs-field textarea { min-height: 150px; resize: vertical; }

.ffs-field input:focus,
.ffs-field select:focus,
.ffs-field textarea:focus {
  border-color: var(--agriox-primary);
}

.ffs-field label.error {
  margin-top: 6px;
  font-size: .875rem;
  font-weight: 500;
  color: #c0392b;
}
.ffs-field input.error,
.ffs-field select.error,
.ffs-field textarea.error {
  border-color: #c0392b;
}

/* Honeypot — off-screen rather than display:none, which some bots detect. */
.ffs-hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.ffs-form__status {
  margin-top: 18px;
  padding: 14px 18px;
  border-radius: var(--ffs-radius);
  font-size: .9375rem;
}
.ffs-form__status[data-state="ok"] {
  background: #e8f6ee;
  border: 1px solid #b6e0c7;
  color: #1d6b3e;
}
.ffs-form__status[data-state="error"] {
  background: #fdeceb;
  border: 1px solid #f5c2be;
  color: #a02c22;
}
.ffs-form__status:empty { display: none; }

/* --------------------------------------------------------------------------
   13. Header / footer adjustments
   -------------------------------------------------------------------------- */
.main-header--one__top { background: var(--agriox-primary); }
.main-header--one__top,
.main-header--one__top a,
.main-header--one__top p { color: rgba(255, 255, 255, .9); }
.main-header--one__top a:hover { color: var(--agriox-base-on-dark); }

.ffs-topbar__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 24px;
  padding: 9px 0;
}
.ffs-topbar__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 22px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: .875rem;
}
.ffs-topbar__list li {
  display: flex;
  align-items: center;
  gap: 8px;
}
.ffs-topbar__list i { color: var(--agriox-base-on-dark); }

@media (max-width: 767.98px) {
  .ffs-topbar__inner { justify-content: center; }
  .ffs-topbar__hours { display: none; }
}

.main-menu__list > li > a { font-weight: 500; }
.main-menu__list > li.current > a,
.main-menu__list > li > a:hover { color: var(--agriox-primary); }

/* ---- Brand lockup -------------------------------------------------------
   ffs-logo-original.gif is the whole lockup: the FFS roundel and the
   "FFS FOODS (M) SDN. BHD." wordmark are both baked into the one 398x49
   image, so there is no separate text node here to align against. The flex
   rules sit on the anchor regardless — drop a <span> wordmark in beside the
   mark later and it centres with a 12px gap, no further CSS needed.
   ------------------------------------------------------------------------ */
.ffs-logo,
.ffs-logo a {
  display: flex;
  align-items: center;
  gap: 12px;
}

.ffs-logo img {
  height: 46px;
  width: auto;
  max-width: 100%;
}

/* The drawer is 300px wide inside 15px of padding. The lockup at its natural
   46px height is 373px, which used to run straight out of it. */
.mobile-nav__content .ffs-logo img {
  height: auto;
  width: 100%;
}

/* ---- Header bar layout --------------------------------------------------
   The template centres the brand absolutely (position: absolute; left: 50%)
   over a left-aligned menu, which holds only while the menu is short enough
   to stay clear of the middle. FFS runs seven top-level items: the list
   reaches ~950px and the lockup covers 186px either side of centre, so the
   logo, the wordmark inside it and the links all claimed the same pixels.

   Fix: the brand is back in normal flow at the left — it is also first in the
   DOM now, see partials/header.php — and the nav plus the sales-enquiry box
   are pushed right with margin-left: auto. Nothing in the bar is absolutely
   positioned any more, so nothing can sit on top of anything else.
   ------------------------------------------------------------------------ */
.main-header--one__bottom-inner {
  gap: 12px 40px;
}

.main-header--one__bottom-inner .main-header--one__bottom-middel {
  position: static;
  top: auto;
  left: auto;
  transform: none;
  flex: 0 0 auto;
}

/* Nav and contact box hug the right edge as one group. */
.main-header--one__bottom .main-menu--1 { margin-left: auto; }

/* 60px between seven items is 360px of gutter the bar cannot spare. */
.main-menu .main-menu__list > li + li,
.stricky-header .main-menu__list > li + li {
  margin-left: clamp(20px, 1.9vw, 34px);
}

/* The sticky bar clones .main-menu into an otherwise unstyled container, so
   it inherits no page gutter — without this the logo touches the viewport
   edge. Mirrors .main-header--one__bottom .auto-container. */
.sticky-header__content > .main-menu__inner {
  max-width: 1755px;
  margin: 0 auto;
  padding: 0 50px;
  gap: 12px 40px;
}

/* The sales-enquiry box needs ~240px. Below 1600px the bar cannot carry it
   and the full menu at a comfortable spacing — and the same number already
   sits in the utility bar directly above, so drop the duplicate rather than
   crowd the links. */
@media (max-width: 1599.98px) {
  .main-header--one__bottom-right { display: none; }
}

/* Narrow desktops: shrink the lockup so the links keep their spacing. */
@media (min-width: 1200px) and (max-width: 1499.98px) {
  .ffs-logo img { height: 34px; }
}

@media (max-width: 1199.98px) {
  /* Below the desktop menu breakpoint the template pins the brand and the
     hamburger absolutely into opposite corners of the bar. At phone widths
     the 373px lockup simply ran underneath the toggler. Put both back in
     flow and let flexbox keep them apart. */
  .main-header--one__bottom-inner {
    flex-wrap: nowrap;
    gap: 20px;
  }
  .main-header--one__bottom-inner .main-header--one__bottom-middel {
    min-width: 0;
  }
  .main-header--one__bottom .main-menu--1 {
    position: static;
    top: auto;
    right: auto;
    bottom: auto;
    width: auto;
    height: auto;
    margin-left: auto;
    flex: 0 0 auto;
  }
  /* Sized by width rather than height: the lockup is 8:1, so a height that
     reads well on desktop is wider than the phone it is on. */
  .ffs-logo img {
    height: auto;
    width: clamp(180px, 46vw, 300px);
  }
}

/* ---- Mobile drawer ------------------------------------------------------
   Everything below 1200px navigates through this drawer, so it is the whole
   menu on a phone. agriox.js clones .main-menu__list into it verbatim, which
   means the desktop menu's colours come along for the ride — and the desktop
   bar sits on white while the drawer sits on the brand blue. Three of the
   inherited colours were therefore illegible or invisible on the drawer:

     current page link   blue on blue ........  1.00:1  invisible
     .expanded parent    blue on blue ........  1.00:1  invisible
     toggle + contact    blue on blue ........  1.00:1  invisible

   The first is the one that reads as a bug: on index.php the "Home" row was
   a blank 46px gap between the logo and "About", and every page lost its own
   link the same way. All three are re-pointed at the gold, which is the one
   colour §1 sanctions on a blue ground — 5.30:1, past AA for the text rows
   and well past the 3:1 WCAG 1.4.11 asks of the icons and the toggle.

   Measured at 390x844 (iPhone 12/13/14, the narrowest common viewport).
   ------------------------------------------------------------------------ */

/* 100vh is the *largest* viewport on mobile Safari — the tallest the page gets
   once the toolbars retract. Sized by it, the drawer's last rows sit behind the
   toolbar on first paint, which at 390x844 is the whole contact block. dvh is
   the live value; the vh line stays as the fallback for browsers without it. */
.mobile-nav__wrapper {
  height: 100vh;
  height: 100dvh;
}

/* 300px of a 390px viewport leaves 90px of overlay to tap out through, which
   is comfortable — but the same 300px on a 320px phone leaves 20px, and the
   only way out is then the close button. The min() keeps 390px exactly as it
   was and only narrows the drawer where it has to. */
.mobile-nav__content {
  width: min(300px, 82vw);
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding-top: 20px;
}

/* The lockup is one image with a white ground baked in, so on the blue it
   lands as a white slab either way. Made deliberate — a white card, inset
   from the close button rather than running under it. Was full-width, which
   put the wordmark directly beneath the ×. */
.mobile-nav__content .logo-box {
  max-width: calc(100% - 56px);
  margin-bottom: 30px;
  padding: 9px 11px;
  background: #fff;
  border-radius: 8px;
}

/* 18px of glyph is a ~20px target; the AAA floor is 44px. Grown to 44 and
   given a hover/focus ground so it is findable against the blue. */
.mobile-nav__close {
  top: 12px;
  right: 12px;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: 8px;
  font-size: 20px;
  color: var(--agriox-base-on-dark);
  transition: background-color 200ms ease, color 200ms ease;
}
.mobile-nav__close:hover,
.mobile-nav__close:focus-visible {
  background: rgba(242, 169, 0, .22);
  color: #fff;
}

/* 46px fixed height clipped the two-line labels ("Business Solutions" wraps
   at this width once the toggle button takes its 36px). Auto height with a
   48px floor keeps the target size and lets the row grow. */
.mobile-nav__content .main-menu__list li a {
  height: auto;
  min-height: 48px;
  padding: 7px 0;
  gap: 12px;
  color: rgba(255, 255, 255, .92);
  font-family: var(--agriox-font);
}

/* The fix for the invisible row. Gold at 600 (5.30:1 on the blue), plus a
   gold rule down the leading edge so the current page is marked by more than
   colour alone (WCAG 1.4.1). Only the current item is indented, so nothing
   else shifts. */
.mobile-nav__content .main-menu__list > li.current > a {
  padding-left: 12px;
  box-shadow: inset 3px 0 0 var(--agriox-base-on-dark);
  color: var(--agriox-base-on-dark);
  font-weight: 600;
}

.mobile-nav__content .main-menu__list li a:hover,
.mobile-nav__content .main-menu__list li a.expanded {
  color: var(--agriox-base-on-dark);
}

.mobile-nav__content .main-menu__list li a:focus-visible {
  outline: 2px solid var(--agriox-base-on-dark);
  outline-offset: -2px;
}

/* Submenu rows step back half a level so the Products children read as
   children at a glance rather than as seven more top-level items. */
.mobile-nav__content .main-menu__list li ul li a {
  min-height: 44px;
  font-size: .9375rem;
  color: rgba(255, 255, 255, .78);
}

/* Collapsed: outlined in gold with a gold chevron — the border gives the
   control a visible boundary at 5.30:1 on the blue, which a bare glyph would
   not have. Expanded: filled gold with a near-black chevron (9.26:1), so open
   and closed differ in fill as well as in the template's rotation. */
.mobile-nav__content .main-menu__list li a button {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 8px;
  background-color: transparent;
  border: 1px solid var(--agriox-base-on-dark);
  color: var(--agriox-base-on-dark);
}
.mobile-nav__content .main-menu__list li a button.expanded {
  background-color: var(--agriox-base-on-dark);
  color: var(--ffs-on-brand);
}

/* --agriox-text-dark is not a token this stylesheet defines — the template's
   rule resolved to nothing and the li fell back to inheriting body colour. */
.mobile-nav__contact li {
  color: rgba(255, 255, 255, .9);
  font-size: .9375rem;
  letter-spacing: normal;
}
.mobile-nav__contact li > i {
  width: 38px;
  height: 38px;
  font-size: 15px;
  background-color: var(--agriox-base-on-dark);
  color: var(--agriox-primary);
}
.mobile-nav__contact li a:hover {
  color: var(--agriox-base-on-dark);
}

/* Both floats sit under the drawer's overlay while it is open — visible,
   dimmed and unclickable, which reads as a dead control. Taken out for the
   duration; body.locked is already toggled with the drawer by agriox.js. */
body.locked .ffs-wa,
body.locked .scroll-to-top {
  display: none;
}

.ffs-footer-note {
  font-size: .875rem;
  color: rgba(255, 255, 255, .65);
}

/* --------------------------------------------------------------------------
   12b. Legal pages — privacy-policy.php
   -------------------------------------------------------------------------- */
.ffs-legal h2 {
  font-size: 1.25rem;
  margin-top: 2.2em;
  /* Anchored from the table of contents; keep the heading clear of the
     sticky header when a #fragment is followed. */
  scroll-margin-top: 100px;
}
.ffs-legal h2:first-of-type { margin-top: 1.4em; }

.ffs-legal ul {
  padding-left: 1.2em;
  max-width: 68ch;
}
.ffs-legal li + li { margin-top: .5em; }

.ffs-legal__updated {
  display: inline-block;
  padding: 6px 14px;
  border-radius: 999px;
  background: var(--ffs-surface);
  border: 1px solid var(--ffs-border);
  font-size: .875rem;
}

.ffs-legal__toc {
  position: sticky;
  top: 100px;
  padding: 26px;
  border: 1px solid var(--ffs-border);
  border-radius: var(--ffs-radius);
  background: var(--ffs-surface);
}
.ffs-legal__toc h2 {
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--agriox-primary);
  margin: 0 0 14px;
}
.ffs-legal__toc ol {
  margin: 0;
  padding-left: 1.3em;
  font-size: .9375rem;
}
.ffs-legal__toc li + li { margin-top: .45em; }
.ffs-legal__toc a { color: var(--agriox-color-1); }
.ffs-legal__toc a:hover { color: var(--agriox-base); }

@media (max-width: 991.98px) {
  .ffs-legal__toc { position: static; }
}

/* --------------------------------------------------------------------------
   12c. Thank-you page
   -------------------------------------------------------------------------- */
.ffs-thanks {
  max-width: 680px;
  margin: 0 auto;
  text-align: center;
}
.ffs-thanks .ffs-lede { margin-inline: auto; }

.ffs-thanks__mark {
  display: grid;
  place-items: center;
  width: 72px;
  height: 72px;
  margin: 0 auto 26px;
  border-radius: 50%;
  background: rgba(var(--agriox-base-rgb), .1);
  color: var(--agriox-base);
  font-size: 1.75rem;
}

.ffs-thanks__panel {
  margin-top: 40px;
  padding: 30px;
  border: 1px solid var(--ffs-border);
  border-radius: var(--ffs-radius);
  background: var(--ffs-surface);
  text-align: left;
}
.ffs-thanks__panel h2 { font-size: 1.125rem; margin-top: 0; }

.ffs-thanks__contact {
  margin: 18px 0 0;
  padding: 0;
  list-style: none;
  font-size: .9375rem;
}
.ffs-thanks__contact li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}
.ffs-thanks__contact li + li { margin-top: 12px; }
.ffs-thanks__contact i {
  width: 20px;
  color: var(--agriox-base);
  text-align: center;
}

.ffs-thanks__next {
  font-size: 1.125rem;
  margin-top: 48px;
}
.ffs-thanks__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin-top: 20px;
}

/* --------------------------------------------------------------------------
   12d. Footer columns and contact icons
   --------------------------------------------------------------------------
   The footer markup is the template's, so everything below is the FFS layer
   reaching into it rather than editing agriox.css.
   -------------------------------------------------------------------------- */

/* The four contact icons — address, phone, email, WhatsApp — set to white.
   White on the footer blue is 10.66:1.

   Note the `::before` in every selector, which is the whole point of this rule.
   Font Awesome does not draw the glyph on the `<i>`; it draws it in the `<i>`'s
   ::before, and agriox.css hard-colours THAT in three places:

     .footer-widget__about-contact-box .text  i::before
     .footer-widget__about-contact-box .phone a i::before
     .footer-widget__about-contact-box p     a i::before

   all of them `color: var(--agriox-base)`. A pseudo-element's own colour
   declaration always wins over inheriting from the element it hangs off, so
   colouring the `<i>` alone — which is what this rule used to do — has no
   effect on the glyph at all. And because the FFS layer re-skins --agriox-base
   to #2e3192, the same blue as the footer ground, the glyphs were painting at
   1.00:1: still rendering, perfectly invisible.

   Specificity, since the three template rules do not all sit at the same
   weight — the selectors below are built to win outright rather than to rely
   on this file loading second:

     .text  i::before   (0,2,2)  <  `… p i::before`    (0,2,3)   address row
     .phone a i::before (0,2,3)  <  `… p a i::before`  (0,2,4)   phone, WhatsApp
     p a    i::before   (0,1,4)  <  `… p a i::before`  (0,2,4)   email

   Every contact row is a <p>, so the `p` costs nothing in coverage while
   buying the extra weight. */
.footer-one .footer-widget__about-contact-box p i,
.footer-one .footer-widget__about-contact-box p i::before,
.footer-one .footer-widget__about-contact-box p a i::before,
.footer-one .footer-widget__about-contact-box p a:hover i::before,
.footer-one .footer-widget__about-contact-box p a:focus i::before {
  color: #fff;
}

/* Fixed-width icon column so the four lines align on their text rather than
   stepping in and out with each glyph's natural width.

   The template puts `padding-right: 10px` on the ::before to space the glyph
   off its label. That has to go, or it is added INSIDE this 22px box — pushing
   the glyph off centre and overflowing the column it is supposed to define.
   The gap is this element's margin-right instead. */
.footer-one .footer-widget__about-contact-box p i {
  display: inline-block;
  width: 22px;
  margin-right: 10px;
  text-align: center;
}
.footer-one .footer-widget__about-contact-box p i::before,
.footer-one .footer-widget__about-contact-box p a i::before {
  padding-right: 0;
}

/* Even vertical rhythm inside the contact box — the template gives these
   paragraphs mixed margins depending on which class they carry. */
.footer-one .footer-widget__about-contact-box p {
  margin: 0 0 14px;
}
.footer-one .footer-widget__about-contact-box p:last-child {
  margin-bottom: 0;
}

/* Item 9 — one shared start position for every footer column, so Products,
   Company and Get in touch line up on their headings and on their first row of
   content regardless of how much copy sits above them. The column widths are
   equalised in partials/footer.php; this equalises the internal spacing. */
.footer-one .footer-widget__column {
  height: 100%;
}

/* The three template offsets that stopped the four columns from sharing a top
   edge or an even rhythm across the row. All of them are the Agriox demo's
   layout, where the footer was 1 wide column + 2 narrow lists + a newsletter
   block; none of them survive contact with four equal quarters.

     .footer-widget__about   margin-top: -15px   lifted "Fun Food Specialist"
                                                 15px above the other three
     .footer-widget__explore margin-left: 50px   indented Products and Company
                                                 50px right of their gutter, so
                                                 the four gaps read 30/80/30/30
     .footer-widget__explore margin-top: 60px    below 1200px, dropped those two
                                                 columns 60px down the page —
                                                 declared three times over, in
                                                 the <=1199, <=991 and <=767
                                                 blocks of agriox.css

   Zeroed rather than re-tuned: the row is four `col-xl-3`s and Bootstrap's own
   gutter already distributes them evenly, so the correct offset is none. Each
   selector is scoped to .footer-one, which puts it at 0,2,0 against the
   template's 0,1,0 — that wins on specificity regardless of media query, so
   the responsive margin-top is covered by this one rule and does not need
   restating per breakpoint (and does not need !important).

   Top alignment itself comes from the row's default `align-items: stretch`
   plus the height:100% above: with the margins gone, every column's first line
   box starts on the same y. Do not add `align-items: flex-start` to force it —
   that collapses the stretched height these columns are measured by. */
.footer-one .footer-widget__about,
.footer-one .footer-widget__explore {
  margin-top: 0;
  margin-left: 0;
}

/* Even columns are not even SPACING, which is the thing actually being asked
   for here. Four col-xl-3s put the column boxes on a perfect 300px pitch, but
   the boxes are not what a reader sees — the text inside them is, and the four
   blocks do not fill their boxes equally:

     Fun Food Specialist   262px of ink in a 270px box   (a wrapped paragraph)
     Products              135px                          (short link labels)
     Company               137px                          (short link labels)
     Get in touch          270px                          (paragraph + buttons)

   which rendered as gaps of 38px / 166px / 162px. The first two columns nearly
   touch while the others sit in a lake of blue, and no amount of equalising the
   GRID fixes it, because the grid was already equal.

   So above 1200px, where all four sit on one line, the columns size to their
   content and space-between shares out what is left. Every column carries the
   same 15px of gutter padding on both sides, so equal box gaps are equal ink
   gaps — the leftover divides into three identical channels of ~119px.

   max-width caps the two prose columns at the 300px they occupy today, so
   their paragraphs wrap exactly as before rather than running out to their
   max-content width (one long unwrapped line, which is what `width: auto`
   would otherwise give a <p>). flex-shrink stays at 1 so a future longer menu
   label compresses the channels instead of overflowing the container.

   Scoped to >=1200px deliberately. Below that the four become 2x2 and then a
   single stack, where content-width columns would leave one item hugging the
   left edge and its neighbour floating mid-row; the even 50/50 grid is the
   right answer there and is left alone. */
@media (min-width: 1200px) {
  .footer-one__top .row {
    justify-content: space-between;
  }
  .footer-one__top .row > [class*="col-"] {
    flex: 0 1 auto;
    width: auto;
    max-width: 300px;
  }
}

.footer-one .footer-widget__title {
  margin: 0 0 24px;
}
.footer-one .footer-widget__explore-list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.footer-one .footer-widget__explore-list-item {
  margin: 0 0 12px;
}
.footer-one .footer-widget__explore-list-item:last-child {
  margin-bottom: 0;
}

/* Footer CTA pair — "Contact us" and the Shopee store link — as one stack of
   identical bars, on every screen size rather than only below 576px.

   The base .ffs-btn-group wraps a row, which is fine in a wide panel: the
   buttons shrink-wrap their labels and sit side by side. In a footer column
   roughly 255px wide they can never fit on one line, so they wrapped to two
   rows anyway — but as two DIFFERENT widths, because "Shopee Store" is a much
   shorter label than "Contact us" is not. Column direction with stretch makes
   the pair share the column's width, so both edges line up and both labels
   centre on the same axis.

   Width, padding and alignment all come from this one rule plus the base
   .ffs-btn box; the 16px between them is --ffs-btn-gap, the same value every
   other button pair on the site uses. Nothing here re-declares padding — the
   buttons already carry the site's single 15px/32px, and full-width is what
   makes that identical rather than merely equal.

   `align-items: stretch` is the default for a flex column, but it is written
   out because the whole point of the rule is that it must not be overridden. */
.footer-one .ffs-btn-group {
  flex-direction: column;
  align-items: stretch;
}

/* The same defect, one row down. The social icons on the bottom bar rest at
   #fff but the template hovers them to var(--agriox-base) — which is now the
   brand blue, sitting on the bottom bar's #242672 at 1.24:1. Hovering made them
   vanish. Gold is the accent that belongs on the dark surfaces (6.55:1). */
.footer-one .footer-one__bottom-social-links ul li a:hover,
.footer-one .footer-one__bottom-social-links ul li a:focus {
  color: var(--agriox-base-on-dark);
}

/* --------------------------------------------------------------------------
   Token-collapse fallout
   --------------------------------------------------------------------------
   All four rules below share one cause, so they are kept together.

   agriox.css was built on a palette where --agriox-base was gold (#f1cf69) and
   --agriox-primary was green. It leans on that difference constantly: paint a
   surface in one token, paint the thing sitting ON that surface in the other.
   The FFS layer sets BOTH to #2e3192, because the brand has one blue and 86
   text declarations hang off --agriox-base. Every such pairing therefore
   resolves to blue-on-blue — 1.00:1. The glyphs still render; you just cannot
   see them.

   These are the instances that survive on this site's markup. Anything else in
   the list is on a page the FFS build does not ship. If a future page pulls in
   a new template component, check it for this before trusting it.
   -------------------------------------------------------------------------- */

/* Footer contact links. White at rest, and the template hovers them to
   --agriox-base — straight into the footer ground. Gold: 5.30:1. */
.footer-one .footer-widget__about-contact-box p a:hover,
.footer-one .footer-widget__about-contact-box p a:focus {
  color: var(--agriox-base-on-dark);
}

/* Back-to-top button, bottom-right of every page. Its ground is --agriox-base
   and the template draws the arrow in --agriox-primary, so the control has
   been sitting there blank — it only appeared on hover, where the template
   happens to hard-code #fff. White at rest too, which also makes the hover a
   background change rather than the arrow blinking into existence. 10.66:1. */
.scroll-to-top i {
  color: #fff;
}

/* Header phone chip. Rests white-on-#242672 (13.16:1) and is correct; the
   hover swaps the chip to --agriox-base and the glyph to --agriox-primary,
   i.e. both to the same blue. Hold the glyph at white — 10.66:1 on the hover
   ground, and the chip colour change still reads as the hover feedback. */
.main-header--one__bottom-right .contact-box .icon:hover span::before,
.main-header--one__bottom-right .contact-box .icon:focus span::before {
  color: #fff;
}

/* --------------------------------------------------------------------------
   12e. Scroll fade
   --------------------------------------------------------------------------
   Both classes are added by initScrollFade() in assets/js/ffs.js — .ffs-fade
   on load, .ffs-fade--in when the element scrolls into view. Nothing in the
   markup carries .ffs-fade, and it must stay that way: if the hidden state
   were in the HTML, a visitor whose JS failed would be left with an empty
   page instead of an un-animated one.
   -------------------------------------------------------------------------- */
.ffs-fade {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .6s ease-out, transform .6s ease-out;
  /* Hint only — the element is never fixed or layered, so this stays cheap. */
  will-change: opacity, transform;
}
.ffs-fade--in {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* Belt and braces. The script already declines to animate under reduced
   motion, but if .ffs-fade is ever applied by some other route, this makes
   sure it still resolves to visible rather than to a blank section. */
@media (prefers-reduced-motion: reduce) {
  .ffs-fade,
  .ffs-fade--in {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   12f. Vitaplex B+ promotional banner — index.php
   --------------------------------------------------------------------------
   A single full-bleed photograph (assets/images/box.jpg) with the copy laid
   over its right-hand third. The shot was composed for exactly this: the
   waffle plate and props sit left, and the right side is empty worktop. So the
   panel is right-aligned to land in that gap rather than over the food.

   NO SCRIM — and why that is safe here. An earlier revision laid a gradient
   wash over the right-hand side for contrast. It was removed deliberately: the
   photograph carries its own graduated indigo grade, baked into the pixels of
   box.jpg rather than applied in CSS, which leaves the copy-space side dark on
   its own. Measured over the whole panel region, the brightest pixel is
   rgb(102,105,160) and white text against it is 5.13:1 — clear of the 4.5:1
   WCAG AA asks for body copy, with no overlay at all.

   That measurement is the whole basis for the text colours below, so it is
   also the constraint on changing them:

     white  #ffffff   5.13:1   PASS
     gold   #f2a900   2.55:1   FAIL — do not use for text on this banner

   The eyebrow is therefore white, not the --ffs-gold used for eyebrows
   elsewhere on the site. Gold is legible on the tinted-and-scrimmed version of
   this banner and on the light sections, but on this image without a scrim it
   lands at less than half the required ratio. If the background image is ever
   re-exported lighter, re-measure before assuming white still clears AA.
   -------------------------------------------------------------------------- */
.ffs-vbanner {
  position: relative;
  width: 100%;
  border-radius: 16px;
  overflow: hidden;
  background-image: url("../images/box.jpg");
  background-size: cover;
  background-position: center;
  /* Ground for the moment before the image paints. Sampled from the right-hand
     side of the shot itself, so text stays legible even in that gap. */
  background-color: #4a4c8c;
}

/* flex-start, not centre: the copy is top-aligned so the logo sits near the top
   edge of the frame. The padding is deliberately asymmetric — a smaller top
   than bottom — which pulls the whole block up while leaving the CTA breathing
   room above the lower edge. */
.ffs-vbanner__inner {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  padding: 32px 28px 40px;
}

/* 620px is measured, not chosen by eye. Two constraints, in this order:

     LOWER bound — the longest bullet ("Fortified with B1, B2 … nutrition.") is
     559px at the desktop 20px/600 size, so the column needs 597px for it to sit
     on one line. Below that it wraps and the list goes ragged.

     UPPER bound — at container width the rightmost edge of the plate and props
     falls at x=359 of 1170. A 620px column starts at x=494, clearing the food
     by 135px. There is room to go wider before the text would crowd the plate;
     620 is the narrower, safer end of the range that still satisfies the
     bullet.

   Re-measure both if the copy or the type sizes change. */
.ffs-vbanner__panel { max-width: 620px; }

/* Sized by HEIGHT with width auto, so the badge's 2.96:1 ratio is preserved and
   the cap is on the dimension that actually competes with the text beside it.
   The width/height attributes on the <img> still supply the intrinsic ratio, so
   nothing reflows once it loads. */
.ffs-vbanner__logo {
  display: block;
  width: auto;
  height: 64px;
  max-width: 100%;
  margin: 0 0 22px;
}

.ffs-vbanner__eyebrow {
  display: block;
  margin: 0 0 18px;
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  /* White, not --ffs-gold — see the contrast note above. */
  color: #fff;
}

.ffs-vbanner__title {
  margin: 0 0 28px;
  color: #fff;
  font-size: 2.25rem;
  font-weight: 900;
  line-height: 1.1;
}

.ffs-vbanner__list {
  margin: 0 0 36px;
  padding: 0;
  list-style: none;
  color: #fff;
}
.ffs-vbanner__list li {
  position: relative;
  padding-left: 38px;
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.5;
}
.ffs-vbanner__list li + li { margin-top: 14px; }

/* See the "two sanctioned exceptions" note in section 4 before touching this.
   It may be increased; it may NOT be reduced below the 1.1667rem global floor,
   which is what keeps white-on-orange accessible. */
.ffs-vbanner .ffs-btn {
  font-size: 1.25rem;
  min-height: 60px;
  padding: 16px 34px;
}

/* The glyph is decorative and the text beside it already carries the meaning,
   so the <i> is aria-hidden in the markup and this is pure presentation.

   Gold is fine HERE where white is not required: these are 6px-wide solid
   glyphs, which WCAG 1.4.11 scores as non-text content at 3:1, and gold clears
   that. The bullet TEXT stays white. */
.ffs-vbanner__list i {
  position: absolute;
  left: 0;
  /* Optically centred on the first line rather than the whole <li>, so a
     bullet that wraps to two lines keeps its icon beside line one. */
  top: .2em;
  width: 24px;
  text-align: center;
  color: var(--ffs-gold);
}

/* ---- responsive ---------------------------------------------------------
   Below 992px the layout stops being "text beside food": the container is too
   narrow for the panel to sit clear of the plate, and background-position
   centre crops the empty right-hand worktop away first — so the copy would
   land over the waffle and the bright marble, where white text fails badly.

   The fix is to move the crop rather than to darken the picture. Pinning
   background-position to the right edge keeps the dark indigo end of the
   photograph under the text at every narrow width, which preserves both the
   contrast and the client's "no tint" requirement. The plate is cropped out at
   these sizes, which is the right trade: the copy has to be readable, and the
   product is shown in full on the three category cards directly below.
   ------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .ffs-vbanner { background-position: right center; }
  .ffs-vbanner__inner { justify-content: flex-start; }
  .ffs-vbanner__panel { max-width: none; }
}

@media (min-width: 992px) {
  /* min-height is a floor, not a target — the block is taller than this at the
     desktop type sizes, so the banner grows past it and the value only matters
     if the copy is ever shortened. align-items stays flex-start from the base
     rule: the logo is meant to sit near the top edge, not be centred. */
  .ffs-vbanner__inner { padding: 44px 56px 64px; min-height: 480px; }
  .ffs-vbanner__logo { height: 80px; margin-bottom: 26px; }
  .ffs-vbanner__eyebrow { font-size: .875rem; }
  .ffs-vbanner__title { font-size: 3rem; }
  .ffs-vbanner__list li { font-size: 1.25rem; }
}

/* --------------------------------------------------------------------------
   14. Utilities
   -------------------------------------------------------------------------- */
.ffs-section { padding: 80px 0; }
@media (min-width: 992px) { .ffs-section { padding: 110px 0; } }

.ffs-section--tint { background: var(--ffs-surface); }

/* Interior-page masthead. The brief keeps white the dominant background and
   asks the neutral gray for "background variations", so the banner takes the
   gray band and the blue is reserved for the header, the footer and the hero.
   No colour overrides are needed inside: the default heading (blue, 9.18:1)
   and body (7.53:1) colours both clear AA on the band. */
.ffs-page-banner {
  padding: 56px 0;
  background: var(--ffs-surface);
  border-bottom: 1px solid var(--ffs-border);
}
@media (min-width: 992px) {
  .ffs-page-banner { padding: 72px 0; }
}

/* The blue as a contained panel, not a band across the viewport. The section
   itself stays white and the container carries the fill, so the blue reads as
   a deliberate block on the page rather than a change of ground for the whole
   width. Was .ffs-section--dark. */
.ffs-section--panel {
  padding-top: 20px;
  padding-bottom: 20px;
}
@media (min-width: 992px) {
  .ffs-section--panel {
    padding-top: 40px;
    padding-bottom: 40px;
  }
}
.ffs-section--panel > .auto-container {
  padding: clamp(32px, 5vw, 56px) clamp(20px, 4vw, 40px);
  border-radius: 16px;
  background: var(--agriox-primary);
  color: rgba(255, 255, 255, .88);
}
.ffs-section--panel h1,
.ffs-section--panel h2,
.ffs-section--panel h3 { color: #fff; }

/* The CTA needs no inversion on the blue grounds any more. Tertiary Orange
   measures 3.12:1 against Primary Blue, which clears the 3:1 WCAG 1.4.11
   asks of a control boundary, and its near-black label is unaffected by the
   ground behind the fill — 5.44:1 either way. The old rule inverted the
   button here because the accent blue was 1.65:1 on the old navy; that
   problem is gone with the palette that caused it. */
.ffs-hero .ffs-btn--outline,
.ffs-section--panel .ffs-btn--outline {
  border-color: rgba(255, 255, 255, .55);
  color: #fff;
}
.ffs-hero .ffs-btn--outline:hover,
.ffs-hero .ffs-btn--outline:focus,
.ffs-section--panel .ffs-btn--outline:hover,
.ffs-section--panel .ffs-btn--outline:focus {
  background: #fff;
  border-color: #fff;
  color: var(--agriox-primary);
}

.ffs-lede {
  font-size: 1.125rem;
  line-height: 1.6;
  color: var(--agriox-color-1);
  max-width: 62ch;
}

/* Lifts the 62ch measure so a lede spans the same width as the h1 above it,
   which carries no max-width of its own and fills .auto-container.

   CURRENTLY UNUSED. Both callers were page banners that have since been
   rebuilt as .ffs-hero sections, where the copy is .ffs-hero__text and takes
   its own 52ch measure against the scrim instead:
     - the About banner, whose 86-character sentence broke onto a second line
       with one orphaned word ("since 1998.") under it;
     - the Business Solutions banner, whose ~330-character lede wrapped at
       roughly half the headline width and read as a narrow column beside it.

   Kept because the problem it solves recurs on any wide banner lede, and it is
   an opt-in modifier that costs nothing while it has no callers. 62ch is a
   readability cap and is right for every other .ffs-lede on the site, which is
   why this is a modifier rather than a change to the base rule.

   Deliberately NOT `white-space: nowrap`. Below roughly 800px the sentence
   cannot fit on one line at any width, and nowrap would push it out of the
   container — where .page-wrapper's overflow:hidden would silently clip the
   end of it rather than wrap it. This lets it be one line wherever one line
   fits and wrap naturally where it does not. */
.ffs-lede--wide {
  max-width: none;
}

.ffs-eyebrow {
  display: block;
  margin-bottom: 12px;
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--agriox-primary);
}

/* Global heading/paragraph spacing. Eyebrows get 0.75rem below; headings get
   1rem below; body text gets 1.6 line-height and 1rem between paragraphs.
   Paragraphs use text-wrap: pretty globally to reduce single-word widows;
   component-level overrides remain in their own rules. */
p {
  text-wrap: pretty;
}

.ffs-section h1,
.ffs-section h2,
.ffs-section h3 {
  margin-bottom: 1rem;
}

.ffs-section p {
  line-height: 1.6;
  margin-bottom: 1rem;
  text-wrap: pretty;
}

.ffs-section p:last-child {
  margin-bottom: 0;
}

.ffs-section--panel .ffs-eyebrow { color: var(--agriox-base-on-dark); }

/* Body copy on the blue panel. --agriox-color-1 is a mid grey tuned for white
   grounds and drops to roughly 2:1 on #2e3192, so a lede inside the panel has
   to be lifted off the base rule. White at 85% measures 8.4:1 — comfortably
   AAA, and still visibly secondary to the pure-white heading above it. */
.ffs-section--panel .ffs-lede { color: rgba(255, 255, 255, .85); }

/* White cards sitting inside the blue panel.
   The panel paints h1-h3 white so its own headings read on the blue; a card
   with a white background inverts that ground, so its heading has to go back
   to the ink blue or it disappears entirely. Same reasoning for the body copy,
   which returns to the standard grey it uses everywhere else on white. */
.ffs-panel-card {
  height: 100%;
  padding: 28px;
  border-radius: var(--ffs-radius);
  background: #fff;
}
.ffs-section--panel .ffs-panel-card h3 {
  margin: 0 0 8px;
  font-size: 1.0625rem;
  color: var(--ffs-ink);
}
.ffs-section--panel .ffs-panel-card p {
  margin: 0;
  font-size: .9375rem;
  color: var(--agriox-color-1);
}

.ffs-stack > * + * { margin-top: 1.1em; }
