/* ==========================================================================
   NNP Raisins - site styles
   Direction: "Caliber A". Paper ground, packaging violet, Bricolage 800.
   One accent colour (violet). One radius scale (3px). No em-dashes in copy.
   ========================================================================== */

:root {
  /* colour */
  --paper:      #edece6;
  --paper-hi:   #f6f5f1;
  --ink:        #16141c;
  --dim:        #5d596b;
  --line:       #d6d4cc;
  --navy:       #1b2652;   /* the logo's own colour. Logo only, nothing else. */
  --violet:     #4b1e78;
  --violet-dp:  #37125e;
  --violet-lt:  #e2b7ff;
  --chalk:      #f2edf8;
  --chalk-dim:  #b6a3cc;

  /* type */
  --display: "Bricolage Grotesque", system-ui, sans-serif;
  --body:    "Public Sans", system-ui, -apple-system, sans-serif;
  --mono:    "IBM Plex Mono", ui-monospace, monospace;

  /* space */
  --gut: 40px;
  --sec: 96px;
  --r: 3px;
  --max: 1280px;

  /* masthead. Two heights: generous over the hero, compact once the page moves.
     The body reserves the taller one, so the change never shifts content. */
  --mast-h: 90px;
  --mast-h-compact: 66px;

  /* motion. Built-in easings are too weak, these are the working set. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --t-press: 120ms;
  --t-hover: 150ms;
  --t-reveal: 560ms;
}

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

/* The compact height, not the resting one. Any anchor jump lands you scrolled,
   which is exactly when the bar is compact. */
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; scroll-padding-top: var(--mast-h-compact); }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* Lenis smooth scroll. These are the library's own required rules. The
   `scroll-behavior: auto !important` matters: native smooth scrolling and Lenis
   fight each other, so the rule above stays as the no-JS fallback and this one
   switches it off only once Lenis has actually initialised and added .lenis to
   <html>. Under reduced motion site.js never starts it, so the native path and
   the CSS above stay in charge. */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
.lenis.lenis-smooth iframe { pointer-events: none; }

body {
  margin: 0;
  /* Reserves the masthead's tallest state. The header is fixed, so it never
     takes part in flow and its height change costs nothing below it. */
  padding-top: var(--mast-h);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.6;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; }

/* The focus ring is violet, which is invisible on a violet ground. The masthead
   already flipped it by hand when it went violet on scroll; this makes that a
   property instead, so every violet surface fixes itself. Zero visual change on
   paper - `--focus` resolves to the same violet it always was. */
:root { --focus: var(--violet); }
.dark, .cstrip { --focus: var(--chalk); }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

.wrap { max-width: var(--max); margin: 0 auto; padding-inline: var(--gut); }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip {
  position: absolute; left: 12px; top: -60px; z-index: 100;
  background: var(--violet); color: #fff; padding: 10px 16px; border-radius: var(--r);
  font-weight: 600; text-decoration: none; transition: top .15s;
}
.skip:focus { top: 12px; }

/* --------------------------------------------------------------- type ---- */
h1, h2, h3, h4 { font-family: var(--display); font-weight: 800; letter-spacing: -.045em; margin: 0; text-wrap: balance; }
h1 { font-size: clamp(30px, 5.2vw, 66px); line-height: 1.0; }
h2 { font-size: clamp(27px, 3.6vw, 44px); line-height: 1.04; }
h3 { font-size: clamp(19px, 1.6vw, 23px); line-height: 1.14; letter-spacing: -.035em; }
p { margin: 0; }

.accent { color: var(--violet); }
.lead  { font-size: clamp(16px, 1.25vw, 18px); line-height: 1.62; color: var(--dim); max-width: 54ch; }
.label { font-family: var(--mono); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--dim); }
.num   { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ buttons ---- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 46px; padding: 12px 24px; border-radius: var(--r);
  font-family: var(--body); font-size: 15px; font-weight: 600; line-height: 1;
  text-decoration: none; white-space: nowrap; border: 1px solid transparent;
  cursor: pointer; transition: transform .12s ease, background-color .15s ease, color .15s ease;
}
/* THE BUTTON, one concept for the whole site.

   Every button fills from the LEFT on hover. That is not decoration picked off a
   shelf: left to right is this site's only direction of travel, and it is
   already load-bearing three times over. The caliber curtain on the home page
   wipes off to the right, the production-stage rule grows from the left, and the
   coverflow hands its cards leftwards into the paper. A button that fills the
   same way makes the whole site feel operated by one hand.

   Built as a pseudo-element behind the label rather than an animated
   `background`, for two reasons. Colour cannot be transformed, so a background
   change can only cross-fade, which reads as a lamp being switched rather than
   as something arriving. And a transform is composited: the fill costs no paint
   on the main thread, so it stays smooth on the cheapest phone a buyer owns.

   `isolation: isolate` is what lets `z-index: -1` sit behind the text but still
   inside the button. Without it the layer escapes to the page and paints behind
   the SECTION instead, which is invisible on paper and baffling to debug. */
.btn { position: relative; isolation: isolate; overflow: hidden; }
.btn::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  transform: scaleX(0); transform-origin: left;
  transition: transform 300ms var(--ease-out);
}
.btn:hover::before, .btn:focus-visible::before { transform: scaleX(1); }
/* The label changes 70ms late, so the colour flips as the fill passes under it
   rather than before it arrives. Without the delay the text turns pale against a
   ground that has not reached it yet and is briefly unreadable. */
.btn { transition: transform var(--t-press) var(--ease-out), color 200ms ease 70ms; }
.btn:active { transform: translateY(1px); }

/* Each variant names its own arriving colour. Violet deepens to ink, ghost
   floods to ink, chalk brightens to white. */
