/* ==========================================================================
   Jagruti Appliances — Site Stylesheet
   Palette: sampled directly from the brand wordmark artwork.
   Root brand tokens: Jagruti Blue / Deep Blue / Sky Tint / Ink / Paper —
   the full blue and ink scales below are generated from those five anchors
   so every component on the site inherits the same brand colour.
   ========================================================================== */

/* ==========================================================================
   Webfont — General Sans (Fontshare / Indian Type Foundry)
   Self-hosted from assets/fonts/ rather than hotlinked from Fontshare's
   CDN: no third-party request on page load, no visitor data leaving the
   site, and no outage in someone else's infrastructure taking our type
   with it. Paths are relative to THIS file (assets/css/), hence ../fonts/.

   Only the four weights the stylesheet actually uses are shipped, ~96KB
   total. `font-display: swap` renders fallback text immediately and swaps
   General Sans in when it arrives, so copy is never invisible while the
   font loads.
   ========================================================================== */
@font-face {
  font-family: 'General Sans';
  src: url('../fonts/GeneralSans-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
  font-style: normal;
}
@font-face {
  font-family: 'General Sans';
  src: url('../fonts/GeneralSans-500.woff2') format('woff2');
  font-weight: 500;
  font-display: swap;
  font-style: normal;
}
@font-face {
  font-family: 'General Sans';
  src: url('../fonts/GeneralSans-600.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
  font-style: normal;
}
@font-face {
  font-family: 'General Sans';
  src: url('../fonts/GeneralSans-700.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
  font-style: normal;
}

/* Marcellus (Google Fonts, OFL) — the display serif for hero headlines only.
   Self-hosted from assets/fonts/ like General Sans, NOT hotlinked from
   fonts.gstatic.com, so the site keeps owning its own delivery and leaks no
   visitor data to a third party.

   One weight ships (400) because that is all Marcellus has — never ask for a
   bold, or the browser will synthesise a smeared faux-bold. Split latin /
   latin-ext on the same unicode-range boundaries Google serves, so the
   8.9KB ext file is only fetched by pages that actually need those glyphs. */
@font-face {
  font-family: 'Marcellus';
  src: url('../fonts/Marcellus-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
  font-style: normal;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Marcellus';
  src: url('../fonts/Marcellus-400-latin-ext.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
  font-style: normal;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* ---- Brand anchors (sampled from artwork) ---- */
  --brand-primary: #0d8ac4;  /* Jagruti Blue — fills the wordmark */
  --brand-deep:    #096590;  /* Deep Blue — hover / pressure states */
  --brand-tint:    #ebf5fa;  /* Sky Tint — washes / backgrounds */
  --brand-ink:     #172026;  /* Ink — body text on light ground */
  --brand-paper:   #ffffff;  /* Paper — the ground the mark ships on */
  --brand-canvas:  #f2f7f9;  /* Canvas — the page ground white cards sit on */

  /* ---- Blue scale, generated from the brand anchors ---- */
  --blue-900: #053d57;
  --blue-800: #075174;
  --blue-700: var(--brand-deep);
  --blue-600: var(--brand-primary);
  --blue-500: #13a7ec;
  --blue-400: #4eb8e9;
  --blue-300: #86caea;
  --blue-200: #bce0f0;
  --blue-100: var(--brand-tint);
  --blue-50:  #e9f2f7;  /* alt-section wash — sits a step below the canvas */

  /* ---- Ink scale ---- */
  --ink-900: var(--brand-ink);
  --ink-700: #344551;
  --ink-500: #607685;
  --ink-300: #a3b0b8;

  --white:  var(--brand-paper);
  --canvas: var(--brand-canvas);
  /* WhatsApp's brand green (#25d366) is only 1.98:1 against white — well
     below AA for a 14px label. These keep its exact hue (142°) and
     saturation but drop the lightness until the pill passes: 4.88:1 at
     rest, 6.09:1 on hover. Still unmistakably WhatsApp, now legible.
     The raw brand value stays available for the icon glyph. */
  --whatsapp: #17823f;
  --whatsapp-dark: #147137;
  --whatsapp-brand: #25d366;
  --gold: #d7a53b;
  --danger: #c0432f;

  /* ---- Gradients — pick a mood for a richer, less flat surface ---- */
  --gradient-primary: linear-gradient(135deg, var(--blue-500) 0%, var(--blue-600) 50%, var(--blue-700) 100%); /* energetic */
  --gradient-deep:    linear-gradient(160deg, var(--blue-900) 0%, var(--blue-700) 55%, var(--blue-600) 100%); /* premium / dark */
  --gradient-sheen:   linear-gradient(120deg, var(--blue-700) 0%, var(--blue-500) 55%, var(--blue-300) 100%); /* glossy accent */

  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 22px;
  --radius-pill: 999px;

  --shadow-sm: 0 2px 10px rgba(5, 61, 87, 0.08);
  --shadow-md: 0 10px 30px rgba(5, 61, 87, 0.12);
  --shadow-lg: 0 20px 50px rgba(5, 61, 87, 0.18);

  /* Page measurement. The site runs edge-to-edge rather than in a fixed
     box, so every .container (header included) shares one width and one
     gutter; reading-width blocks keep their own narrower caps. */
  --container: 100%;
  --gutter: 40px;
  --header-h: 76px;

  /* ==== Vertical rhythm ====
     The block padding every normal section uses, so the page keeps one
     spacing language from top to bottom.

     Fluid, not a fixed 72px: the hero and the pinned sections are sized in
     viewport units and grow with the window, so a fixed pad fell further
     behind them the taller the viewport got (72px against the journey
     section's 81px at 900px tall, 92px at 1024px). The vh term keeps the
     padded sections breathing at the same rate as the viewport-sized ones.

     These are the exact values .journey-section already proved in place —
     promoted to a token rather than reinvented. The floor keeps phones
     compact; the ceiling stops very tall windows opening a void. */
  --section-pad-y: clamp(64px, 9vh, 120px);
  --section-pad-y-tight: clamp(40px, 5.5vh, 72px);

  /* ---- Typography ----
     Geometric grotesque with a tall x-height and near-circular bowls.
     General Sans (Fontshare / Indian Type Foundry) is the face, chosen as
     the closest free stand-in for Aeonik — same geometric-grotesque
     construction and single-storey `a`, without Aeonik's commercial
     licence. It is self-hosted from assets/fonts/ (see the @font-face
     blocks above), NOT hotlinked from Fontshare's CDN, so the site owns
     its own delivery and leaks no visitor data to a third party.

     General Sans is the page face again (it briefly was Marcellus). Only the
     family was reverted: every --fs-*, --lh-*, --ls-* and --fw-* token below
     was left exactly as tuned during the Marcellus period. The Marcellus
     @font-face blocks above are now unused and can be removed. */

  /* ---- The page face ----
     General Sans, site-wide: headings, body, buttons, nav, labels. */
  --font-sans: "General Sans", "Segoe UI", -apple-system, BlinkMacSystemFont,
               "Helvetica Neue", Arial, sans-serif;

  /* Kept as its own token so headline-only tuning (the display line-height
     and tracking below) stays separable from the page face, even though both
     now resolve to the same family. */
  --font-display: "General Sans", "Segoe UI", -apple-system, BlinkMacSystemFont,
                  "Helvetica Neue", Arial, sans-serif;
  --fw-display: 400;

  /* The survivor of the General Sans era, for the two places that are drawn
     ARTWORK rather than text and need a REAL 700 at poster size:
       .stage-word          — the viewport-spanning product name behind the
                              machine on the home page
       .footer-wordmark span — the JAGRUTI wordmark closing every page
     At that scale a synthesised bold smears visibly, and the serif's hairline
     strokes thin out badly. The GeneralSans-*.woff2 files are still shipped
     for these two, so the cost is one extra font file per page. */
  --font-stage: "General Sans", "Segoe UI", -apple-system, BlinkMacSystemFont,
                "Helvetica Neue", Arial, sans-serif;
  /* Tighter than --lh-title: a serif at display size sets closer without the
     lines colliding, and 76/80 is the ratio the banner was tuned at. */
  --lh-display: .95;
  --ls-display: .005em;

  /* Heading colour — headings sit a step lighter than body ink, the way
     the reference greys its headings back from its body colour. */
  --heading-color: var(--blue-800);

  /* ---- The one page ground ----
     Every section on every page uses this. Do not introduce a second
     section background; use borders, cards and shadows for separation. */
  --page-bg: var(--brand-canvas);

  /* ==== Type scale ====
     One ramp for the whole site. Every size below is a step on it, and
     nothing outside this block should invent a size of its own — reach
     for the nearest step instead.

     Deliberately `em`, not px-bounded clamp(): the fluid root on `html`
     below already scales this whole ramp with the viewport, and px
     bounds here would pin the result and defeat that. The px comment
     beside each step is its delivered size at the 16px root (1440px
     viewport), not a fixed value.

     The ratio is roughly 1.25 (major third) through the body range and
     opens up above it, so display sizes separate clearly from titles
     while prose steps stay close enough to feel like one family. */
  --fs-caption: .75em;   /* 12px — timestamps, form notes, fine print */
  --fs-label:   .875em;  /* 14px — eyebrows, tags, table headers, meta */
  --fs-small:   1em;     /* 16px — dense UI copy, card descriptions */
  --fs-body:    1.13em;  /* 18px — running prose: p, li, blockquote */
  --fs-lead:    1.25em;  /* 20px — intro paragraph under a title */
  --fs-h6:      1.2em;   /* 19px */
  --fs-h5:      1.5em;   /* 24px */
  --fs-h4:      1.88em;  /* 30px */
  --fs-h3:      2.19em;  /* 35px */
  --fs-h2:      2.5em;   /* 40px */
  --fs-h1:      3em;     /* 48px */
  --fs-display: 3.75em;  /* 60px — hero and page-opening statements */

  /* Section-head aliases. Kept as names so a section's three-part head
     (eyebrow / title / lead) is tuned in one place; they point at the
     ramp above rather than carrying sizes of their own. */
  --fs-eyebrow: var(--fs-label);
  --fs-title:   var(--fs-h2);

  /* ==== Weight ====
     Three weights carry the whole site, and the contrast between them is
     what builds hierarchy — not size alone:
       700 headings and display   — the only weight that reads as "bold"
       500 labels and UI chrome   — eyebrows, tags, buttons, nav, meta
       400 prose                  — every paragraph and list item
     600 exists in the loaded set and stays available for the rare label
     that must out-rank its neighbours, but prefer 500.

     General Sans ships here at 400/500/600/700 only (see @font-face
     above). 800 and 900 are NOT loaded — asking for them makes the
     browser synthesise a smeared fake bold, so 700 is the real ceiling.
     Anything heavier needs a real font file added first. */
  --fw-body:    400;
  --fw-label:   500;
  --fw-strong:  600;
  --fw-title:   700;

  /* ==== Line height ====
     Tight for display (large text needs less leading to look set), open
     for prose (small text needs more to stay readable). */
  --lh-display: 105%;
  /* Section headings set tight, the way the hero does. 125% was too open
     to read as a scaled-down hero: the hero sets at .95, so a two-line
     section heading at 1.25 opened a gap the hero never has and the two
     stopped looking like the same system. 1.1 is the same "set tight"
     character at heading sizes — and it is what the bespoke section
     headings (.why-section 1.08, .services-intro .96) had each already
     reached for independently, which is the tell that the shared token
     was wrong rather than that they needed to opt out.

     --lh-card is the SMALL-heading counterpart: a 20px two-line product
     name or a 17px FAQ question needs more leading than a 40px banner
     heading, so card titles keep 1.25 rather than inheriting 1.1. Same
     family, different optical need. */
  --lh-title:   1.1;
  --lh-card:    1.25;
  --lh-body:    155%;
  --lh-tight:   120%;

  /* ==== Tracking ====
     Large type tightens optically; small caps-y labels open up.

     One tracking value per JOB, so the same kind of text tracks the same
     everywhere. Before this, uppercase labels doing identical work ran at
     .05 / .06 / .07 / .08 / .10 / .12 / .16 / .22em depending on which
     component they sat in; they now all resolve through --ls-label, with
     --ls-label-loose reserved for the two decorative marquee/watermark
     treatments where the wide track IS the effect. */
  /* --ls-display is the HERO's tracking. The hero sets at -.035em and is
     the site's visual benchmark, but this token said -.025em — so any
     display-scale heading that reached for the token landed looser than
     the thing it was supposed to match. Aligned to the hero. */
  --ls-display: -.035em;
  --ls-title:   -.02em;
  --ls-body:    0;
  --ls-label:   .12em;        /* uppercase labels — caps need the space */
  --ls-label-tight: .02em;    /* the same labels set in natural case */
  --ls-label-loose: .22em;  /* decorative marquee / watermark only */
  --ls-button:  .06em;

  /* ==== Card / UI sub-steps ====
     The ramp above covers prose and headings. These three name the sizes
     the CARD components kept re-inventing: a card title was written as
     1.13em here and 1.5em there, a card description as .88 / .8625 / .85 /
     .8438em, a category tag as .6875 / .7188 / .75em. Each job now has one
     name, so retuning a card title is one edit rather than nine. */
  --fs-card-title: 1.25em;   /* 20px — product / blog / need card titles */
  --fs-card-desc:  .9375em;  /* 15px — the copy under a card title */
  --fs-tag:        .6875em;  /* 11px — the uppercase category tag on a card */

  /* ==== Stat figures ====
     A statistic is not paragraph content: the number has to out-rank its
     label by size AND weight or the strip reads as a row of sentences.
     The hero strip previously set its figures at 17px/600 against a
     12.5px label — a 4.5px gap that flattened them.
     Only the label step is named here: the site has exactly one stats
     block and its figure size is bounded by that five-column layout, so
     it is set there rather than promised as a global step it would be the
     sole consumer of. */
  --fs-stat-label: .8125em;  /* 13px — the caption under a stat figure */

  /* ==== Button ====
     One label treatment for every CTA on the site; variants differ by
     colour, border and padding only. */
  /* Floored in px so a button label stays legible on a phone. These are
     `rem`, and the fluid root drops to 14px at narrow widths, which took
     the 14px label down to 11.4px and the compact one to 10.6px — too
     small for a control the reader has to tap. The max() holds them at
     13/12px while everything else still scales, so mobile buttons adapt
     by padding and width (below) rather than by shrinking their text. */
  --fs-button:    max(13px, .875rem);   /* 14px desktop, 13px floor */
  --fs-button-sm: max(12px, .8125rem);  /* 13px desktop, 12px floor */
  --fw-button:    500;
  /* The button's own leading, rather than borrowing --lh-tight: a button
     label is a single centred line and its leading affects the pill's
     height, so it should not move when --lh-tight is retuned for labels. */
  --lh-button:    1.2;
}

* { box-sizing: border-box; }
html {
  /* No `scroll-behavior: smooth` here on purpose. Lenis animates the scroll
     position itself, and the native smooth behaviour fights it: the browser
     and Lenis both try to own the same easing, which reads as a stutter.
     Anchor scrolling goes through lenis.scrollTo() in main.js instead. */
  /* Fluid root: the whole em-based scale below rides on this one value,
     so every heading, body size and utility step scales together with the
     viewport instead of stepping at breakpoints.
     - The floor and ceiling are in `rem`, not px (0.875rem = 14px and
       1.25rem = 20px at a default browser setting). px bounds would pin
       the result and silently defeat the rem term below, so a reader who
       raises their browser font size would see no change at all.
     - The `rem + vw` form (rather than raw `vw`) is deliberate: a pure
       `vw` root cannot be overridden by the browser's font-size setting,
       which breaks zoom for anyone who needs larger text. Keeping a rem
       term preserves that. Tuned so the root is exactly 16px at 1440px,
       the most common desktop width.
     The px comments beside every `em` below are the values at a 16px
     root — the true delivered size scales with this. */
  font-size: clamp(0.875rem, 0.685rem + 0.35vw, 1.25rem);
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-weight: 400;
  /* Body stays at the 16px root so every `em` below resolves against 16px
     rather than compounding. Paragraph copy gets its 18px on `p`/`li`. */
  font-size: 1em;
  line-height: 140%;
  letter-spacing: 0;
  color: var(--ink-700);
  background: var(--page-bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; margin: 0; padding: 0; }
button { font-family: inherit; cursor: pointer; }

/* ---------- Tag defaults ----------
   Heading weight is 700 (Bold) throughout. Body copy stays at 400, so the
   contrast between the two is what carries the hierarchy. Display sizes
   tighten optically; body sizing stays at 0 tracking. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-sans);
  font-weight: 700;
  color: var(--heading-color);
}
h1 { margin: 0 0 1em;    font-size: var(--fs-h1); line-height: var(--lh-title); letter-spacing: var(--ls-title); }
h2 { margin: 2em 0 .5em; font-size: var(--fs-h2); line-height: var(--lh-title); letter-spacing: var(--ls-title); }
h3 { margin: 0 0 .5em;   font-size: var(--fs-h3); line-height: var(--lh-title); letter-spacing: var(--ls-title); }
h4 { margin: 0 0 .4em;   font-size: var(--fs-h4); line-height: var(--lh-card); letter-spacing: -0.01em; }
h5 { margin: 0 0 .4em;   font-size: var(--fs-h5); line-height: var(--lh-tight); letter-spacing: -0.01em; }
h6 { margin: 0 0 .4em;   font-size: var(--fs-h6); line-height: var(--lh-tight); letter-spacing: -0.01em; }

/* Visually hidden but read by search engines and screen readers - used for
   section headings that keep the H1-H6 outline unbroken without adding
   visible text to the design. */
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Prose. 400 weight, the open body leading, and the one body size — the
   contrast against the 700 headings above is what carries the hierarchy,
   so body copy never needs to reach for a heavier weight to be read. */
p  { margin: .5em 0 1em; font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-700); }
li { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--ink-700); }
blockquote {
  margin: 0 0 10px;
  padding: 10px 20px;
  border-left: 5px solid var(--blue-400);
  font-size: var(--fs-lead);
  font-weight: var(--fw-body);
  line-height: var(--lh-title);
  color: var(--ink-700);
}
small { font-size: var(--fs-label); }
/* Body prose is 400 and headings are 700. <strong>/<b> sits at 700 to carry
   semantic emphasis inside running prose, and .logo-mark is a wordmark, not
   text in the scale. The .text-*-medium utilities run at 600 so mid-level
   labels stay visibly subordinate to the 700 headings. */
strong, b { font-weight: 700; }

/* ==========================================================================
   Semantic type utilities

   Named for the JOB the text does, not its pixel size — `.lead` stays the
   lead paragraph after the ramp is retuned, where a `.text-20-regular`
   would quietly become a lie. Each one is a step on the --fs-* ramp in
   :root; none invents a size of its own.

   Colour is inherited so a utility can sit on any ground; add a colour
   class or a parent colour to steer it.

   This replaced a 40-class `.text-{px}-{weight}` scale that no page ever
   referenced — and which had drifted internally (`.text-22-medium` was
   24px, `.text-45-regular` 38.4px). If you need a size that is not here,
   add a step to the ramp rather than a one-off rule.
   ========================================================================== */
.u-display, .u-title, .u-lead, .u-body,
.u-small, .u-label, .u-caption {
  font-family: var(--font-sans);
  margin-top: 0;
  margin-bottom: 0;
}

/* Display — hero and page-opening statements. Tight leading and negative
   tracking: at this size the default spacing reads as loose. */
.u-display {
  font-size: var(--fs-display); font-weight: var(--fw-title);
  line-height: var(--lh-display); letter-spacing: var(--ls-display);
  color: var(--heading-color);
}
/* Title — a section heading outside the h1–h6 flow. */
.u-title {
  font-size: var(--fs-title); font-weight: var(--fw-title);
  line-height: var(--lh-title); letter-spacing: var(--ls-title);
  color: var(--heading-color);
}
/* Lead — the intro paragraph under a title. One step above body, still
   400: it earns emphasis from size, not weight. */
.u-lead {
  font-size: var(--fs-lead); font-weight: var(--fw-body);
  line-height: var(--lh-body); color: var(--ink-700);
}
/* Body — running prose. Matches the p/li defaults, for when the element
   is not a p (a div, a td) but should read as prose. */
.u-body {
  font-size: var(--fs-body); font-weight: var(--fw-body);
  line-height: var(--lh-body); color: var(--ink-700);
}
/* Small — dense UI copy: card descriptions, table cells, helper text. */
.u-small {
  font-size: var(--fs-small); font-weight: var(--fw-body);
  line-height: var(--lh-body); color: var(--ink-700);
}
/* Label — eyebrows, tags, meta, column heads. Uppercase and tracked out;
   500 so it reads as chrome, clearly subordinate to a 700 heading. */
.u-label {
  font-size: var(--fs-label); font-weight: var(--fw-label);
  line-height: var(--lh-tight); letter-spacing: var(--ls-label);
  text-transform: uppercase;
}
/* Caption — timestamps, form notes, fine print. The floor of the ramp. */
.u-caption {
  font-size: var(--fs-caption); font-weight: var(--fw-body);
  line-height: var(--lh-tight); color: var(--ink-500);
}

.container { max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); }
.section { padding-block: var(--section-pad-y); background: var(--page-bg); }
.section-tight { padding-block: var(--section-pad-y-tight); }
/* Kept so the ~41 pages using it still parse, but it no longer alternates —
   one ground runs the length of every page. Separation comes from cards
   and rules, not from a second background colour. */
.section-alt { background: var(--page-bg); }
.eyebrow {
  display: inline-block;
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-label);
  /* Was inheriting the 1.55 body leading, which padded the pill unevenly
     and left the label sitting differently depending on whether its parent
     was prose or a flex column. A label is one line — set it tight. */
  line-height: var(--lh-tight);
  /* Natural case. The HTML already writes these as "Featured", "Why
     Jagruti", "Service & Support" — the uppercase transform was what made
     every section open with a shouted label. With it gone the eyebrow
     reads as quiet metadata, which is its job.

     Tracking drops with the case: +.12em is set for CAPS, where the extra
     space separates letterforms that all share one height. On lowercase
     it just looks loose, so this uses the smaller label tracking. */
  letter-spacing: var(--ls-label-tight);
  text-transform: none;
  color: var(--blue-600);
  background: var(--blue-100);
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  margin-bottom: 16px;
}
.section-head { max-width: 700px; margin: 0 auto 44px; text-align: center; }
/* h2 inside a section head loses the tag's top margin — the section
   padding already supplies the space. */
.section-head h2 { margin-top: 0; }  /* typography: the section-heading system at the end of this file */
/* The lead under a section title. 400 weight and one step above body —
   size carries the emphasis, so it never competes with the 700 h2. */
.section-head p  { font-size: var(--fs-lead); font-weight: var(--fw-body); line-height: var(--lh-body); }
.need-section .section-head p,
.ranges-section .section-head p { font-size: var(--fs-small); line-height: var(--lh-body); }

/* ---------- Buttons ----------
   One geometry for every button on the site, following the reference:
   a full pill, .88em/1.30em padding, an uppercase label at weight 400,
   and a background-only hover with no lift. Sizing is in `rem` so a
   button dropped into a smaller-typed context (a table cell, a card)
   keeps its size instead of compounding down. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .625rem;                    /* 10px — the reference's dot/label gap */
  padding: .875rem 1.3rem;         /* .88em / 1.30em */
  border-radius: var(--radius-pill);
  font-family: var(--font-sans);
  font-weight: var(--fw-button);
  font-size: var(--fs-button);
  line-height: var(--lh-button);
  /* Natural case. Every CTA on the site inherits this base, so the site
     previously shouted GET PRICE / VIEW PRODUCT / EXPLORE PRODUCTS at the
     reader from every card and band at once. The label now renders in the
     case the HTML already writes ("Explore Products", "View Product"), so
     no content had to be rewritten to calm it down.
     Tracking stays: a small amount still helps a short pill label, and it
     is what the hero CTA — the reference for this system — carries. */
  text-transform: none;
  letter-spacing: var(--ls-button);
  border: 1px solid transparent;
  transition: background .3s ease, color .3s ease, border-color .3s ease;
  white-space: nowrap;
}
/* The reference's hover is a background shift only — no transform. */
/* Solid — the reference's filled pill (#636f3d darkening to #404f1d),
   mapped onto the brand blues. Their rest state is 5.42:1 against white
   and their hover 8.92:1; blue-700 -> blue-800 gives 6.41:1 -> 8.61:1,
   the same "already legible, deepens on hover" behaviour.
   blue-600 was the obvious pick by name, but it is only 3.85:1 with
   white — below AA for a 14px label — so the pill starts one step darker. */
.btn-primary { background: var(--blue-700); color: var(--white); }
.btn-primary:hover { background: var(--blue-800); }

/* Translucent — the reference's .btn-new-transparent: a low-opacity wash
   over whatever sits behind, lifting slightly on hover.
   The site is overwhelmingly light ground, so the ink version is the
   DEFAULT and the white/glass version is opted into with .on-dark. A
   descendant selector was tried first and silently missed buttons that sit
   outside a .section, leaving white text on a white ground. */
.btn-outline {
  background: rgba(9,101,144,.06); color: var(--blue-700);
  border-color: var(--blue-200);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
}
.btn-outline:hover { background: rgba(9,101,144,.10); border-color: var(--blue-300); }

/* Opt-in for dark or photographic grounds. */
.btn-outline.on-dark, .hero .btn-outline, .page-hero .btn-outline,
.cat-hero .btn-outline, .range-feature .btn-outline, .cta-band .btn-outline {
  background: rgba(255,255,255,.07); color: var(--white); border-color: rgba(255,255,255,.22);
}
.btn-outline.on-dark:hover, .hero .btn-outline:hover,
.page-hero .btn-outline:hover, .cat-hero .btn-outline:hover,
.range-feature .btn-outline:hover, .cta-band .btn-outline:hover {
  background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.34);
}

/* Solid white — the reference's .btn-new-white, for dark grounds. */
.btn-ghost { background: var(--white); color: var(--blue-700); border-color: transparent; }
.btn-ghost:hover { background: rgba(255,255,255,.9); }

.btn-whatsapp { background: var(--whatsapp); color: var(--white); }
.btn-whatsapp:hover { background: var(--whatsapp-dark); }

/* Small variant keeps the pill and the label treatment, just tighter. */
.btn-sm { padding: .625rem 1rem; font-size: var(--fs-button-sm); }
.btn-block { width: 100%; }
.btn svg { width: 18px; height: 18px; flex: none; }

/* ---------- Header ---------- */
@keyframes navSheen {
  0% { transform: translateX(-120%) skewX(-12deg); }
  100% { transform: translateX(220%) skewX(-12deg); }
}
@keyframes waPulse {
  0%, 100% { box-shadow: 0 8px 20px rgba(37,211,102,.30), 0 0 0 0 rgba(37,211,102,.35); }
  50% { box-shadow: 0 8px 20px rgba(37,211,102,.30), 0 0 0 7px rgba(37,211,102,0); }
}
@keyframes navItemIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

/* ==========================================================================
   Header + slide-in menu overlay
   Pattern taken from farmminerals.com/promo: the bar holds no link list at
   any breakpoint — only a MENU button, the wordmark and the primary action.
   Every navlink lives in .menu-side-bar, a full-height panel that slides in
   from the left over a scrim. Because one component serves every viewport,
   responsiveness is a matter of the panel's width and type scale only.
   ========================================================================== */
.site-header {
  position: fixed; top: 0; left: 0; right: 0; z-index: 500;
  background: transparent;
  border-bottom: 1px solid transparent;
  height: var(--header-h);
  transition: height .3s cubic-bezier(.4,0,.2,1), box-shadow .3s ease,
              background .3s ease, border-color .3s ease, backdrop-filter .3s ease;
}
/* Scrolled state: the bar stays transparent — no white strip across the page.
   It shrinks slightly and drops the wordmark, so what is left reads as two
   floating controls over the content rather than as a chrome bar. */
.site-header.is-scrolled {
  height: calc(var(--header-h) - 14px);
  background: transparent;
  border-bottom-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
}
/* Once the bar is scrolled it becomes a compact utility strip: the wordmark
   drops out (the page itself is the context by then) and only the MENU
   affordance and the WhatsApp action remain. Visibility rather than display
   keeps the grid's three columns, so menu and action hold their edges. */
.site-header.is-scrolled .logo {
  opacity: 0; visibility: hidden; pointer-events: none;
  transform: translateY(-6px);
}
.logo {
  transition: opacity .25s ease, transform .25s cubic-bezier(.4,0,.2,1), visibility .25s;
}
/* With no ground behind it the MENU label must carry its own contrast; a soft
   pill keeps it legible over both light sections and photography. */
.site-header.is-scrolled .menu-btn-wrap {
  background: rgba(255,255,255,.9);
  backdrop-filter: blur(10px) saturate(1.3);
  -webkit-backdrop-filter: blur(10px) saturate(1.3);
  border-radius: var(--radius-pill);
  padding: 8px 14px;
  box-shadow: 0 4px 16px rgba(5,61,87,.12);
  color: var(--ink-900);
}
/* Three columns with equal 1fr outers keep the wordmark optically centered
   even though the menu button and the action button differ in width. */
.nav-wrap {
  height: 100%;
  padding-left: var(--gutter); padding-right: var(--gutter);
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 20px;
}

/* The bar sits over the page rather than pushing it down, so every page needs
   its own clearance. Pages that open on the light .hero pull it back up so
   header and hero read as one surface (as the reference does). */
   Which of the three it gets is declared once per page as data-nav on <body>,
   stamped by assets/js/build-partials.js from the page's opening section:

     data-nav="flow"        default. The page reserves header height, so its
                            content starts below the floating bar.
     data-nav="over-dark"   the page opens on a dark hero that runs up under
                            the bar: no padding, scrim across the header band,
                            white MENU.
     data-nav="over-light"  same tuck-under, but on a light opening section:
                            no padding, no scrim, dark MENU.

   Anything page-specific (the homepage hero's inner padding, contact's
   half-width scrim) stays with that page's own block further down. */
body { padding-top: var(--header-h); }
body[data-nav="over-dark"],
body[data-nav="over-light"] { padding-top: 0; }
body[data-nav="over-dark"] main > .hero:first-child { padding-top: var(--header-h); }

/* Over a dark hero the MENU affordance inverts to white so it stays legible
   on the photo. Over a light one it keeps the page's own ink. */
body[data-nav="over-dark"] .site-header:not(.is-scrolled) .menu-btn-wrap {
  color: #fff;
}
body[data-nav="over-light"] .site-header:not(.is-scrolled) .menu-btn-wrap {
  color: var(--ink-900);
}
/* The logo is never altered to suit the hero: no recolour, no shadow, no
   ground of its own. It renders as the brand artwork on every page. What
   changes instead is the photo behind it.

   The hero sky sits at roughly the logo's own blue (~1.04:1 contrast), so the
   mark is invisible until the picture behind it is darkened. This scrim is a
   top-down gradient across the header band only, fading out well before the
   hero copy: the brand blue then reads against a deeper sky instead of an
   equally bright one. It hangs off .site-header rather than .hero because the
   header is fixed and .hero is overflow:hidden, which would clip it.
   Full-bleed and fading to nothing, it reads as depth in the photograph
   rather than as a panel behind the logo. */
body[data-nav="over-dark"] .site-header:not(.is-scrolled)::before {
  content: "";
  position: absolute; left: 0; right: 0; top: 0;
  height: calc(var(--header-h) + 46px);
  pointer-events: none;
  z-index: 0;
  background: linear-gradient(180deg,
    rgba(0,22,36,.86) 0%,
    rgba(0,22,36,.74) 42%,
    rgba(0,22,36,.46) 68%,
    rgba(0,22,36,.16) 88%,
    transparent 100%);
}
/* The bar's own controls sit above the scrim. */
.site-header .nav-wrap { position: relative; z-index: 1; }

/* ---------- MENU button ----------
   Icon + lowercase caps label, exactly the reference's "menu" affordance.
   The three rules converge and cross-fade into an X while the panel is open,
   so the same control reads as the close affordance if the panel is dismissed
   by the scrim. */
.menu-btn-wrap {
  justify-self: start; align-self: center;
  display: flex; align-items: center; gap: .38em;
  line-height: 1;
  background: none; border: none; padding: 6px 2px; cursor: pointer;
  color: var(--ink-900);
  transition: color .2s ease, opacity .2s ease;
}
.menu-btn-wrap:hover { opacity: .68; }
.icon-menu { display: flex; align-items: center; justify-content: center; width: 1.15em; height: 1.15em; }
.icon-menu svg { width: 100%; height: 100%; }
.menu-btn-wrap .ml1, .menu-btn-wrap .ml2, .menu-btn-wrap .ml3 {
  transform-origin: 50% 50%;
  transition: transform .4s cubic-bezier(.23,1,.32,1), opacity .2s ease;
}
.menu-btn-wrap[aria-expanded="true"] .ml1 { transform: translateY(5px) rotate(45deg); }
.menu-btn-wrap[aria-expanded="true"] .ml2 { opacity: 0; }
.menu-btn-wrap[aria-expanded="true"] .ml3 { transform: translateY(-5px) rotate(-45deg); }
.menu-text {
  font-size: var(--fs-label); font-weight: var(--fw-label); line-height: var(--lh-tight);
  text-transform: uppercase; letter-spacing: var(--ls-label);
}

/* The logo is a two-line block (wordmark + tagline) sitting between two
   single-line controls. Centering the whole block would push the wordmark
   above the menu label and the WhatsApp pill, so the tagline is taken out of
   the flow: the wordmark itself is what gets centered, and the tag hangs
   under it without shifting the baseline the other two controls share. */
.logo { display: flex; align-items: center; align-self: center; justify-content: center; gap: 10px; flex: none; }
.logo > div { position: relative; }
/* The real brand logo is a single lockup: the badge already carries both the
   wordmark and the "Every Home Need..." tagline, so no separate text sits
   beside it. Height drives the size and width follows the 986x256 aspect,
   which keeps it from crowding the 76px bar. */
.logo-img {
  display: block; height: 46px; width: auto; max-width: 100%;
  transition: transform .25s cubic-bezier(.34,1.56,.64,1);
}
.logo:hover .logo-img { transform: translateY(-1px) scale(1.03); }
.logo-mark {
  position: relative; overflow: hidden;
  font-weight: 700; font-size: 1.3125em; /* 21px */ letter-spacing: .02em; color: var(--blue-700);
  border: 2.5px solid var(--blue-500); border-radius: var(--radius-pill);
  padding: 5px 16px; line-height: 1;
  transition: border-color .25s ease, box-shadow .25s ease, transform .25s cubic-bezier(.34,1.56,.64,1), color .25s ease;
}
.logo-mark::after {
  content: ""; position: absolute; top: 0; left: 0; width: 40%; height: 100%;
  background: linear-gradient(120deg, transparent, rgba(255,255,255,.75), transparent);
  transform: translateX(-120%) skewX(-12deg);
}
.logo:hover .logo-mark {
  transform: translateY(-1px) scale(1.03);
  border-color: var(--blue-700);
  box-shadow: 0 6px 18px rgba(13,138,196,.25);
}
.logo:hover .logo-mark::after { animation: navSheen .9s ease; }
.logo-tag {
  position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  display: block; white-space: nowrap;
  font-size: 10.5px; font-style: italic; color: var(--ink-500); margin-top: 2px;
  transition: color .25s ease;
}

.header-actions { display: flex; align-items: center; align-self: center; justify-content: flex-end; gap: 10px; }
.btn-whatsapp { animation: waPulse 2.6s ease-in-out infinite; }
.btn-whatsapp:hover { animation-play-state: paused; }

/* ---------- Slide-in menu overlay ----------
   .menu-side-bar is the full-viewport layer; .menu-side-bg is the scrim that
   fades in behind it; .menu-side-bar-inner is the coloured panel that slides.
   Splitting scrim from panel is what lets the two animate on different curves,
   which is most of why the reference's open feels weighted rather than linear. */
.menu-side-bar {
  position: fixed; inset: 0; z-index: 900;
  height: 100vh; height: 100dvh;
  visibility: hidden; pointer-events: none;
  /* visibility is what makes the overlay non-interactive at rest, but it is
     NOT interpolatable: without a delay it flips to hidden on the frame the
     .open class leaves, blanking the panel, scrim and text while their own
     transitions are still running — the close was fully animated, just
     invisible. Holding visibility until the slowest close step has finished
     (panel: .28s delay + .85s travel = 1.13s) lets the dismissal actually be
     seen; the open flips it to visible instantly, so only the close waits. */
  transition: visibility 0s linear 1.13s;
}
.menu-side-bar[hidden] { display: none; }
.menu-side-bar.open {
  visibility: visible; pointer-events: auto;
  transition: visibility 0s linear 0s;
}
/* Scrim timing measured off the live reference: it does not fade with the
   panel — it waits until the panel is nearly home (~660ms) and then fades up
   over ~450ms. The close plays that same step in reverse. */
.menu-side-bg {
  position: absolute; inset: 0; z-index: 1;
  background: rgba(5,61,87,.45);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  opacity: 0;
  /* True reverse of the open. The open is: text in from .66s, scrim up at
     .66s over .45s, panel travelling 0-.85s. Played backwards the scrim is
     the LAST thing to go, not the first — it holds while the panel slides
     out from under it and only then clears. Total close = .28s + .85s =
     1.13s, so the scrim starts at .68s and fades over .45s to land with it. */
  transition: opacity .45s ease .68s;
}
.menu-side-bar.open .menu-side-bg {
  opacity: 1;
  transition: opacity .45s ease .66s;
}

.menu-side-bar-inner {
  position: absolute; top: 0; left: 0; z-index: 2;
  /* The reference panel is 47em; its five short words need less room than
     seven longer ones, but 47em on a wide screen reads as half the page, so
     this sits a step under it and caps in px on very wide monitors. */
  width: min(38em, 92vw); max-width: 100vw;
  /* The panel must never exceed the visible viewport, or its scrolling column
     measures against a height that is partly under a mobile browser toolbar
     and the last rows sit below the fold with nothing to scroll them into
     view. dvh tracks the toolbar; vh is the fallback where it is unsupported. */
  height: 100vh; height: 100dvh; max-height: 100dvh;
  padding: 1.63em 1.88em 1.88em var(--gutter);
  /* Row, not column: the CLOSE rail and the vertical divider stand beside
     the link column, exactly as .menu-side-bar-inner does in their CSS
     (display:flex with the default row direction). */
  display: flex; flex-direction: row; align-items: stretch; gap: 1.88em;
  /* The reference's flat dark ground, translated to the brand gradient. */
  background: var(--gradient-deep);
  color: #fff;
  overflow: hidden;
  transform: translateX(-100%);
  /* Sampled from the live reference: the panel travels its full width in
     ~850ms on a hard deceleration (outQuart — 36% of the distance covered by
     150ms, 89% by 450ms, settling around 850ms). Closing is quicker at
     ~450ms, so the menu opens with weight and dismisses without ceremony. */
  /* Close mirrors the open: same .85s travel, same curve.
     The .28s delay is the mirror of the open's text-lag. On the open the
     panel leads and the words follow it in; reversed, the words leave first
     and the panel follows them out, so it waits out the leading edge of the
     text stagger before it starts moving. The overlap is identical in both
     directions — only the order is flipped. */
  transition: transform .85s cubic-bezier(.165,.84,.44,1) .28s;
}
.menu-side-bar.open .menu-side-bar-inner {
  transform: translateX(0);
  transition: transform .85s cubic-bezier(.165,.84,.44,1);
}

/* CLOSE occupies a narrow rail of its own, aligned to the top of the panel,
   with the link column beginning on the far side of the divider. */
.close-btn-wrap {
  flex: none; display: flex; align-items: flex-start; justify-content: flex-start;
}
.menu-side-bar .close-btn {
  display: flex; align-items: center; gap: .38em;
  background: none; border: none; padding: 6px 2px; cursor: pointer;
  color: #fff; transition: opacity .2s ease;
}
.menu-side-bar .close-btn:hover { opacity: .68; }
/* Vertical hairline: their .light-divider is width:1px / height:100% at
   opacity .1, turning horizontal only on mobile. */
.light-divider { flex: none; width: 1px; height: 100%; background: rgba(255,255,255,.5); opacity: .22; }

/* The items column and the utility block push apart to fill the panel, the
   same distribution the reference uses. */
.menu-items {
  display: flex; flex-direction: column; justify-content: space-between;
  gap: 1em; flex: 1 1 auto; min-width: 0; min-height: 0;
  /* Their 6.88em top inset drops the first link well below the CLOSE row. */
  padding-top: 4.5em;
}
/* With seven top-level rows and a seven-item Products list, an expanded
   section can exceed any viewport — the reference never faces this with its
   five short sections. So the row column scrolls on its own while the header
   and the utility block below stay pinned. */
.menu-items-wrap {
  display: flex; flex-direction: column;
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.35) transparent;
  /* The mask's faded tail is painted over the container's own bottom edge, so
     without a runway the final sub-link of an expanded section can never be
     scrolled clear of it and reads as unreachable. This padding is that
     runway; it also keeps the last row off the utility block's hairline. */
  padding-bottom: 2.25em;
  /* Vertical only. Anything wide enough to need sideways travel is a layout
     bug, not something to scroll to. */
  overflow-x: hidden;
  /* Fades the rows out as they meet the utility block rather than clipping
     them on a hard edge. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 18px), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 18px), transparent 100%);
}
/* flex children default to min-height:auto, but these rows must be free to
   report their expanded height to the scroll container instead of refusing to
   shrink and pushing the tail of the list outside it. */
.menu-items-wrap > .link-item { flex: none; }
.menu-items-wrap::-webkit-scrollbar { width: 4px; }
.menu-items-wrap::-webkit-scrollbar-thumb { background: rgba(255,255,255,.35); border-radius: 4px; }

/* ---------- Menu rows ----------
   Each row: 36px icon, display-scale title, optional chevron, and an
   underline that wipes in from the left on hover. */
/* Row geometry, read off the reference's IX2 action lists (a-3 / a-5 in
   webflow.0b3be3c5.js, "Menu Link Hover Products" / "…About"):

     .menu-link-heading  translateX  0 → 3.25em   800ms  outQuart
     .icon-36            opacity     0 → 1        300ms  @200ms delay
     .link-item_links    height      0 → 8-10em  1000ms  outQuart @200ms
     sibling .link-item  opacity     1 → .2       500ms  @200ms delay

   The icon sits in the gutter the title vacates as it slides — at rest the
   row is title-only, which is why the reference's menu reads as bare type. */
.menu-link-heading {
  position: relative;
  display: flex; align-items: center;
  width: 100%; padding: .5em 0 .55em; text-align: left;
  /* The accordion rows are <button> and the rest are <a>; without this the
     UA button box adds its own padding and shifts those two titles right. */
  box-sizing: border-box; margin: 0; font: inherit; appearance: none;
  background: none; border: none; cursor: pointer;
  color: #fff; text-decoration: none;
  /* The reference does NOT stagger these in: sampling the live page shows
     every .link-item sitting at opacity 1 for the whole travel, riding in
     with the panel rather than assembling after it. Only the utility block
     at the foot fades up separately. */
}
.menu-side-bar .menu-link-heading { transition: color .25s ease; }
/* Sibling dimming — the move that gives the reference's menu its focus. The
   hovered row stays lit while every other row drops to .2. Driven from the
   wrap so it works on hover and on keyboard focus alike. */
@media (hover: hover) {
  .menu-items-wrap:hover .link-item,
  .menu-items-wrap:focus-within .link-item {
    opacity: .2;
    transition: opacity .5s ease .2s;
  }
}
.menu-items-wrap .link-item:hover,
.menu-items-wrap .link-item:focus-within { opacity: 1; }
/* outQuart, the reference's easing throughout the menu. */
.link-item { transition: opacity .5s ease .2s; }

.icon-36 {
  position: absolute; left: 0; top: 50%; margin-top: -1.125em;
  width: 2.25em; height: 2.25em; /* 36px */
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; border: 1px solid rgba(255,255,255,.28);
  color: var(--blue-200);
  /* Hidden at rest; appears 200ms into the slide, as in a-3. */
  opacity: 0;
  transition: opacity .3s ease;
}
.menu-link-heading:hover .icon-36,
.menu-link-heading:focus-visible .icon-36,
.link-item.is-open .icon-36 { opacity: 1; transition-delay: .2s; }
.icon-36 svg { width: 1.15em; height: 1.15em; display: block; }
.icon-36 { box-sizing: border-box; }
.menu-link-title {
  /* The reference sets 4.4em against five rows. Seven rows plus a pinned
     utility block need a shorter row, so the scale steps down while keeping
     their weight and 90% line-height — which is what makes it read as a menu
     rather than a link list. */
  font-weight: 500; font-size: clamp(1.5rem, 2.4vh + .9rem, 2.25rem);
  line-height: 90%; letter-spacing: -.01em;
  /* min-width:0 is what lets this shrink below its text width inside the flex
     row. Without it the title keeps its full intrinsic size, and the 3.25rem
     open-slide pushes that width past the panel edge — which is the sideways
     overflow that cut the longer labels off. */
  flex: 1 1 auto; min-width: 0;
  /* Mask for the slide-up: their .menu-link-title is overflow:hidden with the
     word inside it parked at translateY(140%) until the menu opens. */
  overflow: hidden;
  /* A clipped block would crop descenders on "y"/"g"; the padding gives them
     room and the negative margin keeps the row's height unchanged. */
  padding-bottom: .12em; margin-bottom: -.12em;
  /* The reference's 3.25em slide on its 800ms outQuart. The distance is
     expressed in rem, not em: 3.25em there resolves against the row's font
     size, whereas this title sets its own much larger size, so an em here
     would slide it more than twice as far. 3.25rem == 3.25 * 16px. */
  /* padding-right rides the same curve as the slide so the row narrows as the
     title travels, rather than snapping its width on the first frame. */
  transition: transform .8s cubic-bezier(.165,.84,.44,1),
              padding-right .8s cubic-bezier(.165,.84,.44,1);
}
/* The slide moves the title right by 3.25rem, so the row must give up exactly
   that much width or the tail of a long label lands outside the panel. The
   right padding keeps the travel identical while the text stays inside. */
.menu-link-heading:hover .menu-link-title,
.menu-link-heading:focus-visible .menu-link-title,
.link-item.is-open .menu-link-title {
  transform: translateX(3.25rem);
  padding-right: 3.25rem;
}

/* ---------- Link text: masked slide-up ----------
   Sampled from the live reference: each word sits at translateY(140%) behind
   the mask while the panel travels, then rises to 0% — the rows staggering
   ~67ms apart starting ~660ms in, each taking ~800ms on a hard ease-out.
   Closing pushes them back down to 130% in REVERSE row order, ~62ms apart
   and quicker overall (~730ms), so the menu empties from the bottom up. */
.title-inner {
  display: block;
  transform: translateY(130%);
  /* Identical to the open: same 800ms per row, same ease-out curve. The
     only difference between the directions is the stagger order — top-down
     in, bottom-up out — so the close is the open run backwards.
     Both directions must name the SAME cubic-bezier: outQuart and outExpo
     look alike on paper but outExpo carries a long tail that makes the
     same duration feel noticeably slower. */
  transition: transform .8s cubic-bezier(.16,1,.3,1);
}
.menu-side-bar.open .title-inner {
  transform: translateY(0);
  transition: transform .8s cubic-bezier(.16,1,.3,1);
}
/* Open: top row first. */
.menu-side-bar.open .link-item:nth-child(1) .title-inner { transition-delay: .66s; }
.menu-side-bar.open .link-item:nth-child(2) .title-inner { transition-delay: .727s; }
.menu-side-bar.open .link-item:nth-child(3) .title-inner { transition-delay: .794s; }
.menu-side-bar.open .link-item:nth-child(4) .title-inner { transition-delay: .861s; }
.menu-side-bar.open .link-item:nth-child(5) .title-inner { transition-delay: .928s; }
.menu-side-bar.open .link-item:nth-child(6) .title-inner { transition-delay: .995s; }
.menu-side-bar.open .link-item:nth-child(7) .title-inner { transition-delay: 1.062s; }
/* Close: bottom row leaves first — the reverse stagger is what makes the
   dismissal read as the open played backwards rather than a plain fade. */
.link-item:nth-child(7) .title-inner { transition-delay: 0s; }
.link-item:nth-child(6) .title-inner { transition-delay: .067s; }
.link-item:nth-child(5) .title-inner { transition-delay: .134s; }
.link-item:nth-child(4) .title-inner { transition-delay: .201s; }
.link-item:nth-child(3) .title-inner { transition-delay: .268s; }
.link-item:nth-child(2) .title-inner { transition-delay: .335s; }
.link-item:nth-child(1) .title-inner { transition-delay: .402s; }
.heading-chev {
  flex: none; width: 1.1em; height: 1.1em; color: rgba(255,255,255,.7);
  transition: transform .45s cubic-bezier(.23,1,.32,1), opacity .3s ease;
  opacity: 0;
}
.menu-link-heading:hover .heading-chev,
.menu-link-heading:focus-visible .heading-chev,
.link-item.is-open .heading-chev { opacity: 1; }
.heading-chev svg { width: 100%; height: 100%; }
.menu-link-heading[aria-expanded="true"] .heading-chev { transform: rotate(180deg); }
/* Underline wipe — the reference's .underline bar, animated from 0 to full
   width on hover and pinned open while a section is expanded. */
.underline {
  position: absolute; left: 0; bottom: 0; height: 1px; width: 100%;
  background: rgba(255,255,255,.9);
  transform: scaleX(0); transform-origin: left center;
  transition: transform .55s cubic-bezier(.23,1,.32,1);
}
.menu-link-heading:hover .underline,
.menu-link-heading:focus-visible .underline,
.menu-link-heading[aria-expanded="true"] .underline { transform: scaleX(1); }
.menu-link-heading.active .underline { transform: scaleX(1); background: var(--blue-300); }
.menu-link-heading.active .icon-36 { background: var(--blue-300); color: var(--blue-900); border-color: var(--blue-300); }

/* ---------- Accordion reveal ----------
   grid-template-rows 0fr → 1fr animates to the content's real height with no
   JS measurement and no max-height guess, on the reference's .7s expo curve. */
.link-item_links {
  display: grid; grid-template-rows: 0fr;
  padding-left: 3.4em;
  /* a-3: 1000ms outQuart, opening 200ms after the pointer lands; closing
     (a-4) is faster at 400ms, so the menu tidies itself briskly. */
  transition: grid-template-rows .4s cubic-bezier(.165,.84,.44,1);
}
.link-item.is-open .link-item_links {
  grid-template-rows: 1fr;
  transition: grid-template-rows 1s cubic-bezier(.165,.84,.44,1) .2s;
}
.link-item.is-open .link-item_links-inner {
  visibility: visible; pointer-events: auto;
  transition: visibility 0s linear 0s;
}
@media (hover: hover) {
  /* Once a row has been clicked, is-open is the only thing that opens a
     section. Without this the pointer still resting on the row re-expands it
     on the very frame the click collapsed it, so the chevron looks dead. */
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links,
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links {
    grid-template-rows: 1fr;
    transition: grid-template-rows 1s cubic-bezier(.165,.84,.44,1) .2s;
  }
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links-inner,
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links-inner {
    visibility: visible; pointer-events: auto;
    transition: visibility 0s linear 0s;
  }
}
.link-item_links-inner {
  overflow: hidden;
  /* A collapsed section still occupies a 0fr grid row, so its sub-links stay
     in the tab order and remain clickable through the row above them. Making
     the inner box inert while closed is what stops a hidden sub-link from
     stealing a click meant for the heading, and keeps focus-within from
     re-opening a section the visitor just collapsed. */
  visibility: hidden; pointer-events: none;
  transition: visibility 0s linear .4s;
  display: flex; flex-direction: column; gap: .1em;
  /* min-height:0 lets the 0fr→1fr grid row actually collapse and expand;
     without it the inner box floors at its content height and the section
     measures wrong inside the scrolling column. */
  min-height: 0;
}
.link-item_links .nav-link {
  display: flex; flex-direction: column; gap: 2px;
  padding: .55em 0; text-decoration: none;
  color: rgba(255,255,255,.92);
  /* "Food Processing Machinery" is wider than the padded column on a narrow
     panel; letting it wrap keeps every sub-link readable in place instead of
     pushing the column sideways behind a horizontal scrollbar. */
  min-width: 0; overflow-wrap: break-word;
  opacity: 0; transform: translateY(8px);
  transition: opacity .4s ease, transform .5s cubic-bezier(.23,1,.32,1), padding-left .25s ease, color .2s ease;
}
.link-item.is-open .link-item_links .nav-link { opacity: 1; transform: translateY(0); }
@media (hover: hover) {
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links .nav-link,
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links .nav-link { opacity: 1; transform: translateY(0); }
}
.link-item.is-open .link-item_links .nav-link:nth-child(1) { transition-delay: .25s; }
@media (hover: hover) {
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links .nav-link:nth-child(1),
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links .nav-link:nth-child(1) { transition-delay: .25s; }
}
.link-item.is-open .link-item_links .nav-link:nth-child(2) { transition-delay: .30s; }
@media (hover: hover) {
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links .nav-link:nth-child(2),
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links .nav-link:nth-child(2) { transition-delay: .30s; }
}
.link-item.is-open .link-item_links .nav-link:nth-child(3) { transition-delay: .35s; }
@media (hover: hover) {
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links .nav-link:nth-child(3),
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links .nav-link:nth-child(3) { transition-delay: .35s; }
}
.link-item.is-open .link-item_links .nav-link:nth-child(4) { transition-delay: .40s; }
@media (hover: hover) {
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links .nav-link:nth-child(4),
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links .nav-link:nth-child(4) { transition-delay: .40s; }
}
.link-item.is-open .link-item_links .nav-link:nth-child(5) { transition-delay: .45s; }
@media (hover: hover) {
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links .nav-link:nth-child(5),
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links .nav-link:nth-child(5) { transition-delay: .45s; }
}
.link-item.is-open .link-item_links .nav-link:nth-child(6) { transition-delay: .50s; }
@media (hover: hover) {
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links .nav-link:nth-child(6),
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links .nav-link:nth-child(6) { transition-delay: .50s; }
}
.link-item.is-open .link-item_links .nav-link:nth-child(7) { transition-delay: .55s; }
@media (hover: hover) {
  .menu-side-bar:not(.has-clicked) .link-item:hover .link-item_links .nav-link:nth-child(7),
  .menu-side-bar:not(.has-clicked) .link-item:focus-within .link-item_links .nav-link:nth-child(7) { transition-delay: .55s; }
}
.link-item_links .nav-link:hover { padding-left: .5em; color: #fff; }
.link-item_links .nav-link strong { font-size: 1.5em; font-weight: 400; line-height: 110%; }
.link-item_links .nav-link span { font-size: .875em; color: var(--blue-200); }
/* "View all" closes the list as a small caps label, not another destination. */
.link-item_links .nav-link.view-all {
  margin-top: .5em; padding-top: .9em; border-top: 1px solid rgba(255,255,255,.2);
  font-size: var(--fs-label); font-weight: var(--fw-label); text-transform: none; letter-spacing: var(--ls-label-tight);
  color: var(--blue-300);
}

/* ---------- Panel utility block ---------- */
.menu-bottom-links-group {
  display: flex; justify-content: space-between; align-items: flex-end;
  width: 100%; flex: none; gap: 1.5em; padding-top: 1.5em;
  border-top: 1px solid rgba(255,255,255,.18);
  /* Last thing in, and on the reference it is a pure fade with no travel:
     up from ~1030ms over ~500ms, and out over ~470ms on close. */
  opacity: 0;
  /* First thing in is the last thing out: the open fades this up at 1.03s,
     so the close drops it immediately, over the same .5s. */
  transition: opacity .5s ease;
}
.menu-side-bar.open .menu-bottom-links-group {
  opacity: 1;
  transition: opacity .5s ease 1.03s;
}
.menu-bottom-links-1, .menu-bottom-links-2 { display: flex; flex-direction: column; gap: .38em; }
.menu-caps {
  font-size: var(--fs-label); font-weight: var(--fw-label); text-transform: none; letter-spacing: var(--ls-label-tight); line-height: var(--lh-tight);
  color: var(--blue-300); text-decoration: none;
}
a.menu-caps.link { transition: color .2s ease; }
a.menu-caps.link:hover { color: #fff; }

/* Locks the page behind the open panel without the scrollbar's disappearance
   shifting the layout. */
body.menu-open {
  overflow: hidden;
  /* overflow:hidden alone does not hold on iOS Safari and leaks wheel
     momentum to the page on desktop, so JS also pins the body with
     position:fixed and restores the offset on close. */
  position: fixed;
  width: 100%;
  overscroll-behavior: none;
}
html.menu-open { overflow: hidden; }
/* Wheel/touch anywhere over the panel must stay in the panel, and the rows
   column must not chain its scroll back to the page once it bottoms out. */
.menu-side-bar { overscroll-behavior: contain; }
.menu-items-wrap {
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
}

/* ---------- Panel responsiveness ----------
   One component at every width, so these are size steps, not layout rewrites. */
@media (max-width: 900px) {
  /* Matches their mobile override: flex-flow column, full width, and the
     divider laid flat under the CLOSE row. */
  .menu-side-bar-inner {
    width: 100vw; padding: 1em var(--gutter) 1.5em;
    flex-direction: column; align-items: stretch; gap: 1.1em;
  }
  .close-btn-wrap { width: 100%; justify-content: flex-end; }
  .light-divider { width: 100%; height: 1px; }
  .menu-items { padding-top: 2em; }
  .link-item_links { padding-left: 2em; }
  .link-item_links .nav-link strong { font-size: 1.25em; }
}
@media (max-width: 600px) {
  /* The label is redundant next to the icon once space is tight, so the bar
     is left as the icon and the centred logo. */
  .menu-btn-wrap .menu-text { display: none; }
  /* Icon + logo + WhatsApp pill share a narrow bar, so the lockup steps down
     rather than squeezing the action off the edge. */
  .logo-img { height: 34px; }
  .icon-36 { width: 1.9em; height: 1.9em; }
  /* Narrower slide so the shifted title still clears the panel edge. */
  .menu-link-heading:hover .menu-link-title,
  .menu-link-heading:focus-visible .menu-link-title,
  .link-item.is-open .menu-link-title { transform: translateX(2.6rem); }
  .link-item_links { padding-left: 1.25em; }
  .link-item_links .nav-link strong { font-size: 1.1em; }
  .menu-items { padding-top: 1.5em; }
  .menu-bottom-links-group { flex-direction: column; align-items: flex-start; gap: 1em; padding-top: 1.5em; }
}
/* Short viewports (landscape phones): the panel scrolls rather than the rows
   compressing into each other. */
@media (max-height: 600px) {
  .menu-items { padding-top: .75em; }
  .menu-link-heading { padding: .3em 0 .35em; }
  .icon-36 { width: 1.75em; height: 1.75em; }
}

@media (prefers-reduced-motion: reduce) {
  /* JS settles on the last scene and adds no further classes; everything
     here just removes the movement and shows that scene's resting state. */
  .js .hero-scene .hl,
  .js .hero-plate,
  .js .hero-copy,
  .js .hero-scene { animation: none !important; }
  .js .hero-plate { transform: none; }
  .js .hero-scene .hl { opacity: 1; }
  .js .hero-leafframe .hlf { animation: none !important; opacity: 1; }
  /* All four corners now settle at the desktop leaf plane's resting 1.10;
     only each plate's own mirror still has to survive. */
  .js .hero-leafframe .hlf-tl { transform: scale(1.10); }
  .js .hero-leafframe .hlf-tr { transform: scaleX(-1) scale(1.10); }
  .js .hero-leafframe .hlf-bl { transform: scaleX(-1) scale(1.10); }
  .js .hero-leafframe .hlf-br { transform: scale(1.10); }
  .js .hero-scene.is-current { visibility: visible; transform: none; }
  .js .hero-copy.is-current { opacity: 1; transform: translateY(-50%); }
  .hero-scrollcue .hs-arrow { animation: none; }
}
/* In the portrait layout the copy is not vertically centred, so the -50%
   above would lift it off its box. */
@media (prefers-reduced-motion: reduce) and (max-aspect-ratio: 1/1) and (max-width: 1024px) {
  .js .hero-copy.is-current { transform: none; }
}

/* Landscape phones and small tablets still crop; nudge the focal point up so
   the juicer and the burst sit inside the frame rather than the floor. */
@media (min-aspect-ratio: 1/1) and (max-width: 900px) {
  .hero-scene .hl { object-position: 50% 42%; }
}


/* ---------- Hero (homepage — layered parallax stage) ----------
   The section is a full-viewport stage holding three layered scenes. Each
   scene is a stack of 1920x1080 PNGs exported on one shared canvas, so
   stacking them at inset:0 with object-fit:cover reproduces the source
   composite exactly. Depth comes from how the layers move, not where they
   sit.                                                                   */
.hero {
  position: relative;
  background: #002032;
  color: #fff;
  overflow: hidden;
  padding: 0 0 18px;
  text-align: left;
  height: 100svh;
  min-height: 620px;
}
.hero .container {
  height: 100%;
  display: flex;
  flex-direction: column;
  padding-top: calc(var(--header-h) * 2 - 24px);
  gap: 0;
}
.hero::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background: url("../images/Home%20page/page%20background.png") no-repeat 50% 58% / cover;
}
.hero::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background:
    linear-gradient(90deg, rgba(0,28,45,.80) 0%, rgba(0,28,45,.30) 20%, transparent 32%,
                           transparent 68%, rgba(0,28,45,.30) 80%, rgba(0,28,45,.80) 100%),
    linear-gradient(180deg, rgba(0,30,48,.42) 0%, transparent 24%, transparent 72%, rgba(0,26,42,.48) 100%);
}
.hero .container { position: relative; z-index: 1; }

/* ===================================================================
   HERO PARALLAX STAGE
   =================================================================== */
/* Matched to the average colour along the scene artwork's top and bottom
   edges (~rgb(85,155,205)), not the pale sky. Anything that ever shows
   between or behind the scenes then reads as part of the picture rather
   than as a light gap. */
.hero-parallax { background: #559bcd; }
/* The photographic layers replace the old single background image; only the
   left-column scrim below is kept, and it is redefined further down. */
.hero-parallax::after { content: none; }

/* Dissolves the hero artwork into the page canvas so the stats strip below
   reads as a continuation of the page rather than a hard horizontal cut.
   The stops are weighted low and late: the picture stays fully itself for
   most of the fade and only turns opaque in the last few percent, which
   keeps the machine and splash art clear of the wash. Sits above the
   scene layers (z-index 0) but below .hero .container (z-index 1). */
.hero-parallax > .hero-fade {
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 16vh;
  min-height: 110px;
  max-height: 200px;
  /* Above the left-column vignette (z-4), so the wash covers the darkened
     corner too and the join reads the same across the full width. Still
     below the dots and scroll cue, which are lifted to z-7. */
  z-index: 6;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(242,247,249,0)    0%,
    rgba(242,247,249,.10) 34%,
    rgba(242,247,249,.30) 56%,
    rgba(242,247,249,.60) 76%,
    rgba(242,247,249,.88) 92%,
    rgba(242,247,249,1)  100%
  );
}

.hero-stagewrap {
  position: absolute; inset: 0; z-index: 0;
  /* Not pointer-events:none - the scenes carry real CTA links. The
     decorative layers opt out individually instead (see .hl). */
  overflow: hidden;
  perspective: 1200px;
  transform-style: preserve-3d;
}
/* Layers are deliberately over-scaled (1.03 - 1.10) to hide their edges, so
   a scene PARKED below the frame would poke back up into view as a strip -
   hence the clip. But the clip must not apply to a scene that is on screen:
   while the outgoing scene scales up during a handover, clipping it to its
   own box cropped its over-scaled artwork and let the section ground show
   as a flat band along the top edge. Only parked scenes are clipped. */
.hero-scene { position: absolute; inset: 0; overflow: hidden; }
/* An ENTERING scene must clip: its plate counter-translates upward, so
   without the clip the artwork would spill above the window instead of
   being revealed by it. A current or leaving scene must NOT clip, or its
   deliberately over-scaled layers get cropped and the section ground shows
   through at the edges. */
.hero-scene.is-current,
.hero-scene.is-leaving { overflow: visible; }
.hero-scene.is-entering { overflow: hidden; }
.hero-scene .hl {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  will-change: transform, opacity;
  backface-visibility: hidden;
  /* Decorative artwork must never swallow clicks aimed at the copy's CTA. */
  pointer-events: none;
}

/* Resting scales per plane. Held by each entrance animation's `both` fill;
   there is no ambient drift loop - every scene comes to a complete stop.

   Every plane rests at 1: the layers are exports of ONE shared 1920x1080
   canvas, so they reproduce the art director's full-view composite only when
   none of them is scaled relative to the others. Verified by rendering the
   hero against each scene's `full view` reference across a scale sweep -
   agreement peaks at 1 on all three scenes and falls off either side. */
.hl-bg      { transform: scale(1); }
.hl-leaf    { transform: scale(1); }
.hl-juicer,
.hl-blender,
.hl-grinder { transform: scale(1); }
.hl-fruitL, .hl-fruitR,
.hl-bowlL,  .hl-bowlR,
.hl-spiceL, .hl-spiceR { transform: scale(1); }

/* The portrait-only leaf frame (see index.html). Desktop keeps the leaf
   plane that ships inside each scene's own canvas, so this stays off here. */
.hero-leafframe { display: none; }
/* The brand-film scene is full-frame footage: no corner foliage over it. */
.hero-parallax.is-video .hero-leafframe { visibility: hidden; }

/* --- Scene stacking and state ---------------------------------------
   The sequence is driven by JS (initHeroScenes in main.js), not by a fixed
   CSS timeline, so the scene indicator can jump straight to any scene on
   click. JS owns exactly one class per scene:

     .is-current   on screen, holding its landed state
     .is-entering  climbing up from below (scenes reached going forwards)
     .is-leaving   easing back while the next scene covers it
     (no class)    parked out of frame, not painted

   Stacking is set inline by JS as well, so whichever scene is arriving is
   always above the one it replaces - a fixed z-index order cannot express
   that when the user can jump backwards. */
.js .hero-scene { visibility: hidden; }
.js .hero-scene.is-current,
.js .hero-scene.is-entering,
.js .hero-scene.is-leaving { visibility: visible; }

/* Deliberately no opacity interpolation on the climb: animating opacity on
   the scene composites its layers as one translucent group, so the scene
   underneath shows through the incoming scene's own opaque sky and it reads
   as a pane of glass sliding up. Pure transform keeps the group opaque, and
   each background PNG (verified 100% alpha) occludes by itself. */
/* The scene slides up, but the ARTWORK INSIDE IT DOES NOT MOVE.
   The scene box is the window; .hero-plate inside it runs the exact
   opposite translate, cancelling the scene's movement so the picture stays
   pinned to the viewport while the window uncovers it from the bottom up.
   The image is revealed rather than carried along - the incoming scene
   reads as already in place behind the outgoing one, and only the edge
   between them travels. Both must share duration and easing exactly, or
   the artwork visibly drifts. */
.hero-plate { position: absolute; inset: 0; }
.js .hero-scene.is-entering {
  animation: heroSceneUp 1200ms cubic-bezier(.22,.7,.24,1) both;
}
.js .hero-scene.is-entering .hero-plate {
  animation: heroPlateHold 1200ms cubic-bezier(.22,.7,.24,1) both;
}
/* The climb distance is a variable rather than a literal 100%: on portrait
   the stage is SHORTER than the section (it carries the artwork's own 16:9),
   so 100% of the scene box stops the reveal partway up the screen. There it
   is overridden with a distance measured against the section instead. */
:root { --scene-travel: 100%; }
@keyframes heroSceneUp {
  from { transform: translateY(var(--scene-travel)); }
  to   { transform: translateY(0); }
}
@keyframes heroPlateHold {
  from { transform: translateY(calc(-1 * var(--scene-travel))); }
  to   { transform: translateY(0); }
}
/* The outgoing scene pushes AWAY from the viewer rather than shrinking on
   screen. Scaling it below 1 shrank it inside its own box and exposed a
   band of the section's pale ground at the top and bottom edges - visible
   as a light gap for the whole 1.2s climb. Scaling UP keeps it covering the
   frame edge to edge while still reading as receding behind the arriving
   scene, and a small downward drift adds the parallax. */
.js .hero-scene.is-leaving {
  animation: heroSceneOut 1200ms cubic-bezier(.4,0,.2,1) both;
}
@keyframes heroSceneOut {
  from { transform: scale(1)     translateY(0); }
  to   { transform: scale(1.045) translateY(1.5%); }
}
.js .hero-scene.is-current { transform: none; }

/* The first scene on load has nothing to climb over, so it just appears and
   its layers play their own entrance. */
.js .hero-scene.is-current.is-first { animation: none; }

/* --- Layer entrances -------------------------------------------------
   Delays are now relative to the scene becoming active, not to page load,
   so a scene plays the same beat whenever it is reached - on the timer or
   from a click on the indicator. The whole set is keyed off the scene's
   state class, so re-adding that class replays the entrance.

   Per-scene beat:
     +0     background settles its scale
     +320   product rises onto the podium
     +560   foreground leaves sweep in
     +860   left cluster bursts from behind the product
     +1000  right cluster follows                                        */
.js .hero-scene .hl { opacity: 0; }
/* An outgoing scene keeps its layers fully painted at their landed state.
   Without this they fall back to the transparent default the moment the
   scene stops being current, and the section ground shows through the strip
   the arriving scene has not climbed over yet - a flat blue band along the
   top edge for the whole handover. */
.js .hero-scene.is-leaving .hl { opacity: 1; animation: none; }

/* Backgrounds. Scene 1 on first load fades up from nothing; any scene that
   arrives by climbing must NOT fade, or the scene below shows through its
   own sky and the rise reads as a translucent pane. */
@keyframes heroBgIn   { from { opacity: 0; transform: scale(1.08); }
                        to   { opacity: 1; transform: scale(1); } }
@keyframes heroBgHold { from { opacity: 1; transform: scale(1.04); }
                        to   { opacity: 1; transform: scale(1); } }
.js .hero-scene.is-current .hl-bg,
.js .hero-scene.is-entering .hl-bg {
  animation: heroBgHold 1100ms cubic-bezier(.22,.61,.36,1) both;
}
.js .hero-scene.is-first .hl-bg {
  animation: heroBgIn 1100ms cubic-bezier(.22,.61,.36,1) both;
}

/* Products. */
@keyframes heroJuicerIn {
  from { opacity: 0; transform: translateY(9%) scale(.90); }
  to   { opacity: 1; transform: translateY(0)  scale(1); }
}
/* The blender stands rather than sits, so it rises a little further. */
@keyframes heroBlenderIn {
  from { opacity: 0; transform: translateY(11%) scale(.88); }
  to   { opacity: 1; transform: translateY(0)   scale(1); }
}
@keyframes heroGrinderIn {
  from { opacity: 0; transform: translateY(9%) scale(.90); }
  to   { opacity: 1; transform: translateY(0)  scale(1); }
}
.js .hero-scene.is-current .hl-blender,
.js .hero-scene.is-entering .hl-blender {
  animation: heroBlenderIn 1000ms cubic-bezier(.2,.8,.25,1) 320ms both;
}
.js .hero-scene.is-current .hl-juicer,
.js .hero-scene.is-entering .hl-juicer {
  animation: heroJuicerIn 1000ms cubic-bezier(.2,.8,.25,1) 320ms both;
}
.js .hero-scene.is-current .hl-grinder,
.js .hero-scene.is-entering .hl-grinder {
  animation: heroGrinderIn 1000ms cubic-bezier(.2,.8,.25,1) 320ms both;
}

/* Foreground leaves. */
@keyframes heroLeafIn { from { opacity: 0; transform: scale(1.09); }
                        to   { opacity: 1; transform: scale(1); } }
.js .hero-scene.is-current .hl-leaf,
.js .hero-scene.is-entering .hl-leaf {
  animation: heroLeafIn 1200ms cubic-bezier(.22,.61,.36,1) 560ms both;
}

/* --- The burst -------------------------------------------------------
   Cued at 320ms, the same moment the machine begins rising, so the splash
   and the product arrive together rather than the splash trailing it. The
   right cluster keeps a small 80ms offset behind the left purely so the two
   sides do not read as one symmetrical pop.

   Every cluster shares duration, easing and start scale. Only the travel
   distance differs, and it is set per cluster from that artwork's
   visual mass: the bowls carry ~82k opaque px, the fruit ~33k and the
   spices ~24k, so identical percentages would move very different amounts
   of material and read as very different energy. Lighter clusters travel
   further to land with the same weight.

   Scene 1 is the exception: its splash is a single centred plate rather
   than a left/right pair, so it has no side to travel from and expands
   from the centre instead (see heroBowlLeftIn). It still takes the 320ms
   cue, so all three scenes start their burst on the same beat. */
.js .hero-scene.is-current .hl-bowlL,
.js .hero-scene.is-entering .hl-bowlL {
  animation: heroBowlLeftIn 1150ms cubic-bezier(.16,.9,.3,1.06) 320ms both;
}
.js .hero-scene.is-current .hl-bowlR,
.js .hero-scene.is-entering .hl-bowlR {
  animation: heroBowlRightIn 1150ms cubic-bezier(.16,.9,.3,1.06) 400ms both;
}
.js .hero-scene.is-current .hl-fruitL,
.js .hero-scene.is-entering .hl-fruitL {
  animation: heroFruitLeftIn 1150ms cubic-bezier(.16,.9,.3,1.06) 320ms both;
}
.js .hero-scene.is-current .hl-fruitR,
.js .hero-scene.is-entering .hl-fruitR {
  animation: heroFruitRightIn 1150ms cubic-bezier(.16,.9,.3,1.06) 400ms both;
}
.js .hero-scene.is-current .hl-spiceL,
.js .hero-scene.is-entering .hl-spiceL {
  animation: heroSpiceLeftIn 1150ms cubic-bezier(.16,.9,.3,1.06) 320ms both;
}
.js .hero-scene.is-current .hl-spiceR,
.js .hero-scene.is-entering .hl-spiceR {
  animation: heroSpiceRightIn 1150ms cubic-bezier(.16,.9,.3,1.06) 400ms both;
}

/* Burst keyframes. Every cluster shares duration, easing, stagger and start
   scale; only travel distance differs, set per cluster from that artwork's
   visual mass (bowls ~82k opaque px, fruit ~33k, spices ~24k). Identical
   percentages would move very different amounts of material and read as
   very different energy, so lighter clusters travel further. */
/* Scene 1's splash is ONE centred plate, not a left/right pair, so it cannot
   burst sideways the way the fruit and spice clusters do - a lateral travel
   on a centred plate reads as the whole splash sliding in from one side,
   which is what made this scene look unlike the other two. It expands from
   the centre instead: no translate, no rotation, so the material opens
   outward symmetrically around the machine. Duration, easing and the 320ms
   cue are the shared ones, and the start scale sits between the paired
   clusters' .34-.39 so the opening covers the same visual distance. */
@keyframes heroBowlLeftIn {
  0%   { opacity: 0; transform: translate(0, 0) scale(.37); }
  38%  { opacity: 1; }
  100% { opacity: 1; transform: translate(0, 0) scale(1); }
}
@keyframes heroBowlRightIn {
  0%   { opacity: 0; transform: translate(-6.3%, 8%) scale(.39) rotate(7deg); }
  38%  { opacity: 1; }
  100% { opacity: 1; transform: translate(0, 0)      scale(1) rotate(0deg); }
}
@keyframes heroFruitLeftIn {
  0%   { opacity: 0; transform: translate(12.5%, 11%) scale(.34) rotate(-11deg); }
  38%  { opacity: 1; }
  100% { opacity: 1; transform: translate(0, 0)       scale(1) rotate(0deg); }
}
@keyframes heroFruitRightIn {
  0%   { opacity: 0; transform: translate(-11.2%, 10%) scale(.34) rotate(10deg); }
  38%  { opacity: 1; }
  100% { opacity: 1; transform: translate(0, 0)        scale(1) rotate(0deg); }
}
/* Spices are the lightest clusters of the three, so they travel furthest. */
@keyframes heroSpiceLeftIn {
  0%   { opacity: 0; transform: translate(12%, 11%) scale(.34) rotate(-11deg); }
  38%  { opacity: 1; }
  100% { opacity: 1; transform: translate(0, 0)     scale(1) rotate(0deg); }
}
@keyframes heroSpiceRightIn {
  0%   { opacity: 0; transform: translate(-11.3%, 10%) scale(.34) rotate(10deg); }
  38%  { opacity: 1; }
  100% { opacity: 1; transform: translate(0, 0)        scale(1) rotate(0deg); }
}

/* ================= PER-SCENE COPY =================
   Each scene carries its own headline and CTA, pinned to the left column so
   it never crosses the centre band where the product sits and the splash
   animates. Type is kept small deliberately - the artwork is the hero. */
/* Soft vignette behind the left column only. The artwork's sky is bright,
   so without this the accent line and body text wash out. It darkens the
   copy column while leaving the centre band untouched. */
/* The vignette is masked off over the bottom band so it stops short of the
   hero's edge. Without this it paints its full .72 dark over the left
   corner right down to the cut, and no amount of wash on top of it can
   reach the page canvas — the join stays hard on the left and right while
   the centre blends. The mask's stops mirror .hero-fade's height. */
.hero-parallax::before {
  content: ""; position: absolute; inset: 0; z-index: 4;
  pointer-events: none;
  background: linear-gradient(90deg,
    rgba(0,28,46,.72) 0%, rgba(0,28,46,.55) 18%,
    rgba(0,28,46,.22) 34%, transparent 48%);
  -webkit-mask-image: linear-gradient(180deg,
    #000 0%, #000 calc(100% - 20vh), transparent calc(100% - 2vh));
  mask-image: linear-gradient(180deg,
    #000 0%, #000 calc(100% - 20vh), transparent calc(100% - 2vh));
}
.hero-copy {
  position: absolute; z-index: 5;
  left: var(--gutter);
  top: 50%; transform: translateY(-50%);
  /* Wide enough for the 600-weight headline to set in two lines ("Powerful
     Grinding," / "Made Easy"). At 430px it wrapped to three and the block
     grew down into the scene tabs (.hero-dots) below it. */
  width: min(38%, 640px);
  text-align: left;
  color: #fff;
  text-shadow: 0 1px 16px rgba(0,32,52,.55), 0 1px 3px rgba(0,32,52,.5);
}
.hc-eyebrow {
  margin: 0 0 .5em;
  font-size: var(--fs-label); font-weight: var(--fw-strong);
  letter-spacing: var(--ls-label-tight); text-transform: none;
  color: rgba(255,255,255,.86);
}
.hc-title {
  margin: 0 0 .5em;
  /* Stepped up from clamp(24px, 2.9vw, 46px): the serif at 400 reads lighter
     and optically smaller than the 700 grotesque this slot was sized for, so
     it needs more scale to carry the hero. */
  font-size: clamp(30px, 4.2vw, 66px);
  /* Heavier than the other display headlines (--fw-display, 400) so the
     copy holds its own over the busy scene artwork. General Sans ships a
     real 600, so this is a true cut, not a synthesised bold. */
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
  color: #fff;
}
/* Brighter than --blue-400, which vanished against the sky. */
.hc-title .accent { color: #7fd4ff; }
.hc-sub {
  margin: 0 0 1.4em;
  font-size: clamp(13px, 1.05vw, 16px); line-height: 1.5;
  font-weight: var(--fw-label);
  color: rgba(255,255,255,.90);
  max-width: 30em;
}
/* Deliberately smaller than the site's standard .btn so it sits inside the
   scene without competing with the product. */
.hc-cta {
  display: inline-flex; align-items: center; gap: .5em;
  padding: .72em 1.5em; border-radius: 999px;
  background: var(--blue-500); color: #fff;
  /* Keeps its fluid size — this pill sits in the absolutely-positioned
     hero overlay, where it has to track the stage rather than the type
     ramp — but takes the shared CTA weight and tracking so it still reads
     as the same family of button as every other one on the site. */
  font-size: clamp(12.5px, .95vw, 15px);
  font-weight: var(--fw-button);
  line-height: var(--lh-button);
  letter-spacing: var(--ls-button);
  text-decoration: none; white-space: nowrap;
  box-shadow: 0 6px 20px rgba(0,40,66,.34);
  transition: background .18s ease, transform .18s ease, box-shadow .18s ease;
}
.hc-cta:hover {
  background: var(--blue-600);
  transform: translateY(-1px);
  box-shadow: 0 9px 26px rgba(0,40,66,.42);
}
.hc-cta .arw { transition: transform .18s ease; }
.hc-cta:hover .arw { transform: translateX(3px); }

/* ================= SCENE INDICATOR + SCROLL CUE ================= */
.hero-dots {
  position: absolute; z-index: 7;
  left: var(--gutter);
  /* Just above the opaque tail of the .hero-fade wash, so the white labels
     stay readable without climbing into the scene copy and its CTA. */
  bottom: clamp(58px, 9vh, 96px);
  display: flex; flex-direction: column; gap: .55em;
  font-size: .8125em;
}
/* The indicator doubles as the scene switcher, so each entry is a real
   button: JS marks the active one and clicking jumps to that scene. */
.hero-dots .hd {
  appearance: none; background: none; border: 0;
  font: inherit; text-align: left;
  display: flex; align-items: center; gap: .7em;
  padding: 3px 2px; cursor: pointer; border-radius: 4px;
  color: rgba(255,255,255,.45);
  transition: color .3s ease;
  /* Doubled up: the labels sit where .hero-fade is part-opaque, so the
     single soft shadow no longer separates white type from the wash. */
  text-shadow: 0 1px 10px rgba(0,32,52,.75), 0 0 3px rgba(0,32,52,.55);
}
.hero-dots .hd i {
  width: 2px; height: 14px; border-radius: 2px;
  background: rgba(255,255,255,.32);
  transition: background .3s ease, height .3s ease;
}
.hero-dots .hd:hover { color: rgba(255,255,255,.85); }
.hero-dots .hd:hover i { background: rgba(255,255,255,.6); }
.hero-dots .hd:focus-visible { outline: 2px solid var(--blue-400); outline-offset: 3px; }
.hero-dots .hd.is-current { color: #fff; }
.hero-dots .hd.is-current i { background: var(--blue-400); height: 20px; }

/* Sits in the bottom-right corner, mirroring the scene indicator opposite
   it on the same gutter and baseline. The homepage has no floating
   WhatsApp button, so nothing crowds this corner. Doubles as the "next
   scene" control. */
.hero-scrollcue {
  position: absolute; z-index: 7;
  right: var(--gutter);
  /* Same clearance as .hero-dots opposite it, keeping the pair on one
     baseline above the fade's opaque tail. */
  bottom: clamp(58px, 9vh, 96px);
  appearance: none; background: none; border: 0; font: inherit;
  padding: 4px 2px; cursor: pointer; border-radius: 4px;
  display: flex; align-items: center; gap: .8em;
  color: rgba(255,255,255,.82); font-size: .8125em;
  text-shadow: 0 1px 10px rgba(0,32,52,.75), 0 0 3px rgba(0,32,52,.55);
  transition: color .2s ease;
}
.hero-scrollcue:hover { color: #fff; }
.hero-scrollcue:hover .hs-arrow {
  border-color: rgba(255,255,255,.85);
  background: rgba(255,255,255,.12);
}
.hero-scrollcue:focus-visible { outline: 2px solid var(--blue-400); outline-offset: 3px; }
/* On the last scene there is nothing to advance to. */
.hero-scrollcue[hidden] { display: none; }
.hero-scrollcue .hs-arrow {
  width: 2.2em; height: 2.2em; border-radius: 50%;
  border: 1px solid rgba(255,255,255,.42);
  display: grid; place-items: center;
  animation: hsBob 2.4s ease-in-out infinite;
  transition: border-color .2s ease, background .2s ease;
}
.hero-scrollcue svg { width: 60%; height: 60%; }
@keyframes hsBob {
  0%,100% { transform: translateY(0); }
  50%     { transform: translateY(4px); }
}

.js .hero-copy { opacity: 0; pointer-events: none; }
/* Copy is state-driven like the scenes: JS marks the active one, so it
   plays the same entrance whether the scene was reached on the timer or by
   clicking the indicator. It rides in while the scene is finishing, so it
   lands with the burst (~1.45s) rather than after everything has stopped. */
.js .hero-copy.is-current {
  animation: heroCopyIn 650ms cubic-bezier(.22,.7,.24,1) 800ms both;
  pointer-events: auto;
}
.js .hero-copy.is-leaving {
  animation: heroCopyOut 500ms cubic-bezier(.4,0,.2,1) both;
}
@keyframes heroCopyIn {
  from { opacity: 0; transform: translateY(calc(-50% + 20px)); }
  to   { opacity: 1; transform: translateY(-50%); }
}
@keyframes heroCopyOut {
  from { opacity: 1; transform: translateY(-50%); }
  to   { opacity: 0; transform: translateY(calc(-50% - 14px)); }
}


.hero-eyebrow {
  flex: none; text-align: center;
  font-size: var(--fs-label); font-weight: var(--fw-label); letter-spacing: var(--ls-label-tight); text-transform: none;
  color: rgba(255,255,255,.72); margin-bottom: 8px;
}
.hero-eyebrow .accent { color: var(--blue-500); }

.hero h1 {
  flex: none;
  color: #fff;
  /* Centred so the wordmark sits directly behind the appliances, which are
     centred in the photograph. Capped against viewport height too. */
  text-align: center;
  font-size: min(10.2vw, 15vh, 165px);
  /* Display-scale wordmark: keeps its viewport sizing, but takes the
     system's 700 weight and display tracking like every other heading. */
  font-weight: 700;
  line-height: .95;
  letter-spacing: -.035em;
  margin: 0 0 -.04em;
}
.hero h1 .accent { color: var(--blue-500); }

/* --- Stage: the open band the background products show through, with the
   three feature callouts pinned down either side. It takes the largest share
   of the section height so the columns always have room to sit apart. --- */
/* Sized by its own content — the taller of the two callout columns — so the
   columns always fit inside it and never spill onto the CTA row below. */
.hero-stage {
  position: relative; flex: 1 1 auto;
  display: flex;
  min-height: 0;
}

/* Right edge of the stage is one flex column: the sub-line sits tight to the
   top, directly beneath the headline, with the callouts spaced below it. */
.hero-right-col {
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; flex-direction: column; align-items: flex-start;
  gap: clamp(26px, 5vh, 64px);
}
/* Never allowed to shrink or be crowded by the callouts beneath it. */
.hero-sub {
  flex: none;
  margin: 0; width: max-content; text-align: left;
  font-size: 1.25em; /* 20px */ line-height: 1.4;
  color: rgba(255,255,255,.95); font-weight: 500;
}

/* Grouped tightly rather than spread, matching the approved composition.
   Both columns are bounded by the stage so they can never overhang the CTA
   row below; the gap collapses first when height is tight. */
.hero-feats {
  display: flex; flex-direction: column; justify-content: flex-start;
  gap: clamp(10px, 2.2vh, 30px); text-align: left; min-height: 0;
}
/* The left column is absolute and sits in the upper part of the stage; the
   right one flows inside .hero-right-col beneath the sub-line. */
.hero-feats-left  { position: absolute; z-index: 2; left: 0; top: 4%; bottom: 0; }
/* Lays out from its top edge so it cannot ride up into the sub-line. */
.hero-feats-right { flex: 1 1 auto; justify-content: flex-start; }
.hero-feats li { display: flex; align-items: center; gap: 11px; }
.hero-feats .ic {
  flex: none; width: clamp(34px, 3.4vw, 44px); height: clamp(34px, 3.4vw, 44px); border-radius: 50%;
  border: 1px solid rgba(255,255,255,.30);
  display: grid; place-items: center; color: #fff;
}
.hero-feats .ic svg { width: 46%; height: 46%; }
.hero-feats div { display: flex; flex-direction: column; line-height: 1.25; }
.hero-feats strong { color: var(--blue-500); font-size: 1em; /* 16px */ font-weight: 500; }
.hero-feats span { color: rgba(255,255,255,.82); font-size: var(--fs-label); max-width: 10em; }

/* --- CTAs: centred in the clear ground beneath the machines --- */
.hero .container > .hero-cta {
  position: relative; z-index: 3; flex: none;
  display: flex; width: 100%; justify-content: center;
  margin-top: clamp(10px, 1.6vh, 20px);
}
.hero .hero-cta .arw { transition: transform .18s ease; }
.hero .hero-cta .btn:hover .arw { transform: translateX(3px); }
/* Same glass treatment as .btn-outline on a dark ground — kept as its own
   name because the hero markup uses it, but the values no longer diverge. */
.btn-hero-ghost {
  background: rgba(255,255,255,.07); color: #fff; border-color: rgba(255,255,255,.22);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
}
.btn-hero-ghost:hover { background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.34); }

/* --- Category strip --- */
/* --- CTA row shared by the hero and inner-page bands --- */
.hero-cta { display: flex; flex-wrap: wrap; gap: 12px; }

/* --- Mobile sticky action bar: hidden until the narrow breakpoint --- */
/* Height of the sticky bar, for anything that must sit clear of it. Zero
   until the breakpoint that actually shows the bar. */
:root { --mobile-bar-h: 0px; }
.mobile-bar { display: none; position: fixed; left: 0; right: 0; bottom: 0; z-index: 55; }
.mobile-bar .row { display: grid; grid-template-columns: 1fr 1fr; }
.mobile-bar a {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 14px 8px; font-weight: 500; font-size: .9375em; color: #fff;
}
.mobile-bar svg { width: 20px; height: 20px; }
.call-btn { background: var(--blue-700); }
.wa-btn   { background: var(--whatsapp); }

.hero-cats {
  flex: none;
  /* Narrower than the container and centred, so it reads as a contained
     strip rather than spanning the full width. */
  width: min(100%, 1040px); margin-inline: auto;
  margin-top: clamp(12px, 1.8vh, 24px); padding: clamp(6px, 1vh, 12px) 8px;
  display: grid; grid-template-columns: repeat(4, 1fr);
  border: 1px solid rgba(255,255,255,.14); border-radius: var(--radius-lg);
  background: rgba(255,255,255,.045);
}
.hero-cats > a {
  display: flex; align-items: center; gap: 10px; text-align: left;
  padding: 5px 14px; border-right: 1px solid rgba(255,255,255,.12);
}
.hero-cats > a:last-child { border-right: 0; }
.hero-cats .thumb { flex: none; width: 56px; height: 56px; display: grid; place-items: center; overflow: hidden; }
.hero-cats .thumb img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }
.hero-cats .thumb-tile {
  border-radius: var(--radius-sm); background: rgba(19,167,236,.16);
  border: 1px solid rgba(19,167,236,.34); color: var(--blue-400);
}
.hero-cats .thumb-tile svg { width: 26px; height: 26px; }
.hero-cats .txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.hero-cats strong { color: var(--blue-500); font-size: var(--fs-label); font-weight: var(--fw-label); letter-spacing: var(--ls-body); }
.hero-cats .txt > span { color: rgba(255,255,255,.80); font-size: var(--fs-caption); line-height: var(--lh-tight); }
.hero-cats .go { color: rgba(255,255,255,.62); font-size: var(--fs-caption); font-weight: var(--fw-body); margin-top: 3px; transition: color .16s ease; }
.hero-cats > a:hover .go { color: var(--blue-400); }

/* --- Closing stat row --- */
/* --- Stats strip: its own band directly below the hero --- */
/* Runs on the page canvas with no band of its own, so the figures read as
   part of the page rather than a separate bar. The colour lift on scroll-in
   is what carries the section instead: each stat rises, and its number
   settles from the light brand blue into full ink. */
.stats-strip {
  background: transparent;
  border-top: 0;
  /* Sits in the gap between the hero and Shop By Need. The hero adds no
     padding below itself, but .need-section already contributes
     --section-pad-y above, so the band takes that same value on top and
     nothing on the bottom — that makes the space above and below the
     figures equal on screen, instead of 42px over ~120px. */
  padding-top: var(--section-pad-y);
  padding-bottom: 0;
  color: var(--ink-900);
}
/* A flat trust row, not five cards: the items spread evenly on the page
   canvas and are separated by a hairline dot, so the band stays quiet
   between two heavy sections. */
.stats-strip .stats-eyebrow {
  text-align: center;
  color: var(--ink-700);
  font-size: calc(var(--fs-stat-label) * 1.15);
  font-weight: var(--fw-display);
  letter-spacing: .14em;
  text-transform: uppercase;
  margin: 0 0 22px;
}
.hero-stats {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px 0;
}
.hero-stats > div {
  position: relative;
  /* Sized to content, not equal fifths: the long figures ("35,000+ sq.ft.",
     "ISO 9001:2015") would otherwise be squeezed into wrapping. */
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 11px;
  text-align: left;
  padding: 0 4px;
  background: none;
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
/* The separator between stats — a single centred dot on the flex gap. */
.hero-stats > div + div::before {
  content: "";
  position: absolute;
  left: -2px;
  top: 50%;
  width: 4px;
  height: 4px;
  margin-top: -2px;
  border-radius: 50%;
  background: var(--ink-500);
  opacity: .32;
}
.hero-stats .ic { flex: none; color: var(--blue-500); }
.hero-stats .ic svg { width: 32px; height: 32px; }
.hero-stats div div { display: flex; flex-direction: column; line-height: 1.25; }
/* The figure has to out-rank its label by size AND weight, or the strip
   reads as five short sentences. It was 17px/600 over a 12.5px label — a
   gap too small to register as a statistic.

   This strip is a five-across icon+text row, not a stacked stat grid, so
   it takes a contained step — 22px/700 over a 14px/500 label. A 40px
   figure would break the five columns, and two of the five entries are
   words ("ISO 9001:2015", "Pan India") rather than short numerals, so the
   size has to hold those without wrapping too — hence the em-based figure,
   which the strip's own font-size keeps in range on narrow desktops. */
/* The figure is on Marcellus, the page face — font consistency across the
   site is the requirement here.

   Marcellus ships ONE weight (400), so the 700 below is SYNTHESISED by the
   browser. That is a deliberate, accepted choice (the same trade-off the
   whole site already makes — see the --font-sans note near the top), not an
   oversight: it is the only way to add weight without leaving the family.
   Size, black ink and tight tracking carry most of the emphasis; the
   synthesised bold adds the rest. */
.hero-stats strong {
  font-family: var(--font-display);
  color: var(--blue-700);
  /* Fluid, so all five figures stay on one row, each on one line, from the
     900px breakpoint up; 34px cap on wide screens. */
  font-size: clamp(24px, 2.45vw, 34px);
  font-weight: 700;
  white-space: nowrap;
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-title);
  font-variant-numeric: tabular-nums;
}
/* The label sits on the same page serif, black. It renders in its authored
   sentence case ("Certified Company"), NOT all-caps — the separation from
   the figure comes from size, colour and the figure's weight instead.
   Slight tracking is kept so the small serif does not set too tight. */
.hero-stats span {
  font-family: var(--font-display);
  color: var(--ink-900);
  font-size: .875rem;
  font-weight: 600;              /* synthesised on Marcellus */
  line-height: var(--lh-tight);
  letter-spacing: .01em;
}

/* Reveal: the stat lands in light brand blue and DEEPENS to --blue-700 a
   beat later. (It used to resolve to ink; the figure is blue at rest now, so
   the travel is light-blue to deep-blue — a quieter settle than before, but
   it keeps the figure moving as it arrives.) The colour transition is
   deliberately slower than the lift and shares the same stagger delay that
   main.js writes onto [data-reveal-child]. Gated on html.js like every other
   reveal, so with JS off the final colours are what render. */
.js .stats-strip[data-reveal] [data-reveal-child] {
  transition: opacity .7s cubic-bezier(.22, .61, .36, 1),
              transform .7s cubic-bezier(.22, .61, .36, 1);
}
.js .stats-strip[data-reveal] [data-reveal-child] strong,
.js .stats-strip[data-reveal] [data-reveal-child] .ic {
  color: var(--blue-500);
  transition: color .9s ease .25s;
}
.js .stats-strip[data-reveal] [data-reveal-child] span { transition: color .9s ease .25s; }
.js .stats-strip[data-reveal].is-revealed [data-reveal-child] strong { color: var(--blue-700); }
.js .stats-strip[data-reveal].is-revealed [data-reveal-child] .ic { color: var(--blue-600); }

/* page hero (inner pages) */
.page-hero { background: var(--gradient-deep); color: #fff; padding: 58px 0 68px; text-align: center; }
/* When it is the page's opening section the transparent bar sits over it
   (data-nav="over-dark"), so it carries the header's clearance itself. */
body[data-nav="over-dark"] main > .page-hero:first-child { padding-top: calc(var(--header-h) + 58px); }
.page-hero h1 { color: #fff; font-size: var(--fs-h1); line-height: var(--lh-title); letter-spacing: var(--ls-title); }

/* ---------- Hero headlines: the display serif ----------
   Every page's main hero heading, whichever hero it uses — the centred
   .page-hero (23 pages) and the full-bleed .cat-hero banner (2). Set in one
   place so the two hero types cannot drift apart.

   font-weight is forced to 400 because Marcellus ships no other weight: both
   heroes otherwise inherit a heavier --fw-title / --fw-strong, which the
   browser would synthesise into a smeared faux-bold. */
.page-hero h1,
.cat-hero h1 {
  font-family: var(--font-display);
  font-style: normal;
  font-weight: var(--fw-display);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
}
.page-hero p { color: rgba(255,255,255,.9); max-width: 640px; margin: 0 auto 22px; font-size: var(--fs-lead); line-height: var(--lh-body); }
.page-hero .hero-cta { justify-content: center; }

/* ---------- Trust bar ---------- */
.trust-bar { background: var(--gradient-deep); color: #fff; padding: 16px 0; }
.trust-bar .container { display: flex; flex-wrap: wrap; gap: 14px 34px; align-items: center; justify-content: center; font-size: var(--fs-label); font-weight: var(--fw-body); letter-spacing: var(--ls-body); }
.trust-bar .dot { color: var(--blue-300); }

/* ---------- Cards / grids ---------- */
.grid { display: grid; gap: 26px; }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }

.need-card {
  background: var(--white); border: 1px solid var(--blue-100); border-radius: var(--radius-lg);
  padding: 30px 26px; box-shadow: var(--shadow-sm); transition: transform .18s ease, box-shadow .18s ease;
}
.need-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-md); }
.need-card .icon { width: 54px; height: 54px; border-radius: 16px; background: var(--blue-100); display: flex; align-items: center; justify-content: center; margin-bottom: 18px; color: var(--blue-700); }
.need-card .icon svg { width: 26px; height: 26px; }
.need-card h3 { font-size: var(--fs-card-title); line-height: var(--lh-card); }
.need-card a.link { font-weight: var(--fw-button); font-size: var(--fs-button); line-height: var(--lh-button); letter-spacing: var(--ls-button); color: var(--blue-600); display: inline-flex; align-items: center; gap: 6px; margin-top: 8px; }
/* ---------- "How to Choose" image cards ----------
   A need-card with a .need-media image behind the copy. The image shows at full
   strength up top and sinks into a bottom-weighted dark scrim, which is what
   carries the white copy. Text keeps the card ramp: --fs-card-title /
   --fs-card-desc. */
.need-card.need-visual {
  position: relative; overflow: hidden; isolation: isolate;
  min-height: 300px; padding: 28px 26px;
  display: flex; flex-direction: column; justify-content: flex-end;
  /* The image runs edge to edge, so the stock pale card chrome would read as
     a seam around it. */
  background: var(--ink-900); border: 0;
}
.need-card.need-visual .need-media {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
}
.need-card.need-visual .need-media img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 1;
  transform: scale(1.02); transition: transform .5s ease;
}
/* Paper wash: keeps the copy readable over the image at any crop. */
.need-card.need-visual .need-media::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(10,26,43,0) 0%,
    rgba(10,26,43,.06) 32%,
    rgba(10,26,43,.45) 60%,
    rgba(10,26,43,.74) 82%,
    rgba(10,26,43,.86) 100%);
}
.need-card.need-visual:hover .need-media img { transform: scale(1.06); }
.need-card.need-visual .need-body { position: relative; z-index: 1; }
.need-card.need-visual .need-step {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%;
  background: rgba(255,255,255,.16); color: #fff;
  border: 1px solid rgba(255,255,255,.34);
  backdrop-filter: blur(2px);
  font-weight: var(--fw-label); font-size: var(--fs-label);
  letter-spacing: var(--ls-label-tight); margin-bottom: 14px;
}
.need-card.need-visual h3 {
  font-size: var(--fs-card-title); line-height: var(--lh-card);
  letter-spacing: -.01em; color: #fff; margin: 0 0 .4em;
}
.need-card.need-visual p {
  font-size: var(--fs-card-desc); line-height: var(--lh-body);
  color: rgba(255,255,255,.88); margin: 0;
}
@media (max-width: 720px) {
  .need-card.need-visual { min-height: 250px; }
}


/* ---------- Scroll reveal ----------
   Elements marked [data-reveal] are hidden, then main.js adds .is-revealed
   the first time they scroll into view. Children marked [data-reveal-child]
   get a stagger delay set inline by the script, so a container animates as
   a sequence rather than all at once.
   The hidden state is gated on html.js — main.js sets that class the moment
   it runs, so with JS off or broken nothing is ever hidden and the section
   renders as plain content. Content must not depend on the animation. */
.js [data-reveal] [data-reveal-child] {
  opacity: 0; transform: translateY(28px);
  transition: opacity .7s cubic-bezier(.22, .61, .36, 1),
              transform .7s cubic-bezier(.22, .61, .36, 1);
}
.js [data-reveal].is-revealed [data-reveal-child] { opacity: 1; transform: none; }

/* The photo un-zooms as the tile rises, so each card assembles itself
   instead of simply fading in. This scales the tile's own overflow box, not
   the media element, because .need-tile:hover img/video owns its transform for the
   hover zoom — driving both from one property would have the (much more
   specific) reveal rule permanently override hover. */
.js .need-strip[data-reveal] .need-tile {
  transition: transform 1.1s cubic-bezier(.22, .61, .36, 1),
              opacity .7s cubic-bezier(.22, .61, .36, 1);
}

/* Two Ranges: each card GROWS out of its own top-left corner — the card box
   starts small and its edges sweep outward into the full frame, while the
   photograph inside is counter-scaled so it holds its final size the whole
   way. That combination is what reads as the frame opening up to uncover the
   picture, rather than a small card being magnified. The right card starts
   tighter and offset toward the left one, so the pair spreads out of the
   section's top-left as a single movement.
   These override the generic [data-reveal-child] translateY above (higher
   specificity), and the script's inline transition-delay supplies the ~110ms
   stagger that gives the left card its lead. Transform and opacity only —
   never width/height — so the reveal stays on the compositor. */
.js .split-grid[data-reveal] .split-card {
  transform-origin: top left;
  opacity: 0;
  transform: scale(.42, .55);
  transition: opacity .6s cubic-bezier(.16, 1, .3, 1),
              transform 1.35s cubic-bezier(.16, 1, .3, 1);
  will-change: transform, opacity;
}
/* The photograph and the copy are counter-scaled against the card's own
   scale, anchored to the same top-left corner, so the picture stays at
   (near) its final size while the card's edge sweeps outward across it:
   the frame opens up to uncover the image instead of a shrunken picture
   being blown up. Keeping the photo still is what makes this read as
   unfolding rather than zooming.
   Note both transitions must share one curve and duration — an eased
   inverse of an eased value is not constant, so any mismatch shows up as
   the photo breathing mid-flight. Same curve, same 1.05s, both here and on
   the card above. */
.js .split-grid[data-reveal] .split-card img,
.js .split-grid[data-reveal] .split-card video,
.js .split-grid[data-reveal] .split-card .content {
  transform-origin: top left;
  transform: scale(2.381, 1.8182);  /* 1/.42, 1/.55 */
  transition: transform 1.35s cubic-bezier(.16, 1, .3, 1);
  will-change: transform;
}
/* The copy fades in over the back half of the open, so it never appears
   stretched while the frame is still travelling. */
.js .split-grid[data-reveal] .split-card .content {
  opacity: 0;
  transition: transform 1.35s cubic-bezier(.16, 1, .3, 1),
              opacity .5s ease .55s;
}
/* The right card opens from the same corner but starts a touch tighter and
   offset toward the left card, so the pair reads as one movement spreading
   out of the top-left of the section. */
.js .split-grid[data-reveal] .split-card:nth-child(2) {
  transform: scale(.38, .5) translate3d(-9%, -6%, 0);
}
.js .split-grid[data-reveal] .split-card:nth-child(2) img,
.js .split-grid[data-reveal] .split-card:nth-child(2) video,
.js .split-grid[data-reveal] .split-card:nth-child(2) .content {
  transform: scale(2.6316, 2);  /* 1/.38, 1/.5 */
}
/* Settled: static, and the compositor hints are dropped so no layer is kept
   alive after the entrance. No looping motion is left behind. */
.js .split-grid[data-reveal].is-revealed .split-card,
.js .split-grid[data-reveal].is-revealed .split-card img,
.js .split-grid[data-reveal].is-revealed .split-card video,
.js .split-grid[data-reveal].is-revealed .split-card .content,
.js .split-grid[data-reveal].is-revealed .split-card:nth-child(2),
.js .split-grid[data-reveal].is-revealed .split-card:nth-child(2) img,
.js .split-grid[data-reveal].is-revealed .split-card:nth-child(2) video,
.js .split-grid[data-reveal].is-revealed .split-card:nth-child(2) .content {
  opacity: 1;
  transform: none;
  will-change: auto;
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] [data-reveal-child] {
    opacity: 1 !important; transform: none !important; transition: none !important;
  }
  .js .split-grid[data-reveal] .split-card,
  .js .split-grid[data-reveal] .split-card:nth-child(2),
  .js .split-grid[data-reveal] .split-card img,
  .js .split-grid[data-reveal] .split-card video,
  .js .split-grid[data-reveal] .split-card .content,
  .js .split-grid[data-reveal] .split-card:nth-child(2) img,
  .js .split-grid[data-reveal] .split-card:nth-child(2) video,
  .js .split-grid[data-reveal] .split-card:nth-child(2) .content {
    opacity: 1 !important; transform: none !important; transition: none !important;
  }
  .js .stats-strip[data-reveal] [data-reveal-child] strong { color: var(--blue-700) !important; }
  .js .stats-strip[data-reveal] [data-reveal-child] .ic { color: var(--blue-600) !important; }
  .js .stats-strip[data-reveal] [data-reveal-child] strong,
  .js .stats-strip[data-reveal] [data-reveal-child] .ic,
  .js .stats-strip[data-reveal] [data-reveal-child] span { transition: none !important; }
}

/* ---------- Shop By Need — full-bleed image tiles ----------
   Four tall photographs run edge-to-edge with the copy sitting on the
   image itself. The shots are already lit dark at the foot, so the text
   reads on them; a brand-blue scrim is layered under the copy anyway so
   contrast holds even if a photo is later swapped for a lighter one. */
/* Doubled class for the same reason .section.stage is doubled — the
   responsive block at the foot of this file re-declares .section
   padding-block at 600px and below, at equal specificity and later in
   source order. As a shorthand it resets padding-bottom too, so a single
   .need-section here lost on phones and left a ~46px band of page
   background between the last tile and the Featured poster. The strip is
   meant to meet the poster edge to edge at every width. */
.section.need-section { padding-bottom: 0; }
.need-strip {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 0;
  background: var(--blue-900);
}
.need-tile {
  position: relative; display: block; overflow: hidden;
  aspect-ratio: 3 / 4; color: var(--white); text-decoration: none;
}
.need-tile img,
.need-tile video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: center bottom;
  transition: transform .5s ease;
}
/* All four tiles are 16:9 clips dropped into a 3/4 frame, so `cover` crops
   them hard on the sides and the vertical framing is all that's left to
   choose. `center bottom` seats the machine on the foot of the card; the
   food-processing shot sits higher in its frame and needs centring instead
   (.need-tile--center), or its subject runs off the bottom edge. */
.need-tile--center video { object-position: center center; }
/* The roti clip's mixer sits right of frame centre, so the crop is slid
   right to bring the machine and its bowl back to the middle of the card. */
.need-tile--right video { object-position: 65% bottom; }
/* The home clip's flour mill is only just right of centre, so a lighter nudge. */
.need-tile--right-slight video { object-position: 58% bottom; }

.need-tile::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top, rgba(5, 61, 87, .88) 0%, rgba(5, 61, 87, .45) 32%, rgba(5, 61, 87, 0) 58%);
}
.need-tile-body { position: absolute; left: 0; right: 0; bottom: 0; z-index: 1; padding: 34px 30px 30px; }
.need-tile-body h3 {
  margin: 0 0 10px; color: var(--white);
  font-size: 1.75em; line-height: 115%; letter-spacing: -.01em;
  /* A card title, not a label — the HTML writes "My Home" / "My Business"
     and the caps transform was making a heading read as a badge. */
  text-transform: none; font-weight: 700;
}
.need-tile-body h3 em { font-style: normal; color: var(--blue-400); }
.need-tile-body p {
  margin: 0 0 16px; color: rgba(255, 255, 255, .86);
  font-size: .875em; line-height: 1.6; max-width: 22ch;
}
.need-tile-link {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--fs-button); font-weight: var(--fw-button);
  line-height: var(--lh-button);
  letter-spacing: var(--ls-button); text-transform: none; color: var(--blue-400);
}
@media (hover: hover) {
  .need-tile:hover img,
  .need-tile:hover video { transform: scale(1.05); }
  .need-tile:hover .need-tile-link { color: var(--white); }
}
.need-tile-arrow {
  display: inline-grid; place-items: center;
  width: 34px; height: 34px; border-radius: 50%;
  border: 1.5px solid currentColor;
  transition: background-color .35s ease, border-color .35s ease, color .35s ease, transform .35s ease;
}
.need-tile-arrow svg { width: 16px; height: 16px; }
.need-tile:hover .need-tile-arrow,
.need-tile:focus-visible .need-tile-arrow {
  background: var(--white); border-color: var(--white); color: var(--blue-700);
  transform: translateX(4px);
}
.need-tile:focus-visible { outline: 3px solid var(--blue-300); outline-offset: -3px; }

@media (max-width: 1080px) {
  .need-strip { grid-template-columns: repeat(2, 1fr); }
  .need-tile { aspect-ratio: 4 / 5; }
}
@media (max-width: 600px) {
  .need-strip { grid-template-columns: 1fr; }
  .need-tile { aspect-ratio: 4 / 3; }
  .need-tile-body { padding: 26px 22px 24px; }
  .need-tile-body h3 { font-size: 1.5em; }
  .need-tile-body p { max-width: none; }
}

.product-card {
  background: var(--white); border: 1px solid var(--blue-100); border-radius: var(--radius-lg);
  overflow: hidden; box-shadow: var(--shadow-sm); transition: transform .18s ease, box-shadow .18s ease; display: flex; flex-direction: column;
}
.product-card:hover { transform: translateY(-6px); box-shadow: var(--shadow-md); }
.product-card .thumb { aspect-ratio: 4/3.1; overflow: hidden; background: var(--blue-50); }
.product-card .thumb img { width: 100%; height: 100%; object-fit: cover; transition: transform .35s ease; }
.product-card:hover .thumb img { transform: scale(1.06); }
.product-card .body { padding: 20px 20px 22px; display: flex; flex-direction: column; flex: 1; }
.product-card .cat-tag { font-size: var(--fs-tag); font-weight: var(--fw-label); text-transform: none; letter-spacing: var(--ls-label-tight); color: var(--blue-600); margin-bottom: 6px; }
.product-card h3 { font-size: var(--fs-card-title); line-height: var(--lh-card); margin-bottom: 6px; }
.product-card p.desc { font-size: var(--fs-card-desc); line-height: var(--lh-body); color: var(--ink-500); margin-bottom: 14px; flex: 1; }
.spec-chip-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.spec-chip { font-size: var(--fs-tag); font-weight: var(--fw-label); background: var(--blue-100); color: var(--blue-700); padding: 5px 10px; border-radius: var(--radius-pill); }
.product-card .card-actions { display: flex; gap: 8px; margin-top: auto; }
/* Card buttons share the .btn-sm geometry so every pill on the site is one
   of exactly two heights. Only the flex behaviour differs.

   DIMENSIONS are context-aware, TYPOGRAPHY is shared: two CTAs sit side by
   side inside a card column ("View Product" + "Get Price"), so they take
   the compact padding and the one-step-down size. Family, weight, leading,
   tracking and case all still come from the .btn base — so they read as
   the same component as the hero CTA, just built for a tighter slot. */
.product-card .card-actions .btn { flex: 1; padding: .625rem 1rem; font-size: var(--fs-button-sm); }

/* feature list */
.feature-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px 24px; }
.feature-list li { display: flex; gap: 10px; align-items: flex-start; font-weight: var(--fw-label); font-size: var(--fs-card-desc); color: var(--ink-900); }
.feature-list li .chk { flex: none; width: 20px; height: 20px; border-radius: 50%; background: var(--blue-600); color: #fff; display: flex; align-items: center; justify-content: center; margin-top: 1px; }
.feature-list li .chk svg { width: 11px; height: 11px; }

/* ---------- Why Jagruti — cinematic photographic section ----------
   A full-height editorial composition, not a centred feature grid. The
   source frame is ~2:1: clean blue wall across the left, the mill sitting
   right of centre. The layout follows that split — the heading and a full
   4x2 card grid occupy the left ~64%, and the right ~34% is left to the
   machine.

   The photograph is a real <img> inside .why-bg rather than a CSS
   background, so it can be object-fit cropped per breakpoint AND
   transformed for parallax without the section's own layout being
   involved. .why-bg and .why-product are two separate layers over the same
   source: the second is cropped to the machine alone and travels at its own
   rate, which is what gives the composition its depth. */
/* Written as .section.why-section, not .why-section, on purpose. The shared
   responsive blocks at the foot of this file carry their own
   `.section { padding: … }` overrides at 600px and below; those are later in
   source order and at equal specificity, so a bare .why-section rule loses to
   them and the section silently falls back to the generic 48px padding. The
   doubled class outranks them without reaching for !important. Every padding
   rule for this section below is written the same way. */
/* The runway. The sticky child inside is one viewport tall, so the hold lasts
   for this height MINUS that viewport: 200vh gives ~one screen of hold, which
   is the same ratio .range-panel uses. data-why lives HERE, not on the pin —
   initWhyReveal derives its parallax progress from this element's rect, and a
   pinned element's rect stops moving while it is held. Keeping the hook on the
   runway means the observer and the parallax see exactly the travel they saw
   before, so the entrance and the two photographic layers are unchanged. */
.why-runway {
  /* The runway no longer adds scroll distance. It previously ran 175vh so the
     sticky section below could be held for ~75vh of hold; the section is not
     pinned any more, so that surplus height would just be empty page. It stays
     as an element because [data-why] hangs off it and the reveal observer
     measures its rect. */
  height: auto;
  position: relative;
}

.section.why-section {
  /* Scrolls with the page. This was position:sticky/top:0 against a 175vh
     runway, which pinned the composition for about three-quarters of a
     viewport; it now moves with everything else. The one-viewport frame below
     is kept, so the section still reads as a single authored screen — it just
     passes through rather than being held. */
  position: relative;
  overflow: hidden;              /* contains the layer scale overshoot */
  isolation: isolate;            /* keeps the card shadows off the backdrop */
  background: var(--blue-50);    /* what shows for the moment before decode */
  /* Exactly one viewport, pinned at all three so neither content nor the
     global .section padding can grow it. Everything inside is sized to fit
     this box rather than the box growing to fit the content.

     100svh is the unit the hero uses (.hero { height: 100svh }), so the two
     sections measure identically — svh is the stable viewport height, so a
     mobile browser's collapsing toolbar cannot make this box disagree with
     the hero's. */
  height: 100svh;
  /* The hero does not shrink below 620px (.hero { min-height: 620px }), so
     neither can this section: on a short desktop window a plain 100svh box
     would keep shrinking past the hero's floor and the two would stop
     measuring the same. The floor has to win over the 100svh min, hence
     max() rather than a second min-height declaration. */
  min-height: max(100svh, 620px);
  max-height: max(100svh, 620px);
  /* Top padding clears the fixed header. Everything below is distributed by
     the flex column in .why-inner rather than by fixed heights — the
     viewport sets the section height and the grid absorbs whatever is left,
     which is what keeps row two inside the frame at every viewport.

     The top band carries 14px of surplus over the fixed bar it exists to
     clear: --header-h is 76px, the bar measures 62px. On a tall window that
     surplus is just breathing room and the clamp keeps all 76px. On a short
     one the composition needs it — head + grid floor + padding wants 734px
     inside a 700px frame — so the clamp hands it back as the viewport
     shortens, down to a 62px floor that still clears the bar exactly. This
     is the one band that can give without touching a card. */
  padding: clamp(62px, 9.4vh, var(--header-h)) 0 18px;
  margin: 0;
  display: flex;
  align-items: stretch;
}
/* Both photographic layers are stretched past the section edges so the
   parallax always has overhang to travel into — no bare edge is ever pulled
   into frame, and the crop on the mill never changes. */
.why-bg {
  position: absolute;
  inset: -7% 0;
  z-index: 0;
  pointer-events: none;
  will-change: transform;
}
.why-bg img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* Anchored left-of-centre: this layer supplies the blue wall and light,
     while the machine itself is carried by .why-product above. */
  object-position: 34% 50%;
  display: block;
}
/* The two layers are crops of the same photograph at different scales, so
   where .why-bg ends and .why-product begins the wall tone steps — a hard
   vertical seam straight down the composition. Feathering the product layer's
   left edge dissolves that join: the machine keeps its full density and only
   the surplus wall either side of it fades out into the layer below. */
.why-product {
  /* Feathered on THREE edges, not just the left. The left gradient dissolves
     the crop seam against .why-bg as before; the vertical one now also fades
     the top and bottom, which is what stopped the layer reading as a picture
     laid ON the section — a hard horizontal edge top and bottom is exactly
     what makes a photographic layer look like a floating card. The two masks
     are composited with mask-composite:intersect so a pixel survives only
     where BOTH gradients keep it, leaving the machine itself at full density
     in the middle and every boundary dissolving into the wall behind it. */
  -webkit-mask-image:
    linear-gradient(90deg,
      transparent 0%, rgba(0,0,0,.35) 12%, #000 32%, #000 100%),
    linear-gradient(180deg,
      transparent 0%, #000 10%, #000 93%, transparent 100%);
  mask-image:
    linear-gradient(90deg,
      transparent 0%, rgba(0,0,0,.35) 12%, #000 32%, #000 100%),
    linear-gradient(180deg,
      transparent 0%, #000 10%, #000 93%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
/* The machine, as its own layer on the right third. Sized in vw so it holds
   its share of the composition at any width, and scaled slightly past its
   box so the parallax drift never exposes the crop seam. */
.why-product {
  position: absolute;
  top: -5%; bottom: -5%;
  right: 0;
  width: 42%;
  z-index: 1;
  pointer-events: none;
  will-change: transform;
}
.why-product img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* Frames the mill itself out of the wide source — the machine sits at
     roughly 78% across the original, so the crop centres there. */
  object-position: 82% 52%;
  display: block;
}
/* Readability wash, left-weighted only. It lifts the copy off the
   photograph without flattening it; the machine side stays clear, which is
   the whole point of the shot. Deliberately stops short of the right edge. */
.why-section::before {
  content: "";
  position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background:
    linear-gradient(100deg,
      rgba(255,255,255,.88) 0%,
      rgba(255,255,255,.70) 30%,
      rgba(233,242,247,.30) 54%,
      rgba(233,242,247,0) 70%);
}
/* Fills the section so the grid has a definite height to take its share of.
   min-height:0 is what lets the grid actually shrink inside this column —
   without it a flex child refuses to go below its content size and the last
   row is pushed past the section edge again. */
.why-inner {
  position: relative;
  z-index: 3;
  width: 100%;
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* The head sits upper-left over the clean band, not centred across the
   machine. Capped so it never runs the full width of the section. */
.why-section .section-head {
  max-width: 680px;
  /* Tight: on a 900px-tall viewport every pixel here comes out of the card
     grid, which is what clips the longest card's last line. Pulled in from
     clamp(20px, 2.6vh, 34px) so the heading sits higher over the clean band
     and the reclaimed pixels go to the card track below, which needed them
     to fit its longest copy. */
  margin: 0 0 clamp(12px, 1.5vh, 20px);
  text-align: left;
  /* Never compresses: it keeps its natural height and the grid below takes
     the remainder. Without this the head would shrink under flex pressure
     and the heading would crush before the cards gave up any space. */
  flex: none;
}
.why-section .section-head .eyebrow { margin-bottom: 16px; }
.why-section .section-head h2 {
  color: var(--blue-800);
  /* The section has to hold a heading, eight cards and the machine inside
     one viewport, so the heading stays contained — which the shared step
     already does, more tightly than the old ceiling did.

     Was clamp(1.9rem, 2.6vw, 3.625rem) — a 58px ceiling against the 40px
     every other section heading uses, with its own 1.08 leading and
     -.025em tracking. That made this the one section heading on the site
     that looked custom-built. It now takes the shared step; the section
     still fits because --fs-title is well under the old ceiling. */
  margin-bottom: 0;
  /* A shade heavier and tighter than the shared step, so the section's
     opening line reads as the headline the client asked it to be. */
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.12;
}
/* "31+ Years" is the claim the whole section rests on. The phrase itself is
   marked with a soft accent underline drawn behind the baseline rather than a
   colour swap, so the heading stays one colour and still points at the claim. */
/* Four across, two down, inside the left band. The right ~34% is left for
   the machine by capping the grid's width rather than by padding it — the
   cards then get their full share of the space instead of being squeezed. */
/* Two rows of equal height, and the grid takes its height from the space the
   section has left rather than declaring one — that is what guarantees the
   second row lands inside the viewport instead of being pushed past it by a
   long card, at any viewport height. */
.why-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr));
  /* rem, matching the rem track below — a px gap would not scale with the
     rows it separates. 1rem/1.25rem = the old 16px/20px at a 16px root. */
  gap: 1rem 1.25rem;
  width: 66%;
  max-width: 1180px;
  /* Sized in rem, because everything inside the card is.

     The card's type rides the fluid root (html font-size is a clamp tuned to
     16px at 1440px, climbing to a 20px ceiling around 1900px), so h4 at 1rem
     delivers 16px on a 1440px screen and 20px on a wide one — about 25% more.
     The track used to be px/vh, which cannot follow that: past ~1440px the
     type kept growing while the box it sits in stayed put, short headings
     ("Wide Product Range", "After-Sales Support") tipped over to a second
     line, and each wrap pushed a paragraph line out of the glass. That is the
     overflow — not copy length, which is why a long card like "Reliable
     Performance" sat fine while shorter ones spilled.

     In rem the track scales with the text it holds, so the fit is identical at
     every width. 27.9rem was the old 446px at a 16px root: two rows of ~215px
     (48px icon + margin, a two-line heading, a four-line paragraph at the
     narrowest desktop column, 36px padding) plus the 16px row gap.

     That track was still ~4px short for the four longest cards ("Advanced
     Technology", "Reliable Performance", "Designed for Indian Kitchens",
     "Quality-Focused Engineering"), whose last paragraph line sat just past
     the glass — measured at 1280/1440/1600/1920 wide. Raised to 30.4rem, with
     the extra coming from the head's tightened bottom margin and the slack
     that was sitting unused below the grid, so the cards are both larger and
     no longer overflowing without the section growing.

     The vh term is intentionally kept as the middle value so a short window
     still compresses the grid rather than pushing row two into the section's
     overflow:hidden — on a short *and* wide window vh wins and the cards go
     tight again, which is the one case still worth checking in a browser. */
  flex: 0 1 auto;
  /* +1.4rem over the previous 30.4/31.9 pair: the larger card title (16px)
     and copy (14px) need slightly more room than the smaller icon disc gives
     back, so the track grows with them rather than letting the longest cards
     re-overflow the glass. */
  height: clamp(31.8rem, 49vh, 33.3rem);
  min-height: 0;
}

/* ---- Why Jagruti on the product category pages ----
   Those pages reuse .why-grid inside a plain `.section.section-alt`, but with
   four cards instead of the home page's eight. The base grid above is a fixed
   4x2 whose rows are sized to divide the home page's one-screen section, so
   with a single row of cards the second row stayed in the layout as ~15rem of
   empty space between the cards and the footer.

   Scoped by :not(.why-section) rather than by a new class so the five pages
   need no markup change: the home page's grid always sits inside
   .why-section, these never do. One content-sized row, and the fixed height
   released — there is no viewport box to divide here, so the cards size to
   their copy exactly as the neighbouring .section blocks do. */
.section:not(.why-section) .why-grid {
  grid-template-rows: none;
  height: auto;
  min-height: 0;
  flex: none;
}

/* ---- Trust strip (product category pages) ----
   Replaces the four .why-item glass cards those pages used to carry. Each
   card's paragraph only restated its own heading ("ISO 9001:2015 Certified"
   -> "Quality-controlled manufacturing at every stage"), so the copy was
   costing 463px of page to say what four labels say. This is the same four
   claims as one horizontal row: icon above label, no card, no paragraph.

   Not a glass card like .why-item on purpose. The cards belong to the home
   page's photographic .why-section, where the translucency reads against the
   image behind it. Here the ground is flat --page-bg, so a blurred fill has
   nothing to blur and would just look like a grey box. The strip carries
   itself on rhythm and a hairline instead. */
.trust-strip {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* No column gap: the hairline is the separator, and a gap on top of it
     would read as two different kinds of division at once. The breathing
     room comes from each cell's own padding-inline instead, which keeps the
     rule centred in the space between two labels rather than offset. */
  gap: 0;
  align-items: start;
  max-width: 900px;
  margin: 0 auto;
}
/* Pulls the strip up under the heading. `.section-head`'s 44px is sized for
   a block of cards beneath it; against a single 64px row it left the label
   floating. */
.trust-strip-head.section-head { margin-bottom: 30px; }
.trust-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: .625rem;
  /* Separators between items, not around them: a hairline on every item but
     the first reads as division rather than as four boxed cells. */
  padding-inline: 1rem;
  border-left: 1px solid var(--blue-100);
}
.trust-item:first-child { border-left: 0; }
.trust-item .icon {
  width: 2rem; height: 2rem;
  display: flex; align-items: center; justify-content: center;
  color: var(--blue-700);
  flex: none;
}
.trust-item .icon svg { width: 100%; height: 100%; }
/* The label is the whole content now, so it carries the weight the heading
   and paragraph used to share. Kept to two lines at every width by the
   column count below rather than by shrinking the type. */
.trust-item h4,
.trust-item h3 {
  margin: 0;
  font-size: .9375rem;       /* 15px */
  font-weight: 600;
  line-height: 1.3;
  color: var(--blue-800);
  hyphens: none;
  overflow-wrap: normal;
}

@media (max-width: 860px) {
  /* Two columns. The hairline has to move with them: it now belongs on the
     right-hand item of each pair, not on items 2/3/4. */
  .trust-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2.25rem 0; }
  .trust-item:nth-child(odd) { border-left: 0; }
  .trust-item:nth-child(even) { border-left: 1px solid var(--blue-100); }
}
@media (max-width: 460px) {
  /* One column on a phone. Vertical hairlines make no sense stacked, so the
     rhythm carries it alone. */
  .trust-strip { grid-template-columns: 1fr; gap: 1.75rem 0; }
  .trust-item { border-left: 0 !important; padding-inline: 0; }
}

/* The glass card. Deliberately under-opaque so the blue behind stays
   readable through the fill — that translucency is what sells the material.
   A fixed min-height keeps the two rows even when copy lengths differ. */
.why-item {
  position: relative;
  text-align: left;
  /* No min-height: the row track sets the height now, and a min-height here
     would fight it and re-introduce the overflow. */
  min-height: 0;
  /* Visible, not hidden: clipping is what disguised the overflow before.
     If the copy ever fails to fit now it must be visible so it can be fixed
     at the source rather than silently cut. */
  overflow: visible;
  /* A column so the paragraph occupies a real box in the card's flow rather
     than spilling past a fixed row height. */
  display: flex;
  flex-direction: column;
  /* rem, for the same reason as the track and the icon: fixed padding around
     rem-scaled copy shrinks the usable box as the root grows. */
  padding: 1.125rem 1.375rem;
  border-radius: 24px;
  background: rgba(255,255,255,.20);
  -webkit-backdrop-filter: blur(20px) saturate(155%);
  backdrop-filter: blur(20px) saturate(155%);
  border: 1px solid rgba(255,255,255,.42);
  box-shadow:
    0 20px 50px rgba(5, 61, 87, .12),
    inset 0 1px 0 rgba(255,255,255,.55);   /* top-edge reflection */
  transition:
    transform .35s cubic-bezier(.22,.61,.36,1),
    box-shadow .35s cubic-bezier(.22,.61,.36,1),
    background .35s ease,
    border-color .35s ease;
}
/* Icon container: a small glass disc, same material as the card. */
.why-item .icon {
  position: relative;
  flex: none;
  /* rem, to match the rem-sized track and type above: a px icon would hold
     its 48px while the copy around it grew with the fluid root, eating the
     card's budget from the top on wide screens. */
  /* Reduced from 2.375rem: the disc is decoration, and every pixel it gives
     back goes to the heading and paragraph, which now run larger. */
  width: 1.875rem; height: 1.875rem; margin: 0 0 .4375rem;
  border-radius: 50%;
  background: rgba(255,255,255,.34);
  border: 1px solid rgba(255,255,255,.55);
  display: flex; align-items: center; justify-content: center;
  color: var(--blue-700);
  box-shadow: 0 6px 16px rgba(19,167,236,.16);
  transition: box-shadow .35s ease, color .35s ease;
}
.why-item .icon svg { width: .9375rem; height: .9375rem; position: relative; z-index: 1; }
/* Sized in rem, not em: the card is a small-typed context and an em-based
   heading would compound down into the hyphenation the old layout showed. */
.why-item h4,
.why-item h3 {
  /* Up from .875rem and a weight heavier: the card title is the thing being
     scanned, so it carries more presence than the copy under it. */
  font-size: 1rem;           /* 16px */
  font-weight: 700;
  line-height: 1.22;
  letter-spacing: -.005em;
  margin-bottom: 6px;
  color: var(--blue-800);
  /* The headings are short noun phrases — never break them mid-word. */
  hyphens: none;
  overflow-wrap: normal;
}
.why-item p {
  /* rem, not em: an em here resolved against the card's own font-size and so
     compounded as the fluid root grew, which is what ran the four longest
     cards a line past the glass on wide screens. Fixed rem plus a tighter
     leading keeps every paragraph inside its card. */
  font-size: .875rem;        /* 14px, up from 13px */
  line-height: 1.45;
  margin: 0;
  color: var(--ink-700);
  hyphens: none;
}

/* Hover: a 6px lift and a touch of scale, a brighter rim, a slightly denser
   fill and more glow on the icon. Nothing here changes layout. */
@media (hover: hover) {
  .why-item:hover {
    transform: translateY(-6px) scale(1.015);
    background: rgba(255,255,255,.30);
    border-color: rgba(255,255,255,.62);
    box-shadow:
      0 28px 60px rgba(5, 61, 87, .18),
      inset 0 1px 0 rgba(255,255,255,.7);
  }
  .why-item:hover .icon {
    box-shadow: 0 8px 22px rgba(19,167,236,.34), 0 0 0 6px rgba(19,167,236,.10);
    color: var(--blue-600);
  }
}

/* ---- Entrance ----
   Gated on html.js so content is never hidden without the script, and driven
   by .is-in, which main.js adds AND removes — so the sequence replays when
   the section is scrolled back into. Per-element delays are custom properties
   written inline by the script (see initWhyReveal), which is what sequences
   the phases: photograph, then eyebrow, then heading lines, then cards.

   Card entry is 3D, so the grid needs a shared perspective for the rotateX
   to read as depth rather than as a squash. */
.js .why-section[data-why] .why-bg,
.js .why-section[data-why] .why-product,
.js .why-section[data-why] .section-head .eyebrow,
.js .why-section[data-why] .why-line > span {
  opacity: 0;
  transition:
    opacity .95s cubic-bezier(.22,.61,.36,1) var(--why-delay, 0ms),
    transform .95s cubic-bezier(.22,.61,.36,1) var(--why-delay, 0ms);
}
/* Phase 1 — the photograph and the machine emerge, the product drifting in
   from the right so it reads as arriving rather than fading up. */
.js .why-section[data-why] .why-bg { transform: scale(1.08); }
.js .why-section[data-why] .why-product { transform: scale(1.08) translateX(40px); }
/* Phase 2 / 3 — eyebrow, then the heading line by line. Each line rides in
   its own overflow-clipped row, so it wipes upward instead of just fading. */
.js .why-section[data-why] .section-head .eyebrow { transform: translateY(30px); }
.js .why-section[data-why] .why-line { display: block; overflow: hidden; }
.js .why-section[data-why] .why-line > span { display: block; transform: translateY(100%); }
/* The cards deliberately have no entrance animation: they are simply present
   from the start, so only the photograph and the heading play on scroll. */

.js .why-section[data-why].is-in .why-bg,
.js .why-section[data-why].is-in .why-product,
.js .why-section[data-why].is-in .section-head .eyebrow,
.js .why-section[data-why].is-in .why-line > span {
  opacity: 1;
  transform: none;
}
/* Previously dropped the transition on both layers once settled, so the
   parallax's per-frame transform writes were not fighting a 950ms ease. The
   parallax is gone and nothing writes to these layers any more, so the rule
   would only strip the entrance transition on a replay. */

/* Stillness: everything at its settled state, no entrance, no parallax.
   initWhyReveal also returns early, so no transform is ever written. */
@media (prefers-reduced-motion: reduce) {
  .js .why-section[data-why] .why-bg,
  .js .why-section[data-why] .why-product,
  .js .why-section[data-why] .section-head .eyebrow,
  .js .why-section[data-why] .why-line > span {
    opacity: 1 !important; transform: none !important; transition: none !important;
  }
}

/* ---- Why Jagruti, narrower ----
   Kept with the section rather than in the shared responsive block at the
   foot of the file: the columns, the crop and the entrance distances all
   have to move together. An older `.why-grid { grid-template-columns }`
   rule in that shared block is exactly what broke the previous layout. */
@media (max-width: 1400px) {
  /* The grid needs a larger share before the cards get tight, so the
     machine's band narrows first. */
  .why-grid { width: 72%; }
  .why-product { width: 36%; }
}
@media (max-width: 1200px) {
  /* Tablet: two columns, four rows. The machine stays a real presence on
     the right rather than being dropped.
     The section is taller than the viewport here, so it no longer centres
     its content — align to the top and carry enough top padding to clear
     the fixed header, or the heading lands underneath it. */
  /* Below 1200px the one-viewport lock is released: eight cards in two
     columns (and one on a phone) cannot fit a 100vh box without crushing the
     copy, so the section becomes content-driven again and the page scrolls
     normally. The desktop height/max-height must be cleared explicitly or
     they keep squeezing the rows. */
  .section.why-section {
    height: auto;
    min-height: 0;
    max-height: none;
    align-items: flex-start;
    padding: 32px 0 80px;
  }
  /* The desktop flex column exists to divide a fixed viewport between head
     and grid. Here there is no fixed height to divide, so it is unwound —
     height:100% against an auto-height parent and a flex:1 grid would both
     fight the content-driven layout this breakpoint wants. */
  .why-inner { height: auto; display: block; }
  .why-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: none;      /* rows size to their content again */
    height: auto;
    /* The desktop rem floor and ceiling must be cleared explicitly too, or
       the ceiling would hold this content-driven grid to one desktop track
       and crush the stacked rows. */
    min-height: 0;
    max-height: none;
    /* Releases the desktop `flex: 1`: with .why-inner unwound to a block
       above this is inert, but left in place it would resurface the moment
       anything re-established a flex column here. */
    flex: none;
    width: 64%;
    gap: 18px;
  }
  .why-item { overflow: visible; }
  .why-product { width: 36%; }
  .why-product img { object-position: 78% 52%; }
  .why-section .section-head { margin-bottom: 44px; }
}
@media (max-width: 900px) {
  /* The two columns now need most of the width to stay readable, so the
     machine moves behind them and the wash carries the contrast. */
  .why-grid { width: 100%; max-width: 640px; }
  .why-product { width: 58%; opacity: .55; }
  .why-section::before {
    background:
      linear-gradient(115deg,
        rgba(255,255,255,.92) 0%,
        rgba(255,255,255,.80) 45%,
        rgba(233,242,247,.50) 75%,
        rgba(233,242,247,.25) 100%);
  }
}
@media (max-width: 760px) {
  /* Phone: a single column over the photograph. The machine is not hidden —
     the section opens with a band of clear photograph at the top, the mill
     sitting in it at full strength, and the cards begin below that. Travel is
     shortened and the script tightens the stagger to match. */
  .section.why-section {
    /* The opening band is 268px tall and starts at the very top of the
       section, so the copy clears it by exactly that plus one gap. The old
       value assumed a band inset by --header-h and left a dead strip of
       empty wall above the machine. */
    padding: 28px 0 64px;
    align-items: flex-start;
  }
  .why-bg img { object-position: 40% 50%; }
  /* Held in the opening band rather than washed out behind the cards: full
     opacity, its own crop centred on the mill, and a mask that feathers both
     the left edge and the foot so the band settles into the wash instead of
     ending on a line. Wider and pushed further right than the desktop layer
     so the machine reads at this size rather than becoming a sliver. */
  /* The source is ~2:1, so a short band across a 390px viewport can only ever
     show a slice of the machine. The band is given real height instead and
     sits full-bleed, which is what lets the mill read at this size. */
  .why-product {
    /* Right-hand band: the copy now starts at the top of the section on the
       clear left, so the machine holds the right side beside it rather than
       occupying a full-width strip above it. */
    width: 62%; right: 0;
    /* Starts at the section's own top edge — the previous inset left a
       band of bare wall between the section start and the machine. */
    top: 0; bottom: auto;
    height: 300px;
    opacity: 1;
    /* Only the foot is feathered now. The old 90deg gradient erased the
       whole left 22% of a full-bleed band, and with the machine crop
       centred it took a visible bite out of the mill. */
    -webkit-mask-image:
      linear-gradient(180deg, #000 68%, transparent 100%),
      linear-gradient(90deg, transparent 0%, #000 18%);
    mask-image:
      linear-gradient(180deg, #000 68%, transparent 100%),
      linear-gradient(90deg, transparent 0%, #000 18%);
    -webkit-mask-composite: source-in;
    mask-composite: intersect;
  }
  /* Centred on the machine itself (~78% across the source), now that the band
     is full-width and tall enough to hold it. */
  .why-product img { object-position: 78% 45%; }
  /* The head sits in that same opening band, over the clear left of it. */
  .why-section .section-head { margin-bottom: 26px; }
  /* The wash starts clear over the opening band so the machine actually
     reads, and only comes up to full strength below it where the cards
     need the contrast. */
  .why-section::before {
    background:
      linear-gradient(160deg,
        rgba(255,255,255,.80) 0%,
        rgba(255,255,255,.55) 22%,
        rgba(255,255,255,.35) 38%,
        rgba(255,255,255,.88) 56%,
        rgba(233,242,247,.94) 100%);
  }
  .why-grid { grid-template-columns: 1fr; gap: 14px; width: 100%; max-width: none; }
  .why-item { min-height: 0; padding: 20px 20px 22px; border-radius: 22px; }
  .why-item .icon { width: 32px; height: 32px; margin-bottom: 9px; }
  .why-item .icon svg { width: 16px; height: 16px; }
  .why-section .section-head { margin-bottom: 32px; }

  .js .why-section[data-why] .why-bg { transform: scale(1.04); }
  .js .why-section[data-why] .why-product { transform: scale(1.04) translateX(20px); }
}

/* split (domestic vs commercial etc.) */
/* ---------- Two Ranges — paired image cards ----------
   Two large photographic cards side by side on a light cool wash. The
   images carry the section, so the cards stay tall and the copy sits on
   the photograph itself, lifted off the bottom-left corner. Height is
   fluid between the desktop bounds so the pair keeps its proportions
   across viewport widths without a step at every breakpoint. */
.split-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; }
.split-card {
  position: relative; border-radius: 14px; overflow: hidden;
  height: clamp(390px, 32vw, 430px);
  display: flex; align-items: flex-end; box-shadow: var(--shadow-md);
}
/* Both source frames are 16:9 and the machine sits right of centre, so a
   default centre crop on the taller card would push the product toward the
   edge and start clipping it. The crop is biased right to follow the
   machine, and a little above centre so the product clears the copy block
   and the deepest part of the scrim at the foot. */
.split-card img,
.split-card video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: 62% 42%;
}
/* Brand-blue scrim rising from the foot: deep enough under the copy for
   white text, clear of the machine in the upper half of the frame. */
.split-card::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(0deg, rgba(8, 46, 66, .88) 0%, rgba(8, 46, 66, .58) 32%, rgba(8, 46, 66, .12) 62%, rgba(8, 46, 66, 0) 100%);
}
.split-card .content { position: relative; z-index: 2; padding: 34px; color: #fff; }
.split-card h3 { color: #fff; font-size: 1.5em; }  /* 24px */
.split-card p { color: rgba(255,255,255,.85); }

/* ---------- Two Ranges — pinned full-bleed 50/50 panels ----------
   Each panel is a runway; the .range-pin inside it is one viewport tall
   and is held in place by ScrollTrigger while the runway scrolls past, so
   the split stays still and then hands over to the next one. Media and
   copy swap sides between the two panels (--flip), which is what makes the
   pair read as one alternating movement rather than two repeats. */
.ranges-section { padding-bottom: 0; }
.ranges-section .section-head { margin-bottom: 56px; }

/* Every layer in the pinned chain carries the SAME dark ground. A pin is
   engaged and released by taking the element out of flow and putting it
   back, and at each of those boundaries whatever sits behind it paints for
   a frame. With the page's near-white --page-bg behind a near-black panel
   that one frame reads as a white flash. Painting the runway and the
   section itself the same colour means there is nothing lighter to expose,
   whatever the pin does. */
.ranges-section { background: var(--white); }

/* ---- The seams around the white heading band ----
   This band has a full-bleed photograph above it and full-bleed video
   below, and butted flat against either one it draws a hard horizontal
   rule across the page. Each seam is feathered by a gradient painted on
   the element that OWNS that edge, so the white arrives and leaves
   instead of starting and stopping.

   The TOP seam is not here: it belongs to the journey section, whose
   sticky stage is what gets cut off at that line. See
   .journey-panel:last-child::after. The bottom seam is below. */
.ranges-section { position: relative; }

/* Bottom seam: REMOVED. There used to be a white wash on the first pin
   feathering the join between the white heading band and the first frame
   of video. It was driven off --split, which the scrub resets in a single
   frame when you re-enter the panel from below — so on an upward scroll
   the wash appeared abruptly over a video that was already framed, and on
   the way down it read as a haze over the top of the shot. The seam it was
   covering is not worth either artefact, so the video now meets the white
   band directly. */

.range-panel {
  /* The runway. The sticky child inside is one viewport tall, so the hold
     lasts for this height MINUS that viewport — 200vh here gives ~one
     screen of hold per panel. Raise it for a longer hold; at 100vh the
     panel is exactly as tall as its pin and there is no hold at all. */
  height: 200vh;
  position: relative;
  background: var(--white);
}
/* position:sticky, after the reference. The panel above is the runway; this
   child holds the screen while that runway scrolls past, exactly as
   .stage-pin and .services-pin already do in this file. No JS owns the
   hold, so there is no pin engage/release boundary to flash at — which is
   what the ScrollTrigger pin this replaced had to paint around. */
.range-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  /* The visible frame, matched to the hero: same 100svh unit and the same
     620px floor (.hero { height: 100svh; min-height: 620px }), so a short
     window cannot make this pin shorter than the hero. The TRACK height
     above is untouched — scroll duration and animation timing are
     unchanged; only the frame the pin paints into is pinned. */
  height: 100svh;
  min-height: max(100svh, 620px);
  max-height: max(100svh, 620px);
  overflow: hidden;
  background: var(--white);
  isolation: isolate;

  /* The push. --split is the media's width, scrubbed 100vw -> 50vw by
     initTwoRanges while the pin is held: the video fills the view on
     arrival, then is pushed aside as the copy is revealed beside it. On the
     flipped panel the children are ordered the other way, so the same
     single value pushes the video right instead.

     A FLEX row, after the reference (farmminerals.com/promo builds this as
     .numbers-wrap-sticky: sticky, 100vh, overflow hidden, display flex).
     The media is a flex:none item whose width IS --split, scrubbed
     100vw -> 50vw; the copy is a fixed 50vw sibling that simply waits off
     the edge until the video vacates the space. Grid tracks were the wrong
     tool: at --split:100% a companion track still had to resolve against
     leftover space, which is what made the flipped panel slide sideways to
     settle before the push began. Two fixed widths in a row overflowing a
     hidden box have nothing to negotiate, so both panels start genuinely
     full-bleed and the push is the first movement in either direction. */
  --split: 100vw;
  display: flex;
  flex-wrap: nowrap;
}
/* No JS / reduced motion: the split is simply the settled 50/50. */
.range-panel.is-static .range-pin { --split: 50vw; }
/* Flipped panel: copy left, media right — as a REVERSED row, not an order
   swap. The two are not equivalent here. With order:1/order:2 the row still
   lays out from the left edge, so the copy painted at the left from the
   first frame and the 100vw media began 50vw off to the right with only its
   left half on screen — the video slid in past standing copy instead of
   filling the screen and pushing the copy in. row-reverse lays the row out
   from the RIGHT edge instead: the media anchors to the right and fills
   leftward at 100vw, the copy waits clipped off the LEFT edge, and the same
   --split scrub pushes it in from that side. An exact mirror of the
   unflipped panel, driven by the identical tween.

   DOM order stays media-then-copy in both panels (row-reverse flips only
   the visual main axis), so the reading order the order-swap was protecting
   is unchanged. */
.range-panel--flip .range-pin { flex-direction: row-reverse; }

/* flex:none so the width is exactly --split and nothing shrinks it; the
   reference's .numbers-video-new is the same (flex:none, width:50vw). */
.range-media { position: relative; overflow: hidden; flex: none; width: var(--split); }
/* object-fit:cover, matching the reference's .farmer-video. cover is what
   lets the video FILL the screen at the 100vw start with no letterboxing —
   the shrink is carried by the BOX narrowing around it, not by fitting the
   picture inside the box. (contain would letterbox badly at full-bleed,
   since the frame's aspect ratio there is the viewport's, not the video's.)
   The height:120% overspill gives cover somewhere to crop from vertically,
   so the framing tightens rather than the picture sliding. */
.range-media video,
.range-media img {
  position: absolute; left: 0; width: 100%;
  top: 50%; height: 120%; transform: translateY(-50%);
  object-fit: cover; object-position: 50% 42%;
}
/* No wash over the media. A white gradient from the copy side was laying a
   light shade across the video for the whole push; the video is meant to
   read as clean at every width. */

/* Fixed 50vw, flex:none — the settled half. It sits off the right edge
   (clipped by the pin's overflow:hidden) for as long as the media is wider
   than 50vw, so the copy is genuinely PUSHED into view as the video
   retreats, rather than being squeezed open from zero width. Mirrors the
   reference's .numbers-content-new / .carbon-content-new at width:50vw. */
.range-content {
  display: flex; align-items: center;
  flex: none; width: 50vw; min-width: 0;
  padding: 0 clamp(28px, 5vw, 80px);
  background: var(--white); color: var(--ink-900);
}
.range-content-inner { max-width: 30em; }
.range-content-inner .eyebrow {
  color: var(--blue-600); background: var(--blue-50);
}
.range-content-inner h3 {
  margin: 0 0 18px;
  color: var(--ink-900);
  /* An h3 by tag, a SECTION HEADING by role — it sits under an .eyebrow
     with body copy beneath, exactly like a .section-head. It keeps its
     fluid size (the panel is a pinned 50vw column, so it has to track the
     split) but takes the shared heading leading and tracking; the stray
     122% was the last thing making it read looser than its neighbours. */
  font-size: clamp(1.9em, 3.2vw, 2.6em);
  font-weight: var(--fw-title);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
}
.range-content-inner p {
  margin: 0 0 30px;
  color: var(--ink-700);
  font-size: .875em; line-height: 1.6;
}
/* Per-character split: words stay whole so lines break normally. */
.range-word { display: inline-block; white-space: nowrap; }
.range-char { display: inline-block; }

/* The copy starts hidden BEFORE first paint, gated on html.js the same way
   the hero scenes are. Without this the block paints fully visible, then
   GSAP snaps it to opacity 0 on its first frame and animates it back in —
   which is the flash. Anything JS cannot reach stays visible, so the
   no-script and no-GSAP paths are unaffected. */
.js .range-content-inner > * {
  opacity: 0;
  will-change: opacity, transform;
}
/* Once the panel has played, the compositor hint is dropped. */
.js .range-panel.is-revealed .range-content-inner > * { will-change: auto; }
/* No GSAP / reduced motion: never hide it in the first place. */
.js .range-panel.is-static .range-content-inner > * { opacity: 1; will-change: auto; }

@media (prefers-reduced-motion: reduce) {
  .js .range-content-inner > * { opacity: 1 !important; will-change: auto; }
}

/* No GSAP / reduced motion: everything is simply present, and the pin
   becomes an ordinary block (no sticky, nothing to hand over). */
.range-panel.is-static { height: auto; }
.range-panel.is-static .range-pin {
  position: relative; height: auto;
  /* Clears the desktop one-viewport lock; this path is a plain block. */
  min-height: 0;
  max-height: none;
  /* Stacked, like the other two fallback paths: the children carry fixed
     widths off --split, so dropping the pin without turning the row into a
     column would leave a half-width video beside a half-width copy at the
     56vh media height below — not the plain block this path intends. */
  flex-direction: column;
}
.range-panel.is-static .range-media { height: 56vh; min-height: 300px; width: 100%; }
.range-panel.is-static .range-content { width: 100%; }

@media (max-width: 900px) {
  /* Stacked: no pin, no runway — a pinned full-height panel on a phone
     fights the scroll. Media on top, copy beneath, as a normal block. */
  .range-panel { height: auto; }
  /* Stacked as a flex column. Both children carry fixed widths off --split
     (100vw and 50vw), so the direction change has to be joined by width
     resets or the pair would simply sit side by side and overflow. */
  .range-pin,
  .range-panel--flip .range-pin {
    position: relative; height: auto;
    /* Clears the desktop one-viewport lock; this path is a plain block. */
    min-height: 0;
    max-height: none;
    flex-direction: column;
    /* Nothing is pinned here, so keeping a full-screen layer alive costs
       memory for no benefit. */
    will-change: auto;
  }
  .range-media { height: 56vh; min-height: 300px; width: 100%; }
  .range-content { width: 100%; padding: 44px clamp(18px, 6vw, 40px) 52px; }
}

@media (prefers-reduced-motion: reduce) {
  .range-panel { height: auto; }
  /* Both selectors, and the widths reset: --split stays at its 100vw start
     when no scrub runs, so without this the media fills the row and the
     copy is pushed off the edge entirely. Stacked is the right
     reduced-motion shape anyway — the push is the motion being opted out
     of. */
  .range-pin,
  .range-panel--flip .range-pin {
    position: relative; height: auto;
    /* Clears the desktop one-viewport lock; this path is a plain block. */
    min-height: 0;
    max-height: none;
    flex-direction: column;
  }
  .range-media { height: 56vh; min-height: 300px; width: 100%; }
  .range-content { width: 100%; }
}

/* steps */
.steps-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; counter-reset: step; }
.step { text-align: center; padding: 20px; position: relative; }
.step .num { width: 42px; height: 42px; border-radius: 50%; background: var(--blue-600); color: #fff; font-weight: 500; display: flex; align-items: center; justify-content: center; margin: 0 auto 14px; }
.step h4,
.step h3 { font-size: 1em; line-height: var(--lh-card); letter-spacing: -0.01em; margin-bottom: 4px; }
.step p { font-size: var(--fs-label); }

/* testimonial */
.testimonial-card { background: var(--white); border: 1px solid var(--blue-100); border-radius: var(--radius-lg); padding: 26px; box-shadow: var(--shadow-sm); }
.testimonial-card .stars { color: var(--gold); font-size: var(--fs-card-desc); margin-bottom: 10px; letter-spacing: 2px; }
.testimonial-card .who { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
.testimonial-card .avatar { width: 42px; height: 42px; border-radius: 50%; background: var(--blue-200); color: var(--blue-800); display: flex; align-items: center; justify-content: center; font-weight: 500; }
.testimonial-card .who strong { display: block; font-size: var(--fs-label); }
.testimonial-card .who span { font-size: var(--fs-caption); color: var(--ink-500); }
.testimonial-empty { text-align: center; padding: 40px 20px; border: 1.5px dashed var(--blue-300); border-radius: var(--radius-lg); color: var(--ink-500); font-size: var(--fs-card-desc); }

/* CTA band */
.cta-band { background: var(--gradient-sheen); color: #fff; border-radius: var(--radius-lg); padding: 56px 40px; text-align: center; }
.cta-band h2 { color: #fff; }
.cta-band p { color: rgba(255,255,255,.88); max-width: 560px; margin: 0 auto 26px; }
.cta-band .hero-cta { justify-content: center; }

/* ---------- Filters / product listing ---------- */
.filter-bar { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 30px; align-items: center; justify-content: space-between; }
.filter-pills { display: flex; flex-wrap: wrap; gap: 8px; }
.filter-pill { padding: 9px 18px; border-radius: var(--radius-pill); border: 1.5px solid var(--blue-200); font-weight: var(--fw-body); font-size: var(--fs-label); color: var(--ink-700); background: var(--white); }
.filter-pill.active, .filter-pill:hover { background: var(--blue-600); color: #fff; border-color: var(--blue-600); }
.search-box { display: flex; align-items: center; gap: 8px; border: 1.5px solid var(--blue-200); border-radius: var(--radius-pill); padding: 9px 16px; min-width: 240px; background: #fff; }
.search-box input { border: none; outline: none; font-size: var(--fs-label); flex: 1; }
.search-box svg { width: 16px; height: 16px; color: var(--ink-500); flex: none; }
.no-results { text-align: center; padding: 60px 20px; color: var(--ink-500); display: none; }

/* ---------- Product Detail Page ---------- */
/* The breadcrumb sits on the bar's centre line at this width, so it needs more
   than the body's header clearance to clear the wordmark behind it. */
.pd-top { padding: 42px 0 20px; }
body[data-nav="flow"] main > .pd-top:first-child { padding-top: 76px; }
.pd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 50px; align-items: flex-start; }
.pd-gallery img { width: 100%; border-radius: var(--radius-lg); border: 1px solid var(--blue-100); box-shadow: var(--shadow-sm); }
/* Transparent PNG/WebP cut-outs have no photographic ground of their own,
   so the bordered card that suits a rectangular photo would draw a box
   around a floating machine. These sit on the section wash instead and
   carry a soft contact shadow, the way the homepage stage handles them. */
.pd-gallery img.is-cutout {
  border: none; box-shadow: none; background: none;
  padding: 10px 0;
  filter: drop-shadow(0 18px 26px rgba(5, 61, 87, .16));
}
/* Cards crop to a landscape window with object-fit: cover, which would
   slice the top and bottom off an upright machine. A cut-out is contained
   and centred on the card wash instead, so the whole product reads. */
.product-card .thumb img.is-cutout {
  object-fit: contain;
  padding: 12px;
  background: var(--blue-50);
}
/* The flour mill cut-outs are upright (~1:2), so at full column width they
   would run far below the fold. Cap the height and let `contain` centre the
   machine in the column instead. */
.pd-gallery > img.is-cutout,
.pd-stage > img.is-cutout { max-height: 620px; object-fit: contain; }
@media (max-width: 640px) { .pd-gallery > img.is-cutout, .pd-stage > img.is-cutout { max-height: 440px; } }
/* The product's name set large behind the machine, the way .stage-word sits
   behind each machine in the homepage Featured showcase - same face, weight,
   tracking and low-opacity blue - but scaled to this image column instead of
   the viewport. initProductWord (main.js) solves the size so the widest line
   spans the column; the clamp below is only the pre-JS fallback. */
.pd-stage { position: relative; container-type: inline-size; }
.pd-stage > img { position: relative; z-index: 1; display: block; }
/* Held smaller than the column so more of the name shows on either side. */
.pd-stage > img.is-cutout { width: 58%; max-height: 400px; margin: 0 auto; }
@media (max-width: 640px) { .pd-stage > img.is-cutout { width: 64%; max-height: 300px; } }
/* Landscape (3:2) cut-outs - the mixer grinders with their jars set out side
   by side - come out only two-thirds as tall as the square and upright
   machines at the same 58% width, so they read noticeably smaller. Give them
   a wider box so the machine carries the same visual weight. */
.pd-stage > img.is-cutout.is-wide { width: 84%; max-height: 460px; }
@media (max-width: 640px) { .pd-stage > img.is-cutout.is-wide { width: 94%; max-height: 320px; } }
.pd-word {
  position: absolute;
  left: 50%;
  top: 46%;
  z-index: 0;
  margin: 0;
  width: max-content;
  max-width: none;
  white-space: nowrap;
  text-align: center;
  font-family: var(--font-stage);
  font-weight: 700;
  font-size: clamp(2.4rem, 16cqw, 10rem);
  line-height: .82;
  letter-spacing: -.055em;
  text-transform: uppercase;
  color: var(--blue-700);
  opacity: 1;
  pointer-events: none;
  user-select: none;
  transform: translate(-50%, -50%);
}
/* Door-design picker: one swatch per artwork the model ships in. */
.pd-variants { display: flex; justify-content: center; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
.pd-variant {
  width: 58px; height: 78px; padding: 4px; cursor: pointer;
  background: var(--blue-50); border: 1.5px solid transparent; border-radius: var(--radius-md, 10px);
  transition: border-color .2s ease;
}
.pd-variant:hover { border-color: var(--blue-100); }
.pd-variant.is-active { border-color: var(--blue-600); }
.pd-variant:focus-visible { outline: 2px solid var(--blue-600); outline-offset: 2px; }
.pd-gallery .pd-variant img {
  width: 100%; height: 100%; object-fit: contain;
  border: none; border-radius: 0; box-shadow: none;
}
.pd-gallery .cap { text-align: center; font-size: var(--fs-caption); color: var(--ink-500); margin-top: 10px; }
.pd-info .cat-tag { font-size: var(--fs-tag); font-weight: var(--fw-label); text-transform: none; letter-spacing: var(--ls-label-tight); color: var(--blue-600); }
/* h2 step, not h1: this sits in a half-width column beside the gallery,
   and product names run long ("Food Processing Machinery for Restaurants &
   Food Businesses" is 59 characters). At the h1 step those wrap to four
   lines and push the buy actions below the fold. The page-hero h1 gets the
   full step because it spans the container. */
.pd-info h1 { font-size: var(--fs-h2); line-height: var(--lh-title); letter-spacing: var(--ls-title); margin-top: 8px; }
.pd-info .tagline { font-size: 1em; /* 16px */ color: var(--ink-500); margin-bottom: 18px; }
.pd-info .model-row { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 22px; }
.model-chip { background: var(--blue-100); color: var(--blue-800); font-weight: var(--fw-label); font-size: var(--fs-label); padding: 8px 14px; border-radius: var(--radius-sm); }
.pd-info .cta-row { display: flex; gap: 12px; flex-wrap: wrap; margin: 22px 0; }
.pd-info .mini-note { font-size: var(--fs-label); color: var(--ink-500); background: var(--blue-50); border: 1px solid var(--blue-100); padding: 12px 16px; border-radius: var(--radius-sm); }

.spec-table { width: 100%; border-collapse: collapse; font-size: var(--fs-card-desc); }
.spec-table th, .spec-table td { text-align: left; padding: 13px 16px; border-bottom: 1px solid var(--blue-100); }
.spec-table th { background: var(--blue-700); color: #fff; font-weight: 500; }
.spec-table tbody tr:nth-child(even) { background: var(--blue-50); }
.spec-table td:first-child { font-weight: 400; color: var(--ink-900); white-space: nowrap; }
.spec-table-wrap { overflow-x: auto; border-radius: var(--radius-md); border: 1px solid var(--blue-100); }

.fit-grid { --fit-cols: 5; display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; }
.fit-grid > .fit-pill { flex: 0 1 calc((100% - (var(--fit-cols) - 1) * 12px) / var(--fit-cols)); }
.fit-pill { background: var(--blue-50); border: 1px solid var(--blue-100); border-radius: var(--radius-md); padding: 16px 12px; text-align: center; font-size: var(--fs-label); font-weight: var(--fw-body); color: var(--blue-800); }

.related-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }

/* ---------- Compare studio ----------
   The comparison page runs its own full-bleed two-column layout: a sticky
   editorial rail on the left and the three product columns + spec table on
   the right. The rail is decoration and context; the right side is the tool. */
.compare-studio {
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  background: var(--page-bg);
  border-top: 1px solid var(--blue-100);
  /* The whole comparison scrolls with the page: columns, tabs and spec rows
     move together as you scroll, rather than the table scrolling inside a
     fixed viewport-height frame. The rail sticks instead (see .cs-rail).
     align-items must stay stretch (the default): a sticky child can only
     travel inside its own grid area, so shrinking the rail's track to its
     content would unstick it the moment that short area scrolled past. */
}

/* --- Left rail ---
   Two parts on purpose. The <aside> is the stretched grid track: it carries
   the ground and the divider for the section's whole height. The sticky
   travel belongs to .cs-rail-inner, because a sticky box as tall as its own
   container has nowhere to travel and would simply scroll away with the
   page — which is what a sticky .cs-rail did. */
.cs-rail {
  background: var(--blue-50);
  border-right: 1px solid var(--blue-100);
}
.cs-rail-inner {
  position: sticky; top: var(--header-h);
  height: calc(100vh - var(--header-h)); overflow: hidden;
  display: flex; flex-direction: column; justify-content: space-between;
  gap: 40px;
  padding: 44px 34px 36px;
}
.cs-eyebrow {
  font-size: var(--fs-label); font-weight: var(--fw-label);
  text-transform: uppercase; letter-spacing: var(--ls-label);
  color: var(--ink-700); margin: 0;
}
.cs-rule { display: block; width: 46px; height: 1px; background: var(--ink-500); opacity: .45; margin: 20px 0 26px; }
.cs-rail-title {
  font-family: var(--font-display); font-weight: 400;
  font-size: 2.25em; line-height: 1.08; letter-spacing: var(--ls-title);
  color: var(--ink-900); margin: 0 0 20px;
}
.cs-rail-copy { font-size: var(--fs-card-desc); line-height: var(--lh-body); color: var(--ink-500); margin: 0; }
.cs-rail-caps {
  font-size: .75em; font-weight: var(--fw-label);
  text-transform: uppercase; letter-spacing: var(--ls-label);
  line-height: 1.9; color: var(--ink-500); margin: 0;
}

/* --- Right side --- */
.cs-main {
  min-width: 0;
  padding: 24px 34px 60px;
}

/* Three product columns. Each column owns its own selector, so the choice
   lives on top of the machine it changes rather than in a separate row. */
.cs-columns {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 22px; align-items: stretch;
  min-height: 0;
  max-width: 760px; margin: 0 auto; width: 100%;
}
.cs-col {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  display: flex; flex-direction: column; min-height: 0;
}
.cs-col-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 9px; }
.cs-col-num,
.cs-col-cat {
  font-size: .6875em; font-weight: var(--fw-label);
  text-transform: uppercase; letter-spacing: var(--ls-label);
  color: var(--ink-500);
}
.cs-col-cat { text-align: right; }

.cs-select-wrap { position: relative; margin-bottom: 10px; }
.cs-select-wrap svg { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; pointer-events: none; color: var(--ink-500); }
.compare-select {
  appearance: none; -webkit-appearance: none;
  width: 100%; background: var(--blue-50);
  border: 1px solid var(--blue-200); border-radius: 0;
  padding: 9px 32px 9px 11px;
  font: inherit; font-size: var(--fs-label); font-weight: var(--fw-label);
  color: var(--ink-900); cursor: pointer;
}
.compare-select:focus-visible { outline: 2px solid var(--blue-500); outline-offset: 1px; }

.cs-stage {
  /* Same ground as the products pages' .grid-editorial tiles (#e0e2e6), so a
     machine reads identically here and on its category page. */
  background: #e0e2e6; border-radius: 0;
  flex: 0 0 auto; height: 200px;
  display: flex; align-items: center; justify-content: center;
  padding: 12px; overflow: hidden; margin-bottom: 8px;
}
/* Bounded, never stretched: the machine shrinks to fit the stage on its
   longest side and keeps its proportions, so nothing is ever clipped. */
.cs-stage img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  object-fit: contain; display: block;
}

/* Thumbnail strip — one frame per view of the chosen machine. */
.cs-thumbs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: 10px; flex: 0 0 auto; }
.cs-thumbs .cs-thumb { aspect-ratio: auto; height: 42px; }
.cs-thumb {
  background: #e0e2e6; border: 1px solid transparent;
  border-radius: 0; padding: 4px; aspect-ratio: 1; overflow: hidden; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: border-color .2s ease;
}
.cs-thumb img {
  max-width: 100%; max-height: 100%; width: auto; height: auto;
  object-fit: contain; display: block;
}
.cs-thumb.is-active { border-color: var(--blue-500); }
.cs-thumb:hover { border-color: var(--blue-200); }

.cs-col h3 { font-size: 1em; line-height: var(--lh-tight); margin: 0 0 4px; }
.cs-col h3 a { color: inherit; text-decoration: none; }
.cs-col h3 a:hover { color: var(--brand-primary); }
.cs-col-desc { font-size: var(--fs-label); color: var(--ink-500); line-height: var(--lh-body); margin: 0 0 10px; flex: 0 0 auto; }
.cs-col .btn { width: 100%; justify-content: center; padding-block: 9px; font-size: .8125em; }

.cs-col.is-empty .cs-stage,
.cs-col.is-empty .cs-thumbs { opacity: .4; }

/* --- Tabs --- */
.cs-tabs {
  display: grid; grid-template-columns: repeat(4, 1fr);
  border-bottom: 1px solid var(--blue-100);
  margin-top: 22px; flex: 0 0 auto;
}
.cs-tab {
  background: none; border: 0; border-bottom: 2px solid transparent;
  padding: 13px 8px; cursor: pointer;
  font: inherit; font-size: var(--fs-label); font-weight: var(--fw-label);
  text-transform: uppercase; letter-spacing: var(--ls-label);
  color: var(--ink-500); transition: color .2s ease, border-color .2s ease;
}
.cs-tab:hover { color: var(--ink-900); }
.cs-tab.is-active { color: var(--ink-900); border-bottom-color: var(--ink-900); }

/* --- Spec / content panels --- */
.cs-panels { min-height: 0; }
.cs-panel { display: none; }
.cs-panel.is-active { display: block; }
.compare-table-wrap { overflow-x: auto; }
.compare-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.compare-table th,
.compare-table td {
  padding: 11px 18px; border-bottom: 1px solid var(--blue-100);
  font-size: var(--fs-label); text-align: left; vertical-align: top;
}
.compare-table td.attr {
  width: 24%; font-weight: var(--fw-label);
  text-transform: uppercase; letter-spacing: var(--ls-label);
  font-size: .75em; color: var(--ink-900);
}
.compare-table td.val { color: var(--ink-900); }
.compare-table tbody tr:nth-child(odd) { background: rgba(233,242,247,.45); }
.cs-empty { text-align: center; color: var(--ink-500); padding: 48px 0; font-size: var(--fs-card-desc); }

/* Short viewports and small screens: let the section grow with the page
   again rather than squeezing the columns into an unusable frame. */
@media (max-width: 1100px) {
  .compare-studio { grid-template-columns: 1fr; }
  .cs-rail { border-right: 0; border-bottom: 1px solid var(--blue-100); }
  .cs-rail-inner {
    position: static; height: auto; overflow: visible;
    padding: 40px var(--gutter);
  }
  .cs-rail-mid, .cs-rail-bottom { display: none; }
  .cs-rail-title { font-size: 2em; }
  .cs-main { padding: 24px var(--gutter) 60px; }
}

/* ---------- FAQ ----------
   Editorial, paper-white treatment: no dark hero band, no card chrome.
   The questions are the page, so the accordions are hairline rules on
   white and the only colour is the brand blue on the marker and links. */
.faq-hero { background: var(--white); padding: 90px 0 30px; }
.faq-hero-inner { max-width: none; margin: 0; }
.faq-hero h1 {
  /* Marcellus, the site's page face, at display size. Weight stays at
     --fw-display (400) like .page-hero/.cat-hero: Marcellus ships only a
     400, so asking for a bold here gets a smeared synthetic one. */
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: clamp(2em, 6.2vw, 5.2em);
  line-height: 1.0;
  letter-spacing: -0.01em;
  color: var(--ink-900);
  text-transform: uppercase;
  margin: 0;
  max-width: 15ch;
}
.faq-hero-tag { color: var(--blue-600); }
.faq-hero-arrow { color: var(--blue-600); margin: 0 .06em 0 .02em; font-weight: 400; }

/* The aside sits on the right and well below the headline, mirroring the
   reference's diagonal reading path from the mark down to the contact line. */
.faq-hero-aside {
  margin: clamp(40px, 6.5vw, 88px) 0 0 auto;
  max-width: 26ch; text-align: left;
  color: var(--ink-700); font-size: 1em; line-height: 1.5;
}
.faq-hero p a { color: var(--blue-600); text-decoration: underline; text-underline-offset: 3px; }
.faq-hero p a:hover { color: var(--blue-700); }

.faq-section { background: var(--white); padding-top: 20px; }

/* Category jump links — quiet pills that only fill in on hover. */
.faq-cat-nav { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 56px; }
.faq-cat-nav a {
  padding: 7px 15px; border: 1px solid var(--blue-100); border-radius: var(--radius-pill);
  font-size: .8125em; font-weight: var(--fw-strong, 600); letter-spacing: .02em;
  color: var(--ink-700); background: var(--white); transition: background .18s ease, border-color .18s ease, color .18s ease;
}
.faq-cat-nav a:hover { background: var(--brand-tint); border-color: var(--blue-300); color: var(--blue-700); }

/* Each topic sits as its own block with a lot of air above it, the way
   the reference separates "Free mockup" from "Licensing". */
.faq-group { margin-bottom: 64px; scroll-margin-top: calc(var(--header-h) + 24px); }
.faq-group:last-of-type { margin-bottom: 0; }
.faq-group h3 {
  font-size: .8125em; line-height: 1.3; letter-spacing: .14em; text-transform: uppercase;
  font-weight: var(--fw-strong, 600); color: var(--blue-600);
  margin: 0 0 6px; padding-bottom: 14px; border-bottom: 1px solid var(--ink-900);
}

/* Default (product pages, inside a tinted .section-alt): bordered cards. */
.faq-item { border: 1px solid var(--blue-100); border-radius: var(--radius-md); margin-bottom: 12px; overflow: hidden; background: #fff; }
.faq-item summary { padding: 18px 22px; font-weight: var(--fw-strong); font-size: 1.0625em; line-height: var(--lh-card); cursor: pointer; display: flex; justify-content: space-between; align-items: center; list-style: none; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after { content: "+"; font-size: 1.375em; color: var(--blue-600); font-weight: 400; }
.faq-item[open] summary::after { content: "–"; }
.faq-item .a { padding: 0 22px 20px; color: var(--ink-700); font-size: var(--fs-card-desc); line-height: var(--lh-body); }

/* On the FAQ page: hairline-ruled rows on paper, rather than cards. */
.faq-group .faq-item { border: 0; border-bottom: 1px solid var(--blue-100); border-radius: 0; margin: 0; background: transparent; overflow: hidden; }
.faq-group .faq-item summary {
  padding: 24px 44px 24px 0; position: relative;
  font-weight: var(--fw-strong); font-size: 1.0625em; line-height: var(--lh-card);
  color: var(--ink-900); cursor: pointer; display: block; list-style: none;
  transition: color .18s ease;
}
.faq-group .faq-item summary:hover { color: var(--blue-600); }
.faq-group .faq-item summary::-webkit-details-marker { display: none; }
.faq-group .faq-item summary::after {
  content: "+"; position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
  font-size: 1.5em; line-height: 1; color: var(--blue-600); font-weight: 300;
  transition: transform .2s ease;
}
.faq-group .faq-item[open] summary { color: var(--blue-700); }
.faq-group .faq-item[open] summary::after { content: "–"; }
.faq-group .faq-item .a {
  padding: 0 44px 26px 0; max-width: 660px;
  color: var(--ink-500); font-size: var(--fs-card-desc); line-height: 1.7;
}

@media (max-width: 680px) {
  .faq-hero { padding: 44px 0 18px; }
  .faq-hero h1 { max-width: none; letter-spacing: -0.015em; }
  .faq-hero-aside { margin-top: 32px; margin-left: 0; max-width: none; }
  .faq-cat-nav { margin-bottom: 40px; }
  .faq-group { margin-bottom: 48px; }
  .faq-group .faq-item summary { padding: 20px 36px 20px 0; font-size: 1em; }
  .faq-group .faq-item .a { padding-right: 0; }
}

/* ---------- Contact ---------- */
.contact-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 44px; align-items: flex-start; }
.form-field { margin-bottom: 16px; }
.form-field label { display: block; font-weight: var(--fw-label); font-size: var(--fs-label); margin-bottom: 6px; color: var(--ink-900); }
.form-field input, .form-field select, .form-field textarea {
  width: 100%; padding: 12px 14px; border: 1.5px solid var(--blue-200); border-radius: var(--radius-sm);
  font-size: var(--fs-card-desc); font-family: inherit; outline: none; background: #fff;
}
.form-field input:focus, .form-field select:focus, .form-field textarea:focus { border-color: var(--blue-500); }
.form-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.contact-info-card { background: var(--blue-50); border: 1px solid var(--blue-100); border-radius: var(--radius-lg); padding: 26px; margin-bottom: 18px; }
.contact-info-card h4,
.contact-info-card h3 { font-size: .875rem; font-weight: 600; line-height: 1.25; text-transform: none; letter-spacing: var(--ls-label-tight); color: var(--blue-600); margin-bottom: 5px; }
.contact-info-card .val { font-size: .8125rem; line-height: 1.45; font-weight: var(--fw-label); color: var(--ink-900); }
/* The address wraps to several lines, so it sits one step down from the
   single-line phone/email values; the email keeps the root size so a long
   address does not force the whole card's type larger. Both were inline
   style= overrides before. */
.contact-info-card .val-address { font-size: .8125rem; font-weight: var(--fw-strong); line-height: 1.45; }
.contact-info-card .val-email   { font-size: .8125rem; }
.map-embed { border-radius: var(--radius-lg); overflow: hidden; border: 1px solid var(--blue-100); }
.map-embed iframe { width: 100%; height: 320px; border: 0; display: block; }
.form-note { font-size: var(--fs-caption); color: var(--ink-500); margin-top: 10px; }

/* ---------- Blog ---------- */
.blog-card { background: #fff; border: 1px solid var(--blue-100); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow-sm); }
.blog-card .thumb { aspect-ratio: 16/10; background: linear-gradient(135deg, var(--blue-600), var(--blue-300)); display: flex; align-items: center; justify-content: center; }
.blog-card .thumb span { color: #fff; font-size: var(--fs-tag); font-weight: var(--fw-label); text-transform: none; letter-spacing: var(--ls-label-tight); opacity: .9; }
.blog-card .body { padding: 20px; }
.blog-card .cat { font-size: var(--fs-tag); font-weight: var(--fw-label); line-height: var(--lh-tight); color: var(--blue-600); text-transform: none; letter-spacing: var(--ls-label-tight); }
.blog-card h3 { font-size: var(--fs-card-title); line-height: var(--lh-card); margin: 8px 0; }
.blog-card p { font-size: var(--fs-card-desc); line-height: var(--lh-body); }
/* Container stays at the root size so the `em` values on its children
   resolve against 16px instead of compounding. */
.article-body { max-width: 760px; margin: 0 auto; }
/* Article headings run a step below the global h1–h3: a 760px measure
   column does not need 48px display type, and the tighter ramp keeps the
   heading-to-prose contrast the same as everywhere else. This replaces an
   identical inline `font-size:clamp(26px,3.4vw,40px)` that had been
   pasted onto the h1 of all nine article pages. */
.article-body h1 { font-size: var(--fs-h3); line-height: var(--lh-title); letter-spacing: var(--ls-title); margin: 0 0 .4em; }
/* --fs-body, matching the global `p` rule near the top of this sheet, which
   is what every ordinary paragraph on the site (home page included) already
   renders at. The root is FLUID, so this token is ~15.9px in practice, not
   the 18px its comment suggests — anything smaller here made article prose
   visibly finer than the rest of the site. */
.article-body p, .article-body li { font-size: var(--fs-small); line-height: var(--lh-body); }
.article-body h2 { font-size: var(--fs-h5); margin-top: 38px; color: var(--blue-800); }
.article-body h3 { font-size: var(--fs-h6); margin-top: 26px; }
/* Article prose headings must not reach into embedded COMPONENTS. Nine
   article pages embed .product-card blocks, and `.article-body h3` (two
   classes + a tag) out-specifies `.product-card h3`, so the same card
   title rendered 19.2px inside an article and 20px everywhere else. These
   put the component back in charge of its own typography. */
.article-body .product-card h3,
.article-body .blog-card h3,
.article-body .need-card h3 {
  font-size: var(--fs-card-title);
  line-height: var(--lh-card);
  margin-top: 0;
}
/* The category label above an article headline. `.cat-tag` previously had
   rules only under .product-card, .pd-info and .stage-copy — so the bare
   one used on all nine article pages matched nothing and rendered as
   unstyled default text. This is the label step of the ramp. */
.article-body .cat-tag,
.cat-tag {
  font-size: var(--fs-tag); font-weight: var(--fw-label);
  line-height: var(--lh-tight); letter-spacing: var(--ls-label-tight);
  text-transform: none; color: var(--blue-600);
  margin-bottom: 10px;
}
.article-body ul { margin: 0 0 18px; padding-left: 22px; list-style: disc; }

/* ---------- Pull quote + attribution (About) ----------
   Was three inline style= blocks, one of which asked for font-weight:800
   — a weight General Sans does not ship, so the browser faked it with a
   synthetic bold. 700 is the real ceiling and is what renders here. */
.quote-copy { font-style: italic; color: var(--ink-900); }
.attribution { margin-top: 18px; font-weight: var(--fw-title); color: var(--blue-700); }
.attribution-role {
  display: inline-block; margin-top: 2px;
  font-size: var(--fs-label); font-weight: var(--fw-label);
  color: var(--ink-500);
}
.footnote { margin-top: 26px; color: var(--ink-500); }
.article-body li { margin-bottom: 8px; color: var(--ink-700); }
.article-meta { display: flex; gap: 16px; align-items: center; color: var(--ink-500); font-size: var(--fs-label); margin-bottom: 30px; }

/* ---------- Support ---------- */
.support-card { background: #fff; border: 1px solid var(--blue-100); border-radius: var(--radius-lg); padding: 30px; box-shadow: var(--shadow-sm); text-align: center; }
.support-card .icon { width: 56px; height: 56px; border-radius: 16px; background: var(--blue-100); color: var(--blue-700); display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; }
.support-card .icon svg { width: 26px; height: 26px; }

/* ---------- Timeline (About) ---------- */
.timeline { position: relative; padding-left: 30px; border-left: 3px solid var(--blue-200); }
.timeline-item { position: relative; padding-bottom: 34px; }
.timeline-item::before { content: ""; position: absolute; left: -39px; top: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--blue-600); border: 3px solid var(--blue-100); }
.timeline-item h4 { font-size: 1em; color: var(--blue-700); margin-bottom: 4px; }
.timeline-item p { font-size: var(--fs-label); margin: 0; }

/* ---------- Footer ----------
   Full-viewport footer, laid out as three stacked bands: signup + link
   columns on top, the oversized wordmark in the middle, legal/social and
   the copyright line at the base.

   The one-screen lock follows SECTION-HEIGHTS.md. It is safe here because
   the wordmark row is the flexible element — it takes whatever height the
   other two bands leave and scales its own type to fit, so the content
   flexes to the box rather than the box to the content. Released below
   1200px, where the link columns have to stack and one screen stops being
   achievable. */
.site-footer {
  background: var(--gradient-deep);
  color: rgba(255,255,255,.78);
  height: 100svh;
  min-height: max(100svh, 620px);
  max-height: max(100svh, 620px);
  padding: 0;
  overflow: hidden;
}
.site-footer > .container {
  /* Footer copy is chrome: one step under the shared label size so the
     signup line, link columns and address all read quieter than page
     content. Scoped here so --fs-label stays untouched elsewhere. */
  --fs-footer: .8125em;  /* 13px */
  display: flex;
  flex-direction: column;
  height: 100%;
  padding-top: 58px;
  padding-bottom: 26px;
}

/* --- Top band: signup left, link columns right --- */
.footer-top {
  display: grid;
  grid-template-columns: minmax(300px, 1fr) auto;
  gap: 40px 60px;
  align-items: start;
}
.footer-signup h2 {
  color: #fff;
  /* Deliberately below the section step — footer copy stays quieter than
     page content — but on the ramp (--fs-h4) with the shared heading
     leading and tracking rather than three hand-picked values. */
  font-size: 1.5em;  /* 24px — was --fs-h4 (30px) */
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
  margin: 0 0 22px;
  max-width: 460px;
}
/* The underlined inline field: no box, just a rule with the arrow parked
   at its right end. */
.signup-form {
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 460px;
  border-bottom: 1.5px solid rgba(255,255,255,.45);
  padding-bottom: 10px;
  transition: border-color .25s ease;
}
.signup-form:focus-within { border-bottom-color: #fff; }
.signup-form input {
  flex: 1;
  min-width: 0;
  background: none;
  border: 0;
  padding: 4px 0;
  color: #fff;
  font-family: inherit;
  font-size: .9375em;  /* 15px */
}
.signup-form input::placeholder { color: rgba(255,255,255,.55); }
.signup-form input:focus { outline: none; }
.signup-form button {
  flex: none;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  background: none; border: 0; cursor: pointer;
  color: #fff;
  transition: transform .25s cubic-bezier(.34,1.56,.64,1);
}
.signup-form button:hover { transform: translateX(3px); }
.signup-form svg { width: 22px; height: 22px; }

.footer-nav {
  display: grid;
  grid-template-columns: repeat(4, minmax(120px, max-content));
  gap: 30px 44px;
}

.footer-nav h5,
.footer-nav h3 {
  margin-top: 0;
  line-height: var(--lh-tight);
  color: #fff;
  font-size: var(--fs-footer);
  font-weight: var(--fw-label);
  text-transform: none;
  letter-spacing: var(--ls-label-tight);
  margin-bottom: 16px;
}
.footer-nav li { margin-bottom: 8px; font-size: var(--fs-footer); font-weight: var(--fw-body); }
/* At 13px the old .78 -> 1.0 opacity shift was too quiet to register as a
   hover, so the link also slides a couple of px and underlines. */
.footer-nav a {
  display: inline-block;
  color: rgba(255,255,255,.78);
  cursor: pointer;
  text-decoration: none;
  transition: color .2s ease, transform .2s ease;
}
.footer-nav a:hover,
.footer-nav a:focus-visible {
  color: #fff;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transform: translateX(2px);
}
.footer-addr {
  color: rgba(255,255,255,.6);
  max-width: 230px;
  /* Sized explicitly: as a <p> it inherited the 18px prose step, which
     made the address the loudest thing in the footer — a step ABOVE the
     column heads it sits under. Footer copy is chrome, so it matches the
     link size beside it. */
  font-size: var(--fs-footer);
  line-height: var(--lh-body);
  margin-top: 14px;
}
.footer-addr p { font-size: inherit; line-height: inherit; color: inherit; }

/* --- Middle band: the oversized wordmark ---
   flex:1 hands it the leftover height; the clamp keeps the glyphs filling
   the container width across desktop sizes without ever outgrowing the
   band it sits in. */
.footer-wordmark {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px 0;
  user-select: none;
}
.footer-wordmark span {
  display: block;
  width: 100%;
  text-align: center;
  /* The wordmark keeps General Sans, like .stage-word: it is the brand name
     drawn as ARTWORK at 19rem, and it is the second place on the site that
     needs a real 700 rather than the synthesised bold the serif would give. */
  font-family: var(--font-stage);
  font-weight: 700;
  font-size: clamp(4rem, 19.5vw, 19rem);
  line-height: .82;
  letter-spacing: .01em;
  color: #fff;
  white-space: nowrap;
}

/* --- Base band: legal links left, social right, then the copyright --- */
.footer-base {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 14px 24px;
  padding-bottom: 18px;
}
.footer-base .legal-links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 26px;
  font-size: .8125em;  /* 13px */
}
.footer-base .legal-links a { color: rgba(255,255,255,.8); cursor: pointer; transition: color .2s ease; }
.footer-base .legal-links a:hover,
.footer-base .legal-links a:focus-visible { color: #fff; text-decoration: underline; text-underline-offset: 3px; }
.footer-social { display: flex; gap: 10px; }
.footer-social a {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: rgba(255,255,255,.08);
  display: flex; align-items: center; justify-content: center;
  color: #fff;
  cursor: pointer;
  transition: background .2s ease, transform .2s ease;
}
.footer-social a:hover,
.footer-social a:focus-visible { background: rgba(255,255,255,.18); transform: translateY(-2px); }
.footer-social svg { width: 16px; height: 16px; }
.footer-copy {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 6px 24px;
  border-top: 1px solid rgba(255,255,255,.14);
  padding-top: 16px;
  /* Just enough to keep the credit clear of the back-to-top pill. */
  padding-right: 10px;
  font-size: var(--fs-caption);
  color: rgba(255,255,255,.55);
}
.footer-credit a { color: #5eead4; text-decoration: none; font-weight: 800; padding-left: 2px; }

/* ---------- Legal ---------- */
.legal-body { max-width: 800px; margin: 0 auto; }
.legal-body h2 { font-size: var(--fs-h5); line-height: var(--lh-title); color: var(--blue-800); margin-top: 32px; }
.legal-body p, .legal-body li { color: var(--ink-700); font-size: var(--fs-card-desc); line-height: var(--lh-body); }
.legal-body ul { padding-left: 20px; list-style: disc; margin-bottom: 16px; }

/* ---------- Utility ---------- */
.mt-0 { margin-top: 0; }
.text-center { text-align: center; }
.tag-list { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.divider { height: 1px; background: var(--blue-100); margin: 40px 0; }

/* ---------- Responsive ---------- */
/* The footer's one-screen lock is released here, at 1200px, not at the
   1080px breakpoint below. SECTION-HEIGHTS.md sets the release at 1200px,
   and the two must agree: between 1201px and 1081px the layout has already
   reflowed while the lock would still be pinning the box to 100svh, and
   `overflow: hidden` then clips everything below the fold — the whole
   footer renders as an empty band with only the middle of the wordmark
   showing. Keep this block above the 1080px one and do not fold it in. */
@media (max-width: 1200px) {
  .site-footer {
    height: auto;
    min-height: 0;
    max-height: none;
    overflow: visible;
  }
  /* Clear the fixed call/WhatsApp bar so it doesn't cover the copyright line. */
  .site-footer > .container { padding-top: 52px; padding-bottom: calc(24px + var(--mobile-bar-h)); }
  .footer-top { grid-template-columns: 1fr; gap: 40px; }
  .footer-nav { grid-template-columns: repeat(4, minmax(120px, 1fr)); }
  .footer-wordmark { padding: 34px 0; }
  .footer-wordmark span { font-size: clamp(3.5rem, 20vw, 13rem); }
}
@media (max-width: 1080px) {
  /* The four link columns fall to two once the signup has stacked above
     them and there is no longer width to keep them side by side. */
  .footer-nav { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
  /* .why-grid deliberately has no rule here. Its columns, width, crop and
     entrance distances all move together and live in the Why Jagruti block
     above — an override at this breakpoint is what previously collapsed
     that grid into a narrow strip. */
  .related-strip { grid-template-columns: repeat(2, 1fr); }
  .fit-grid { --fit-cols: 3; }
  /* Tablet keeps the two columns — there is still width for them — with the
     cards and gutter stepped down proportionally. */
  .split-grid { gap: 20px; }
  .split-card { height: clamp(320px, 38vw, 390px); }
  .split-card .content { padding: 26px; }
}
@media (max-width: 900px) {
  /* Gutter tightens with the viewport; every .container follows. */
  :root { --gutter: 24px; }

  /* Tablet type step. Without this, headings held their full desktop size
     all the way down to the 600px phone breakpoint and then dropped in one
     jump — so the worst heading wrapping on the whole site happened in the
     600-900px band, where a 40px h2 met a single-column layout. The root's
     fluid clamp narrows the gap but does not close it, because the ratio
     between heading and body is fixed in the ramp.

     One deliberate step between desktop and phone, sized so the two-line
     h2s in this band stay two lines. Body copy is NOT stepped: it is
     already near its comfortable floor and the single column gives it more
     measure, not less. */
  h1 { font-size: 2.5em; }   /* 40px */
  h2 { font-size: 2.1em; }   /* 34px */
  h3 { font-size: 1.8em; }   /* 29px */
  h4 { font-size: 1.6em; }   /* 26px */
  h5 { font-size: 1.35em; }  /* 22px */
  .page-hero h1    { font-size: 2.5em; }
  .pd-info h1      { font-size: 2.1em; }
  .u-display { font-size: 3em; }
  .u-title   { font-size: 2.1em; }
  .pd-grid, .split-grid, .contact-grid, .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
  /* Stacked: each card takes the full column width and keeps its own
     top-left-anchored open, but shorter and with no sideways travel — the
     diagonal offset only makes sense against a neighbouring column. */
  .split-grid { gap: 18px; }
  .split-card { height: clamp(280px, 56vw, 340px); border-radius: 12px; }
  .split-card .content { padding: 24px; }
  /* Same corner-opening gesture, shorter and less extreme: a stacked card is
     already full-width, so it only needs to open from half height. */
  .js .split-grid[data-reveal] .split-card,
  .js .split-grid[data-reveal] .split-card:nth-child(2) {
    transform: scale(.55, .5);
    transition: opacity .45s cubic-bezier(.16, 1, .3, 1),
                transform .95s cubic-bezier(.16, 1, .3, 1);
  }
  .js .split-grid[data-reveal] .split-card img,
  .js .split-grid[data-reveal] .split-card video,
  .js .split-grid[data-reveal] .split-card .content,
  .js .split-grid[data-reveal] .split-card:nth-child(2) img,
  .js .split-grid[data-reveal] .split-card:nth-child(2) video,
  .js .split-grid[data-reveal] .split-card:nth-child(2) .content {
    transform: scale(1.8182, 2);  /* 1/.55, 1/.5 */
    transition: transform .95s cubic-bezier(.16, 1, .3, 1);
  }
  .js .split-grid[data-reveal] .split-card .content {
    transition: transform .95s cubic-bezier(.16, 1, .3, 1),
                opacity .4s ease .4s;
  }
  .steps-row { grid-template-columns: repeat(2, 1fr); }
  /* Hero: content stacks below 900px, so release the one-screen lock and
     let the section take its natural height instead of clipping. */
  .hero { height: auto; min-height: 0; padding: 0 0 40px; }
  /* The parallax hero has no flowing content to give it height (the copy is
     commented out), so it keeps an explicit one-screen box of its own. */
  .hero-parallax { height: 100svh; min-height: 460px; padding: 0; }
  /* Stacked layout: the photo moves out of the section background and into
     the stage band itself, so it never sits behind the stacked text. */
  .hero::before { content: none; }
  .hero::after { background: linear-gradient(180deg, rgba(0,26,42,.35) 0%, transparent 40%, rgba(0,22,35,.55) 100%); }
  .hero .container { display: block; height: auto; min-height: 0; }
  /* The stage becomes a plain band showing the photo; the callout lists are
     pushed out below it rather than overlapping it. */
  .hero-stage { display: block; margin: 6px 0 0; padding-top: 60vw; position: relative; min-height: 0; }
  /* The right-hand column unpins and flows under the photo band. The
     sub-line moves to the top of the stage so it still reads directly
     beneath the headline rather than stranded between callouts. */
  .hero-right-col { position: static; display: block; max-height: none; }
  .hero-sub {
    position: absolute; top: 0; left: 0; right: 0; margin: 0;
    text-align: center; width: auto;
  }
  /* Room above the photo band for that line. */
  .hero-stage { padding-top: calc(60vw + 3.4em); }
  .hero-stage::before { top: 3.4em; }
  /* Photo lives on its own layer so the feather mask never touches the
     callout lists that follow it. */
  .hero-stage::before {
    content: ""; position: absolute; top: 0; left: 0; right: 0; height: 60vw;
    background: url("../images/Home%20page/page%20background.png") no-repeat 50% 50% / contain;
    -webkit-mask-image: linear-gradient(to bottom, #000 66%, transparent 98%),
                        linear-gradient(to right, transparent 0, #000 12%, #000 88%, transparent 100%);
    -webkit-mask-composite: source-in;
            mask-image: linear-gradient(to bottom, #000 66%, transparent 98%),
                        linear-gradient(to right, transparent 0, #000 12%, #000 88%, transparent 100%);
            mask-composite: intersect;
  }
  .hero h1 { font-size: clamp(38px, 9vw, 72px); }
  /* Unpin the overlaid callouts and let them stack under the shot. */
  .hero-feats, .hero-feats-left, .hero-feats-right {
    position: static; transform: none; top: auto; bottom: auto; left: auto; right: auto;
  }
  .hero-feats { display: grid; grid-template-columns: 1fr 1fr; gap: 20px 16px; margin-top: 24px; }
  .hero-feats-right { margin-top: 20px; }
  .hero-feats span { max-width: none; }
  /* Callouts now sit below the shot, so the CTA pull-up no longer applies. */
  .hero .hero-cta { margin-top: 28px; }
  .hero-cats { grid-template-columns: 1fr 1fr; gap: 6px 0; }
  .hero-cats > a:nth-child(2n) { border-right: 0; }
  /* Stats on tablets: see the 601–900px block after the phone query. */
  .cs-columns { grid-template-columns: 1fr; }
  .cs-tabs { grid-template-columns: repeat(2, 1fr); }
  .compare-table td.attr { width: 40%; }
  .mobile-bar { display: block; }
  :root { --mobile-bar-h: 50px; }
  /* The home page drops the sticky call/WhatsApp bar, so the hero cue and
     dots sit against the true viewport bottom there.

     Hidden in CSS rather than deleted from the markup on purpose: the footer
     is a shared partial, and assets/js/build-partials.js rewrites every
     page's FOOTER block from footer.html, so any markup removed here is put
     straight back the next time that script runs. Editing footer.html would
     instead drop the bar from every page, which is not what is wanted. */
  :root:has(.hero-parallax) { --mobile-bar-h: 0px; }
  :root:has(.hero-parallax) .mobile-bar { display: none; }
  /* On tablet and mobile the header's "Get Price" action is dropped from the
     bar and carried by the sticky mobile action bar instead, so the narrow
     header holds only MENU and the wordmark.

     Hidden in CSS rather than removed from the markup: navbar.html is a
     shared partial and build-partials.js rewrites each page's NAVBAR block,
     so deleted markup comes straight back the next time it runs. */
  .header-actions { display: none; }

  body { padding-bottom: 0; }
  .form-row-2 { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
  :root { --gutter: 18px; }
  /* Mobile type scale. The reference only overrode h1/h3/h5 and left h2
     at full desktop size on phones — that gap is closed here. */
  h1 { font-size: 2.1em; }   /* 34px */
  h2 { font-size: 1.8em; }   /* 29px */
  /* These three now take ramp steps rather than bespoke desktop sizes
     (--fs-h1 / --fs-h2), so they only need to be re-pointed at the mobile
     step here — not given a third set of hand-picked numbers. The product
     h1 stays one notch below the page-hero h1 because it shares its row
     with the gallery even on a phone. */
  .page-hero h1 { font-size: 2.1em; }
  .pd-info h1   { font-size: 1.8em; }
  h3 { font-size: 1.6em; }   /* 26px */
  h4 { font-size: 1.45em; }  /* 23px */
  h5 { font-size: 1.3em; }   /* 21px */
  h6 { font-size: 1.1em; }   /* 18px */
  /* The root is already at its 14px floor here, so body copy keeps its
     1.13em rather than stepping down again — a second reduction would
     land prose at 14px, below comfortable reading size on a phone. */
  p, li, blockquote { font-size: 1.13em; }  /* ~15.8px at the 14px floor */
  .section-head p  { font-size: 1.13em; }
  .need-section .section-head p,
  .ranges-section .section-head p { font-size: var(--fs-small); }

  /* Display steps down hard on a phone — 60px would overflow a narrow
     screen. The rest of the ramp rides the root's 14px floor and needs
     no override. */
  .u-display { font-size: 2.4em; }
  .u-title   { font-size: 1.8em; }

  .related-strip { grid-template-columns: 1fr 1fr; }
  .feature-list { grid-template-columns: 1fr; }
  .fit-grid { --fit-cols: 2; }
  /* The token's own clamp floor already lands near this on a phone, but
     it is stated here because the vh term can still drift on a tall narrow
     device; this keeps phone sections compact and content-driven. */
  .section { padding-block: clamp(44px, 5.5vh, 64px); }
  .footer-signup h2 { font-size: 1.3125em; }
  .footer-nav { grid-template-columns: 1fr 1fr; gap: 26px 20px; }
  .footer-addr { max-width: none; }
  .footer-base { justify-content: flex-start; }
  .footer-wordmark { padding: 26px 0; }
  .footer-copy { font-size: var(--fs-caption); }
  .hero-feats { grid-template-columns: 1fr; gap: 16px; }
  .hero-cats { grid-template-columns: 1fr; }
  .hero-cats > a { border-right: 0; border-bottom: 1px solid rgba(255,255,255,.12); padding: 12px 16px; }
  .hero-cats > a:last-child { border-bottom: 0; }
  /* Phones: the same tile grid, tightened - icon above the figure so each
     tile is a compact square block, and the fifth stat runs full width as a
     wide footer tile rather than sitting alone in a half-empty row. */
  .hero-stats { flex-wrap: wrap; justify-content: center; gap: 14px 0; }
  .hero-stats > div { flex: 0 0 50%; }
  .hero-stats > div + div::before { display: none; }
  .hero-stats > div {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 8px;
    padding: 16px 12px;
  }
  .hero-stats div div { align-items: center; }
  .hero-stats .ic svg { width: 32px; height: 32px; }
  /* Icon-over-figure tiles are narrower; sized so the longest figure
     stays on one line down to a 360px phone. */
  .hero-stats strong { font-size: clamp(18px, 5.2vw, 28px); }
  .hero-stats > div:last-child {
    flex: 0 0 100%;
    flex-direction: row;
    gap: 12px;
    padding: 14px 16px;
  }
  .hero-stats > div:last-child div { align-items: flex-start; text-align: left; }
}
/* Stats on tablets: five items never split evenly, and the old 2-2-1 grid
   left-aligned two columns over one lone centred stat. Instead a six-track
   grid gives a balanced 3 + 2: the first three tiles span two tracks, the
   last two span three. Each stat is a soft centred card (icon over figure
   over label) so the rows read as one set. Scoped to 601px+ so the phone
   flex layout above is untouched. */
@media (min-width: 601px) and (max-width: 900px) {
  .hero-stats {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 14px;
  }
  .hero-stats > div { grid-column: span 2; }
  .hero-stats > div:nth-child(n+4) { grid-column: span 3; }
  .hero-stats > div + div::before { display: none; }
  .hero-stats > div {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 10px;
    padding: 22px 12px;
    background: #fff;
    border: 1px solid rgba(15, 76, 129, .10);
    border-radius: 14px;
    box-shadow: 0 6px 18px -12px rgba(15, 50, 90, .25);
  }
  .hero-stats div div { align-items: center; gap: 4px; }
  .hero-stats .ic {
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: rgba(30, 110, 190, .08);
  }
  .hero-stats .ic svg { width: 26px; height: 26px; }
  /* Third-width tiles: sized so "35,000+ sq.ft." stays on one line at 601px. */
  .hero-stats strong { font-size: clamp(18px, 2.9vw, 26px); }
}

/* --- FULL-BLEED PORTRAIT HERO (phones + portrait tablets) ----------------
   The scene art is a 16:9 export; a portrait phone is roughly 9:19. The old
   treatment letterboxed it into a 16:9 band with the copy stacked below on
   a white ground, which read as a banner rather than a hero.

   Here the artwork instead FILLS the section edge to edge (100svh, cover)
   and the copy sits on top of it, in the upper third, where the artwork's
   own sky is empty. That works because `cover` on a tall viewport crops the
   16:9 horizontally, and the composition's subject - product on its podium,
   with the leaf frame hugging the corners - is centred, so the crop keeps
   exactly what matters. `object-position` is pulled slightly below centre so
   the podium and its reflection stay in frame while the sky, which the copy
   covers anyway, is what gets trimmed off the top.

   Every layer keeps its own entrance transform, so the full parallax
   animation still plays; only the framing changes. */
@media (max-aspect-ratio: 1/1) and (max-width: 1024px) {

  /* Back to a true viewport-height hero. The scene wrapper is the section's
     own absolute overlay again (base rule), so nothing here re-stacks it. */
  .hero-parallax {
    --hero-h: 100svh;
    display: block;
    /* Full-viewport hero on portrait, same as desktop. Shrinking the SECTION
       was once how the subject was scaled down, but that bought a smaller
       machine at the cost of a hero that no longer filled the screen and let
       the next section peek in. --stage-over on the stage below now does
       that job on its own - it zooms the artwork out WITHIN a full-height
       section - so the height is free to go back to the full viewport. */
    height: var(--hero-h); min-height: 460px;
    aspect-ratio: auto;
    padding-top: 0;
    background: #559bcd;
  }
  /* --- Subject size ----------------------------------------------------
     The artwork is a 16:9 canvas and the viewport is portrait, so `cover`
     scales it until the HEIGHT fits and crops the sides away. The machine
     and its burst then fill far more of the frame than in the reference.

     Two approaches that do NOT work, for the record: widening the stage
     (past 16:9 the height drives the scale again, so nothing changes), and
     scaling the layers individually (every layer is an export of one shared
     canvas and they only stay registered while they transform together -
     shrink the machine alone and it lifts off the podium it stands on).

     What works is giving the stage MORE HEIGHT than the section and letting
     the section clip it. `cover` fits that taller box, so the composition
     renders smaller relative to the viewport, and the surplus is cropped off
     the top and bottom - sky and water, the parts there is most to spare of
     - while the artwork still bleeds to all four edges. */
  .hero-parallax { overflow: hidden; }
  /* --- How the composition is scaled ---------------------------------
     `cover` magnifies the 16:9 artwork until it fills the box on BOTH axes,
     so on a tall portrait box the height drives the scale and the machine is
     blown up far past the reference. Simply making the stage taller does not
     help - it makes the box even more extreme and magnifies further.

     Instead the stage is given the artwork's OWN aspect ratio and sized by
     WIDTH. `cover` then has nothing left to make up: the composition renders
     at its natural proportions, the full podium and arches stay in frame,
     and the machine reads at the size it does in the reference. --stage-w
     is the zoom control - above 100% the art is wider than the viewport and
     crops in slightly; below 100% it pulls further back. */
  .hero-parallax .hero-stagewrap {
    --stage-w: 215%;
    /* Zoomed out, the stage is well short of the section's height and the
       leftover ground splits evenly above and below. The copy occupies the
       top, so the scene is nudged DOWN off centre - that spends the surplus
       where the copy already covers it and keeps the podium clear of the
       scroll cue, rather than leaving a wide empty band under the water. */
    --stage-y: 62%;
    position: absolute; left: 50%; right: auto;
    top: var(--stage-y); bottom: auto;
    width: var(--stage-w); height: auto;
    aspect-ratio: 16 / 9;
    transform: translate(-50%, -50%);
  }
  /* Sized by width, the 16:9 stage is SHORTER than a portrait section, so a
     band of section ground is left above and below it. Filling those bands
     with the artwork's own edge colours - sampled from the scene plates,
     sky at the top and water at the bottom - makes them read as more sky and
     more water rather than as empty margins, and the soft stops let the
     stage's real edges dissolve instead of showing as seams. */
  .hero-parallax {
    background:
      linear-gradient(180deg,
        #5e9ccb 0%, #5e9ccb 18%,
        #5b9dce 32%, #4fa0d0 62%,
        #4d9fd0 82%, #4d9fd0 100%);
  }
  /* The stage's top and bottom edges meet that ground on a straight line,
     which reads as a seam however well the colours match. Fading the artwork
     out over a short distance at each edge dissolves the join, so the sky and
     water simply continue into the band. */
  .hero-parallax .hero-stagewrap {
    /* The top edge is open sky, so it can take a long fade without losing
       anything. The bottom is water whose own colour already matches the
       ground beneath it, and a long fade there would wash out the podium's
       reflection - so it gets only a short one, just enough to kill the
       line. */
    -webkit-mask-image: linear-gradient(180deg,
      transparent 0, #000 16%, #000 96%, transparent 100%);
            mask-image: linear-gradient(180deg,
      transparent 0, #000 16%, #000 96%, transparent 100%);
  }
  .hero-scene .hl {
    /* The stage now matches the art's ratio, so `contain` and `cover` agree;
       `cover` is kept so any rounding is absorbed rather than letting the
       section ground show as a hairline. */
    object-fit: cover;
    object-position: 50% 50%;
  }
  /* --- Making the handover sweep the whole hero ------------------------
     The stage carries the artwork's 16:9 ratio, so it - and every scene box
     inside it - is SHORTER than the section. The reveal is the top edge of
     the entering scene's box, so it could only ever uncover that short box:
     the handover visibly stopped partway up the screen.

     So each scene is stretched beyond the stage, far enough to reach the
     section's edges (--scene-bleed), while .hero-plate inside it stays put.
     The reveal edge now starts below the fold and travels past the top of
     the section, sweeping the entire hero, and because the plate is not
     stretched with it the artwork itself is unchanged.

     The climb distance has to grow to match, or the scene would stop short
     of covering its own taller box. */
  .hero-scene {
    --scene-bleed: 100svh;
    top: calc(-1 * var(--scene-bleed));
    bottom: calc(-1 * var(--scene-bleed));
  }
  .hero-scene .hero-plate {
    /* Puts the artwork back exactly where the stage had it, undoing the
       bleed for the layers only. */
    top: var(--scene-bleed);
    bottom: var(--scene-bleed);
  }
  :root { --scene-travel: 100svh; }
  /* Every layer is an export of ONE shared canvas, so they only compose
     correctly while they share an identical fit AND object-position - an
     exception for any single plane slides it off the others. Scaling the
     canvas leaf plane up on its own therefore is not an option, and once
     the stage is zoomed out its fronds sit well inside the frame instead of
     hugging the corners. So it is dropped here and replaced by the two
     dedicated corner plates, which are independent of the canvas. */
  .hl-leaf { display: none; }

  /* Above the ::before scrim (z-index 4), not below it. The scrim is a dark
     blue wash whose densest band is along the TOP edge - exactly where the
     two upper leaf clusters sit - so painting it over the foliage drained
     the greens and read as the leaves being washed out. It is there to keep
     the white copy legible against the bright sky, which only requires it to
     sit over the SKY; the copy is lifted to z-index 6 and so clears both.
     Putting the frame above it lets the foliage keep its own colour while
     the scrim goes on doing its job behind it. */
  .hero-leafframe {
    display: block;
    position: absolute; inset: 0; z-index: 5;
    pointer-events: none;
    overflow: hidden;
  }
  /* Each plate is a 16:9 export whose foliage sits against transparency in
     one corner region. Sized by WIDTH the art keeps its natural proportions
     and a sane on-screen scale (stretching it to the frame's full height
     with `cover` blows the fronds up until they swallow the machine).

     One instance is then anchored per corner, the bottom pair mirrored
     vertically so their fronds reach up and in rather than down and out.
     --leaf-w tunes how far into the frame the foliage reaches. */
  .hero-leafframe {
    /* How far each plate reaches in from its corner. Wider plates put more
       foliage over the frame rather than clinging to the very edge. */
    --leaf-w: 72%;
  }
  .hero-leafframe .hlf {
    position: absolute;
    width: var(--leaf-w); height: auto;
    object-fit: contain;
    /* The plates are 16:9 rectangles laid over the scene, and their PNG
       ground is not perfectly clear where it meets the section's scrim - the
       plate's own bottom edge showed as a straight line across the hero.
       Fading each one out towards its inner edge removes that boundary.

       The ramp is deliberately LATE and SHORT. It only has to hide the
       plate's own edge, so it stays fully opaque across the whole foliage
       region and does its fading in the last stretch of empty ground. The
       earlier 55%->100% ramp spread the falloff over most of the plate,
       which washed the fronds out and drained their colour - the leaves
       were being masked, not just their edge. */
    -webkit-mask-image: linear-gradient(180deg, #000 88%, transparent 100%);
            mask-image: linear-gradient(180deg, #000 88%, transparent 100%);
  }
  /* The bottom pair carries its foliage in the LOWER half of the plate, so
     the top-down fade above would erase exactly the part that shows. Theirs
     runs the other way: solid at the bottom, fading out towards the centre
     of the frame. */
  .hlf-bl, .hlf-br {
    -webkit-mask-image: linear-gradient(0deg, #000 88%, transparent 100%);
            mask-image: linear-gradient(0deg, #000 88%, transparent 100%);
  }
  /* Page1R1 carries one cluster, bottom-right, so the two BOTTOM corners use
     it directly with the left one mirrored. The top pair uses Page1L1top - a
     variant of Page1L1 with its lower half cleared. The full L plate carries
     clusters top-left AND bottom-left, and since a plate spans the whole
     frame, that second cluster landed a third of the way down the hero as a
     stray leaf under each top corner. Cropping to the top cluster leaves one
     piece of foliage per corner, which is all the frame wants. */
  .hlf-tl { top: 0; left: 0;  transform: scale(1.10); transform-origin: center; }
  .hlf-tr { top: 0; right: 0; transform: scaleX(-1) scale(1.10); transform-origin: center; }
  /* scaleY(-1) mirrors the mask along with the image, so the bottom pair
     fades towards the centre too without needing its own gradient. */
  /* Mirrors reflect about the box's own CENTRE. With the origin on an edge
     instead, the flip threw the plate clean out of the frame - which is why
     only the top pair was showing. */
  .hlf-bl { bottom: 0; left: 0;  transform: scaleX(-1) scale(1.10); transform-origin: center; }
  .hlf-br { bottom: 0; right: 0; transform: scale(1.10); transform-origin: center; }

  /* --- Entrance: the desktop leaf plane's move, exactly ---------------
     The portrait corner plates play the SAME entrance as the desktop canvas
     leaf plane: heroLeafIn - fade up from an over-scaled 1.20 and settle to
     the resting 1.10, no travel and no rotation, 1200ms on
     cubic-bezier(.22,.61,.36,1) cued at +560ms so the foliage sweeps in
     after the machine has begun rising, just as it does on desktop.

     It is keyed off the state classes the same way too. The frame lives
     outside the scenes, so JS mirrors .is-current / .is-entering / .is-first
     onto it (see initHeroScenes) - which is what makes the plates replay
     their entrance on EVERY scene change rather than only once on load.

     Each corner still needs its own keyframes: an animated `transform`
     replaces the property outright, so a single shared track would drop the
     corner's scaleX(-1) and flip the plate back mid-animation. The move
     inside each is identical - only the mirror differs. */
  .js .hero-leafframe .hlf { opacity: 0; }
  .js .hero-leafframe.is-current .hlf,
  .js .hero-leafframe.is-entering .hlf {
    animation-duration: 1200ms;
    animation-timing-function: cubic-bezier(.22,.61,.36,1);
    animation-delay: 560ms;
    animation-fill-mode: both;
  }
  .js .hero-leafframe.is-current .hlf-tl,
  .js .hero-leafframe.is-entering .hlf-tl { animation-name: heroLeafFrameTL; }
  .js .hero-leafframe.is-current .hlf-tr,
  .js .hero-leafframe.is-entering .hlf-tr { animation-name: heroLeafFrameTR; }
  .js .hero-leafframe.is-current .hlf-bl,
  .js .hero-leafframe.is-entering .hlf-bl { animation-name: heroLeafFrameBL; }
  .js .hero-leafframe.is-current .hlf-br,
  .js .hero-leafframe.is-entering .hlf-br { animation-name: heroLeafFrameBR; }

  /* Desktop's heroLeafIn, once per corner so each keeps its own mirror. */
  @keyframes heroLeafFrameTL {
    from { opacity: 0; transform: scale(1.20); }
    to   { opacity: 1; transform: scale(1.10); }
  }
  @keyframes heroLeafFrameTR {
    from { opacity: 0; transform: scaleX(-1) scale(1.20); }
    to   { opacity: 1; transform: scaleX(-1) scale(1.10); }
  }
  @keyframes heroLeafFrameBL {
    from { opacity: 0; transform: scaleX(-1) scale(1.20); }
    to   { opacity: 1; transform: scaleX(-1) scale(1.10); }
  }
  @keyframes heroLeafFrameBR {
    from { opacity: 0; transform: scale(1.20); }
    to   { opacity: 1; transform: scale(1.10); }
  }

  /* No full-height scrim in portrait: stacked under the header scrim it
     turned the whole top band navy. The header scrim behind the logo is the
     desktop one, unchanged, so the top reads the same at every size; below
     it the sky keeps its own colour and the copy's text-shadow carries
     legibility. */
  /* Re-used as a LIGHT wash rather than a dark scrim: the scene's sky sits
     at a deep saturated blue on portrait, so a pale sky tint over the upper
     band lifts it towards the lighter sky of the artwork's own horizon.
     It fades out above the machine so the product keeps its own colour.
     z-index 4: over the scenes, under the leaf frame (5) and copy (6). */
  .hero-parallax::before {
    content: ""; position: absolute; inset: 0; z-index: 4;
    pointer-events: none;
    background: linear-gradient(180deg,
      rgba(214,236,250,0)   0%,
      rgba(214,236,250,.26) 10%,
      rgba(214,236,250,.30) 22%,
      rgba(214,236,250,.20) 32%,
      rgba(214,236,250,.08) 40%,
      rgba(214,236,250,0)   48%);
  }
  /* The band above the stage is filled with the section's own ground, so it
     is lifted to the same paler sky or it would show as a darker strip. */
  .hero-parallax {
    background:
      linear-gradient(180deg,
        #7fb6de 0%, #79b2dc 18%,
        #5b9dce 32%, #4fa0d0 62%,
        #4d9fd0 82%, #4d9fd0 100%);
  }

  /* Portrait header scrim: the desktop scrim's dark top is kept (same
     colour, same .86 at the top edge behind the logo); only its fall-off
     is reshaped. It starts easing off inside the bar and trails out in
     ever-smaller steps well below it, so there is no point where the
     shade visibly ends. Homepage only; other over-dark heroes keep the
     desktop scrim. */
  body[data-nav="over-dark"]:has(.hero-parallax) .site-header:not(.is-scrolled)::before {
    /* Holds dark through the whole bar so the logo has a solid ground,
       then trails off over a long, eased run below it. */
    height: calc(var(--header-h) + 100px);
    background: linear-gradient(180deg,
      rgba(0,22,36,.92) 0,
      rgba(0,22,36,.86) calc(var(--header-h) * .35),
      rgba(0,22,36,.76) calc(var(--header-h) * .7),
      rgba(0,22,36,.62) var(--header-h),
      rgba(0,22,36,.46) calc(var(--header-h) + 14px),
      rgba(0,22,36,.30) calc(var(--header-h) + 30px),
      rgba(0,22,36,.17) calc(var(--header-h) + 48px),
      rgba(0,22,36,.08) calc(var(--header-h) + 66px),
      rgba(0,22,36,.03) calc(var(--header-h) + 84px),
      rgba(0,22,36,0) 100%);
  }


  /* --- Copy: centred, in the upper band, over the artwork --------------- */
  .hero-parallax .hero-copywrap {
    position: absolute; z-index: 6;
    left: 0; right: 0; top: calc(var(--header-h) + 4svh);
    display: grid;                 /* all three copies share one cell */
    pointer-events: none;
  }
  .hero-copy {
    grid-area: 1 / 1;
    position: static; transform: none;
    width: auto; max-width: min(90vw, 34rem);
    margin: 0 auto;
    padding: 0 clamp(20px, 5vw, 40px);
    text-align: center;
    color: #fff;
    text-shadow: 0 1px 18px rgba(0,32,52,.6), 0 1px 3px rgba(0,32,52,.55);
  }
  /* Portrait type runs a step smaller than desktop: the artwork is the hero
     and the copy has to clear the machine below it, so the block is kept
     short - a compact eyebrow, a two-line title, and a sub held to roughly
     two lines by a wider measure at a smaller size rather than by wrapping
     more often. */
  /* Intentional exception to --ls-label: at 9.5-11px, the standard .12em
     is not enough to keep uppercase letterforms apart on a phone. Tracking
     opens as size drops — an optical correction, not drift. */
  .hc-eyebrow {
    font-size: clamp(9.5px, 2.4vw, 11px);
    letter-spacing: .18em;
    color: rgba(255,255,255,.9);
    margin-bottom: .6em;
  }
  .hc-title {
    font-size: clamp(27px, 6.6vw, 42px);
    line-height: 1.1;
    text-wrap: balance;
    color: #fff;
    margin-bottom: .42em;
  }
  .hc-title .accent { color: #9fdfff; }
  .hc-sub {
    font-size: clamp(12px, 2.7vw, 14px);
    line-height: 1.45;
    color: rgba(255,255,255,.92);
    /* A wider measure at the smaller size, so the sentence lands in about
       two lines instead of three or four and the block stops rather than
       creeping down over the product. */
    max-width: 25em; margin: 0 auto 1.1em;
    text-wrap: pretty;
  }
  .hc-cta {
    font-size: clamp(12px, 2.9vw, 14px);
    padding: .7em 1.4em;
  }
  /* All four copies share one grid cell, so only the visible scene's CTA
     may take clicks - otherwise the last (invisible) copy's link sits on
     top and every tap goes to that one product page. The .js .is-current
     rule below re-enables its copy; without JS there is no sequence. */
  html:not(.js) .hero-copy:first-child .hc-cta { pointer-events: auto; }

  /* Slide-up entrance, since the copy is no longer vertically centred. */
  .js .hero-copy.is-current {
    animation: heroCopyInFlat 650ms cubic-bezier(.22,.7,.24,1) 800ms both;
    pointer-events: auto;
  }
  .js .hero-copy.is-leaving { animation: heroCopyOutFlat 400ms ease both; }
  @keyframes heroCopyInFlat {
    from { opacity: 0; transform: translateY(16px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @keyframes heroCopyOutFlat { from { opacity: 1; } to { opacity: 0; } }

  /* --- Scroll cue: centred at the bottom, as in the reference ----------- */
  .hero-scrollcue {
    position: absolute; z-index: 6;
    left: 50%; right: auto;
    transform: translateX(-50%);
    bottom: calc(var(--mobile-bar-h) + max(14px, 2.4svh));
    flex-direction: column-reverse;   /* pill above, label below */
    gap: .5em;
    color: rgba(255,255,255,.9);
    font-size: clamp(11px, 3.2vw, 13px);
  }
  .hero-scrollcue .hs-arrow {
    /* A mouse-shaped pill rather than a circle, matching the reference. */
    width: 1.5em; height: 2.5em; border-radius: 999px;
    border-color: rgba(255,255,255,.65);
  }
  .hero-scrollcue svg { width: 45%; height: 45%; }

  /* --- Scene indicator: horizontal, clear of the centred cue ------------ */
  .hero-dots {
    left: 50%; transform: translateX(-50%);
    bottom: calc(var(--mobile-bar-h) + max(14px, 2.4svh) + 5.2em);
    flex-direction: row; gap: 1.1em;
    font-size: clamp(10px, 2.8vw, 12px);
  }
  .hero-dots .hd { gap: .5em; }
  .hero-dots .hd i { width: 14px; height: 2px; }
  .hero-dots .hd.is-current i { width: 20px; height: 2px; }
}

/* A portrait tablet is far closer to the artwork's own 16:9 than a phone is,
   so `cover` magnifies less there and less surplus stage height is needed to
   bring the composition down to the same visual weight. */
@media (max-aspect-ratio: 1/1) and (min-width: 561px) and (max-width: 1024px) {
  .hero-parallax .hero-stagewrap { --stage-w: 138%; }
}

/* Phones are too narrow for three scene labels on one row - the indicator
   drops to bare ticks there and the label is carried by the copy's eyebrow. */
@media (max-aspect-ratio: 1/1) and (max-width: 560px) {
  /* Flour mill (scene 4): the machine is far taller than the other three
     products, so its top rises into the CTA. The whole plate is
     shrunk around the podium and nudged down - every layer moves together,
     so the machine stays registered on its podium. Uses the individual
     `scale`/`translate` properties so it composes with the plate's own
     transform-based entrance animation instead of replacing it. */
  .hero-scene-4 .hero-plate {
    transform-origin: 50% 80%;
    scale: .9;
    translate: 0 3svh;
  }
  /* Shrinking the plate pulls the artwork's top edge down below the stage's
     own edge fade, where it showed as a hard horizontal cut. Unlike the other
     scenes, whose tops are open sky in the ground's own colour, this one is
     a dark kitchen wall (~#1f62a6) against the pale ground, so a short fade
     still read as a seam. The fade has to be long - but the machine's top
     sits at ~16% of the canvas, so a long fade on the whole plate would wash
     the machine out too. It goes on the background layer alone instead,
     eased so the dark wall thins out gradually into the sky ground while
     the machine above it stays crisp. */
  .hero-scene-4 .hl-bg {
    -webkit-mask-image: linear-gradient(180deg,
      transparent 0, rgba(0,0,0,.18) 10%, rgba(0,0,0,.5) 20%,
      rgba(0,0,0,.82) 30%, #000 40%);
            mask-image: linear-gradient(180deg,
      transparent 0, rgba(0,0,0,.18) 10%, rgba(0,0,0,.5) 20%,
      rgba(0,0,0,.82) 30%, #000 40%);
  }

  .hero-dots .hd { font-size: 0; gap: 0; padding: 6px 3px; }
  .hero-dots { gap: .5em; }
  .hero-dots .hd i { width: 18px; }
  .hero-dots .hd.is-current i { width: 26px; }
}

/* Short landscape phones: the copy and the cue would collide in a ~380px
   tall viewport, so the cue steps out and the copy tightens. */
@media (min-aspect-ratio: 1/1) and (max-height: 520px) {
  .hero-scrollcue { display: none; }
  .hero-copy .hc-sub { display: none; }
}

/* The hero copy lives in .hero-copywrap, so .hero-parallax > .container is an
   empty box that still carries position:relative and z-index:1 from the base
   hero and paints over the artwork. Declared last so the responsive
   `.hero .container { display: block }` cannot resurrect it. */
.hero-parallax > .container { display: none !important; }

/* The portrait hero is full-bleed artwork again, so the header sits over the
   photo exactly as it does on desktop - the on-light override that the old
   stacked layout needed is gone. */

/* ==========================================================================
   PRELOADER
   The homepage hero paints its layers from opacity:0 and its artwork is a
   stack of large PNGs, so the first frame of the page is an empty coloured
   band. This overlay covers that window: it is painted immediately (the
   .preloading class is set by an inline script in <head>, before first
   paint) and main.js removes it once the first scene's images have decoded
   — or after a hard cap, so a slow connection never traps the visitor.
   ========================================================================== */
.preloader {
  position: fixed; inset: 0; z-index: 999;
  display: grid; place-items: center;
  /* Lighter than the deep cut this replaced, at the cost of separation: the
     logo keeps the artwork's exact #0d8ac4, so lifting the ground toward that
     same blue puts the mark at roughly 3:1 where it sits. Chosen deliberately
     over re-tinting the logo — colour fidelity to the mark wins here. */
  background: linear-gradient(160deg, #032533 0%, #053d57 55%, #064459 100%), #053d57;
  /* No transition here: the fade is driven by .is-done so the overlay never
     animates on the way IN. */
}
/* Without JS the class is never set, so the overlay must not exist as a
   permanent blocker. But this must NOT hide the overlay during the handover:
   finish() drops html.preloading and adds .is-done in the same frame, and a
   plain html:not(.preloading) rule would snap the element to display:none
   instantly, skipping the fade entirely. Excluding .is-done lets the exit
   transition play, after which JS removes the node outright. */
html:not(.preloading) .preloader:not(.is-done) { display: none; }

/* The page underneath must not scroll behind the overlay, and must not be
   read out by assistive tech while it is still assembling. */
html.preloading, html.preloading body { overflow: hidden; }

.pl-inner {
  text-align: center;
  color: var(--brand-primary);
  padding: 0 var(--gutter);
  /* The logo no longer scales on entry — the frame drawing itself IS the
     entrance. The inner block only fades, so the traced line starts at its
     final size and the stroke reads at a constant weight while it travels. */
  transform-origin: 50% 50%;
  will-change: opacity;
  animation: plFadeIn .4s ease both;
}
@keyframes plFadeIn { from { opacity: 0; } to { opacity: 1; } }

/* The frame and the wordmark are stacked in the same box at the same aspect
   ratio, so the drawn outline lands exactly on the artwork's own border. */
.pl-logo {
  position: relative;
  width: min(52vw, 260px);
  aspect-ratio: 986 / 256;
  margin-inline: auto;
}
.pl-frame,
.pl-wordmark {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* THE DRAW.
   pathLength="100" on the rect normalises its perimeter, so the dash values
   are plain percentages and no measuring is needed at runtime. The dash is
   one full-length segment pushed entirely out of view by the offset; easing
   the offset to 0 pulls the line around the path. overflow:visible keeps the
   stroke's outer half from being clipped at the viewBox edge. */
.pl-frame {
  overflow: visible;
  /* The logo's own blue — --brand-primary is already exactly the artwork's
     rgb(13,138,196), so the drawn line and the wordmark it hands off to are
     the same colour by definition rather than by a copied hex. */
  color: var(--brand-primary);
  stroke-dasharray: 100 100;
  stroke-dashoffset: 100;
  animation: plDraw 1.5s cubic-bezier(.65, 0, .35, 1) .15s both;
}
@keyframes plDraw {
  from { stroke-dashoffset: 100; }
  to   { stroke-dashoffset: 0; }
}

/* The letterforms are raster and cannot be traced, so they resolve inside the
   frame once it has closed — the delay is the draw's duration plus its own
   delay, which is what makes the two read as one gesture rather than two
   things happening at once. Blue artwork on the blue overlay would be
   invisible, so it is knocked out to pure white. */
.pl-wordmark {
  object-fit: contain;
  animation: plWordIn .7s cubic-bezier(.16, .84, .32, 1) 1.5s both;
}
@keyframes plWordIn {
  from { opacity: 0; transform: scale(.94); }
  to   { opacity: 1; transform: scale(1); }
}

/* Once the logo is whole, it breathes. Without this the screen is frozen for
   the rest of the wait — which is exactly the complaint this replaces. The
   loop starts after the wordmark has landed and runs until handover. */
.pl-logo {
  animation: plBreathe 2.8s ease-in-out 2.2s infinite both;
}
@keyframes plBreathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.035); }
}

/* Handover: the mark retreats exactly the way it arrived — back down to the
   same .35 it grew from, so the intro closes on itself. Its own fade is
   slower than the overlay's, which is what makes the shrink legible rather
   than lost behind the curtain. main.js removes the node when it finishes. */
.preloader.is-done {
  opacity: 0;
  /* Slightly longer than the mark's .55s retreat, and eased so the ground
     lingers early on: the wordmark must finish shrinking before the overlay
     is gone, or the gesture gets cut off mid-way. */
  transition: opacity .6s cubic-bezier(.6, 0, .9, .5);
  pointer-events: none;
}
/* Handover: the logo lifts and fades as one piece. The breathe loop lives on
   .pl-logo and holds its frame via `both`, so the exit has to be declared
   there too or the idle animation would win over a plain transform. */
.preloader.is-done .pl-inner {
  animation: plLogoOut .55s cubic-bezier(.55, 0, .85, .4) both;
}
.preloader.is-done .pl-logo {
  animation: plLogoOut .55s cubic-bezier(.55, 0, .85, .4) both;
}
@keyframes plLogoOut {
  0%   { opacity: 1; transform: scale(1); }
  /* Opacity is held most of the way so the lift is actually seen — fading in
     step with the scale would blank the logo while it is still near full
     size, and the gesture would never register. */
  55%  { opacity: .55; }
  100% { opacity: 0; transform: scale(1.08); }
}

@media (prefers-reduced-motion: reduce) {
  /* No drawing, no breathing for visitors who ask for stillness: the frame is
     simply whole and the logo simply present. The overlay still fades on
     handover, which is a change of state rather than a motion effect. */
  .pl-inner { animation: none; }
  .pl-frame { animation: none; stroke-dashoffset: 0; }
  .pl-wordmark { animation: none; }
  .pl-logo { animation: none; }
  .preloader.is-done .pl-inner,
  .preloader.is-done .pl-logo { animation: none; opacity: 0; transition: opacity .3s ease; }
}

/* ==========================================================================
   Featured — Popular Jagruti Machines
   A pinned, fullscreen poster sequence: one machine owns the screen at a
   time, its name set enormous behind it, cut-out props floating around it,
   and the existing copy set small at the lower left.

   There is no card here by design — no border, no panel, no thumbnail box.
   The machines are alpha cut-outs (assets/images/showcase/) so they sit
   directly on the ground the way an object sits in a photograph.

   Colour is entirely the tokens at the top of this file: the blue scale,
   the ink scale and --page-bg. Nothing new is introduced.
   ========================================================================== */

/* Doubled class for the same reason .section.why-section is doubled — the
   responsive blocks at the foot of this file re-declare .section padding at
   equal specificity and later in source order. The poster is pinned to a full
   viewport and the Why section that follows is its own full viewport, so any
   section padding here shows as a bare band of page background between the
   two. They are meant to meet edge to edge. */
.section.stage {
  position: relative;
  padding: 0;
  background: var(--page-bg);
  /* The pinned child would otherwise be clipped by any ancestor that
     establishes a scroll container. */
  overflow: visible;

  /* --- The showcase grid ---------------------------------------------
     Every layer in this section used to carry its own edge value: the
     copy and the tagline sat on var(--gutter) (40px), the callout row on
     clamp(20px,2.2vw,34px) (~31px) and the arrows on clamp(10px,1.6vw,22px)
     (22px). Three different rails on one poster, so nothing read as
     deliberately aligned to anything else. The four tokens below are now
     the ONLY source of position for the whole section — change one and
     every layer that answers to it moves together.

     --stage-rail   the outer rail. The arrows sit ON it, so it is the true
                    viewport margin and both arrows get the same one.
     --stage-inset  the text rail, one arrow-width plus a gap further in.
                    The tagline, the copy column and the callout row all
                    start here, so left-edge text and right-edge text are
                    equidistant from their own side and neither runs under
                    an arrow.
     --stage-top    the top band. The header and the tagline share it.
     --stage-base   the bottom baseline. The copy/CTA column and the
                    callout row share it, so the bottom of the poster is
                    one bar rather than two blocks at similar heights. */
  --stage-rail: clamp(20px, 2.6vw, 44px);
  --stage-arrow: 42px;
  --stage-inset: calc(var(--stage-rail) + var(--stage-arrow) + clamp(10px, 1.4vw, 24px));
  --stage-top: clamp(16px, 3.6vh, 34px);
  /* The callout row's own right boundary, one step outside the text rail:
     far enough out to give the row the width it wants, still clear of the
     arrow on the right edge. The last tile sits exactly here. */
  --stage-feat-rail: calc(var(--stage-rail) + clamp(6px, 0.9vw, 16px));
  /* One chip's horizontal padding (.spec-chip is padding: 5px 10px). The
     chip row is pulled left by this so the chip's LABEL lands on the same
     rail as the plain-text lines above and the buttons below. */
  --stage-chip-pad: 10px;
  /* Lower than the 8vh the copy and the callouts each used to carry, so
     both bottom blocks sit further down the poster — together, by exactly
     the same amount, which is the whole point of them sharing the token.
     The floor keeps the row clear of the viewport edge on a short window;
     the ceiling stops a tall one from stranding it mid-screen. */
  --stage-base: clamp(34px, 5.4vh, 68px);
}
/* Height is written by JS from the product count — one viewport of travel
   per machine plus the intro and the release. */
.stage-track { position: relative; }

.stage-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  /* The visible frame, matched to the hero: same 100svh unit and the same
     620px floor (.hero { height: 100svh; min-height: 620px }), so a short
     window cannot make this pin shorter than the hero. The TRACK height
     above is untouched — scroll duration and animation timing are
     unchanged; only the frame the pin paints into is pinned. */
  height: 100svh;
  min-height: max(100svh, 620px);
  max-height: max(100svh, 620px);
  overflow: hidden;
  /* A soft blue-into-white wash rather than the flat page background. It
     lives on the PIN, not on .section.stage: the section's track is many
     viewports tall, so a gradient there would only ever show one thin
     slice at a time. On the pin the full blend is visible in every frame.
     Blue is heaviest at the top, passes through a paler mid tone and
     resolves to white at the bottom, so the machines still stand on a
     near-white ground. */
  background:
    linear-gradient(180deg,
      #6aa8e2 0%,
      #93c4ef 34%,
      #c2dcf8 70%,
      #eaf3fd 100%);
  /* The whole poster is centred in this box; every layer inside is
     absolutely placed against it. */
  isolation: isolate;
}

/* --- The two section states -------------------------------------------
   The section is either INTRO or PRODUCTS, never both. The mode is written
   on .stage by the engine (is-intro / is-products) and every layer below
   derives from it, so there is no state in which the section identity and
   a machine are on screen together.

   Both stages are gated symmetrically: whichever one is not the current
   mode is opacity:0 + visibility:hidden + pointer-events:none, so it
   neither paints nor takes part in the stacking of the other. */
/* --- The section header ------------------------------------------------
   The identity is NOT a state any more. It is a small standing header in
   the top band of the stage, on screen the whole time the section is, with
   the machines running underneath it. There is no longer a moment where
   the section stops being itself to become the showcase.

   The top band is free by construction: the giant name sits at top:46%,
   the machine below that and the copy block lower still, so nothing the
   scenes draw reaches up here. */
/* --- The section identity --------------------------------------------
   Lives INSIDE .stage-scenes as its first child, not in a panel of its
   own: it is the top of the showcase rather than a separate thing that
   has to hand over to it. It therefore rides up with the sheet when the
   scenes slide in, which is the point — one object arriving, not two.

   The top band is free by construction: the giant name sits at top:46%,
   the machine below it and the copy block lower still, so nothing the
   scenes draw reaches up here. Above the scene layers (wash 0, word 1,
   props 2, machine 3, copy 5) so it is never covered. */
.stage-head {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* Shares --stage-top with the tagline, so the header baseline and the
     top of the stacked eyebrow belong to one top band. */
  padding: var(--stage-top) var(--stage-inset) 0;
  pointer-events: none;
}
/* The section's heading. It carries no size of its own: the shared
   section-heading step and the shared responsive steps below own it, the
   same as every other H2 on the site.

   It previously took a bespoke clamp so it would stay small against
   .stage-word (the viewport-spanning product name below it). That kept it
   visibly out of the system — 31.7px against the 40px every other section
   heading uses at 1440px, and the gap widened at every breakpoint
   (22.5 vs 36.4 at 1024, 16.1 vs 25.2 at 390). .stage-word is low-opacity
   background artwork, so there is no real competition to design around. */
.stage-head h2 {
  margin: 0;
  color: var(--heading-color);
}


/* The showcase does not travel. It fills the pin from the start, so there
   is never a frame in which the section shows empty behind a sheet that
   has not arrived yet — the machines are simply what this section IS. */
.stage.is-intro .stage-nav {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .3s ease, visibility 0s linear .3s;
}

/* PRODUCT STATE — the machines arrive under the header, which stays put. */
.stage.is-products .stage-nav {
  opacity: 1;
  visibility: visible;
  transition: opacity .3s ease, visibility 0s linear 0s;
}
.stage.is-products .stage-nav { pointer-events: auto; }

/* No entrance animation: the header is simply THERE, the way a section
   heading is there. It is part of the showcase, so it arrives with the
   sheet and needs no reveal of its own on top of that. */

/* --- Scenes ----------------------------------------------------------- */
/* Above the intro's z-index:8, so the showcase slides up OVER the identity
   and covers it, rather than passing beneath it where the travel would not
   be visible at all. The scenes carry an opaque wash, so once the sheet is
   home the intro underneath is fully hidden. */
.stage-scenes { position: absolute; inset: 0; z-index: 9; }
.stage-scene {
  position: absolute;
  inset: 0;
  visibility: hidden;
  will-change: opacity;
}
.stage-scene.is-live { visibility: visible; }

/* LAYER 1 — a wash that tints the ground behind the current machine.
   Built from the existing blue scale only, so each scene can feel distinct
   without the section ever leaving the palette. */
.stage-wash {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 1;
  /* A product-studio ground, not a white page with a wash over it. The
     base is a real blue field; the centre only LIFTS that field (it is
     still blue-tinted, never paper white) so a white machine reads against
     it. The rim sits deeper so the frame closes. Tokens only. */
  background:
    /* lit pool behind the machine — blue-tinted, not white */
    radial-gradient(52% 50% at 50% 46%, var(--blue-100) 0%, rgba(235,245,250,0) 68%),
    /* deeper rim, closing the frame on all four sides */
    radial-gradient(118% 108% at 50% 44%, rgba(13,138,196,0) 42%, rgba(13,138,196,.20) 78%, rgba(9,101,144,.32) 100%),
    /* the field itself */
    linear-gradient(168deg, var(--blue-300) 0%, var(--blue-200) 30%, var(--blue-100) 62%, var(--blue-200) 100%);
}
/* A soft floor under the machine so it reads as standing on something
   rather than floating on a flat panel. */
.stage-wash::after {
  content: "";
  position: absolute;
  left: 50%; bottom: 14%;
  width: min(760px, 62vw); height: 16vh;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(5, 61, 87, .22) 0%, rgba(5, 61, 87, 0) 70%);
}

/* LAYER 2 — the giant product name. This is artwork, not a heading: it
   spans the viewport, and the machine is deliberately allowed to cover its
   middle. Sized in vw so it always reaches edge to edge. */
.stage-word {
  position: absolute;
  left: 50%;
  top: 46%;
  z-index: 1;
  margin: 0;
  width: max-content;
  max-width: none;
  white-space: nowrap;
  /* This one element keeps General Sans (see --font-stage below), so 700 is a
     REAL weight here, not a synthesised one — unlike everywhere else on the
     site, which now runs the single-weight serif. */
  font-family: var(--font-stage);
  font-weight: 700;
  /* --fit is written per scene by initMachinePosters: it measures the name
     and solves for the size that lands its INK exactly on the two
     --stage-inset edges — the same rail the tagline above and the copy
     column below sit on, so all three read as one left and one right edge
     rather than three. A name can therefore be any length and still span
     that band. The clamp is only a pre-JS floor/ceiling. */
  --fit: 14vw;
  /* The fitter's solved size is what makes every name span the section
     identically, so the ceiling must not clip it — a short name needs a
     much larger size than a long one to cover the same width. The bounds
     stay only as a pre-JS floor and a runaway guard. */
  font-size: clamp(2.6rem, var(--fit), 60rem);
  line-height: .78;
  /* The grotesque's own display tracking, restored with the face. */
  letter-spacing: -.055em;
  text-transform: uppercase;
  /* A deep blue from the existing scale at low opacity, so the name reads
     as a large background GRAPHIC on the light field rather than washing
     out against it. Near-white (--blue-50) disappeared here; --blue-700 at
     this strength stays clearly legible while still sitting behind the
     machine. No new colour is introduced. */
  color: var(--blue-700);
  opacity: .18;
  pointer-events: none;
  user-select: none;
  /* --fit-shift is written per scene by the fitter: it cancels the gap
     between the element box's centre and the INK's centre (trailing
     letter-spacing, side bearings), so the visible letters — not the box —
     land symmetrically between the left and right edges. 0 until JS runs. */
  --fit-shift: 0px;
  transform: translate3d(calc(-50% + var(--fit-shift)), -50%, 0);
}
/* Per-letter placement: each glyph carries its own drift so the name
   assembles rather than sliding in as one block. */
.stage-word span {
  display: inline-block;
  will-change: transform, opacity;
}

/* LAYER 4 — the machine. Centred, large, and free-standing: the cut-outs
   carry their own shadow instead of a box. */
.stage-machine {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 3;
  height: clamp(280px, 62vh, 680px);
  width: auto;
  max-width: 82vw;
  object-fit: contain;
  /* A grounded contact shadow, not a card shadow — it follows the alpha
     silhouette, so the machine reads as an object standing in the scene. */
  filter: drop-shadow(0 34px 42px rgba(5, 61, 87, .26));
  will-change: transform, opacity, filter;
  /* Centred on the machine's INK, not on its file box.
     Some cut-outs carry uneven transparent padding in the source art, so
     box-centring leaves the visible machine off centre and the giant name
     behind it then reads as shifted the other way. Measured alpha-bounds
     offsets (ink centre vs box centre, as a share of file width):
        pride-blender   +1.59%      premium-1000    -2.29%
        all others       ~0%
     --ink-off is that correction, set per scene below and defaulting to
     zero, so a machine whose art is already centred is untouched. It only
     shifts placement — the image file, its size and its aspect are as they
     were. */
  --ink-off: 0%;
  transform: translate3d(calc(-50% - var(--ink-off)), -50%, 0);
}
/* Per-machine ink corrections. The value is the measured offset expressed
   against the RENDERED width, which is what translate percentages resolve
   against, so it holds at every size the machine is drawn at. */
.stage-scene[data-name="Jagruti Pride Portable Hand Blender"] .stage-machine { --ink-off: 1.59%; }
.stage-scene[data-name="Jagruti PREMIUM 1000 Automatic Roti Making Machine"] .stage-machine { --ink-off: -2.29%; }
/* Feathers a machine that ships as a hard rectangle (a photograph carrying
   its own background) into the ground, rather than letting its edges cut.
   Nothing uses this at present — every stage machine now has a real alpha
   cut-out, Aura Plus included — but it is kept for the next product whose
   only source art is a background photo. Never put it on a cut-out: the
   radial mask would eat the machine's own edges. */
.stage-machine.is-masked {
  -webkit-mask-image: radial-gradient(62% 62% at 50% 50%, #000 58%, transparent 92%);
          mask-image: radial-gradient(62% 62% at 50% 50%, #000 58%, transparent 92%);
  border-radius: var(--radius-lg);
}

/* LAYER 5 — the existing copy, set small at the lower left, directly on the
   scene. No panel, no card, no white rectangle. */
.stage-copy {
  position: absolute;
  left: var(--stage-inset);
  /* The shared bottom baseline. Lowering --stage-base moves this column
     and the callout row down by the SAME amount, which is what keeps the
     bottom of the poster level. */
  bottom: var(--stage-base);
  z-index: 5;
  /* Wide enough for the longest spec string (the 58-character Supreme 900
     one), but held to under a third of the viewport so it stops short of
     the centred CTA block at EVERY width, not just at 1440px. The CTA is
     centred and at most 42vw wide, so its left edge never falls below
     29vw — this column therefore cannot reach it. */
  width: min(420px, 27vw);
  max-width: calc(100vw - var(--gutter) * 2);
  text-align: left;
}
.stage-copy .cat-tag {
  font-size: var(--fs-tag);
  font-weight: var(--fw-label);
  text-transform: none;
  letter-spacing: var(--ls-label-tight);
  color: var(--blue-600);
  margin-bottom: 8px;
}
.stage-copy h3 {
  margin: 0 0 8px;
  font-size: 1.5em;
  line-height: 120%;
  color: var(--heading-color);
}
.stage-copy h3 a { color: inherit; text-decoration: none; }
.stage-copy h3 a:hover { color: var(--blue-600); }
.stage-copy .desc {
  margin: 0 0 12px;
  font-size: .94em;
  line-height: 145%;
  color: var(--ink-700);
  /* Long descriptions wrap rather than truncating. */
  overflow-wrap: break-word;
}
/* Chips wrap onto as many rows as they need; the 58-character Supreme 900
   spec is the worst case and must not push anything off screen.

   The row is pulled left by exactly one chip's horizontal padding so the
   chip's TEXT — not its faint pill outline — lands on the column rail. The
   cat-tag, the h3, the desc and the buttons are all plain text boxes whose
   ink starts at the rail; the chip is the only item here with a filled
   background, and at 10px of padding its label sat 10px inside that line
   while everything above and below it sat on it. Aligning the box made the
   ink disagree, which is what read as "slightly inward". */
.stage-copy .spec-chip-row {
  margin-bottom: 0;
  flex-wrap: wrap;
  margin-left: calc(var(--stage-chip-pad) * -1);
  max-width: calc(100% + var(--stage-chip-pad));
}
.stage-copy .spec-chip {
  white-space: normal;
  text-align: left;
  line-height: 135%;
}
/* Each line lifts in on its own beat once the machine has settled. */
.stage-copy > *,
.stage-actions { will-change: transform, opacity; }

/* LAYER 6 — the existing CTAs, unchanged in wording and destination.
   The pair is the main action area of the scene, so it sits in the LOWER
   CENTRE. It is centred against the whole section (left:0/right:0 with the
   group centred inside), not against the space left over by the copy — so
   the centre stays true however long the product name on the left runs. */
.stage-actions {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 8vh;
  z-index: 6;
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  /* The box hugs the two buttons rather than being a wide block that
     merely CENTRES them: a 42vw-wide box would start at 29vw and collide
     with the copy column below ~1400px even though the buttons themselves
     sit nowhere near it. At max-content the centred region is only as wide
     as the pills, so both side columns get real clearance. */
  width: max-content;
  max-width: min(42vw, 460px);
  margin: 0 auto;
  /* Only the active scene accepts clicks — see .is-active below. */
  pointer-events: none;
}
.stage-scene.is-active .stage-actions { pointer-events: auto; }
/* Same rule for the product link in the copy block. */
.stage-copy { pointer-events: none; }
.stage-scene.is-active .stage-copy { pointer-events: auto; }

/* LAYER 8 — previous/next product controls, one on each edge.
   These are the actual navigation. Small and refined rather than
   carousel-sized, in the existing blue/ink tokens. The row spans the full
   stage and pushes the two arrows to the far left and far right edges,
   vertically centred on the section, so neither ever sits over the copy,
   the CTAs or the callouts. The row itself is inert; only the buttons take
   the pointer. */
.stage-nav {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  /* Above .stage-scenes (z-index: 9). In the old lower-right corner the
     arrows sat outside anything the scene painted, but centred on the edge
     they cross the full-bleed wash, which would otherwise swallow the
     click. */
  z-index: 11;
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* The arrows sit ON the outer rail: this is the section's true viewport
     margin, and because the row is left:0/right:0 with space-between both
     arrows get exactly the same margin from their own edge. */
  padding: 0 var(--stage-rail);
  pointer-events: none;
}
.stage-nav button { pointer-events: auto; }
.stage-nav button {
  /* --stage-arrow feeds --stage-inset, so the text rail is always exactly
     one arrow plus a gap inside the outer rail. Change the size here and
     the copy/callout columns re-clear it automatically. */
  width: var(--stage-arrow);
  height: var(--stage-arrow);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--blue-300);
  border-radius: 50%;
  background: rgba(255, 255, 255, .55);
  color: var(--blue-600);
  cursor: pointer;
  transition: background .25s ease, border-color .25s ease, color .25s ease, transform .25s ease;
}
.stage-nav button:hover {
  background: var(--blue-600);
  border-color: var(--blue-600);
  color: #fff;
  transform: translateY(-2px);
}
.stage-nav button:focus-visible { outline: 3px solid var(--blue-300); outline-offset: 4px; }
/* At the ends of the sequence the control has nowhere to go. */
.stage-nav button[disabled] { opacity: .38; cursor: default; transform: none; }
.stage-nav button[disabled]:hover {
  background: rgba(255, 255, 255, .55);
  border-color: var(--blue-300);
  color: var(--blue-600);
}
.stage-nav svg { width: 17px; height: 17px; display: block; }

/* --- Mobile ----------------------------------------------------------
   The same poster, re-composed for portrait: the copy moves under the
   machine, the CTAs sit beneath it, and only two props survive. */
@media (max-width: 860px) {
  /* Portrait re-states the SAME four tokens rather than each layer picking
     its own number again: the rail tightens, the text rail collapses onto
     it (nothing is beside an arrow any more once the copy is centred), and
     the baseline shortens for the taller stack. */
  .section.stage {
    --stage-rail: clamp(12px, 3vw, 22px);
    --stage-arrow: 38px;
    --stage-inset: var(--stage-rail);
    --stage-top: clamp(14px, 2.6vh, 26px);
    --stage-base: clamp(22px, 4vh, 40px);
    /* Portrait stacks the copy directly UNDER the machine, so the stack
       cannot be positioned by guessed viewport fractions the way the
       side-by-side desktop layout could: at 390x844 a 25vh guess put the
       product name straight across the machine's base. These two tokens
       describe the machine's real geometry, and the blocks below are
       measured from the edge they produce. */
    /* 40vh left the machine too tall for the stack that now sits under it:
       at 390x844 the copy, the CTA pair and the callout row together need
       more room than the floor-to-machine gap 40vh left over, so the copy
       ran into the callouts. The machine gives that room back — it is the
       one element here with slack, and at 34vh it still dominates the
       portrait poster. */
    --stage-mach-h: clamp(190px, 34vh, 420px);
    --stage-mach-top: 39%;
    /* Where the machine actually ends, in px from the top of the pin. */
    --stage-mach-bottom: calc(var(--stage-mach-top) + var(--stage-mach-h) / 2);
  }
  /* Size still comes from the fitter (--fit); only the floor changes. */
  .stage-word { font-size: clamp(2rem, var(--fit), 40rem); top: 36%; }
  .stage-machine { height: var(--stage-mach-h); max-width: 86vw; top: var(--stage-mach-top); }
  /* Below this width a prop cannot sit clear of the machine, the copy and
     the dots at a size where it still reads — so it is hidden outright
     rather than shrunk to a smudge or cropped. */
  /* Stacked from the shared baseline upward: callouts, then the CTA pair,
     then the copy. Each step is one measured offset from the token, so the
     three blocks are evenly spaced instead of sitting at 3vh / 11vh / 15vh
     — three unrelated values that only happened not to collide. */
  .stage-copy {
    left: 0; right: 0;
    width: auto;
    /* The base rule caps this column at calc(100vw - --gutter * 2) for the
       left-anchored desktop layout. Here the column is a full-bleed band
       that does its own insetting with --stage-rail, so that cap has to be
       released — left over, it shaved a gutter off the band and pushed the
       whole "centred" stack off-centre by half of it. */
    max-width: none;
    padding: 0 var(--stage-rail);
    /* Anchored to the machine, not to the floor: the block starts one
       clear gap below where the machine actually ends, so it can never
       ride up over the product however short the window is. The CTA row
       and the callouts then follow it in normal order. */
    top: calc(var(--stage-mach-bottom) + clamp(10px, 2vh, 22px));
    bottom: auto;
    text-align: center;
  }
  .stage-copy h3 { font-size: 1.22em; }
  .stage-copy .desc { font-size: .9em; }
  .stage-copy .spec-chip-row { justify-content: center; }
  /* Portrait keeps the same hierarchy, stacked: copy, then the centred CTA
     pair, then the arrows beneath them. The arrows sit at the LEFT edge. */
  .stage-actions {
    left: 0; right: 0;
    bottom: calc(var(--stage-base) + clamp(92px, 15vh, 150px));
    /* No rail of its own: on mobile this row is a static child inside
       .stage-copy, which has already applied the rail. Padding it again
       inset the buttons a second time and left them narrower than the
       copy they sit under. */
    padding: 0;
    /* Back to a full-width band here: the desktop rule hugs its buttons to
       keep the centre column narrow, which would otherwise stop the pills
       from going full width when they stack below. */
    width: auto;
    max-width: none;
    justify-content: center;
  }
  /* The featured pills are sized for the desktop copy column; at tablet
     and phone widths that same size eats the band between the copy and the
     callout row. Scaled down to a compact pill that still clears the 44px
     touch target. */
  .stage-actions .btn {
    min-height: 40px;
    flex: 0 1 auto;
    padding: .55rem .95rem;
    font-size: .82rem;
    gap: .4rem;
  }
  .stage-nav {
    left: 0;
    right: 0;
    padding: 0 var(--stage-rail);
  }

}

@media (max-width: 430px) {
  /* Two pills side by side get tight here, so they stack and go full width. */
  /* Kept side by side rather than stacked full-width: at this size the two
     shrunken pills fit on one line, and the stacked pair was what pushed the
     callout row off the bottom of the scene. */
  .stage-actions { gap: 8px; }
  .stage-actions .btn {
    min-height: 36px;
    padding: .45rem .8rem;
    font-size: .76rem;
  }
  /* The pills stack here, making the copy column taller. It is anchored to
     the machine's bottom edge (see the 860px block), so it needs no bottom
     value of its own — the stack simply grows downward from that edge and
     the callout row below it gets out of the way instead. */
  .stage-copy .desc { font-size: .88em; }
}

/* --- Stillness -------------------------------------------------------
   Reduced motion, or no JS: the section stops being a pinned animation and
   becomes a plain vertical run of posters, every image, spec and CTA
   present. Nothing stays hidden behind an animation that will not run. */
@media (prefers-reduced-motion: reduce) {
  .stage-track { height: auto !important; }
  .stage-pin { position: static; height: auto; min-height: 0; max-height: none; overflow: visible; }
  /* The column layout has no pinned scene for the intro to cover, so both
     stages simply stay. The mode classes are neutralised here: there is no
     showcase to enter, so neither stage may be gated away. */
  .stage-head,
  .stage.is-intro .stage-head,
  .stage.is-products .stage-head {
    position: static;
    padding: 56px var(--gutter) 10px;
    opacity: 1 !important;
    transform: none !important;
    visibility: visible !important;
    transition: none !important;
  }
  /* The glide-up is off here, and its resting state is opacity:0 — so the
     children must be restored explicitly. The rules above only neutralise
     the block, not the lines inside it, which would otherwise leave the
     section identity blank for anyone who asked for stillness. */
  .stage-head > *,
  .stage.is-intro .stage-head > *,
  .stage.is-products .stage-head > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
    transition-delay: 0s !important;
  }
  .stage.is-intro .stage-scenes,
  .stage.is-products .stage-scenes {
    opacity: 1 !important;
    visibility: visible !important;
  }
  /* The slide-up is off here, and its parked state is one viewport DOWN —
     so the sheet must be put back, or the whole column of posters would
     start a screen below where it belongs. z-index goes too: with nothing
     sliding over anything, the scenes are simply the next block in flow. */
  .stage-scenes,
  .stage.is-intro .stage-scenes,
  .stage.is-products .stage-scenes {
    position: static;
    z-index: auto;
    transform: none !important;
    transition: none !important;
  }
  .stage-scene {
    position: static;
    visibility: visible;
    opacity: 1 !important;
    min-height: 78vh;
    padding: 40px 0 56px;
  }
  .stage-wash { display: none; }
  .stage-word {
    position: static;
    transform: none;
    font-size: clamp(2rem, 9vw, 4rem);
    text-align: center;
    margin: 0 auto 10px;
    width: auto;
    white-space: normal;
  }
  .stage-word span { opacity: 1 !important; transform: none !important; }
  .stage-machine {
    position: static;
    display: block;
    margin: 0 auto;
    transform: none !important;
    filter: none !important;
    opacity: 1 !important;
    height: auto;
    max-height: 46vh;
  }
  .stage-copy,
  .stage-actions {
    position: static;
    width: auto;
    max-width: 620px;
    margin: 18px auto 0;
    padding: 0 var(--gutter);
    text-align: center;
    opacity: 1 !important;
    transform: none !important;
  }
  .stage-copy > * { opacity: 1 !important; transform: none !important; }
  .stage-copy .spec-chip-row { justify-content: center; }
  .stage-actions { justify-content: center; }
  /* The section is a plain column here — there is no timeline left to
     scrub, so neither control has anything to drive. */
  .stage-nav { display: none; }
}
html:not(.js) .stage-track { height: auto; }
html:not(.js) .stage-pin { position: static; height: auto; min-height: 0; max-height: none; overflow: visible; }
html:not(.js) .stage-head {
  position: static; padding: 56px var(--gutter) 10px;
  opacity: 1; visibility: visible; transform: none;
}
html:not(.js) .stage-head > * { opacity: 1; transform: none; }
html:not(.js) .stage-scenes { position: static; opacity: 1; visibility: visible; }
html:not(.js) .stage-scene { position: static; visibility: visible; min-height: 0; padding: 40px 0 56px; }
html:not(.js) .stage-wash,
html:not(.js) .stage-nav { display: none; }
html:not(.js) .stage-word {
  position: static; transform: none; width: auto; white-space: normal;
  font-size: clamp(2rem, 9vw, 4rem); text-align: center; margin: 0 auto 10px;
}
html:not(.js) .stage-machine {
  position: static; display: block; margin: 0 auto; transform: none;
  height: auto; max-height: 46vh; filter: none;
}
html:not(.js) .stage-copy,
html:not(.js) .stage-actions {
  position: static; width: auto; max-width: 620px; margin: 18px auto 0;
  padding: 0 var(--gutter); text-align: center;
}
html:not(.js) .stage-actions { justify-content: center; }

/* ==========================================================================
   Journey — "From Grain to Fresh Flour"
   --------------------------------------------------------------------------
   A SCROLL-DRIVEN SLIDESHOW, after the mechanism hexpens.com uses on its
   home page.

   The shape of it: two layers share one section.

   .journey-stage is position:sticky, one viewport tall, and holds the three
   photographs stacked on top of each other — each absolutely placed,
   filling the frame, object-fit:cover. .journey-body is pulled back OVER
   that stage with margin-top:-100svh and holds three 100svh text panels.
   So the section is three screens tall, the copy scrolls normally, and the
   picture layer stays pinned behind it for the whole run.

   Stacking: photograph 0 sits on top, 3 at the bottom, via
   z-index: calc(var(--journey-bg-index) * -1). Each one then animates
   ITSELF OUT across its own one-screen slice of the timeline — fading,
   scaling up to 1.2 and blurring to 20px — uncovering the next. The last
   never animates: it is the floor the run settles onto.

   Two engines, same visual vocabulary:

   - Modern browsers: a CSS view-timeline on the section, with each
     photograph's animation-range carved out of it by index. Entirely
     scroll-linked, scrubs both directions, no JavaScript involved.

   - Browsers without animation-timeline: initJourney() in main.js watches
     the text panels with an IntersectionObserver and writes the active
     index to --journey-active-item. The same three properties then
     transition between two discrete states instead of scrubbing.

   Every colour is an existing token. No new palette.
   ========================================================================== */
.journey-section {
  --journey-active-item: 0;
  --journey-timeline: --journey-scroll;
  /* The timeline the photographs animate against: the section's own
     progress through the viewport, measured on the block axis. */
  /* How much scrolling one step costs. A full 100svh per panel — four
     screens for the section — made the run feel like work: the visitor
     has read a step long before the scroll that clears it is finished.
     At 62svh a step still gets a deliberate beat of its own but the
     whole section is under two and a half screens instead of four.

     The stage stays a full 100svh; only the TRAVEL shortens. That is the
     whole trick — the picture keeps filling the screen while the copy
     panel that drives it is shorter than one. */
  --journey-step: 62svh;
  view-timeline-name: var(--journey-timeline);
  view-timeline-axis: block;
  position: relative;
  /* clip rather than hidden — hidden on the ancestor would defeat the
     sticky stage, clip does not create a scroll container. */
  overflow-x: clip;
  /* The section's own height comes entirely from .journey-body's three
     panels, so the vertical padding every other .section carries would
     just add dead space above the first frame. */
  padding-top: 0;
  padding-bottom: 0;
  /* Undoes the fixed one-viewport lock the mosaic version needed. */
  height: auto;
  min-height: 0;
  max-height: none;
}

/* ---- The pinned picture layer ---- */
.journey-stage {
  height: 100svh;
  position: sticky;
  top: 0;
  z-index: 1;
}
.journey-stage-inner {
  height: 100%;
  position: relative;
}

.journey-bg {
  --journey-bg-index: 0;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* 0 on top, then -1, -2, -3 — so the first photograph is the one the
     visitor meets and each fade-out reveals the next one down. */
  z-index: calc(var(--journey-bg-index) * -1);
  /* Promotes the layer and stops the blur+scale from flickering while it
     composites over the one beneath. */
  backface-visibility: hidden;
  background: var(--canvas);
}
.journey-bg img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The subject sits to one side in each frame and the opposite side is
     clean gradient the copy sits on, so the crop is held to centre and
     the composition survives at any viewport ratio. */
  object-position: center center;
}

/* The fade-out itself: the photograph recedes as it leaves — pulling back
   in scale and softening — rather than simply dissolving. */
@keyframes journey-bg-change {
  from { opacity: 1; scale: 1;   filter: blur(0); }
  to   { opacity: 0; scale: 1.2; filter: blur(20px); }
}

/* Engine A — scroll-driven animations. Each photograph owns the slice of
   the timeline between its own index and the next, so the crossfade is
   scrubbed by the scroll position itself. */
@supports (animation-timeline: view()) {
  .journey-bg {
    animation: journey-bg-change forwards;
    animation-timeline: var(--journey-timeline);
    animation-range: contain calc(var(--journey-bg-index) * var(--journey-step))
                     contain calc((var(--journey-bg-index) + 1) * var(--journey-step));
  }
  /* The floor. Nothing is underneath it, so fading it out would expose
     the section background. */
  .journey-bg:last-child { animation: none; }
}

/* Engine B — the fallback. --journey-active-item is written by main.js;
   this resolves it to a 0/1 per layer and transitions between the two
   endpoints of the same keyframe. */
@supports not (animation-timeline: view()) {
  .journey-bg {
    --journey-bg-active: clamp(0, calc(var(--journey-bg-index) + 1 - var(--journey-active-item)), 1);
    transform: translateZ(0) scale(calc(1.2 - var(--journey-bg-active) * .2));
    filter: blur(calc(20px - var(--journey-bg-active) * 20px));
    opacity: var(--journey-bg-active);
    transition-property: opacity, transform, filter;
    transition-duration: .4s;
    transition-timing-function: ease-in-out;
  }
  .journey-bg:last-child {
    opacity: 1;
    transform: none;
    filter: none;
  }
}

/* ---- The copy layer ----
   Lifted back over the stage so the two occupy the same screen, and given
   the higher stacking order so the text is never behind a photograph. */
.journey-body {
  /* Pulls the copy layer back over the sticky stage. Still a full
     viewport, because that is the stage's height — not --journey-step. */
  margin-top: -100svh;
  position: relative;
  z-index: 2;
}

/* A panel is now SHORTER than the screen it is read on, so it cannot
   simply centre its own copy — that would put each block 62svh apart
   while the frame behind it is 100svh tall, and the text would sit
   progressively higher in the picture on each step.

   Instead the panel is the scroll BUDGET and the inner is the stage: the
   inner is pinned one full viewport tall and centres the copy in the
   screen, while the panel's shorter height is what decides how much
   scrolling the step costs. */
.journey-panel {
  height: var(--journey-step);
  display: flex;
  align-items: flex-start;
}
/* The LAST panel gets a full screen rather than a step's budget.

   Every other panel can be short because the one after it supplies the
   scrolling that carries it across the screen. The last has nothing
   following it, so at --journey-step the section ended 100svh - 62svh
   before its own copy reached the middle: the sticky stage hit the
   bottom of the section and unstuck, and the final frame slid up the
   screen with blank page beneath it instead of resting like the other
   three. A full viewport here is exactly the runway that was missing. */
.journey-panel:last-child {
  height: 100svh;
  position: relative;
}

/* ---- The seam into Two Ranges ----
   The sticky stage unpins exactly at this section's bottom edge, so the
   last photograph is cut off there by a hard horizontal line with the
   white heading band below it. This feathers that edge: a white-to-clear
   wash sitting over the LAST stretch of the final panel, so the picture
   dissolves into the band instead of ending at a rule.

   On the last panel rather than on the section, because the section's own
   ::after would have to clear the copy layer (z-index 2) and would then
   also sit over the text. Anchored to the panel's bottom, it covers only
   the final screen — where the cut actually is. */
.journey-panel:last-child::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  /* Deep enough to read as a dissolve rather than a soft-edged line. */
  height: 42svh;
  /* Above the photograph (the stage is z-index 1) but BELOW the copy
     layer (.journey-body is z-index 2) — the wash is there to dissolve
     the picture, not to grey out step four's text and its CTA, which
     must stay fully legible as the section ends. The panel is inside
     .journey-body, so a z-index here orders it against its siblings
     within that layer; 0 keeps it behind the copy in the same panel. */
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(to bottom,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, .35) 45%,
    rgba(255, 255, 255, .8) 75%,
    var(--white) 100%);
}
.journey-panel-inner {
  display: flex;
  width: 100%;
  /* One viewport tall regardless of the panel's budget, pinned so it
     holds the screen for the length of its step, and centred so the copy
     lands in the middle of the picture rather than riding high in it.

     Sticky rather than a negative margin: the panel is shorter than this
     inner, so a margin would just shift every block up by the same
     amount and leave them all off-centre together. Sticky lets the inner
     hold at the top of the viewport — where it is exactly as tall as the
     screen — through its panel's whole travel. */
  height: 100svh;
  align-items: center;
  position: sticky;
  top: 0;
}
/* The photographs alternate which side the machine stands on, so the copy
   alternates to meet the clean half of each frame. */
.journey-panel--left  .journey-panel-inner { justify-content: flex-start; }
.journey-panel--right .journey-panel-inner { justify-content: flex-end; }
/* Pulled in off the gutter, towards the middle of the frame.

   Each photograph puts its subject on one side and leaves the other as
   clean gradient, and the copy was sitting at the far edge of that clean
   half — hard against the page gutter, with the whole centre of the
   screen left empty between the words and the machine. This pushes the
   block inwards so it sits INSIDE the open space rather than at its
   outer rim, closing that gap.

   It is an inset on the copy rather than narrower gutters on the
   container, so the section keeps the same page margins as every other
   section and only this block moves. */
.journey-panel--left  .journey-copy { margin-left: clamp(0px, 7vw, 132px); }
/* The right-hand panels need more of an inset than the left ones. Their
   photographs put the machine hard against the LEFT edge, so the open
   half runs from roughly mid-frame all the way to the right gutter —
   twice the empty ground of a left-hand panel, and the copy was sitting
   at the far end of it. */
.journey-panel--right .journey-copy { margin-right: clamp(0px, 11vw, 210px); }
/* On the right-hand panels the block is flush to the gutter, so the copy
   is set from that edge too — ranged left inside a right-hand column
   leaves a ragged edge floating in the middle of the frame with nothing
   to hold it. */
.journey-panel--right .journey-copy {
  align-items: flex-end;
  text-align: right;
}

.journey-copy {
  max-width: 34rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
}
/* Steps 02-04 carry one short sentence each, so their blocks were
   collapsing to their own text width — barely 350px against panel 01's
   544px — which left them looking stranded in the middle of the frame
   whatever the margin. Giving the block a floor makes all four occupy
   roughly the same column, so the copy reads as one consistent band
   down the section rather than four differently-sized scraps. */
.journey-panel:not(:first-child) .journey-copy {
  min-width: min(28rem, 34vw);
}
.journey-copy h2,
.journey-copy h3 {
  margin: 0;
}
/* Panels 02-04 are headed by an h3 rather than the section's own h2, but
   they play the same role on screen — one step title per full-screen
   frame, under an eyebrow with copy beneath.

   That is exactly .range-content-inner h3's situation a section further
   down, so this takes the same treatment rather than inventing one: the
   shared heading weight, leading and tracking, and a fluid size that
   tracks the column. The earlier version set the display serif at weight
   400 with display leading, which made these titles the only headings on
   the page that were large but not bold — they read as washed out beside
   the 700 h3s in Two Ranges and Shop By Need. */
.journey-copy h3 {
  font-size: clamp(1.9em, 3.2vw, 2.6em);
  font-weight: var(--fw-title);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
}
/* Panel 01 is the only frame carrying BOTH the section's h2 and a step
   title, so its h3 steps down to sit under that h2 instead of rivalling
   it — at the shared size the two read as competing headings stacked on
   one screen. The other panels have the frame to themselves and keep the
   full size above. */
.journey-panel:first-child .journey-copy h3 {
  font-size: var(--fs-h4);
}
/* On a phone the h2 has already stepped down to 1.8em while the clamp
   above is still on its 1.9em floor, which inverts the two — a step
   title ends up LARGER than the section heading. Below 600px the step
   titles drop under it again. */
@media (max-width: 600px) {
  .journey-copy h3,
  .journey-panel:first-child .journey-copy h3 { font-size: 1.5em; }
}
/* --fs-small, not --fs-lead. The lead token is 20px, but every section
   head on this page overrides it down to --fs-small — see the
   .need-section / .ranges-section rule beside .section-head p — so a
   20px lead here was the one paragraph on the home page setting larger
   than its neighbours rather than the system's intended size. */
.journey-copy .journey-lead {
  margin: 0;
  max-width: 44ch;
  font-size: var(--fs-small);
  font-weight: var(--fw-body);
  line-height: var(--lh-body);
  color: var(--ink-700);
}
/* The step sentences are the same job as a card's description elsewhere
   on the page (.why-item p, .range-content p), so they take the same
   size rather than the larger running-prose --fs-body. */
.journey-text {
  margin: 0;
  max-width: 46ch;
  font-size: var(--fs-small);
  line-height: var(--lh-body);
  color: var(--ink-700);
}

/* The step marker: a small capitalised chip carrying the number, so the
   sequence stays legible now that the connector arcs are gone. */
/* The step marker does the same job as an .eyebrow — a small label above
   a title — so it is sized and weighted from the same tokens rather than
   from one-off rem values. It also drops the uppercase transform: the
   eyebrow rule above deliberately gave that up because it made every
   section "open with a shouted label", and this chip reintroduced it. */
.journey-step {
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-label);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-label);
  color: var(--brand-primary);
}
/* The number itself is the one thing here allowed to out-rank its label,
   since it is what carries the sequence. */
.journey-step-num {
  font-size: var(--fs-h6);
  font-weight: var(--fw-strong);
  letter-spacing: 0;
}
.journey-cta {
  margin-top: 8px;
}

/* Inline photograph for the stacked phone/tablet layout only; on desktop
   the pinned .journey-stage carries the pictures. */
.journey-photo { display: none; }

/* ---- Entrance of the copy ----
   Each panel's text rises and settles as its own frame comes forward, so
   the words arrive with the photograph rather than sitting there waiting
   for it.

   Both the entrance and the exit are scrubbed against ONE timeline per
   panel, named on the panel itself and inherited by everything inside it.

   That naming is the whole point. An anonymous view() on each element
   would track THAT ELEMENT's own box, and these elements are short — a
   heading is a couple of lines tall, so its entry range is a couple of
   lines of scrolling and it slams from invisible to finished almost
   instantly, long before the section has settled. Measured against the
   panel, every element shares the same full-screen travel, so the copy
   moves at the pace of the frame it belongs to.

   The fallback simply shows the copy: without scroll-driven animation
   there is nothing to scrub against, and a half-finished entrance is
   worse than none. */
@supports (animation-timeline: view()) {
  .journey-panel {
    view-timeline-name: --journey-panel;
    view-timeline-axis: block;
  }

  /* A shorter lift and a lighter blur than the first pass. Over a long
     travel a big move reads as considered; over this shorter one it read
     as a lurch, because the same distance is now covered in less
     scrolling. Less distance, less softening — the copy settles rather
     than flies in. */
  @keyframes journey-copy-in {
    from { opacity: 0; transform: translateY(16px); filter: blur(3px); }
    to   { opacity: 1; transform: translateY(0);    filter: blur(0); }
  }
  .journey-copy > * {
    animation: journey-copy-in both;
    animation-timeline: --journey-panel;
    /* Eased rather than linear. A scroll-driven animation runs at whatever
       speed the hand does, so a linear curve starts and stops abruptly at
       both ends of its range — the jerk you feel at the edges. ease-out
       puts the speed at the START, where the scroll is carrying it, and
       lets it glide into place. */
    animation-timing-function: ease-out;
    /* cover is the panel's whole passage across the viewport. This opens
       as it comes forward and is finished well before it is centred, so
       the copy is settled and still by the time it is being read. */
    animation-range: cover 22% cover 38%;
  }
  /* A short stagger down the block, so the chip, the title and the
     sentence arrive in reading order rather than as one slab.

     The whole run has to be FINISHED by 50% — the point at which the
     panel is centred and at rest. Overrunning that leaves the last lines
     fading in while the frame is already sitting still in front of the
     reader, which reads as the copy lagging the page rather than
     arriving with it. The steps are 2% so all six fit inside that. */
  .journey-copy > *:nth-child(2) { animation-range: cover 24% cover 40%; }
  .journey-copy > *:nth-child(3) { animation-range: cover 26% cover 42%; }
  .journey-copy > *:nth-child(4) { animation-range: cover 28% cover 44%; }
  .journey-copy > *:nth-child(5) { animation-range: cover 30% cover 46%; }
  .journey-copy > *:nth-child(6) { animation-range: cover 32% cover 48%; }

  /* And the exit. The panels scroll while the pictures behind them are
     pinned, so without this a block of copy is still legible over the
     TOP of the frame while the next photograph is already fading in
     underneath it — the two layers come apart and two steps are readable
     at once. Fading the whole block out as its panel leaves keeps one
     step on screen at a time, and matches the way the photograph it
     belongs to goes: up, softening, out of the way.

     This rides on .journey-copy itself rather than its children so the
     block leaves as one piece, and it composes with the per-child
     entrance above instead of fighting it. */
  @keyframes journey-copy-out {
    from { opacity: 1; transform: translateY(0);     filter: blur(0); }
    to   { opacity: 0; transform: translateY(-22px); filter: blur(4px); }
  }
  .journey-copy {
    animation: journey-copy-out both;
    animation-timeline: --journey-panel;
    /* ease-in on the way out, the mirror of the entrance: the block holds
       still at first and gathers speed as it goes, rather than starting
       to drift the instant the range opens. */
    animation-timing-function: ease-in;
    /* Tracks the photograph it sits on. That picture fades across the
       back half of the panel's travel, so the copy leaves on the same
       stretch rather than ahead of it — an earlier exit left the frame
       holding a step's image with its words already gone. Still clear of
       the next panel's opening at 22%, since that is measured on the NEXT
       panel's own timeline. */
    animation-range: cover 66% cover 92%;
  }
}

/* ---- Phones ----
   The pinned stage costs four screens of scrolling and the copy has no
   clean half of the frame to sit in once the crop narrows, so below 860px
   the section becomes what it describes: four photographs, each with its
   step underneath it. No pin, no crossfade, nothing hidden behind an
   animation that will not run. */
@media (max-width: 860px) {
  .journey-section {
    view-timeline-name: none;
    /* No top pad. The section above already closes on its own bottom
       padding, so adding a second one here opened a band of empty page
       between them — the white gap that showed above the first
       photograph on phones and tablets. The bottom pad stays: nothing
       follows it that supplies its own. */
    padding-top: 0;
    padding-bottom: var(--section-pad-y);
  }
  .journey-stage { display: none; }
  .journey-body { margin-top: 0; }
  /* Including the last one: the full-viewport rule above exists only to
     give the sticky stage runway, and there is no stage here — stacked,
     it would just hang an empty screen off the end of the section. */
  .journey-panel,
  .journey-panel:last-child {
    height: auto;
    display: block;
  }
  .journey-panel + .journey-panel { margin-top: clamp(40px, 7vw, 72px); }
  /* The desktop seam wash feathers the pinned photograph into Two Ranges.
     Stacked there is no pinned photograph under it, so it just sat on
     top of step 03's copy and CTA as a washed-out white box. */
  .journey-panel:last-child::after { content: none; }
  /* Instead, the section's own bottom padding fades to white so it meets
     the white Two Ranges band without a hard grey edge. */
  .journey-section {
    background-image: linear-gradient(to bottom, transparent calc(100% - var(--section-pad-y) - 40px), var(--white));
  }
  /* The inner is the pinned stage on desktop — a sticky, one-viewport-tall
     box. Stacked, there is nothing to pin it against, and leaving it at
     100svh gave every step a screen of empty space beneath it and pushed
     the section past five screens. Back to a plain block that is as tall
     as its content. */
  .journey-panel-inner {
    display: block;
    position: static;
    height: auto;
  }
  .journey-copy { max-width: none; }
  /* Stacked, every step is a full-width block, so the right-hand panels'
     flush-right setting has no column to hold it — it just left a ragged
     edge mid-screen on steps 02 and 04. All four read from the same edge
     here, and the desktop gutter insets go with it. */
  .journey-panel--left  .journey-copy,
  .journey-panel--right .journey-copy {
    align-items: flex-start;
    text-align: left;
    margin-left: 0;
    margin-right: 0;
  }
  /* Each panel gets its own photograph back, inline above the copy. It is
     a real <img> inside .journey-photo (hidden on desktop) rather than a
     background, so the frame and the picture inside it can move apart. */
  .journey-photo {
    display: block;
    position: relative;
    overflow: hidden;
    width: 100%;
    aspect-ratio: 16 / 10;
    margin: 0 0 20px;
    border-radius: 16px;
  }
  .journey-photo img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* Panel 01 carries the section's own title as well as step 01. Stacked,
     its photograph sat above both, so the section opened on a picture with
     the heading underneath it. Here the title block leads, then each step
     follows with its photograph above it, like steps 02 and 03.

     The inner and copy wrappers dissolve (display: contents) so the
     photograph and the copy's children become siblings that can be
     reordered; the container's gutter is put back on the copy items. */
  .journey-panel:first-child {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 14px;
  }
  .journey-panel:first-child .journey-panel-inner,
  .journey-panel:first-child .journey-copy { display: contents; }
  .journey-panel:first-child .journey-copy > * {
    order: 2;
    margin-inline: max(var(--gutter), (100% - var(--container)) / 2 + var(--gutter));
  }
  .journey-panel:first-child .journey-copy > .eyebrow,
  .journey-panel:first-child .journey-copy > h2,
  .journey-panel:first-child .journey-copy > .journey-lead { order: 0; }
  .journey-panel:first-child .journey-photo {
    order: 1;
    margin: 18px 0 6px;
  }

  /* The desktop entrance and exit are scrubbed against a panel that owns a
     whole screen. Stacked, a panel is a fraction of that, so those ranges
     would leave copy half-faded while it sits still in front of the reader,
     and the exit would dim a step that is merely further up a list. Both
     are cancelled here and replaced by the reveal below. */
  .journey-copy,
  .journey-copy > * {
    animation: none;
    opacity: 1;
    transform: none;
    filter: none;
  }

  /* ---- The animation, kept for phones and tablets ----
     Time-based, not scroll-scrubbed: main.js marks the section
     .is-journey-anim and adds .is-journey-in to each panel as it comes on
     screen, and everything below plays from those two classes. That works
     in every browser (iOS Safari before 26 has no animation-timeline, which
     is why the old scrubbed version never ran there), and it also plays for
     step 01, which is already on screen when the page loads. Without JS
     the classes never appear and the section is simply shown.

     Per panel, in order:
       1. the frame wipes open from the panel's own side (left panels from
          the left, right panels from the right),
       2. the photograph settles out of a zoom, then keeps a slow drift so
          the picture stays alive while it is read,
       3. a single light sweep crosses it,
       4. the copy rises in a stagger, sliding in from the same side,
       5. the step number pops and a short accent line draws under the title. */
  .journey-section.is-journey-anim .journey-photo {
    clip-path: inset(0 100% 0 0 round 16px);
    transition: clip-path 1.1s cubic-bezier(.77, 0, .18, 1);
  }
  .journey-section.is-journey-anim .journey-panel--right .journey-photo {
    clip-path: inset(0 0 0 100% round 16px);
  }
  .journey-section.is-journey-anim .journey-photo img {
    scale: 1.28;
    transition: scale 1.8s cubic-bezier(.22, .61, .36, 1);
    will-change: scale, translate;
  }
  .journey-section.is-journey-anim .journey-photo::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(105deg, transparent 35%, rgba(255, 255, 255, .45) 50%, transparent 65%);
    transform: translateX(-120%);
    pointer-events: none;
  }

  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-photo {
    clip-path: inset(0 0 0 0 round 16px);
  }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-photo img {
    scale: 1;
    /* Uses the separate translate property so it never fights the scale
       transition above. Starts once the zoom has mostly landed. */
    animation: journey-drift 9s ease-in-out 1.6s infinite alternate;
  }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-photo::after {
    animation: journey-sweep 1.2s ease-out .9s both;
  }
  @keyframes journey-drift {
    from { translate: 0 0; }
    to   { translate: -3% -2%; }
  }
  @keyframes journey-sweep {
    from { transform: translateX(-120%); }
    to   { transform: translateX(120%); }
  }

  /* The copy: hidden and offset toward the panel's side until it is in. */
  .journey-section.is-journey-anim .journey-copy > * {
    opacity: 0;
    transform: translate(-28px, 16px);
    transition: opacity .7s ease, transform .9s cubic-bezier(.22, .61, .36, 1);
  }
  .journey-section.is-journey-anim .journey-panel--right .journey-copy > * {
    transform: translate(28px, 16px);
  }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-copy > * {
    opacity: 1;
    transform: none;
  }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-copy > *:nth-child(1) { transition-delay: .35s; }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-copy > *:nth-child(2) { transition-delay: .45s; }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-copy > *:nth-child(3) { transition-delay: .55s; }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-copy > *:nth-child(4) { transition-delay: .65s; }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-copy > *:nth-child(5) { transition-delay: .75s; }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-copy > *:nth-child(6) { transition-delay: .85s; }

  /* Step number pops once its row has arrived. */
  .journey-section.is-journey-anim .journey-step-num {
    display: inline-block;
    transform: scale(.4);
    opacity: 0;
    transition: transform .6s cubic-bezier(.34, 1.56, .64, 1), opacity .3s ease;
  }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-step-num {
    transform: none;
    opacity: 1;
    transition-delay: .7s;
  }

  /* Accent line drawn under each step title. */
  .journey-section.is-journey-anim .journey-copy h3 {
    background: linear-gradient(var(--brand-primary), var(--brand-primary)) no-repeat left bottom / 0 3px;
    padding-bottom: 8px;
    transition: opacity .7s ease, transform .9s cubic-bezier(.22, .61, .36, 1), background-size .8s cubic-bezier(.22, .61, .36, 1);
  }
  .journey-section.is-journey-anim .journey-panel.is-journey-in .journey-copy h3 {
    background-size: 56px 3px;
  }
}

/* ---- Stillness ----
   Reduced motion keeps the layout — the pinned stage is structure, not
   decoration — but nothing fades, scales, blurs or rises. The first
   photograph simply stays put and the copy is present from the start. */
@media (prefers-reduced-motion: reduce) {
  .journey-bg {
    animation: none !important;
    transition: none !important;
    transform: none !important;
    filter: none !important;
  }
  .journey-copy > *,
  .journey-step-num,
  .journey-photo,
  .journey-photo img {
    animation: none !important;
    transition: none !important;
    opacity: 1 !important;
    transform: none !important;
    scale: none !important;
    translate: none !important;
    clip-path: none !important;
    filter: none !important;
  }
  .journey-photo::after { display: none !important; }
}

/* ==========================================================================
   Service showcase  (index.html — .services)
   --------------------------------------------------------------------------
   A PINNED HORIZONTAL RUN, after the reference site's Chapter III.

   The shape of it: .services-track is a tall block that gives the section
   its scroll travel. .services-pin is a sticky child one viewport tall, so
   it holds the screen while the track scrolls past underneath. Inside,
   .services-rail is a flex row wider than the screen, and initServices()
   translates it LEFT in step with the track's progress — which is what
   brings each card in from the RIGHT as the visitor scrolls DOWN.

   Nothing here steals the wheel. The rail is a pure function of scroll
   position, so Lenis keeps the scroll and the section cannot trap anyone
   the way an input-capturing pin can.

   Per card, the reveal is the reference's: a wash-and-image layer masked by
   clip-path inset(100% 0 0 0) — fully closed from the bottom — opening to
   inset(0) while the image counter-moves a few percent against it. The
   picture reads as uncovered in place rather than slid in.

   Below 768px, and under reduced motion, the rail is a plain vertical
   stack with every shot already open: no pin, no travel, nothing hidden
   behind an animation that will not run.

   Every colour is an existing token. No new palette.
   ========================================================================== */

.services {
  position: relative;
  background: var(--blue-700);
  color: var(--white);
  /* The pinned child is clipped by any ancestor that establishes a scroll
     container, so this one must not become one. */
  overflow: visible;
}

/* Height is written by JS from the rail's overflow — one screen to hold,
   plus the travel itself. This fallback keeps the section usable in the
   moment before JS runs. */
.services-track {
  position: relative;
  height: 300vh;
}

.services-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  /* The visible frame, matched to the hero: same 100svh unit and the same
     620px floor (.hero { height: 100svh; min-height: 620px }), so a short
     window cannot make this pin shorter than the hero. The TRACK height
     above is untouched — scroll duration and animation timing are
     unchanged; only the frame the pin paints into is pinned. */
  height: 100svh;
  min-height: max(100svh, 620px);
  max-height: max(100svh, 620px);
  overflow: hidden;
  display: flex;
  align-items: stretch;
  isolation: isolate;
}

/* The rail is wider than the screen; JS moves it. will-change keeps the
   transform on its own layer — without it the four cover images repaint
   the whole rail on every frame. */
.services-rail {
  display: flex;
  align-items: stretch;
  height: 100%;
  /* No trailing gutter. Travel is rail.scrollWidth - pin.clientWidth, so
     any padding here parks that much bare band to the right of the last
     card at the end of the run — a dead strip beside card 04. With the
     padding gone the last card's own right edge IS the rail's, and it
     lands flush against the screen. */
  transform: translate3d(0, 0, 0);
  will-change: transform;
}

/* ---- The heading, first panel on the rail -----------------------------
   It travels with the cards rather than sitting above them, so the run
   reads as one continuous band the way the reference's does. */
.services-intro {
  flex: 0 0 auto;
  /* Half the viewport, so the first card's left edge lands on the same
     line as the Two Ranges video's left edge above — that split is driven
     by --split:50vw, so the same unit is what matches it. (A percentage
     would resolve against .services-rail, whose width is content-driven,
     not against the pin.) initServices derives travel from
     rail.scrollWidth, so the run adapts on its own. */
  width: 50vw;
  display: flex;
  flex-direction: column;
  /* Label at the top, title block at the foot, the middle deliberately
     empty — the reference's proportion. */
  justify-content: space-between;
  padding: calc(var(--header-h) + clamp(18px, 3vh, 40px)) clamp(32px, 4vw, 80px) clamp(28px, 5vh, 64px) var(--gutter);
}
.services-intro-foot { display: block; }
.services-intro .eyebrow {
  margin: 0;
  align-self: flex-start;
  color: var(--blue-200);
  background: rgba(255, 255, 255, .10);
}
/* INTENTIONAL EXCEPTION — display, not a section heading.
   This is the first panel of a full-viewport pinned horizontal rail: it
   occupies a whole screen and travels with the cards, so it plays the
   hero's role in its own section rather than labelling a block of copy.
   It is therefore tuned to the HERO (700, near-1 leading, -.035em
   tracking), which is exactly the benchmark this pass works from — the
   same design language at display scale, not a fourth H2 treatment. */
.services-intro h2 {
  margin: 0;
  font-size: clamp(2.5rem, 1.1rem + 5.2vw, 6rem);
  /* A full-viewport pinned panel playing the hero's role, so it takes the
     hero face at 400 -- Marcellus has no bold to inherit. */
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  line-height: 96%;
  letter-spacing: var(--ls-display);
  color: var(--white);
}
.services-intro p {
  margin: clamp(12px, 2vh, 20px) 0 0;
  max-width: 44ch;
  font-size: .875em;
  line-height: 1.6;
  color: rgba(255, 255, 255, .72);
}

/* ---- A card -----------------------------------------------------------
   Portrait panels, square-cornered: each is a window cut into the band,
   not a card floating on it. */
.service-card {
  position: relative;
  flex: 0 0 auto;
  width: clamp(300px, 30vw, 440px);
  display: grid;
  /* number / spring / title / spring / copy — the two springs are what put
     the title on the card's middle and hold the copy at its foot. */
  grid-template-rows: auto 1fr auto 1fr auto;
  padding: calc(var(--header-h) + clamp(14px, 2.4vh, 30px)) clamp(22px, 2.2vw, 38px) clamp(24px, 4vh, 48px);
  border-left: 1px solid rgba(255, 255, 255, .22);
  text-decoration: none;
  color: var(--white);
  background: var(--blue-700);
  overflow: hidden;
  isolation: isolate;
}

/* The masked layer. Closed from the bottom at rest — inset(100% 0 0 0)
   leaves nothing visible — and opened by the hover/focus rules below. */
.service-bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  clip-path: inset(100% 0% 0% 0%);
  transition: clip-path .8s cubic-bezier(.16, 1, .3, 1);
}
/* A wash over the photograph: the number and title sit on top of it and
   have to stay legible against whatever the shot happens to be. */
.service-wash {
  position: absolute;
  inset: 0;
  z-index: 1;
  /* Sits over the photograph once the mask opens, so the number, title and
     copy stay legible on top of whatever the shot happens to be. It is a
     constant: the MASK is what reveals, exactly as the reference does it. */
  background: rgba(15, 13, 12, .55);
}
/* Counter-moves against the mask. The image is oversized (scale 1.1) so
   the movement never walks a hard edge into view. */
.service-media {
  display: block;
  width: 100%;
  height: 100%;
  transform: translate3d(0, 6%, 0);
  transition: transform .8s cubic-bezier(.16, 1, .3, 1);
}
.service-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 42%;
  display: block;
  transform: scale(1.1);
}

.service-card:hover .service-bg,
.service-card:focus-visible .service-bg { clip-path: inset(0% 0% 0% 0%); }
.service-card:hover .service-media,
.service-card:focus-visible .service-media { transform: translate3d(0, -3%, 0); }

.service-num {
  font-size: clamp(2rem, 1.2rem + 2.4vw, 3.4rem);
  font-weight: var(--fw-title);
  line-height: 1;
  letter-spacing: -.024em;
  color: rgba(255, 255, 255, .55);
  transition: color .5s ease;
}
.service-card:hover .service-num,
.service-card:focus-visible .service-num { color: var(--white); }

/* Link cue: a circled arrow in the number's row, so every panel reads as
   clickable before it is hovered. It fills and turns to point forward on
   hover/focus. */
.service-arrow {
  grid-row: 1;
  grid-column: 1;
  justify-self: start;
  align-self: center;
  display: inline-grid;
  place-items: center;
  width: clamp(44px, 3.4vw, 56px);
  height: clamp(44px, 3.4vw, 56px);
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, .55);
  color: var(--white);
  transition: background-color .4s ease, border-color .4s ease, color .4s ease;
}
.service-arrow svg {
  width: 45%;
  height: 45%;
  transition: transform .5s cubic-bezier(.16, 1, .3, 1);
}
.service-card:hover .service-arrow,
.service-card:focus-visible .service-arrow {
  background: var(--white);
  border-color: var(--white);
  color: var(--blue-700);
}
.service-card:hover .service-arrow svg,
.service-card:focus-visible .service-arrow svg { transform: rotate(45deg); }

.service-num   { grid-row: 1; }
.service-title { grid-row: 3; }
.service-copy  { grid-row: 5; }
.service-title {
  display: block;
  font-size: clamp(1.25rem, .95rem + 1.1vw, 1.9rem);
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  line-height: 118%;
  letter-spacing: var(--ls-display);
  color: var(--white);
}
.service-title--yt {
  display: flex;
  align-items: center;
  gap: .45em;
}
.service-yt-icon {
  flex: none;
  width: 1.3em;
  height: auto;
}
.service-copy {
  display: block;
  font-size: .9em;
  line-height: 145%;
  color: rgba(255, 255, 255, .74);
}

.service-card:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: -6px;
}

/* ---- Entrance ---------------------------------------------------------
   Each panel also fades and lifts as it arrives, so the run has some depth
   rather than four rigid panels sliding by. .is-in is written by JS; with
   no JS the .js guard never applies and every panel is simply present. */
.js .service-card,
.js .services-intro {
  opacity: 0;
  transform: translate3d(40px, 0, 0);
  transition: opacity .7s cubic-bezier(.16, 1, .3, 1),
              transform .7s cubic-bezier(.16, 1, .3, 1);
}
.js .service-card.is-in,
.js .services-intro.is-in {
  opacity: 1;
  transform: none;
}

/* ---- Phone ------------------------------------------------------------
   No pin and no rail: a vertical stack, each shot already uncovered. A
   horizontal run on a phone would be a second scroll direction competing
   with the page's own. */
@media (max-width: 767px) {
  .services-track { height: auto; }
  .services-pin {
    position: static;
    height: auto;
    /* The desktop one-viewport lock must be cleared explicitly here, or it
       would hold the stacked cards to a single screen and clip them. */
    min-height: 0;
    max-height: none;
    display: block;
    overflow: visible;
  }
  .services-rail {
    display: block;
    height: auto;
    padding: 0;
    transform: none !important;
    will-change: auto;
  }
  .services-intro {
    width: auto;
    padding: 48px var(--gutter) 28px;
  }
  .services-intro h2 { margin-top: 10px; font-size: clamp(2.25rem, 1.4rem + 4vw, 3rem); }

  .service-card {
    width: auto;
    min-height: 54vh;
    padding-top: 32px;
    border-left: 0;
    border-top: 1px solid rgba(255, 255, 255, .22);
    padding: 32px var(--gutter);
  }
  /* Open, and staying open: there is no hover to reveal it with. */
  .service-bg { clip-path: inset(0% 0% 0% 0%); }
  .service-wash { background: rgba(5, 61, 87, .66); }
  .service-media { transform: none; }
  .js .service-card,
  .js .services-intro { opacity: 1; transform: none; }
}

/* ---- Touch spotlight --------------------------------------------------
   No hover on phones and touch tablets, so initServiceSpotlight() in
   main.js marks the section .is-spotlight and gives the card crossing the
   middle of the screen .is-active. That card gets the full desktop hover
   treatment; the rest wait in a muted state, so the photographs light up
   one by one as the visitor scrolls. */
.is-spotlight .service-card.is-active .service-bg { clip-path: inset(0% 0% 0% 0%); }
.is-spotlight .service-card.is-active .service-media { transform: translate3d(0, -3%, 0); }
.is-spotlight .service-card.is-active .service-num { color: var(--white); }
.is-spotlight .service-card.is-active .service-arrow {
  background: var(--white);
  border-color: var(--white);
  color: var(--blue-700);
}
.is-spotlight .service-card.is-active .service-arrow svg { transform: rotate(45deg); }

/* The lit card swaps the flat wash for a gradient: dark only where the
   title and copy sit, so the photograph shows in its real colours. */
.is-spotlight .service-wash {
  transition: background-color .6s ease, opacity .6s ease;
}
.is-spotlight .service-card.is-active .service-wash {
  background: linear-gradient(to top, rgba(5, 40, 60, .82) 0%, rgba(5, 40, 60, .25) 50%, rgba(5, 40, 60, .45) 100%);
}
.is-spotlight .service-media img {
  transition: filter .7s ease, transform 1.2s cubic-bezier(.16, 1, .3, 1);
}
.is-spotlight .service-card.is-active .service-media img { transform: scale(1.16); }

@media (max-width: 767px) {
  /* Phone: the stack keeps every photograph uncovered, so the resting
     cards are muted (greyed, under the blue wash) and the lit one comes
     up to full colour. */
  .is-spotlight .service-media img { filter: grayscale(1) brightness(.85); }
  .is-spotlight .service-card.is-active .service-media img { filter: none; }
  .is-spotlight .service-title,
  .is-spotlight .service-copy {
    transition: transform .6s cubic-bezier(.16, 1, .3, 1);
  }
  .is-spotlight .service-card.is-active .service-title { transform: translateY(-6px); }
}

/* ---- Stillness --------------------------------------------------------
   Reduced motion: no pin, no travel, no masking. The same vertical stack
   the phone gets, with every transition off. initServices() returns early,
   so no transform is ever written to the rail. */
@media (prefers-reduced-motion: reduce) {
  .services-track { height: auto; }
  .services-pin {
    position: static;
    height: auto;
    /* The desktop one-viewport lock must be cleared explicitly here, or it
       would hold the stacked cards to a single screen and clip them. */
    min-height: 0;
    max-height: none;
    display: block;
    overflow: visible;
  }
  .services-rail {
    display: block;
    height: auto;
    padding: 0;
    transform: none !important;
  }
  .services-intro { width: auto; padding: 48px var(--gutter) 28px; }
  .service-card {
    width: auto;
    border-left: 0;
    border-top: 1px solid rgba(255, 255, 255, .22);
  }
  .service-bg,
  .service-media,
  .service-num,
  .service-arrow,
  .service-arrow svg,
  .js .service-card,
  .js .services-intro { transition: none !important; }
  .service-bg { clip-path: inset(0% 0% 0% 0%); }
  .service-media { transform: none; }
  .js .service-card,
  .js .services-intro { opacity: 1; transform: none; }
}

/* --- LAYER 5b — the reference poster furniture -------------------------
   Two additions that complete the Eco Pulverizer reference composition on
   every scene: the stacked tagline at the top left, and the row of four
   feature callouts at the lower right. Both are built from the existing
   blue/ink tokens only — no new colour enters the palette. */

/* The small stacked eyebrow at the top left. It is artwork rather than
   content (the section already has a real heading in .stage-head), so it
   is aria-hidden in the markup and never picked up as a heading. */
.stage-tagline {
  position: absolute;
  /* The same left rail as the copy column directly below it: the two
     left-hand blocks now share one edge instead of the tagline sitting on
     --gutter while the copy sat on its own value. */
  left: var(--stage-inset);
  /* One step below the shared top band — far enough to clear the header,
     close enough that it still reads as part of the same top row. */
  top: calc(var(--stage-top) + clamp(58px, 9vh, 104px));
  z-index: 5;
  margin: 0;
  font-size: var(--fs-tag);
  font-weight: var(--fw-label);
  line-height: 190%;
  letter-spacing: var(--ls-label-loose);
  text-transform: uppercase;
  color: var(--ink-500);
  pointer-events: none;
  user-select: none;
  will-change: transform, opacity;
}

/* The four icon + label callouts, lower right. They share the CTA/copy
   baseline so the bottom of the poster reads as one bar: copy+CTA on the
   left, callouts on the right. The arrows now sit vertically centred on
   the side edges instead of this corner, so the row no longer has to stop
   short of them and runs out to the right edge itself. */
.stage-feats {
  position: absolute;
  /* The callout row gets its own right boundary, one step OUTSIDE the text
     rail the copy column uses. The two blocks are no longer mirror images:
     the left column is a reading column that wants the text rail, while
     this row is a band of small centred tiles that reads better carried
     out toward the edge. --stage-feat-rail is that boundary, and the last
     tile ("Air-Cooled Filter") sits exactly on it. */
  right: var(--stage-feat-rail);
  bottom: var(--stage-base);
  z-index: 5;
  display: flex;
  gap: clamp(18px, 2.6vw, 42px);
  margin: 0;
  padding: 0;
  list-style: none;
  text-align: center;
  pointer-events: none;
}
.stage-feats li {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  width: clamp(64px, 7vw, 92px);
  font-size: .6875em;
  font-weight: 500;
  line-height: 145%;
  color: var(--ink-700);
  will-change: transform, opacity;
}
.stage-feats svg {
  width: 26px;
  height: 26px;
  display: block;
  color: var(--blue-600);
}
/* A hairline divider between callouts, as in the reference. */
.stage-feats li + li { position: relative; }
.stage-feats li + li::before {
  content: "";
  position: absolute;
  left: calc(clamp(18px, 2.6vw, 42px) / -2);
  top: 6%;
  height: 88%;
  border-left: 1px solid var(--blue-200);
}

/* The CTA pair now sits in the left copy column, directly under the spec
   chips, instead of being centred at the bottom of the section. It is a
   normal block in that column, so the old absolute centring — and the
   collision rules that existed only to keep a centred box clear of this
   very column — no longer apply. */
.stage-copy .stage-actions {
  position: static;
  width: auto;
  max-width: none;
  margin: 18px 0 0;
  justify-content: flex-start;
}

/* The left column now carries the buttons too, so it needs more room than
   the 27vw the text alone was held to. */
.stage-copy { width: min(460px, 34vw); }

@media (min-width: 861px) and (max-width: 1180px) {
  /* Four callouts plus the arrows no longer fit beside the copy column
     here, so the row drops under the machine and centres. It keeps
     answering to the grid: it clears the copy column by exactly one
     --stage-base step, and the copy column lifts by the same step, so the
     two blocks stay evenly spaced instead of one crowding the other. */
  .stage-feats {
    left: 0; right: 0;
    bottom: var(--stage-base);
    justify-content: center;
    gap: clamp(14px, 2.4vw, 30px);
    padding: 0 var(--stage-rail);
  }
  .stage-copy { bottom: calc(var(--stage-base) + clamp(96px, 15vh, 150px)); }
}

@media (max-width: 860px) {
  /* Portrait: the tagline would crowd the machine, and the callouts become
     a centred two-up grid beneath the stacked copy. */
  .stage-tagline { display: none; }
  .stage-copy { width: auto; }
  .stage-copy .stage-actions { justify-content: center; }
  /* Held to the floor on the shared baseline. The copy column above now
     grows downward from the machine, so the guard below keeps this row
     from being reached on the shortest windows. */
  .stage-feats {
    display: grid;
    grid-template-columns: repeat(4, auto);
    justify-content: center;
    gap: 14px;
    /* Full-bleed band, centred. The base rule anchors this row to the
       right rail for the desktop layout; portrait centres it under the
       machine instead, so that anchor has to be released explicitly —
       left over, it held the row against the right edge and only the
       padding kept it on screen. */
    left: 0;
    right: 0;
    bottom: var(--stage-base);
    padding: 0 var(--stage-rail);
  }
  .stage-feats li { width: clamp(58px, 20vw, 78px); font-size: .625em; }
  .stage-feats svg { width: 21px; height: 21px; }
  /* The divider reads as clutter once the row is this tight. */
  .stage-feats li + li::before { display: none; }
}

@media (max-width: 430px) {
  /* The four-across row is KEPT here. Folding it to 2x2 doubled its height
     at exactly the width where the CTA pills also stack, and the two
     together were what pushed the copy column down into this row. The
     labels stay readable across four columns at this size because the
     tile width and font step down with the viewport. */
  /* justify-self is what centres a grid whose columns are narrower than
     the track; without it the auto-columns hug the start and the row sat
     off to the left of the poster it is supposed to be centred under. */
  .stage-feats { gap: 10px 8px; justify-content: center; justify-items: center; }
  .stage-feats li { width: clamp(52px, 21vw, 72px); font-size: .5875em; gap: 6px; }
  .stage-feats svg { width: 19px; height: 19px; }
}

/* Portrait tablets: the 860px block sizes everything for a phone, which
   left the copy, pills and callouts tiny on an iPad with a wide empty band
   between the CTAs and the callout row. Here the type, pills and callouts
   step up to tablet scale and the machine takes back some of that height.
   Gated on min-height so a short window keeps the compact phone stack. */
@media (min-width: 601px) and (max-width: 860px) and (min-height: 700px) {
  .section.stage { --stage-mach-h: clamp(260px, 36vh, 480px); }
  .stage-copy { max-width: 620px; margin: 0 auto; }
  .stage-copy .cat-tag { font-size: .95rem; margin-bottom: 10px; }
  .stage-copy h3 { font-size: clamp(1.75rem, 3.6vw, 2.25rem); margin-bottom: 10px; }
  .stage-copy .desc { font-size: clamp(1rem, 2vw, 1.15rem); margin-bottom: 16px; }
  .stage-copy .spec-chip { font-size: .95rem; padding: 7px 14px; }
  .stage-copy .stage-actions { margin-top: 24px; gap: 14px; }
  .stage-actions .btn {
    min-height: 48px;
    padding: .75rem 1.5rem;
    font-size: 1rem;
  }
  .stage-feats { gap: 22px; }
  .stage-feats li { width: clamp(96px, 15vw, 130px); font-size: .9rem; gap: 10px; }
  .stage-feats svg { width: 28px; height: 28px; }
}

/* Stillness: the column layout has no pinned scene, so these simply sit in
   flow with everything else rather than being absolutely placed. */
@media (prefers-reduced-motion: reduce) {
  .stage-tagline { display: none; }
  .stage-feats {
    position: static;
    justify-content: center;
    flex-wrap: wrap;
    margin: 18px auto 0;
  }
  /* The entrance beats never run here, so the callouts must not be left
     sitting at the opacity the animation would otherwise have lifted. */
  .stage-feats li { opacity: 1 !important; transform: none !important; }
}

/* ==========================================================================
   Connect band — merged social-proof + enquiry CTA (homepage)
   One editorial slab on the sky-tint ground: compact form on the left,
   oversized condensed headline on the right, social row pinned bottom-left.
   Replaces the old separate "Social Proof" and .cta-band sections.
   ========================================================================== */
.connect-band-section {
  /* Plain full-screen band. It does NOT pin and it does NOT move on its own
     — the reveal is done entirely by the footer sitting behind it, so this
     stays one ordinary screen in normal flow (see .site-footer below). */
  position: relative;
  z-index: 1;
  padding: 0;
  height: 100svh;
  min-height: max(100svh, 620px);
  max-height: max(100svh, 620px);
  background: var(--blue-900);
}
.connect-pin {
  height: 100%;
  display: flex;
  align-items: center;
  overflow: hidden;
  isolation: isolate;
}

/* ---- The footer reveal -----------------------------------------------
   Matches the reference exactly: the FOOTER is stuck to the bottom of the
   viewport at a NEGATIVE z-index, so it sits behind the page rather than
   after it. It is therefore already in place, held at the bottom of the
   screen, while the content above scrolls; the connect band simply slides
   up off the top and uncovers it. Nothing pins, nothing transforms, and
   there is no gap to close — the band ends exactly where it ends and the
   footer was always underneath it.

   z-index: -1 is what puts it behind, so every ancestor between it and the
   root must stay transparent or the footer is painted over. body already
   carries the page background, so the footer is given its own painted
   ground and body is made transparent over the footer's own height. */
/* SCOPED TO THIS PAGE ONLY. Every rule below hangs off a body:has() test
   for the connect band being the last thing in <main> — which is true on
   the homepage and nowhere else. An unscoped .site-footer here would make
   the footer sticky and transparent-backed on every page of the site. */
body:has(> main > .connect-band-section:last-child) .site-footer {
  position: sticky;
  bottom: 0;
  /* 0 with <main> at 1 (below) gives the same "behind the page" stacking as
     -1 did, but keeps the footer above body for hit-testing — at -1 every
     footer link on the homepage was unclickable. */
  z-index: 0;
}
/* A z-index:-1 element is painted behind its stacking context, so the page
   background would cover the footer. The background is moved off body onto
   the content above it, leaving body transparent for the footer to show
   through — this is the one rule the reveal depends on. */
body:has(> main > .connect-band-section:last-child) { background: transparent; }
body:has(> main > .connect-band-section:last-child) > main { background: var(--page-bg); position: relative; z-index: 1; }

/* Full-bleed photographic ground. It settles from a slight over-scale as the
   band enters view — enough to feel alive behind the slab, not enough to
   read as a parallax gimmick. */
.connect-bg {
  position: absolute; inset: 0; z-index: -1;
  overflow: hidden;
}
.connect-bg img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
  transform: scale(1.08);
  transition: transform 1.4s cubic-bezier(.22,.61,.36,1);
}
.connect-pin:has(.connect-band.is-revealed) .connect-bg img { transform: scale(1); }
.connect-pin::after {
  /* Keeps the slab legible whatever the photo does underneath. */
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, rgba(5,61,87,.28) 0%, rgba(5,61,87,.46) 100%);
  pointer-events: none;
}
.connect-pin .container { width: 100%; }

/* ---- The slab -------------------------------------------------------- */
.connect-band {
  position: relative;
  /* Deliberately narrower than the page container and centred in it, so the
     photograph reads as a frame around the card rather than a thin margin. */
  max-width: 660px;
  margin: 0 auto;
  padding: 0 clamp(16px, 2vw, 26px) clamp(16px, 2vw, 24px);
  border-radius: var(--radius-md);
  color: #fff;
  background: rgba(5, 61, 87, .46);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  border: 1px solid rgba(255,255,255,.10);
  box-shadow: 0 30px 80px rgba(3, 26, 38, .32);
  /* Arriving: the card lifts slightly into place — a settle, not an
     entrance. Duration and curve are the reference's own motion tokens,
     read off their stylesheet: --duration-extra-long (.6s) paired with
     --ease-out-slow, cubic-bezier(0,0,.3,1). That curve leaves its start
     immediately and decelerates the whole way, which is what makes the
     movement read as settling rather than sliding in. */
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .6s cubic-bezier(0, 0, .3, 1), transform .6s cubic-bezier(0, 0, .3, 1);
}
.connect-band.is-revealed { opacity: 1; transform: none; }

/* Children stagger in behind the slab itself. */
.connect-band [data-reveal-child] {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .6s cubic-bezier(0, 0, .3, 1), transform .6s cubic-bezier(0, 0, .3, 1);
}
.connect-band.is-revealed [data-reveal-child] { opacity: 1; transform: none; }
.connect-band.is-revealed .connect-band-head    { transition-delay: .12s; }
.connect-band.is-revealed .connect-sub          { transition-delay: .20s; }
.connect-band.is-revealed .connect-band-actions { transition-delay: .28s; }
.connect-band.is-revealed .connect-band-foot    { transition-delay: .36s; }

/* ---- Top meta rail --------------------------------------------------- */
.connect-band-top {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  padding: 12px 0 11px;
  border-bottom: 1px solid rgba(255,255,255,.16);
}
.connect-band-tag {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--fs-button-sm);
  letter-spacing: .10em;
  text-transform: uppercase;
  color: rgba(255,255,255,.82);
}
.connect-band-tag-mid { justify-self: center; }
.connect-band-tag-end { justify-self: end; }
.connect-band-tag svg { width: 15px; height: 15px; flex: none; }

/* ---- Headline row ---------------------------------------------------- */
.connect-band-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(24px, 4vw, 64px);
  padding: clamp(20px, 2.8vw, 30px) 0 clamp(16px, 2vw, 22px);
  border-bottom: 1px solid rgba(255,255,255,.16);
}
.connect-headline {
  color: #fff;
  margin: 0;
  font-size: clamp(1.625rem, 3.2vw, 2.375rem);
  /* Plays the hero's role at the end of the page, so it takes the hero face.
     1.02 is kept over --lh-display: this headline is deliberately set on two
     short lines and the tighter ratio is what stacks them. */
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  line-height: 1.02;
  letter-spacing: var(--ls-display);
}
/* The headline CTA is the site's standard pill (.btn.btn-primary.btn-sm);
   only its flex behaviour belongs here, so it keeps the shared radius,
   padding, case and hover along with every other CTA on the site. */
.connect-band-cta { flex: none; }
.connect-band-cta svg { width: 14px; height: 14px; }

/* ---- Body ------------------------------------------------------------ */
.connect-band-body { padding-top: clamp(16px, 2vw, 22px); }
.connect-sub {
  color: rgba(255,255,255,.80);
  max-width: 46ch;
  margin: 0 0 14px;
  font-size: var(--fs-small);
  line-height: 1.55;
}

/* Two enquiry rows standing where the reference puts its email field: a
   dark inset bar with the label on the left and a solid button on the right. */
.connect-band-actions { display: grid; gap: 10px; }
.connect-band-action {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  background: rgba(3, 26, 38, .55);
  border: 1px solid rgba(255,255,255,.08);
  /* The site's input/surface radius, so these read as fields in the same
     system as every other form control on the site. */
  border-radius: var(--radius-sm);
  padding: 7px 7px 7px 16px;
  color: rgba(255,255,255,.86);
  transition: background .2s ease, border-color .2s ease;
}
.connect-band-action:hover { background: rgba(3, 26, 38, .72); border-color: rgba(255,255,255,.18); }
.connect-band-action-label { font-size: var(--fs-small); }
/* Both row buttons are the site's standard small pills (.btn.btn-sm), so
   only their flex behaviour is set here. */
.connect-band-action-btn { flex: none; }

.connect-band-foot {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 16px;
  margin-top: 14px;
}
.connect-band-note { font-size: var(--fs-small); color: rgba(255,255,255,.62); }

.connect-social-icons { display: flex; gap: 8px; }
.connect-social-icons a {
  width: 32px; height: 32px; border-radius: var(--radius-pill);
  display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid rgba(255,255,255,.35); color: #fff;
  transition: background .18s ease, color .18s ease, border-color .18s ease;
}
.connect-social-icons a:hover { background: #fff; color: var(--blue-900); border-color: #fff; }
.connect-social-icons svg { width: 15px; height: 15px; }

@media (max-width: 900px) {
  /* Below the two-column break the slab no longer fits one viewport, so the
     height lock AND the pin are both released and the section flows. */
  /* Below the break the card no longer fits one viewport, so the band stops
     being a fixed screen and simply flows at its own height. The footer
     reveal is dropped with it: a footer taller than the viewport cannot be
     held behind a shorter band, so it returns to normal flow after it. */
  .connect-band-section {
    height: auto; min-height: 0; max-height: none;
  }
  .connect-pin {
    height: auto;
    display: block;
    padding: 56px 0;
  }
  body:has(> main > .connect-band-section:last-child) .site-footer { position: relative; bottom: auto; z-index: 0; }
  body:has(> main > .connect-band-section:last-child) { background: var(--page-bg); }
  .connect-band-top { grid-template-columns: 1fr auto; }
  .connect-band-tag-mid { display: none; }
  .connect-band-head { flex-direction: column; align-items: flex-start; gap: 24px; }
  .connect-band-cta { width: 100%; }
  .connect-band-action { flex-direction: column; align-items: stretch; gap: 12px; padding: 18px; text-align: center; }
  .connect-band-action-btn { text-align: center; }
  .connect-band-foot { flex-direction: column; align-items: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  .connect-band,
  .connect-band [data-reveal-child] { opacity: 1; transform: none; transition: none; }
  .connect-bg img { transform: none; transition: none; }
  /* The rise is motion for its own sake, so it is dropped entirely here —
     the plain sticky release still uncovers the footer. */
}

/* ==========================================================================
   THE NORMAL SECTION HEADING SYSTEM
   ==========================================================================
   One rule owns the typography of every NORMAL section heading on the site.

   Why this exists: the same four token values were previously restated by
   five separate component rules (.section-head h2, .stage-head h2,
   .why-section .section-head h2, .cta-band h2, .journey-copy h2), and their
   responsive steps were duplicated in six more places. Nothing linked them,
   so each new section re-declared the set from scratch and any one of them
   could drift without the others noticing — which is exactly how the stage
   heading ended up at 25px while its neighbours sat at 40px, twice.

   Now there is one list. To bring a new section heading into the system,
   add its selector here and give it NO typography of its own. To change
   every section heading, edit the tokens.

   Placed at the end of the file deliberately: several of these components
   are defined late (.stage-head at ~4086, .journey-copy at ~5320),
   and a shared rule declared earlier would
   lose to them on source order. This is the one place a single rule can
   win for all of them without inflating specificity.

   LAYOUT IS NOT TYPOGRAPHY: width, alignment, margin, colour and position
   stay with each component. Only family/size/weight/leading/tracking/case
   are shared, so sections keep their own composition.

   NOT in this system (genuine display headlines, documented in §8):
     .hero h1 / .hc-title     — the hero, the site's display benchmark
     .services-intro h2       — a full-viewport pinned rail panel that
                                plays the hero's role in its own section
     .stage-word              — decorative artwork, not a heading at all
     .connect-headline        — the closing band is now a full-viewport
                                pinned slab over a photograph, playing the
                                hero's role at the end of the page
     .article-body / .legal-body h2 — prose headings inside a 760px
                                measure column, deliberately a step down
     .footer-signup h2        — footer chrome, deliberately quieter
   ========================================================================== */
.section-head h2,
.why-section .section-head h2,
.stage-head h2,
.journey-copy h2,
.cta-band h2,
.ab-statement {
  /* The display serif, as on every hero headline. Section heads are the
     page's other tier of big type, so they take the same face — the
     grotesque keeps everything below them (body, eyebrows, card titles).

     font-weight is 400 because Marcellus ships no other weight; asking for
     --fw-title here would have the browser synthesise a faux-bold. The serif
     also sets tighter than the grotesque at the same size, hence the display
     line-height and tracking rather than --lh-title / --ls-title. */
  font-family: var(--font-display);
  font-size: var(--fs-title);
  font-weight: var(--fw-display);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
  text-transform: none;
}

/* The shared responsive steps, in ONE place rather than repeated per
   selector. Same values the system already used. */
@media (max-width: 900px) {
  .section-head h2,
  .why-section .section-head h2,
  .stage-head h2,
  .journey-copy h2,
  .cta-band h2,
  .ab-statement { font-size: 2.1em; }
}
@media (max-width: 600px) {
  .section-head h2,
  .why-section .section-head h2,
  .stage-head h2,
  .journey-copy h2,
  .cta-band h2,
  .ab-statement { font-size: 1.8em; }
}

/* ---- "31+ Years" in the Why Jagruti heading ----
   Placed after the consolidated heading block above on purpose: that block
   sets font-family/size/weight on .why-section .section-head h2 and, being
   later in the file at equal specificity, used to win over an earlier rule
   here and flatten this emphasis back to the heading's own style.

   The emphasis is colour, not weight: the heading is Marcellus, which ships
   a single weight, so a bolder span would only get a synthesised faux-bold.
   --blue-500 is the brightest step of the brand scale and is already the
   section's accent (the eyebrow and the card icons carry it), so the number
   lifts off the --blue-800 heading while staying inside the palette. */
.why-section .section-head h2 .why-hl { white-space: nowrap; }
.why-section .section-head h2 .why-hl-num {
  color: var(--blue-500);
  /* Weight is unavailable here — Marcellus ships one cut — so the lift comes
     from size and colour instead: ~15% larger than the words around it, which
     is enough to register as the figure being claimed without re-wrapping the
     line. */
  font-size: 1.15em;
  /* The serif sets figures a touch loose at display size; a hair tighter
     keeps "31+" reading as one unit. */
  letter-spacing: -.015em;
}

/* ==========================================================================
   Contact hero — split layout (image left / enquiry form right)
   Replaces the centred .page-hero band on contact.html. The image column is
   a full-bleed portrait that sets the row height; the form column scrolls
   its own content if the viewport is short.
   ========================================================================== */
.contact-hero {
  background: var(--blue-50);
  padding: 0;
  overflow: hidden;
}
.contact-hero-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: stretch;
  min-height: 640px;
}
.contact-hero-media {
  position: relative;
  min-height: 100%;
  background: var(--blue-200);
}
.contact-hero-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}
.contact-hero-form {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(40px, 5vw, 72px) clamp(24px, 5vw, 80px);
  background: #fff;
}
.contact-hero-form .eyebrow { align-self: flex-start; margin-bottom: 12px; }
.contact-hero-form h1 {
  font-size: clamp(1.9em, 3.2vw, 2.5em);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
  margin: 0 0 12px;
}
.contact-hero-form .contact-hero-lead {
  color: var(--ink-700);
  font-size: var(--fs-card-desc);
  line-height: var(--lh-body);
  max-width: 48ch;
  margin: 0 0 26px;
}

@media (max-width: 900px) {
  .contact-hero-grid { grid-template-columns: 1fr; min-height: 0; }
  .contact-hero-media { min-height: 380px; }
  .contact-hero-media img { position: static; height: 380px; }
}

/* Info cards below the split hero: a 4-up row rather than a stacked
   sidebar, since the form no longer shares that column. */
.contact-info-grid .contact-info-card { margin-bottom: 0; height: 100%; }

/* The contact hero runs under the fixed header the same way the homepage hero
   does: the body drops its offset and the form column pads itself clear of the
   bar, so the photo fills the full frame including the strip behind the nav. */
body[data-nav="over-dark"] main > .contact-hero:first-child .contact-hero-form {
  padding-top: calc(var(--header-h) + clamp(24px, 3vw, 48px));
}
/* Dark scrim across the header band only, so the logo reads against the photo
   in the left column. Same treatment as the homepage hero. */
body[data-nav="over-dark"]:has(> main > .contact-hero:first-child) .site-header:not(.is-scrolled)::before {
  content: "";
  position: absolute; left: 0; right: 50%; top: 0;
  height: calc(var(--header-h) + 46px);
  pointer-events: none;
  z-index: 0;
  background: linear-gradient(180deg,
    rgba(0,22,36,.72) 0%,
    rgba(0,22,36,.52) 50%,
    rgba(0,22,36,.20) 80%,
    transparent 100%);
}

@media (max-width: 900px) {
  main > .contact-hero:first-child .contact-hero-form { padding-top: clamp(32px, 6vw, 48px); }
  main > .contact-hero:first-child .contact-hero-media { padding-top: 0; }
  body[data-nav="over-dark"]:has(> main > .contact-hero:first-child) .site-header:not(.is-scrolled)::before { right: 0; }
}

/* ---------- Contact hero form — reference styling ----------
   Underline-only fields: no boxes, no fills, labels hidden and carried by the
   placeholder instead, with a hairline rule under each field. Matches the
   .contact-hero-form is now the only user of this treatment — the homepage
   connect band it was modelled on no longer carries a form. */
.contact-hero-form form { display: block; }
.contact-hero-form .form-field { margin-bottom: 0; }
.contact-hero-form .form-field label {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.contact-hero-form .form-field input,
.contact-hero-form .form-field select,
.contact-hero-form .form-field textarea {
  width: 100%;
  border: 0;
  border-bottom: 1.5px solid rgba(23, 32, 38, .22);
  border-radius: 0;
  background: transparent;
  padding: 16px 2px;
  font-size: var(--fs-card-desc);
  font-family: inherit;
  color: var(--ink-900);
  outline: none;
  transition: border-color .18s ease;
}
.contact-hero-form .form-field input::placeholder,
.contact-hero-form .form-field textarea::placeholder { color: var(--ink-500); }
.contact-hero-form .form-field select {
  color: var(--ink-500);
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23607685' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 2px center;
  background-size: 18px; padding-right: 26px;
}
.contact-hero-form .form-field input:focus,
.contact-hero-form .form-field select:focus,
.contact-hero-form .form-field textarea:focus { border-bottom-color: var(--blue-700); }
.contact-hero-form .form-field textarea { resize: none; }
.contact-hero-form .form-row-2 { gap: 0 28px; }

/* Compact pill submit sized to its label, not the column. */
.contact-hero-form form .btn {
  width: auto;
  display: inline-flex;
  margin-top: 28px;
  border-radius: var(--radius-pill);
}
.contact-hero-form .form-note { margin-top: 14px; }

/* ===================================================================
   EDITORIAL CATEGORY HERO  (.cat-hero)
   -------------------------------------------------------------------
   A full-bleed photographic band used at the top of a category page in
   place of the centred .page-hero gradient. The picture carries the
   section; type sits on it rather than beside it — headline and CTAs
   pinned bottom-left, a short positioning line bottom-right, both on the
   same baseline. Typography is the site ramp (General Sans via
   --font-sans, inherited) so it reads as the same family as the homepage
   hero: the eyebrow takes the label scale, the headline the
   h1 scale at the shared title weight and tracking.
   =================================================================== */
/* One screen tall, pinned three ways — the full-viewport contract from
   SECTION-HEIGHTS.md, the same box the homepage .hero declares. The three
   declarations are all required: 100svh sets the box in the stable unit a
   collapsing mobile toolbar cannot change, and the max(…, 620px) floor stops
   it shrinking past the hero's own floor on a short window. This section
   qualifies for the lock because its content is authored to fit — the copy
   row is ~260px and flexes into whatever height is left, so the box never
   clips it. */
.cat-hero {
  position: relative;
  height: 100svh;
  min-height: max(100svh, 620px);
  max-height: max(100svh, 620px);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: var(--header-h) 0 clamp(34px, 5vh, 62px);
  background: #1b1512;
  color: #fff;
  overflow: hidden;
}
.cat-hero .ch-img {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 45%;
}
/* Two scrims, not one: a left-to-right wash that keeps the headline column
   readable over the bright part of the picture, and a bottom gradient that
   anchors both text blocks on the same baseline. */
.cat-hero::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    linear-gradient(90deg, rgba(14,10,8,.62) 0%, rgba(14,10,8,.30) 34%, rgba(14,10,8,.06) 56%, transparent 72%),
    linear-gradient(180deg, rgba(14,10,8,.46) 0%, transparent 26%, transparent 52%, rgba(14,10,8,.60) 100%);
}
.cat-hero .container { position: relative; z-index: 2; width: 100%; }

/* The headline row: copy left, positioning line right, sharing a baseline.
   It collapses to a single stacked column on narrow screens. */
.cat-hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 23em);
  align-items: end;
  gap: 30px clamp(28px, 5vw, 80px);
}
.cat-hero-eyebrow {
  margin: 0 0 .75em;
  font-size: var(--fs-label); font-weight: var(--fw-label);
  letter-spacing: var(--ls-label-tight);
  color: rgba(255,255,255,.86);
}
/* The headline runs larger than the flat --fs-h1 the centred .page-hero uses:
   on a full-bleed photograph the type has a whole screen to hold, and the
   reference sets it at display scale. It stays on the ramp by taking --fs-h1
   as its floor and the shared title line-height and tracking, so it is the
   same typeface and proportions, just one step up the scale. */
/* The face, weight, line-height and tracking now come from the shared
   `.page-hero h1, .cat-hero h1` display-serif rule near .page-hero. They are
   NOT repeated here: this block sits later in the file at equal specificity,
   so restating font-weight would win over the shared 400 and leave the
   browser synthesising a faux-bold from Marcellus's single weight. */
.cat-hero h1 {
  margin: 0;
  color: #fff;
  font-size: clamp(var(--fs-h1), 6.2vw, 92px);
  max-width: 12em;
  text-wrap: balance;
  text-shadow: 0 2px 22px rgba(12,8,6,.42);
}
.cat-hero .hero-cta {
  justify-content: flex-start;
  margin-top: clamp(22px, 3vh, 34px);
}
/* The reference's buttons are square, but every CTA on this site is a pill
   (.btn takes --radius-pill, as does the homepage hero's .hc-cta), so these
   keep the site's radius rather than the reference's — consistency with the
   rest of the site wins over copying that one detail. */
.cat-hero .hero-cta .btn {
  min-width: 11em;
  justify-content: center;
}
/* Deliberately the small print of the pair — the label scale against the
   headline's display scale, so the row reads with one clear focal point. */
.cat-hero-note {
  margin: 0;
  font-size: var(--fs-label);
  line-height: var(--lh-body);
  color: rgba(255,255,255,.88);
  max-width: 30em;
  margin-left: auto;
  text-shadow: 0 1px 14px rgba(12,8,6,.5);
}

/* Below 900px the lock is released, matching .hero, which goes height:auto at
   the same breakpoint. A section matched to the hero on desktop is
   deliberately not matched here. */
@media (max-width: 900px) {
  .cat-hero {
    height: auto;
    min-height: min(70svh, 560px);
    max-height: none;
    padding: calc(var(--header-h) + 40px) 0 48px;
  }
  .cat-hero-grid { grid-template-columns: 1fr; align-items: start; }
  .cat-hero h1 { max-width: none; }
  .cat-hero-note { max-width: 34em; }
}
@media (max-width: 560px) {
  .cat-hero .hero-cta .btn { min-width: 0; flex: 1 1 auto; }
}

/* ---------- Collection banner variant (.cat-hero-wide) ----------
   The reference's collection header is a BANNER, not a full-screen hero: the
   picture stops well above the fold so the first row of products is already
   visible on load, which is the whole point of a collection page. So the
   height lock drops from 100svh to a band, and the copy collapses from the
   two-column headline/note split into one left column with the paragraph
   sitting directly under the headline.

   Everything else -- the scrims, the transparent header, the type ramp --
   is inherited from .cat-hero unchanged. */
/* The band height is shared with the support hero (min(82svh, 720px)) so every
   full-bleed photographic banner on the site is one length. It is taller than
   the 62svh/560px this started at: at that height the picture was cropped hard
   enough to lose the subject, and the first product row only gained a sliver of
   fold. 82svh still stops short of the viewport, so the grid below remains
   visible on load -- the point of the banner over the full-screen .cat-hero. */
.cat-hero.cat-hero-wide {
  height: auto;
  min-height: min(82svh, 720px);
  max-height: none;
  padding: calc(var(--header-h) + 40px) 0 clamp(40px, 6vh, 64px);
}
/* One column: the note stops being a right-hand rail and becomes the
   standfirst under the headline, as in the reference. */
.cat-hero-wide .cat-hero-grid {
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
/* The face, weight, line-height and tracking come from the shared
   `.page-hero h1, .cat-hero h1` rule above. Only what is specific to the
   banner is set here: its size and its slightly-off-white ink. */
.cat-hero-wide h1 {
  max-width: none;
  color: rgb(240, 240, 240);
  /* 80px at desktop, as specified. Kept as a clamp rather than a flat 80px so
     the headline still comes down on small screens -- a fixed 80px overflows
     the gutter below ~460px. */
  font-size: clamp(38px, 5.6vw, 80px);
}
.cat-hero-wide .cat-hero-note {
  margin-left: 0;
  margin-top: clamp(16px, 2.2vh, 24px);
  /* A step under --fs-label (14px): against the 80px serif headline the
     standfirst is deliberately the small print of the pair, and the tighter
     measure keeps it to three short lines. */
  font-size: 13px;
  max-width: 30em;
}
/* The banner crops from higher up the frame than the standard .cat-hero's
   50% 45%: on this picture the machine and the operator's hand sit in the
   upper half, and the default framing pushed them down behind the headline.
   Pulling the focal point up keeps the subject clear of the copy. */
.cat-hero-wide .ch-img { object-position: 50% 28%; }

/* Slideshow banner: a hero carrying data-hero-slides stacks several .ch-img
   pictures in the same box and main.js (initHeroSlides) moves .is-active
   along them. Only the active one is opaque; the swap is a slow crossfade
   with a faint settle in scale so the change reads as a dissolve, not a cut. */
.cat-hero[data-hero-slides] .ch-img {
  opacity: 0;
  transform: scale(1.04);
  transition: opacity .8s ease, transform 3s ease-out;
}
.cat-hero[data-hero-slides] .ch-img.is-active {
  opacity: 1;
  transform: scale(1);
}
@media (prefers-reduced-motion: reduce) {
  .cat-hero[data-hero-slides] .ch-img { transform: none; transition: opacity .4s linear; }
}

/* The collection grid sits CLOSE under the banner rather than a full
   --section-pad-y (up to 120px) below it: on a collection page the products
   are the content the banner is introducing, so a full section gap reads as
   a break between two unrelated blocks. The bottom padding stays at the
   normal rhythm so the section still closes properly. */
.section-editorial.section {
  padding-top: clamp(20px, 2.6vh, 34px);
}

@media (max-width: 900px) {
  .cat-hero.cat-hero-wide { min-height: min(64svh, 520px); }
}

/* ---------- Support banner variant (.support-hero) ----------
   The support page takes the same full-bleed band. Its height now comes from
   .cat-hero-wide, which the product banners share, so only what is specific to
   this page is set here: the copy sits on the picture's lower edge rather than
   below the header, the crop is centred, and the headline is a step down the
   scale. Scrims, type ramp and the transparent header are inherited. */
.cat-hero.cat-hero-wide.support-hero {
  justify-content: flex-end;
  padding-bottom: clamp(30px, 4.5vh, 54px);
}
/* The copy block returns to bottom-aligned (the -wide banner top-aligns it),
   which is what puts the headline on the picture's lower edge. */
.support-hero .cat-hero-grid { align-items: end; }
/* A step down from the -wide banner's 80px. That size is set for the short
   two- or three-word category names ("Roti Making Machines"); this headline is
   a five-word sentence, so at 80px it ran the full width of the container and
   read as the whole picture. Capping at 60px and giving it a measure keeps it
   the focal point without crowding the frame. */
.support-hero h1 {
  font-size: clamp(32px, 4.2vw, 60px);
  max-width: 17em;
}
/* The subject in this frame sits centre-low, so the default -wide crop of
   50% 28% pushed it behind the copy. Centring the focal point keeps it clear
   of the headline column. */
.support-hero .ch-img { object-position: 50% 45%; }
/* The note is the standfirst above the CTA here, so it keeps a tight measure
   and the button row follows on the site's hero-cta rhythm. The gaps are
   pulled in from the -wide defaults so the whole eyebrow/headline/note/CTA
   stack stays shallow and the headline lands near the picture's lower edge,
   as in the reference, rather than drifting up to mid-frame. */
.support-hero .cat-hero-eyebrow { margin-bottom: .5em; }
.support-hero .cat-hero-note {
  max-width: 34em;
  margin-top: clamp(12px, 1.6vh, 18px);
}
.support-hero .hero-cta { margin-top: clamp(16px, 2.2vh, 24px); }

@media (max-width: 900px) {
  .support-hero .cat-hero-grid { align-items: start; }
}

/* Service & Repairs page: the machine sits on the right of this frame and the
   copy on the open wall to its left, so the crop leans right to keep the
   machine in view as the band narrows. */
.service-hero .ch-img { object-position: 78% 50%; }

/* ---------- Collection count bar (.range-bar) ----------
   The reference sets a hairline rule above the grid carrying the product
   count. The Filters control and the grid-density / Lookbook toggles beside
   it are omitted deliberately: on a six-tile range they would be controls
   with nothing to do. The count keeps the bar's purpose without the chrome. */
.range-bar {
  border-top: 1px solid rgba(96,118,133,.24);
  padding: 14px 0 20px;
  text-align: center;
  font-size: var(--fs-label);
  font-weight: var(--fw-label);
  letter-spacing: var(--ls-label-tight);
  color: var(--ink-500);
}

/* ===================================================================
   EDITORIAL PRODUCT GRID  (.grid-editorial)
   -------------------------------------------------------------------
   A gallery-style range listing: the photograph carries each tile and the
   only type is a name/price row beneath it. No card chrome, no per-card
   buttons — the whole tile is one link, the way a lookbook grid works.

   Scoped to the grid with a modifier rather than restyling .product-card,
   which 43 pages share. Everything here overrides the base card inside
   .grid-editorial only; the same markup on every other page is untouched.
   =================================================================== */
.grid.grid-editorial {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* The reference listing runs the photographs nearly edge to edge, with only
     a hairline of the page ground between tiles in both directions, so the
     grid reads as one continuous gallery rather than a set of separate cards
     with air around them. */
  gap: 10px 12px;
}
/* Strip the card: no ground, no border, no radius, no shadow, no lift. */
.grid-editorial .product-card {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: visible;
}
.grid-editorial .product-card:hover {
  transform: none;
  box-shadow: none;
}
/* A tile is HALF a panel row: two of them stacked fill the panel's height
   exactly, which is what makes the reference's rows square off. So the thumb
   takes whatever height is left after the name/price line rather than an
   aspect-ratio of its own — an intrinsic ratio would size the image from its
   own width and leave the card short, which is what left dead space under
   the tiles beside the panel. */
/* In the reference the tiles are SHORTER than the panel beside them, not
   stretched to match it: they take their own near-square ratio, sit at the
   top of the row and leave the space below empty. That is what keeps the
   panel reading as the feature and the tiles as the supporting row. */
.grid-editorial .product-card { align-self: start; }
/* rgb(224,226,230) — the value sampled from the reference's own product
   tiles, where it is the photography's studio backdrop rather than a CSS
   colour. Very slightly cool (blue one step above red), which is what keeps
   it sitting naturally next to this page's blue-tinted ground. */
.grid-editorial .product-card .thumb {
  display: block;
  aspect-ratio: 450/560;
  border-radius: 0;
  background: #e0e2e6;
}
/* The product sits INSIDE the tile with air around it rather than being
   cropped to fill it: `contain` keeps the whole machine visible and the
   padding holds it off the edges, so the tile reads as a product shot on a
   ground — the reference's treatment — instead of a bleed crop. */
.grid-editorial .product-card .thumb img {
  object-fit: contain;
  padding: 6%;
}
/* The site-wide `.product-card .thumb img.is-cutout` rule paints --blue-50 on
   the IMAGE, which covers the tile's own ground — so a cut-out rendered a
   different colour from the tiles beside it. In this grid the ground belongs
   to .thumb, so the image stays transparent and its padding comes from the
   rule above. */
.grid-editorial .product-card .thumb img.is-cutout {
  background: none;
  padding: 6%;
}
/* A slower, smaller push than the base card's .35s/1.06 — at this size the
   image is the whole tile, so the movement wants to be barely there. */
/* The All Products grid gets the same framing as the category pages: the
   whole product contained with air around it, rather than the base card's
   cover crop, which zoomed the photos in and sliced the machines. */
.grid-3[data-product-grid] .product-card .thumb img,
.grid-3[data-product-grid] .product-card .thumb img.is-cutout {
  object-fit: contain;
  padding: 6%;
}
.grid-editorial .product-card .thumb img { transition: transform .6s cubic-bezier(.22,.61,.36,1); }
.grid-editorial .product-card:hover .thumb img { transform: scale(1.03); }

/* The one type row: name left, action right, on a single baseline under the
   picture. Everything the base card stacks inside a padded body is either
   hidden below or laid out on this line. */
/* flex-direction must be stated: the base .product-card .body sets `column`
   at equal specificity and later in the file, so omitting it here leaves the
   row stacked. */
.grid-editorial .product-card .body {
  padding: 8px 2px 0;
  display: flex;
  flex-direction: row;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  flex: none;
}
/* The category tag, the description and the spec chip all repeat what the
   section head already says, and the reference's row carries neither. */
.grid-editorial .product-card .cat-tag,
.grid-editorial .product-card p.desc,
.grid-editorial .product-card .spec-chip-row { display: none; }

/* The name is a caption under a picture, not a card title: it sits on one
   line beside the action with no description under it, so it takes the body
   size rather than --fs-card-title (20px), which is sized for a stacked card
   with copy beneath. This matches the action opposite it, so the row reads
   as one line of type — as the reference's name/price row does. */
.grid-editorial .product-card h3 {
  margin: 0;
  min-width: 0;
  flex: 1;
  font-size: var(--fs-card-desc);
  line-height: var(--lh-card);
  font-weight: var(--fw-label);
  letter-spacing: var(--ls-body);
}
.grid-editorial .product-card h3 a { color: var(--ink-900); text-decoration: none; }
.grid-editorial .product-card:hover h3 a { color: var(--blue-700); }

/* "From $195.00" sits opposite the name in the reference. We do not publish
   prices, so the price slot carries the pricing ACTION instead — same
   position, same quiet treatment, as a text link rather than a button, so
   the row stays one line of type. */
.grid-editorial .product-card .card-actions {
  margin: 0;
  flex: none;
  gap: 0;
}
.grid-editorial .product-card .card-actions .btn-outline { display: none; }
/* The base .btn-whatsapp carries a border, a box-shadow and the waPulse
   attention animation — all of which are for a green pill, not a line of
   type. They have to be cleared explicitly, or the link renders as a faintly
   glowing outlined box. */
.grid-editorial .product-card .card-actions .btn-whatsapp {
  flex: none;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  animation: none;
  color: var(--ink-500);
  font-size: var(--fs-card-desc);
  font-weight: var(--fw-body);
  line-height: var(--lh-card);
  white-space: nowrap;
}
.grid-editorial .product-card .card-actions .btn-whatsapp svg { display: none; }
.grid-editorial .product-card .card-actions .btn-whatsapp:hover {
  background: none;
  color: var(--blue-700);
  text-decoration: underline;
}

/* The section head goes left-aligned to match the grid's left edge — a
   centred head over a flush gallery grid reads as two different systems. */
.section-editorial .section-head {
  max-width: none;
  margin-left: 0;
  margin-right: 0;
  text-align: left;
}

@media (max-width: 767px) {
  .grid.grid-editorial { gap: 12px 12px; }
}
@media (max-width: 560px) {
  .grid.grid-editorial { gap: 12px 10px; }
  .grid-editorial .product-card .body { flex-direction: column; align-items: flex-start; gap: 3px; }
}

/* ---------- Editorial grid: feature panel ----------
   Matched to the reference's own .featured-products spec rather than
   approximated: the grid runs FOUR columns and the panel spans two of them,
   so exactly two product tiles sit beside it and the row reads 1 feature +
   2 products. The panel occupies a single row at a near-square ratio
   (the reference uses 696/704) — it is not a two-row block.

   Below 768px the grid drops to two columns and the panel goes full width
   at a tall portrait ratio, which is what the reference does at its own
   48rem breakpoint. */
/* The panel spans TWO tile rows. Each row is one tile plus its name line, so
   two stacked tiles come out exactly as tall as one panel — which is what
   squares off the reference's rows. Height comes from the span, so the panel
   carries no aspect-ratio of its own. */
.grid-editorial .range-feature {
  aspect-ratio: 696/704;
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: clamp(24px, 2.4vw, 40px);
  overflow: hidden;
  background: #0b2030;
  color: #fff;
  text-decoration: none;
}
/* The reference alternates the panel's side between sections. Here every
   panel lives in one grid, so each is pinned to an explicit column AND row,
   alternating sides down the page.

   Each panel occupies ONE row, spanning two of the four columns — so the two
   columns left beside it hold exactly TWO product tiles, not four. (A
   two-row panel would leave a 2x2 space and take four tiles.) */
.grid-editorial { grid-auto-flow: dense; }
.grid-editorial .range-feature   { grid-column: 1 / span 2; grid-row: 1; }
.grid-editorial .range-feature-2 { grid-column: 3 / span 2; grid-row: 2; }
.grid-editorial .range-feature-3 { grid-column: 1 / span 2; grid-row: 3; }
.grid-editorial .range-feature-4 { grid-column: 3 / span 2; grid-row: 4; }
.grid-editorial .range-feature-5 { grid-column: 1 / span 2; grid-row: 5; }

/* The final row holds the three products left over after the panels. Left in
   the 4-column flow they sit in columns 1-3 and leave a hole at the right.
   This row spans the full width and uses space-between, so the tiles KEEP
   the column width they have everywhere else — the extra space goes into the
   gaps instead of stretching the tiles. First is flush left, last flush
   right. */
.grid-editorial .range-tail {
  grid-column: 1 / -1;
  display: flex;
  justify-content: space-between;
  gap: clamp(16px, 1.8vw, 26px);
}
/* One column's width on the parent's 4-track grid. */
.grid-editorial .range-tail > .product-card {
  flex: 0 0 calc((100% - 3 * clamp(16px, 1.8vw, 26px)) / 4);
}

/* The image is oversized by the parallax travel on BOTH edges, then pulled
   up by that same amount, so it still covers the panel at either end of the
   drift. --px-travel must match the data-parallax value in the markup; the
   reference does the same thing with its own --fp-parallax-travel. */
.grid-editorial .range-feature img {
  --px-travel: 30%;
  position: absolute; left: 0; z-index: 0;
  top: calc(var(--px-travel) * -1);
  width: 100%;
  height: calc(100% + var(--px-travel) * 2);
  object-fit: cover; object-position: 62% 58%;
  will-change: transform;
}
/* The hover zoom is dropped on parallax images: a transition on transform
   fights the scrubbed tween, which sets transform every frame. */
.grid-editorial .range-feature:hover img { transform: none; }
.grid-editorial .range-feature:hover img { transform: scale(1.03); }
/* A radial vignette, as the reference uses — it darkens the edges and leaves
   the centre of the photograph clear, unlike a bottom-up linear scrim which
   flattens the whole lower half. */
.grid-editorial .range-feature::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 0%, rgba(4,14,22,.92) 100%);
  opacity: .62;
}
/* .rf-contain: the photograph fills the panel exactly — sized to the panel
   itself, with no parallax oversize and no hover zoom, so only the minimum
   is trimmed. --rf-pos keeps the machine in frame for each photo. */
.grid-editorial .rf-contain img {
  top: 0; height: 100%;
  object-fit: cover; object-position: var(--rf-pos, center);
  will-change: auto;
}
.grid-editorial .rf-contain:hover img { transform: none; }
/* A contained panel that opts back into parallax gets just enough oversize
   for the drift. --px-travel must equal data-parallax (0.16 -> 16%). */
.grid-editorial .rf-contain img[data-parallax] {
  --px-travel: 16%;
  top: calc(var(--px-travel) * -1);
  height: calc(100% + var(--px-travel) * 2);
  will-change: transform;
}

/* Copy sits bottom-right with the headline pulled to the left edge — the
   reference's align-items:flex-end plus align-self:flex-start on the title. */
.grid-editorial .rf-content {
  position: relative; z-index: 2;
  display: flex; flex-direction: column;
  align-items: flex-end;
  gap: clamp(14px, 1.4vw, 22px);
}
.grid-editorial .rf-title {
  align-self: flex-start;
  margin: 0;
  color: #fff;
  font-size: clamp(var(--fs-h3), 2.4vw, 38px);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
  max-width: 8em;
  text-wrap: balance;
}
/* The reference pins this column to 18rem; ours matches so the paragraph
   sets as a narrow block against the headline rather than a wide band. */
.grid-editorial .rf-detail {
  display: flex; flex-direction: column;
  gap: clamp(14px, 1.4vw, 22px);
  width: 18rem;
  max-width: 100%;
}
.grid-editorial .rf-copy {
  margin: 0;
  font-size: var(--fs-card-desc);
  line-height: var(--lh-body);
  color: rgba(255,255,255,.92);
}
.grid-editorial .range-feature .btn { width: 100%; }
/* .rf-panel: a range panel — photograph with the scroll parallax, copy
   centred vertically and kept clear of the arrow's side. A left-to-right
   scrim keeps the white copy readable over the bright kitchen photographs. */
.grid-editorial .rf-panel { justify-content: center; padding-right: 120px; }
.grid-editorial .range-feature-2.rf-panel,
.grid-editorial .range-feature-4.rf-panel { padding-right: clamp(24px, 2.4vw, 40px); padding-left: 120px; }
.grid-editorial .rf-panel .rf-content { align-items: flex-start; }
.grid-editorial .rf-panel::after {
  opacity: 1;
  background: linear-gradient(90deg, rgba(4,14,22,.78) 0%, rgba(4,14,22,.5) 50%, rgba(4,14,22,.2) 100%);
}
.grid-editorial .rf-panel .rf-models { color: #9fd8f2; }
/* The machine sits at the centre of each landscape photograph — keep it
   centred in the panel. Copy: left on panels 1/3/5, right on panels 2/4. */
.grid-editorial .rf-panel { --rf-pos: 50% center; }
/* A moderate drift (data-parallax 0.12): visible on scroll while keeping
   the photo less zoomed than the default 0.16. --px-travel must equal
   data-parallax so the oversized photo never exposes an edge. */
.grid-editorial .rf-panel img[data-parallax] { --px-travel: 12%; }
.grid-editorial .range-feature-2.rf-panel .rf-content,
.grid-editorial .range-feature-4.rf-panel .rf-content { align-items: flex-end; text-align: right; }
.grid-editorial .range-feature-2.rf-panel .rf-title,
.grid-editorial .range-feature-4.rf-panel .rf-title { align-self: flex-end; }
.grid-editorial .range-feature-2.rf-panel::after,
.grid-editorial .range-feature-4.rf-panel::after {
  background: linear-gradient(270deg, rgba(4,14,22,.78) 0%, rgba(4,14,22,.5) 50%, rgba(4,14,22,.2) 100%);
}
/* Arrow pointing at the panel's products: panels on the left point right,
   panels on the right (2 and 4) point left. */
.grid-editorial .rf-arrow {
  position: absolute; z-index: 2; top: 50%; right: clamp(16px, 1.6vw, 24px);
  width: 72px; height: 72px; margin-top: -36px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--blue-700); color: #fff;
  box-shadow: 0 8px 22px rgba(0,0,0,.35);
  transition: transform .3s ease;
}
.grid-editorial .rf-arrow svg { width: 34px; height: 34px; }
.grid-editorial .rf-panel:hover .rf-arrow { transform: translateX(4px); }
.grid-editorial .range-feature-2 .rf-arrow,
.grid-editorial .range-feature-4 .rf-arrow { right: auto; left: clamp(16px, 1.6vw, 24px); }
.grid-editorial .range-feature-2 .rf-arrow svg,
.grid-editorial .range-feature-4 .rf-arrow svg { transform: rotate(180deg); }
.grid-editorial .range-feature-2:hover .rf-arrow,
.grid-editorial .range-feature-4:hover .rf-arrow { transform: translateX(-4px); }
/* Stacked on phones, the products sit below the panel — point down. */
@media (max-width: 767px) {
  .grid-editorial .rf-arrow,
  .grid-editorial .range-feature-2 .rf-arrow,
  .grid-editorial .range-feature-4 .rf-arrow {
    top: auto; bottom: 16px; margin-top: 0;
    left: 50%; right: auto; margin-left: -28px;
    width: 56px; height: 56px;
  }
  .grid-editorial .rf-arrow svg,
  .grid-editorial .range-feature-2 .rf-arrow svg,
  .grid-editorial .range-feature-4 .rf-arrow svg { transform: rotate(90deg); }
  .grid-editorial .rf-panel:hover .rf-arrow { transform: none; }
  .grid-editorial .rf-panel,
  .grid-editorial .range-feature-2.rf-panel,
  .grid-editorial .range-feature-4.rf-panel { padding: 24px 24px 96px; justify-content: flex-end; }
  .grid-editorial .range-feature-2.rf-panel .rf-content,
  .grid-editorial .range-feature-4.rf-panel .rf-content { align-items: flex-start; text-align: left; }
  .grid-editorial .range-feature-2.rf-panel .rf-title,
  .grid-editorial .range-feature-4.rf-panel .rf-title { align-self: flex-start; }
  .grid-editorial .rf-panel::after,
  .grid-editorial .range-feature-2.rf-panel::after,
  .grid-editorial .range-feature-4.rf-panel::after {
    background: linear-gradient(0deg, rgba(4,14,22,.85) 0%, rgba(4,14,22,.55) 45%, rgba(4,14,22,.1) 80%);
  }
}
/* The models that make up a range, set under the range name on its panel. */
.grid-editorial .rf-models {
  margin: 0;
  font-size: clamp(16px, 1.4vw, 20px);
  font-weight: 600;
  line-height: var(--lh-title);
  color: #fff;
}

@media (max-width: 767px) {
  .grid.grid-editorial { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-editorial .range-feature,
  .grid-editorial .range-feature-2,
  .grid-editorial .range-feature-3,
  .grid-editorial .range-feature-4,
  .grid-editorial .range-feature-5 {
    grid-column: 1 / -1;
    grid-row: auto;
    height: auto;
    aspect-ratio: 370/537;
  }
  .grid-editorial .rf-detail { width: 100%; }
}

/* ===================================================================
   RANGE HELP BAR  (.range-help)
   -------------------------------------------------------------------
   Replaces the dashed .testimonial-empty placeholder that used to close
   each collection grid. That box was a notice; this is an offer — so it
   reads as a card with a single action rather than a fenced-off note.

   Layout is one row: a round icon chip, the copy column (eyebrow /
   question / answer), then the WhatsApp button pinned right. The copy
   column takes the free space (1fr) so the icon and the button keep
   their intrinsic widths and the bar reflows cleanly.
   =================================================================== */
.range-help {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 18px;
  /* Capped and centred rather than spanning the container: at full width the
     three items were flung to opposite edges with a lake of dead space
     between the copy and the button. The cap keeps them as one group. */
  max-width: 720px;
  margin: 28px auto 0;
  padding: 16px 20px;
  background: var(--white);
  border: 1px solid var(--blue-100);
  border-radius: var(--radius-md);
  /* The bar's own type scale. Every size inside is an em off this, so the
     whole block steps down together and stays in proportion. */
  font-size: 15px;
}
/* The icon chip. Tinted rather than solid: at this size a filled brand
   circle pulled focus off the green button, which is the actual action. */
.range-help .rh-icon {
  width: 36px;
  height: 36px;
  flex: none;
  border-radius: 50%;
  background: var(--blue-100);
  color: var(--blue-600);
  display: flex;
  align-items: center;
  justify-content: center;
}
.range-help .rh-icon svg { width: 17px; height: 17px; }

.range-help .rh-eyebrow {
  display: block;
  font-size: .7333em;   /* 11px */
  font-weight: var(--fw-label);
  line-height: var(--lh-tight);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--blue-600);
}
.range-help .rh-title {
  margin: 4px 0 0;
  /* Well under --fs-card-title (20px): this is a prompt sitting below a
     grid of product cards, so it must not out-rank their titles. */
  font-size: 1em;   /* 15px */
  font-weight: var(--fw-strong);
  line-height: var(--lh-tight);
  color: var(--ink-900);
}
.range-help .rh-note {
  margin: 3px 0 0;
  font-size: .8667em;   /* 13px */
  line-height: var(--lh-tight);
  color: var(--ink-500);
}
/* The button is already a .btn .btn-whatsapp; it only needs to stop
   stretching to the grid cell's full height. */
.range-help .btn {
  flex: none;
  white-space: nowrap;
  font-size: .8667em;   /* 13px */
  padding: .6rem 1.05rem;
}
.range-help .btn svg { width: 16px; height: 16px; }

/* Under ~620px the three-column row would crush the copy, so the bar
   stacks: icon and copy stay on one line, the button drops full-width
   beneath them where it stays an easy tap target. The breakpoint sits
   below the 720px max-width so the bar holds its single row all the way
   down to its capped size, and only stacks once the viewport is genuinely
   narrower than the layout needs. */
@media (max-width: 620px) {
  .range-help {
    grid-template-columns: auto minmax(0, 1fr);
    gap: 14px 16px;
  }
  .range-help .btn {
    grid-column: 1 / -1;
    width: 100%;
    justify-content: center;
  }
}

/* ===================================================================
   SUPPORT PAGE — ROUTE CARDS & MAINTENANCE PANEL
   -------------------------------------------------------------------
   The two sections under the support hero, rebuilt to the supplied
   design: photo-topped route cards, then a two-column maintenance
   block with an image panel. Colour comes entirely from the existing
   blue ramp and --gradient-* tokens; no new hues are introduced.
   Scoped to .support-routes / .maint so the shared .support-card,
   .feature-list and .section-head rules other pages use are untouched.
   =================================================================== */

/* ---------- Route cards (.route-card) ----------
   One <a> per card, so the whole tile is the link — the "Learn More" row
   is a visual affordance inside it, not a nested anchor. */
.support-routes .grid-3 { gap: clamp(18px, 2vw, 26px); }
.route-card {
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: 1px solid var(--blue-100);
  /* Just enough to take the hard point off the corner, no more. */
  border-radius: 6px;
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  transition: transform .28s ease, box-shadow .28s ease, border-color .28s ease;
}
.route-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 18px 44px rgba(7,81,116,.16);
  border-color: var(--blue-200);
}
/* The picture band. A fixed aspect keeps the three cards' copy on one
   baseline regardless of each source image's proportions. */
.route-card .rc-media {
  display: block;
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--blue-50);
  overflow: hidden;
}
.route-card .rc-media img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .5s ease;
  /* The picture dissolves into the card rather than stopping at a hard edge:
     the mask fades the lower band of the image to transparent so the white
     body shows through progressively. mask-image alone would leave a visible
     line on the img's own bottom edge, so the fade starts at 62% — well above
     it — and the ::after wash below carries the last of the blend. */
  -webkit-mask-image: linear-gradient(180deg, #000 82%, rgba(0,0,0,.72) 92%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 82%, rgba(0,0,0,.72) 92%, transparent 100%);
}
/* A short wash of the card's own ground over the image's foot. It sits on top
   of the masked picture and matches .route-card's background, so the two meet
   with no seam. Browsers without mask support still get this gradient, which
   softens the join on its own. */
.route-card .rc-media::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 24%;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.55) 55%, var(--white) 100%);
}
.route-card:hover .rc-media img { transform: scale(1.045); }

.route-card .rc-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  padding: clamp(20px, 2vw, 26px);
}
/* The icon overlaps up into the picture band, as in the design. */
.route-card .rc-icon {
  width: 44px; height: 44px;
  margin-top: -40px;
  border-radius: 13px;
  background: var(--white);
  color: var(--blue-700);
  border: 1px solid var(--blue-100);
  box-shadow: 0 6px 18px rgba(7,81,116,.14);
  display: flex; align-items: center; justify-content: center;
}
.route-card .rc-icon svg { width: 21px; height: 21px; }
.route-card .rc-title {
  font-family: var(--font-display, inherit);
  font-size: 1.18em;
  color: var(--heading-color);
  line-height: var(--lh-card, 1.25);
}
.route-card .rc-desc {
  font-size: var(--fs-label);
  line-height: var(--lh-body);
  color: var(--ink-500);
}
/* The circular arrow + label row. */
.route-card .rc-more {
  display: inline-flex; align-items: center; gap: 10px;
  margin-top: 4px;
  font-size: var(--fs-label);
  font-weight: var(--fw-label);
  color: var(--blue-700);
}
.route-card .rc-arrow {
  width: 30px; height: 30px;
  border-radius: 50%;
  background: var(--gradient-primary);
  color: var(--white);
  display: flex; align-items: center; justify-content: center;
  flex: none;
  transition: transform .28s ease;
}
.route-card .rc-arrow svg { width: 15px; height: 15px; }
.route-card:hover .rc-arrow { transform: translateX(3px); }

/* Per-card tints: the middle and right cards sit a step warmer / cooler so
   the row reads as three distinct routes, as in the design. Both are drawn
   from the existing ramp rather than new hues. */
/* The per-card image grounds are deliberately NOT tinted any more. They were a
   loading ground behind an opaque picture; now that the image fades out over
   its lower band, whatever sits behind it shows through the blend, and a warm
   or green ground read as a coloured band across the card. All three photos
   share a light blue-grey surround, so one neutral ground blends invisibly. */

/* ---------- Maintenance block (.maint) ---------- */
/* Left-aligned head, and the second half of the headline takes the brand
   primary — the two-tone treatment from the design. */
.maint-head { max-width: none; margin-bottom: clamp(26px, 3vw, 38px); text-align: left; }
.maint-head h2 span { color: var(--blue-600); }

.maint-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 420px);
  gap: clamp(28px, 4vw, 56px);
  align-items: start;
}

/* The tips: icon-in-tile rows on a hairline list. */
.maint-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.maint-list li {
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr);
  align-items: center;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid rgba(96,118,133,.16);
  font-size: var(--fs-label);
  line-height: var(--lh-body);
  color: var(--ink-700);
}
.maint-list li:last-child { border-bottom: 0; }
.maint-list .mi {
  width: 42px; height: 42px;
  border-radius: 12px;
  background: var(--blue-100);
  color: var(--blue-700);
  display: flex; align-items: center; justify-content: center;
  flex: none;
}
.maint-list .mi svg { width: 20px; height: 20px; }

/* ---------- The image panel (.maint-panel) ---------- */
.maint-panel {
  position: relative;
  border-radius: var(--radius-lg);
  background: var(--gradient-deep);
  padding: clamp(16px, 1.6vw, 22px);
  overflow: hidden;
}
.maint-panel .mp-img {
  width: 100%;
  aspect-ratio: 4 / 3.4;
  object-fit: cover;
  border-radius: var(--radius-md);
  display: block;
}
/* The round "Care Today" badge, top-right over the panel. */
.maint-panel .mp-badge {
  position: absolute;
  top: clamp(14px, 1.6vw, 22px);
  right: clamp(14px, 1.6vw, 22px);
  z-index: 2;
  width: 104px; height: 104px;
  border-radius: 50%;
  background: rgba(255,255,255,.94);
  color: var(--blue-800);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  line-height: 1.15;
  box-shadow: 0 10px 26px rgba(5,61,87,.28);
}
.maint-panel .mp-badge strong { font-size: 1.05em; font-weight: 600; }
.maint-panel .mp-badge span { font-size: 1.05em; }
.maint-panel .mp-badge em {
  font-style: normal;
  margin-top: 4px;
  font-size: 9px;
  letter-spacing: var(--ls-label-tight, .02em);
  text-transform: uppercase;
  color: var(--ink-500);
  max-width: 8em;
}
/* The three-stat strip across the panel's lower edge. */
.maint-panel .mp-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin-top: clamp(12px, 1.4vw, 18px);
  text-align: center;
  font-size: 11px;
  line-height: 1.3;
  color: rgba(255,255,255,.92);
}
.maint-panel .mp-stats > div {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 12px 6px;
  border-radius: var(--radius-sm);
  background: rgba(255,255,255,.09);
}
.maint-panel .mps-icon { color: var(--blue-300); }
.maint-panel .mps-icon svg { width: 18px; height: 18px; }

@media (max-width: 900px) {
  .maint-grid { grid-template-columns: minmax(0, 1fr); }
  /* The panel leads on narrow screens: the picture introduces the list
     rather than trailing a long scroll of tips. */
  .maint-panel { order: -1; }
  .maint-head { text-align: left; }
}
@media (max-width: 560px) {
  .maint-panel .mp-badge { width: 86px; height: 86px; }
  .maint-panel .mp-badge em { display: none; }
}

/* ==========================================================================
   WHY JAGRUTI — editorial page
   Two movements, in the order a visitor meets them:
     1. .wj-hero   full-bleed photograph, copy centred low in the frame
     2. .wj-split  a 50/50 panel: tinted column of copy beside the photo
   The headline voice throughout is the shared display face at the site's own
   sizes (--fs-h1/--fs-h3 clamps, --lh-title, --ls-display). No italics and no
   bespoke scale: emphasis comes from size and space, not a second style.
   ========================================================================== */

.wj-hero {
  position: relative;
  /* svh, not vh: mobile browser chrome must not push the copy out of frame. */
  min-height: max(100svh, 640px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--header-h) 0 clamp(40px, 7vh, 80px);
  background: #1b1512;
  color: #fff;
  overflow: hidden;
  text-align: center;
}
.wj-hero-img {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 42%;
}
/* Two stacked washes: a vertical one so the type never sits on raw highlight,
   and a soft centre pool that holds the headline specifically. */
.wj-hero::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(70% 50% at 50% 68%, rgba(12,9,7,.58) 0%, transparent 100%),
    linear-gradient(180deg, rgba(12,9,7,.42) 0%, transparent 30%, transparent 46%, rgba(12,9,7,.52) 100%);
}
.wj-hero .container { position: relative; z-index: 2; }
.wj-hero-inner {
  max-width: 44rem;
  margin: 0 auto;
  /* Pushes the block well below centre — the picture reads first, the words
     arrive under it. */
  padding-top: clamp(120px, 34vh, 340px);
}

.wj-hero h1 {
  margin: 0;
  color: #fff;
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-display);
  font-size: clamp(var(--fs-h2), 4.6vw, 66px);
  text-wrap: balance;
  text-shadow: 0 2px 30px rgba(0,0,0,.28);
}
.wj-hero p {
  margin: 1.1em auto 0;
  max-width: 30rem;
  color: rgba(255,255,255,.92);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  text-shadow: 0 1px 18px rgba(0,0,0,.34);
}

/* --- 2. The split panels ------------------------------------------------- */
/* Full-bleed: the panel breaks the container so colour meets photo edge to
   edge, exactly as the reference does. Copy keeps its own inner measure. */
.wj-split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: stretch;
  min-height: clamp(520px, 86vh, 820px);
  /* align-items must stay stretch (the default) so the copy track runs the
     panel's full height — a sticky child can only travel inside its own
     grid area. */
  background: var(--brand-tint);
}
/* The copy column is the stretched grid track; it carries the ground for the
   panel's whole height. The travel belongs to .wj-split-copy-inner, exactly as
   .compare-studio splits .cs-rail from .cs-rail-inner: a sticky box as tall as
   its own container has nowhere to go and would just scroll away with the page.

   No padding and no centring here — the inner block's sticky offset does the
   positioning, so this track must stay a plain full-height column. */
.wj-split-copy { display: block; text-align: center; }
/* Sticky, so the copy holds near the top of the viewport while the panel
   scrolls past it, then releases at the panel's end — which reads as the text
   travelling DOWN its column relative to the photo beside it. Pure CSS: no
   scrub, no JS, and it degrades to a normal static block wherever position
   sticky is unsupported.

   The top offset clears the fixed header, which overlays the page everywhere.
   max-width keeps the measure; the auto margins centre it in the track. */
.wj-split-copy-inner {
  position: sticky;
  top: calc(var(--header-h) + clamp(28px, 9vh, 96px));
  max-width: 30rem;
  margin: 0 auto;
  /* Bottom padding is what gives the sticky block its runway: it holds until
     this much of the column is left, so the release lands just inside the
     panel rather than at its very edge. */
  padding: clamp(48px, 10vh, 110px) clamp(28px, 6vw, 72px) clamp(56px, 12vh, 132px);
}
.wj-split h2 {
  margin: 0 0 .7em;
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  color: var(--ink-900);
  font-size: clamp(1.5em, 2.1vw, 2em);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-display);
}
.wj-split p {
  margin: 0 0 1.1em;
  color: var(--ink-700);
  font-size: .95em;
  line-height: 1.7;
}
.wj-split p:last-of-type { margin-bottom: 0; }
.wj-split-media { position: relative; overflow: hidden; background: #1b1512; min-height: 320px; }
.wj-split-media img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 45%;
}
/* Reversed panel: photo left, copy right, on the warmer ground. */
.wj-split.is-reversed { background: var(--brand-canvas); }
.wj-split.is-reversed .wj-split-media { order: -1; }

@media (max-width: 900px) {
  /* Stacked. The picture leads each panel so the page keeps alternating
     image/word rather than running two text blocks together. */
  .wj-split { grid-template-columns: minmax(0, 1fr); min-height: 0; }
  .wj-split-media { order: -1; min-height: clamp(280px, 52vh, 420px); }
  .wj-split.is-reversed .wj-split-media { order: -1; }
  /* Stacked: there is no second column to travel against, so the copy is a
     plain static block again. */
  .wj-split-copy-inner {
    position: static;
    padding: clamp(40px, 8vh, 72px) clamp(24px, 6vw, 48px);
  }
  .wj-hero-inner { padding-top: clamp(24px, 10vh, 90px); }
}
@media (max-width: 560px) {
}

/* --- 3. The nine reasons -------------------------------------------------
   Replaces the glass-card grid on this page only (index.html keeps .why-grid).
   Glass needs a dark or photographic ground to read as a material; on this
   page's light canvas it was nearly invisible and the 4/4/1 wrap left a lone
   card stranded on its own row.

   The layout is a pinned title beside a single scrolling column: the heading
   holds while the nine entries pass it, so the section reads as one continuous
   argument rather than a field of tiles. Each entry is a full-width row —
   number, then heading and copy side by side — which gives the prose a long
   measure and keeps every heading on one line. */
.wj-reasons-layout {
  display: grid;
  grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
  gap: clamp(32px, 6vw, 96px);
  align-items: start;
}
/* Pinned beside the list. --header-h clears the fixed bar, which overlays the
   page on every route. */
.wj-reasons-head {
  position: sticky;
  top: calc(var(--header-h) + clamp(24px, 5vh, 56px));
}
.wj-reasons-head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: clamp(var(--fs-h3), 3.2vw, 46px);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-display);
  color: var(--ink-900);
}

.wj-reasons {
  display: grid;
  /* Equal-height entries: every row takes the height of the tallest, so the
     nine rules land on a consistent rhythm instead of tracking copy length. */
  grid-auto-rows: 1fr;
  border-top: 1px solid var(--blue-100);
}

/* One row per reason: a narrow number rail, then heading and copy as two
   columns so the entry reads across rather than stacking. */
.wj-reason {
  display: grid;
  grid-template-columns: 4.25rem minmax(0, 12rem) minmax(0, 1fr);
  column-gap: clamp(16px, 2.4vw, 40px);
  align-items: start;
  align-content: center;
  /* A single row: the rail holds number and icon side by side, so nothing
     stacks and the entry is only as tall as its copy. */
  grid-template-rows: auto;
  padding: clamp(12px, 1.3vw, 17px) clamp(14px, 1.6vw, 24px);
  /* Cancels that inner padding at the column edges: the hairline and the
     number rail stay flush with the list, while the hover wash still bleeds
     a little past the text on both sides. */
  margin-inline: calc(-1 * clamp(14px, 1.6vw, 24px));
  border-bottom: 1px solid var(--blue-100);
  /* The row is the positioning context for the two overlays below. */
  position: relative;
  isolation: isolate;
  transition: background .45s cubic-bezier(.22,.61,.36,1);
}
/* A wash that fades in from the left rather than a flat fill: the entry
   lights up along the reading direction. Sits behind the copy via z-index. */
.wj-reason::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(90deg,
    rgba(13,138,196,.075) 0%,
    rgba(13,138,196,.03) 45%,
    transparent 100%);
  opacity: 0;
  transition: opacity .45s cubic-bezier(.22,.61,.36,1);
}
/* A brand rule that draws in along the row's bottom edge, over the hairline.
   scaleX from the left mirrors the wash, so the two read as one gesture. */
.wj-reason::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: linear-gradient(90deg, var(--blue-600), rgba(13,138,196,.12));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .55s cubic-bezier(.22,.61,.36,1);
}
.wj-reason:hover::before { opacity: 1; }
.wj-reason:hover::after  { transform: scaleX(1); }
.wj-reason-rail {
  grid-column: 1;
  display: flex;
  align-items: center;
  gap: .5em;
}
.wj-reason-num {
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: .8em;
  letter-spacing: .12em;
  line-height: 1;
  /* Starts as a faint index and resolves to brand blue on hover — the rail
     is what tells you which row you are on. */
  color: var(--ink-300);
  /* Tabular figures so 01–09 hold one width and the rail never shifts. */
  font-variant-numeric: tabular-nums;
  transition: color .45s ease;
}
.wj-reason:hover .wj-reason-num { color: var(--blue-600); }
/* The icon sits in the number rail beneath its digit, so the rail carries the
   entry's two markers and the heading column stays pure type. */
.wj-reason-icon {
  grid-column: 1;
  display: block;
  color: var(--blue-600);
  opacity: .5;
  transition: opacity .45s ease;
}
.wj-reason:hover .wj-reason-icon { opacity: 1; }
.wj-reason-icon svg { width: 18px; height: 18px; display: block; }
.wj-reason h3 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: 1.12em;
  line-height: var(--lh-title);
  letter-spacing: var(--ls-display);
  color: var(--ink-900);
  /* A few pixels of travel, in the same direction as the wash. Enough to
     register as response; not enough to reflow the row. */
  transition: transform .45s cubic-bezier(.22,.61,.36,1), color .45s ease;
}
.wj-reason:hover h3 { transform: translateX(4px); color: var(--blue-700); }
.wj-reason p {
  margin: 0;
  font-size: .95em;
  line-height: 1.6;
  color: var(--ink-500);
  transition: color .45s ease;
}
.wj-reason:hover p { color: var(--ink-700); }

@media (max-width: 1000px) {
  /* The title unpins and leads the list. */
  .wj-reasons-layout { grid-template-columns: minmax(0, 1fr); gap: clamp(24px, 4vh, 40px); }
  .wj-reasons-head { position: static; }
}
@media (max-width: 680px) {
  /* Copy drops under the heading, number keeps its rail. */
  .wj-reason { grid-template-columns: 4.25rem minmax(0, 1fr); row-gap: .3em; }
  .wj-reason { grid-template-rows: auto auto; }
  .wj-reason h3 { grid-column: 2; grid-row: 1; }
  .wj-reason p  { grid-column: 2; grid-row: 2; }
}

/* ==========================================================================
   ABOUT PAGE — editorial layout
   --------------------------------------------------------------------------
   A magazine-style treatment: an oversized Marcellus statement set hard to
   the left gutter, a hairline rule under it, then a caption/body row that
   hangs a small grey label in the left column and runs the prose in the
   columns to its right.

   The ground here is white rather than --page-bg. That is the one deliberate
   departure from the "one page ground" rule at the top of this file: these
   sections are typographic spreads, and the canvas tint muddies the contrast
   the huge serif depends on. It is scoped to .about-page so nothing else on
   the site inherits it.
   ========================================================================== */
.about-page { background: var(--white); }

/* ---- The editorial spread ---------------------------------------------
   One shared skeleton. Every band on this page is an .ab-spread, so the
   rule, the gutters and the column starts line up down the whole page. */
.ab-spread {
  padding: clamp(72px, 11vh, 150px) 0 clamp(56px, 8vh, 104px);
  background: var(--white);
}
.ab-spread + .ab-spread { padding-top: clamp(48px, 7vh, 96px); }

/* About-page ink tiers. The page is a long read on white, and the site-wide
   --ink-500/--ink-300 sat light enough here that sub-lines and side captions
   looked washed out against the letter. These sit a step darker while keeping
   the same three-tier hierarchy — scoped to .ab-* so nothing else shifts. */
:root {
  --ab-ink-sub:     #4a5c6a;  /* sub-lines: role, notes, journey copy */
  --ab-ink-caption: #7b8a95;  /* side captions in the left rail */
}

/* The statement is a NORMAL section heading, so it carries no typography of
   its own: family, size, weight, leading and tracking all come from the
   section-heading system at the end of this file, which .ab-statement is
   listed in. Only layout and colour live here — per that system's rule that
   layout is not typography. */
.ab-statement {
  margin: 0;
  color: var(--ink-900);
  text-wrap: balance;
  max-width: 20ch;
}
/* A statement that runs long (the founder's letter opener) needs room to
   breathe across more of the measure. */
.ab-statement.is-wide { max-width: 22ch; }

/* The hairline. The reference's signature move — it separates the statement
   from the caption row and is what makes the page read as a spread rather
   than as stacked sections. */
.ab-rule {
  border: 0;
  border-top: 1px solid rgba(23, 32, 38, .16);
  margin: clamp(32px, 5vh, 64px) 0 clamp(28px, 4vh, 48px);
}

/* ---- Caption / body row -------------------------------------------------
   Left column holds the small grey caption, the remaining columns hold the
   prose. Twelve-column base so a row can run one, two or three body columns
   without inventing a new grid each time. */
.ab-row {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: clamp(28px, 4vh, 48px);
  align-items: start;
}
.ab-row-caption { grid-column: 1 / span 4; }
/* Two body columns starting at the halfway mark, as the reference sets it. */
.ab-row-body { grid-column: 6 / span 4; }
.ab-row-body + .ab-row-body { grid-column: 10 / span 3; }
/* A single wide prose column (the founder's letter) instead of two. */
.ab-row-prose { grid-column: 6 / span 7; }

.ab-caption {
  margin: 0;
  font-size: var(--fs-small);
  line-height: var(--lh-body);
  color: var(--ab-ink-caption);
}
.ab-col-head {
  margin: 0 0 .6em;
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: var(--fs-small);
  line-height: var(--lh-card);
  letter-spacing: var(--ls-label-tight);
  color: var(--ink-900);
}
.ab-col-body { margin: 0 0 1em; font-size: .875em; line-height: 1.65; color: var(--ink-700); }
.ab-col-body:last-child { margin-bottom: 0; }
/* The letter sets a step larger than the two-column notes — it is the page's
   main read, not a sidebar. */
.ab-row-prose .ab-col-body { font-size: var(--fs-small); line-height: var(--lh-body); }

.ab-signoff {
  margin: clamp(24px, 3.5vh, 40px) 0 0;
  font-size: .875em;
  line-height: 1.5;
  color: var(--ink-900);
}
.ab-signoff span { display: block; color: var(--ab-ink-sub); }

/* The director's name carries the letter — set it up a size and a weight so
   the eye lands on it, and let the role below stay at signoff scale. */
.ab-signoff-name {
  display: block;
  font-size: 1.35em;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--ink-900);
  margin-bottom: 2px;
}

/* ---- Full-bleed media ---------------------------------------------------
   Breaks the gutter and runs edge to edge, the way the reference drops its
   video in under the opening spread. */
.ab-media {
  position: relative;
  width: 100%;
  /* The clip is 1920x1080, and the whole frame is meant to be seen — so the
     VIDEO's aspect ratio sets the height of this band rather than a height
     being imposed and the picture cropped to fill it. At full-bleed width
     that gives a section-height band on a normal desktop window (1440 wide
     → 810 tall) with nothing cut off any edge.

     This is the opposite choice from .range-media on the home page, which
     uses cover because its BOX narrows around the video as the panel
     animates. Here the box is a fixed full-width band, so cover would only
     ever mean crop.

     --ab-media-crop is how far the picture is pushed past the band so the
     edges are trimmed and the framing tightens. The band keeps the clip's
     own ratio, so the same factor trims top, bottom and both sides by the
     same proportion — a small even cut in from every edge, not a reframe.
     1.06 takes roughly 3% off each side. */
  --ab-media-crop: 1.06;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--ink-900);
}
/* cover, so a replacement clip of a different shape still fills the band
   rather than letterboxing against the dark ground. With the band already
   carrying this clip's own ratio, cover alone would show the full frame —
   the scale above it is what trims the edges.

   Scaled from the centre so the trim is even on all four sides; the band
   clips the overspill via overflow:hidden. */
.ab-media video,
.ab-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transform: scale(var(--ab-media-crop, 1));
  transform-origin: 50% 50%;
}
/* No height override on phones: the 16/9 ratio above already gives a sensible
   band on a narrow viewport (390 wide → 219 tall) and keeps the full frame
   visible, which a fixed height would letterbox. */

/* ---- Numbered list (the journey) ---------------------------------------
   The timeline re-set as editorial rows: a tabular index in its own rail,
   heading and copy in the column beside it, hairline between entries. */
.ab-list { display: grid; gap: 0; }
.ab-item {
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr);
  column-gap: var(--gutter);
  padding: clamp(22px, 3vh, 34px) 0;
  border-top: 1px solid rgba(23, 32, 38, .12);
}
.ab-item:last-child { border-bottom: 1px solid rgba(23, 32, 38, .12); }
.ab-item-num {
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: .875em;
  line-height: var(--lh-card);
  letter-spacing: var(--ls-label);
  /* Tabular figures so 01–06 hold one width and the rail never shifts. */
  font-variant-numeric: tabular-nums;
  color: var(--blue-600);
}
.ab-item-text { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); column-gap: var(--gutter); row-gap: .4em; }
.ab-item h3 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: var(--fs-h6);
  line-height: var(--lh-card);
  letter-spacing: var(--ls-display);
  color: var(--ink-900);
}
.ab-item p { margin: 0; font-size: .875em; line-height: 1.65; color: var(--ab-ink-sub); }

/* ---- Three-up capability columns (manufacturing) ----------------------- */
.ab-cols {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: clamp(32px, 5vh, 56px);
}
.ab-cols > div { border-top: 1px solid rgba(23, 32, 38, .16); padding-top: clamp(18px, 2.5vh, 26px); }

/* ---- Image + text pair -------------------------------------------------- */
.ab-pair {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: clamp(28px, 4vh, 44px);
  align-items: center;
}
.ab-pair-media { grid-column: 1 / span 6; }
.ab-pair-media img { width: 100%; height: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; }
.ab-pair-text { grid-column: 8 / span 5; }

/* ---- Closing call to action -------------------------------------------- */
.ab-cta { border-top: 1px solid rgba(23, 32, 38, .16); padding-top: clamp(40px, 6vh, 72px); }
.ab-cta .hero-cta { margin-top: clamp(24px, 3.5vh, 36px); }

/* ---- Responsive ---------------------------------------------------------
   The grid collapses in two steps. At tablet the caption unpins and leads
   its row full width; at phone the numbered rail narrows and the item's
   heading and copy stack. */
@media (max-width: 1000px) {
  .ab-row-caption,
  .ab-row-body,
  .ab-row-body + .ab-row-body,
  .ab-row-prose { grid-column: 1 / -1; }
  .ab-row { row-gap: clamp(20px, 3vh, 32px); }
  .ab-statement,
  .ab-statement.is-wide { max-width: none; }
  .ab-cols { grid-template-columns: minmax(0, 1fr); }
  .ab-pair-media,
  .ab-pair-text { grid-column: 1 / -1; }
  .ab-item-text { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 680px) {
  .ab-item { grid-template-columns: 2.75rem minmax(0, 1fr); column-gap: 16px; }
}

/* ==========================================================================
   Blog — editorial layout
   ==========================================================================
   A light, magazine-style index and article page, modelled on the reference
   layout the brief supplied: a breadcrumb rail, an oversized uppercase page
   title, a "Filter by Tag" select, and a two-up card grid whose photos carry
   a floating date chip.

   This block sits AFTER the older .blog-card rules near the "---------- Blog
   ----------" comment above and deliberately supersedes them, so the two
   coexist without either being deleted: pages that still use the old gradient
   thumb keep working, while .blog-grid pages get the editorial treatment.
   ========================================================================== */

/* ---- Index header: breadcrumb, title, filter rail ---- */

/* The index opens on PAPER, not the dark .page-hero gradient the rest of the
   site uses, so these pages carry data-nav="flow" and reserve header height
   instead of running up under the bar. */
/* The header floats transparent over the page in every nav mode, so a section
   that opens a page must reserve its height itself — `flow` only adds that
   padding for `.pd-top`. Without this the breadcrumb rail renders underneath
   the MENU button and the logo. */
.blog-index-head,
.article-head {
  padding: calc(var(--header-h) + 26px) 0 0;
  background: var(--white);
}

.blog-crumbs {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 10px;
  padding-bottom: 22px;
  border-bottom: 1px solid var(--blue-100);
  font-size: var(--fs-label);
  color: var(--ink-500);
}
.blog-crumbs a { color: var(--ink-700); text-decoration: none; }
.blog-crumbs a:hover { color: var(--blue-600); text-decoration: underline; }
/* The chevron is drawn as a separator rather than typed, so it never lands in
   a copied string or a screen reader's output. */
.blog-crumbs .sep {
  width: 6px; height: 6px;
  border-right: 1.5px solid var(--ink-300);
  border-top: 1.5px solid var(--ink-300);
  transform: rotate(45deg);
  flex: none;
}
.blog-crumbs [aria-current="page"] { color: var(--ink-500); }

/* The poster title. Marcellus ships one weight, so the presence here comes
   from size and tracking rather than a synthesised bold. */
.blog-index-title {
  margin: 46px 0 0;
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  /* --fs-h1, the same step every other page title on the site uses, rather
     than a bespoke clamp — the index title is a page title like any other. */
  font-size: var(--fs-h1);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
  text-transform: uppercase;
  color: var(--ink-900);
}
.blog-index-intro {
  max-width: 62ch;
  margin: 18px 0 0;
  /* --fs-small, the same step the article prose uses, rather than --fs-lead
     (19.93px): the lead size made this standfirst read a full step larger
     than any other paragraph on the blog. */
  font-size: var(--fs-small);
  line-height: var(--lh-body);
  color: var(--ink-700);
}

/* ---- Filter by Tag ---- */
.blog-filter {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 16px;
  margin: 38px 0 0;
}
.blog-filter-label {
  /* Matches the select beside it and the intro above, instead of the
     much larger --fs-lead. */
  font-size: var(--fs-small);
  color: var(--ink-900);
}
/* The chevron is a background element on the wrapper, not on the select, so
   it survives `appearance: none` across engines and keeps one hit target. */
.blog-select-wrap { position: relative; display: inline-block; }
.blog-select-wrap::after {
  content: "";
  position: absolute; right: 18px; top: 50%;
  width: 8px; height: 8px;
  margin-top: -6px;
  border-right: 1.6px solid var(--ink-700);
  border-bottom: 1.6px solid var(--ink-700);
  transform: rotate(45deg);
  pointer-events: none;
}
.blog-select {
  appearance: none; -webkit-appearance: none;
  min-width: 260px;
  padding: 13px 46px 13px 20px;
  border: 1px solid var(--blue-200);
  border-radius: var(--radius-pill);
  background: var(--white);
  font-family: inherit;
  font-size: var(--fs-label);
  color: var(--ink-900);
  cursor: pointer;
}
.blog-select:hover { border-color: var(--blue-400); }
.blog-select:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
  border-color: var(--blue-600);
}

/* ---- The card grid ----
   Two up, matching the reference. .blog-grid is its own grid rather than a
   .grid-3 reuse because the editorial card wants a much wider column than the
   three-up product rows elsewhere on the site. */
.blog-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Tight, near-flush columns with just enough room under each card for its
     caption — the editorial rail the brief pointed at, rather than the wide
     52/40 gutters a card-panel grid needs. */
  gap: 34px 10px;
  margin-top: 52px;
}

/* The editorial card: no panel, no border, no shadow — image, then text on
   the page ground, exactly as the reference sets it. These rules are scoped
   under .blog-grid so the older bordered .blog-card used elsewhere (inside
   article bodies, related rails) is left untouched. */
.blog-grid .blog-card {
  display: block;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: visible;
  text-decoration: none;
  color: inherit;
}

.blog-grid .blog-card .thumb {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  /* Barely-rounded, near-square corners, matching the reference rail. The
     22px --radius-lg read as a soft card panel; this reads as a still. */
  border-radius: 4px;
  overflow: hidden;
  /* Doubles as the fallback ground when a post has no photo yet: the card
     still reads as a card instead of collapsing to a white gap. */
  background: linear-gradient(135deg, var(--blue-600), var(--blue-300));
}
.blog-grid .blog-card .thumb img {
  /* Positioned, so it paints ABOVE the absolutely-positioned .ph fallback
     label underneath it. Left static, the img lost the stacking contest to
     .ph and the category word showed through the photo. */
  position: absolute; inset: 0;
  z-index: 1;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .5s cubic-bezier(.22, .61, .36, 1);
}
.blog-grid .blog-card:hover .thumb img { transform: scale(1.04); }

/* The category word shown on the gradient when no photo has been supplied.
   .thumb img paints above it, so once a real image lands in
   assets/images/Resources/Blog/ the label is simply covered — no markup
   change needed. */
.blog-grid .blog-card .thumb .ph {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 0 24px;
  text-align: center;
  color: rgba(255, 255, 255, .92);
  font-size: var(--fs-lead);
  letter-spacing: var(--ls-label-tight);
}

/* The floating date chip, top-left over the photo.
   Scoped through .blog-card .thumb so it outweighs the older
   `.blog-card .thumb span { color: #fff }` rule further up the sheet — the
   chip is a <span> inside the thumb, so that rule was painting its text white
   on a white chip and the date was invisible. */
.blog-grid .blog-card .thumb .blog-date,
.article-hero .blog-date,
.blog-date {
  position: absolute; top: 14px; left: 14px;
  z-index: 2;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 14px;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, .94);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: var(--shadow-sm);
  font-size: var(--fs-tag);
  letter-spacing: var(--ls-label-tight);
  text-transform: uppercase;
  color: var(--ink-900);
  /* Both inherited from the older .thumb span rule; the chip is its own
     opaque surface and must not be dimmed with the placeholder label. */
  opacity: 1;
  font-weight: var(--fw-label);
  white-space: nowrap;
}
.blog-date svg {
  width: 14px; height: 14px;
  flex: none;
  color: var(--blue-600);
}

.blog-grid .blog-card .body { padding: 20px 2px 0; }
.blog-grid .blog-card .cat {
  font-size: var(--fs-tag);
  letter-spacing: var(--ls-label-tight);
  text-transform: uppercase;
  color: var(--blue-600);
}
.blog-grid .blog-card h3 {
  margin: 10px 0 0;
  /* The shared card-title step, so a blog card's headline sits at the same
     size as a product card's on every other page. */
  font-size: var(--fs-card-title);
  line-height: var(--lh-card);
  color: var(--ink-900);
}
/* The reference underlines the headline on hover and leaves the standfirst
   alone, which keeps the affordance on the link target itself. */
.blog-grid .blog-card:hover h3 {
  color: var(--blue-700);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.blog-grid .blog-card p {
  margin: 12px 0 0;
  font-size: var(--fs-card-desc);
  line-height: var(--lh-body);
  color: var(--ink-700);
}

/* ---- Article page ----
   The same editorial language as the index, so a click does not change
   worlds: the breadcrumb rail carries over, the headline uses the index's
   display ramp, and the lead photo repeats the card's rounded frame. */
/* (.article-head's padding/background is set alongside .blog-index-head above,
   so both page types open on the same white band below the floating header.) */
/* Wider than .article-body's 760px measure: the headline block and the lead
   image are allowed to breathe past the reading column, as on the reference. */
.article-lede { max-width: 900px; margin: 0 auto; }
.article-kicker {
  margin: 40px 0 0;
  font-size: var(--fs-tag);
  letter-spacing: var(--ls-label-tight);
  text-transform: uppercase;
  color: var(--blue-600);
}
.article-title {
  margin: 14px 0 0;
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  /* --fs-h3 matches what .article-body h1 already used on these pages before
     the rebuild, so the headline size did not change when the layout did. */
  font-size: var(--fs-h3);
  line-height: var(--lh-title);
  letter-spacing: var(--ls-title);
  color: var(--ink-900);
}

.article-hero {
  position: relative;
  margin: 34px 0 0;
  aspect-ratio: 16 / 7;
  border-radius: 4px;
  overflow: hidden;
  background: linear-gradient(135deg, var(--blue-600), var(--blue-300));
}
/* Positioned above the .ph fallback for the same reason as the card thumb. */
.article-hero img { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; object-fit: cover; display: block; }
.article-hero .ph {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: rgba(255, 255, 255, .92);
  font-size: var(--fs-lead);
  letter-spacing: var(--ls-label-tight);
}

/* Meta rail under the headline: author, date, read time. */
.article-lede .article-meta { margin: 20px 0 0; }
.article-lede .article-meta .dot { color: var(--ink-300); }

/* ---- Related posts, closing an article ---- */
.blog-related { margin-top: 64px; padding-top: 44px; border-top: 1px solid var(--blue-100); }
.blog-related h2 {
  margin: 0 0 30px;
  font-size: var(--fs-h5);
  text-transform: uppercase;
  letter-spacing: -0.005em;
  color: var(--ink-900);
}
/* Three across inside the narrower related rail, where the index runs two. */
.blog-related .blog-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 30px 10px; margin-top: 0; }

/* ---- Responsive ---- */
@media (max-width: 900px) {
  .blog-grid { grid-template-columns: minmax(0, 1fr); gap: 30px; }
  .blog-related .blog-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .blog-index-title { margin-top: 34px; }
}
@media (max-width: 680px) {
  .blog-filter { gap: 10px; }
  .blog-select-wrap { display: block; width: 100%; }
  .blog-select { min-width: 0; width: 100%; }
  .blog-grid { margin-top: 38px; }
  .blog-related .blog-grid { grid-template-columns: minmax(0, 1fr); }
  .article-kicker { margin-top: 28px; }
  .article-hero { aspect-ratio: 16 / 10; }
}

/* ---- "Recommended Models" on an article page ----
   The card grid is lifted OUT of .article-body (a 760px reading measure) and
   into its own full-width band, so the three product cards get the same room
   they have on the products page instead of being squeezed into the prose
   column. Heading matches the related-posts rail above it. */
.blog-recommended { padding-top: 0; }
/* Heading and card grid both sit on the same centred 900px column as
   .article-lede, which carries the article headline and the "Keep Reading"
   rail above — so the whole band lines up with everything above it. */
.blog-recommended > .container > h2,
.blog-recommended > .container > .grid {
  max-width: 900px;
  margin-inline: auto;
}
/* The tiles use .grid-editorial — the flat, gallery-style listing the
   products category pages use (photo, then a name / Get Price row, no card
   chrome). That grid ships at four across for a full-width page; three fits
   the three recommended models inside the narrower 900px column. */
.blog-recommended > .container > .grid-editorial {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.blog-recommended > .container > h2 {
  /* Block margins only — the shorthand would reset the `margin-inline: auto`
     above and drop the heading back to the page edge. */
  margin-block: 0 30px;
  font-size: var(--fs-h5);
  letter-spacing: -0.005em;
  color: var(--ink-900);
}
@media (max-width: 680px) {
  .blog-recommended > .container > .grid-editorial { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------- Product pages: light-blue ground ----------
   Client request: the product pages (everything under /products/) run on a
   soft light-blue gradient instead of the near-white --page-bg. The gradient
   sits on <main> so it spans the whole content column in one sweep; the
   sections inside go transparent so it isn't restarted per section. Cards,
   tables and pills keep their own white/tinted fills on top of it. */
body.page-products { background: var(--blue-200); }
body.page-products > main {
  background: linear-gradient(180deg, var(--blue-200) 0%, #a1d5ed 50%, var(--blue-300) 100%);
}
body.page-products .section,
body.page-products .section-alt,
body.page-products .pd-top { background: transparent; }

/* ==========================================================================
   Back-to-top button — injected on every page by initBackToTop() in main.js.
   A slim frosted-glass pill tucked into the bottom-right corner: chevron over
   a small "TOP" label. A brand-blue fill rises inside it as the page is read,
   so it doubles as a reading-progress gauge; on hover it fills completely.
   ========================================================================== */
.to-top {
  --tt-fill: 0;
  position: fixed; z-index: 60;
  /* Tight to the edge so it clears the footer's "Designed by" credit. */
  right: 4px;
  bottom: calc(var(--mobile-bar-h) + 18px);
  width: 36px; height: 54px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  padding: 0; border-radius: 999px; cursor: pointer; overflow: hidden;
  isolation: isolate;
  color: var(--blue-800);
  background: rgba(255, 255, 255, .72);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  backdrop-filter: blur(14px) saturate(160%);
  border: 1px solid rgba(7, 81, 116, .14);
  box-shadow: 0 12px 30px rgba(5, 61, 87, .16), 0 2px 6px rgba(5, 61, 87, .08),
              inset 0 1px 0 rgba(255,255,255,.9);
  font: 600 7.5px/1 var(--font-sans);
  letter-spacing: .22em; text-indent: .22em;   /* indent re-centres the tracked word */
  opacity: 0; visibility: hidden;
  transform: translateY(24px);
  transition: opacity .45s ease, transform .6s cubic-bezier(.16,1,.3,1),
              visibility 0s linear .6s, color .35s ease, box-shadow .35s ease, border-color .35s ease;
  -webkit-tap-highlight-color: transparent;
}
.to-top.is-visible {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity .45s ease, transform .6s cubic-bezier(.16,1,.3,1),
              visibility 0s, color .35s ease, box-shadow .35s ease, border-color .35s ease;
}
/* The rising fill: a faint tint tracks scroll progress, the solid brand
   gradient takes over on hover. */
.to-top::before, .to-top::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  transform-origin: bottom;
}
.to-top::before {
  background: linear-gradient(0deg, rgba(13,138,196,.22), rgba(13,138,196,.08));
  transform: scaleY(var(--tt-fill));
}
.to-top::after {
  background: linear-gradient(160deg, var(--blue-600) 0%, var(--blue-800) 100%);
  transform: scaleY(0);
  transition: transform .5s cubic-bezier(.16,1,.3,1);
}
.to-top__icon { width: 14px; height: 14px; display: block; }
.to-top__icon svg {
  width: 100%; height: 100%; display: block;
  fill: none; stroke: currentColor; stroke-width: 2.2;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform .5s cubic-bezier(.16,1,.3,1);
}
.to-top__label { display: block; }
@media (hover: hover) {
  .to-top:hover {
    color: #fff; border-color: transparent;
    transform: translateY(-4px);
    box-shadow: 0 18px 38px rgba(5, 61, 87, .30), 0 3px 8px rgba(5, 61, 87, .12);
  }
  .to-top:hover::after { transform: scaleY(1); }
  .to-top:hover .to-top__icon svg { transform: translateY(-3px); }
}
.to-top:active { transform: scale(.95); }
.to-top:focus-visible { outline: 2px solid var(--blue-500); outline-offset: 3px; }
@media (max-width: 640px) {
  .to-top { right: 14px; bottom: calc(var(--mobile-bar-h) + 14px); width: 34px; height: 50px; }
}
@media (prefers-reduced-motion: reduce) {
  .to-top, .to-top.is-visible, .to-top__icon svg { transition: opacity .2s ease, visibility 0s; transform: none; }
  .to-top::after { transition: none; }
}
@media print { .to-top { display: none; } }


/* ---------- Service & Repairs split (support/service.html) ----------
   One section: the four-step process reads down the left as a timeline,
   the request form sits beside it on the right. The left column is sticky
   on desktop so the steps stay in view while the (taller) form scrolls. */
.svc-split {
  position: relative; overflow: hidden;
  padding: var(--section-pad-y-tight) 0;
  background:
    radial-gradient(60% 50% at 0% 0%, rgba(19, 167, 236, .10), transparent 70%),
    radial-gradient(50% 45% at 100% 100%, rgba(9, 101, 144, .10), transparent 70%),
    var(--canvas);
}
.svc-grid {
  display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(32px, 4.5vw, 72px); align-items: center;
}
.svc-head.section-head { text-align: left; margin: 0 0 26px; max-width: 520px; }
.svc-head .eyebrow { margin-bottom: 10px; }
.svc-head.section-head h2 { font-size: clamp(1.75rem, 2.6vw, 2.4rem); }
.svc-head h2 span { color: var(--blue-600); }
.svc-head.section-head .svc-lead { margin: 10px 0 0; font-size: 1rem; line-height: 1.55; color: var(--ink-500); max-width: 44ch; }

/* Timeline: numbered nodes threaded on a hairline that fills with brand
   blue as the list reveals. */
.svc-steps { list-style: none; margin: 0; padding: 0; position: relative; }
.svc-steps::before,
.svc-steps::after {
  content: ""; position: absolute; left: 19px; top: 20px; bottom: 20px; width: 2px; border-radius: 2px;
}
.svc-steps::before { background: var(--blue-200); }
.svc-steps::after {
  background: linear-gradient(var(--blue-500), var(--blue-700));
  transform-origin: top; transform: scaleY(1);
  transition: transform 1.6s cubic-bezier(.22, .61, .36, 1) .2s;
}
.js .svc-steps[data-reveal]:not(.is-revealed)::after { transform: scaleY(0); }
.svc-step {
  position: relative; display: grid; grid-template-columns: 40px 1fr; gap: 16px;
  padding: 0 0 18px; margin: 0;
}
.svc-step:last-child { padding-bottom: 0; }
.svc-num {
  position: relative; z-index: 1;
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: .8125rem; font-weight: 600; letter-spacing: .02em; font-variant-numeric: tabular-nums;
  color: var(--blue-700); background: var(--white);
  border: 2px solid var(--blue-200);
  box-shadow: 0 0 0 5px var(--canvas);
  transition: background .3s ease, color .3s ease, border-color .3s ease, transform .3s ease;
}
.svc-step:hover .svc-num { background: var(--blue-600); border-color: var(--blue-600); color: #fff; transform: scale(1.06); }
.svc-step h3 { font-size: 1rem; font-weight: 600; color: var(--blue-900); margin: 1px 0 2px; line-height: 1.3; }
.svc-step p { font-size: .875rem; color: var(--ink-500); margin: 0; line-height: 1.5; }

.svc-direct { margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--blue-200); }
.svc-direct-label { display: block; font-size: var(--fs-label); font-weight: var(--fw-label); color: var(--ink-500); margin-bottom: 10px; }
.svc-direct-row { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 24px; }
.svc-call { display: inline-flex; align-items: center; gap: 12px; color: var(--blue-900); font-weight: 600; text-decoration: none; }
.svc-call svg {
  width: 40px; height: 40px; padding: 10px; border-radius: 50%;
  background: var(--white); color: var(--blue-600); border: 1px solid var(--blue-200);
  transition: background .25s ease, color .25s ease;
}
.svc-call:hover svg { background: var(--blue-600); color: #fff; }
.svc-call span { display: flex; flex-direction: column; line-height: 1.25; }
.svc-call small { font-size: var(--fs-caption); font-weight: 500; color: var(--ink-500); }

/* The form card: white sheet with a brand-gradient top edge. */
.service-form {
  position: relative; background: var(--white);
  border: 1px solid var(--blue-100); border-radius: 24px;
  box-shadow: 0 30px 70px -30px rgba(5, 61, 87, .28), var(--shadow-sm);
  padding: clamp(20px, 2.4vw, 32px);
  overflow: hidden;
}
.service-form::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 5px;
  background: var(--gradient-sheen);
}
.sf-head {
  display: flex; justify-content: space-between; align-items: flex-end; gap: 16px;
  padding-bottom: 14px; margin-bottom: 18px; border-bottom: 1px dashed var(--blue-200);
}
.sf-kicker { display: block; font-size: var(--fs-caption); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--blue-600); margin-bottom: 4px; }
.sf-head h3 { margin: 0; font-size: 1.25rem; font-weight: 600; color: var(--blue-900); line-height: 1.2; }
.sf-req { font-size: var(--fs-caption); color: var(--ink-500); white-space: nowrap; }

.service-form .form-field { margin-bottom: 12px; }
.service-form .form-row-2 { gap: 0 14px; }
.service-form .form-field label { font-size: .75rem; font-weight: 600; color: var(--ink-700); margin-bottom: 5px; letter-spacing: .01em; }
.service-form .form-field input,
.service-form .form-field select,
.service-form .form-field textarea {
  background-color: var(--canvas); border: 1.5px solid transparent; border-radius: 10px;
  padding: 10px 14px; font-size: .875rem; color: var(--ink-900);
  transition: border-color .2s ease, background-color .2s ease, box-shadow .2s ease;
}
.service-form .form-field input::placeholder,
.service-form .form-field textarea::placeholder { color: var(--ink-300); }
.service-form .form-field input:hover,
.service-form .form-field select:hover,
.service-form .form-field textarea:hover { border-color: var(--blue-200); }
.service-form .form-field input:focus,
.service-form .form-field select:focus,
.service-form .form-field textarea:focus {
  background-color: var(--white); border-color: var(--blue-500);
  box-shadow: 0 0 0 4px rgba(19, 167, 236, .14);
}
.service-form .form-field select {
  appearance: none; -webkit-appearance: none; padding-right: 42px;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23607685' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center; background-size: 16px;
}
.service-form textarea { resize: vertical; min-height: 76px; }
.sf-submit {
  margin-top: 4px; padding-top: .8rem; padding-bottom: .8rem;
  background: var(--gradient-primary); font-weight: 600; letter-spacing: .02em;
  box-shadow: 0 14px 30px -12px rgba(9, 101, 144, .6);
  transition: transform .25s ease, box-shadow .25s ease;
}
.sf-submit svg { width: 18px; height: 18px; transition: transform .25s ease; }
.sf-submit:hover { transform: translateY(-2px); box-shadow: 0 18px 36px -12px rgba(9, 101, 144, .7); }
.sf-submit:hover svg { transform: translateX(4px); }
.service-form .form-note { text-align: center; margin: 10px 0 0; }

@media (max-width: 1000px) {
  .svc-grid { grid-template-columns: 1fr; }
  .svc-head.section-head { max-width: none; }
}
@media (max-width: 600px) {
  .service-form { border-radius: 20px; padding: 22px 16px; }
  .sf-head { flex-direction: column; align-items: flex-start; }
  .sf-head h3 { font-size: 1.25rem; }
}
@media (prefers-reduced-motion: reduce) {
  .svc-steps::after, .sf-submit, .sf-submit svg, .svc-num { transition: none; }
}

/* ================= HERO BRAND-FILM SCENE =================
   While the film scene is on screen (html.hero-video-on, set by
   initHeroScenes) it plays clean and full-bleed: the film ends on its own
   Jagruti logo, so the header wordmark and its dark scrim step aside, and
   the vignette, scene tabs, scroll cue and bottom wash all fade out. */
.site-header::before,
.hero-parallax::before,
.hero-parallax > .hero-fade,
.hero-dots,
.hero-scrollcue { transition: opacity .5s ease, visibility .5s; }
html.hero-video-on body[data-nav] .site-header:not(.is-scrolled)::before { opacity: 0; }
html.hero-video-on .site-header .logo {
  opacity: 0; visibility: hidden; pointer-events: none;
}
html.hero-video-on .hero-parallax::before,
html.hero-video-on .hero-parallax > .hero-fade,
html.hero-video-on .hero-parallax .hero-dots,
html.hero-video-on .hero-parallax .hero-scrollcue {
  opacity: 0; visibility: hidden; pointer-events: none;
}

/* The film layer sits over the stage (z-0) and under the vignette, frame,
   wash and controls - all of which are hidden while it plays. It arrives
   with the same window reveal as a scene climb (outer box rises, inner
   content counter-moves so the picture itself stays put), and leaves by
   fading while the next scene climbs underneath it. */
.hero-film {
  position: absolute; inset: 0; z-index: 3;
  overflow: hidden;
  background: #232426;
  opacity: 0; visibility: hidden;
  transition: opacity .9s ease, visibility 0s linear .9s;
}
.hero-film-inner { position: absolute; inset: 0; }
.hero-parallax.is-video .hero-film {
  opacity: 1; visibility: visible;
  transition: none;
  animation: heroSceneUp 1200ms cubic-bezier(.22,.7,.24,1) both;
}
.hero-parallax.is-video .hero-film-inner {
  animation: heroPlateHold 1200ms cubic-bezier(.22,.7,.24,1) both;
}
/* The film's own travel is always the full section height, whatever the
   portrait stage overrides --scene-travel to. */
.hero-film { --scene-travel: 100%; }
.hero-film-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  /* Fills the hero at its true shape - never stretched; any overflow
     beyond 16:9 is trimmed evenly off the edges. */
  object-fit: cover;
  /* Trim a little less off the top than the bottom: the film's closing
     logo sits close to its top edge, while the line-up's feet have more
     room below them. */
  object-position: 50% 40%;
  pointer-events: none;
}
/* The film's marker scene is empty; dark rather than the hero blue, so the
   handover back to scene 1 fades from the film to dark, not to a blue sheet. */
.hero-scene-5 { background: #232426; }