.btn-primary { background: var(--violet); color: #fff; }
.btn-primary::before { background: var(--ink); }
.btn-ghost { border-color: var(--ink); color: var(--ink); }
.btn-ghost::before { background: var(--ink); }
.btn-ghost:hover, .btn-ghost:focus-visible { color: var(--paper); }
.btn-light { background: var(--chalk); color: var(--violet-dp); }
.btn-light::before { background: #fff; }

@media (prefers-reduced-motion: reduce) {
  /* The fill still happens, it just does not travel. */
  .btn, .btn::before { transition: none; }
  .btn:active { transform: none; }
}

/* ---------------------------------------------------------------- nav ---- */
/* Fixed, not sticky. Sticky keeps its slot in flow, so growing the bar on
   scroll would push every section down by the difference in one frame. */
.masthead { position: fixed; top: 0; left: 0; right: 0; z-index: 40;
  border-bottom: 1px solid var(--line); background: var(--paper);
  transition: background-color var(--t-reveal) var(--ease-out),
              border-color var(--t-reveal) var(--ease-out); }
.masthead .wrap { display: flex; align-items: center; gap: 30px; height: var(--mast-h);
  transition: height var(--t-reveal) var(--ease-out); }

/* Scrolled state: the bar tightens and carries the packaging violet. */
.masthead.is-stuck { background: var(--violet); border-bottom-color: transparent; }
.masthead.is-stuck .wrap { height: var(--mast-h-compact); }
.masthead.is-stuck .brand { color: #fff; }
.masthead.is-stuck .navtoggle { color: #fff; border-color: rgba(255,255,255,0.35); }
@media (prefers-reduced-motion: reduce) {
  .masthead, .masthead .wrap, .brand-logo { transition: none; }
}
/* The supplied logo artwork. Colour comes from the link, the SVG inherits it,
   so the same symbol serves navy on paper and white on the violet footer. */
.brand { display: inline-flex; align-items: center; margin-right: auto; color: var(--navy); text-decoration: none; }
.brand-logo { display: block; height: 46px; width: auto; transition: height var(--t-reveal) var(--ease-out); }
.masthead.is-stuck .brand-logo { height: 34px; }
.brand:focus-visible { outline: 2px solid var(--violet); outline-offset: 4px; border-radius: var(--r); }
.mainnav { display: flex; align-items: center; gap: 28px; }
/* :not(.btn) matters. Without it this rule outranks .btn-primary and paints the
   nav CTA's label dim grey on violet, which is unreadable. */
.mainnav a:not(.btn) { font-size: 14px; color: var(--dim); text-decoration: none; }
.mainnav a:not(.btn):hover, .mainnav a:not(.btn)[aria-current="page"] { color: var(--ink); }
.navtoggle { display: none; background: none; border: 1px solid var(--line); border-radius: var(--r); min-height: 44px; min-width: 44px; align-items: center; justify-content: center; cursor: pointer; }
.navtoggle svg { width: 20px; height: 20px; }
/* Only while the nav sits inline on the violet bar. Below 901px the links live
   in a paper dropdown underneath it and must keep their ink colours. */
@media (min-width: 901px) {
  .masthead.is-stuck .mainnav a:not(.btn) { color: var(--chalk-dim); }
  .masthead.is-stuck .mainnav a:not(.btn):hover,
  .masthead.is-stuck .mainnav a:not(.btn)[aria-current="page"] { color: #fff; }
  /* Violet on violet is invisible, so the CTA inverts with the bar. */
  .masthead.is-stuck .btn-primary { background: var(--chalk); color: var(--violet-dp); }
  /* The fill has to be re-pointed here, not just the resting colour. On the
     violet bar this button inverts to violet-dp on chalk, so the default ink
     fill would arrive under violet type and make the label unreadable. White
     keeps the same contrast the resting state has. */
  .masthead.is-stuck .btn-primary::before { background: #fff; }
  .masthead.is-stuck :focus-visible { outline-color: #fff; }
}

/* --------------------------------------------------------------- hero ---- */
.hero { position: relative; overflow: hidden; padding: clamp(56px, 8vw, 104px) 0 clamp(44px, 6vw, 72px); }
/* Kept from the carousel era: the contact page's map is still absolutely
   positioned inside its hero, so the copy has to hold a stacking context above
   it. Costs nothing on the pages with no panel. */
.hero .wrap { position: relative; z-index: 1; }
.hero h1 { max-width: none; margin-bottom: 24px; }

/* ---------------------------------------------------- hero carousel ---- */
/* Inset top and bottom to the hero box, which is exactly the gap between the
   masthead and the caliber belt — so the frame fills that band without either
   of them moving. Right edge runs to the viewport, not to .wrap's 1280px. */
/* The panel reaches well past the headline — a card leaving to the left has to
   travel behind the type, not stop short of it — and the mask below is what
   keeps that from being visible as an overlap. */
.hero-carousel {
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 0;
  width: 74%; pointer-events: none;
  /* Card HEIGHT is the only measurement the rake needs: pitch, recession, the
     viewing distance and the 4:3 width are all multiples of it, so the whole
     thing scales from this one number. site.js overwrites it with a value
     fitted to the band's real height; this is the no-JS value. */
  --cf-card: clamp(170px, 20vw, 300px);
  /* Viewing distance, as a multiple of card height. Smaller is a wider lens and
     a harder rake. */
  perspective: calc(var(--cf-card) * 2.6);
  /* The hand-off to the headline, and the reason a departing card can pass
     behind the type at all: the type has no plate behind it, so a real overlap
     would show the photograph through the counters of the letters.
     Nine stops, not two. A straight two-stop ramp has a kink at each end where
     the slope changes abruptly, and the eye reads that kink as a pale band
     travelling with the card. These stops trace a smoothstep curve (f²(3−2f)),
     which starts and ends flat, so there is no edge to see anywhere in it.
     The ramp ends at 32%, which is where the front card's left edge falls — far
     enough right that the front card is never touched by it, and long enough
     (a third of a wide panel) that the dissolve has real distance to happen in.
     Two properties, not one: Safari still needs the prefix. */
  -webkit-mask-image: linear-gradient(to right,
    rgba(0,0,0,0) 0%, rgba(0,0,0,.043) 4%, rgba(0,0,0,.156) 8%,
    rgba(0,0,0,.316) 12%, rgba(0,0,0,.5) 16%, rgba(0,0,0,.684) 20%,
    rgba(0,0,0,.844) 24%, rgba(0,0,0,.957) 28%, #000 32%);
          mask-image: linear-gradient(to right,
    rgba(0,0,0,0) 0%, rgba(0,0,0,.043) 4%, rgba(0,0,0,.156) 8%,
    rgba(0,0,0,.316) 12%, rgba(0,0,0,.5) 16%, rgba(0,0,0,.684) 20%,
    rgba(0,0,0,.844) 24%, rgba(0,0,0,.957) 28%, #000 32%);
}
/* A zero-size origin point, not a box. Every card is placed by transform off
   this one spot, so there is no layout to reflow when the ring turns. Sits at
   just over half the panel, which puts the front card centre-right of the
   viewport with the stack raking off the right edge behind it. */
.cf-track {
  position: absolute; top: 50%; left: 57%; width: 0; height: 0;
  transform-style: preserve-3d;
}
/* The box is always the full 4:3 of the front position. site.js squeezes it
   back to a square for every card that is not in front, so the frame OPENS as a
   card arrives rather than the photograph being rescaled. */
.cf-card {
  position: absolute; top: 0; left: 0;
  width: calc(var(--cf-card) * 4 / 3); height: var(--cf-card);
  margin: calc(var(--cf-card) / -2) 0 0 calc(var(--cf-card) * -2 / 3);
  border-radius: 16px; overflow: hidden;
  /* Paper, not paper-hi. Any ground that shows through a half-faded card has to
     be invisible against the page — a lighter ground reads as a pale rectangle
     sliding along behind the type, which is the one thing the mask is there to
     prevent. */
  background: var(--paper);
  box-shadow: 0 22px 48px -22px rgba(22, 20, 28, .38);
  will-change: transform, opacity;
}
/* Counter-scaled against its own card's squeeze, so the photograph holds its
   true proportions and its size on screen while the frame narrows around it.
   The frame is a window; the picture behind it never moves. */
.cf-card img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  will-change: transform;
}
/* Without JS every card lands on the same spot at full opacity and the last one
   in the source is what shows: one photograph, framed, which is a complete
   hero. Nothing here depends on the script running. */
@media (max-width: 900px) {
  /* The headline runs the full width here, so there is no column for a
     photograph to sit in without landing under the type. */
  .hero-carousel { display: none; }
}
.hero .lead { margin-bottom: 34px; }
.hero .actions { display: flex; gap: 12px; flex-wrap: wrap; }

/* ------------------------------------------------- home hero + poster ---- */
/* The coverflow carousel was removed from the HOME hero on 2026-08-16, his call,
   and replaced by the SIAL Paris 2026 poster.
   ⚠ **ONLY the home page's markup went. The carousel component itself STAYS.**
   `.hero-carousel` / `.cf-*` above and the `heroCoverflow` module in site.js are
   still live, because the PRODUCTS page runs the same component with its own six
   photographs. Deleting them because the home page stopped using them takes the
   Products hero down with it — which is exactly what happened here first, caught
   by screenshotting Products after the change. Check both pages before touching
   anything `cf-`.
   The JS bails on `[data-coverflow]` being absent, so the home page costs
   nothing for the module still being loaded.

   A GRID, not the carousel's absolute positioning. The poster is content with
   its own intrinsic 3:2 shape, so it needs to occupy real space and push the
   section's height rather than be fitted into a band by script. It also means
   the poster aligns to `.wrap`'s right edge like everything else on the page,
   where the carousel deliberately ran past it to the viewport edge. */
/* The panel, in the contact map's treatment. `.hero` is already the band from
   the masthead's underside to the caliber belt's top edge, so top/bottom: 0
   lands on both by construction and NOTHING below the hero moves. */
.hero-sial { display: none; }

@media (min-width: 901px) {
  .hero-sial {
    display: block;
    /* THREE TERMS, and the two ceilings are not decoration.
       - `56vw` is the design proportion and governs up to ~1440.
       - `50vw + 88px` stops the panel crossing back over `.wrap`. The wrap
         stops growing at --max 1280 and centres; a straight 56% keeps marching
         inward past it and eats the headline column from the right (at 2560 it
         left ~400px for a headline needing ~490).
       - `880px` is the one that keeps the ARTWORK intact, and it is the least
         obvious. The panel is a fixed-height band (the gap between masthead and
         caliber belt) but a growing width, so the wider it gets the more
         `cover` has to crop off the top and bottom. At 1920 an uncapped panel
         was 1048px wide and cut the SIAL logo in half and the "Premium Uzbek
         raisins" paragraph off at the bottom. 880px, with the min-height below,
         holds the crop at ~3% a side, which clears both.
       Whichever is smallest wins, so there is no breakpoint anywhere in it. */
    position: absolute; top: 0; right: 0; bottom: 0;
    width: min(56vw, calc(50vw + 88px), 880px);
    /* ⚠ `left center`, NOT `center`. `cover` crops on whichever axis has slack,
       and on a narrow-ish desktop the band is TALLER than the panel is wide in
       3:2 terms, so the slack is horizontal - centring it took equal bites out
       of both sides and at 1100px "MEET / NNP RAISINS / SIAL PARIS" lost its
       first letters off the left. Every word on this poster is left-aligned and
       the right third is sky, Eiffel tower and leaves, so all crop belongs on
       the right. Vertical stays centred, which is what `center` here means. */
    background: url(../img/sial-paris-panel-1530.jpg) left center / cover no-repeat;
    pointer-events: none;
    /* ⚠ THE RAMP AND THE FILE ARE ONE DESIGN. Read this before changing either.
       `cover` on a panel narrower than the image fits by WIDTH, so image-x%
       lands on panel-x% and the ramp's ceiling is a property of the FILE: run it
       past the artwork's blank left margin and it dissolves ink. The supplied
       poster has only 4.1% of margin (leftmost ink is the "Premium Uzbek
       raisins" paragraph, x=58 of 1400) - so a 15% ramp ate "MEET", the front of
       "NNP RAISINS" and that paragraph, and a ramp short enough to spare them
       was too short to read as a dissolve at all. Both were tried; both were
       wrong. There is no ramp length that fixes this, because the margin is the
       constraint.
       So the MARGIN was made bigger, not the ramp smaller. The file this points
       at is not the raw poster: it is the poster with 130px of extra canvas on
       the left, filled by replicating the artwork's leftmost pixel column
       outwards (see the build note in STATUS.md). That column is flat parchment
       carrying only a slow vertical shade, so the extension is continuous with
       the artwork by construction - there is no seam to find - and it moves the
       first ink from 4.1% to 12.3% of the file.
       That buys a 10% ramp: ~80px of dissolve at 1440, all of it falling on
       replicated parchment, with ~2.3% of clearance before any ink. The
       clearance only widens as the panel narrows, because `cover` then fits by
       height and scales the margin up faster than the panel.
       ⚠ If the poster is ever replaced: re-run the pad step on the new file,
       scan for the leftmost pixel under ~120 luma, and keep the ramp below that
       percentage. Nine stops tracing a smoothstep, not two - a straight ramp
       changes slope at each end and the eye reads that kink as a band down the
       picture. The COPY is held off the panel by --copyw below, never by this. */
    -webkit-mask-image: linear-gradient(to right,
      rgba(0,0,0,0) 0%, rgba(0,0,0,.043) 1.25%, rgba(0,0,0,.156) 2.5%,
      rgba(0,0,0,.316) 3.75%, rgba(0,0,0,.5) 5%, rgba(0,0,0,.684) 6.25%,
      rgba(0,0,0,.844) 7.5%, rgba(0,0,0,.957) 8.75%, rgba(0,0,0,1) 10%);
    mask-image: linear-gradient(to right,
      rgba(0,0,0,0) 0%, rgba(0,0,0,.043) 1.25%, rgba(0,0,0,.156) 2.5%,
      rgba(0,0,0,.316) 3.75%, rgba(0,0,0,.5) 5%, rgba(0,0,0,.684) 6.25%,
      rgba(0,0,0,.844) 7.5%, rgba(0,0,0,.957) 8.75%, rgba(0,0,0,1) 10%);
  }
  /* The panel is opaque from ~4% of its own width, so the COPY has to stop
     before the panel starts - the ramp will not hide an overlap the way the
     contact page's long one does, and at 4% it hides nothing at all.
     ⚠ `33vw - gut` was here and was wrong in both directions: it ignored that
     `.wrap` centres at --max, so it over-reported the room on wide screens, and
     it was never checked against the LONGEST ROTATOR PHRASE. Rotator lines are
     `white-space: nowrap` inside an `overflow: hidden` box, so a phrase too wide
     for the column is not wrapped, it is SILENTLY GUILLOTINED - which is what he
     saw as "Naturally Drie" disappearing under the divider.
     --copyw is the real arithmetic: panel's left edge, minus the wrap's content
     left edge (which is `--gut` until the wrap centres at --max, `50vw - 600px`
     after), minus a 48px clearance so the copy never runs right up to the
     dissolve. Both terms mirror the panel's `left` above - CHANGE THEM TOGETHER. */
  .hero-home {
    --sialw: min(56vw, calc(50vw + 88px), 880px);
    --copyw: calc(100vw - var(--sialw) - max(var(--gut), 50vw - 600px) - 48px);
    /* The band has to keep up with the panel or `cover` crops the artwork away
       - see the 880px ceiling above. 1.64 is the padded file's exact aspect
       (1530x933), so at this height the poster fits the panel with NOTHING off
       the top or bottom: the SIAL logo and the bottom paragraph are safe at
       every width from here up. Only ever ADDS height - under ~1300px the copy
       is taller than this, the slack turns horizontal instead, and `left center`
       above sends that crop to the sky-and-Eiffel side where it costs nothing. */
    min-height: calc(var(--sialw) / 1.64);
    /* The other end of the same problem. Under ~1250px the band is taller than
       the panel is wide at 3:2, so `cover` starts cropping sideways instead -
       and the poster can only afford that on its right. Trimming the home
       hero's own padding (the shared `.hero` value is 8vw/6vw) keeps the band
       nearer the poster's shape and holds that crop to a few per cent. */
    padding: clamp(40px, 5vw, 88px) 0 clamp(36px, 4vw, 64px);
    /* Centres the copy in the height this just added, instead of leaving it
       parked at the top with the slack below it. */
    display: flex; align-items: center;
  }
  .hero-home > .wrap { width: 100%; }
  /* Then the type is sized to the column instead of the column being hoped to
     fit the type. 7.5 is the width-to-font-size ratio of the widest phrase in
     `data-rotator-lines`: "Certified Quality" measures 487px at 66px, so it
     needs 7.379x its font-size, plus a little air.
     ⚠ Re-measure and update 7.5 if a longer phrase is ever added to that list.
     In practice this trims 66px to ~62px between 1440 and 2560 and bites harder
     only under ~1150px, where the alternative is a chopped word. */
  .hero-home h1 {
    max-width: var(--copyw);
    font-size: min(5.2vw, 66px, calc(var(--copyw) / 7.5));
  }
  .hero-home .lead { max-width: min(46ch, var(--copyw)); }
  /* The button row has to respect --copyw too. It is not type, so it went
     uncapped at first and the longer "Book a meeting at SIAL" label pushed the
     pair into the panel at the narrow end of desktop - at 901px its right edge
     landed 42px inside a 50px ramp, i.e. over the artwork at ~85% opacity.
     `.actions` already wraps, so capping it stacks the two buttons there
     instead. That is the correct trade: two lines of button beats a button
     sitting on the poster. */
  .hero-home .actions { max-width: var(--copyw); }
}

/* --------------------------------------------- section backgrounds ---- */
/* The illustration runs BEHIND two sections rather than as a band of its own,
   drifting as each section crosses the viewport. The standalone band was
   removed on request (2026-08-14) — it did not hold up as a set piece, and the
   artwork does more work as ground under the copy than as the subject.
   Only the section clips; the artwork is deliberately taller than it. */
.has-bg { position: relative; }
.sec-bg {
  position: absolute; inset: 0; overflow: hidden;
  z-index: 0; pointer-events: none;
}
/* Everything above the background has to claim a stacking position, or the
   background paints over the copy. .wrap is the only child that matters. */
.has-bg > .wrap { position: relative; z-index: 1; }
.sec-bg img {
  position: absolute; left: 0; top: 50%;
  width: 100%; height: auto;
  /* A section is usually taller than the artwork's natural height at full
     width, so min-height is the normal case here, not the edge case it was in
     the band. object-fit then crops the sides, which costs an illustration
     that has nothing at its sides. The 116% is the travel room: the art has to
     overhang the section or the drift pulls its own edge into view. */
  min-height: 116%; object-fit: cover;
  transform: translateY(-50%);
  will-change: transform;
  /* Same compositing model as the caliber strip: the artwork's ground is
     white-point corrected to pure white on export, and white multiplied by
     paper IS paper. The ground vanishes with no masking, the halftone
     survives, and the overhang beyond the section is invisible. */
  mix-blend-mode: multiply;
  /* Soft top and bottom edges, and this is what stops them reading as panels.
     A section background clipped square ends on a hard horizontal line, and
     with two of these stacked the page grows two full-width seams where the
     tint stops dead. Fading both edges lets each one dissolve into the paper,
     so it reads as ground the page is printed on rather than a block behind
     one section. Tight ramps — 12% — so the artwork keeps its middle. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 12%, #000 86%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 12%, #000 86%, transparent 100%);
  /* THE NUMBER TO TUNE. This is ground under body copy, not a picture to
     admire — anything strong enough to look at properly is too strong to set
     text on. The rows sit lower again than the leaves because that section is
     a grid of eight bordered tiles, so there is far more type and rule work
     for a tint to interfere with. */
  opacity: .20;
}
/* The artwork is a border composition — leaves and the bunch around an empty
   middle — and the section is much wider than 16:9, so a centred crop shows
   the empty middle and throws away the bunch entirely. Biasing the crop
   upwards is what keeps the fruit in the frame, on the left, behind the copy. */
.sec-bg img[src*="bg-leaves"] { object-position: 50% 24%; }
.sec-bg img[src*="bg-rows"] { opacity: .14; object-position: 50% 42%; }
@media (prefers-reduced-motion: reduce) {
  /* The backgrounds simply hold still. site.js binds no listener at all. */
  .sec-bg img { will-change: auto; }
}

/* ----------------------------------------------------------- products ---- */
/* One entry per variety. Layout families alternate on purpose: two splits,
   then Golden full width, then two more. Five identical splits in a row is a
   spreadsheet with pictures, and it is what this page used to be. The rule
   above each entry is the separator, not a card, because a card implies
   something to click and these are reference. */
.prod { border-top: 1px solid var(--line); padding-top: clamp(32px, 4vw, 52px); margin-top: clamp(40px, 5vw, 72px); }
.prod:first-of-type { margin-top: clamp(28px, 3vw, 40px); }
.prod .split { align-items: start; }
/* Even entries put the photograph on the left. Source order is unchanged, so
   the heading still comes before its own picture for a screen reader.

   The column widths are MIRRORED, not just reordered, and that is the fix for
   a real bug. `order: 2` alone moved the photograph into the 1.08fr column, so
   Type 9 and Red Flame rendered 661px wide against Sun-dried's 563px and
   `object-fit: cover` sliced the extra off the top and bottom of the frame.
   The crop was visible on exactly those two entries and on no others. Swapping
   the track sizes keeps the photograph in the .92fr column whichever side it
   sits on, so all four are identical. */
.prod:nth-of-type(even) .split { grid-template-columns: .92fr 1.08fr; }
.prod:nth-of-type(even) .body { order: 2; }
/* And with the width now constant, hold the box at the photograph's own 4:3 so
   `cover` has nothing left to crop. `.split > .media` otherwise forces
   `aspect-ratio: auto` plus a min-height, which is what let the box drift off
   the frame's shape in the first place. */
.prod .split > .media { aspect-ratio: 4 / 3; min-height: 0; }
.prod-name { margin-bottom: 6px; }
.prod-sub { font-size: 14px; color: var(--dim); margin: 0; }
/* `.prod-cal` was the violet caliber number at poster scale above each product
   name. Removed on request 2026-08-14, markup and styles together. Caliber is
   still the first figure in every entry's `.figs` grid, so nothing was lost but
   the display treatment. The home page's caliber strip is a different component
   (`.caliber-*`) and is untouched. */

/* Figures at display scale, replacing a five-row hairlined table per variety.
   The page carried 45 ruled rows before this; a line under every row is the
   densest and least readable way to show five numbers.

   ALL five specs now live in ONE grid. Harvest and certification used to sit
   below in a separate flex row, which meant they aligned to nothing and read
   as debris under the figures. Two columns, a wide cell for certification,
   and a single gap governing everything: the four short values line up in a
   proper 2x2 and the long one runs the full width beneath them. */
.figs { display: grid; grid-template-columns: repeat(2, 1fr); gap: 26px 40px; margin: 32px 0 0;
  align-items: start; }
/* Six cells in a 2x3: caliber, count / moisture, harvest / certification, sheet.
   `.figs` is a DIV wrapper, not the list itself, because the button has to be a
   cell of the same grid and a `<dl>` may only contain dt/dd groups. The list
   sits inside it under `display: contents`, which drops its own box so its five
   pairs become cells of `.figs` directly, and the button is its sibling in the
   sixth. Do not move the anchor inside the `<dl>`; it is invalid there. */
.figs > dl { display: contents; margin: 0; }
.figs > dl > div { min-width: 0; }
/* Bottom-aligned, so it sits on the certification's last line rather than
   floating at the top of a cell that is often two lines tall. */
.figs-btn { align-self: end; justify-self: start; }
.figs dt { font-size: 11.5px; letter-spacing: .04em; color: var(--dim); line-height: 1.3; }
.figs dd { margin: 9px 0 0; font-family: var(--display); font-weight: 800;
  font-size: clamp(19px, 1.7vw, 24px); letter-spacing: -.04em; line-height: 1.05; color: var(--ink); }
/* Certification is a list, not a figure, so it stays out of display scale. At
   24px "Non-GMO, FSSC 22000, Organic" wraps to three lines and dwarfs the
   numbers above it. */
.figs .fig-cert dd { font-family: var(--body); font-weight: 500; font-size: 15px;
  letter-spacing: 0; line-height: 1.45; }

/* Golden takes the wide slot. It breaks the run of splits, and it is the entry
   carrying the sulphur dioxide note, which wants room rather than a cramped
   grey box at the bottom of a column.

   The band is full width but the photograph is `contain`, not `cover`, and
   that is the whole point. Cropping a 4:3 pile into a full-width strip renders
   it 2.3x larger than the four beside it (measured), which tells a buyer
   Golden berries are twice the size of everything else on the page. On a page
   whose signature is an honest size comparison that is not a cosmetic problem.
   Contained in a band, the pile lands at exactly the size the other four
   render at, which is what he asked for: one standard photograph size across
   the page. It works because the multiply is baked in: the photograph's ground
   IS the page, so there is no plate edge to give the letterboxing away.

   The height is derived, not guessed. A split's media column is
   `.92fr` of `(wrap - 56px gap)`, so its width is `(100vw - 136px) x 0.46` and
   its 4:3 height is `x 0.345`. Feeding the band that same height makes the
   contained pile the same size as its neighbours at every viewport, and the
   395px cap is where the wrap stops growing. Change `.split`'s columns or gap
   and this number has to be recomputed. */
.media.prod-wide-media {
  aspect-ratio: auto;
  height: min(395px, calc((100vw - 136px) * 0.345));
  margin-bottom: clamp(24px, 3vw, 36px);
}
/* Doubled class again, and for the same reason: `.media img` sets cover further
   down the file at equal specificity, so `.prod-wide-media img` loses on
   source order. This is the second rule on this page to be silently beaten by
   `.media`; if you add a third, check where `.media` sits before you write it. */
.media.prod-wide-media img { object-fit: contain; }
.prod-wide-body { display: grid; grid-template-columns: 1.08fr .92fr; gap: 56px; align-items: start; }
/* `.prod-note` lived here and carried the sulphur dioxide disclosure under
   Golden. Removed on request 2026-08-14, markup and styles together. See
   claims.md: the BAN on claiming the range is chemical-free still stands, and
   is now the only thing holding that line. */

/* The white plate is gone. `mix-blend-mode: multiply` was tried here and did
   not take, cause never found, so the multiply is now baked into the files at
   export instead: out = src x paper / 255, per channel. White multiplied by
   paper IS paper, so the studio sweep becomes the page ground exactly, the
   soft shadow under each pile survives (which a cut-out would have lost), and
   the frame is no longer needed to hide the seam. Deploy files are
   `product-<slug>-1120.jpg`; the unmultiplied `caliber-*` files are still
   there and are what the home page's hover tiles use.
   If these are ever re-exported, re-apply the multiply or the plates come
   back.

   Written as `.media.media-plain` on purpose. `.media` is defined further down
   this file, so at equal specificity it wins on source order and a single
   class here silently does nothing. That is exactly what happened first time:
   the border and the paper-hi plate were still being painted while this rule
   looked correct. Do not "tidy" the doubled class away. */
.media.media-plain { border: 0; background: transparent; border-radius: 0; }

/* `.prod-sub` then `.lead` are two sibling paragraphs; `.split .body p + p`
   only covers the ones inside a split, so the wide entry needs this. */
.prod p + p { margin-top: 16px; }

/* The label above the hero h1. `.hero h1` carries its own bottom margin, so
   only the top of the stack needs setting. Products is the only page with a
   label in the hero; the home page opens straight on the headline. */
.hero .label { display: block; margin-bottom: 16px; }
/* The home page's hero lead reserves 34px for the CTA row beneath it. This one
   has no CTAs (the ask lives at the foot of the page and in the masthead), so
   that reserve is just a gap, and it is most obvious on mobile where the
   carousel is hidden and nothing fills the band. */
.hero-page .lead { margin-bottom: 0; }

/* An inner page's hero headline needs a measure; the home page's does not.
   `.hero h1 { max-width: none }` is right there, where the headline is
   "NNP Raisins." over a short rotating phrase and nothing reaches the ring.
   "Five varieties, one standard." is 29 characters and sets ~900px on one
   line at 66px, which runs the last two words underneath the front card.
   The type wins on z-index so it stays on top, but dark ink over a bright
   photograph is a collision either way.
   Scoped to the width where the carousel is actually on screen: below 901px
   the ring is hidden and the headline should have the full width back.

   A `ch` cap alone is NOT enough. The front card's left edge sits at a
   near-constant 51.5% of the viewport (measured 748/1440 and 484/940), so the
   copy has to be capped against the VIEWPORT as well as against its own font
   size. At a flat 46ch the lead cleared the card by 140px at 1440 and
   overlapped it by 6px at 940. `44vw - gut` holds a ~70px gutter at the narrow
   end, and the ch value takes over on wide screens where it is the tighter of
   the two. */
@media (min-width: 901px) {
  .hero-page h1    { max-width: min(14ch, calc(44vw - var(--gut))); }
  .hero-page .lead { max-width: min(46ch, calc(44vw - var(--gut))); }
}

/* --------------------------------------------------- range specification -- */
/* Deliberately NOT the home page's bento. That block already carries packaging,
   cleaning, sorting equipment and growing in bordered tiles, and repeating the
   same content in the same form here made the two pages read as one page shown
   twice. What survives is only what a specification sheet needs and the home
   page does not give: shelf life, capacity, testing, certification.
   Plain figures on paper. No tiles, no borders, nothing to click. */
.rangespec { display: grid; grid-template-columns: repeat(4, 1fr); gap: 34px 40px; margin: 0;
  border-top: 2px solid var(--violet); padding-top: 32px; }
.rangespec > div { min-width: 0; }
.rangespec dt { font-size: 11.5px; letter-spacing: .04em; color: var(--dim); }
.rangespec dd { margin: 9px 0 0; font-family: var(--display); font-weight: 800;
  font-size: clamp(17px, 1.5vw, 21px); letter-spacing: -.035em; line-height: 1.15; color: var(--ink); }

@media (max-width: 900px) {
  /* Stacked, alternating order would put two photographs together. */
  .prod:nth-of-type(even) .body { order: 0; }
  /* And the mirrored track sizes have to be undone explicitly. The desktop
     rule is `.prod:nth-of-type(even) .split` (0,3,0), which outranks the
     single-column `.split` in this block (0,1,0) whatever the viewport, so
     without this Type 9 and Red Flame keep a two-column layout on a phone
     while the other three stack. Media queries add no specificity. */
  .prod:nth-of-type(even) .split { grid-template-columns: 1fr; }
  .prod-wide-body { grid-template-columns: 1fr; gap: 32px; }
  /* Stacked, the band is no longer competing with a text column beside it, so
     it can take the full width the way the split photographs do. */
  .media.prod-wide-media { height: auto; aspect-ratio: 4 / 3; }
  .figs { gap: 22px 32px; }
  .rangespec { grid-template-columns: repeat(2, 1fr); }
}

/* Honeypot. Taken out of the layout without `display: none` or `hidden` —
   some bots skip fields hidden that obviously, and some browsers decline to
   submit them. This is off-screen and unreachable but still a real field. */
.hp {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  overflow: hidden; opacity: 0; pointer-events: none;
}

/* ------------------------------------------------------------ rotator ---- */
/* "NNP Raisins." holds still. The claim under it slides up and is replaced.
   Purpose: state indication, one company shown from six angles.
   Lines stack in a single grid cell, and an invisible sizer holds the height
   of the longest line so nothing below ever moves. */
.h1-fixed { display: block; white-space: nowrap; }
.rotator {
  display: grid;
  overflow: hidden;                   /* lines enter and leave behind this edge */
  padding-bottom: .1em;               /* descender room for the y in Quality */
}
.rot-sizer { grid-area: 1 / 1; visibility: hidden; pointer-events: none; white-space: nowrap; }
.rot-line {
  grid-area: 1 / 1;
  color: var(--violet);
  white-space: nowrap;                /* a phrase never breaks across two lines */
  opacity: 0;
  transform: translateY(0.9em);
  transition: opacity 520ms var(--ease-out), transform 520ms var(--ease-out);
}
.rot-line.is-current { opacity: 1; transform: none; }
/* Exits quicker than it enters, so the slot never feels congested. */
.rot-line.is-out { opacity: 0; transform: translateY(-0.9em); transition-duration: 360ms; }

/* A permanently moving headline is the one thing that has to stop completely.
   Reduced motion holds line one and never rotates. */
@media (prefers-reduced-motion: reduce) {
  .rot-line { transition: none; transform: none; }
}

/* ------------------------------------------------------------ calibers --- */
.calibers { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.calibers .grid { display: grid; grid-template-columns: repeat(5, 1fr); }
.caliber {
  position: relative; overflow: hidden; isolation: isolate;
  padding: 26px 22px 32px; border-right: 1px solid var(--line);
  text-decoration: none; color: inherit; display: block;
}
.caliber:last-child { border-right: 0; }
/* The resting type sits UNDER both layers, so the curtain wipes it off rather
   than fading its colour. The white copy comes back with the photograph. */
.caliber > div { position: relative; z-index: 0; }

/* The hover wipe. Two layers, one transition each, and the delays are what
   sequence them: the violet sheet grows from the left, then the photograph
   unclips from the left on top of it, so the violet reads as sliding off to
   the right. Delays on the base state run the exit, delays on the hover state
   run the entry, which is what makes it reverse cleanly without any JS.
   .caliber-shot is violet underneath the image, so a variety whose photo has
   not been shot yet simply lands on the old flat-violet hover. */
.caliber-shot, .caliber-wipe { position: absolute; inset: 0; pointer-events: none; }
.caliber-wipe {
  z-index: 1; background: var(--violet);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 300ms var(--ease-in-out) 260ms;
}
/* Ground is paper, not white, and the photograph multiplies onto it. The shots
   were taken on a white sweep, and white multiplied against paper IS paper, so
   the tile blends into the strip instead of flashing brighter than it. Keeps
   the soft shadow under the pile, which cutting the background out would lose.
   .caliber-shot makes its own stacking context, so the blend cannot reach the
   violet curtain underneath it. */
.caliber-shot {
  z-index: 2; background: var(--paper);
  /* Only the right edge animates. The other three sides are held 1px outside
     the box because at 1x the clip can round a hair short and leak a line of
     the violet curtain along the bottom. .caliber has overflow:hidden, so the
     overshoot never paints. */
  clip-path: inset(-1px 100% -1px 0);
  transition: clip-path 320ms var(--ease-in-out) 0ms;
}
.caliber-shot img { mix-blend-mode: multiply; }
/* The whole frame is shown, pile and white ground both, so the photograph is
   never pre-cropped. It breathes instead: a slow push in while the cursor is
   on the tile, reversing once it has gone far enough, forever, via `alternate`. */
.caliber-shot img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  transform: scale(1); transform-origin: center;
  transition: transform 500ms var(--ease-out);
}
@keyframes caliber-breathe { from { transform: scale(1); } to { transform: scale(1.14); } }

/* No scrim and no type over the photograph. The revealed state is the
   photograph alone, full bleed. The curtain carries the type off and nothing
   replaces it, so there is nothing here that needs contrast cover. */

@media (hover: hover) and (pointer: fine) {
  .caliber:hover .caliber-wipe { transform: scaleX(1); transition-delay: 0ms; }
  .caliber:hover .caliber-shot { clip-path: inset(-1px -1px -1px 0); transition-delay: 260ms; }
  /* Promote on entering the strip, not the tile. Asking for the layer in the
     same rule that starts the animation means the layer is built on the frame
     it is first needed, which is the hitch. Entering the strip is always
     earlier than entering any one tile. */
  .calibers:hover .caliber-shot img { will-change: transform; }
  .caliber:hover .caliber-shot img {
    animation: caliber-breathe 6.5s var(--ease-in-out) 260ms infinite alternate;
  }
}
.caliber:focus-visible .caliber-wipe { transform: scaleX(1); transition-delay: 0ms; }
.caliber:focus-visible .caliber-shot { clip-path: inset(-1px -1px -1px 0); transition-delay: 260ms; }
.caliber:focus-visible .caliber-shot img {
  animation: caliber-breathe 6.5s var(--ease-in-out) 260ms infinite alternate;
}
/* HIERARCHY SWAPPED 2026-08-16, his call: "replace caliber and raisin name, as
   caliber is secondary thing." The belt used to lead on the caliber figure at up
   to 82px with the variety name at 14px underneath, so the strip read as five
   numbers rather than five products. Name is now the display element and caliber
   drops to a mono spec line beside the count.

   The name CANNOT take the old 82px. These columns are ~256px at 1440 and
   "Black Kish Mish" is 15 characters, so anything near that size wraps to three
   lines and blows the belt's height out. 29px is the largest that holds
   "Sultana Sun-dried" on two lines in the narrowest column. */
.caliber-name {
  font-family: var(--display); font-weight: 800;
  font-size: clamp(21px, 2.1vw, 29px); line-height: 1.04; letter-spacing: -.04em;
  min-height: 2.08em;   /* two lines, so five tiles keep one baseline for the specs below */
}
/* "Sultana Sun-dried" is the only name with a hyphen in it, and CSS treats a
   hyphen as a break opportunity, so at this size it wrapped as "Sultana Sun-" /
   "dried". The span keeps the compound whole and pushes the turn back to the
   space, giving "Sultana" / "Sun-dried". Not a hard <br>: at 900px and below the
   belt goes 2-up and the name fits on one line again. */
.caliber-name .nb { white-space: nowrap; }
/* Was the headline, now a spec. Carries the word "Caliber" because a bare "6+"
   at 12px next to a piece count is a riddle - at 82px it never needed saying. */
.caliber-size { font-family: var(--mono); font-size: 12px; color: var(--ink); margin-top: 18px; letter-spacing: .02em; }
.caliber-spec { font-family: var(--mono); font-size: 11px; color: var(--dim); margin-top: 6px; }
@media (prefers-reduced-motion: reduce) {
  /* No sweep and no breathing. The photograph is still worth showing, so it
     simply appears. */
  .caliber-wipe, .caliber-shot, .caliber-shot img { transition: none; }
  .caliber:hover .caliber-shot img, .caliber:focus-visible .caliber-shot img { animation: none; }
}

/* ------------------------------------------------------------- section --- */
.section { padding: var(--sec) 0; }
.section-head { max-width: 60ch; margin-bottom: 48px; }
.section-head h2 { margin-bottom: 18px; }

/* -------------------------------------------------------- integration ---- */
/* The photo takes its height from the text beside it rather than from its own
   crop, so the two columns always finish level whatever the copy does. */
.split { display: grid; grid-template-columns: 1.08fr .92fr; gap: 56px; align-items: stretch; }
.split > .media { aspect-ratio: auto; min-height: 380px; }
/* The image is taken out of flow so it cannot push the figure taller than the
   text column. Without this it falls back to its own 4:5 height and drives the
   grid row, which is the opposite of what stretching is for. */
.split > .media img { position: absolute; inset: 0; }
.split .body p + p { margin-top: 18px; }
.figures { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); margin-top: 36px; }
.figures div { background: var(--paper); padding: 20px 22px; }
.figures b { display: block; font-family: var(--display); font-weight: 800; font-size: 32px; letter-spacing: -.05em; line-height: 1; }
.figures span { display: block; font-size: 13px; color: var(--dim); margin-top: 8px; }

/* ------------------------------------------------------------- process --- */
.steps { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--line); }
.step { padding: 24px 22px 30px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.step:nth-child(4n) { border-right: 0; }
.step-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 20px; }
.step-n { font-family: var(--mono); font-size: 11px; color: var(--dim); letter-spacing: .1em; }
.step h3 { margin: 0 0 8px; }
.step p { font-size: 14px; color: var(--dim); line-height: 1.55; }

/* Hover. These tiles are not links, so the response has to read as material,
   not as an invitation to click: no lift, no shadow, nothing that promises a
   destination. The ground warms, the stage number takes the accent, and a
   violet rule runs the top edge from the left, which is the caliber strip's
   direction of travel. Quieter than that strip on purpose. The wipe is the
   page's one signature and this must not compete with it. */
.step { position: relative; transition: background-color var(--t-hover) ease; }
.step::after {
  content: ""; position: absolute; left: 0; right: 0; top: -1px; height: 2px;
  background: var(--violet); pointer-events: none;
  transform: scaleX(0); transform-origin: left center;
  transition: transform 240ms var(--ease-in-out);
}
.step-n { transition: color var(--t-hover) ease; }
.step-icon { transition: transform 240ms var(--ease-out); }

@media (hover: hover) and (pointer: fine) {
  .step:hover { background: var(--paper-hi); }
  .step:hover::after { transform: scaleX(1); }
  .step:hover .step-n { color: var(--violet); }
  .step:hover .step-icon { transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  /* Colour stays, movement goes: the rule appears rather than growing, and the
     icon holds still. */
  .step::after { transition: none; }
  .step:hover .step-icon { transform: none; }
}

/* Icons. One stroke weight for the whole set, set here rather than per icon,
   so adding a ninth cannot drift off the system. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.step-icon {
  width: 28px; height: 28px; flex: none;
  fill: none; stroke: var(--violet); stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ---------------------------------------------------------- dark block --- */
.dark { background: var(--violet-dp); color: var(--chalk); padding: var(--sec) 0; }
.dark h2 { color: var(--chalk); }
.dark .lead { color: var(--chalk-dim); }
.dark .label { color: var(--chalk-dim); }
.certs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.cert { position: relative; overflow: hidden; background: rgba(242,237,248,.07); border: 1px solid rgba(242,237,248,.16); border-radius: var(--r); padding: 24px 22px 26px;
  transition: background-color var(--t-hover) ease, border-color var(--t-hover) ease; }
/* Content sits above the light. Both are positioned, so without this the glow
   would paint over the type and the marks. */
.cert-mark, .cert h3, .cert p { position: relative; z-index: 1; }

/* Certification marks on a white plate. They are NOT knocked out to white:
   these are third-party marks and their official colours are part of what makes
   them recognisable, and only Sedex ships a usable mono version anyway. The
   plate is a fixed box for all four so the row stays tidy despite the artwork
   running from square (FSSC) to 3.8:1 (Sedex); the caps below size each mark
   inside it rather than stretching any of them. */
.cert-mark {
  display: flex; align-items: center; justify-content: center;
  width: 152px; height: 64px; margin-bottom: 18px;
  background: #fff; border-radius: var(--r);
}
/* Height is set per mark, inline, because one cap cannot serve artwork running
   from 1.7:1 (EU) to 5.4:1 (the FSSC lockup). Sizing logos individually by eye
   is the normal way round this. Widths follow from the aspect ratios and all
   land inside the plate.
   `object-fit: contain` is the guard, not the layout: with a fixed height and
   `width: auto`, `max-width` clamps the width while the height holds, which
   SQUEEZES the artwork. That is exactly what happened when the FSSC mark was
   swapped from a square diamond to the wide lockup. With object-fit the worst
   case is letterboxing, never distortion. Keep it if you swap a logo. */
.cert-mark img { height: var(--mark-h, 32px); width: auto; max-width: 120px; object-fit: contain; }

/* Cursor spotlight. One light source, four windows onto it: every card carries
   its own copy of the glow, and all four track the same cursor point in their
   own local coordinates. The card's overflow clips it, so the light stops dead
   at the card border and never appears in the gaps between them.
   Created and positioned by site.js; there is no markup for it. */
.cert-glow {
  position: absolute; top: 0; left: 0; z-index: 0;
  width: 260px; height: 260px; margin: -130px 0 0 -130px;  /* centres on the reported point */
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle closest-side, rgba(226,183,255,.22), rgba(226,183,255,0));
  opacity: 0; transition: opacity 260ms ease;
  will-change: transform;
}

@media (hover: hover) and (pointer: fine) {
  .cert:hover { background: rgba(242,237,248,.12); border-color: rgba(242,237,248,.34); }
}
.cert h3 { color: var(--chalk); margin-bottom: 10px; }
.cert .issuer { font-size: 13.5px; color: var(--chalk-dim); line-height: 1.5; }
.cert .ref { font-family: var(--mono); font-size: 11px; color: var(--violet-lt); margin-top: 14px; word-break: break-all; }
.dark .after { margin-top: 40px; display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }

/* -------------------------------------------------------------- bento ---- */
.bento { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.tile { border: 1px solid var(--line); border-radius: var(--r); padding: 26px 24px 30px; background: var(--paper-hi); }
.tile-violet { background: var(--violet); border-color: var(--violet); color: #fff; }
.tile-violet .tile-sub { color: rgba(255,255,255,.78); }
.tile-wide { grid-column: span 2; }
.tile b { display: block; font-family: var(--display); font-weight: 800; font-size: clamp(28px, 2.6vw, 38px); letter-spacing: -.05em; line-height: 1; }
.tile h3 { margin-bottom: 10px; }
.tile-sub { font-size: 14px; color: var(--dim); line-height: 1.55; margin-top: 10px; }
.tile-list { list-style: none; margin: 14px 0 0; padding: 0; }
.tile-list li { font-size: 14px; color: var(--dim); padding: 9px 0; border-bottom: 1px solid var(--line); display: flex; justify-content: space-between; gap: 16px; }
.tile-list li:last-child { border-bottom: 0; }
.tile-list b { display: inline; font-family: var(--mono); font-size: 12.5px; font-weight: 500; letter-spacing: 0; color: var(--ink); }

.tile-violet h3 { color: #fff; }

/* --------------------------------------------------------------- news ---- */
.news { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.post { text-decoration: none; color: inherit; display: block; }
.post .media { margin-bottom: 18px; }
.post time { font-family: var(--mono); font-size: 11px; color: var(--dim); letter-spacing: .1em; text-transform: uppercase; }
.post h3 { margin: 10px 0 0; transition: color var(--t-hover) ease; }
.post .media img { transition: transform 420ms var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .post:hover h3 { color: var(--violet); }
  .post:hover .media img { transform: scale(1.03); }
}
@media (prefers-reduced-motion: reduce) {
  .post .media img { transition: none; }
  .post:hover .media img { transform: none; }
}

/* --------------------------------------------------------------- form ---- */
.contact { display: grid; grid-template-columns: .85fr 1.15fr; gap: 56px; align-items: start; }
.deets { list-style: none; margin: 30px 0 0; padding: 0; }
.deets li { padding: 16px 0; border-bottom: 1px solid var(--line); }
/* A separator under the last item separates it from nothing. Shared with the
   home page's contact block, where it removes one hairline and nothing else. */
.deets li:last-child { border-bottom: 0; }
.deets .label { display: block; margin-bottom: 6px; }
.deets a { font-weight: 600; text-decoration: none; font-size: 17px; }
.deets a:hover { color: var(--violet); }
.deets address { font-style: normal; font-size: 15px; color: var(--dim); line-height: 1.55; }

.form { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.field { display: flex; flex-direction: column; gap: 7px; }
.field-full { grid-column: span 2; }
.field label { font-size: 13.5px; font-weight: 600; }
.field .hint { font-size: 12.5px; color: var(--dim); }
.field input, .field select, .field textarea {
  font-family: var(--body); font-size: 15px; color: var(--ink);
  background: var(--paper-hi); border: 1px solid #c4c1b7; border-radius: var(--r);
  padding: 13px 14px; min-height: 46px; width: 100%;
}
.field textarea { min-height: 120px; resize: vertical; }
.field input::placeholder, .field textarea::placeholder { color: #6f6b7d; }
.field input:focus, .field select:focus, .field textarea:focus { outline: 2px solid var(--violet); outline-offset: 1px; border-color: var(--violet); }

/* ------------------------------------------------------------- footer ---- */
.footer { background: var(--ink); color: #cfccd6; padding: 68px 0 34px; }
.footer a { color: #cfccd6; text-decoration: none; }
.footer a:hover { color: #fff; }
.footer-top { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1.3fr; gap: 44px; padding-bottom: 44px; border-bottom: 1px solid rgba(255,255,255,.13); }
.footer .brand { color: #fff; margin: 0 0 18px; }
.footer .brand-logo { height: 44px; }
.footer .tagline { font-size: 14.5px; line-height: 1.6; max-width: 34ch; color: #a5a1ad; }
.footer h4 { font-family: var(--mono); font-size: 11px; font-weight: 500; letter-spacing: .14em; text-transform: uppercase; color: #837e90; margin: 0 0 16px; }
.footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 11px; }
.footer li { font-size: 14.5px; }
.footer address { font-style: normal; font-size: 14px; line-height: 1.6; color: #a5a1ad; margin-top: 12px; }
.social { display: flex; gap: 12px; margin-top: 20px; }
.social a { width: 40px; height: 40px; border: 1px solid rgba(255,255,255,.2); border-radius: var(--r); display: flex; align-items: center; justify-content: center; }
.social a:hover { border-color: #fff; }
.social svg { width: 17px; height: 17px; fill: currentColor; }
.footer-base { padding-top: 26px; display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; font-size: 13px; color: #837e90; }

/* --------------------------------------------------------- responsive ---- */
@media (max-width: 1080px) {
  .certs { grid-template-columns: repeat(2, 1fr); }
  .footer-top { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 900px) {
  :root { --gut: 20px; --sec: 64px; --mast-h: 74px; --mast-h-compact: 58px; }
  .brand-logo { height: 38px; }
  .masthead.is-stuck .brand-logo { height: 30px; }
  .footer .brand-logo { height: 38px; }
  .navtoggle { display: inline-flex; }
  .mainnav {
    display: none; position: absolute; left: 0; right: 0; top: 100%;
    background: var(--paper); border-bottom: 1px solid var(--line);
    flex-direction: column; align-items: stretch; gap: 0; padding: 8px var(--gut) 20px;
  }
  .mainnav[data-open="true"] { display: flex; }
  .mainnav a { padding: 14px 0; border-bottom: 1px solid var(--line); font-size: 16px; }
  .mainnav .btn { margin-top: 16px; }
  .calibers .grid { grid-template-columns: repeat(2, 1fr); }
  /* Two up, the columns are wide enough that every name fits on one line, so the
     two-line reserve is just a hole between the name and the caliber. */
  .caliber-name { min-height: 0; }
  .caliber { border-bottom: 1px solid var(--line); }
  .caliber:nth-child(2n) { border-right: 0; }
  .split, .contact { grid-template-columns: 1fr; gap: 36px; }
  /* Stacked, it gets its own crop back rather than a stretched one. */
  .split > .media { aspect-ratio: var(--ar, 4 / 3); min-height: 0; }
  .steps { grid-template-columns: repeat(2, 1fr); }
  .step:nth-child(4n) { border-right: 1px solid var(--line); }
  .step:nth-child(2n) { border-right: 0; }
  .bento, .news { grid-template-columns: 1fr; }
  .tile-wide { grid-column: span 1; }
  .form { grid-template-columns: 1fr; }
  .field-full { grid-column: span 1; }
  .footer-top { grid-template-columns: 1fr; gap: 34px; }
}
@media (max-width: 560px) {
  .certs { grid-template-columns: 1fr; }
  .figures { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Media. Every photo reserves its box before it loads, so nothing shifts.
   To fill a remaining slot, delete the placeholder element carrying class
   "ph" and put a real photo in its place, declaring the same width and
   height attributes. Nothing else in this file needs to change.
   ========================================================================== */
.media {
  position: relative; margin: 0; overflow: hidden;
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--paper-hi);
  aspect-ratio: var(--ar, 4 / 3);
}
.media img { width: 100%; height: 100%; object-fit: cover; }
.media .ph {
  position: absolute; inset: 0; display: flex; align-items: flex-end; padding: 18px;
  background: repeating-linear-gradient(135deg, #e5e3db 0 11px, #eceae3 11px 22px);
}
.media .ph p {
  font-family: var(--mono); font-size: 10.5px; line-height: 1.6;
  letter-spacing: .06em; text-transform: uppercase; color: #8f887c;
}
/* ⚠ THE SCRIM IS SIZED BY ARITHMETIC, NOT BY EYE, and the old one failed.
   It was `padding: 14px 16px` under a two-stop `rgba(22,20,28,.82) → 0`, which
   looks correct in a mockup and is not: `to top` puts the .82 end at the BOTTOM
   of the box, so the text - sitting 14 to 31px up from there in a 49px box -
   landed where the ramp was only 25-56% opaque. Measured against the rendered
   pixels of the vineyard photograph, white 13px type came out at 1.32:1 against
   a required 4.5:1, and 1.48:1 over the production photograph. Both are WCAG
   1.4.3 failures and neither is visible as one in a screenshot - a bright patch
   of grapes behind a caption just reads as a bright patch of grapes.
   The floor: white on ink needs the composite under it at luminance <= .183,
   which over a WORST-CASE pure-white photo pixel means alpha >= .58. Every stop
   the text crosses is held at .78 or above, so the guarantee holds over any
   photograph rather than over the two currently in place.
   The top padding tripled to give the ramp somewhere to happen. Over 20px it
   would read as a hard band across the picture; over ~47px it reads as light
   falling off, which is the point of a scrim. */
.media figcaption {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 34px 16px 14px;
  font-size: 13px; color: #fff;
  background: linear-gradient(to top,
    rgba(22,20,28,.92) 0%,
    rgba(22,20,28,.90) 22%,
    rgba(22,20,28,.78) 48%,
    rgba(22,20,28,.45) 68%,
    rgba(22,20,28,.18) 85%,
    rgba(22,20,28,0)  100%);
}
.dark .media { border-color: rgba(242,237,248,.2); background: rgba(242,237,248,.05); }
.dark .media .ph { background: repeating-linear-gradient(135deg, rgba(242,237,248,.05) 0 11px, rgba(242,237,248,.09) 11px 22px); }
.dark .media .ph p { color: var(--chalk-dim); }

/* ==========================================================================
   Reveal on scroll
   Purpose: hierarchy. Content is visible by default and only hidden once
   JS has confirmed it can bring it back, so a failed script never blanks
   the page.
   ========================================================================== */
/* A section animates itself only when it has no children opting in.
   Otherwise the section and its children would both move, which reads mushy. */
.js [data-reveal]:not(:has([data-reveal-child])) {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--t-reveal) var(--ease-out), transform var(--t-reveal) var(--ease-out);
}
.js [data-reveal]:not(:has([data-reveal-child])).is-in { opacity: 1; transform: none; }

.js [data-reveal] [data-reveal-child] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--t-reveal) var(--ease-out), transform var(--t-reveal) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.js [data-reveal].is-in [data-reveal-child] { opacity: 1; transform: none; }

/* Prefilled select flashes once so the buyer sees it was carried over. */
.field select.is-filled { border-color: var(--violet); background: #fff; }

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal]:not(:has([data-reveal-child])),
  .js [data-reveal] [data-reveal-child] { transform: none; transition-duration: 200ms; }
  .btn:active { transform: none; }
  html { scroll-behavior: auto; }
}

/* ==========================================================================
   Contacts page
   Appended at the END of the file on purpose. Products lost two rules to
   `.media` on source order alone (see the note above `.media-plain`), and this
   page adds new class names to a 950-line stylesheet. Last in the file means
   nothing already written can outrank it at equal specificity.

   REVISED 2026-08-15 after his review. Five changes, all his:
     1. The page has its OWN form again, copied from the home page.
     2. Two channels, not three - the phone number was printed twice.
     3. The page lacked colour: the channels are now a full-bleed violet strip.
     4. Coordinates are gone from the page. They only ever fed the map links.
     5. "Open in Maps" is a BUTTON that pops a sheet of map apps, the way
        Telegram does it - not a dropdown that expands in place.
   The closing CTA section is gone too: a contact page IS the call to action.
   ========================================================================== */

/* ----------------------------------------------------------- plain hero --- */
/* The only hero on this site with no photography in the band, so the headline
   holds it alone. `.hero h1 { max-width: none }` is set for the home page,
   where the headline is two words; "Your direct line to Samarkand." sets about
   1100px at 66px and reads as a banner rather than a sentence.
   Capped in `ch` only - unlike `.hero-page` there is no carousel card to clear,
   so there is no viewport term and no need to scope it to a width. */
.hero-plain h1   { max-width: 15ch; }
.hero-plain .lead { max-width: 52ch; margin-bottom: 0; }
/* The violet strip sits directly under this, the way the caliber belt does on
   the home page, so the hero does not carry its own full bottom air as well. */
.hero-plain { padding-bottom: clamp(34px, 4vw, 56px); }

/* --------------------------------------------------------- the 3D map --- */
/* His brief, 2026-08-15: the map fills the band from the bottom of the header
   to the top of the violet strip, and dissolves into the paper behind the
   headline.

   The frame needs no measuring. `.hero` is already exactly that band - `body`
   reserves the fixed masthead's height above it and `.cstrip` begins where it
   ends - so `top: 0; bottom: 0` inside it lands on both edges by construction.
   `right: 0` runs it to the viewport edge rather than to `.wrap`'s 1280.

   A BACKGROUND, not an <img>, for one reason: a background declared inside a
   min-width query is never fetched on a phone, where the panel is hidden and
   the file would be 221 KB of nothing. The head carries a matching media-scoped
   `preload` so it still starts early on desktop, which is what an <img> with
   fetchpriority would have bought.

   The image's own ground was corrected on export to land on --paper, so it does
   not need a blend mode. Do NOT reach for `mix-blend-mode: multiply` here the
   way the caliber shots and the parallax layers do: those sit on WHITE, and
   white multiplied by paper is paper. Paper multiplied by paper is darker than
   paper, and the panel would show as a grey rectangle. */
.hero-map { display: none; }

@media (min-width: 901px) {
  .hero-map {
    display: block;
    position: absolute; top: 0; right: 0; bottom: 0; width: 66%;
    background: url(../img/contact-map-samarkand-1600.jpg) center / cover no-repeat;
    pointer-events: none;
    /* Nine stops tracing a smoothstep, not a two-stop ramp. A linear gradient
       changes slope abruptly at each end and the eye reads that as a visible
       band down the picture - the same lesson the home hero's mask cost.
       Reaches full opacity at 44% of the panel, about 63% of the viewport. The
       headline's 15ch measure ends near 43%, so ink type is always over paper
       and never over the map.
       The ramp cannot be stretched much further to soften the map's own outer
       place names (Kimyogarlar on the Samarkand artwork): they sit around 12%
       into the panel, so lengthening the ramp moves them only a few percent
       while it starts eating SAMARKAND and the pin, which are the subject. They
       land near 20% opacity and read as texture beside the lead. Cropping them
       out is not available either - `cover` on a panel this wide fits the image
       by WIDTH and crops only height, so background-position-x does nothing.
       That same fact is why this mask survived the 2026-08-16 artwork swap
       untouched: image-width percentages map straight onto panel percentages,
       and SAMARKAND centres near 42% with the pin near 48%, both clearing the
       44% full-opacity point. Re-check those two numbers on any new artwork. */
    -webkit-mask-image: linear-gradient(to right,
      rgba(0,0,0,0) 0%, rgba(0,0,0,.043) 5.5%, rgba(0,0,0,.156) 11%,
      rgba(0,0,0,.316) 16.5%, rgba(0,0,0,.5) 22%, rgba(0,0,0,.684) 27.5%,
      rgba(0,0,0,.844) 33%, rgba(0,0,0,.957) 38.5%, rgba(0,0,0,1) 44%);
    mask-image: linear-gradient(to right,
      rgba(0,0,0,0) 0%, rgba(0,0,0,.043) 5.5%, rgba(0,0,0,.156) 11%,
      rgba(0,0,0,.316) 16.5%, rgba(0,0,0,.5) 22%, rgba(0,0,0,.684) 27.5%,
      rgba(0,0,0,.844) 33%, rgba(0,0,0,.957) 38.5%, rgba(0,0,0,1) 44%);
  }
}

/* -------------------------------------------------- violet contact strip -- */
/* His note, 2026-08-15: "page lacks color". This is the answer, and it does two
   jobs at once - it is also the "different layout" he asked for in place of
   three cards, one of which printed the same phone number as the one beside it.

   Structurally it is the caliber belt again: a full-bleed band immediately
   under the hero carrying the one thing the page is for. Packaging violet,
   chalk type, poster scale. Nothing else on the page needs to shout after it.

   WhatsApp is not a third entry any more - it is a line under the phone number,
   because it IS the phone number. */
.cstrip { background: var(--violet); color: var(--chalk); }
.cstrip .wrap { display: grid; grid-template-columns: 1fr 1fr; }
.cline {
  display: block; text-decoration: none; color: inherit;
  padding: clamp(30px, 3.4vw, 46px) 28px clamp(30px, 3.4vw, 46px) 0;
}
/* A rule between them rather than a gap: the two belong to one band. */
.cline + .cline { border-left: 1px solid rgba(242, 237, 248, .22); padding-left: clamp(26px, 3.2vw, 52px); }
.cline .label { display: block; color: var(--chalk-dim); }
.cline b {
  /* inline-block so the underline below matches the text, not the column. */
  position: relative; display: inline-block; margin-top: 15px; padding-bottom: 7px;
  font-family: var(--display); font-weight: 800; letter-spacing: -.045em;
  font-size: clamp(23px, 2.9vw, 40px); line-height: 1.05;
  overflow-wrap: anywhere; transition: color var(--t-hover) ease;
}
.cline b::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: currentColor; transform: scaleX(0); transform-origin: left;
  transition: transform 260ms var(--ease-out);
}
.cline .sub { display: block; margin-top: 10px; font-size: 14.5px; line-height: 1.55; color: var(--chalk-dim); }
@media (hover: hover) and (pointer: fine) {
  .cline:hover b { color: #fff; }
  .cline:hover b::after { transform: scaleX(1); }
}
.cline:focus-visible b::after { transform: scaleX(1); }

/* ----------------------------------------------------- addresses + maps --- */
/* The two addresses reuse `.deets`, the home page's own contact list, so this
   column is a genuine copy of the one beside the form there. */
.deets .mapbtn { margin-top: 15px; }
/* 44px, not the 42 it was built at: below that it stops being a comfortable
   touch target, and this is the one control on the page a buyer uses standing
   in a warehouse rather than sitting at a desk. */
.mapbtn { gap: 9px; min-height: 44px; padding: 11px 18px; font-size: 14px; }
.mapbtn svg { width: 16px; height: 16px; flex: none; }

/* ------------------------------------------------------------ map sheet --- */
/* His brief, 2026-08-15: a button that pops the map apps up, the way Telegram
   does - not a menu that unfolds in place.

   Built on the POPOVER API, so it still needs no JavaScript: `popovertarget`
   opens it, it renders in the top layer above everything, clicking outside
   closes it (`popover="auto"` light-dismiss) and Escape closes it. A <dialog>
   would have needed `showModal()` and therefore a script.

   Degradation: a browser with no popover support ignores the attribute and the
   sheet renders as a plain visible list of links under the button. Not pretty,
   but every link still works - which is why the animation below is behind an
   @supports guard. Putting `opacity: 0` in the base state unguarded would hide
   the sheet permanently in exactly those browsers. */
.mapsheet {
  width: min(360px, calc(100vw - 32px));
  border: 1px solid var(--line); border-radius: var(--r);
  background: var(--paper-hi); color: var(--ink);
  padding: 22px 22px 20px;
  /* A top-layer sheet is the one place elevation is correct on this site. The
     "no shadow" rule is about hover states pretending to be clickable. */
  box-shadow: 0 26px 64px rgba(22, 20, 28, .30);
}
.mapsheet::backdrop { background: rgba(22, 20, 28, .45); }
/* No eyebrow above this heading: the sheet opens from one specific address, so
   a "PRODUCTION SITE" label over "Navobod" told the reader what they had just
   clicked. The heading carries both instead. */
.mapsheet h3 { margin: 0 0 16px; font-size: 19px; }
.maplinks { display: grid; gap: 8px; }
.maplinks a {
  display: flex; align-items: center; gap: 12px; min-height: 44px;
  font-size: 15px; font-weight: 600; text-decoration: none; color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--r); padding: 11px 14px;
  background: #fff;
  transition: border-color var(--t-hover) ease, color var(--t-hover) ease;
}
/* The arrow trails the name. Leading it, the four icons formed a column the eye
   read before any of the labels, which is backwards: the map's name is the
   choice being made and the arrow only says "this leaves the site". */
.maplinks a svg { width: 16px; height: 16px; flex: none; margin-left: auto; color: var(--violet); }
.maplinks a:hover, .maplinks a:focus-visible { border-color: var(--violet); color: var(--violet); }
.mapsheet-close {
  margin-top: 14px; width: 100%; min-height: 44px; padding: 11px;
  background: none; border: 0; cursor: pointer;
  font-family: var(--body); font-size: 14px; font-weight: 600; color: var(--dim);
  transition: color var(--t-hover) ease;
}
.mapsheet-close:hover { color: var(--ink); }

@supports selector(:popover-open) {
  .mapsheet {
    opacity: 0; transform: translateY(10px) scale(.98);
    transition: opacity 200ms var(--ease-out), transform 220ms var(--ease-out),
                overlay 220ms allow-discrete, display 220ms allow-discrete;
  }
  .mapsheet:popover-open { opacity: 1; transform: none; }
  /* The entry state. Without it the sheet appears already open and only the
     exit animates, which reads as a glitch rather than as a sheet arriving. */
  @starting-style { .mapsheet:popover-open { opacity: 0; transform: translateY(10px) scale(.98); } }
  .mapsheet::backdrop { opacity: 0; transition: opacity 200ms ease, overlay 200ms allow-discrete, display 200ms allow-discrete; }
  .mapsheet:popover-open::backdrop { opacity: 1; }
  @starting-style { .mapsheet:popover-open::backdrop { opacity: 0; } }
}

@media (max-width: 900px) {
  /* Stacked, the divider has to move from the side to the top. */
  .cstrip .wrap { grid-template-columns: 1fr; }
  .cline { padding-right: 0; }
  .cline + .cline { border-left: 0; border-top: 1px solid rgba(242, 237, 248, .22); padding-left: 0; }
  .hero-plain h1 { max-width: none; }
}

@media (max-width: 560px) {
  /* A bottom sheet on a phone, which is what his reference actually does -
     Telegram's app chooser slides up from the bottom. It is also the reachable
     half of the screen: a centred box on a tall phone sits under nothing your
     thumb can get to.
     `inset` and `margin` have to be set together because the popover UA rule
     centres the sheet with `inset: 0; margin: auto`, and overriding only one of
     them leaves it centred. Radius stays on the site's single 3px scale. */
  .mapsheet {
    width: 100%; max-width: none;
    inset: auto 0 0 0; margin: 0;
    border-width: 1px 0 0;
    border-radius: var(--r) var(--r) 0 0;
    padding: 22px 18px calc(18px + env(safe-area-inset-bottom));
  }
}

@media (prefers-reduced-motion: reduce) {
  .cline b, .cline b::after { transition: none; }
  .mapsheet, .mapsheet::backdrop { transition: none; transform: none; }
}

/* ==========================================================================
   Certification page
   Appended after the contacts block, same reasoning as that block: last in the
   file means nothing already written outranks it at equal specificity.

   The brief this answers: the home page already carries a certificate block -
   four translucent cards on violet with mark, issuer and reference number,
   ending in a button that points here. If this page restated that, the two
   would read as one page shown twice, which is exactly what went wrong when
   the Products page repeated the home bento.
   So this is a REGISTER, not a card grid. Full-width rows on paper, hairline
   separated, and the reference number - not the logo - is the largest thing in
   each row, because the number is the part a buyer actually verifies. It is set
   in mono at a size the mono face is used at nowhere else on this site, which
   is what gives the page its own texture without a new colour.
   ========================================================================== */

/* "Standards that speak for themselves." is 36 characters against the contacts
   page's 32, so it needs MORE measure than `.hero-plain`'s 15ch, not less. 18ch
   breaks it "Standards that speak / for themselves." - two lines, the turn
   falling at the natural pause. At 15ch it broke after "that", splitting the
   phrase mid-thought. (Was 13ch for the earlier "The documents, not the logos.") */
.hero-cert h1 { max-width: 18ch; }

/* ==========================================================================
   About us (`site/about-us/index.html`)
   Four new components. Everything else on that page is reused as-is:
   `.split`, `.section-head`, `.bento` / `.tile`, `.figures`, `.media`.
   ========================================================================== */

/* ⚠ REBUILT 2026-08-16, his instruction. The band was copy-left / copy-right
   because the page had no photography; it now has his portrait, so it is the
   normal shape - copy left, artwork right - like the other four heroes.
   The photograph column is `auto`: the image is sized by its HEIGHT (see
   `.hero-portrait img`), so the column has to take whatever width that height
   implies rather than dictating one. `align-items: end` stands the figure on
   the bottom of the band and drops the copy onto the same line, which is what
   makes him look like he is standing on the violet strip below rather than
   floating in a cell. */
.hero-about-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(32px, 4vw, 72px); align-items: end;
}
/* The copy is ~300px against a 480px figure, so bottom-aligning BOTH left a
   150px hole under the masthead and put the label almost halfway down the band.
   Centred, the slack is split above and below and the type sits in the middle
   of the figure rather than at its feet. */
.hero-about-copy { align-self: center; }
/* The band already carries the portrait, so it needs less of its own air than
   `.hero`'s generic 104px top - which was set for a hero whose only content is
   a headline. */
.hero-about { padding-top: clamp(44px, 5vw, 76px); }
/* `.hero-plain` caps the h1 at 15ch for the contacts page's headline. This one
   is four short words and wants the column, not a measure inside it. */
.hero-about h1 { max-width: 14ch; margin-bottom: clamp(22px, 2.4vw, 32px); }

/* ------------------------------------------------------- founder quote --- */
/* Moved into the hero 2026-08-16 - it was the whole content of the violet
   strip below. On violet at 37px it WAS the page's opening statement; under a
   66px headline on paper it is a subhead, so it drops to lead-ish scale. Set in
   the display face at 600 anyway, because it is a quotation and has to read as
   a different voice from the body copy that starts two sections down.
   ⚠ PIXELS, NOT `ch`, as before: Bricolage's `ch` is far narrower than its
   average letter at display weight, so a `ch` measure tuned on body copy sets
   this about half as wide as intended. */
/* ⚠ `margin: 0` KILLS THE UA INDENT. A `blockquote` carries `margin-inline:
   40px` by default and nothing on this site resets it, so the quote and its
   attribution stood 38px right of the label and the headline and the column's
   left edge came out ragged. It was invisible on the violet strip because the
   quote was the only thing in the band. */
.hero-quote { margin: 0; max-width: 30em; }
.hero-quote p {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(18px, 1.6vw, 23px); line-height: 1.34; letter-spacing: -.02em;
  /* Hangs the opening quote mark into the margin so the first letter lines up
     optically with the headline above it, rather than the punctuation doing so
     and pushing the sentence right. First line only, which is what
     `text-indent` does. */
  text-indent: -.42em;
  /* The quote marks are typed by the browser, not baked into the copy, so the
     text stays exactly as `current-site-copy.md` has it and stays selectable
     and searchable without them. */
  quotes: "\201C" "\201D";
}
.hero-quote p::before { content: open-quote; color: var(--violet); }
.hero-quote p::after  { content: close-quote; color: var(--violet); }
.hero-quote footer { margin-top: 18px; }
.hero-quote footer b {
  display: block; font-family: var(--display); font-weight: 800;
  font-size: 15px; letter-spacing: -.02em;
}
.hero-quote footer span {
  display: block; margin-top: 5px;
  font-family: var(--mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--dim);
}

/* ---------------------------------------------------- founder portrait --- */
/* ⚠ MULTIPLY, NOT A CUT-OUT AND NOT A FRAME. The file is a masked portrait on
   pure white (measured 255,255,255 at every corner and edge midpoint), and
   white multiplied by paper IS paper - so the ground disappears with no alpha
   channel, no mask and no box. Put a border, a card or a --paper-hi plate
   behind this and the white rectangle comes straight back inside it.
   Sized by HEIGHT so the figure keeps a constant presence in the band while the
   viewport changes; the column is `auto` so the width follows. The negative
   bottom margin is exactly `.hero-plain`'s bottom padding, so he stands on the
   band's edge - which is the top edge of the violet strip. `.hero` is
   `overflow: hidden`, so nothing can spill past it. */
/* ⚠ THE PAPER BACKGROUND IS WHAT MAKES THE MULTIPLY WORK, and it is not
   decoration. `mix-blend-mode` blends an element with the backdrop inside its
   own stacking context, and `.hero .wrap` carries `z-index: 1` (it has to, or
   the hero artwork paints over the copy) - so the img's backdrop is that wrap,
   which is transparent, and white multiplied by nothing stays white. The first
   build shipped a white rectangle around him for exactly this reason. Painting
   --paper on the figure puts the page's own ground inside the group.
   The negative bottom margin is exactly `.hero-plain`'s bottom padding, so he
   stands on the band's edge, which is the top edge of the violet strip. */
.hero-portrait {
  margin: 0; margin-bottom: calc(-1 * clamp(34px, 4vw, 56px));
  background: var(--paper);
  /* His note, 2026-08-17: bring him "a bit left towards the center of right".
     He sat against the wrap's right edge because the column is `auto` and there
     is no free space in it for `justify-self` to distribute - the margin is the
     only lever that moves him without changing the copy column. 6vw lands his
     centre at ~75% of the viewport at 1440, which is the middle of the band's
     right half. Zeroed below 900px, where the hero stacks and he is centred. */
  margin-right: clamp(0px, 6vw, 96px);
}
.hero-portrait img {
  display: block; height: clamp(330px, 34vw, 480px); width: auto;
  mix-blend-mode: multiply;
}

/* --------------------------------------------------------- principle row --- */
/* The caliber belt's idiom - bordered cells, no boxes - carrying three short
   principles. See the note in `about-us/index.html`: this exists so the page
   does not run the same card grid three times. */
.prow {
  display: grid; grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
}
.pcell { padding: clamp(26px, 3vw, 36px) clamp(20px, 2.4vw, 32px) clamp(30px, 3.4vw, 40px); }
.pcell + .pcell { border-left: 1px solid var(--line); }
/* ⚠ The outer cells lose their outer padding. This row sits INSIDE `.wrap`,
   unlike the full-bleed counter strip, so a 32px inset on the first cell puts
   "Sustainable farming" 32px right of the heading directly above it and the
   column edge visibly breaks. Full-bleed strips can afford the inset because
   there is no grid edge beside them to disagree with. */
.pcell:first-child { padding-left: 0; }
.pcell:last-child  { padding-right: 0; }
.pcell h3 { margin-bottom: 12px; }
.pcell p { font-size: 14.5px; line-height: 1.6; color: var(--dim); }

/* ------------------------------------------------------ tagline strip ---- */
/* ⚠ THIS IS THE OLD FOUNDER STRIP, ON A DIET. It used to carry the quotation at
   37px over three lines with an attribution under it - about 420px of violet
   directly under the hero. His instruction 2026-08-16: the quote goes up into
   the hero and this becomes "a thin one or two line strip".
   Thin is the whole point, so the two elements sit SIDE BY SIDE rather than
   stacked: the headline sets on one line, the sentence beside it on two, and
   the band is two lines of body copy deep. Stack them and it is four lines and
   the slab is back.
   ⚠ `align-items: center`, his call 2026-08-17: "align the title to the center
   of text on the right". It shipped as `baseline` - the sentence's first line
   sitting on the headline's baseline - which hung the headline off the top of a
   two-line paragraph. Centred, the two blocks share a middle. */
.tagstrip { background: var(--violet); color: var(--chalk); }
.tagstrip .wrap {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: clamp(24px, 3.4vw, 56px); align-items: center;
  padding-block: clamp(26px, 3vw, 40px);
}
.tagstrip h2 {
  font-size: clamp(24px, 2.6vw, 34px); line-height: 1.1;
  /* `nowrap` because this column is `auto`: the headline is what sets the
     column's width, and letting it wrap turns a one-line band into two. It is
     20 characters and the sentence beside it has `minmax(0, 1fr)`, so the
     sentence takes the reflow instead. */
  white-space: nowrap; margin: 0;
}
/* On violet, --violet nearly disappears. --violet-lt is the accent the dark
   bands use everywhere else on the site. */
.tagstrip .accent { color: var(--violet-lt); }
.tagstrip p {
  margin: 0; max-width: 62ch;
  font-size: clamp(15px, 1.15vw, 16.5px); line-height: 1.6; color: var(--chalk-dim);
}

/* ------------------------------------------------------ counter strip ---- */
/* Five figures across, bordered top and bottom, in the caliber belt's idiom -
   the belt is already this site's way of saying "five facts in a row", and the
   About page needs the numbers early.
   ⚠ Every one of these rendered as `0` on the old site (Elementor animated
   counters). They are hardcoded text here, like the home page's `.figures`. */
.figstrip { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.figstrip .wrap { display: grid; grid-template-columns: repeat(5, 1fr); }
.figcell { padding: clamp(24px, 3vw, 38px) clamp(14px, 1.8vw, 26px); }
.figcell + .figcell { border-left: 1px solid var(--line); }
.figcell b {
  display: block; font-family: var(--display); font-weight: 800;
  font-size: clamp(24px, 2.4vw, 34px); letter-spacing: -.05em; line-height: 1;
}
.figcell span { display: block; margin-top: 10px; font-size: 13px; color: var(--dim); }

/* --------------------------------------------------------- split, flipped -- */
/* `order`, not a second `grid-template-columns`. The columns are 1.08fr/.92fr
   and the text column has to stay the wider of the two whichever side it sits
   on, so flipping the TRACKS would also flip the proportion. Flipping the item
   keeps the text wide and just moves the photograph to the left. */
.split-rev { grid-template-columns: .92fr 1.08fr; }
.split-rev > .media { order: -1; }

/* ----------------------------------------------------------- bento, four --- */
.bento-4 { grid-template-columns: repeat(4, 1fr); }

/* ----------------------------------------------------------------- ticks --- */
/* A list that reads as a specification rather than as prose. The marker is a
   violet square on the site's 3px radius, not a tick glyph: there is no tick
   anywhere else here and one font's tick is another font's dingbat. */
/* ⚠ `padding: 0` is load-bearing. `list-style: none` removes the marker but NOT
   the browser's default `padding-inline-start: 40px`, so every item sat 40px
   right of the paragraph above it - plus this list's own 22px marker inset, so
   62px in total. Obvious once the section stacked on a phone and the two blocks
   shared a left edge to be measured against. */
.ticks { list-style: none; margin-top: 26px; padding: 0; display: grid; gap: 13px; }
.ticks li {
  position: relative; padding-left: 22px;
  font-size: 15.5px; line-height: 1.6; color: var(--dim);
}
.ticks li::before {
  content: ""; position: absolute; left: 0; top: .55em;
  width: 7px; height: 7px; border-radius: 1px; background: var(--violet);
}

/* ---------------------------------------------------------- partnerships --- */
/* Violet again, closing the page on the ground the founder strip opened it on.
   It is the last section he asked for, so it also has to do the work a closing
   CTA would normally do. */
.partners { background: var(--violet); color: var(--chalk); padding: var(--sec) 0; }
.partners .label { color: var(--chalk-dim); }
.partners h2 { color: var(--chalk); }
.partners .lead { color: var(--chalk-dim); }
.pgrid {
  list-style: none; margin-top: clamp(34px, 4vw, 54px);
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px;
  background: rgba(242, 237, 248, .22);
}
.pgrid li {
  background: var(--violet); padding: 24px 22px 26px;
  font-family: var(--display); font-weight: 600;
  font-size: clamp(16px, 1.35vw, 19px); line-height: 1.3; letter-spacing: -.02em;
}

/* ------------------------------------------------ about us, colour bands --- */
/* ⚠ THE COLOUR IS THE PAGE'S STRUCTURE, not decoration on top of it. Grounds
   run paper / violet / chalk / paper / deep violet / paper / deep violet /
   paper / chalk / violet. The two violet strips bookend, the two chalk bands
   sit inside them, the two deep-violet bands rhyme across the middle, and no
   two coloured bands ever touch - every paper section is a rest between them.
   NOTHING NEW WAS ADDED TO THE PALETTE to get here: `--chalk` and `--violet-dp`
   were already defined and already used (the home page's certificate band is
   `.dark`). If a band needs a colour that is not in `:root`, the answer is a
   different band, not a new colour. */
.band-chalk { background: var(--chalk); }
/* Paper-coloured borders vanish on chalk, so anything drawn in `--line` inside
   a chalk band has to be re-tinted from the violet the band is made of. */
.band-chalk.figstrip,
.band-chalk .figcell + .figcell,
.band-chalk .figcell { border-color: var(--chalk-dim); }
.band-chalk .tile { background: rgba(255,255,255,.62); border-color: var(--chalk-dim); }

/* ------------------------------------------------------ logistics truck --- */
/* His brief, 2026-08-17: "use it for logistics section, with animation as if it
   comes from right and stops before hitting left column."
   ⚠ IT IS A GRID CELL, NOT AN ABSOLUTE ELEMENT, and that is what makes "stops
   before the copy" true at every width instead of at the one it was tuned at.
   An absolute truck sized in `vw` collides with the copy somewhere around
   1000px, because the copy's measure stops growing (60ch) while a percentage
   width keeps marching left. As a column it cannot overlap by construction -
   the gap IS the grid gap.
   The head's own bottom margin moves to `.loghead` so the two cells share a
   bottom edge; left on the head it sits inside the copy's cell.

   ⚠ DOUBLED 2026-08-17, his call: "make the truck 2x bigger, it is ok, the back
   of the truck doesnt fit in the frame, important thing is logo being visible,
   and align it to the center of left text colomn, moving upper."
     - `width: 200%` of its own cell, so it is exactly twice the size it was at
       every width, and the tail runs off the right of the viewport. A grid item
       with a definite width sits at its cell's inline START, so it overflows
       rightwards and the cell's LEFT edge - the thing that must stay clear of
       the copy - has not moved at all. That is why this is still a grid cell
       and not an absolute element: anchoring the left edge in `vw` or `%` of
       the section walks into the copy at around 950px, where the copy's 60ch
       measure has stopped shrinking.
     - **The logo is the constraint, not the tail.** It sits 41-56% along the
       artwork, so at 1440 it lands around x 1230-1380, inside the viewport, and
       further in at narrower widths because the truck scales with its cell.
       **If the artwork is ever re-rendered with the logo further back, re-check
       this** - the tail is expendable, the wordmark is not.
     - `align-items: center`, was `end`: he asked for it centred on the copy,
       which also lifts it, and at 1440 the doubled truck is 344px against the
       copy's 345 so the two now span the same band. The section does not grow -
       the truck is never the taller cell. */
.loghead {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .8fr);
  /* ⚠ The 40px floor is the CLEARANCE, not a rhythm choice. This gap is the
     whole gap between the copy and the truck's mirror once the copy fills its
     cell, which it does below ~1150. At the old 28px floor the cab crowded the
     last line of copy at 1000. */
  gap: clamp(40px, 3.4vw, 56px); align-items: center;
  margin-bottom: 48px;
}
.loghead .section-head { margin-bottom: 0; }
/* ⚠ `max-width: none` IS LOAD-BEARING. Line 78 sets `img { max-width: 100% }`
   for the whole site, which silently clamps this 200% back to the cell's width -
   the first build of the doubled truck rendered at exactly its old size and
   looked like the change had not applied at all. This is the one image on the
   site that is meant to be wider than its box. */
.truck { display: block; width: 200%; max-width: none; height: auto; }
/* ⚠ THE SECTION MUST CLIP. The truck starts a full width plus a slice of the
   viewport to the right of its resting place, which is scrollable overflow -
   without this the document grows a horizontal scrollbar on a page that has
   none anywhere else. */
.sec-logistics { overflow: hidden; }

/* ⚠ THE OFF-SCREEN START IS SCOPED THREE WAYS ON PURPOSE, and each one is a
   fallback, not a nicety:
     `.js`  - without scripting nothing ever adds `.is-in`, so an unscoped start
              state would park the truck off the right edge forever.
     no-preference - reduced motion gets no travel at all. There is no gentler
              version of "a lorry drives across the section"; the still frame is
              the whole content.
     `.is-in` - the existing reveal observer already sets it on this section, so
              the drive costs no new JS, no new observer and no scroll handler.
   The curve starts fast and brakes long: it is already moving when it enters
   the frame, which is what separates a vehicle arriving from a box sliding. */
@media (prefers-reduced-motion: no-preference) {
  .js .truck {
    /* ⚠ 72% OF ITS OWN WIDTH, NOT 100%, and the number is measured rather than
       chosen. The truck already bleeds off the right edge at rest, so it only
       has to travel far enough to clear the viewport: at 1440 this puts its
       left edge at 1692 against a 1440 viewport, and the margin stays positive
       all the way down to 950. A full 100% would start it 1234px out, and with
       an ease-out that spends most of the duration off-screen - the visible
       part would be the slow tail of the curve, which reads as drifting, not
       arriving. The phone overrides this below. */
    transform: translateX(calc(72% + 10vw));
    transition: transform 1500ms cubic-bezier(.15,.72,.22,1);
  }
  .js .sec-logistics.is-in .truck { transform: none; }
}

/* Tiles on the deep-violet bands. Same construction as `.cert` on the home
   page's dark band - a translucent chalk plate rather than a solid card - so
   the two dark bands on this page belong to the same family as that one. */
.dark .tile {
  background: rgba(242,237,248,.07);
  border-color: rgba(242,237,248,.16);
}
.dark .tile h3 { color: var(--chalk); }
.dark .tile-sub { color: var(--chalk-dim); }
/* ⚠⚠ WITHOUT THESE THREE THE SPEC ROWS ARE INVISIBLE, and it is not subtle.
   `.tile-list li` is `--dim` and `.tile-list b` is `--ink` - both chosen for a
   paper ground, and `--ink` is #16141c, which on the black end of `.dark-dusk`
   is black on black. Found by rendering the page after the logistics band went
   dark on 2026-08-18: "Equipment  Tomra, Bigtem and VISYS" and "Metal
   detection  Yes, every batch" had disappeared completely and four more rows
   were unreadable. The values are the whole point of that tile.
   ⚠ `.dark .tile` existed before this and `.dark .tile-list` did not, so the
   trap was already loaded for any dark band that used a `.tile-list`; the
   logistics band is simply the first one that did. */
.dark .tile-list li { color: var(--chalk-dim); border-bottom-color: rgba(242,237,248,.16); }
.dark .tile-list b { color: var(--chalk); }
.dark .tile b { color: var(--chalk); }
.dark .prow { border-color: rgba(242,237,248,.22); }
.dark .pcell + .pcell { border-left-color: rgba(242,237,248,.22); }
.dark .pcell h3 { color: var(--chalk); }
.dark .pcell p { color: var(--chalk-dim); }
/* `.dark` sets its own vertical padding, so a `.section` class on the same
   element would double it. The dark bands here carry `.dark` alone. */

/* -------------------------------------------------------------- icons ----- */
/* ⚠ ONE FAMILY, matched to the home page's production-stage icons: 24x24
   viewBox, 1.5 stroke, round caps and joins, no fill. Colour comes from the
   stroke so a single symbol serves both grounds.
   `--violet-lt` on the dark bands, not `--violet`: the accent violet on deep
   violet is nearly invisible, and an icon that cannot be read is worse than no
   icon at all. */
.f-icon {
  display: block; width: 26px; height: 26px; margin-bottom: 16px;
  fill: none; stroke: var(--violet); stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform 240ms var(--ease-out);
}
.dark .f-icon { stroke: var(--violet-lt); }

/* --------------------------------------------------------- our people ---- */
/* Copy two-up above, photograph full width beneath - see the note in the
   markup. The ticks column is the narrower of the two because it is a scan
   list, not prose; the paragraph beside it keeps the reading measure. */
.people-head {
  display: grid; grid-template-columns: 1.1fr .9fr;
  gap: clamp(36px, 4.5vw, 72px); align-items: start;
  margin-bottom: clamp(40px, 4.5vw, 64px);
}
.people-head .ticks { margin-top: 0; }
/* ⚠ `aspect-ratio`, not a stretched box. `.split > .media` overrides the
   aspect to `auto` and stretches; this figure is NOT in a split and must keep
   the photograph's own 3:2, because the subject is a line of people and any
   crop removes some of them. */
.people-shot { aspect-ratio: 3 / 2; }
.people-shot img { width: 100%; height: 100%; object-fit: cover; }

/* ------------------------------------------------- about us, hover ------- */
/* ⚠ THIS IS A COHESION FIX, NOT DECORATION. The eight production steps on the
   home page already answer the pointer - the ground warms, the stage number
   takes the accent, and a violet rule wipes the top edge from the left. These
   tiles and cells are the same visual object on a different page and were
   completely inert, which reads as an unfinished page rather than as restraint.
   The register is copied from `.step` deliberately, including its constraint:
   NO LIFT, NO SHADOW, nothing that promises a destination. None of these are
   links, and a card that rises on hover is making a promise it cannot keep.
   The rule wipes; the icon steps 2px in the same direction; the ground warms.
   Three things, one direction of travel, 240ms. */
.bento-4 .tile, .prow .pcell {
  position: relative;
  transition: background-color var(--t-hover) ease;
}
.bento-4 .tile::after, .prow .pcell::after {
  content: ""; position: absolute; left: 0; right: 0; top: -1px; height: 2px;
  background: var(--violet); pointer-events: none;
  transform: scaleX(0); transform-origin: left center;
  transition: transform 240ms var(--ease-in-out);
}
.dark .bento-4 .tile::after, .dark .prow .pcell::after { background: var(--violet-lt); }

@media (hover: hover) and (pointer: fine) {
  .band-chalk .tile:hover { background: #fff; }
  .dark .tile:hover { background: rgba(242,237,248,.12); }
  .bento-4 .tile:hover::after, .prow .pcell:hover::after { transform: scaleX(1); }
  .bento-4 .tile:hover .f-icon, .prow .pcell:hover .f-icon { transform: translateX(2px); }
}

/* The rule still wipes under reduced motion - it is a state change, not
   decoration - but instantly, and the icon stops moving entirely. A blanket
   `transition: none` here would remove the feedback along with the movement. */
@media (prefers-reduced-motion: reduce) {
  .bento-4 .tile::after, .prow .pcell::after { transition-duration: 1ms; }
  .f-icon { transition: none; }
  .bento-4 .tile:hover .f-icon, .prow .pcell:hover .f-icon { transform: none; }
}

/* -------------------------------------------------- about us, responsive --- */
/* ⚠ THESE LIVE HERE, NOT IN THE SHARED `@media` BLOCKS FURTHER UP, and that is
   not a filing preference. The site's responsive section sits at around line
   1050, ABOVE this block. A media query adds no specificity, so
   `.figstrip .wrap` up there and `.figstrip .wrap` down here tie at 0,2,0 and
   the LATER one wins at every width - the overrides simply did nothing, and the
   five counters stayed five across at 1000px with the last cell hanging out of
   the wrap. Caught by screenshotting at 1000. Any future rule for a component
   defined in this block belongs here too. */
@media (max-width: 1080px) {
  .bento-4, .pgrid { grid-template-columns: repeat(2, 1fr); }
  /* The principle row keeps three across here - the cells are short and the
     row's whole job is to be a horizontal beat against the grids below it.
     Stepping it to two would make it another grid. */
  /* ⚠ The counter strip is deliberately NOT stepped here. It was, to three and
     two, and that leaves a fifth cell alone on its own row with dead paper
     beside it - a five-item row reads as a set, three-then-two reads as a
     layout that ran out of room. Five across survives to the site's own 900px
     breakpoint: at 901 each cell is ~164px wide, ~132px inside its padding, and
     the longest label ("Years of excellence", 13px) sets about 118. Below 900
     it goes to two, where the last cell is alone by arithmetic rather than by
     choice. Checked at 940 and 901 before removing the step. */
}
@media (max-width: 900px) {
  /* ⚠ BOTH OF THESE, and the first one is not optional. `.split-rev` is defined
     in the block above, which sits BELOW the shared responsive section, so its
     two-column `grid-template-columns` outranks the single-column `.split` rule
     up there at equal specificity - the Production section stayed two columns
     on a phone. `.split > .media` then had a stretched row to fill, and
     `aspect-ratio` turned that 1105px height into a 1474px WIDTH: the document
     scrolled to 1724px against a 497px viewport and every line of copy on the
     page ran off the right edge.
     Found by measuring `documentElement.scrollWidth` against `clientWidth`, not
     by looking - a mobile screenshot crops to the canvas and shows clipped text
     that looks like a font problem. */
  .split-rev { grid-template-columns: 1fr; }
  /* And the photograph gives up its left-hand position so the TEXT leads. It
     sits left on a wide screen for rhythm against the section above it, and
     there is no rhythm to keep in a single column. */
  .split-rev > .media { order: 0; }
  /* Two counters across, not one. These are short numbers and a single column
     of five is a lot of scrolling for five facts. Row borders are drawn per
     cell, because a cell that starts a row must not keep a left border it no
     longer sits against. */
  .figstrip .wrap { grid-template-columns: repeat(2, 1fr); }
  .figcell { border-top: 1px solid var(--line); }
  .figcell:nth-child(-n + 2) { border-top: 0; }
  .figcell:nth-child(2n + 1) { border-left: 0; }
  .bento-4, .pgrid { grid-template-columns: 1fr; }
  /* The strip stacks and the headline is allowed to wrap. It is two lines on a
     phone instead of one, which is the honest cost of a 375px column - and
     still a strip, not the slab it replaced. */
  .tagstrip .wrap { grid-template-columns: 1fr; gap: 10px; }
  .tagstrip h2 { white-space: normal; }
  /* The hero stacks: the copy, then the portrait under it, centred and still
     standing on the violet strip - the negative bottom margin is inherited from
     the desktop rule and is what keeps him on that edge. */
  .hero-about-grid { grid-template-columns: 1fr; gap: 26px; align-items: start; }
  .hero-about h1 { max-width: none; }
  .hero-quote { max-width: none; }
  /* The logistics head stacks and the truck runs the full column under the
     copy. It keeps its drive-in: the section still clips, so it still arrives
     from off the right edge.
     ⚠ It does NOT get the desktop's 200%. There is no second column to bleed
     past on a phone, and a truck twice the column's width would show the cab
     alone with the wordmark off the edge - the one thing that has to stay in
     frame.
     ⚠ The start offset goes back to a full width for the same reason (at 512
     the desktop's 72% leaves the cab still on screen), but it lives in its OWN
     combined media query below, not in here: this block is not motion-scoped,
     and a start transform written here would park the truck off-screen for
     good under reduced motion. */
  .loghead { grid-template-columns: 1fr; gap: 24px; }
  .truck { width: 100%; }
  /* The desktop nudge off the right edge has nothing to be off here. */
  .hero-portrait { margin-right: 0; }
  .hero-portrait img { height: clamp(300px, 62vw, 430px); margin-inline: auto; }
  .people-head { grid-template-columns: 1fr; gap: 28px; }
  /* The principle row stacks, and the dividers turn with it. The horizontal
     padding goes entirely: in a row it separated neighbouring cells, but in a
     column there are no neighbours and it just indented every cell except the
     first, which `:first-child` had already zeroed. Caught on a phone render. */
  .prow { grid-template-columns: 1fr; }
  .pcell { padding-left: 0; padding-right: 0; }
  .pcell + .pcell { border-left: 0; border-top: 1px solid var(--line); }
  .dark .pcell + .pcell { border-top-color: rgba(242,237,248,.22); }
}

/* The phone's start offset. It needs BOTH conditions, which is why it cannot
   live in either block above: inside the `max-width` block it would survive
   reduced motion and park the truck off-screen for good, and inside the
   `no-preference` block it would apply at every width. Later in the file than
   the desktop rule, so it wins the tie at equal specificity.
   A full width plus 14vw, because at 512 the desktop's 72% leaves the cab still
   on screen at the start. */
@media (prefers-reduced-motion: no-preference) and (max-width: 900px) {
  .js .truck { transform: translateX(calc(100% + 14vw)); }
}

/* ------------------------------------------------- certification hero art --- */
/* His render, in 2026-08-16: the certificate stack, wax seals in the packaging
   violet, raisins, on cream. It ends `.hero-plain`'s run as the only hero with
   nothing in the band - the class stays on the section because everything else
   it sets (measure, spacing) is still what this hero wants.
   ⚠ NOT the `.hero-sial` construction, and not by oversight. That one is
   `cover` on a band-shaped panel, which crops to fit. This artwork is 1.116:1
   and its subject fills the frame top to bottom, so cropping it to a wide band
   would take the top off the stack. `auto 100%` fits it by HEIGHT and right-
   anchors it: the whole render is always visible, uncropped, at every width,
   and the panel's width follows from the band's height. */
.hero-cert-art { display: none; }

@media (min-width: 1101px) {
  /* ⚠ 1101px, not the site's usual 901px. Below about 1100 the copy column left
     over is too narrow for a 36-character headline at this size and it breaks
     into four lines. Under that the page reverts to the plain hero, which was a
     finished design in its own right and still is. */
  .hero-cert {
    /* Width first, everything else derived from it. --certw IS the artwork's
       rendered width; the 620px ceiling stops the render swelling to fill a
       2560px screen, where it would dwarf the headline it sits beside. */
    /* ⚠ 540px, and the ceiling is set by the HEADLINE, not by the picture.
       `.hero-cert h1` is capped at 18ch because 18ch is what breaks "Standards
       that speak / for themselves." at its natural pause - that break was tuned
       against 15ch and 13ch and is the reason the cap exists at all. 18ch of
       Bricolage at 66px is ~612px, so the copy column has to be able to reach
       612 or the headline silently drops to three lines and the tuning is lost.
       Working backwards through --copyw at 1440 and above, that puts the
       artwork's ceiling at 540. It was 620 first and cost exactly that. */
    --certw: min(46vw, 540px);
    /* The band has to hold the whole render or `.hero`'s `overflow: hidden`
       clips it. 1.116 is the FILE's aspect (1000x896) - re-derive it if the
       render is ever re-cropped. Only ever ADDS height. */
    min-height: calc(var(--certw) / 1.116);
    display: flex; align-items: center;
    /* Copy column: the render's left edge, minus the wrap's content left edge
       (`--gut` until the wrap centres at --max, `50vw - 600px` after), minus
       48px of clearance. Identical construction to the home hero's --copyw. */
    /* Distance from the viewport's right edge to `.wrap`'s content right edge:
       `--gut` until the wrap centres at --max, `50vw - 600px` after. */
    --certoff: max(var(--gut), 50vw - 600px);
    --copyw: calc(100vw - (2 * var(--certoff)) - var(--certw) - 48px);
  }
  .hero-cert > .wrap { width: 100%; }
  /* An IMG, not a background, and this is the load-bearing detail. A mask's
     percentages resolve against the ELEMENT, so a background-image right-
     anchored inside a wider div would put the ramp on empty space and leave the
     artwork's own left edge at full opacity - a hard edge, the exact fault this
     ramp exists to prevent. An img's box IS the artwork, so the ramp lands on
     it at every width. */
  .hero-cert-art {
    display: block;
    /* ⚠ Aligned to `.wrap`'s right edge, NOT bled to the viewport's - the one
       place this hero deliberately breaks the site's panel convention. Home,
       Products and Contacts all run their art to the viewport edge because it
       is a full-height PANEL there, a field the copy sits beside. This is an
       object on paper with its own soft edges, and an object belongs on the
       grid. Bled to the edge at 1920 it also opened a dead zone between the
       centred copy and the far-right render; on the grid the gap is a constant
       ~530px of paper at every width. */
    position: absolute; right: var(--certoff); top: 50%; transform: translateY(-50%);
    width: var(--certw); height: auto;
    pointer-events: none; user-select: none;
    /* Short ramp, and it lands on nothing. The render's ground is #ede9e0
       against the page's #edece6 - three points of green, six of blue - so this
       is not a photograph laid on the page, it reads AS the page. The ramp only
       dissolves that difference, and the file keeps ~30% of empty cream on its
       left for it to happen in. No ink is anywhere near it. */
    -webkit-mask-image: linear-gradient(to right,
      rgba(0,0,0,0) 0%, rgba(0,0,0,.5) 9%, rgba(0,0,0,1) 18%);
    mask-image: linear-gradient(to right,
      rgba(0,0,0,0) 0%, rgba(0,0,0,.5) 9%, rgba(0,0,0,1) 18%);
  }
  .hero-cert h1   { max-width: min(18ch, var(--copyw)); }
  .hero-cert .lead { max-width: min(52ch, var(--copyw)); }
}

/* ------------------------------------------------------------- register --- */
/* The violet rule opening the register is the same move as `.rangespec` on the
   Products page: a 2px violet edge is this site's way of saying "specification
   follows". Rows after the first are separated in hairline, not violet - four
   violet rules in a column would read as four separate sections. */
.creg { border-top: 2px solid var(--violet); }
/* The reference column is 360px because of ONE string. `UZ-BIO-149.860-0000070`
   was published truncated until 2026-08-16; the full document number from the
   certificate is `UZ-BIO-149.860-0000070.2025.001`, 31 characters, which sets
   ~348px at the 19px cap. At the old 300px it wrapped - and CSS breaks it at a
   hyphen, so it split as "UZ-BIO-149.860-" / "0000070.2025.001", cutting the
   identifier at a meaningless point and leaving one row taller than the rest.
   A reference number a buyer transcribes must never break. If a longer number
   ever arrives, widen this again rather than letting it wrap. */
.creg-row {
  display: grid;
  grid-template-columns: 180px minmax(0, 1fr) 360px;
  gap: 40px;
  align-items: start;
  padding: 34px 0 38px;
}
.creg-row + .creg-row { border-top: 1px solid var(--line); }

/* Fixed plate, exactly as on the home page's dark cards and for the same
   reason: these marks run from 1.1:1 (EU Organic) to 5.4:1 (FSSC), so the box
   is what holds the column edge straight, and object-fit inside it means the
   worst case is letterboxing rather than a stretched third-party logo.
   On a white plate, not knocked out - the official colours are part of what
   makes a certification mark recognisable, and paper is not white enough to
   sit them on directly. */
.creg-mark {
  display: flex; align-items: center; justify-content: center;
  width: 168px; height: 84px;
  background: #fff; border: 1px solid var(--line); border-radius: var(--r);
}
.creg-mark img { height: var(--mark-h, 32px); width: auto; max-width: 128px; object-fit: contain; }

.creg-body h3 { margin-bottom: 12px; }
.creg-issuer { font-size: 13.5px; line-height: 1.5; color: var(--dim); }
.creg-covers { font-size: 15px; line-height: 1.6; margin-top: 14px; max-width: 46ch; }

.creg-side .label { display: block; margin-bottom: 10px; }
/* The page's one typographic event. Mono is otherwise an 11px labelling face
   here; at 19px it reads as a serial number, which is what it is. `break-word`
   rather than `break-all` so "UZ-BIO-149.860-0000070" breaks at a hyphen and
   stays readable if the column narrows. */
.creg-ref {
  font-family: var(--mono); font-weight: 500;
  font-size: clamp(16px, 1.45vw, 19px); line-height: 1.35; letter-spacing: -.01em;
  color: var(--violet); overflow-wrap: break-word;
}
.creg-btn { margin-top: 22px; }
/* Sedex has no button - the file behind it is the full audit report and is not
   published. This note stands in its place at the same optical weight as the
   surrounding body copy, not as an apology in small print. */
.creg-note { font-size: 13.5px; line-height: 1.55; color: var(--dim); margin-top: 18px; max-width: 34ch; }

/* The verification section's styles (`.section-tint`, `.vsteps`, `.vstep-n`)
   lived here and were removed with the section on 2026-08-16, his call. Nothing
   else used them. */

/* ------------------------------------------------------------------ ask --- */
.creg-ask { display: grid; grid-template-columns: 1.08fr .92fr; gap: 56px; align-items: end; }
.creg-ask-actions { display: flex; gap: 12px; flex-wrap: wrap; }

@media (max-width: 1080px) {
  /* The reference column is the first to suffer: at this width 300px forces
     "UZ-BIO-149.860-0000070" to break, so the mark and the number share a row
     above the copy instead. */
  .creg-row { grid-template-columns: 180px minmax(0, 1fr); gap: 28px 40px; }
  .creg-side { grid-column: 2; }
  .creg-body { grid-column: 2; }
  .creg-mark { grid-row: span 2; }
}
@media (max-width: 900px) {
  .creg-row { grid-template-columns: 1fr; gap: 22px; padding: 28px 0 32px; }
  .creg-mark, .creg-body, .creg-side { grid-column: 1; }
  .creg-mark { grid-row: auto; }
  .creg-ask { grid-template-columns: 1fr; gap: 32px; align-items: start; }
  .hero-cert h1 { max-width: none; }
}

/* ==========================================================================
   Production page (`site/production/index.html`)
   Built 2026-08-17. His brief: the floema.com concept, in our palette and
   type - "how steps are shown with smooth photo scroll transitions with line
   of process indicating the steps".
   Appended at the END of the file, like every page block before it, so
   nothing already written can outrank these at equal specificity.
   ========================================================================== */

/* ⚠ TWO COLUMNS, PHOTOGRAPH LEFT. Every other page on this site puts artwork
   on the right; this one is the exception on purpose. The process line runs
   down the left edge of the steps, so photograph-left puts the line BETWEEN
   the picture and the text it belongs to, and the eye reads photo, line, step
   as one row. Artwork-right would leave the line orphaned on the far side.
   `align-items: start` is required, not cosmetic: a stretched grid item cannot
   be sticky, because it is already as tall as the row. */
/* ⚠ The section carries its own vertical rhythm. `.stages` is not `.section`
   (it needs its own block to hang the sticky machinery off), so it does not
   inherit `padding: var(--sec) 0` - and without this the "Production process"
   label sat 11px under the figure strip's bottom rule. */
.stages { padding: var(--sec) 0; }

.stagegrid {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr);
  gap: clamp(36px, 5vw, 88px); align-items: start;
  margin-top: clamp(40px, 5vw, 64px);
}

/* ------------------------------------------------------- sticky panel --- */
/* The offset is the COMPACT masthead plus air. Any sticky element on this site
   has to clear the fixed bar, and by the time anything is stuck the bar is in
   its compact state by definition - you cannot be scrolled and at rest. */
.stage-panel { position: sticky; top: calc(var(--mast-h-compact) + 34px); }
/* ⚠ THE STACK NEEDS ITS OWN HEIGHT. Its children are absolutely positioned, so
   it has no content height of its own and would collapse to zero, taking the
   sticky panel with it. `aspect-ratio` gives it one from its width, and 3/2 is
   the shape all eight sources already are - see the build script. */
.stage-stack {
  position: relative; aspect-ratio: 3 / 2;
  border-radius: var(--r); overflow: hidden; background: var(--paper-hi);
}
.stage-shot { margin: 0; }
.stage-shot img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* ⚠ THE STACKING IS SCOPED TO `.js`, AND THAT IS THE WHOLE FALLBACK. Nothing
   ever adds `.is-active` without scripting, so an unscoped rule would leave
   eight photographs at `opacity: 0` and the page would have no photography at
   all. Without `.js` they stay in normal flow: a plain column of eight
   photographs beside a plain list of eight steps, which is a complete page. */
.js .stage-stack > .stage-shot { position: absolute; inset: 0; opacity: 0; }
.js .stage-shot.is-active { opacity: 1; }

/* The cross-dissolve. Opacity carries the change; the scale is what stops it
   reading as two flat pictures swapping - the arriving frame settles in rather
   than appearing. Both are compositor properties, so eight stacked photographs
   cost nothing to animate. */
@media (prefers-reduced-motion: no-preference) {
  .js .stage-shot {
    transform: scale(1.035);
    transition: opacity 620ms var(--ease-out), transform 1100ms var(--ease-out);
  }
  .js .stage-shot.is-active { transform: none; }
}

/* -------------------------------------------------------- the process --- */
/* ⚠ THE LINE IS TWO ELEMENTS, NOT ONE. `::before` is the full-length track in
   `--line`; `::after` is the violet fill that grows. Drawing one line and
   changing its colour cannot work, because the fill has to be able to sit at
   a fraction of the track's height. The fill is `scaleY` off a top origin, so
   it animates on the compositor and never touches layout. */
.stage-steps {
  list-style: none; margin: 0; padding: 0; position: relative;
  --stage-rail: 15px;   /* centre of the rail, from the column's left edge */
  --p: 0;               /* 0 to 1, set by site.js */
}
.stage-steps::before,
.stage-steps::after {
  content: ""; position: absolute; left: var(--stage-rail); top: 6px; bottom: 6px;
  width: 2px; margin-left: -1px; border-radius: 1px;
}
.stage-steps::before { background: var(--line); }
.stage-steps::after {
  background: var(--violet); transform-origin: top;
  transform: scaleY(var(--p));
  transition: transform 620ms var(--ease-out);
}

.stage { position: relative; padding-left: calc(var(--stage-rail) + 30px); }
.stage + .stage { margin-top: clamp(52px, 6vw, 86px); }

/* The number sits ON the line, which is why it needs a paper plate: the track
   runs behind it and would otherwise show through the counter of the 0. */
.stage-num {
  position: absolute; left: 0; top: 2px;
  display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--paper); border: 2px solid var(--line);
  font-family: var(--mono); font-size: 11px; letter-spacing: .04em; color: var(--dim);
  transition: background-color 320ms var(--ease-out), border-color 320ms var(--ease-out),
              color 320ms var(--ease-out);
}
.stage.is-done .stage-num { border-color: var(--violet); color: var(--violet); }
.stage.is-active .stage-num { background: var(--violet); border-color: var(--violet); color: #fff; }

/* ⚠ THE STEPS DIM, THEY DO NOT HIDE. An inactive step at very low opacity is
   the obvious way to focus the active one and it is wrong here: this is a
   reference page, a buyer scans it, and text that has to be scrolled into
   focus before it can be read is text they cannot skim. .55 is legible on its
   own and still clearly behind the live step. */
.stage-body { transition: opacity 380ms var(--ease-out); }
.js .stage-body { opacity: .55; }
.js .stage.is-active .stage-body,
.js .stage.is-done .stage-body { opacity: 1; }

.stage-when {
  font-family: var(--mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--violet); margin-bottom: 10px;
}
.stage h3 { font-size: clamp(20px, 1.9vw, 26px); letter-spacing: -.02em; }
.stage-body > p { margin-top: 12px; color: var(--dim); line-height: 1.62; max-width: 46ch; }
.stage .ticks { margin-top: 18px; }

@media (max-width: 900px) {
  /* ⚠ THE PANEL STAYS STICKY, IT JUST MOVES TO THE TOP. Dropping the sticky
     behaviour here would put eight photographs in a column before the first
     step, which is the one layout the whole component exists to avoid. Stuck
     under the masthead, the photograph stays with the step being read.
     It needs its own paper ground and a little air below it, or the steps
     scroll visibly under a transparent panel. */
  .stagegrid { grid-template-columns: 1fr; gap: 0; }
  .stage-panel {
    top: var(--mast-h-compact); z-index: 2;
    background: var(--paper); padding: 10px 0 16px;
    margin-bottom: 8px;
  }
  .stage-steps { --stage-rail: 13px; }
  .stage { padding-left: calc(var(--stage-rail) + 24px); }
  .stage-body > p { max-width: none; }
}

/* --------------------------------------- production: strips and grids --- */
/* Four-cell variants of two shared components. Both parents are hard-coded to
   the About page's counts (five counters, three principles), and a short count
   in a longer grid leaves a hole at the end of the row rather than sharing the
   width out. */
.figstrip-4 .wrap { grid-template-columns: repeat(4, 1fr); }
.prow-4 { grid-template-columns: repeat(4, 1fr); }

/* The supplier line above each equipment name. Mono, like every other spec on
   this site, and it sits ABOVE the heading because the machine is the subject
   and the maker is the qualifier - the live page ran them together as
   "Laser Sorter - Tomra, Belgium", which buries the machine. */
.tile-kicker {
  font-family: var(--mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--violet); margin-bottom: 8px;
}
.dark .tile-kicker { color: var(--violet-lt); }

@media (max-width: 1080px) {
  /* Same step the bento takes at this width - four bordered cells at 1080 are
     ~250px each, and "Certification compliance" sets three lines in that. */
  .prow-4 { grid-template-columns: repeat(2, 1fr); }
  /* A cell that starts a row must not keep a left border it no longer sits
     against, and the second row needs the rule that used to be its neighbour's
     left edge. Same fix as the counter strip. */
  .prow-4 .pcell:nth-child(2n + 1) { border-left: 0; }
  .prow-4 .pcell:nth-child(n + 3) { border-top: 1px solid var(--line); }
}
@media (max-width: 900px) {
  .figstrip-4 .wrap { grid-template-columns: repeat(2, 1fr); }
  .prow-4 { grid-template-columns: 1fr; }
  .prow-4 .pcell:nth-child(n + 2) { border-top: 1px solid var(--line); }
}

/* -------------------------------------------------- production: the film --- */
/* His ask, 2026-08-17: the company film in the hero. Copy left, film right,
   the shape every other hero on this site uses.
   ⚠ THE PLAY CONTROL IS AN <a href> TO YOUTUBE, upgraded to an inline player
   by `site.js`. Without scripting it still plays, on YouTube, in a new tab -
   which is why it must not become a <button>. */
/* ⚠ SHARED WITH THE NEWS POSTS (`.hero-post-grid`), added 2026-08-17 on his
   note: *"dont use big photo for whole section, use it in hero section
   instead"*. Same two-column hero, copy left and the picture right, which is the
   shape every other hero on this site already uses. Listed as a second selector
   rather than duplicated so the two can never drift apart, and rather than
   renaming the class, which would touch the Production page for no reason. */
.hero-prod-grid,
.hero-post-grid {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .92fr);
  gap: clamp(32px, 4.5vw, 76px); align-items: center;
}
/* The hero's own photograph carries its caption on the image, so it needs no
   space below it, and `.hero-plain`'s bottom padding is the only gap wanted
   before the first body section. */
.hero-post-grid .media { margin: 0; }
.videoframe {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  border-radius: var(--r); background: var(--ink);
}
.video-poster { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The whole frame is the target, not just the button: a play control that only
   works on a 60px circle is a control that gets missed. */
/* ⚠ CENTRED, AND A FLAT SCRIM RATHER THAN A BOTTOM GRADIENT. The first build
   put the mark and its label bottom-left under a rising gradient, which is the
   usual treatment and was wrong for this poster: the film's own titles are
   burned into that corner ("more than 250 employees"), so the label landed on
   top of them and both became unreadable. The centre is the one region of a
   video frame that convention keeps clear, and an even tint means the control
   survives whatever frame he swaps in later. */
.video-play {
  position: absolute; inset: 0;
  display: grid; place-content: center; justify-items: center; gap: 14px;
  text-decoration: none; color: #fff; text-align: center;
  background: rgba(11, 9, 15, .3);
  transition: background-color 260ms var(--ease-out);
}
@media (hover: hover) {
  .video-play:hover { background: rgba(11, 9, 15, .42); }
}
.video-play-mark {
  flex: none; display: grid; place-items: center;
  width: 52px; height: 52px; border-radius: 50%;
  background: var(--violet); color: #fff;
  transition: transform 260ms var(--ease-out), background-color 260ms var(--ease-out);
}
.video-play-mark svg { width: 22px; height: 22px; margin-left: 2px; }
.video-play-label {
  font-family: var(--display); font-weight: 800; font-size: 17px; letter-spacing: -.02em;
  /* A shadow, not a plate: the label sits centred over a photograph and a plate
     behind two words reads as a second button. */
  text-shadow: 0 1px 12px rgba(11, 9, 15, .55);
}
.video-play-meta {
  display: block; margin-top: 4px;
  font-family: var(--mono); font-weight: 400; font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: rgba(255,255,255,.72);
}
@media (hover: hover) {
  .video-play:hover .video-play-mark { transform: scale(1.08); background: var(--violet-dp); }
}
.video-play:focus-visible { outline: 2px solid var(--chalk); outline-offset: -6px; }
/* Once `site.js` swaps the player in, the poster and the link are gone and the
   iframe fills the frame. */
.videoframe iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* ------------------------------- production: corrections, 2026-08-17 ------ */
/* All four are his, from one review. */

/* 1. "align process photos to the center when it gets sticky". The panel was
   sticky at a fixed offset below the masthead, so the photograph sat high and
   the eye had to travel to it. It is now a full-viewport-height sticky box with
   the photograph centred inside, which is the floema behaviour he asked for.
   ⚠ `height` here, not `min-height`: the box must not grow with its content or
   it stops being a viewport-sized frame. */
.stage-panel {
  top: var(--mast-h-compact);
  height: calc(100vh - var(--mast-h-compact));
  display: grid; align-content: center;
}

/* 2. "line that shows the process doesnt move properly with the scroll".
   It was filled in eight discrete jumps, one per step, with a 620ms transition
   doing the smoothing - so it moved AFTER the scroll rather than with it.
   `site.js` now writes `--p` continuously from cached geometry, and the
   transition has to go: a transition on a value that updates every frame is a
   permanent lag behind the pointer. The smoothing is Lenis's job. */
.stage-steps::after { transition: none; }

/* 3. "for key elements you used squear for each step, try something better,
   and they get color when step is in active."
   A check, drawn from two borders rather than a glyph so it stays crisp at any
   size and needs no SVG per bullet. It is grey until the step is live and takes
   violet when it is, so the marks read as a checklist being worked through.
   ⚠ Scoped to `.stage` - the About page's `.ticks` keep their squares. */
.stage .ticks li { padding-left: 26px; }
.stage .ticks li::before {
  width: 11px; height: 6px; top: .38em; border-radius: 0;
  background: none;
  border-left: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: color 380ms var(--ease-out);
  /* ⚠ Violet is the BASE state, grey is the scripted one. Without JavaScript no
     step is ever "active", so a grey base would leave every mark on the page
     permanently dead. Reversing these two lines breaks the no-JS page and
     nothing else, which is exactly the kind of thing that ships unnoticed. */
  color: var(--violet);
}
.js .stage .ticks li::before { color: var(--line); }
.js .stage.is-active .ticks li::before,
.js .stage.is-done .ticks li::before { color: var(--violet); }

/* 4. "last two section lacks image, icons for cars, they seem tooo dry".
   Icons are the site's existing `.f-icon`. The hover treatment was written for
   `.bento-4` and `.prow` on the About page; the equipment grid is a 3-column
   `.bento`, so it was getting the icons without the response. */
.bento .tile { transition: background-color 240ms var(--ease-out), border-color 240ms var(--ease-out); }
@media (hover: hover) {
  .dark .bento .tile:hover { background: rgba(242,237,248,.12); border-color: rgba(242,237,248,.3); }
  .bento .tile:hover .f-icon { transform: translateX(2px); }
}

@media (max-width: 900px) {
  .hero-prod-grid,
  .hero-post-grid { grid-template-columns: 1fr; gap: 30px; }
  /* The film comes second on a phone: the headline is what the page is for. */
  .videoframe { order: 1; }
  /* Same argument for a news post: the headline and the standfirst say what
     happened, the photograph confirms it. Text first on a narrow screen. */
  .hero-post-grid .media { order: 1; }
  /* A viewport-height sticky panel on a phone leaves no room to read the step
     beside it. Back to a band at the top, the height the photograph needs. */
  .stage-panel { height: auto; display: block; }
}

/* ==================================== home: the export globe (WebGL) ======
   ⚠ REPLACED 2026-08-18, HIS CALL: *"in this folder i put globe-scroll for
   home page, replace it with the video one!"* The scroll-scrubbed VIDEO that
   used to live here is gone - poster, `.globe-card`, `--globe-p`, the whole
   `globeScrub` module - and a real WebGL planet stands in its place. His file,
   `raw/inbox/globe-scroll.html`. See the header of `assets/js/globe.js` for
   everything that changed porting it in.

   ⚠ THE SECTION IS THE TRACK AND THE PIN IS STICKY INSIDE IT. 640vh of section,
   one viewport of sticky stage. Same construction as the video version, so the
   same thing is still true: NOTHING LOCKS THE SCROLL, no wheel or touch is
   captured, and a visitor who flicks past sees the arcs fly by. That is a
   legitimate outcome, not a bug to fix by taking their input away.

   ⚠ NO `padding-top` FOR THE MASTHEAD, UNLIKE THE VIDEO VERSION. That one
   needed it because the card was a 16:9 frame with a top edge to protect. This
   is a full-bleed scene: the planet is meant to run under the bar, and by the
   time this section is on screen the masthead is in its compact violet state,
   which sits over space perfectly well. The headline is anchored at ~40% of the
   frame and the cue at the bottom, so neither goes near it.

   ⚠ THE PALETTE HERE IS THE PLANET'S, NOT THE BRAND'S. Arcs, limb and city
   lights are pale blue and warm white against a blue marble, set in the shaders
   in globe.js. Do not "correct" them to packaging violet - the violet would
   fight the texture, and this is the one section on the site that is a
   photograph of the world rather than a piece of brand furniture.

   ⚠⚠ THE TYPE IS HIS, UNCHANGED, AND THAT IS DELIBERATE FOR NOW.
   It shipped first with the site's own fonts - Bricolage 800, sentence case,
   full stop - on the argument that every other headline on the site is set
   that way. He asked for it back: *"first place it as it is, then may be we
   can adapt the font"* (2026-08-18). So the headline, the region names and the
   country lists are on his own stack, at his sizes, and the headline is
   uppercase with no full stop, exactly as `raw/inbox/globe-scroll.html` has it.
   **ADAPTING THIS TO THE SITE'S FONTS IS THE NEXT ROUND, ON HIS WORD, NOT A
   TIDY-UP TO DO IN PASSING.** The Bricolage version is one edit away: swap
   `--g-font` for `var(--display)` and drop `text-transform` on
   `.globe3d-title`. Only the `Scroll` cue is on the site's mono, because his
   own stack has no monospace and that label is a spec label everywhere else.
   ⚠ His note on WHY the stack is explicit rather than `system-ui`, kept: on
   Windows the system font is Segoe UI, whose R and K are drawn differently, and
   the headline stops matching the reference. Helvetica/Arial match across
   platforms. */
.globe3d {
  position: relative;
  height: 640vh;
  background: #02040a;
  color: #f2f5f9;
  --g-accent: #9ed4ff;
  --g-muted: rgba(242,245,249,.45);
  --g-line: rgba(242,245,249,.14);
  /* His stack, verbatim. One variable, so the swap to the site's fonts when he
     asks for it is a single edit rather than five. */
  --g-font: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
  /* "add some gap after video and next section" - his review of the version
     this replaces, and still true: the news section below carries
     `padding-top: 0`, so without this the paper starts on the pixel the space
     ends. A margin, not padding: the gap has to BE paper. */
  margin-bottom: calc(var(--sec) * .75);
}
.globe3d-pin { position: sticky; top: 0; height: 100svh; overflow: hidden; }

/* Layer order is the reference's: the headline sits UNDER the planet, so the
   globe rising through the frame covers the bottom of the line. */
.globe3d-layer { position: absolute; inset: 0; }
.globe3d-back { z-index: 1; pointer-events: none; }
.globe3d-stage { z-index: 2; opacity: 0; transition: opacity 1s var(--ease-out); }
.globe3d-stage[data-ready="1"] { opacity: 1; }
.globe3d-stage canvas { display: block; width: 100%; height: 100%; }
.globe3d-front { z-index: 3; pointer-events: none; }

/* Anchored by its BOTTOM edge, not its top. The top of the planet on slide one
   sits at a fixed fraction of the frame height whatever the window's shape, so
   anchoring from the bottom gives the same tuck-behind-the-horizon on a desktop
   and on a phone; anchored from the top, a tall headline in a short window slid
   under the planet entirely. `--planet-top` is computed in globe.js from the
   camera geometry and set on the section. The tuck itself is the negative
   margin on the title, and it is in `em` rather than `vh` on purpose: as a
   percentage of the window it would eat a whole line on a phone, where the type
   is a quarter of the size. (His note, kept.) */
.globe3d-hero {
  position: absolute; left: 0; right: 0;
  bottom: calc(100% - var(--planet-top, 40.5%));
  display: grid; justify-items: center; text-align: center;
  padding: 0 clamp(24px, 6vw, 88px);
  will-change: opacity, transform, filter;
}
/* His values, verbatim. The negative margin takes the last line about 13% of a
   cap height behind the horizon - "a little", and the same on any screen. ⚠ It
   is in `em`, not `vh`: as a percentage of the window it would eat a whole line
   on a phone, where the type is a quarter of the size. (His note.) */
.globe3d-title {
  margin: 0 0 -.34em;
  font-family: var(--g-font); font-weight: 500;
  font-size: clamp(30px, 7.6vw, 132px); line-height: 1.03;
  letter-spacing: -.018em; text-transform: uppercase;
  color: #f2f5f9;
}

/* City labels: DOM elements projected onto the arrival markers each frame. */
.globe3d-geo { position: absolute; inset: 0; overflow: hidden; }
.globe3d-geolabel {
  position: absolute; left: 0; top: 0; white-space: nowrap;
  font-family: var(--g-font); font-size: 12px; font-weight: 500;
  letter-spacing: .09em;
  color: rgba(242,245,249,.88);
  text-shadow: 0 1px 10px rgba(2,4,10,.95), 0 0 3px rgba(2,4,10,.9);
  opacity: 0; transition: opacity .45s var(--ease-out);
  will-change: transform, opacity;
}
.globe3d-geolabel-origin { color: var(--g-accent); letter-spacing: .12em; text-transform: uppercase; font-size: 11px; }

/* The region panels. Two columns so the copy never lands on the planet, which
   globe.js pushes to the right of the frame once the camera has pulled back. */
.globe3d-hud {
  position: absolute; inset: 0; display: grid;
  grid-template-columns: 1fr 1fr; align-items: center;
  padding: 0 clamp(24px, 6vw, 88px);
}
.globe3d-col { grid-column: 1; max-width: min(34vw, 520px); }
/* Height is fixed to the tallest group - four countries in a column - so the
   panel does not resize as the regions swap. Content is bottom-aligned, so a
   long region name grows upward and the country list stays on one baseline. */
.globe3d-slides { position: relative; height: clamp(160px, 27.5vh, 255px); }
.globe3d-slide {
  position: absolute; inset: 0; opacity: 0;
  display: flex; flex-direction: column; justify-content: flex-end;
  will-change: opacity, transform;
}
.globe3d-slide h3 {
  margin: 0; font-family: var(--g-font); font-weight: 600;
  font-size: clamp(40px, 5.4vw, 76px); line-height: 1;
  letter-spacing: -.04em; color: #f2f5f9;
}
/* The countries are the content of the panel, not a caption to it: they sit at
   roughly 40% of the region name and almost at full strength. (His note.) */
.globe3d-list {
  margin: 18px 0 0; padding: 0; list-style: none;
  font-family: var(--g-font);
  font-size: clamp(19px, 2.1vw, 31px); line-height: 1.3;
  letter-spacing: -.015em; font-weight: 500; color: rgba(242,245,249,.86);
}

.globe3d-rail {
  position: absolute; right: clamp(20px, 4vw, 52px); top: 50%;
  transform: translateY(-50%); display: grid; gap: 10px; will-change: opacity;
}
.globe3d-tick {
  width: 1px; height: 28px; background: var(--g-line);
  position: relative; overflow: hidden; justify-self: end;
}
.globe3d-tick i {
  position: absolute; inset: 0; background: var(--g-accent);
  transform: scaleY(0); transform-origin: top; will-change: transform;
}

.globe3d-cue {
  position: absolute; left: 50%; bottom: max(28px, env(safe-area-inset-bottom));
  transform: translateX(-50%); margin: 0;
  font-family: var(--mono); font-size: 11px; letter-spacing: .22em;
  text-transform: uppercase; color: var(--g-muted);
  transition: opacity .6s var(--ease-out);
}
.globe3d-cue[data-hidden="1"] { opacity: 0; }

.globe3d-fallback {
  position: absolute; inset: 0; display: grid; place-items: center;
  color: var(--g-muted); font-size: 14px; text-align: center; padding: 24px;
}

@media (max-width: 900px) {
  .globe3d { height: 540vh; }
  .globe3d-hud { grid-template-columns: 1fr; align-items: end; padding-bottom: 16vh; }
  .globe3d-col { max-width: none; }
  .globe3d-slides { height: 168px; }
  .globe3d-rail { display: none; }
}

/* ⚠ THREE WAYS THIS SECTION HAS TO WORK WITHOUT THE PLANET, and they collapse
   to one layout, so they share one selector list. Add a fourth cause here
   rather than writing the collapse out again.
     1. NO JS. The module is never imported.
     2. REDUCED MOTION. `site.js` deliberately does not import it either -
        scrubbing a 3D camera with the scrollbar is precisely the motion this
        setting asks us not to do, and there is no gentler variant of it. Same
        call already made for Lenis at the top of this file, and for the video
        that used to be in this section.
     3. THE MODULE FAILED TO LOAD, which sets `data-globe-failed` on the
        section from the loader's catch. ⚠⚠ THIS IS NOT HYPOTHETICAL AND IT IS
        NOT A BUG IN THE PORT: an EXTERNAL ES MODULE CANNOT BE FETCHED OVER
        `file://` - Chrome blocks it as a cross-origin request from a null
        origin - so opening index.html by double-clicking it will ALWAYS land
        here. It works over http and https, which is what Hostinger serves.
        Preview it with `node tools/serve.js` and open the localhost URL; see
        the note at the top of that file. His original worked from the
        filesystem only because its module was INLINE in the HTML.

   In all three the section stops being 640vh of pinned scene and becomes an
   ordinary dark band: the headline, then both markets as plain lists. That is
   why the region panels are real markup - see the note in globe.js. */
html:not(.js) .globe3d,
html:not(.js) .globe3d-pin,
.globe3d[data-globe-failed],
.globe3d[data-globe-failed] .globe3d-pin { height: auto; }
html:not(.js) .globe3d-pin,
.globe3d[data-globe-failed] .globe3d-pin {
  position: relative; overflow: visible; padding: var(--sec) 0;
}
html:not(.js) .globe3d-layer,
.globe3d[data-globe-failed] .globe3d-layer { position: relative; inset: auto; }
html:not(.js) .globe3d-stage,
html:not(.js) .globe3d-rail,
html:not(.js) .globe3d-cue,
.globe3d[data-globe-failed] .globe3d-stage,
.globe3d[data-globe-failed] .globe3d-rail,
.globe3d[data-globe-failed] .globe3d-cue { display: none; }
html:not(.js) .globe3d-hero,
.globe3d[data-globe-failed] .globe3d-hero {
  position: relative; bottom: auto; padding-bottom: clamp(40px, 5vw, 72px);
}
html:not(.js) .globe3d-title,
.globe3d[data-globe-failed] .globe3d-title {
  margin-bottom: 0; font-size: clamp(30px, 5vw, 62px);
}
html:not(.js) .globe3d-hud,
.globe3d[data-globe-failed] .globe3d-hud {
  position: relative; inset: auto; grid-template-columns: 1fr 1fr;
  gap: 40px; align-items: start;
}
html:not(.js) .globe3d-col,
.globe3d[data-globe-failed] .globe3d-col { grid-column: auto; max-width: none; }
html:not(.js) .globe3d-slides,
.globe3d[data-globe-failed] .globe3d-slides { height: auto; }
html:not(.js) .globe3d-slide,
.globe3d[data-globe-failed] .globe3d-slide { position: relative; inset: auto; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .globe3d, .globe3d-pin { height: auto; }
  .globe3d-pin { position: relative; overflow: visible; padding: var(--sec) 0; }
  .globe3d-layer { position: relative; inset: auto; }
  .globe3d-stage, .globe3d-rail, .globe3d-cue { display: none; }
  .globe3d-hero { position: relative; bottom: auto; padding-bottom: clamp(40px, 5vw, 72px); }
  .globe3d-title { margin-bottom: 0; font-size: clamp(30px, 5vw, 62px); }
  .globe3d-hud {
    position: relative; inset: auto; grid-template-columns: 1fr 1fr;
    gap: 40px; align-items: start;
  }
  .globe3d-col { grid-column: auto; max-width: none; }
  .globe3d-slides { height: auto; }
  .globe3d-slide { position: relative; inset: auto; opacity: 1; }
}
@media (max-width: 700px) {
  html:not(.js) .globe3d-hud,
  .globe3d[data-globe-failed] .globe3d-hud { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) and (max-width: 700px) {
  .globe3d-hud { grid-template-columns: 1fr; }
}

/* ⚠ THE BAND ABOVE THIS ONE IS A GRADIENT INTO IT, AND THE TWO COLOURS MUST
   STAY IN STEP. His ask, 2026-08-18: *"make shipping section, the one before
   the globe dark, like black so globe effect transitions smoothly."*
   `.dark` alone would not have done it - `.dark` is deep violet, and the
   certifications band directly ABOVE the logistics one is already `.dark`, so
   flat violet would have given two identical violet bands and then a hard black
   edge at the globe. `.dark-dusk` instead runs the logistics band from
   `--violet-dp` at its top - the exact colour the certificates end on - down to
   `#02040a` at its bottom, which is the exact colour the globe starts on. The
   result is one continuous darkening from the certificates into space with no
   seam anywhere in it.
   ⚠ IF THE GLOBE'S GROUND EVER CHANGES, CHANGE THE BOTTOM STOP WITH IT. They
   are the same colour written twice and nothing enforces that.
   The stop at 62% is not decoration: a straight two-stop ramp puts mid-grey
   violet right behind the bento, where the tiles' translucent plates sit, and
   the tiles read as muddy. Holding violet through the top two thirds keeps the
   tiles on the ground they were designed for and spends the last third going to
   black under nothing but the section's bottom padding. */
.dark-dusk {
  background: linear-gradient(to bottom,
    var(--violet-dp) 0%,
    #2a1049 38%,
    #170a2b 62%,
    #080510 82%,
    #02040a 100%);
  background-color: #02040a;
}

/* ======================================== news posts: the packed layout ===
   His review, 2026-08-17: *"dont use big photos that cover whole section,
   instead use it for hero section, and again dont make it look simple in simple
   frame ... also use efficient layout so page doesnt end like current one, it
   has to be like perfect tetris you know"*.

   Two faults he was pointing at, both real:
     1. THE HERO WAS A PICTURE IN A BOX. Copy in one half, a plain rounded
        rectangle in the other. Nothing held them together.
     2. HALF THE PAGE WAS EMPTY. `.section-head` caps at 60ch and the closing
        band carried one column of copy, so the last two blocks of the page were
        text on the left and nothing at all on the right, twice in a row.

   The fix is one idea applied three times: a TWELVE-COLUMN GRID where every
   block spans a whole number of columns and the leftover space is given a job
   instead of being left over. The hero's facts tuck under its copy so the
   photograph beside them can be full height; the body's aside carries the second
   photograph AND the pull-quote; the closing band carries artwork opposite its
   copy. No block ends in a half-empty row.

   ⚠ THE COLUMN SPANS ARE THE COMPONENT. If a future post has no second
   photograph or no artwork, do not leave the cell empty - change the spans so
   the remaining blocks fill the row. An empty cell is the bug this replaced. */

/* -- the hero ------------------------------------------------------------- */
/* Rows: the copy sits in row 1 and the facts in row 2, and the photograph spans
   BOTH, so its height is defined by the type beside it rather than by its own
   crop. That is what makes the block read as one object instead of two. */
.posthero {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-template-rows: auto auto;
  gap: clamp(16px, 1.6vw, 24px);
  align-items: start;
}
.posthero-copy  { grid-column: 1 / span 5; grid-row: 1; }
.posthero-facts { grid-column: 1 / span 5; grid-row: 2; align-self: end;
                  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
/* ⚠ `align-self: stretch` plus a height on the figure is what makes the
   photograph fill both rows. Without the height the figure is only as tall as
   its own aspect-ratio box and the grid leaves a gap under it. */
.posthero-shot {
  grid-column: 6 / span 7; grid-row: 1 / span 2;
  align-self: stretch; height: 100%; margin: 0;
  position: relative; overflow: hidden; border-radius: var(--r);
}
.posthero-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* -- the portrait variant, added 2026-08-18 for SIAL Shanghai --------------
   ⚠ SOME POSTS HAVE NO LANDSCAPE FRAME. Both of his Shanghai pictures are 3:4.
   The default hero box is defined by the type beside it, so it comes out
   roughly square-to-landscape - and cover-cropping a 3:4 portrait into it
   throws away about a third of the height, which on that photograph is either
   the stand sign at the top or the people at the bottom.
   So: the photograph narrows to four columns and keeps its own 3:4, the copy
   widens to seven, and `align-self: start` stops the figure being stretched.
   The photograph is then the tallest thing in the block, so the rows grow to
   it and `.posthero-facts`'s `align-self: end` lands the fact cells on the
   photograph's bottom edge. That alignment is the point - it is what stops the
   left column reading as a short paragraph with a hole under it. */
.posthero--tall .posthero-copy,
.posthero--tall .posthero-facts { grid-column: 1 / span 7; }
.posthero--tall .posthero-shot {
  grid-column: 9 / span 4;
  align-self: start; height: auto; aspect-ratio: 3 / 4;
}

/* -- the panoramic variant, added 2026-08-18 for SIAL Paris 2024 -----------
   ⚠ THE THIRD HERO SHAPE, AND EACH ONE EXISTS BECAUSE OF A PHOTOGRAPH, not
   for variety's own sake. `--tall` is for posts whose only frames are 3:4;
   this one is for the opposite case. SIAL Paris 2024's stand photograph is
   7648x3456 - 2.21:1, 26 MP, the widest and highest-resolution frame on the
   project. In the default 3:2 hero box a third of its width is cropped away,
   and that width IS the picture: the stand end to end in one frame.
   So: photograph LEFT at its own ratio, copy RIGHT, and - the part that makes
   the block read as a different composition rather than a mirrored one - the
   FACT CELLS RUN THE FULL TWELVE COLUMNS underneath both, four across instead
   of three tucked under a column. */
.posthero--wide .posthero-shot {
  grid-column: 1 / span 7; grid-row: 1;
  align-self: start; height: auto; aspect-ratio: 2.213;
}
.posthero--wide .posthero-copy  { grid-column: 8 / span 5; grid-row: 1; }
.posthero--wide .posthero-facts {
  grid-column: 1 / -1; grid-row: 2;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin-top: clamp(8px, 1vw, 16px);
}

/* -- the type-led variant, added 2026-08-18 for Fi Europe 2025 -------------
   ⚠ THIS HERO HAS NO PHOTOGRAPH, AND THAT IS THE POINT. Fi Europe is the one
   post with no picture of the NNP stand at all - the single frame we have
   shows a national pavilion with no NNP branding in it. Putting it in the hero
   would be a photograph pretending to be evidence.
   What fills the space instead is the specification: Fi Europe is one of only
   two shows in the nine where BOTH a hall and a stand number are on file, so
   the fact cells become the hero's visual event - four of them in a 2x2 panel
   where the photograph would have gone, at display size, with the stand number
   in violet.
   `grid-auto-rows: 1fr` plus `align-self: stretch` is what makes the panel
   reach the full height of the copy beside it; without both, the cells sit at
   their content height and leave a hole under them. The cells lay their own
   type out top-and-bottom so a taller cell does not just gain padding. */
.posthero--type .posthero-copy  { grid-column: 1 / span 7; grid-row: 1; }
.posthero--type .posthero-facts {
  grid-column: 8 / span 5; grid-row: 1;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-self: stretch; grid-auto-rows: 1fr;
}
.posthero--type .factcell {
  display: flex; flex-direction: column; justify-content: space-between;
  padding: clamp(14px, 1.4vw, 20px);
}
.posthero--type .factcell dd { font-size: clamp(17px, 1.7vw, 24px); }

/* -- a COMPOSABLE fact-block modifier, added 2026-08-18 for Gulfood 2025 ----
   The first three hero variants each baked their own fact layout in. This one
   is separate so it can be added to any of them. Gulfood 2025 needs it because
   it has only portrait frames (so it needs `--tall`) AND more verified facts
   than any other post - dates, venue, hall, stand, edition - which the default
   three-cell row cannot hold.
   ⚠ A FULL-WIDTH STRIP WAS TRIED FIRST AND IS WRONG HERE. `--tall` leaves the
   photograph spanning BOTH grid rows, so a strip across all twelve columns in
   row 2 runs underneath it: the fourth cell was half-hidden behind the picture
   and the fifth was gone entirely. Moving the strip to a third row fixes the
   collision but not the cause - a 3:4 photograph beside a short column of copy
   is ~300px taller than the copy, and the fact cells are the only thing that
   was filling that gap.
   So the cells STAY in the copy column and go two across instead of three,
   which makes the block three rows deep and brings the left column back level
   with the bottom of the photograph. The accent cell runs the full width of
   the pair as the last row - which is also what the 560px rule does, so the
   shape is consistent all the way down.
   ⚠ THE ACCENT CELL MUST BE LAST IN THE SOURCE for that to work. */
.posthero--facts-2col .posthero-facts {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.posthero--facts-2col .posthero-facts .factcell-accent { grid-column: 1 / -1; }

/* The fact cells. Small, plain, and they carry the specifics that stop eight
   exhibition posts reading alike - the dates, the hall, the stand number.
   ⚠ ONE OF THEM IS VIOLET, and it should be the stand number: it is the only
   fact on the page a reader could not have guessed, and the accent is what
   stops the row reading as three grey boxes. */
.factcell {
  background: var(--paper-hi); border: 1px solid var(--line); border-radius: var(--r);
  padding: 12px 14px; min-width: 0;
}
.factcell dt {
  font-family: var(--mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--dim); margin: 0 0 6px;
}
.factcell dd {
  font-family: var(--display); font-weight: 800; font-size: clamp(14px, 1.15vw, 17px);
  letter-spacing: -.02em; line-height: 1.15; margin: 0; color: var(--ink);
}
.factcell-accent { background: var(--violet); border-color: var(--violet); }
.factcell-accent dt { color: var(--chalk-dim); }
.factcell-accent dd { color: #fff; }

/* -- the body ------------------------------------------------------------- */
/* Copy left, an aside right that is ALWAYS filled: photograph plus one more
   block. A lone portrait photograph in this column was the other half-empty
   row. */
.postbody {
  display: grid; grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(24px, 3vw, 56px); align-items: start;
}
.postbody-main { grid-column: 1 / span 7; }
.postbody-side { grid-column: 9 / span 4; display: grid; gap: clamp(16px, 1.6vw, 24px); }
/* Mirrored: aside left, copy right. Added 2026-08-18 so a post can turn its
   second body block over rather than run the same shape twice down the page.
   ⚠ SOURCE ORDER IS UNCHANGED - only the columns move - so a screen reader and
   the single-column stack both still get the copy before the aside. */
.postbody--rev .postbody-main { grid-column: 6 / span 7; grid-row: 1; }
.postbody--rev .postbody-side { grid-column: 1 / span 4; grid-row: 1; }
/* ⚠ THE PARAGRAPH GAP, PROMOTED 2026-08-18. `p { margin: 0 }` at the top of
   this file means every gap is explicit, and this had accumulated three
   near-identical copies - `.split .body p + p`, `.section-head .lead + .lead`
   and `.postbody-main .lead + .lead` - each added the day a fourth container
   went without a gap and nobody could see it in the source. The SIAL Shanghai
   hero was the fourth: two leads in `.posthero-copy`, run together.
   The note on the third copy said to promote it at that point, so this is it.
   ONE RULE, ANY CONTAINER. It is deliberately `.lead + .lead` and not `p + p`:
   a block can open with a `.label` paragraph, and that must stay tight to
   whatever follows it. `.split .body p + p` below is left in place because it
   also covers `.prod-sub`, which is not a `.lead`. */
.lead + .lead { margin-top: 18px; }
.h-sub { margin-bottom: 14px; }

/* The pull-quote that fills the aside under the photograph. Chalk on violet,
   the same plate the home page's shelf-life tile uses, so it is a component the
   reader has already met rather than a new object. */
.pullquote {
  background: var(--violet); color: var(--chalk);
  border-radius: var(--r); padding: clamp(20px, 2vw, 28px);
}
.pullquote p {
  font-family: var(--display); font-weight: 800; letter-spacing: -.03em;
  font-size: clamp(17px, 1.5vw, 22px); line-height: 1.25; color: #fff; margin: 0;
}
.pullquote .label { color: var(--chalk-dim); margin-bottom: 12px; }

/* A third block for the aside, added 2026-08-18 on the Anuga post.
   ⚠ THE ASIDE ONLY FILLS ITSELF IF ITS PHOTOGRAPH IS PORTRAIT. Gulfood's is
   3:4 and its two blocks reach the bottom of the copy beside them; Anuga's
   best second frame is 4:3, which is ~140px shorter, and the column ended with
   a quarter of its height empty - the exact fault the three-block layout was
   built to remove.
   So: a quiet paper plate that carries one useful link. It is deliberately NOT
   a second pull-quote - two violet plates in one column would fight, and the
   column already has the loudest thing on the page in it. Reusable by the six
   posts still to come, most of which will have landscape asides too. */
.sidenote {
  background: var(--paper-hi); border: 1px solid var(--line);
  border-radius: var(--r); padding: clamp(20px, 2vw, 28px);
}
.sidenote .label { margin-bottom: 12px; }
.sidenote p { margin: 0 0 18px; font-size: clamp(14px, 1.1vw, 15px); color: var(--dim); }

/* A pull-quote at BAND width rather than in a column. Added 2026-08-18.
   It goes inside `.section.dark`, so the violet ground and the band's own
   padding come free - this rule only sets the type.
   ⚠ IT IS TYPE, NOT A PHOTOGRAPH, and that distinction is the whole reason it
   is allowed to be full width. His ruling on the Gulfood post was "dont use big
   photo for whole section, use it in hero section instead"; a full-bleed
   photographic band is out and stays out. A line of display type across the
   page costs no vertical room a picture would and gives a long post one change
   of register in the middle.
   ⚠ NEVER PUT ONE DIRECTLY ABOVE THE CLOSING BAND. Both are `.section.dark`,
   so adjacent they read as one enormous violet block with a seam in it. Keep a
   paper section between them. */
/* ⚠ NO `max-width` ON THE WRAP. A first build inset the band's content to
   1040px inside the page's 1280px wrap, and the quote then started 120px to the
   right of every other left edge on the page. Read as a misalignment, not as an
   inset. The measure is held on the paragraph instead, so the band shares the
   page's left margin and only the line length is controlled. */
.quoteband p.q {
  font-family: var(--display); font-weight: 800; letter-spacing: -.035em;
  font-size: clamp(26px, 4.2vw, 54px); line-height: 1.06;
  color: #fff; margin: 0; max-width: 900px; text-wrap: balance;
}
.quoteband .label { color: var(--chalk-dim); margin-bottom: clamp(14px, 1.6vw, 22px); }
.quoteband .src {
  font-family: var(--mono); font-size: 12px; color: var(--chalk-dim);
  margin: clamp(16px, 1.8vw, 26px) 0 0;
}

/* -- the closing band ----------------------------------------------------- */
.postnext {
  display: grid; grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(24px, 3vw, 56px); align-items: center;
}
.postnext-copy { grid-column: 1 / span 6; }
.postnext-art  { grid-column: 8 / span 5; margin: 0; border-radius: var(--r); overflow: hidden; }
.postnext-art img { width: 100%; height: auto; display: block; }

@media (max-width: 980px) {
  /* One column, and the photograph goes back to its own aspect ratio - a
     stretched figure in a single-column stack has nothing to match its height
     to, and would collapse to whatever `height: 100%` resolves against. */
  .posthero, .postbody, .postnext { grid-template-columns: 1fr; }
  .posthero-copy, .posthero-facts, .posthero-shot,
  .postbody-main, .postbody-side,
  .postnext-copy, .postnext-art { grid-column: 1 / -1; grid-row: auto; }
  .posthero-shot { height: auto; aspect-ratio: 3 / 2; }
  /* ⚠ THE VARIANT HAS TO REPEAT THE SINGLE-COLUMN RESET, and this is not
     tidiness - it is a specificity bug that was live for one build.
     `.posthero--tall .posthero-shot` is (0,2,0); the reset above it in this
     same block is `.posthero-shot`, (0,1,0). Specificity beats source order and
     beats being inside a media query, so the two-class rule kept its
     `grid-column: 9 / span 4` all the way down to 320px: at 980 the copy was
     squeezed into a 120px column with a five-line headline and the fact cells
     were crushed to 40px each, overlapping the photograph.
     Any future `.posthero--<name>` variant needs its own line here. */
  .posthero--tall .posthero-copy,
  .posthero--tall .posthero-facts,
  .posthero--tall .posthero-shot,
  .posthero--wide .posthero-copy,
  .posthero--wide .posthero-facts,
  .posthero--wide .posthero-shot,
  .posthero--type .posthero-copy,
  .posthero--type .posthero-facts,
  .postbody--rev .postbody-main,
  .postbody--rev .postbody-side { grid-column: 1 / -1; grid-row: auto; }
  /* Four cells across a tablet is ~200px each; three lines of venue name in
     that is a mess. Two up, and the row stays two deep. */
  .posthero--wide .posthero-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The type variant's panel is already 2x2; in the stack it just needs to stop
     stretching to a height it no longer has anything to match. */
  .posthero--type .posthero-facts { align-self: auto; grid-auto-rows: auto; }
  /* The portrait variant keeps its own shape in the single-column stack too -
     3:2 here would crop the same third off that the desktop variant exists to
     protect. But a full-width 3:4 at this breakpoint is 1300px tall, which
     pushes the whole article below one and a half screens before a word of the
     body, so it is capped and centred: a plate, not a wall. Below ~600px the
     cap stops applying on its own and the photograph goes full width, which is
     right there - a phone has no width to give away. */
  .posthero--tall .posthero-shot {
    aspect-ratio: 3 / 4;
    max-width: 560px; justify-self: center;
  }
  .posthero { gap: 20px; }
}
@media (max-width: 560px) {
  /* Three fact cells across a phone leaves ~100px each, which breaks
     "26 to 30 January 2026" onto four lines. Two up, and the accent cell takes
     the full width of the last row. */
  .posthero-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .posthero-facts .factcell-accent { grid-column: 1 / -1; }
  /* ⚠ THE FOUR-CELL VARIANT MUST OPT OUT OF THE FULL-WIDTH ACCENT. With three
     cells, two-up plus a full-width accent is a tidy 2+1. With four it is
     2 + 1 + a stranded single, because the accent is third in the source. Four
     cells simply go 2x2. Both these overrides are two-class selectors on
     purpose - the rules above are one-class and would otherwise win on
     specificity, exactly as the `--tall` variant did at 980px. */
  .posthero--wide .posthero-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .posthero--wide .posthero-facts .factcell-accent { grid-column: auto; }
  /* Same reason: four cells go 2x2 on a phone, so the accent must not take a
     full row and strand the cell before it. */
  .posthero--type .posthero-facts .factcell-accent { grid-column: auto; }
  /* `--facts-2col` needs no override here: it is already two across with the
     accent running the full width as its last row, which is exactly what this
     block does to the default. That is the point of matching the two. */
}

/* ==================================================== news: article meta ===
   The date line under a post's headline. One rule, because everything else on
   the news posts is built from components that already exist - `.hero-plain`,
   `.media`, `.split`, `.tile-list`, `.section-head`, `.dark`. Eight posts are
   coming; anything that needs its own CSS is a decision to be made once, here,
   not per post.
   Mono, because it is a fact rather than prose, and it matches the caliber
   figures and the reference numbers on the certification register. */
.article-meta {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: baseline;
  margin: 18px 0 0;
  font-family: var(--mono); font-size: 12px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--dim);
}
.article-meta span { opacity: .5; }
/* The standfirst sits under the meta line, so it does not need the top margin
   `.hero-plain .lead` would otherwise inherit from a headline directly above. */
.hero-plain .article-meta + .lead { margin-top: 16px; }

/* Folded into the single `.lead + .lead` rule further up, 2026-08-18 - see the
   note there. The `.section-head` copy is gone; nothing rendered differently. */

/* ⚠ A GHOST BUTTON ON A VIOLET BAND WAS INVISIBLE. `.btn-ghost` is ink on
   paper - correct everywhere it was used before, which was never inside `.dark`.
   The news posts close on a dark band with two buttons, so the pairing exists
   now and needs its own values: chalk hairline and chalk text, and the hover
   fill inverts to match `.btn-light` beside it rather than filling with ink. */
.dark .btn-ghost { border-color: rgba(242, 237, 248, .42); color: var(--chalk); }
.dark .btn-ghost::before { background: var(--chalk); }
.dark .btn-ghost:hover,
.dark .btn-ghost:focus-visible { color: var(--violet-dp); }

@media (max-width: 700px) {
  /* Shorter track, and it is THE PHONE'S PLAYBACK RATE. The reasoning survives
     from the video this section used to hold: the same track height on a tall
     phone screen is far more pixels to push with a thumb for the same amount of
     film, so the phone gets a shorter one. 480vh of an 844px screen is about
     4,000px, which is a handful of swipes for the whole flight.
     The 900px rule above already takes it to 540vh; this takes the small phones
     down again. */
  .globe3d { height: 480vh; }
}

/* ======================================================= /news/ index ======
   Built 2026-08-18, the last page of the rebuild. It reuses `.post` from the
   home page's news strip rather than inventing a card, so the two stay in step:
   anything done to a card here shows up there and vice versa. Two things are
   new - the featured lead, and a square image box.

   ⚠ THE CARD IMAGE BOX IS SQUARE, AND IT IS NOT AN AESTHETIC CHOICE. Five of
   the seven cards are his own pre-show promo graphics, and four of those are
   1:1 artboards whose type runs from a logo at the very top to a stand-number
   row at the very bottom. Crop one to landscape and it is decapitated at both
   ends. So the grid takes the shape the artwork already has, and the two
   exceptions were re-exported to match it (see the notes in
   `tools/build-news-anuga-2025.ps1` and `-sial-shanghai-2026.ps1`).
   The home strip keeps its own 4:3 - it is set inline per figure, not here. */

/* The upcoming show leads the page on its own, at landscape width, because the
   one thing on it that must never be cropped is the poster: every word on that
   artwork is load-bearing, and it is already exactly 3:2. It is also the only
   post on the index in the future tense, so giving it a different shape is the
   layout carrying a distinction that is really there rather than decorating. */
.newslead {
  display: grid; grid-template-columns: 7fr 5fr;
  gap: clamp(26px, 3vw, 46px); align-items: center;
  text-decoration: none; color: inherit;
}
.newslead .media img { transition: transform 420ms var(--ease-out); }
.newslead .label { display: block; color: var(--violet); margin-bottom: 14px; }
.newslead h2 { margin: 0; transition: color var(--t-hover) ease; }
.newslead .lead { margin-top: 16px; }
/* `.article-meta` was written for the top of a post, where a headline sits
   above it. Here it closes the block, so its top margin is the wrong end. */
.newslead .article-meta { margin: 20px 0 0; }
/* NOT a `.btn`. The whole card is one anchor, so a second thing shaped like a
   button would promise a second destination it cannot have. This is a cue. */
.newslead-more {
  display: inline-block; margin-top: 22px;
  font-weight: 600; font-size: 15.5px; color: var(--violet);
}
/* ⚠ THE GAP IS A MARGIN, NOT A SPACE IN `content`. `display: inline-block` is
   what lets the arrow translate on hover, and an inline-block collapses the
   leading whitespace inside its own content string, so " \2192" renders hard
   against the last letter. */
.newslead-more::after {
  content: "\2192"; display: inline-block; margin-left: .4em;
  transition: transform var(--t-hover) ease;
}
@media (hover: hover) and (pointer: fine) {
  .newslead:hover h2 { color: var(--violet); }
  .newslead:hover .media img { transform: scale(1.03); }
  .newslead:hover .newslead-more::after { transform: translateX(4px); }
}
@media (prefers-reduced-motion: reduce) {
  .newslead .media img, .newslead-more::after { transition: none; }
  .newslead:hover .media img { transform: none; }
}
@media (max-width: 900px) {
  .newslead { grid-template-columns: 1fr; gap: 24px; }
}

/* Six cards rather than the home strip's three, so they need more air between
   rows than between columns. Written `.news.news--index` on purpose: the plain
   `.news` rule inside the 900px block further up would otherwise win on source
   order and take this straight from three columns to one. */
.news.news--index { row-gap: clamp(36px, 4vw, 58px); }
@media (max-width: 900px) and (min-width: 561px) {
  .news.news--index { grid-template-columns: repeat(2, 1fr); }
}
/* The home strip's cards are a date and a headline. An index has to be
   scannable without opening anything, so these carry one line of standfirst -
   lifted verbatim from the post's own published lead, never rewritten, so
   there is no second version of a sentence to keep true. */
.post-dek { margin: 10px 0 0; font-size: 15px; color: var(--dim); line-height: 1.55; }

/* ==========================================================================
   TOUCH AND SMALL-SCREEN ERGONOMICS  (impeccable `adapt`, 2026-08-18)
   ==========================================================================
   Everything here came out of `tools/responsive-probe.mjs`, which renders all
   fourteen pages at nine device configurations and measures what a screenshot
   cannot: tap-target rectangles, computed font sizes, and anything whose right
   edge crosses the viewport. Run it before and after touching this block.

   ⚠ THE LAYOUT ITSELF PASSED. 126 page-renders, zero horizontal overflow, zero
   image wider than its viewport. Nothing below is a layout repair, and nothing
   below should become one — the grids in the `responsive` section further up
   are what makes the site fit, and they already work. This block only fixes
   what a FINGER and a PHONE SCREEN need that a mouse and a desk do not.

   It lives at the end of the file on purpose. Every rule here is a deliberate
   override of a component's own sizing, and the cascade should let it win
   without anyone having to raise a specificity to make it stick.
   -------------------------------------------------------------------------- */

/* ---- 1. The desktop nav on a touch screen ------------------------------- */
/* The drawer takes over at 900px, which covers every phone and a tablet held
   in portrait. It does NOT cover an iPad in landscape at 1024, and that is a
   genuine touch device getting the pointer layout: the probe measured those
   nav links at 22px tall, exactly half the 44px floor.

   The fix is NOT to move the drawer breakpoint up. A 1024px screen has room
   for six links and a CTA, and burying them behind a hamburger to serve a
   thumb is a downgrade for the sake of a rule. The links stay visible and grow
   a touchable box instead. The bar’s own height is untouched (90px resting,
   66px stuck; a 46px link fits inside both), and on a mouse the rule never
   matches, so the nav is exactly as it was.

   ⚠ THE FLOOR IS BOTH DIMENSIONS. Height alone left "News" at 36x46 - still
   a miss, because a nav link is only ever as wide as its word. `padding-inline`
   gives the short labels their width back, and the gap comes down by exactly
   what the padding adds (28 -> 16, plus 6 a side), so the bar’s rhythm is
   unchanged and the links simply grow into space that was already between
   them. */
@media (min-width: 901px) and (pointer: coarse) {
  .mainnav { gap: 16px; }
  .mainnav a:not(.btn) { padding-block: 12px; padding-inline: 6px; }
}

/* ---- 2. Everything a finger has to hit --------------------------------- */
/* Keyed on `pointer: coarse`, not on width. A 13" laptop with a touchscreen
   needs these and a 1024px window on a desktop does not, and only the pointer
   query can tell those two apart. */
@media (pointer: coarse) {
  /* The footer's social buttons were 40x40 — near miss, and a near miss on a
     40px target is a mis-tap roughly a fifth of the time. */
  .social a { width: 44px; height: 44px; }

  /* The logo link is as tall as the logo inside it, which is 38px on a phone.
     `min-height` grows the hit box without touching the artwork: the SVG keeps
     its 38px and the link centres it in 44. */
  .brand { min-height: 44px; }
}

/* ---- 3. The form, and why 16px is not a preference ---------------------- */
/* iOS Safari zooms the entire page when a field smaller than 16px takes focus,
   and it does not zoom back out afterwards. The visitor is left on a page
   1.07x too wide, scrolling sideways through the rest of the form. The site's
   one conversion action is this form, so the whole cost of that lands on the
   only thing the page is asking anyone to do.

   16px is the trigger threshold exactly, so 16px is what it takes — 15.5 still
   zooms. Applied only where the pointer is coarse, so the desktop form keeps
   the 15px it was designed at. */
@media (pointer: coarse) {
  .field input, .field select, .field textarea { font-size: 16px; }
}

/* ---- 4. The micro-type floor on small screens --------------------------- */
/* The mono eyebrow is a signature of this design — 11px, .14em, uppercase, in
   a dozen places. At a desk that reads as precision. On a phone it is under
   the 12px floor, and uppercase mono at that size is the first thing to go
   when someone reads the site outdoors or on a stand at a trade fair, which is
   exactly where this site gets opened.

   12px, not 13: the tracking and the caps do most of the legibility work here,
   so one point is enough to clear the floor without the eyebrow starting to
   compete with the heading under it. Desktop is deliberately left alone — it
   was designed and approved at 11px and there is nothing wrong with it there. */
/* `.factcell dt` was 10px, which is below the floor at every width, not just on
   a phone. Lifted everywhere; the mobile rule below takes it the rest of the
   way. This is the one desktop type change in this block.
   ⚠ IT MUST STAY ABOVE THAT BLOCK. Both rules are (0,1,1) and a media query
   adds no specificity, so whichever is written last wins. Written after, this
   line silently re-capped the phone at 11px. */
.factcell dt { font-size: 11px; }

@media (max-width: 900px) {
  .label,
  .caliber-spec,
  .step-n,
  .stage-when,
  .stage-num,
  .tile-kicker,
  .video-play-meta,
  .cert .ref,
  .post time,
  .figs dt,
  .rangespec dt,
  .factcell dt,
  .globe3d-cue,
  .footer h4,
  .hero-quote footer span { font-size: 12px; }
}


/* ---- 5. Notched phones: the safe area ---------------------------------- */
/* `viewport-fit=cover` in every page's viewport meta lets the page paint into
   the rounded corners and behind the home indicator, which is what makes a
   full-bleed dark band look right on an iPhone instead of stopping at a white
   letterbox. The cost is that it is now OUR job to keep content out of those
   corners, and the corners only bite in landscape, where the notch and the
   rounded edge eat into the left and right margins.

   `max()` and not a plain addition: on every device with no inset, `env()`
   resolves to 0 and the gutter is exactly what it always was. Nothing changes
   anywhere except the one case this exists for. */
.wrap {
  padding-left: max(var(--gut), env(safe-area-inset-left));
  padding-right: max(var(--gut), env(safe-area-inset-right));
}
/* The footer is the last thing above the home indicator, and 34px of padding
   is not enough to clear it on an iPhone held upright. */
.footer { padding-bottom: max(34px, calc(env(safe-area-inset-bottom) + 20px)); }
/* The masthead is fixed to the top edge, so in landscape its left end sits
   under the notch. It is the one bar that cannot rely on `.wrap` alone,
   because its background must still reach the true edge of the screen. */
.masthead .wrap {
  padding-left: max(var(--gut), env(safe-area-inset-left));
  padding-right: max(var(--gut), env(safe-area-inset-right));
}

/* ---- 6. The SIAL panel, rebuilt for a phone ----------------------------- */
/* The desktop hero hands the whole show to one bleed panel of artwork. A phone
   has no column to bleed into, so below 901px the panel was hidden and the
   "Book a meeting at SIAL" button was left pointing at a show the page never
   named. His call, 2026-08-18: the poster comes back, and the facts come back
   with it as real type.

   Above 901px this whole block is display:none and the desktop panel is
   untouched — `.hero-sial` and `.hero-sial-m` are never both on screen. */
.hero-sial-m { display: none; }

@media (max-width: 900px) {
  .hero-sial-m { display: block; margin-top: 34px; }

  /* A POSTER IS NEVER COVER-CROPPED. Every word on this artwork is
     load-bearing, so the frame takes the picture's own 3:2 and the image fills
     it exactly — no `object-fit: cover`, nothing to crop. `aspect-ratio` on the
     figure reserves the box before the file arrives, so the buttons above it
     never jump. */
  .hero-sial-m picture {
    display: block; aspect-ratio: 3 / 2; overflow: hidden;
    border: 1px solid var(--line); border-radius: var(--r);
    background: var(--paper-hi);
  }
  .hero-sial-m img { display: block; width: 100%; height: 100%; }

  /* Two up, not four. Four cells across 350px of usable width is 80px each,
     and "Paris Nord Villepinte" needs three lines in that. Two up gives the
     venue a full half and keeps the row two deep — the same call the news
     posts' fact rows make at this width, for the same reason. */
  .hero-sial-facts {
    margin: 10px 0 0; display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;
  }
  /* Hall "2" and Booth "B032" are two and four characters. Side by side they
     are the short row that closes the block, and the violet booth cell is the
     one fact a buyer writes down. */
}
