/* =============================================================
   Prizmar -- v1 site styles
   Plain CSS, no build step. Depends on css/tokens.css.
   ============================================================= */

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--page-base);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  position: relative;
  overflow-x: hidden;
}

img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--agri-main);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

.shell {
  width: 100%;
  max-width: var(--shell-max);
  margin-inline: auto;
  padding-inline: clamp(20px, 5vw, 56px);
}

section { position: relative; }
.band { padding-block: clamp(64px, 10vw, 128px); }
/* v2 round 3 item 5: with the teaser cards gone, "Two worlds sharing an
   engine" is the last thing in About and now introduces the Fintech/Agri
   cards in the next section. Two full band paddings between them left a
   256px void, so both facing edges are tightened. */
.band--tight-bottom { padding-bottom: clamp(28px, 3.5vw, 48px); }
.band--tight-top { padding-top: clamp(28px, 3.5vw, 48px); }
/* Tinted section background (v2 round 2 item 1). Scoped to the section
   band only -- --surface-alt still fills small components (stat tiles,
   form inputs, footer), which this round didn't ask to re-tint. */
.band--alt { background: var(--band-alt-bg); }

.eyebrow {
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--ink-soft);
  margin: 0 0 0.9rem;
}

/* SEO heading pass: some headings changed tag (h4 -> h3 "was-h4", h5 -> h4
   "was-h5", and the two Home-card titles stopped being headings). The
   :where() wrappers below add NO specificity, so every moved heading keeps
   its exact previous cascade -- see the "was-h4 / was-h5" notes further
   down each rule that used to key on h4/h5. */
h1, h2, h3, h4:not(:where(.was-h5)) {
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.05;
  margin: 0 0 0.6em;
}
h1 { font-size: clamp(2.4rem, 5.5vw, 4rem); }
h2 { font-size: clamp(1.9rem, 3.6vw, 2.6rem); }
h3:not(:where(.was-h4)) { font-size: clamp(1.25rem, 2vw, 1.5rem); }
/* was-h4 (an h3 that used to be an h4) and was-h5 (an h4 that used to be an
   h5): reproduce the browser defaults the old tag gave them. */
:where(h3.was-h4) { font-size: 1em; }
:where(h4.was-h5) { font-size: 0.83em; margin: 1.67em 0; }
p { margin: 0 0 1em; color: var(--ink-soft); max-width: 62ch; }

.placeholder {
  display: inline-block;
  font-style: italic;
  color: #9A6A2D;
  background: #FBEFD9;
  border: 1px dashed #C99A4A;
  border-radius: var(--radius-sm);
  padding: 0.05em 0.5em;
  font-size: 0.92em;
}

/* =============================================================
   Page-wide background
   ============================================================= */

/* Site-wide prismatic texture (v2 round 2 item 1). One large
   viewport-covering composition, not a repeating tile -- the deck's
   background is big soft facets, which tiling at 340px turned into
   visible wallpaper. */
.bg-texture {
  position: fixed;
  inset: 0;
  z-index: -3;
  background-image: url("../assets/prism-texture.svg");
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
  /* v2 round 3 item 2: 0.9 -> 1.0. The texture SVG paints its own base
     wash, so at 0.9 it was blended 10% toward the page colour, flattening
     its internal contrast by the same amount. Going fully opaque restores
     that ~11% -- the requested 10-15% more visible -- without touching
     the artwork itself. */
  opacity: 1;
  pointer-events: none;
}

/* Round 14 item 1: Home and the closed streams band opt OUT of the
   site-wide prism texture above.

   Round 13 item 1 removed the hero's own faceted layer (.hero__bg,
   assets/hero-facets.svg) and reported Home as reverted to "plain" -- but
   .bg-texture is a FIXED, viewport-covering, fully-opaque prism layer at
   z-index -3, so it kept painting straight through the hero regardless.
   Removing the hero-specific layer only uncovered the site-wide one; the
   client correctly reported the texture was still there. That was the
   miss: "plain" was verified against the hero's own rules rather than
   against what actually rendered.

   The fix is per-section rather than deleting .bg-texture outright,
   because that element is genuinely site-wide and this item is scoped to
   Home/Streams only. Giving these two sections their own opaque
   --page-base background covers the fixed layer for exactly their own
   box and nothing else. Ordinary painting order does the work: a
   positioned element with z-index:auto paints above a z-index:-3 one, so
   no new z-index or stacking context is needed here.

   Scope, deliberately: #home (the wordmark, the lede, and the closed
   Fintech/Agri preview cards -- all one section) and the band holding
   the closed Fintech/Agri stream cards. NOT About or Contact: those
   carry their own opaque --band-alt-bg, already hide .bg-texture, and
   their "P" watermarks (.about-watermark/.contact-watermark) are
   explicitly out of scope for this item. */
.hero,
.band--tight-top {
  background: var(--page-base);
}

/* The sticky header sits ABOVE #home in flow, so the strip either side of
   the header pill is page background, not hero background -- and it kept
   showing the prism texture even once the hero itself went flat, which
   reads worse next to a now-perfectly-flat hero than it did before.

   Covered with a band anchored to the hero rather than by giving
   .site-header its own background: the header is sticky and travels the
   whole page, so an opaque background on it would drag a --page-base
   strip across the tan About/Contact bands. This pseudo scrolls away
   with the hero, so it can never do that. 140px comfortably clears the
   header at every width (~78px desktop, less on mobile); any excess just
   paints --page-base over --page-base, and anything above the document
   top is simply clipped.

   z-index -1 (not -3) so it lands above .bg-texture but still behind all
   hero content. .hero is position:relative with z-index:auto, so it does
   not trap this in a local stacking context. */
.hero::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -140px;
  height: 140px;
  background: var(--page-base);
  z-index: -1;
  pointer-events: none;
}

/* =============================================================
   Round 14 item 3 -- beams (animated background layer)
   ============================================================= */

/* The asset is the client's own designed animation, re-encoded from
   their 7.02MB GIF to a 138KB H.264 MP4 (assets/beams-loop.mp4). The
   motion lives entirely in the file -- no JS animation, no SVG path
   transforms, which is the approach that broke in rounds 4/5/6/9/10.

   Why the file is short: the source GIF is 150 frames / 5s, but it is
   ~4.2 repeats of a 36-frame cycle. Measured, not guessed -- a period
   sweep over the decoded frames gives a clean minimum at exactly 36
   (mean abs diff 0.015/255 against a 0.134 adjacent-frame baseline,
   i.e. frame 36 is ~11% of one frame-step away from frame 0). Encoding
   one cycle is therefore visually identical to encoding all four AND
   loops seamlessly, where the full 150 frames does NOT (frame 149 is
   ~6.6 frame-steps from frame 0, so looping the whole thing visibly
   jumps).

   Compositing: the source has an opaque near-white ground (251-255) and
   no alpha, so mix-blend-mode: multiply is what makes it sit on the page
   -- white multiplies to no change, only the coloured lines darken.
   Verified the encode keeps flat areas flat (they darken by <0.6/255),
   so multiply cannot introduce a grey wash over the section.

   Placement follows the earlier rounds' convention: full-bleed, cover,
   centred. The artwork's own composition already bleeds off the top,
   bottom and right edges (its ink spans x 21.6%-98.8%, y 0-100% of the
   frame), so cover crops it the way it was designed to be cropped and
   no bespoke offset/scale is needed this time. */
.beams-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -1;
  mix-blend-mode: multiply;
  opacity: var(--beams-opacity, 0.55);
  pointer-events: none;
}

/* Portrait screens: `cover` on a wide video shows only a narrow slice, and
   centred that slice is mostly empty ground (the v3 artwork's ink sits in
   x 30%-99% of the frame). Nudge the crop a little toward the right side
   of the artwork (content moves left on screen) so the beams show
   moderately on phones and tablets. */
@media (max-width: 900px) {
  .beams-bg { object-position: 62% 50%; }
}

/* .hero is position:relative with z-index:auto, so it does NOT create a
   stacking context -- a z-index:-1 child would escape to the root layer
   and paint UNDERNEATH .hero's own opaque --page-base background (item
   1), i.e. be invisible. isolate gives .hero its own stacking context so
   the video lands above that background and below the content, and it
   also scopes the multiply blend to the hero rather than letting it
   reach the page layers beneath. */
.hero { isolation: isolate; }

/* Round 19 item 1: the Streams section carries the hero's beams
   treatment as a background layer -- a .beams-bg <video> child of the
   section plus isolation on the section, exactly as .hero does it.
   Always present on the section, independent of any panel's expand
   state. isolation:isolate is needed for the same reason .hero needs
   it: the section has an opaque --page-base background a z-index:-1
   child would otherwise paint beneath. */
.band--tight-top { isolation: isolate; }

/* Expanded stream panels only. The closed card is the same element, so
   without this the beams would show on the closed Fintech/Agri cards --
   which item 1 has just made deliberately plain. display:none also means
   the (preload="none") video is never fetched until a panel is opened.
   .stream-panel.is-expanded already carries z-index:200, so it is
   its own stacking context and needs no isolation of its own. */
.stream-panel .beams-bg { display: none; }
.stream-panel.is-expanded .beams-bg { display: block; }

/* Dialled down against the panel specifically. The hero sits on the warm
   --page-base (#EFEDE9); an expanded panel sits on --surface (#FFFFFF),
   and multiply against pure white leaves the line colours at full
   strength rather than knocking them back, so the same opacity reads
   noticeably busier there -- particularly where lines cross the service
   rows. One variable, set on the panel and inherited by the video. */
.stream-panel.is-expanded { --beams-opacity: 0.38; }

/* Same treatment as every other motion on this site (round 6's sheen,
   round 6 item 3's sweep): fully inert under reduced motion, not merely
   paused. js/main.js also skips play() entirely in that case, so the
   file is never fetched for the stream panels either. */
@media (prefers-reduced-motion: reduce) {
  .beams-bg { display: none !important; }
}

/* About-section prism-"P" watermark (round 4). Round 1 had a
   viewport-fixed version of this (position:fixed, z-index:-1, shown
   via body.past-home) -- removed, because z-index:-1 on a *fixed*
   element paints behind literally every in-flow element's own
   background per CSS stacking order, including .band--alt's opaque
   --band-alt-bg fill. That's the same reason About has no beams
   showing through it (its own opaque background occludes the fixed
   texture/beams layers too) -- so the old watermark was invisible on
   the one class of section it could ever land on. Fixed by scoping it
   to #about specifically, as an absolutely-positioned child with a
   negative z-index.

   That alone was NOT enough, verified live: every <section> already
   has position:relative (see the global `section` rule above), but
   position:relative with z-index:auto does NOT establish a new
   stacking context -- so a negative-z-index child still escapes to
   the document ROOT stacking context instead of staying scoped inside
   #about, and got painted behind something else entirely (confirmed
   by testing at opacity:0.9 in the live DOM: still fully invisible).
   #about below now gets `isolation: isolate` to force it to own its
   stacking context, which correctly contains the child: paints
   *after* #about's own background but *before* #about's normal-flow
   content (heading, founder cards, etc) -- exactly "behind content,
   in front of the beige." Re-verified live after adding isolation:
   visible at opacity:0.9, confirming the fix, before dialing back to
   the shipped 0.12. No JS/past-home dependency needed any more; it's
   simply part of #about's own box now, in flow with the rest of the
   page.

   Source: assets/prizmar-p-watermark.svg -- assets/prizmar-logo.svg
   with its viewBox narrowed to "50 246 265 279", same crop-the-viewBox
   technique as the beams/logo assets, not a redraw, so the original
   mask/facet-shading defs still render correctly. That bbox was found
   by querying getBBox() on the P-mark's flat vector facet paths
   (x:55.7-286.5, y:210.7-522.2) and the raster facet-shading
   clipPaths (x:52-311, y:164-402) in the live DOM, unioned, then
   clamped to the parent logo's own viewBox top (246, since nothing
   above that was ever visible in the full lockup anyway) -- verified
   by rendering the crop standalone before use, not just computed.
   Ends 11px before the "RIZMAR" wordmark group, which starts at
   x=326.4, so there's no risk of bleeding letterforms in. */
/* Round 4b: the same watermark treatment is now on BOTH beige bands.
   Those are exactly the two .band--alt sections -- #about and #contact
   (grepped, not assumed: .band--alt is the only consumer of
   --band-alt-bg / #DED7CB, and only those two sections carry it).
   #about's own markup is deliberately left untouched (already signed
   off); #contact just adds its own element to the shared selector. */
#about, #contact { isolation: isolate; }
/* Round 13 item 2: assets/prizmar-p-watermark.svg's crop fixed -- its
   viewBox height was 279 (bottom edge at document y=525), but the P's
   leg is the lowest point of the entire wordmark and its real ink runs
   to y=~609 (measured by rasterizing the full logo and scanning for the
   true non-transparent pixel bounds, not by trusting the old crop's own
   numbers -- see WORKLOG/commit message for the measurement). The old
   crop was truncating roughly the bottom third of the leg. New viewBox
   is "50 246 265 366": same left/top/width as before (all independently
   re-verified correct -- x:50 left edge and x:315 right edge both still
   sit outside the real ink, which runs y:249-609 vertically and
   x:52.75-311 horizontally with the next letter, R, not starting until
   x:326), height simply extended to 366 -- the FULL logo's own total
   canvas height, since the leg's tip sits almost exactly on that
   boundary already (609 measured vs. 612 canvas bottom). aspect-ratio
   below updated to match (265/279 -> 265/366); background-size: 100%
   auto means the box's own aspect-ratio is what the SVG is stretched to
   fit, so this MUST track the viewBox exactly or the letterform
   distorts. */
/* Round 21 follow-up: the light "P", deferred out of 1f0dfe1 because the
   asset did not exist yet. Both consumers of this rule are now permanently
   dark -- #about.band--alt (1f0dfe1) and #contact.band--alt (f61eda7) both
   paint --ink-deep -- so the dark-toned SVG above was rendering an almost
   black mark at 0.12 over near-black, i.e. nothing at all.

   Swapped in place rather than adding a `--light` variant class: grepped,
   not assumed -- .about-watermark/.contact-watermark have exactly two
   consumers in the whole repo (index.html:146 and :787), one per dark
   section, so a second selectable variant would be dead the moment it was
   written. The shared `.band--alt` tan rule is still deliberately
   untouched for a future light band, but such a band would bring its own
   watermark element, not one of these two id-specific classes.
   assets/prizmar-p-watermark.svg stays on disk unreferenced, same
   convention this repo already follows for superseded assets.

   Asset: assets/prizmar-p-watermark-light.png, cropped from the supplied
   assets/Prizmar_P_Letter_white_PNG.png. That export is a full 16:9
   1920x1080 frame with ~740px of dead canvas right of the mark, so it was
   re-cropped to its TRUE ink bounds the same way round 13 did the SVG --
   rasterise and scan for the real non-transparent pixel extent, do not
   trust the canvas or a stated box. Measured (115,20)-(1180,1077) at
   alpha>0, stable from alpha>0 through alpha>8 (no stray fringe pixels),
   giving a 1066x1058 crop; re-measured after writing to confirm the ink
   touches all four edges. Quantised to 256 colours (octree): visually
   identical on this flat facet art (max RGB delta 29, mean 2.6 where the
   mark is visible) with ZERO formerly-transparent pixels gaining alpha, so
   no background haze -- 307KB lossless RGBA down to 47KB.

   aspect-ratio therefore becomes 1066/1058 (~1.008, near square), NOT the
   old 265/366 (~0.724, portrait): this is a different crop of a different
   drawing, and background-size:100% auto means the box's aspect-ratio is
   what the image is stretched to fit, so a stale value here distorts the
   letterform. Re-derived from this file's own bounds, not inherited.

   Opacity re-tuned live against --ink-deep on both sections rather than
   inheriting 0.12 (which was tuned for a DARK mark on tan and has no
   bearing on a LIGHT mark on near-black). Shipped at 0.10, walked down
   live per this file's convention -- 0.9 first purely to confirm it was
   rendering at all, then 0.30 -> 0.14 -> 0.10 -> 0.08 on both #about and
   #contact. The mark is ~77% large light facet fills (rgb(234,234,234)
   39% of ink, rgb(218,218,218) 38%), so those fills are what actually
   carries the read; over rgb(23,25,31) they composite to:
     0.30 -> rgb(86,88,92)   2.46:1  competes with the founder cards
     0.14 -> rgb(53,54,59)   1.46:1  still reads as a graphic, not a mark
     0.10 -> rgb(44,46,51)   1.29:1  SHIPPED -- present but clearly secondary
     0.08 -> rgb(40,42,47)   1.22:1  structure stops registering
   For scale, the old dark-on-tan watermark sat around 1.17:1, so 0.10 is
   a touch more present than that -- deliberate: this drawing is line art
   with thin strokes rather than a solid mark, and it needs the extra
   step to hold its shape at all.

   The purple/green thread bundles baked into this export are KEPT rather
   than desaturated to match the neutral-grey P used elsewhere -- they are
   only ~9% of the ink (rgb(104,76,121) 5.5%, rgb(110,151,124) 3.4%) and
   at 0.10 they land at rgb(31,30,40) / rgb(32,38,40), a barely-there tint
   against the rgb(23,25,31) band, not colour. Flagged for Oleksii: if he
   wants them gone it is a desaturate-and-re-export of the source PNG, not
   a CSS change. */
.about-watermark,
.contact-watermark {
  position: absolute;
  top: 20px;
  right: -70px;
  z-index: -1;
  width: min(46vw, 560px);
  aspect-ratio: 1066 / 1058;
  background-image: url("../assets/prizmar-p-watermark-light.png");
  background-repeat: no-repeat;
  background-position: left top;
  background-size: 100% auto;
  opacity: 0.1;
  pointer-events: none;
}

/* Round 38 item 1 -- page-level horizontal overflow on mobile.
   `right: -70px` above bleeds the mark off-canvas, which is the intent on
   desktop, but it makes the document exactly 70px wider than the viewport
   at every width. Desktop never shows it (body's overflow-x: hidden
   propagates to the viewport and clips it, and a desktop browser keeps its
   layout viewport at the window width regardless). A phone does not:
   measured at a 390px device, document.documentElement.scrollWidth was 460
   and -- the part that actually hurts -- window.innerWidth was 460 too,
   i.e. the LAYOUT VIEWPORT itself had stretched to fit the overflow. That
   is what reads as the blank strip on the right / everything shifted and
   cut on the left. Neutralising just this one offset took both numbers
   back to 390; nothing else on the page overflows unclipped (checked by
   walking every element and skipping the ones inside a scroll container,
   e.g. the case carousel's own cards).
   Flush right below the mobile breakpoint; the desktop bleed is untouched. */
@media (max-width: 900px) {
  .about-watermark,
  .contact-watermark { right: 0; }
}

/* Round 21: this is fixed, so it scrolls over both the light bands and
   About's new obsidian one. `difference` inverts it against whatever is
   behind it -- no scroll-tracking JS, and it keeps working if any other
   band goes dark later.

   The colour has to be white for that to invert cleanly: difference is
   |backdrop - source|, so a white source reads as near-black over a
   light band and near-white over a dark one, which is exactly the
   auto-inverting behaviour wanted. The old --ink-soft grey would have
   differenced to a muddy mid-tone on BOTH backgrounds instead. Opacity
   then pulls the result back toward the backdrop so it stays a
   watermark rather than becoming body copy -- tuned by eye against both
   backgrounds (see the round 21 report). */
.corner-tagline {
  position: absolute;
  left: 20px;
  bottom: 18px;
  z-index: 5;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  color: #fff;
  opacity: 0.55;
  mix-blend-mode: difference;
  pointer-events: none;
}
/* Round 39 item 1: on a phone this runs straight under the floating
   "Request a call" pill -- measured at 390px, the tagline's box ended at
   x=265 and the pill started at x=252, same y-band, so the tail of
   "Cargo to cash." was sitting behind a button. The pill is the
   functional element and the tagline is decoration, so the tagline is the
   one that goes. Hidden, not shrunk or moved: there is no room on a
   375-430px row for both, and a half-tagline reads worse than none. */
@media (max-width: 760px) {
  .corner-tagline { display: none; }
}

.float-contact {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 40;
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  background: var(--ink);
  color: #fff;
  /* Round 21: a persistent hairline edge. This pill is a solid dark fill
     with a shadow tuned for light bands -- over About's obsidian the
     shadow does nothing and the pill's own silhouette dissolves into the
     background (the white label stays readable, but the button stops
     looking like a button). A faint light border gives it an edge on any
     background, so no scroll-tracking is needed and it still holds up if
     Contact goes dark later too. box-sizing is border-box globally, so
     this doesn't change the pill's outer size. */
  border: 1px solid rgba(255, 255, 255, 0.35);
  padding: 0.7em 1.1em;
  border-radius: var(--radius-pill);
  font-size: 0.85rem;
  font-weight: 600;
  box-shadow: 0 8px 24px rgba(23, 25, 31, 0.25);
  transition: transform var(--dur-fast) var(--ease);
}
.float-contact:hover { transform: translateY(-2px); }

/* =============================================================
   Header
   ============================================================= */

.site-header {
  position: sticky;
  top: 0;
  /* Round 4 item E: raised above .stream-panel.is-expanded (200) and
     .stream-backdrop (199) -- the header must stay visible and clickable
     while a stream card is fullscreen-expanded, not get covered by it.
     Still well below nothing else on the page (next highest is the
     panel itself), so this doesn't change stacking against any other
     element. */
  z-index: 210;
  padding: 14px clamp(12px, 3vw, 28px) 0;
  /* Round 5 item 2: declared here (not on .site-header__inner) so the
     value is visible to BOTH .site-header__inner (a child) and
     .site-header::before (this element's own pseudo, i.e. a sibling of
     .site-header__inner in the box tree, NOT a descendant of it --
     custom properties only inherit down the ancestor chain, so if this
     had stayed on .site-header__inner the backing plate's ::before
     could never have seen it). Both the visible pill and its backing
     read the exact same --facet, which is the whole point: they clip to
     an identical shape at any length. */
  /* Round 8 item 3: --facet (a point LENGTH, for the old triangular-end
     hexagon) is replaced by --chamfer (a corner CUT size, for the new
     octagon -- flat top/bottom/left/right edges, all 4 corners cut at a
     literal 45 degrees). Renamed rather than reused because the two
     values mean geometrically different things and a future reader
     comparing this to the round-7 history should not have to guess
     whether "facet" still means "point length." Same 60-70px bar height
     as before (measured live, unaffected by this change) -- 18px keeps
     the cut well short of the vertical midpoint, so top/bottom/left/
     right edges all stay clearly flat rather than the corners meeting
     and collapsing back into a diamond/hexagon. */
  --chamfer: 18px;
}

/* Round 4 item 0 (carryover bug fix). The clip-path on .site-header__inner
   below cuts its box down to a hexagon, so the two triangular corners it
   removes paint no opaque background at all in that gap -- only the
   ::before/::after tint wedges (0.20 alpha, fading to transparent at the
   very tip) sit there, so whatever scrolls past underneath (confirmed by
   reproducing at "Our Values": the heading text visibly bled through the
   corner strip right above the pill) showed through the sharp tips.

   Root cause is specifically that a solid background painted BY
   .site-header__inner cannot survive its own clip-path -- clipping removes
   paint for that whole box, backgrounds included, corners included.

   Fix: a plain, unclipped backing plate as a NEW box behind it, sized to
   the identical rect via the same max-width/margin-inline/inset math (not
   duplicated blindly -- this recomputes from the same --header-max and
   padding values .site-header__inner itself uses, so the two stay in sync
   if either changes), same fill colour/opacity in both the default and
   past-home states, and its own border-radius switched by the same
   :has(.site-nav.is-open) condition so it doesn't show rounder corners
   than the panel it's backing once the mobile nav opens.

   One accepted trade-off, deliberate: this plate is a full pill/rect, not
   the exact wedge-shaped complement of the hexagon, so inside the hexagon
   itself two 0.92-ish (or 0.62-ish, past-home) white layers now stack
   instead of one -- composited opacity there is nudged up by roughly
   1-(1-x)^2 (0.92->~0.994, 0.62->~0.86). On a near-white pill over a light
   page this is not perceptible; the exact-complement fix would need a
   4-triangle inverse clip-path for a gain nobody would see. Verified after
   shipping: nav links still clickable (backing sits behind .site-header__inner
   in paint order, pointer-events:none besides), and the mobile
   :has(.site-nav.is-open) fallback still drops cleanly to a rounded
   rectangle with no clip-path, same as before this fix.

   Round 5 item 2 -- superseded the "full pill/rect" approximation above.
   Sharpening the point (see --facet below, 28px -> 52px) breaks that
   approximation: a pill's rounded cap radius is roughly half the bar's
   own height (~25-32px here), which happened to be close enough to the
   OLD 28px facet to cover its wedge, but a 52px point runs well past
   where the rounded cap reaches -- the backing would stop covering the
   tip before the visible pill's own point does, and the corner-bleed
   bug (see the "Round 4 item 0" note above) would come straight back.
   Fixed for good instead of re-approximated: this plate now clips to
   the EXACT SAME polygon as .site-header__inner, via the same --facet
   custom property (moved up to .site-header so both the pseudo-element
   and the real element can read it), so backing and visible shape are
   always identical regardless of facet length. Re-verified after
   sharpening: no bleed at "Our Values" (same reproduction spot as the
   original bug), nav links + CTA still all hit-test correctly, and the
   mobile :has(.site-nav.is-open) fallback still drops both this plate
   and .site-header__inner to the same plain rounded rect together. */
.site-header::before {
  content: "";
  position: absolute;
  top: 14px;
  left: clamp(12px, 3vw, 28px);
  right: clamp(12px, 3vw, 28px);
  bottom: 0;
  max-width: var(--header-max);
  margin-inline: auto;
  z-index: -1;
  background: rgba(255, 255, 255, 0.92);
  /* Round 8 item 3: chamfered octagon, replacing the flat-left/pointed-
     right hexagon from round 7. Same polygon as .site-header__inner
     below -- these two MUST stay identical or the corner-bleed bug
     documented above comes straight back, now on all 4 corners instead
     of just the two that used to be pointed. 8 vertices, one pair per
     corner, each cut --chamfer in from both edges it touches -- that
     equal x/y offset is what makes the cut a literal 45 degrees
     regardless of the bar's own aspect ratio. */
  clip-path: polygon(
    var(--chamfer) 0,
    calc(100% - var(--chamfer)) 0,
    100% var(--chamfer),
    100% calc(100% - var(--chamfer)),
    calc(100% - var(--chamfer)) 100%,
    var(--chamfer) 100%,
    0 calc(100% - var(--chamfer)),
    0 var(--chamfer)
  );
  border-radius: 0;
  pointer-events: none;
  transition: background var(--dur-med) var(--ease), border-radius var(--dur-med) var(--ease),
              clip-path var(--dur-med) var(--ease);
}
body.past-home .site-header::before { background: rgba(255, 255, 255, 0.62); }
.site-header:has(.site-nav.is-open)::before { clip-path: none; border-radius: var(--radius-md); }

.site-header__inner {
  max-width: var(--header-max);
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Pre-existing bug, found while verifying item 1's mobile open-nav
     state (round 7). The mobile rule below gives .site-nav
     `flex-basis: 100%` and `order: 4`, which only drops it onto its own
     line if this container can wrap -- it couldn't, because flex-wrap
     defaults to nowrap and was never set here. Result on the committed
     build: opening the mobile nav grew the pill into a tall empty panel
     with the links squeezed to invisibility (confirmed live at 375px on
     the pre-change CSS, computed flex-wrap: nowrap). Desktop is
     unaffected either way -- nothing there is wide enough to wrap. */
  flex-wrap: wrap;
  gap: 1rem;
  background: rgba(255, 255, 255, 0.92);
  /* Round 4c: the pill is now a faceted bar with angular points at both
     ends, echoing the low-poly geometry of the P mark.

     Technique: clip-path polygon (NOT an SVG overlay). The header is a
     fluid-width flex row whose height changes between the desktop bar
     and the mobile open-nav state, so an SVG overlay would need its
     own responsive sizing and would sit on top of the live nav links;
     a clip-path deforms with the box for free and never intercepts
     pointer events. The point length is a custom property, --facet, so
     the padding below and the backing plate above can both be reasoned
     about against the same number (now defined on .site-header so both
     that plate and this element see the same value via inheritance).

     `border` and `box-shadow` are both clipped away by clip-path, so
     the 1px hairline is dropped and the depth comes from
     `filter: drop-shadow(...)` instead -- drop-shadow is applied after
     the clip, so it follows the angular silhouette rather than the
     original rectangle the way box-shadow would.

     Round 5 item 2 -- the tinted ::before/::after facet planes that used
     to sit here (purple/green gradient wedges at 0.20 alpha) are gone
     entirely, per the brief: "no color shine at the ends." That also
     removes the reason this element needed filter to create its own
     stacking context for them -- filter is kept anyway, for the
     drop-shadow. Facet length itself went 28px -> 52px: at 28px, over a
     ~60px-tall bar, the point was real (it already converged to a
     single (0,50%) vertex, this was never actually rounded) but shallow
     enough to read as a soft nub rather than a genuine arrow -- the
     brief called this out directly ("still reads as rounded-pill-with-
     -a-point"). Lengthening the same triangle is the direct fix: same
     shape, no new geometry, just make it long enough to look
     unmistakably like a cut point instead of a corner nick. */
  border: 0;
  border-radius: 0;
  /* Round 8 item 3: chamfered octagon. Kept byte-identical to
     .site-header::before's polygon above -- that backing plate exists
     specifically to stop scrolled content bleeding through the clipped
     corners, so the two shapes have to match on ALL 4 corners now, not
     just the 2 that used to be pointed (verified after this change at
     1440px and 375px, scrolled so content passes under the bar). */
  clip-path: polygon(
    var(--chamfer) 0,
    calc(100% - var(--chamfer)) 0,
    100% var(--chamfer),
    100% calc(100% - var(--chamfer)),
    calc(100% - var(--chamfer)) 100%,
    var(--chamfer) 100%,
    0 calc(100% - var(--chamfer)),
    0 var(--chamfer)
  );
  filter: drop-shadow(0 5px 16px rgba(36, 42, 53, 0.10));
  /* Both ends now have a short 18px chamfer instead of one flat edge and
     one 52px point, so left and right padding go back to a matched
     pair -- 22px clears the chamfer's own horizontal run with a little
     to spare, same value item 1 already established for the flat left
     edge, now applied symmetrically since neither end is a deep point
     any more. */
  padding: 6px 22px 6px 22px;
  transition: background var(--dur-med) var(--ease), filter var(--dur-med) var(--ease),
              clip-path var(--dur-med) var(--ease);
}

body.past-home .site-header__inner {
  background: rgba(255, 255, 255, 0.62);
  backdrop-filter: blur(10px);
}

/* flex-shrink:0 matters: the global `img { max-width: 100% }` plus flex
   shrinking would squeeze the logo's width while its height stayed
   fixed, distorting the wordmark exactly the way the uncropped viewBox
   used to. Width must always be free to follow the aspect ratio. */
.site-logo { display: flex; align-items: center; flex: 0 0 auto; }
/* Round 7 item 2 -- deliberately REVERSES round 6 item 4, which hid the
   header wordmark while a stream panel was expanded via
   `body.stream-open-lock .site-logo { opacity: 0 }`. That rule is gone:
   the header logo now renders identically in the expanded stream view
   and on Home, same 30px height, same position, no state-dependent
   styling of any kind. The opacity transition that served only that rule
   went with it. Nothing replaces this -- the absence of a rule IS the
   feature, so don't reintroduce a stream-open logo override without
   checking this note first. */
/* Halved from round 2's 60px (v2 round 3 item 4); the pill's own height
   comes down with it via the reduced padding below. Legible at this size
   only because the SVG's viewBox is cropped to the artwork -- see the
   "Logo asset" note in README.md. */
.site-logo img { height: 24px; width: auto; max-width: none; }
/* Mobile header (hamburger layout): a further step down, 30px -> 21px. */
@media (max-width: 760px) {
  .site-logo img { height: 21px; }
}

.site-nav__list {
  list-style: none;
  display: flex;
  gap: clamp(0.9rem, 2vw, 1.6rem);
  margin: 0;
  padding: 0;
  font-size: 0.92rem;
  font-weight: 600;
}
.site-nav__list a {
  padding: 0.4em 0.2em;
  color: var(--ink-soft);
  border-bottom: 2px solid transparent;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.site-nav__list a[aria-current="true"],
.site-nav__list a:hover { color: var(--ink); border-color: var(--agri-main); }

/* Stream-coloured nav words (v2 round 2 item 2). Uses the main ramp
   stops, not the tint/wash stops the feedback pointed at: tint on the
   near-white header pill lands around 1.9:1 contrast, which is
   unreadable for text at this size. These read clearly as the brand
   purple/green and stay legible. */
.site-nav__list a.nav--fintech { color: var(--fintech-main); }
.site-nav__list a.nav--agri { color: var(--agri-main); }
.site-nav__list a.nav--fintech[aria-current="true"],
.site-nav__list a.nav--fintech:hover { color: var(--fintech-deep); border-color: var(--fintech-main); }
.site-nav__list a.nav--agri[aria-current="true"],
.site-nav__list a.nav--agri:hover { color: var(--agri-deep); border-color: var(--agri-main); }

.site-header__cta {
  background: var(--ink);
  color: #fff;
  padding: 0.6em 1.2em;
  border-radius: var(--radius-pill);
  font-size: 0.88rem;
  font-weight: 700;
  white-space: nowrap;
}

.nav-toggle {
  display: none;
  padding: 0.4em;
  border-radius: var(--radius-sm);
  position: relative;
}
/* Round 38 item 5: the burger's real box measured 33x15 -- under even
   WCAG 2.5.8's 24x24 minimum, on the one control that IS mobile
   navigation. Enlarged with an invisible absolutely-positioned overlay
   rather than by padding the button out to 44x44: the header row is
   58px tall because a 30px logo + 12px padding + the wrapped nav's 16px
   row-gap add up to that, so a 44px-tall button would push the whole
   pill to 72px. This keeps the bar exactly as it is and only grows what
   the finger has to hit. */
.nav-toggle::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
.nav-toggle span,
.nav-toggle span::before,
.nav-toggle span::after {
  content: "";
  display: block;
  width: 20px;
  height: 2px;
  background: var(--ink);
  position: relative;
  transition: transform var(--dur-fast) var(--ease);
}
.nav-toggle span::before { position: absolute; top: -6px; }
.nav-toggle span::after { position: absolute; top: 6px; }

@media (max-width: 760px) {
  /* At 30px tall the logo is ~111px wide, which fits alongside the
     toggle and Contact button even at 320px, so no phone-specific
     size override is needed any more (it had one when the logo was
     60px/219px wide). */
  /* Round 38 item 3: the mobile header used to run three items across a
     space-between row -- logo, toggle, Contact -- which parked the burger
     dead centre, reading as a mistake rather than a control. The Contact
     button is the one to drop: it is redundant on mobile three times over
     (the fixed .float-contact pill is on screen at all times, the nav's
     own Contact link is one tap into the menu, and the whole page ends in
     the Contact section). With it gone the row is logo + toggle only, so
     space-between gives the conventional [logo] .......... [burger].
     display:none rather than an order change -- it has to leave the flex
     flow entirely, or space-between still has three items to distribute. */
  .nav-toggle { display: block; order: 3; }
  .site-header__cta { display: none; }
  /* Round 8 item 3: no mobile --chamfer override any more. The old
     mobile --facet override existed because a point spanning all the
     way to the bar's own centre ate a much larger share of a narrow
     bar's width -- a fixed 18px CORNER cut doesn't have that problem
     (it's the same 18px regardless of bar width, and 18px against a
     ~320px-wide mobile bar is still a small fraction). Verified live:
     bar height is unchanged at mobile (66.7px, identical to desktop),
     so the same chamfer that reads correctly at desktop reads
     correctly here too. */
  /* Round 40 item 1 -- the logo and burger were not vertically centred in
     the pill, and the pill was taller than it needed to be. Both come from
     the same thing, and it is not align-items (that is already `center`).

     This container is flex-wrap: wrap with `gap: 1rem`, and `gap` sets BOTH
     axes. .site-nav below is order:4 / flex-basis:100% / max-height:0, so it
     wraps onto a second flex line of zero height -- but the 16px ROW-gap
     between line 1 and that empty line 2 still takes up space inside the
     pill. Measured: box 58px tall, logo and burger both centred at y=35
     against a box centre of y=43, i.e. 8px high, exactly half the phantom
     gap.

     row-gap: 0 removes it (the 16px column-gap between logo and burger is
     kept), which both centres the content and takes the box to 42px. The
     brief also asked for roughly 20% off the height, so padding goes 6 -> 8
     and it lands at 46px: 58 -> 46 is 20.7%, and the content is centred by
     construction rather than by a magic offset. Spacing for the OPEN menu
     is unaffected -- that comes from .site-nav__list's own padding below,
     not from this gap. */
  .site-header__inner {
    padding: 8px 16px;
    row-gap: 0;
  }
  /* When the nav expands the pill stops being a bar and becomes a tall
     panel. The apex is at 50% height, so on a ~250px-tall open panel the
     "points" would stretch into a giant diamond and the diagonals would
     cut across the nav links. Drop back to the plain rectangle while
     open -- the facets are a bar-shape flourish and there is no bar. */
  .site-header__inner:has(.site-nav.is-open) {
    clip-path: none;
    border-radius: var(--radius-md);
  }
  .site-nav {
    order: 4;
    flex-basis: 100%;
    max-height: 0;
    overflow: hidden;
    transition: max-height var(--dur-med) var(--ease);
  }
  .site-nav.is-open { max-height: 240px; }
  .site-nav__list {
    flex-direction: column;
    gap: 0;
    padding: 10px 4px 6px;
  }
  .site-nav__list a { display: block; padding: 0.6em 0.4em; }
}

/* =============================================================
   Home
   ============================================================= */

.hero {
  /* Halved (v2 round 2 item 3). Note most of the gap the feedback
     flagged wasn't padding at all -- it was empty canvas baked into the
     logo SVG's uncropped viewBox, fixed in item 0. */
  padding-top: clamp(28px, 5vw, 55px);
  padding-bottom: clamp(64px, 9vw, 110px);
  text-align: center;
}

/* Sized to match the header's width per v2 round-1 feedback -- was
   reading small/isolated at the old 560px cap. */
/* Round 6 item 3: volumetric/animated wordmark. Deliberately restrained
   -- this is the single largest element on the page, so every effect
   here is a small nudge, not a showpiece: two stacked drop-shadows for
   depth (a tight low one plus a soft wide one, the way a raised object
   casts both a contact shadow and an ambient one), a slow one-shot sheen
   that sweeps across then pauses (not a constant loop -- "not too much"
   ruled that out on an early pass), and a couple of degrees of tilt on
   hover/scroll, nowhere near a full 3D showcase. Screenshotted at each
   step before landing here, per the brief's own instruction to check
   given the visual risk of overdoing it on an element this size.

   The margin/width/max-width that used to live on .hero__logo directly
   move to the wrapper (.hero__logo is now the box that fills it, not
   the box that's centred) -- .hero__logo-wrap is the new layout anchor,
   .hero__logo is 100% of it. */
.hero__logo-wrap {
  position: relative;
  margin-inline: auto;
  width: 100%;
  max-width: min(var(--header-max), 88vw);
  margin-bottom: 1.6rem;
}
.hero__logo-wrap .hero__logo {
  width: 100%;
  margin: 0;
  display: block;
  filter: drop-shadow(0 2px 3px rgba(23, 25, 31, 0.10))
          drop-shadow(0 20px 36px rgba(23, 25, 31, 0.14));
}
@media (prefers-reduced-motion: no-preference) {
  .hero__logo-wrap .hero__logo {
    transform: perspective(1400px) rotateX(var(--tilt-x, 0deg));
    transition: transform 600ms var(--ease);
  }
  .hero__logo-wrap:hover .hero__logo {
    transform: perspective(1400px) rotateX(calc(var(--tilt-x, 0deg) - 1.5deg)) rotateY(2.5deg);
  }
}

/* Sheen: a diagonal light band swept across the wordmark by animating
   background-position on a gradient the size of the box. mix-blend-mode:
   soft-light (not a plain semi-transparent overlay) is the reason this
   doesn't need a pixel-accurate mask of the letterforms -- soft-light
   barely affects the near-white page colour peeking through the logo
   SVG's transparent canvas, but visibly lifts the darker facet/letter
   fills it passes over, so it reads as light crossing the wordmark
   itself without needing to clip to its exact silhouette. One sweep
   every 9s, holding at rest for most of that (0%-35% is the actual
   motion, 35%-100% is a pause) -- a constantly-looping shine read as
   too much on an early pass at this size; a single slow glint that
   mostly sits still reads as ambient rather than decorative. */
.hero__logo-wrap::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    115deg,
    transparent 30%,
    rgba(255, 255, 255, 0.6) 48%,
    rgba(255, 255, 255, 0.6) 52%,
    transparent 70%
  );
  background-size: 260% 100%;
  background-repeat: no-repeat;
  mix-blend-mode: soft-light;
  opacity: 0;
  /* First pass left this unmasked -- soft-light still blends against
     whatever's behind the SVG's own transparent canvas (the page
     texture), so the sheen showed as a visible plain rectangle bleeding
     past the letterforms onto the background, not "light crossing the
     wordmark." Masked to the same logo file so the sheen only paints
     where the wordmark itself has ink. mask-mode: alpha (not the
     luminance default) is the deliberate part: this logo's own facets
     are already dark-to-light shaded, and luminance masking would have
     made the sheen fade in and out with the artwork's OWN shading on
     top of its animated sweep -- alpha mode only cares whether a pixel
     is part of the wordmark at all, so the mask is a clean silhouette
     regardless of what's drawn inside it. */
  -webkit-mask-image: url("../assets/prizmar-logo.svg");
  mask-image: url("../assets/prizmar-logo.svg");
  -webkit-mask-mode: alpha;
  mask-mode: alpha;
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}
@media (prefers-reduced-motion: no-preference) {
  .hero__logo-wrap::after {
    opacity: 1;
    animation: logo-sheen 9s ease-in-out infinite;
  }
}
@keyframes logo-sheen {
  0%   { background-position: 160% 0; }
  35%  { background-position: -60% 0; }
  100% { background-position: -60% 0; }
}
/* Bold, ~1.5x the previous size (v2 round 2 item 3). */
h1.hero__lede {
  /* was a <p>: undo the base h1 look (display face, expanded, tight
     tracking, 0.6em margin) so it is pixel-identical to before */
  font-family: inherit;
  font-stretch: inherit;
  letter-spacing: inherit;
  margin-block: 0 1em;
}
.hero__lede {
  max-width: min(var(--header-max), 88vw);
  margin-inline: auto;
  font-size: clamp(1.8rem, 3vw, 2.18rem);
  font-weight: 700;
  line-height: 1.25;
  color: var(--ink);
}

.stream-cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.25rem;
  max-width: 860px;
  margin: 2.6rem auto 0;
  text-align: left;
}
@media (max-width: 640px) { .stream-cards { grid-template-columns: 1fr; } }

.stream-card {
  display: block;
  padding: 1.6rem 1.5rem;
  border-radius: var(--radius-md);
  border: 1px solid var(--hairline);
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}
/* Round 6 item 2: neutral fill, not stream-coloured. The tint/wash
   gradient (v2 round 2 item 3) is gone -- these two cards now share one
   plain parent-neutral fill, with only the heading text staying in
   stream colour (below). Uses --surface-warm (#EBE8E6): the one
   "parent" neutral defined in tokens.css that wasn't already spoken for
   by another component (--surface is Our Values' card fill, --surface-alt
   is stat tiles/inputs) -- so these cards get their own identity instead
   of colliding with an existing role. */
.stream-card--fintech,
.stream-card--agri {
  background: var(--surface-warm);
  border-color: var(--hairline);
}
.stream-card:hover { transform: translateY(-3px); box-shadow: 0 14px 34px rgba(36, 42, 53, 0.1); }

/* The round-33 pulsing glow has never lived here on the Home cards --
   round 33 tried it, round 34 moved it to the expanded panels, round 36
   moved it to the CLOSED Streams-section panels (.stream-panel--fintech /
   --agri :not(.is-expanded), near .stream-panel below). These small Home
   cards stay plain and static. */
.stream-card__title {
  /* was an h3: base heading look + the card's own bottom margin */
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.05;
  font-size: clamp(1.25rem, 2vw, 1.5rem);
  margin: 0 0 0.4rem;
}
/* Round 7 item 5: matched to the header nav links exactly. These were
   --fintech-deep (#6B5B8F) / --agri-deep (#25453A), which are genuinely
   different colours from the nav's --fintech-main (#815BAE) /
   --agri-main (#438234) -- the Agri pair especially (a near-black
   forest green vs the brand green). Now the same two tokens the nav
   uses, so "Prizmar Fintech"/"Prizmar Agri" on the Home cards and
   "Fintech"/"Agri" in the header are the same colour by construction,
   not by two values that happen to look close. */
.stream-card--fintech .stream-card__title { color: var(--fintech-main); }
.stream-card--agri .stream-card__title { color: var(--agri-main); }
.stream-card p { color: var(--ink-soft); }
.stream-card p { margin: 0; font-size: 0.95rem; }

/* =============================================================
   About
   ============================================================= */

/* Round 21: About only goes dark (obsidian), Contact stays tan.

   Both sections carry .band--alt and share --band-alt-bg, so this
   overrides at the #about.band--alt level (id + class beats the shared
   class rule on specificity, independent of source order) rather than
   retinting the token -- Contact still renders from --band-alt-bg
   untouched.

   Text: only the four non-card elements in this section inherit a dark
   ink colour and so need flipping -- the eyebrow, the h2 tagline, and
   both h3s (.section-statement "Two worlds sharing an engine" and "Our
   Approach"). Everything else in here is inside a founder-card or a
   value-card, which keep their own light surfaces and are deliberately
   out of scope for this round -- hence `#about .eyebrow/h2/h3` and not
   a blanket `#about { color: ... }`, which would bleed into card text
   the moment a card stopped setting its own colour. */
#about.band--alt { background: var(--ink-deep); }
#about .eyebrow,
#about h2,
#about h3 { color: var(--ink-on-dark); }

.founders {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.75rem;
  /* Round 20: was 2.5rem, under the "Who we are" h3 that has since been
     removed -- bumped so the tagline above doesn't read as running
     straight into the grid now that the heading (and its margins) are
     gone. */
  margin-top: 3.5rem;
  align-items: stretch;
}
@media (max-width: 760px) { .founders { grid-template-columns: 1fr; } }

.founder-card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 1.8rem;
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}
/* Left-photo / right-content layout, sized per
   founder-card-reference.png (v2 round 2 item 4): a large rounded-square
   photo block filling the left ~42% of the card, with name, role and a
   2x2 stat grid stacked to its right. */
.founder-card__top { row-gap: 1.25rem; }
/* Round 10 item 4: the avatar fills its own grid column, so it must NOT
   also carry width: 42% -- a percentage width on a grid item resolves
   against its grid area, which is already the 42% column, and would
   render the photo at 42% of 42%. The column definition is the single
   place the 42% lives now. */
.founder-card__avatar {
  width: 100%;
  aspect-ratio: 4 / 5;
  border-radius: 26px;
  overflow: hidden;
  background: var(--surface-alt);
}
.founder-card__avatar img { width: 100%; height: 100%; object-fit: cover; }
.founder-card__intro { flex: 1; display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; }
/* Name matches the stat numbers' size/weight, per the reference. */
.founder-card__name {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.35rem;
  font-weight: 800;
  line-height: 1.15;
  color: var(--ink);
}
.founder-card__role { margin: 0 0 0.2rem; font-size: 0.85rem; color: var(--ink-soft); }
/* The <=480px stacking override lives further down, immediately after
   the grid definition it has to beat -- see "Round 10 item 4" there.
   Declaring it here would lose on source order despite the media query,
   since the two selectors have equal specificity. */

/* Round 20: single column, full-width stacked tiles (was a 2x2 grid) --
   dropped from 4 tiles to 3 per founder at the same time, per the
   round-20 reference. The avatar's height still stretches to match this
   column's natural height (see .founder-card__top / .founder-card__avatar
   below, unchanged from round 14) -- with 3 shorter tiles instead of 4
   the intro column is shorter, so the avatar comes out closer to square,
   matching the reference. */
.stat-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 0.65rem;
}
/* Shared min-height across all 6 stat tiles (both founders), same
   flex-centering approach as before round 20 -- just a smaller floor,
   since a full-width single column gives each label much more room per
   line than the old 2-column grid did, so wrapping is rare now. */
.stat {
  background: var(--surface-alt);
  border-radius: var(--radius-sm);
  padding: 0.8rem 1rem;
  text-align: left;
  min-height: 3.5rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Round 18 items 2 + 5: was 1.35rem / 800 -- fine for a single count-up
   number, but far too large and heavy once the tiles carry short
   phrases (both founders now do). Brought down to the site's card-
   heading convention (cf. .value-card h4, 1.02rem) and to weight 700,
   with a real line-height for the phrases that wrap. One shared rule --
   both founder cards get the identical treatment. */
.stat__value {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.02rem;
  line-height: 1.3;
  color: var(--ink);
  display: block;
}
.stat__label { font-size: 0.74rem; color: var(--ink-soft); line-height: 1.3; }

/* Round 18 item 3: quote set larger than the rest of the card text
   (bullets 0.9rem, role 0.85rem), with large bold quotation marks before
   and after. The marks are skipped while a quote is still a
   [PLACEHOLDER] so they don't wrap the amber placeholder box (neither
   card currently ships one, but the guard stays -- cheap insurance
   against a future placeholder quote). */
.founder-card__quote {
  border-left: 3px solid var(--agri-main);
  padding-left: 1.2rem;
  font-style: italic;
  color: var(--ink);
  font-size: 1.15rem;
  line-height: 1.45;
  margin-top: 0;
}
.founder-card__quote:not(:has(.placeholder))::before,
.founder-card__quote:not(:has(.placeholder))::after {
  font-family: var(--font-display);
  font-weight: 800;
  font-style: normal;
  font-size: 1.9rem;
  line-height: 0;
  color: var(--agri-main);
}
.founder-card__quote:not(:has(.placeholder))::before {
  content: "\201C";
  margin-right: 0.15rem;
  vertical-align: -0.05em;
}
.founder-card__quote:not(:has(.placeholder))::after {
  content: "\201D";
  margin-left: 0.1rem;
  vertical-align: -0.3em;
}
/* Round 20 follow-up 3: Yevhen's card is Fintech, not Agri -- override
   the border-left and both quote-mark glyphs to the Fintech purple for
   his card specifically. Oleksii's card is untouched and keeps the
   base rules' green (it IS the Agri card, so var(--agri-main) is
   already correct there -- no matching .founder-card--agri override
   needed). */
.founder-card--fintech .founder-card__quote { border-left-color: var(--fintech-main); }
.founder-card--fintech .founder-card__quote:not(:has(.placeholder))::before,
.founder-card--fintech .founder-card__quote:not(:has(.placeholder))::after {
  color: var(--fintech-main);
}

/* -------------------------------------------------------------
   Round 13 item 3: closed founder card, NO LONGER CLIPPED
   ------------------------------------------------------------- */

/* Rounds 9-11 clipped the closed card's top block to the photo's own
   height (cutting into or hiding the second stat row) and chased three
   rounds of regressions doing it: round 10 item 4's blank-section bug
   (the whole intro column disappearing if aspect-ratio failed to
   resolve), round 10 item 4's rebuild to a grid-based clip, round 11
   item 3(b)'s shared min-height. Round 13's reference
   (assets/refs/round13-founder-cards-closed-reference.png) supersedes
   all of it: the closed card shows photo, name, role, the COMPLETE 2x2
   stat grid, the quote and "Find out more" -- nothing hidden. Per
   instruction, this round does not try to preserve or repair the old
   clip mechanism -- it is removed outright, not reworked.

   .founder-card__top is still a grid (photo-left / content-right, same
   42%/1fr column split as before), but it no longer forces a height:
   no aspect-ratio on the block, no fixed-height avatar, no overflow:
   hidden on the intro. The row simply sizes to whichever column is
   taller -- in practice the intro (name + role + full stat grid), since
   that reliably runs taller than a 4/5 photo at 42% width -- and the
   photo sits at its own natural aspect-ratio, top-aligned, with
   whatever space is left below it. Nothing to clip means nothing that
   can silently fail to clip; the round 10 blank-section failure mode
   (a single point of failure hinging on one aspect-ratio resolving) is
   structurally gone, not just avoided. */
.founder-card__top {
  display: grid;
  grid-template-columns: 42% 1fr;
  column-gap: 3%;
  /* Round 14 item 2: was `start`, which let the photo size itself purely
     from its own aspect-ratio and sit at the top of the row, leaving a
     gap between the photo's bottom edge and the quote below it (measured
     126px at 1440px, both founders). `stretch` makes the photo fill the
     grid row instead, and the row is sized by the taller column -- the
     intro, whose last child IS the stat-row. So the photo's bottom edge
     lands exactly level with the bottom of the last stat tile, by
     construction rather than by a tuned number, and it stays level for
     both founders regardless of how their label text wraps. Round 20
     dropped the stat-row from a 2x2 grid to 3 stacked full-width tiles
     (see .stat-row), which shortens the intro column and, through this
     same stretch mechanic, brings the photo closer to square -- matching
     the round-20 reference without any change needed here. */
  align-items: stretch;
}

/* The photo can only stretch if its height is not already pinned by its
   own aspect-ratio, so that is released here and the img fills whatever
   height the row resolves to (object-fit: cover on the img keeps the
   placeholder from distorting -- it crops instead).

   Kept out of the <=480px stacked layout: there the photo is its own
   grid row with nothing beside it to match, so it has no row height to
   stretch to and would collapse to the img's intrinsic 128px. The base
   rule's aspect-ratio: 4/5 is what sizes it there, and still does. */
@media (min-width: 481px) {
  .founder-card__avatar {
    aspect-ratio: auto;
    height: 100%;
  }
}

/* <=480px: the top block stacks (photo above the text) instead of
   sitting side by side -- unrelated to the clip removal above, this
   was already the narrow-width layout and stays. */
@media (max-width: 480px) {
  .founder-card__top { grid-template-columns: 1fr; row-gap: 1.1rem; }
  .founder-card__avatar { width: 60%; max-width: 190px; }
}

/* Round 20: cards are no longer accordion items -- "Find out more" is a
   real link to LinkedIn now, not an expand/collapse trigger, so it's
   styled as plain text/link (no pill, no border, no "+" glyph) rather
   than reusing the site's accordion-trigger button contract. */
.founder-card__more {
  align-self: flex-start;
  /* Round 20 follow-up: the two cards are already stretched to equal
     height by .founders, but the card is a flex column and the quote
     above this link wraps to a different number of lines per founder --
     so without this the link sat at a different height in each card
     (measured 38px apart at 1280px). `auto` pushes it to the bottom of
     whichever card it's in, so both links land level by construction,
     not by matching the quotes' lengths. */
  margin-top: auto;
  /* Left inset lines the label up with the quote's text above it (the
     quote's 3px rule plus its 0.9rem padding). */
  padding-left: calc(3px + 0.9rem);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.05rem;
  color: var(--ink);
}
.founder-card__more:hover,
.founder-card__more:focus-visible {
  text-decoration: underline;
}

.value-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1.1rem;
  margin-top: 2rem;
}
.value-card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 1.4rem 1.3rem;
}
.value-card__num {
  font-family: var(--font-mono);
  color: var(--tan-deep);
  font-size: 0.9rem;
  display: block;
  margin-bottom: 0.6rem;
}
.value-card :is(h4:not(:where(.was-h5)), :where(.was-h4)) { margin-bottom: 0.4rem; font-size: 1.02rem; }
.value-card p { font-size: 0.88rem; margin: 0; }

/* About's "Our Values" grid only (v2 round-1 feedback) -- scoped to
   .our-values-grid rather than the base .value-grid/.value-card
   rules, which the Agri > Intelligence "The what" cards also reuse
   and which stays out of scope for this round. Extra gap gives the
   scaled-up hover state room; z-index lets it sit above neighbours
   rather than colliding with them. */
/* Round 20 follow-up: this grid's cards grown ~20-25% -- padding, type
   and gaps together, so it reads as more room rather than just bigger
   text. All of it stays scoped to .our-values-grid; the Agri >
   Intelligence "The what" cards reuse the bare .value-card rules above
   and are deliberately untouched.

   The column floor goes 200px -> 230px. That keeps the 4-up row at
   desktop (4x230 + 3x2.4rem gap = 1035px, inside this grid's measured
   1068px) while making the cards wrap to 3-up and 2-up earlier than
   before on tablet widths, where 200px columns had started to crowd.
   Deliberately NOT pushed far enough to break the desktop row into 2x2:
   at 4 cards that would either orphan one card on its own row (3-up) or,
   at 2-up, blow the 1.6x hover scale well past the shell's edges. */
.our-values-grid {
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 2.4rem;
  row-gap: 3.25rem;
}
.our-values-grid .value-card {
  position: relative;
  padding: 1.75rem 1.6rem;
  transition: transform var(--dur-med) var(--ease), box-shadow var(--dur-med) var(--ease);
}
.our-values-grid .value-card__num { font-size: 0.95rem; margin-bottom: 0.7rem; }
.our-values-grid .value-card :is(h4:not(:where(.was-h5)), :where(.was-h4)) { font-size: 1.15rem; margin-bottom: 0.55rem; }
.our-values-grid .value-card p { font-size: 1rem; line-height: 1.55; }
@media (prefers-reduced-motion: no-preference) {
  /* Round 31 item 1: scale cut roughly in half (1.6 -> 1.3, i.e. the
     "grows by" amount goes from 60% to 30%) -- same hover-expand effect,
     much subtler. Shadow softened to match; a shadow sized for the old
     1.6x throw read as too heavy at the new, smaller scale. */
  .our-values-grid .value-card:hover {
    transform: scale(1.3);
    z-index: 5;
    box-shadow: 0 14px 28px rgba(36, 42, 53, 0.18);
  }
}

/* Large, centered, hero-style statement -- used once, for the
   "Two worlds sharing an engine" heading (v2 round-1 feedback). Not
   a generic h3 override: scoped to this one heading. */
.section-statement {
  text-align: center;
  font-size: clamp(2rem, 4.2vw, 3rem);
  max-width: 20ch;
  margin-inline: auto;
  /* Round 20: moved from the end of the About section to sit between
     the founders grid and "Our Approach" -- this generous top margin is
     now the section break between the two, not a lead-in to Fintech/Agri.
     margin-bottom stays 0 so it collapses with "Our Approach"'s own
     margin-top:3rem (inline) rather than stacking on top of it. */
  margin-top: clamp(4rem, 8vw, 7rem);
  margin-bottom: 0;
}

/* =============================================================
   About > the "engine" figure: animated video (see .engine__video
   below); the old SVG figure and its label CSS are gone.
   ============================================================= */

.engine {
  width: 100%;
  margin: 2.2rem 0 0;
}


/* =============================================================
   Accordion -- one component, three uses
   (Fintech vs Agri, Services vs Intelligence, service vs case)
   ============================================================= */

.accordion-item {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface);
  overflow: hidden;
  transition: opacity var(--dur-med) var(--ease), transform var(--dur-med) var(--ease);
}
.accordion-item + .accordion-item { margin-top: 1rem; }

.accordion-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.3rem 1.5rem;
  text-align: left;
}
.accordion-trigger__label { display: flex; flex-direction: column; gap: 0.25rem; }
.accordion-trigger__title { font-family: var(--font-display); font-weight: 800; font-size: 1.05rem; }
.accordion-trigger__tagline { font-size: 0.84rem; color: var(--ink-soft); }
/* Round 31 item 3: this tagline is shared markup between the closed
   Home selector card and the expanded panel header (see the comment on
   the span itself in index.html) -- scoped to #agri-services only, and
   only while IT is expanded, so the closed-card teaser (here and on
   every other accordion-trigger: Fintech's 4 services, Agri
   Intelligence) is untouched. ID selector rather than a class, since
   nothing else needs this and a class would risk drifting onto another
   trigger later. */
#agri-services[data-expanded="true"] .accordion-trigger__tagline,
#fintech-services[data-expanded="true"] .accordion-trigger__tagline,
#agri-intelligence[data-expanded="true"] .accordion-trigger__tagline {
  display: none;
}
/* Agri > Intelligence's expanded view drops its tagline too (all widths),
   matching Services' expanded view above; closed card keeps it.
   Mobile only: with the tagline gone, the title's left edge is pulled in
   to line up with "About the tool" below it (trigger padding-left 12px =
   the body's mobile side padding, so both sit the same distance from the
   page edge), and the gap under the title grows ~55% (+0.5rem). */
@media (max-width: 760px) {
  #agri-intelligence[data-expanded="true"] .accordion-trigger {
    padding-left: 12px;
    padding-bottom: calc(1.3rem + 0.5rem);
  }
}

/* Round 7 item 9: Agri's two sub-stream triggers carry 3 bullets each
   instead of a prose tagline. Inherits the tagline's size/colour so the
   two trigger styles stay visually consistent -- only the layout
   differs. Markers are drawn with ::before rather than list-style
   because these are spans (see index.html for why they can't be a real
   <ul> inside a <button>). */
.accordion-trigger__bullets {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: 0.84rem;
  color: var(--ink-soft);
}
/* Round 8 item 4e: hide the 3 bullets once a sub-stream is expanded --
   they stay in the collapsed card view (item 9's own scope), this only
   fires while the SAME item's own data-expanded is true. display: none,
   deliberately not visibility/opacity: those would leave the bullets'
   own box in flow, an empty space exactly the size of 3 lines of text --
   which is precisely the "large blank band under Services" bug this
   item is fixing, not a different one. display:none removes the box
   from flow entirely, so the trigger's own height (title + padding
   only) shrinks around it. No extra gap-closing rule is needed beyond
   that: measured live before this change, the accordion-panel already
   starts at exactly the trigger's own bottom edge (0px gap between
   them) -- so a shorter trigger already pulls the panel content up to
   follow it, automatically. Scoped to the expanded Agri panel, matching
   where the sub-accordion's own click-to-expand is even reachable (the
   whole stream-body is pointer-events:none while the outer panel is
   closed, so a sub-stream can never actually BE expanded in the closed
   card view -- nothing to scope against there). */
#agri.is-expanded .sub-accordion > .accordion-item[data-expanded="true"] > .accordion-trigger .accordion-trigger__bullets {
  display: none;
}
.accordion-trigger__bullets > span {
  display: block;
  position: relative;
  padding-left: 1.05em;
}
.accordion-trigger__bullets > span::before {
  content: "";
  position: absolute;
  left: 0;
  /* 0.62em centres a 5px dot on the first line's optical middle at this
     font-size without depending on line-height inheritance. */
  top: 0.62em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.55;
}
.accordion-trigger__icon {
  flex: none;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--hairline);
  display: grid;
  place-items: center;
  transition: transform var(--dur-med) var(--ease);
}
.accordion-trigger__icon::before { content: "+"; font-size: 1rem; line-height: 1; }
.accordion-trigger[aria-expanded="true"] .accordion-trigger__icon { transform: rotate(135deg); }

.accordion-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-med) var(--ease);
}
.accordion-panel > .accordion-panel__inner {
  overflow: hidden;
  min-height: 0;
  /* Round 26: the companion to min-height:0 for this grid-rows accordion
     technique. Without it the item keeps min-width:auto (= its content's
     min-content width), which -- for a subtree containing its own
     horizontal scroller, i.e. the Agri > Services case carousel -- left
     that inner scroller's scrollLeft permanently clamped to 0: arrows,
     dots and the "Explore use case" links all computed the right target
     offset but the track would not move. Pre-existing since the carousel
     landed; surfaced now that round 26 leans on goTo(). */
  min-width: 0;
}
/* Direct-child combinator only -- an item's expanded state must not
   leak into a nested item's own panel several levels down (e.g. a
   service accordion inside an open Fintech/Agri panel must stay
   collapsed until its own trigger is clicked). */
.accordion-item[data-expanded="true"] > .accordion-panel { grid-template-rows: 1fr; }
.accordion-panel__body {
  padding: 0 1.5rem 1.5rem;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity var(--dur-med) var(--ease), transform var(--dur-med) var(--ease);
}
.accordion-item[data-expanded="true"] > .accordion-panel > .accordion-panel__inner > .accordion-panel__body {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .accordion-panel,
  .accordion-panel__body,
  .accordion-trigger__icon { transition: none; }
}

/* Sibling de-emphasis: dim, never hide */
.accordion-group[data-open] > .accordion-item[data-expanded="false"] {
  opacity: 0.55;
  transform: scale(0.99);
}

/* Nested service -> case accordion gets a slightly quieter card */
.service-item .accordion-panel__body { padding-top: 0.4rem; }
.case-study {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem 1.5rem;
}
@media (max-width: 640px) { .case-study { grid-template-columns: 1fr; } }
.case-study dt {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 0.3rem;
}
.case-study dd { margin: 0 0 1rem; font-size: 0.9rem; color: var(--ink); }

/* =============================================================
   Stream panels -- Fintech / Agri
   ============================================================= */

/* =============================================================
   Fintech / Agri stream cards (v2 round 3 item 6)
   Two side-by-side cards, both fully visible at rest. The round-2
   outer accordion (both closed on load, expand-to-full-width, mutual
   exclusivity) has been deleted -- these are no longer accordion
   items, and the container is no longer an .accordion-group. Only the
   inner accordions remain (Fintech service rows, Agri sub-stream rows).
   ============================================================= */
.stream-columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem;
  /* stretch (not start): the reference shows both cards the same
     height, with Agri carrying the extra whitespace. */
  align-items: stretch;
}
@media (max-width: 760px) {
  .stream-columns { grid-template-columns: 1fr; }
}

.stream-panel {
  scroll-margin-top: 100px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: clamp(1.4rem, 2.4vw, 2rem);
  display: flex;
  flex-direction: column;
  position: relative;
}

/* =============================================================
   Round 4d -- stream card -> full-viewport panel

   The card and the "modal" are THE SAME ELEMENT: opening promotes the
   existing <article> to position:fixed rather than cloning its markup
   into a dialog. Cloning would have duplicated every id inside it
   (svc-trms, agri-services-panel, ...) for as long as the overlay was
   open, and moving only the body out would have emptied the card
   behind it. Promoting in place keeps one copy of the content, so the
   nested service/case accordions keep working inside the opened panel
   with the listeners they were already bound with at load.
   ============================================================= */

/* Expanded-only content: pricing and the contact CTAs. Present in the
   markup at all times, not rendered until the panel is opened.

   Round 4 item D fix, a real bug (not just a re-check): this used to be
   two rules -- `.stream-expanded-only{display:none}` and
   `.stream-panel.is-expanded .stream-expanded-only{display:block}` --
   which assumed "block" was the right shown-state value for every
   expanded-only element. It isn't: .stream-contact (the "Get in touch"
   CTA blocks) sets its own `display:flex` for its row layout, at EQUAL
   specificity (one class each) and LATER in this file's source order,
   so cascade tie-breaking let .stream-contact's flex win over
   .stream-expanded-only's none outright -- confirmed live via
   getComputedStyle: the closed Fintech card's CTA was `display:flex`
   and had a non-null offsetParent, i.e. genuinely visible while closed,
   contradicting what a shallower check (just confirming the class
   string was present) had concluded. The forced "block" on open was a
   second, smaller version of the same mistake -- it silently stacked
   the CTA instead of its own intended flex row.

   Fixed by only ever forcing a value in the HIDE direction, and only
   while the panel is closed -- the moment .is-expanded is added this
   selector stops matching at all, so each element falls back to
   whatever display IT defines (flex for .stream-contact, block for
   .info-block), instead of this rule guessing one value for all of
   them. !important is deliberate here: this is a hide-utility whose
   entire job is to win regardless of what any given expanded-only
   element sets for itself, the same reasoning most CSS toolkits use
   for a `.d-none`/`hidden` utility class. */
.stream-panel:not(.is-expanded) .stream-expanded-only { display: none !important; }

/* Whole-card click target, closed state only. */
.stream-open {
  position: absolute;
  inset: 0;
  z-index: 3;
  border: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  border-radius: inherit;
}
.stream-open:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.stream-panel.is-expanded .stream-open { display: none; }

/* Closed cards read as one control, so nothing inside them should look
   or behave independently clickable. Keyboard reachability of the inner
   triggers is removed in JS (tabindex -1) rather than with `inert`, so
   the service names stay readable to a screen reader. */
.stream-panel:not(.is-expanded) .stream-body { pointer-events: none; }
.stream-panel:not(.is-expanded) { cursor: pointer; }
@media (prefers-reduced-motion: no-preference) {
  .stream-panel:not(.is-expanded) {
    transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
  }
  .stream-panel:not(.is-expanded):hover {
    transform: translateY(-3px);
    box-shadow: 0 10px 28px rgba(36, 42, 53, 0.10);
  }
}

.stream-close {
  position: absolute;
  /* Round 4 item E: the header now sits above the panel, so the close
     button (and the panel's own top padding, below) is pushed down to
     clear it -- --stream-header-clearance is the header's real measured
     height, set in JS at open time. The 90px fallback is only ever used
     if that JS never ran (e.g. this rule inspected before first open). */
  top: calc(var(--stream-header-clearance, 90px) + 14px);
  right: 14px;
  z-index: 4;
  width: 40px;
  height: 40px;
  display: none;
  place-items: center;
  font-size: 1.6rem;
  line-height: 1;
  background: var(--surface-alt);
  color: var(--ink);
  border: 1px solid var(--hairline);
  border-radius: 50%;
  cursor: pointer;
}
.stream-panel.is-expanded .stream-close { display: grid; }
.stream-close:hover { background: var(--ink); color: #fff; }

.stream-panel.is-expanded {
  position: fixed;
  inset: 0;
  z-index: 200;
  margin: 0;
  max-width: none;
  border: 0;
  border-radius: 0;
  overflow-y: auto;
  /* Keeps a flick at the end of the panel's scroll from chaining to the
     page behind it, without the page needing a scroll lock of its own. */
  overscroll-behavior: contain;
  /* Round 4 item E: top padding cleared to sit below the now-visible
     header (see .stream-close above for what --stream-header-clearance
     is) instead of starting at the true viewport top and running under
     it. Right/bottom/left padding unchanged. */
  padding: calc(var(--stream-header-clearance, 90px) + clamp(1.6rem, 4vw, 3rem))
           clamp(1.2rem, 5vw, 4rem) clamp(3rem, 6vw, 5rem);
}
/* Round 33's pulsing --glow, in its final home: the two CLOSED
   Streams-section panels (this whole side-by-side pair, per the round-36
   screenshot). Round 33 first tried the small Home cards, round 34 the
   fixed full-viewport expanded overlay (where .site-header clipped its top
   edge and a full-screen ring read faint) -- round 36 landed it here.

   Scoped to :not(.is-expanded) so it applies only while the panel is a
   card in the grid: the moment it promotes to the fixed overlay the
   selector stops matching and the animation is gone, so an opened panel
   carries no glow. Outset box-shadow (not the overlay attempt's inset):
   these sit in normal flow with the 1.5rem .stream-columns gap around
   them, room for the ring to draw into. The two pulse in antiphase
   (--agri offset half a cycle) so the section never has both lit at once.
   Same two --glow colour values used everywhere else. */
.stream-panel--fintech:not(.is-expanded) { --glow: 129, 91, 174; }
.stream-panel--agri:not(.is-expanded) { --glow: 67, 130, 52; }
@media (prefers-reduced-motion: no-preference) {
  .stream-panel--fintech:not(.is-expanded),
  .stream-panel--agri:not(.is-expanded) {
    animation: stream-panel-glow 5s ease-in-out infinite;
  }
  .stream-panel--agri:not(.is-expanded) { animation-delay: 2.5s; }
}
@keyframes stream-panel-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--glow), 0); }
  50%      { box-shadow: 0 0 0 4px rgba(var(--glow), 0.22); }
}

/* Centre the column on wide screens so the text doesn't run the full
   width of a desktop monitor. */
.stream-panel.is-expanded > * { width: min(100%, 940px); margin-inline: auto; }
.stream-panel.is-expanded > .stream-close { width: 40px; margin-inline: 0; }
/* Round 14 item 3: the beams video is a direct child of the panel, so
   the 940px content cap above would apply to it too and render the
   background as a centred 940px column instead of full-bleed (measured:
   940px wide in a 1425px panel). Same exemption .stream-close already
   needs, for the same reason -- these are chrome, not content. Also
   pinned to the viewport rather than the panel's scroll content: the
   expanded panel is a fixed, scrollable box, so an absolutely positioned
   child would scroll away with the content and leave the lower part of a
   long panel with no beams. position: fixed keeps it put while the panel
   scrolls under it, and resolves against the viewport, which is exactly
   the expanded panel's own box (inset: 0). */
.stream-panel.is-expanded > .beams-bg {
  position: fixed;
  inset: 0;
  width: 100%;
  margin-inline: 0;
}

/* Round 8 item 4c: the stream-coloured prismatic background (round 6
   item 4) is removed -- both rules deleted outright, not overridden, so
   the expanded panel falls back to .stream-panel's own base rule,
   `background: var(--surface)` (plain white), same neutral surface the
   panel used before round 6 introduced the facet backgrounds. The two
   source files (assets/stream-facets-fintech.svg / -agri.svg) are left
   on disk, unreferenced, matching this project's established convention
   for a superseded asset (see logo-raster.png's history in README.md)
   -- easy to wire back in later if wanted, nothing to regenerate.
   Content cards (service rows, Agri's sub-stream cards) are untouched,
   per instruction -- their own neutral backgrounds (round 7 item 4d)
   were never dependent on this rule existing. */

.stream-backdrop {
  position: fixed;
  inset: 0;
  z-index: 199;
  background: rgba(23, 25, 31, 0.55);
  border: 0;
  padding: 0;
  cursor: pointer;
}
body.stream-open-lock { overflow: hidden; }

/* Card header: "PRIZMAR | <stream>" lockup, motto, brand-coloured rule */
.stream-head { margin-bottom: 1.1rem; }
.stream-lockup {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin: 0 0 0.9rem;
  flex-wrap: nowrap;
  min-width: 0;
}
/* Text for screen readers / crawlers only (e.g. the "Prizmar" prefix of the
   Fintech and Agri panel headings): out of flow, so it adds no flex item
   and no visible pixel. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
h2.stream-lockup {
  /* was a <p>: take back everything the base h2 rule would change */
  font-family: inherit;
  font-stretch: inherit;
  font-weight: inherit;
  font-size: inherit;
  line-height: inherit;
  letter-spacing: inherit;
  color: var(--ink-soft);
  max-width: 62ch;
}
.stream-lockup__logo {
  height: clamp(24px, 2.4vw, 32px);
  width: auto;
  max-width: none;
  flex: 0 0 auto;
}
.stream-lockup__sep {
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  font-weight: 300;
  line-height: 1;
  color: var(--hairline);
  color: rgba(36, 42, 53, 0.35);
}
/* Round 4 item C. The brief asked to match this text's size to
   getComputedStyle's font-size on "RIZMAR" in the wordmark -- checked
   first, and that literally doesn't exist: every "PRIZMAR" on this site
   (hero, header, this same lockup) is one <img> pointing at
   assets/prizmar-logo.svg, with "RIZMAR" baked in as vector letterforms,
   not DOM text. There is no element to read a computed font-size off of.

   What this does instead, to stay in the spirit of "measured, not
   eyeballed": read the wordmark's own actual cap-height and solve for
   the Archivo font-size that reproduces it exactly, using the SAME logo
   <img> sitting right next to this text (.stream-lockup__logo).
     - The "RIZMAR" group's bbox inside prizmar-logo.svg (viewBox
       "50 246 1352 366") is x=326.4 y=225.1 w=1072.5 h=174.9 -- gotten
       via getBBox() on the live SVG DOM, not measured off a picture.
       174.9/366 = 0.4779 of the logo's own displayed height is real
       letter cap-height (R/I/Z/M/A/R has no descenders, so bbox height
       IS cap-height here, no descender allowance needed).
     - Archivo 800 expanded's own cap-height-to-font-size ratio was
       measured with Canvas2D (ctx.font + ctx.fontStretch='expanded',
       measureText('RIZMAR').actualBoundingBoxAscent at a 200px test
       size): 0.690.
     - Solved: font-size = logo-height * 0.4779 / 0.690 = logo-height *
       0.6926. .stream-lockup__logo's own height is
       clamp(24px, 2.4vw, 32px), so the same clamp scaled by 0.6926
       tracks it exactly at every viewport width -- verified live at
       1000px (logo 24px -> solved 16.62px) and 1400px (logo 32px ->
       solved 22.16px), both matching this formula to 3 decimal places.
   Previous value, clamp(1.5rem, 2.6vw, 2rem) i.e. 24-32px, was sized to
   the FULL logo image height (P-mark icon included) -- close to 2x too
   large next to the actual wordmark letters. */
.stream-lockup__name {
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: clamp(16.62px, 1.662vw, 22.16px);
  line-height: 1;
  letter-spacing: -0.01em;
  /* The "RIZMAR" letters sit a little above the middle of the logo image
     and the text sits a little low in its own line box, so plain
     align-items:center left the word ~2-4px under the wordmark's baseline.
     Nudge it up so both share one baseline (measured, all sizes). */
  position: relative;
  top: -0.09em;
}
.stream-panel--fintech .stream-lockup__name { color: var(--fintech-main); }
.stream-panel--agri .stream-lockup__name { color: var(--agri-main); }

.stream-motto {
  margin: 0 0 0.9rem;
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--ink);
}

/* Round 6 item 4: one-line mission description, expanded panel only. */
.stream-description {
  margin: -0.4rem 0 0.9rem;
  font-size: 1.02rem;
  max-width: 56ch;
}

/* =============================================================
   Round 7 items 6/7/8 -- expanded stream header.

   All three are scoped to .stream-panel.is-expanded on purpose: the
   closed dual-card view on Home is explicitly out of scope this round,
   and .stream-head__row (item 7's new wrapper) is deliberately a no-op
   there because the description inside it is display:none while closed,
   leaving a one-child flex row that lays out identically to the plain
   <p> it replaced.
   ============================================================= */

/* Round 9 item 4 -- consistency check, VERIFIED, no change needed.
   The brief asked whether Fintech's expanded panel positions its
   description the same way Agri's does (the round 9 reference image is a
   capture of the live Agri panel). It does, and it cannot help doing so:
   there is no .stream-panel--fintech / --agri selector anywhere in the
   three rules below, so both streams resolve to the same declarations.
   Confirmed live as well as by reading, at 1440px, with both panels
   expanded and measured: identical display (flex), gap (41.6px), motto
   flex (0 0 auto), description flex (1 1 auto) and row width (940px);
   and at 700px both stack identically under the max-width: 760px block.

   The one thing that DOES differ between the two is where the
   description's left edge lands (x=506 on Agri, x=420 on Fintech at
   1440px). That is not the rule differing, it is the motto's own text
   length -- "Decision integrity. Zero noise." is 214px, "Right the first
   time." is 136px -- and the description simply starts after it. Pinning
   the motto to a fixed column would make the two panels start their
   descriptions at the same x, but it would also move Agri off the
   layout the reference image confirms as correct, so it was left alone
   rather than "fixed". */

/* Item 7: motto left (unmoved), description beside it on the same line.
   baseline alignment, not center/start -- the two have quite different
   font sizes (0.95rem bold vs 1.02rem regular) and aligning their
   baselines is what actually makes them read as one line rather than
   two blocks that happen to be adjacent. The description takes the
   remaining width and the motto refuses to shrink, so a long
   description never squeezes the motto onto two lines. */
.stream-panel.is-expanded .stream-head__row {
  display: flex;
  align-items: baseline;
  gap: clamp(1.2rem, 3vw, 2.6rem);
}
.stream-panel.is-expanded .stream-head__row .stream-motto {
  flex: 0 0 auto;
  margin-bottom: 0;
}
.stream-panel.is-expanded .stream-head__row .stream-description {
  flex: 1 1 auto;
  margin: 0;
  /* max-width lifted from the base rule's 56ch: the flex basis now
     governs the measure, and 56ch would leave a ragged gap on wide
     viewports where there is room for the full line. */
  max-width: none;
}
/* Round 31 item 2: shift the description right so it starts roughly
   under the "Z" in the PRIZMAR wordmark directly above it (diagnosed
   live: at 1440px the rendered "Z" glyph in .stream-lockup__logo spans
   x=587-616 on Fintech and x=632-660 on Agri; the description started
   at x=420 / x=505 respectively -- both well left of the logo, under
   the "R"/"I"). Per-stream, not one shared value, because the two
   panels' logos do not land at the same x -- the round 9 item 4 comment
   above already established why: the lockup is centred as a whole
   (logo + separator + stream name), and "Fintech" vs "Agri" are
   different widths, so centring moves the logo itself, not just the
   text beside it. Values are added on top of the existing flex `gap`
   (not a replacement for it), which is what keeps this holding up
   reasonably well down at ~900px too -- verified live: at 1440px this
   lands the description flush on the Z's left edge for both streams;
   at 900px it drifts a few px past the Z into the "M", still reading
   as "under the Z". */
.stream-panel--fintech.is-expanded .stream-head__row .stream-description {
  margin-left: 10.5rem;
}
.stream-panel--agri.is-expanded .stream-head__row .stream-description {
  margin-left: 8rem;
}
/* Narrow viewports: stack, which is the layout the brief explicitly
   allows here. Reverts to the base rules' own vertical spacing. */
@media (max-width: 760px) {
  .stream-panel.is-expanded .stream-head__row {
    display: block;
  }
  .stream-panel.is-expanded .stream-head__row .stream-motto {
    margin-bottom: 0.9rem;
  }
  /* The desktop per-stream indents (which line the description up under
     the logo's "Z") must not carry into the stacked layout: flush left,
     directly under the motto. */
  .stream-panel--fintech.is-expanded .stream-head__row .stream-description,
  .stream-panel--agri.is-expanded .stream-head__row .stream-description {
    margin-left: 0;
  }
}

/* Item 6: lockup centred and enlarged, expanded view only. The logo
   roughly doubles (24-32px -> 44-64px) and the stream name follows it
   through the SAME 0.6926 cap-height ratio derived in the
   .stream-lockup__name comment above -- 44 * 0.6926 = 30.47,
   64 * 0.6926 = 44.33 -- so the text still matches the wordmark's real
   cap height at the new size instead of being eyeballed. The vw term is
   scaled by the same factor as the two ends (1.662 * 44/24 = 3.047).
   The separator keeps its original relationship to the full logo height
   rather than to the letters, same as before. */
.stream-panel.is-expanded .stream-lockup {
  justify-content: center;
  gap: 0.75rem;
  margin-bottom: 1.5rem;
  /* Round 8 item 4a: real bug, not a styling tweak. .stream-lockup is a
     <p> element, so it was inheriting the global `p { max-width: 62ch }`
     rule (a readable-prose-line-length cap meant for body text) --
     measured live at 568px, well short of the 940px content column.
     justify-content: center was therefore centering the logo+name INSIDE
     that too-narrow, left-flush 568px box, not inside the actual column
     -- which is exactly the ~193px-left-of-viewport-centre offset
     measured before this fix (lockup centre x=526.7 vs viewport centre
     x=720 at 1440px). max-width: none lets the <p> fill the real 940px
     column again, so centering the flex children inside it now centers
     them in the column (and the column itself is centered in the
     panel/viewport, unchanged). Scoped to the expanded view only, per
     this round's "don't restructure anything else" instruction -- the
     closed Home card's own lockup is untouched. */
  max-width: none;
}
.stream-panel.is-expanded .stream-lockup__logo {
  height: clamp(44px, 4.4vw, 64px);
}
.stream-panel.is-expanded .stream-lockup__name {
  font-size: clamp(30.47px, 3.047vw, 44.33px);
}
.stream-panel.is-expanded .stream-lockup__sep {
  font-size: clamp(2.75rem, 4.4vw, 4rem);
  font-weight: 200;
}

/* Phones, expanded Fintech/Agri: the close button sits at the right edge
   on the same row as the lockup and the "Fintech" word ran under it. Keep
   the lockup in the space left of the button: reserve the button's width
   on the right and scale the lockup down a touch (same 0.6926 text/logo
   ratio, so the word still matches the wordmark's cap height). */
@media (max-width: 760px) {
  .stream-panel.is-expanded .stream-lockup {
    padding-right: 56px;
    gap: 0.55rem;
  }
  .stream-panel.is-expanded .stream-lockup__logo { height: 38px; }
  .stream-panel.is-expanded .stream-lockup__name { font-size: 26.3px; }
  .stream-panel.is-expanded .stream-lockup__sep { font-size: 2.4rem; }
  /* "Agri" is a shorter word than "Fintech", so centring pulled its lockup
     right of Fintech's. Take the width difference (44.8px) off its right
     side so both lockups start at the same left edge. */
  .stream-panel--agri.is-expanded .stream-lockup { padding-right: 100.8px; }
}

/* Item 8: breathing room between the motto/description row and the
   brand-coloured rule. Applied as top margin on the rule itself rather
   than bottom margin on the row, so it holds whether the row is one
   line (desktop) or two (stacked, mobile) without needing to be
   restated in the media query above. */
.stream-panel.is-expanded .stream-head::after {
  margin-top: clamp(1.6rem, 3.2vw, 2.6rem);
}

/* Brand-coloured rule under the motto */
.stream-head::after {
  content: "";
  display: block;
  height: 2px;
  border-radius: 2px;
}
.stream-panel--fintech .stream-head::after { background: var(--fintech-main); }
.stream-panel--agri .stream-head::after { background: var(--agri-main); }

.stream-body { flex: 1; }

.stream-section-heading {
  font-size: 1.15rem;
  margin: 0 0 0.9rem;
}

/* Agri's sub-stream rows sit one level above Fintech's service rows in
   the hierarchy, so their labels are larger (round 3 item 6). Scoped
   with > so the service rows nested inside their panels keep the
   normal, smaller size. */
.sub-accordion > .accordion-item > .accordion-trigger .accordion-trigger__title {
  font-size: 1.5rem;
  line-height: 1.15;
}

.sub-accordion { margin-top: 1.75rem; }
.sub-accordion .accordion-item { border-color: transparent; background: var(--surface-alt); }

/* =============================================================
   Round 4 item F -- per-subsection tint + corner-facet motif,
   expanded card view only.

   Source: _Prizmar_v3.pdf page 1 ("Design concept"), the "Slide design
   elements" panels -- rounded rect, tinted background, small triangular
   corner cut. Extracted via PyMuPDF (vector paths + page-space bboxes,
   not eyeballed off a render): the panel motif itself turned out to be
   a raster/photographic asset (145x320px embedded bitmaps with real
   lighting, not a flat vector fill), so its own "tint" doesn't reduce
   to a clean hex the way the page's actual colour-palette grid did.
   That grid (6 swatches, clean vector `rg` fills, cross-checked against
   this file's own tokens -- #438234 matches --agri-main exactly,
   #DED7CB matches --tan exactly, #B9D2AD matches --agri-tint exactly)
   IS reliable, and is what the tints below are built from: each
   subsection is --fintech-main / --agri-main mixed into white at an
   increasing percentage, never a hue not already in tokens.css.

   One flagged discrepancy, not silently smoothed over: the deck's OWN
   swatch literally labelled "Fintech main color" is #6B5B8F, which
   matches this file's --fintech-deep, not --fintech-main (#815BAE).
   --fintech-main is used below anyway, as the site's own already-signed
   -off "established" purple, per the brief's own wording -- the deck's
   raw label and this codebase's token name have simply drifted apart
   at some earlier round, a pre-existing fact this pass surfaces rather
   than causes.

   Scoping: every rule below is qualified by #fintech.is-expanded /
   #agri.is-expanded (the class JS adds when a card is promoted to its
   full-viewport panel) so the CLOSED card's plain list is untouched --
   confirmed against the constraint that closed view scope is unchanged. */

/* Corner-facet cut, shared shape for every TINTED subsection (the
   uniform service lists and Agri's two top-level sub-streams). Pricing
   is deliberately excluded as of round 5 item 3 -- see below, it no
   longer carries the tinted-panel look at all. 18px stays inside the
   trigger's own padding (1.3rem 1.5rem ~= 21/24px), so it nicks the
   corner without touching the title/icon. */
#fintech.is-expanded #fintech-services,
#agri.is-expanded #agri-services,
#agri.is-expanded #agri-intelligence {
  clip-path: polygon(18px 0, 100% 0, 100% 100%, 0 100%, 0 18px);
}

/* Round 5 item 3 -- was a 5-step ramp (6/10/14/18/24% --fintech-main),
   one differentiated step per row. Brief: uniform bg on the 4 service
   rows (they're equal siblings, a list, not a sequence -- a ramp implied
   an ordering/hierarchy between them that was never intended), pricing
   visibly a DIFFERENT KIND of section rather than one more step in the
   same ramp.

   Round 6 item 4d -- the panel itself now carries the stream colour (see
   .stream-panel--fintech.is-expanded's facet background above), so this
   card's own fill flips from a tint OF that colour to plain white: the
   two colours competing (a purple-tinted card floating on a purple-
   faceted backdrop) read muddy in an on-screen check, where a neutral
   card reads as a clean surface sitting on top of the colour instead. */
#fintech.is-expanded .stream-body > .accordion-group > .accordion-item.service-item {
  background: var(--surface);
}

/* Pricing: chose NO background over a different color (brief left this
   open). "How we price it" is currently placeholder content wrapped in
   its own amber [PLACEHOLDER] box (see .placeholder styling) -- a
   tinted panel behind that would compete with the amber for attention
   and implies a level of finished design commitment the content doesn't
   have yet. A plain, typographically-separated block reads as "this is
   a different kind of thing" (a closing note, not another list item)
   without adding a second colour into the mix. Widened from the .info-
   block base rule's 1px hairline to a 2px stream-coloured rule + extra
   top space, so the break reads as more deliberate than the default
   divider used between generic info-blocks elsewhere (Agri's
   Requirements/Deliverables, which stay on the plain base rule,
   untouched -- this override is Fintech-scoped only). */
#fintech.is-expanded .info-block {
  margin-top: 2.75rem;
  padding-top: 1.75rem;
  border-top: 2px solid var(--fintech-tint);
}
#fintech.is-expanded .info-block :is(h4:not(:where(.was-h5)), :where(.was-h4)) {
  font-size: 1.15em;
}

/* Round 5 item 4 -- was a 2-step ramp (10%/16% --agri-main). Brief:
   Services and Intelligence are equal siblings (the top-level choice
   between two sub-streams), not a sequence -- same reasoning as
   Fintech's services above. One shared tint, not two.

   Round 6 item 4d -- flipped to plain white for the same reason as
   Fintech's service rows above: the panel background itself is now the
   stream colour, so this card goes neutral rather than doubling up. */
#fintech.is-expanded #fintech-services,
#agri.is-expanded #agri-services,
#agri.is-expanded #agri-intelligence {
  background: var(--surface);
}

/* =============================================================
   Round 7 items 10 + 11 -- expanded Agri sub-stream layout.

   Written as one block because they are two states of one grid: item 10
   is the at-rest layout (two columns), item 11 is what that same grid
   does when a sub-stream opens (one column, opened one first). Splitting
   them would leave a commit where the grid has a rest state and no
   expanded state, or vice versa.

   Item 11 audit, run before writing any of this (reported in full to the
   user): the "sibling moves below it" half was ALREADY working and is
   left alone -- both sub-streams are plain in-flow siblings, so opening
   one has always pushed the other down. Measured on the pre-change
   build: Services grew 116px -> 1711px and Intelligence moved from
   y=410 to y=2006, i.e. below Services' new bottom edge. What was NOT
   happening is the full-screen part: the opened sub-stream was
   position: static at 683px wide and simply grew taller in flow. Only
   that half is added here.
   ============================================================= */

/* Item 10: two columns at rest, expanded Agri panel only. align-items
   start so a taller card doesn't stretch its shorter sibling to match --
   these are independent choices, not a matched pair. */
#agri.is-expanded .sub-accordion {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem;
  align-items: start;
}
/* Round 8 item 4d: real bug, found by measuring, not eyeballed. Services
   and Intelligence were NOT actually top-aligned despite align-items:
   start and identical measured heights (146.5px each) -- Services sat
   at margin-top:0, Intelligence at margin-top:16px, a leftover from the
   global `.accordion-item + .accordion-item { margin-top: 1rem }` rule
   (written for the pre-round-7 STACKED layout, where the two were
   vertically adjacent and needed that gap). CSS doesn't know the layout
   changed to a 2-column grid -- the two are still DOM-adjacent siblings,
   so the rule kept firing and pushed the second one down by exactly the
   16px difference measured live. `gap: 1.5rem` above already provides
   the spacing this layout actually needs (both between columns at rest
   and between the two rows once one is stacked below the other per item
   11), so the sibling margin is zeroed here rather than fought against. */
#agri.is-expanded .sub-accordion > .accordion-item + .accordion-item {
  margin-top: 0;
}

/* Item 11: with either sub-stream open, the grid collapses to a single
   column and the open one is pulled to the top, so the sibling lands
   below it rather than beside it -- preserving the existing
   sibling-moves-down behaviour (see audit above) while adding the
   takeover. :has() on the container is what switches the track count;
   order: -1 is what guarantees the OPEN one leads regardless of which of
   the two it is (they are mutually exclusive, so at most one matches). */
#agri.is-expanded .sub-accordion:has(> .accordion-item[data-expanded="true"]) {
  grid-template-columns: 1fr;
}
#agri.is-expanded .sub-accordion > .accordion-item[data-expanded="true"] {
  order: -1;
  /* The sticky header paints OVER this scroll container (z-index 210 vs
     the panel's 200), so a plain scrollIntoView({block:"start"}) parks
     the card's top edge underneath it -- measured: card top landed at
     viewport y=0 with a 65px header covering it. scroll-margin-top is
     the scroll-snapping equivalent of the panel's own top padding, and
     reads the same live-measured --stream-header-clearance the panel
     uses, so it tracks the real header height rather than a guess. */
  scroll-margin-top: calc(var(--stream-header-clearance, 90px) + 1rem);
  /* "Full screen": fills the panel's visible height, so the opened
     sub-stream is all you see and the sibling is pushed off the fold.
     --stream-header-clearance is the header's real measured height, set
     in JS at panel-open time (same variable the panel's own top padding
     uses), so this tracks the actual header rather than a guess.

     Deliberately NOT a position: fixed overlay, and deliberately NOT
     broken out to full VIEWPORT width -- flagged to the user rather
     than silently chosen. A fixed overlay would take the element out of
     flow, which would destroy the "sibling moves below it" behaviour
     the same item asks to keep; and breaking past the panel's
     min(100%, 940px) content column would give this one block a
     measure no other block in the panel has, producing very long text
     lines. It fills the column's full width and the viewport's full
     height. */
  min-height: calc(100vh - var(--stream-header-clearance, 90px) - 4rem);
}

/* Single column on narrow viewports regardless of open state -- two
   columns of this content is unreadable at phone widths, same threshold
   the outer stream columns already stack at. */
@media (max-width: 760px) {
  #agri.is-expanded .sub-accordion {
    grid-template-columns: 1fr;
  }
}

/* Round 5 item 4, second half -- reusable pattern for the parts WITHIN
   each sub-stream: larger heading + a distinct background per part.
   Targets .info-block--part1/2/3, a markup hook added in index.html
   (not nth-of-type/nth-child sibling counting -- those .info-blocks
   aren't the only element type among their siblings, e.g. Services also
   has the nested service accordion-group before them, so counting by
   position would have been fragile; explicit classes stay correct
   regardless of what else sits nearby). Genuinely reusable: the exact
   same 3 classes are applied to Services' 3 parts (Requirements /
   Deliverables / Pricing) AND Intelligence's 3 parts (Current
   functionality / Upcoming functionality / Pricing), unrelated content,
   same pattern.

   Backgrounds were originally a green ramp lighter than the 12% parent
   above (--agri-wash for part 2, two color-mix steps either side of it),
   so the 3 parts stayed visually distinct without just repeating the
   sibling rule's ramp.

   Round 6 item 4d -- that whole green ramp is now neutral, same reason
   as the two rules above. The 3-way distinction between parts is kept
   (still not flat, identical white-on-white), just rebuilt from three
   neutral parent tokens instead of three green steps: --surface (part 1),
   --surface-warm (part 2, replacing --agri-wash as "the middle, slightly
   warmer one" in the same relative position), --surface-alt (part 3). */
#agri.is-expanded .info-block--part1,
#agri.is-expanded .info-block--part2,
#agri.is-expanded .info-block--part3 {
  padding: 1.4rem 1.6rem;
  border-radius: var(--radius-md);
  border-top: 0;
  clip-path: polygon(14px 0, 100% 0, 100% 100%, 0 100%, 0 14px);
}
#agri.is-expanded .info-block--part1,
#agri.is-expanded .info-block--part2,
#agri.is-expanded .info-block--part3 {
  margin-top: 1.5rem;
}
#agri.is-expanded .info-block--part1 :is(h4:not(:where(.was-h5)), :where(.was-h4)),
#agri.is-expanded .info-block--part2 :is(h4:not(:where(.was-h5)), :where(.was-h4)),
#agri.is-expanded .info-block--part3 :is(h4:not(:where(.was-h5)), :where(.was-h4)) {
  font-size: 1.15em;
}
#agri.is-expanded .info-block--part1 { background: var(--surface); }
#agri.is-expanded .info-block--part2 { background: var(--surface-warm); }
#agri.is-expanded .info-block--part3 { background: var(--surface-alt); }

.info-block {
  margin-top: 2rem;
  padding-top: 2rem;
  border-top: 1px solid var(--hairline);
}
.info-block :is(h4:not(:where(.was-h5)), :where(.was-h4)) { margin-bottom: 0.9rem; }
/* Round 32 item 3: match the "Services"/"Intelligence" panel-title look
   (.sub-accordion > .accordion-item > .accordion-trigger
   .accordion-trigger__title -- font-display, 800, 1.5rem) on these two
   h4s specifically. #agri-services-cases is Services' own "Case
   studies" block (a real id, already unique); .info-block--part3 has
   exactly two uses in the page and .stream-expanded-only is what tells
   them apart -- Services' "Pricing system" carries it, Intelligence's
   own "Pricing system" carries .pricing-block instead and is untouched.
   #agri.is-expanded prefix matches the specificity of the existing
   "#agri.is-expanded .info-block--part3 h4 { font-size: 1.15em }" rule
   above so this actually wins instead of losing to it. */
#agri-services-cases > :is(h4:not(:where(.was-h5)), :where(.was-h4)),
#agri.is-expanded .info-block--part3.stream-expanded-only > :is(h4:not(:where(.was-h5)), :where(.was-h4)) {
  font-size: 1.5rem;
  line-height: 1.15;
}
.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.75rem;
}
.timeline li {
  display: grid;
  grid-template-columns: 90px 1fr;
  gap: 0.9rem;
  font-size: 0.9rem;
  align-items: baseline;
}
.timeline strong { color: var(--ink); font-family: var(--font-mono); font-size: 0.78rem; }

.pricing-tiers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}
.pricing-tier {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 1.3rem;
  background: var(--surface);
}
.pricing-tier :is(h5, :where(.was-h5)) { margin: 0 0 0.5rem; font-family: var(--font-display); font-weight: 800; }
.pricing-tier p { font-size: 0.88rem; margin: 0; }

.stream-contact {
  margin-top: 2.2rem;
  padding: 1.4rem 1.6rem;
  border-radius: var(--radius-md);
  background: var(--ink);
  color: #fff;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.stream-contact p { color: rgba(255, 255, 255, 0.78); margin: 0; font-size: 0.88rem; max-width: 46ch; }
.stream-contact a.button {
  background: #fff;
  color: var(--ink);
  padding: 0.6em 1.2em;
  border-radius: var(--radius-pill);
  font-weight: 700;
  font-size: 0.88rem;
  white-space: nowrap;
}

/* =============================================================
   Contact + footer
   ============================================================= */

/* Round 20 follow-up 3: since the h2 moved into the form column only
   (see .contact-grid__heading below), the "Contact" eyebrow is the only
   full-width heading-level element left for the section -- the base
   .eyebrow rule (0.72rem/600) reads too quiet for that job. Scoped to
   #contact specifically so every other .eyebrow on the page (About,
   the Agri case cards) is untouched. */
#contact .eyebrow {
  font-size: 0.92rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  color: var(--ink);
}

/* Round 20 follow-up: the section's h2 now lives INSIDE this grid, in
   the right-hand (form) column only. Explicit placement rather than
   source order, so the details column can start on row 2 -- level with
   the form's first field -- instead of sitting under an empty heading
   slot and reading lopsided. */
.contact-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  grid-template-rows: auto auto;
  gap: 2.5rem;
  margin-top: 2rem;
}
.contact-grid__heading { grid-column: 2; grid-row: 1; margin-bottom: 0; align-self: end; }
.contact-grid > .contact-details { grid-column: 1; grid-row: 2; }
.contact-grid > .contact-form { grid-column: 2; grid-row: 2; }
/* Single column: placement is dropped so source order (heading ->
   details -> form) rules again, which is the order this section had
   before the heading moved. The row-gap alone spaces them. */
@media (max-width: 760px) {
  .contact-grid { grid-template-columns: 1fr; grid-template-rows: none; gap: 2rem; }
  .contact-grid__heading,
  .contact-grid > .contact-details,
  .contact-grid > .contact-form { grid-column: auto; grid-row: auto; }
}

.contact-details dl { margin: 0; }
.contact-details dt { font-family: var(--font-mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-soft); margin-top: 1rem; }
/* :first-of-type has no heading above it any more (round 20 follow-up),
   so it doesn't need the 1rem separator the later ones do. */
.contact-details dt:first-of-type { margin-top: 0; }
.contact-details dd { margin: 0.2rem 0 0; font-size: 1rem; overflow-wrap: anywhere; }
.contact-details dd a:hover { text-decoration: underline; }
/* "(Fintech division)" / "(Agri division)" -- a quieter qualifier on the
   address it follows, not a line of its own weight. */
.contact-details__role { color: var(--ink-soft); font-size: 0.85rem; white-space: nowrap; }
/* Round 20 follow-up 3: just the stream word inside that qualifier picks
   up its stream's own color -- same purple/green as .nav--fintech/
   .nav--agri in the header -- so the two divisions read as visually
   distinct at a glance, the same as everywhere else the site pairs the
   two streams. font-weight left alone; the color alone is the cue. */
.contact-details__division--fintech { color: var(--fintech-main); }
.contact-details__division--agri { color: var(--agri-main); }

.contact-form { display: grid; gap: 1rem; }
.contact-form .field { display: grid; gap: 0.4rem; }
.contact-form label { font-size: 0.82rem; font-weight: 600; }
/* Lighter and smaller than the label text it sits beside. */
.contact-form .field__optional {
  font-size: 0.72rem;
  font-weight: 400;
  color: var(--ink-soft);
  margin-left: 0.3rem;
}
.contact-form input,
.contact-form textarea {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  padding: 0.7em 0.85em;
  font: inherit;
  background: var(--surface-alt);
}
/* Round 21 follow-up: persistent light hairline, same fix as
   .float-contact -- a solid --ink fill loses its own silhouette against
   --ink-deep once Contact goes dark below. Unconditional (not scoped to
   #contact.band--alt) because it does nothing visible over the current
   tan and needs no dark-only branch if Contact's background ever changes
   again. */
.contact-form button[type="submit"] {
  justify-self: start;
  background: var(--ink);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.35);
  padding: 0.75em 1.6em;
  border-radius: var(--radius-pill);
  font-weight: 700;
}
.contact-form__note { font-size: 0.78rem; color: var(--ink-soft); margin: 0; }

/* Round 36 item 6: privacy-consent checkbox. Horizontal row (the .field
   grid stacks label-over-control, wrong for a checkbox), checkbox aligned
   to the first text line, sentence weight rather than the 600 field-label
   weight. Sits as a normal .contact-form grid item so the 1rem form gap
   spaces it like every other row. */
.contact-form__consent {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
}
.contact-form__consent input {
  flex: 0 0 auto;
  margin-top: 0.2em;
}
.contact-form__consent label {
  font-size: 0.82rem;
  font-weight: 400;
  line-height: 1.45;
}
.contact-form__consent a { text-decoration: underline; }

/* Round 21 follow-up: Contact goes dark (obsidian), matching About.
   Same #id.band--alt override pattern as About's rule above, so the
   shared .band--alt / --band-alt-bg rule stays untouched for any
   section that stays light.

   Text: everything here sits directly on the section background (no
   card surface of its own), so all of it needs a flip -- the eyebrow,
   the h2, dt/dd copy (dd's email links inherit its colour via the
   global `a { color: inherit }`), the role qualifier, and the form's
   labels / optional-tag / footnote. Split the same way the light
   design already splits it: eyebrow/h2/dd/label were full --ink, so
   they get full --ink-on-dark; dt/role/optional/note were the quieter
   --ink-soft, so they get the new --ink-soft-on-dark instead of
   flattening that hierarchy onto one colour.

   Left untouched, same as About's founder/value cards: the inputs and
   textarea (own --surface-alt fill), and the two social-icon buttons
   (own --surface fill) -- both already read fine against a dark
   backdrop on their own opaque surface, no override needed. The "P"
   watermark is untouched too, for the same reason About's still is:
   item 6 (a lighter-toned asset) was deferred there because the file
   doesn't exist in the repo -- same gap applies here, not solved by
   this round.

   Accent words: --fintech-main/--agri-main were tuned for the tan
   band and land under ~4:1 against --ink-deep -- swapped for the
   lighter -tint ramp already in tokens.css so "Fintech"/"Agri" stay
   legible at this text's size.

   Buttons: the primary submit's light hairline border above is
   unconditional, so it's already covered. The secondary ("Request a
   demo") button is a transparent/--ink-outline style, which disappears
   against --ink-deep -- given its own dark-only override, mirroring
   the light version's own hover swap (fill/text invert on hover). */
#contact.band--alt { background: var(--ink-deep); }
#contact.band--alt .eyebrow,
#contact.band--alt .contact-grid__heading,
#contact.band--alt .contact-details dd,
#contact.band--alt .contact-form label { color: var(--ink-on-dark); }
#contact.band--alt .contact-details dt,
#contact.band--alt .contact-details__role,
#contact.band--alt .contact-form .field__optional,
#contact.band--alt .contact-form__note { color: var(--ink-soft-on-dark); }

#contact.band--alt .contact-details__division--fintech { color: var(--fintech-tint); }
#contact.band--alt .contact-details__division--agri { color: var(--agri-tint); }

#contact.band--alt .contact-form button.btn-secondary[type="submit"] {
  color: var(--ink-on-dark);
  border-color: var(--ink-on-dark);
}
#contact.band--alt .contact-form button.btn-secondary[type="submit"]:hover {
  background: var(--ink-on-dark);
  color: var(--ink-deep);
}

.site-footer {
  padding: 2.4rem clamp(20px, 5vw, 56px);
  border-top: 1px solid var(--hairline);
  /* Round 20 follow-up 4: containing block for the centered <small>
     below. justify-content no longer needs to place two items apart --
     the copyright is out of flow now, brand is the only flex item left
     in normal flow. */
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: 1rem;
  background: var(--surface-alt);
}
/* Round 20 follow-up 4: nudges the logo up off the footer's vertical
   center (align-items:center still applies -- brand's margin box grows,
   so centering it shifts the visible logo upward) for clearer separation
   from .corner-tagline, the fixed watermark text that sits just below
   the footer once scrolled to the very bottom of the page. */
.site-footer__brand { display: flex; align-items: center; gap: 0.6rem; font-family: var(--font-display); font-weight: 800; margin-bottom: 2rem; }
.site-footer__brand img { height: 16px; }
/* Round 20 follow-up 4: was flush right (space-between), which put this
   directly under .float-contact (fixed, right:20px/bottom:20px) once
   scrolled to the bottom -- confirmed overlapping at both desktop widths
   (e.g. 1920px) and common mobile widths (~550px effective and narrower)
   before this fix, since .site-footer's own flex-wrap never actually
   triggers at those widths (brand + copyright both fit on one line well
   before wrapping would kick in). Absolute + centered on the relatively
   positioned .site-footer clears the button at anything wide enough for
   true centering to matter (verified from ~600px up); below that the
   media query reverts to a normal, left-clustered flex item sitting
   right after the logo instead, which stays well clear of the
   right-anchored button by construction, not by tuning a number. */
.site-footer small {
  color: var(--ink-soft);
  font-size: 0.8rem;
  /* Two stacked lines ("© 2026 Prizmar" / "prizmargroup.com"), so the
     shorter first line centres over the second rather than sitting
     left-ragged against it. */
  text-align: center;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
@media (max-width: 600px) {
  /* Mobile: copyright + domain stack directly under the logo, left-aligned
     with it (brand's wrapper breaks onto its own row), and the word
     "Prizmar" is dropped from the copyright line. */
  .site-footer { flex-direction: column; align-items: flex-start; gap: 0; }
  .site-footer__brand { margin-bottom: 0.9rem; }
  .site-footer small { position: static; left: auto; top: auto; transform: none; text-align: left; }
  .site-footer__name { display: none; }
}

/* Roadmap list (Agri intelligence "upcoming") */
.roadmap-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.roadmap-list li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.9rem;
  border-bottom: 1px dashed var(--hairline);
  padding-bottom: 0.5rem;
}
.roadmap-list span.eta { font-family: var(--font-mono); font-size: 0.76rem; color: var(--ink-soft); white-space: nowrap; }

/* =============================================================
   Round 10 item 6 -- social links, Contact
   ============================================================= */
/* Round 20 follow-up: this block moved out from under the whole
   .contact-grid and into the details column, directly below Location.
   It stacks now (icons, then the note beneath them) rather than sitting
   in one wide row -- in a column roughly half the old width, the note
   would otherwise be squeezed into a sliver beside the two icons. The
   top margin is the dt rhythm's 1rem plus a little, so it reads as the
   next item in that column rather than a detached afterthought. */
.social {
  margin-top: 1.75rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
}
.social__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 0.6rem;
}
/* Same chamfered-square treatment as the rest of the site's small
   controls, sized to a 44px hit target so it is comfortable on touch
   even though the glyph inside is 20px. */
.social__link {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  color: var(--ink);
  background: var(--surface);
  transition: color var(--dur-med) var(--ease),
              border-color var(--dur-med) var(--ease),
              transform var(--dur-med) var(--ease);
}
.social__link:hover {
  color: var(--fintech-main);
  border-color: var(--fintech-main);
  transform: translateY(-2px);
}
/* focus-visible is inherited from the global a:focus-visible ring. */
.social__note { margin: 0; }
@media (prefers-reduced-motion: reduce) {
  .social__link { transition: none; }
  .social__link:hover { transform: none; }
}

/* Round 10 item 8: the contact form now has two submits. They wrap
   rather than squeeze on narrow viewports, and the demo request is
   styled as the secondary of the two -- same size and hit area, lighter
   weight -- because "Request a call" remains the section's headline
   action. */
.contact-form__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
}
/* Qualified to out-specify .contact-form button[type="submit"], which is
   defined later in this file and would otherwise keep its filled dark
   treatment on both buttons. */
.contact-form button.btn-secondary[type="submit"] {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--ink);
}
.contact-form button.btn-secondary[type="submit"]:hover {
  background: var(--ink);
  color: var(--surface);
}
@media (max-width: 420px) {
  .contact-form__actions > * { flex: 1 1 100%; }
}

/* =============================================================
   Round 15 -- Agri sub-section content reorganisation
   Scoped to the Agri > Services / Intelligence panels only.
   ============================================================= */

/* Services > Case studies: full-width carousel, one card at a time --
   horizontal scroll-snap track + prev/next arrows (js/main.js). */
.case-carousel { position: relative; margin-top: 0.4rem; }
/* Round 40: scoped away from --grid tracks. Those elements are a real grid
   on desktop (.our-values-grid, .pricing-tiers, .svc-matrix) and this rule
   is declared LATER in the file than two of them, so at equal specificity
   it was silently winning up there -- .our-values-grid computed
   `display: flex` with this rule's `gap: 1.1rem` instead of its own
   2.4rem/3.25rem, measured live at 1280px. Rather than re-declare each
   grid's desktop values as an override (duplicating numbers that already
   exist a few hundred lines up), the base carousel styling simply does not
   apply to them at all; the mobile block hands them the equivalent
   properties for the width where they actually are a carousel. */
.case-carousel__track:not(.case-carousel__track--grid) {
  display: flex;
  gap: 1.1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.case-carousel__track::-webkit-scrollbar { display: none; }
.case-carousel__track > .case-card {
  flex: 0 0 100%;
  scroll-snap-align: start;
}

/* =============================================================
   Round 39 item 3 -- .case-carousel--mobile

   A second use of the carousel above, for blocks that are a multi-column
   grid on desktop and a swipeable row on a phone: "Current functionality"
   (.func-panel) and the S/M/L service matrix (.svc-matrix). Both were
   stacking into ~900-1000px ribbons on mobile, which is what read as the
   sections being flattened.

   The whole mechanism is `display: contents` above the breakpoint: the
   wrapper and (where the track is a separate element) the track stop
   generating boxes, so their children are laid out by the original grid
   exactly as before this round. That is why neither desktop grid needed a
   single rule changed -- the markup nests deeper, the layout does not.

   Below it, the wrapper becomes a real box and the track a scroll-snap
   flex row. Swipe + dots only, no arrows -- see js/main.js for why.
   ============================================================= */
@media (min-width: 761px) {
  .case-carousel--mobile { display: contents; }
  .case-carousel--mobile > .case-carousel__dots { display: none; }
  /* A plain wrapper track dissolves with its carousel. A track that IS the
     desktop grid (--grid) must NOT -- dissolving it would take the grid
     with it -- so it only has the carousel behaviour switched off.
     Round 40 replaced a hardcoded :not(.svc-matrix) with this modifier,
     now that three more blocks use the same trick. */
  .case-carousel--mobile > .case-carousel__track:not(.case-carousel__track--grid) {
    display: contents;
  }
  /* --grid tracks need nothing here: the base .case-carousel__track rule
     is scoped away from them, so above this breakpoint they are simply
     themselves -- their own display, gap and template, untouched. */
}

@media (max-width: 760px) {
  /* min-width: 0 on both, and it is load-bearing. A flex/grid item defaults
     to min-width:auto, i.e. "never shrink below your content" -- so the
     track sized itself to all five slides laid end to end (measured 723px
     inside a 317px panel) and the slides' own `flex: 0 0 86%` resolved
     against that runaway width instead of the panel's. Zeroing the floor
     lets the track take the panel's width and the percentages mean what
     they say. */
  .case-carousel--mobile { margin-top: 0.9rem; min-width: 0; }
  .case-carousel--mobile > .case-carousel__track { min-width: 0; }
  /* The dots sit under the track for these two (the case carousel puts
     them above); with the headline already above the func-panel track,
     dots on top would wedge two rows of chrome between title and content. */
  .case-carousel--mobile > .case-carousel__dots {
    order: 2;
    margin: 0.85rem 0 0;
  }
  .case-carousel--mobile { display: flex; flex-direction: column; }
  .case-carousel--mobile > .case-carousel__track { order: 1; }

  /* Unwrap the column wrappers so the five tiles become the slides
     themselves rather than two tall stacks. */
  .func-panel .case-carousel__track > .func-panel__col { display: contents; }
  .func-panel .case-carousel__track > .func-panel__col > .func-tile,
  .svc-matrix.case-carousel__track > .svc-matrix__col {
    flex: 0 0 86%;
    scroll-snap-align: start;
  }
  /* 86% not 100%: a sliver of the next slide is the only affordance that
     says "this scrolls" before the user has touched it -- the dots read as
     pagination after the fact, not as an invitation. */

  .func-panel .case-carousel__track { gap: 0.8rem; align-items: stretch; }
  /* Every grid-track behaves the same on mobile: stop being a grid, start
     being the scroll row. This is the ONLY place they get carousel
     behaviour -- the base .case-carousel__track rule excludes them -- so
     the scroll/snap properties are spelled out here rather than inherited
     from it. */
  /* Child combinator, not the bare class, and that is deliberate: at one
     class this loses to `.svc-matrix { display: grid }`, which is declared
     several hundred lines further down. Round 39 got away with it by
     writing `.svc-matrix.case-carousel__track` (two classes); when round 40
     generalised that to a single --grid modifier the specificity dropped
     and the matrix silently went back to a grid on mobile -- caught live at
     360px. Qualifying with the parent restores the win for every grid-track
     at once, without naming any of them. */
  .case-carousel--mobile > .case-carousel__track--grid {
    display: flex;
    gap: 0.8rem;
    grid-template-columns: none;
    margin-top: 0.4rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    align-items: stretch;
  }
  /* Round 40: same slide sizing for the three blocks added this round --
     Our Approach, the Intelligence pricing tiers, and (from round 39) the
     service matrix. align-items: stretch so cards in a row match height
     instead of each shrinking to its own text. */
  .our-values-grid.case-carousel__track--grid > .value-card,
  .pricing-tiers.case-carousel__track--grid > .pricing-tier {
    flex: 0 0 86%;
    scroll-snap-align: start;
  }
  /* The hover pop is a pointer affordance and would clip inside a scroll
     container anyway -- off while these are slides. */
  .our-values-grid.case-carousel__track--grid > .value-card:hover {
    transform: none;
    box-shadow: none;
  }
}
.case-carousel__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 2.4rem;
  height: 2.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-size: 1.4rem;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  cursor: pointer;
  z-index: 2;
  transition: opacity var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}
.case-carousel__arrow:hover { background: var(--surface-alt); }
.case-carousel__arrow--prev { left: 0.5rem; }
.case-carousel__arrow--next { right: 0.5rem; }
.case-carousel__arrow[disabled] { opacity: 0.3; cursor: default; }
.case-card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 1.25rem 1.2rem;
}
.case-card .eyebrow { margin-bottom: 0.8rem; }
/* the shared .case-study dl is 2-col by default -- force 1-col so the four
   rows stack cleanly. */
.case-card .case-study { grid-template-columns: 1fr; gap: 0.75rem; }
.case-card .case-study dd { margin-bottom: 0.6rem; font-size: 0.85rem; }
@media (prefers-reduced-motion: reduce) {
  .case-carousel__track { scroll-behavior: auto; }
}

/* Intelligence > "Intelligence process explained": a DIAGONAL cascade
   (round 28, supersedes the round 27 single horizontal row -- all of that
   version's CSS is gone, not overridden). Each step is a rounded card the
   width of ~56% of the flow; steps 2-4 are pushed progressively right
   (per-nth-child margin-left) and sit just below the previous one, so the
   block reads as a staircase descending left-to-right and is taller than
   it is wide (per assets/refs/round28-process-diagonal-target.png). The
   connector between consecutive steps is a small thin elbow (an "L" drawn
   with two borders on ::after) tipped with a downward arrowhead (::before)
   -- understated, sitting in the notch off the card's right edge, NOT the
   round-27 soft chevron. Mobile-first base below is the plain stacked
   column; the diagonal offsets + connectors live in the min-width: 901px
   block and simply do not apply on narrow viewports. */
.intel-process { margin-top: 2.5rem; }
.intel-process > :is(h4:not(:where(.was-h5)), :where(.was-h4)) { margin-bottom: 1.1rem; }
.process-flow {
  list-style: none;
  margin: 0;
  padding: 0;
  display: block;
}
.process-step {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 18px;
  /* Round 32: vertical padding trimmed 1.5rem -> 1.1rem alongside the
     shorter min-height below -- at the old padding, step 3's 3-line
     body text alone (its sentence is the longest of 2-4) pushed the
     card past the 30%-shorter target even with the lower floor. */
  padding: 1.1rem 1.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.process-step + .process-step { margin-top: 1rem; }
.process-step__title {
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1.05rem;
  line-height: 1.2;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--ink);
  margin: 0;
}
/* Round 32 correction: 2 columns (Statistics/Prices left, Weather/Signals
   right) instead of a 4-row stack -- grid-auto-flow:column on the SAME
   4 <li>s in the same DOM order lays them out column-major (1,2 down
   the first column, 3,4 down the second) with no markup change. This is
   what let card 1's natural height come down enough to meet the other
   3 cards' new shorter min-height below. */
.process-step__list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.9rem;
  line-height: 1.7;
  color: var(--ink-soft);
  display: grid;
  grid-template-rows: repeat(2, auto);
  grid-auto-flow: column;
  column-gap: 1.25rem;
}
.process-step > p:last-child {
  font-size: 0.9rem;
  line-height: 1.5;
  margin: 0;
  color: var(--ink-soft);
}
/* Full-bleed strip: breaks out of the .accordion-panel__body 1.5rem
   padding to span the panel column edge to edge. (A literal 100vw bleed
   is not reachable here -- the .accordion-panel__inner is overflow:hidden
   for the grid-rows animation and would clip it back to this same width,
   while also eating the arrow tip -- so this pulls to the column edges,
   which is what "breaking out of the panel max-width" means in this
   layout.) */
.process-banner {
  margin: 2.4rem -1.5rem 0;
  padding: 1.1rem 3rem 1.1rem 2rem;
  background: linear-gradient(90deg, var(--agri-wash), #EEF4E9);
  color: var(--ink);
  max-width: none;
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1.05rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-align: center;
  border-radius: 0;
}
@media (min-width: 901px) {
  .process-step {
    flex-direction: row;
    /* Round 31 item 4 set min-height to step 1's own natural 148px so
       nothing about step 1 had to change. Round 32 correction: that read
       as too tall -- shrunk further, and step 1 itself now has to give
       ground to fit, via the 2-column list above (4 rows -> 2 rows cuts
       its list height roughly in half). New natural heights measured
       live: step 1 ~89px (2-col list), steps 2-4 ~72-84px (unchanged,
       single-sentence bodies). min-height 6.25rem (100px) covers all
       four with a little breathing room, and is 32% shorter than round
       31's 9.25rem -- comfortably past the "at least 30%" ask.
       align-items: center unchanged from round 31 (still the right
       call now that step 1 itself is also shorter and more even with
       2-4). */
    align-items: center;
    min-height: 6.25rem;
    gap: 1.6rem;
    width: 56%;
  }
  .process-step + .process-step { margin-top: 0.85rem; }
  /* 42% -> 38%: gives the body column (step 3 especially, the longest
     single sentence of the four) a bit more width so it wraps to 2
     lines instead of 3, which is what actually let the min-height come
     down without any card overflowing it. */
  .process-step__title { flex: 0 0 38%; }
  .process-step__list,
  .process-step > p:last-child { flex: 1; }
  .process-step:nth-child(2) { margin-left: 14.67%; }
  .process-step:nth-child(3) { margin-left: 29.33%; }
  .process-step:nth-child(4) { margin-left: 44%; }
  /* elbow: horizontal stub off the card's right edge, then a short drop.
     Round 33 item 2b: --ink-soft -> --agri-main, and it pulses (see the
     animation block below). The ::before arrowhead changes with it --
     it is the tip of this same connector, and leaving it grey while the
     elbow went green would just look like a bug. */
  .process-step:not(:last-child)::after {
    content: "";
    position: absolute;
    left: 100%;
    top: calc(100% - 1.3rem);
    width: 1.9rem;
    height: 2.5rem;
    border-top: 1.5px solid var(--agri-main);
    border-right: 1.5px solid var(--agri-main);
    border-top-right-radius: 5px;
    pointer-events: none;
  }
  /* downward arrowhead at the foot of the drop */
  .process-step:not(:last-child)::before {
    content: "";
    position: absolute;
    left: calc(100% + 1.9rem);
    top: calc(100% + 1.08rem);
    transform: translateX(-50%);
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 6px solid var(--agri-main);
    pointer-events: none;
  }
  /* 5s loop, opacity only, 0.45 -> 1 -- "subtle, not too strong" means
     the connector never fully disappears and never gets a colour/scale
     change, it just breathes. Staggered 0.45s per connector so the three
     of them read down the cascade in the same 1 -> 2 -> 3 -> 4 order the
     flow itself does, rather than blinking in unison. */
  @media (prefers-reduced-motion: no-preference) {
    .process-step:not(:last-child)::after,
    .process-step:not(:last-child)::before {
      animation: process-connector-pulse 5s ease-in-out infinite;
    }
    .process-step:nth-child(2)::after,
    .process-step:nth-child(2)::before { animation-delay: 0.45s; }
    .process-step:nth-child(3)::after,
    .process-step:nth-child(3)::before { animation-delay: 0.9s; }
  }
  .process-banner {
    clip-path: polygon(0 0, calc(100% - 2.5rem) 0, 100% 50%, calc(100% - 2.5rem) 100%, 0 100%);
  }
}
/* Round 34 item 3: swing widened (0.45-1 -> 0.15-1) so the pulse actually
   reads as blinking rather than a faint shimmer. Same 5s loop, same
   per-connector 0.45s stagger above -- only the keyframe's own range
   changed. */
@keyframes process-connector-pulse {
  0%, 100% { opacity: 0.15; }
  50%      { opacity: 1; }
}

/* Round 28 -- Intelligence > "Current functionality", rebuilt per
   assets/refs/round28-current-functionality-reference.png. One outer
   panel laid out as three columns (left tiles / centre headline / right
   tiles). Stacks to one column below 900px (same breakpoint as the
   process cascade above).
   Round 33: the decorative beam layer that used to sit behind the
   headline is removed entirely (see index.html) -- .func-panel keeps
   position:relative/isolation only because the tiles still rely on the
   z-index stacking it establishes.
   Round 32 item 1: fill switched from --surface-warm to --surface-alt --
   the exact value Services > Pricing system's own outer card
   (#agri.is-expanded .info-block--part3) uses, so the two "one big outer
   card" treatments in Agri now match instead of reading as two subtly
   different greys. */
.func-panel {
  position: relative;
  isolation: isolate;
  margin-top: 0.5rem;
  padding: 2.4rem 2.2rem;
  background: var(--surface-alt);
  border-radius: 22px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr) minmax(0, 1fr);
  align-items: center;
  gap: 1.4rem 2rem;
}
.func-panel__col {
  position: relative;
  z-index: 1;
  display: grid;
  gap: 1.2rem;
  align-content: center;
}
.func-panel__center {
  position: relative;
  z-index: 2;
  margin: 0;
  max-width: none;
  text-align: center;
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1.9rem;
  line-height: 1.15;
  color: var(--ink);
}
.func-tile {
  background: var(--surface);
  border-radius: 14px;
  padding: 1.35rem 1.35rem;
  font-size: 0.9rem;
}
.func-tile__icon {
  display: block;
  color: var(--agri-main);
  margin-bottom: 0.8rem;
}
.func-tile__icon svg { width: 26px; height: 26px; }
/* Round 36 item 4: the one .func-tile__label ("Report sections:") was
   removed from the markup -- that tile now runs icon -> pill-list like the
   other left-column tile, its icon's own 0.8rem margin-bottom carrying the
   gap. Rule dropped with it. */
.func-tile p { margin: 0; color: var(--ink); max-width: none; }
.func-tile--feature {
  display: flex;
  align-items: center;
  gap: 0.9rem;
}
.func-tile--feature .func-tile__icon {
  margin-bottom: 0;
  flex: 0 0 auto;
}
/* Round 36 item 5: the right column's feature tiles are plain <p>, the
   left column's are .pill-list li at font-weight 600. Match the weight so
   the two columns read as one set. Family/size unchanged. */
.func-tile--feature p { font-weight: 600; }
/* olive pill bullet -- new to this redesign, replaces the func-card
   rotated-square tick (which is gone with the rest of func-card). */
.pill-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.55rem;
}
.pill-list li {
  position: relative;
  padding-left: 1.5rem;
  color: var(--ink);
  font-weight: 600;
}
.pill-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.5em;
  width: 0.85rem;
  height: 0.38rem;
  border-radius: 999px;
  background: var(--agri-main);
  transform: translateY(-50%);
}
@media (max-width: 900px) {
  .func-panel {
    grid-template-columns: 1fr;
    padding: 1.8rem 1.4rem;
  }
  .func-panel__center { font-size: 1.5rem; }
}

/* Round 29 -- Services > "Pricing system", rebuilt per
   assets/refs/round29-pricing-system-reference.png. Two changes from the
   round-17 version: the 3 stage cards become an interlocking chevron flow,
   and the .pricing-bands card is replaced by the .svc-matrix service-code
   table below. (.pricing-bands and .func-card__label went with it -- their
   only markup was that card.)

   The chevrons are clip-path polygons, so they carry no border: a border
   would be clipped along with the shape. White-on---surface-alt (the
   .info-block--part3 fill) is what separates them, plus a small grid gap
   showing through as the thin diagonal in the reference. Padding is
   asymmetric so the label still reads centred inside the notch/point. */
.pricing-lead {
  margin: 0.2rem 0 1.4rem;
  gap: 0.7rem;
}
.pricing-lead li { font-size: 0.98rem; font-weight: 600; }
/* Round 36 item 3: the shared .pill-list li::before pins the pill at
   top: 0.5em, which is above the first text line's optical centre (half
   the ~1.55 line-height is ~0.78em) -- measured 4px high here. Scoped
   nudge so the pill lines up with the text; the other .pill-list uses
   (func-tiles) are left as they are. */
.pricing-lead li::before { top: 0.78em; }
.pricing-stage-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.35rem;
}
.pricing-stage {
  background: var(--surface);
  border: 0;
  border-radius: var(--radius-md);
  /* Round 34 item 2: height cut just over half (8.5rem -> 4rem
     min-height, 3rem -> 1.2rem total vertical padding). Horizontal
     padding is untouched -- it is what clears the chevron notch cut by
     the clip-path below, not spacing around the text. */
  padding: 0.6rem 3rem 0.6rem 2.1rem;
  min-height: 4rem;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  clip-path: polygon(0 0, calc(100% - 2rem) 0, 100% 50%, calc(100% - 2rem) 100%, 0 100%);
}
/* stages 2-3 gain the receiving notch on the left */
.pricing-stage + .pricing-stage {
  padding-left: 3.2rem;
  clip-path: polygon(0 0, calc(100% - 2rem) 0, 100% 50%, calc(100% - 2rem) 100%, 0 100%, 2rem 50%);
}
/* the last one ends flat against the block edge, no point */
.pricing-stage:last-child {
  padding-right: 2.1rem;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 2rem 50%);
}
.pricing-stage__step {
  display: block;
  font-family: var(--font-mono);
  color: var(--tan-deep);
  font-size: 0.85rem;
}
.pricing-stage__name {
  margin: 0;
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1.25rem;
  line-height: 1.15;
  color: var(--ink);
}

/* Service-code matrix: SL 1-4 down, S / M / L across. Four sibling cards
   rather than one table, so each band reads as its own column card like
   the reference -- rows stay aligned across all four because every row
   shares one min-height and there are always exactly four of them. */
.svc-matrix {
  display: grid;
  grid-template-columns: minmax(0, 0.74fr) repeat(3, minmax(0, 1fr));
  gap: 0.9rem;
  margin-top: 1.1rem;
}
.svc-matrix__col {
  background: var(--surface);
  border-radius: var(--radius-md);
  padding: 1.2rem 1.15rem 1.3rem;
}
/* Both head variants keep identical box height (the codes column carries a
   transparent rule) so the four columns' rows line up. */
.svc-matrix__head {
  margin: 0 0 0.9rem;
  padding-bottom: 0.55rem;
  min-height: 2.5rem;
  display: flex;
  align-items: center;
  border-bottom: 1.5px solid transparent;
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 0.85rem;
  /* must not wrap: a two-line "Service code" makes this column's head taller
     than the S/M/L heads and pushes its SL labels out of line with the rows
     they belong to (measured 60px vs 40px before this). */
  white-space: nowrap;
  color: var(--ink);
}
.svc-matrix__head--band {
  justify-content: center;
  font-size: 1.9rem;
  line-height: 1;
  border-bottom-color: var(--ink);
}
.svc-matrix__rows {
  list-style: none;
  margin: 0;
  padding: 0;
}
.svc-matrix__rows > li {
  min-height: 3.15rem;
  display: flex;
  align-items: center;
  font-size: 0.82rem;
  line-height: 1.4;
  color: var(--ink);
}
.svc-matrix__col--codes .svc-matrix__rows > li {
  justify-content: center;
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
}
/* Narrow: the flow stacks and the matrix drops its code column -- each row
   labels itself from data-code instead, so nothing is lost. */
@media (max-width: 900px) {
  .pricing-stage-grid { grid-template-columns: 1fr; gap: 0.5rem; }
  .pricing-stage,
  .pricing-stage + .pricing-stage,
  .pricing-stage:last-child {
    clip-path: none;
    min-height: 0;
    padding: 1.1rem 1.3rem;
  }
  .pricing-stage__name { font-size: 1.1rem; margin-top: 0.35rem; }
  .svc-matrix { grid-template-columns: 1fr; gap: 0.8rem; }
  .svc-matrix__col--codes { display: none; }
  .svc-matrix__rows > li {
    min-height: 0;
    display: block;
    padding: 0.7rem 0;
    border-top: 1px solid var(--hairline);
  }
  .svc-matrix__rows > li::before {
    content: attr(data-code);
    display: block;
    margin-bottom: 0.2rem;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    color: var(--tan-deep);
  }
}

/* =============================================================
   Round 26 -- Agri > Services: static card grid + Case 1 rebuild
   ============================================================= */

/* The 4 services as a static 2-up grid (was a nested accordion). Base is
   .value-card; this only pins the columns to 2 (the bare .value-grid
   auto-fits 3-4 at this panel's width) and lets the card be a flex column
   so "Explore use case" can sit at the bottom regardless of copy length.
   Stacks to 1 column at 640px, matching .case-study / Case 1's own
   stack point. */
.services-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.4rem;
}
@media (max-width: 640px) {
  .services-grid { grid-template-columns: 1fr; }
}
.service-card {
  /* Round 32 item 5: same gradient as Intelligence > About the tool's
     cards (.about-tool-grid .value-card) -- was the flat --surface fill
     .value-card carries by default. Angle/stops copied verbatim so the
     two grids' cards read as the same material. */
  background: linear-gradient(158deg, var(--surface) 0%, var(--surface-alt) 100%);
  display: flex;
  flex-direction: column;
  padding: 1.6rem 1.5rem;
}
/* The numeral is a heading-scale mark here, not the small mono tag
   .value-card__num is elsewhere -- reference shows it large, dark and in
   the display face. */
.service-card .value-card__num {
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1.5rem;
  color: var(--ink);
  margin-bottom: 0.9rem;
}
.service-card :is(h4:not(:where(.was-h5)), :where(.was-h4)) {
  font-size: 1.15rem;
  line-height: 1.25;
  margin-bottom: 0.5rem;
}
.service-card p {
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--ink-soft);
}
/* Pushed to the card's bottom edge; the only interactive thing on the
   card (the card itself no longer expands). */
.service-card__link {
  margin-top: auto;
  padding-top: 1.4rem;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.92rem;
  color: var(--ink);
  text-decoration: none;
}
.service-card__link:hover { color: var(--agri-main); }

/* Case-study dots -- one per slide, top-right above the track, filled =
   current / outline = rest, same goTo() the arrows use. */
.case-carousel__dots {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
.case-carousel__dot {
  width: 0.62rem;
  height: 0.62rem;
  padding: 0;
  border: 1.5px solid var(--tan-deep);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.case-carousel__dot:hover { background: var(--tan-deep); opacity: 0.5; }
.case-carousel__dot.is-active { background: var(--tan-deep); opacity: 1; }

/* Mobile: every carousel's dots 10% bigger (dot size and gap) and centred
   on the page instead of right-aligned. */
@media (max-width: 760px) {
  .case-carousel__dots { justify-content: center; gap: 0.55rem; }
  .case-carousel__dot { width: 0.682rem; height: 0.682rem; }
}

/* Cases 1 and 2: two-column what-we-did / outcome / value + mockup.
   (Round 26 built this for Case 1; round 29 moved Case 2 onto it too.)
   Case 3 still carries the .case-study dl. Stacks at 640px like
   .case-study does. */
.case-card--split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: 1.5rem 2.25rem;
  align-items: center;
  padding: 2.05rem 1.95rem;
}
@media (max-width: 640px) {
  .case-card--split { grid-template-columns: 1fr; align-items: start; }
}
.case-card__num {
  margin: 0 0 0.15rem;
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1.9rem;
  line-height: 1.05;
  color: var(--ink);
}
.case-card__name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.95rem;
  line-height: 1.3;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  color: var(--ink);
}
.case-card__rule {
  border: 0;
  border-top: 1px solid var(--hairline);
  margin: 1rem 0 1.1rem;
}
.case-card__head {
  margin: 1.1rem 0 0.5rem;
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1.05rem;
  color: var(--ink);
}
.case-card__head:first-of-type { margin-top: 0; }
.case-card__line {
  margin: 0 0 0.45rem;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--ink);
}
.case-card__media img {
  display: block;
  width: 100%;
  height: auto;
  /* Round 33 item 1: hover magnifies the mockup ~a third. position/
     z-index are companions to the scale, not extra styling -- without a
     stacking position the enlarged image would paint under the card's
     own text column it now overlaps. */
  position: relative;
  z-index: 2;
  transition: transform var(--dur-med) var(--ease);
}
@media (prefers-reduced-motion: no-preference) {
  .case-card__media img:hover { transform: scale(1.32); }
}
/* Round 29 -- Case 2's "what we did" sub-list. Plain small discs: these
   are components of one sentence, not outcomes, so they deliberately do
   NOT take the .check-list green tick. */
.case-card__bullets {
  margin: 0 0 0.55rem;
  padding-left: 1.15rem;
  list-style: disc;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--ink);
}
.case-card__bullets li { margin-bottom: 0.12rem; }
/* "Value" is the one green heading in the card, per both round-29 refs. */
.case-card__head--value { color: var(--agri-main); }

/* Round 29 task 3: a taller card frame. Measured baseline was 434px for
   all three slides (they are equal-height by flex default); 30rem = 480px
   puts the FLOOR at +10.6%, inside the asked-for 10-15%. Cases 1 and 2 now
   run taller than that on content alone (this round's Value checklists and
   Case 2's rebuild take them to ~594px, +37%), so the floor is what keeps
   the frame generous if content is ever trimmed back. Padding is bumped in
   the same spirit -- the extra height is breathing room, not empty space. */
.case-carousel__track > .case-card { min-height: 30rem; }
/* The prev/next arrows are absolutely positioned OVER the slide (left/right
   0.5rem, 2.4rem wide), so a slide whose body text starts at the card's own
   padding runs underneath them. Round 29's longer Case 1/2 bodies made that
   visible -- these gutters keep every slide's text clear of both arrows.
   Higher specificity than .case-card--split's padding shorthand, so it wins
   without !important. */
.case-carousel__track > .case-card {
  padding-left: 3.4rem;
  padding-right: 3.4rem;
}
@media (max-width: 900px) {
  .case-carousel__track > .case-card {
    min-height: 0;
    padding-left: 3rem;
    padding-right: 3rem;
  }
}

/* Round 27: one shared checkmark list. Case 1's Outcome (round 26) and
   the Intelligence pricing tiers (this round) both use it, so there is a
   single check glyph on the site rather than two near-identical ones.
   Glyph is the font's checkmark in a green (--agri-main) circle, matching
   the round 27 pricing reference; round 26 shipped it in a rounded square,
   folded into the circle here. */
.check-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.55rem;
}
/* Direct-child only: a nested list inside a check-list item (the pricing
   Tier 2 "Crush margin estimations:" sub-bullets) keeps its own plain
   disc markers and does NOT also get a checkmark. */
.check-list > li {
  position: relative;
  padding-left: 2rem;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--ink);
}
.check-list > li::before {
  content: "\2713";
  position: absolute;
  left: 0;
  top: 0.05em;
  width: 1.35rem;
  height: 1.35rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--agri-main);
  border: 1.5px solid var(--agri-main);
  border-radius: 50%;
}

/* =============================================================
   Round 27 -- Agri > Intelligence restyle
   About-the-tool grid + Pricing-system outer card.
   (process flow + shared .check-list are edited in place above.)
   ============================================================= */

/* "About the tool": scoped modifier on the shared .value-grid/.value-card
   so About's "Our Approach" and this panel's "Current functionality",
   which reuse the same classes, are untouched. Numeral dropped in the
   markup; here the card goes centred, gets a soft gradient face, and a
   thin agri-toned rule divides the bold title from the muted body. */
.about-tool-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.4rem;
}
@media (max-width: 640px) {
  .about-tool-grid { grid-template-columns: 1fr; }
}
.about-tool-grid .value-card {
  background: linear-gradient(158deg, var(--surface) 0%, var(--surface-alt) 100%);
  padding: 2.1rem 1.9rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.about-tool-grid .value-card :is(h4:not(:where(.was-h5)), :where(.was-h4)) {
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1.3rem;
  line-height: 1.25;
  color: var(--ink);
  margin: 0 0 1.15rem;
  max-width: 20ch;
}
/* The rule: 1px agri-to-transparent gradient, left-to-right fade. Card 1
   only carries a small left-pointing arrowhead (--arrow); it is where the
   deferred line-draw animation begins in the reference GIF. */
.about-tool-card__line {
  position: relative;
  width: 100%;
  max-width: 15rem;
  height: 1px;
  margin: 0 0 1.15rem;
  background: linear-gradient(90deg, var(--agri-main) 0%, rgba(67, 130, 52, 0.04) 100%);
}
.about-tool-card__line--arrow::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translate(-1px, -50%);
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-right: 8px solid var(--agri-main);
}

/* Round 33 item 2a: the deferred line-draw animation from round 27's
   reference GIF, finally built. A small down-pointing green triangle
   rides each card's divider left to right, then the next card's, in DOM
   order 1 -> 2 -> 3 -> 4, on a 5s loop.

   One shared 5s keyframe with a -1.25s stagger per card rather than four
   separate animations: each card's arrow is only opaque during its own
   quarter of the cycle and parked invisible at the line's end for the
   other three quarters, so exactly one triangle is ever on screen and
   the hand-off between cards needs no JS or per-card timing maths.

   The static round-27 arrowhead (::before, card 1 only) is hidden while
   this runs -- two arrowheads on the same divider read as a mistake --
   but it stays as the reduced-motion fallback, which is why it is hidden
   HERE rather than deleted above. */
@media (prefers-reduced-motion: no-preference) {
  .about-tool-card__line--arrow::before { opacity: 0; }
  .about-tool-card__line::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 0;
    height: 0;
    border-left: 7px solid transparent;
    border-right: 7px solid transparent;
    border-top: 11px solid var(--agri-main);
    opacity: 0;
    animation: about-tool-arrow 5s linear infinite;
  }
  .about-tool-grid .value-card:nth-child(2) .about-tool-card__line::after { animation-delay: 1.25s; }
  .about-tool-grid .value-card:nth-child(3) .about-tool-card__line::after { animation-delay: 2.5s; }
  .about-tool-grid .value-card:nth-child(4) .about-tool-card__line::after { animation-delay: 3.75s; }
}
@keyframes about-tool-arrow {
  0%   { left: 0;    opacity: 0; }
  3%   { opacity: 1; }
  22%  { opacity: 1; }
  25%  { left: 100%; opacity: 0; }
  100% { left: 100%; opacity: 0; }
}
.about-tool-grid .value-card p {
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0;
  max-width: 32ch;
}

/* "Pricing system": one outer card wrapping heading + intro + tiers.
   Higher specificity than #agri.is-expanded .info-block--part3 so the
   clipped corner and grey fill from that rule are replaced here with a
   plain generous radius and a soft gradient, no !important. */
#agri.is-expanded .info-block--part3.pricing-block,
.info-block--part3.pricing-block {
  background: linear-gradient(155deg, var(--surface) 0%, var(--surface-alt) 100%);
  border: 0;
  border-radius: 22px;
  clip-path: none;
  padding: 2rem 2rem 2.25rem;
}
.pricing-block > p {
  margin: 0 0 1.6rem;
  font-size: 0.9rem;
  color: var(--ink-soft);
  max-width: 60ch;
}
.pricing-block .pricing-tiers { margin-top: 0; }
.pricing-block .pricing-tier {
  border: 0;
  background: var(--surface);
  border-radius: 16px;
  padding: 1.5rem 1.4rem;
  box-shadow: 0 1px 3px rgba(36, 42, 53, 0.06);
}
.pricing-block .pricing-tier :is(h5, :where(.was-h5)) {
  margin: 0 0 1rem;
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink);
}
.pricing-block .pricing-tier .check-list { gap: 0.7rem; }
.pricing-tier__sub {
  list-style: disc;
  margin: 0.35rem 0 0 1.15rem;
  padding: 0;
  display: grid;
  gap: 0.2rem;
  font-size: 0.85rem;
  line-height: 1.45;
  color: var(--ink-soft);
}

/* =============================================================
   Round 39 item 2 -- reclaim mobile content width

   Deliberately the LAST block in this file. Every rule here is a
   same-or-lower-specificity override of a padding declared earlier
   (.stream-panel.is-expanded, .accordion-panel__body, #agri.is-expanded
   .info-block--partN, .func-panel's own 900px rule), so source order is
   what makes it win. Moving it earlier silently un-does it -- that is not
   a style preference, it was measured failing before this block was moved
   down here.

   Why: four nested containers each pad both sides inside an open stream
   panel, and on a phone they compound into most of the screen. Measured at
   390px before this round -- stream panel 19.5 + accordion body 24 +
   info-block 25.6 + func-panel 22.4, doubled = 183px of chrome, 47% of the
   viewport, leaving 206px to actually draw in. That, not the sections' own
   styling, is the real reason everything inside a panel read as squeezed.

   ~12px a side each recovers ~90px, about 45% more content width, and it
   applies to every open panel rather than just the two sections that got
   carousels. Vertical padding is left alone on purpose: the problem is
   horizontal, and tightening the vertical rhythm too would read as cramped
   rather than roomier.
   ============================================================= */
@media (max-width: 760px) {
  .stream-panel.is-expanded {
    padding-left: 12px;
    padding-right: 12px;
  }
  .accordion-panel__body { padding-left: 12px; padding-right: 12px; }
  #agri.is-expanded .info-block--part1,
  #agri.is-expanded .info-block--part2,
  #agri.is-expanded .info-block--part3,
  #fintech.is-expanded .info-block {
    padding-left: 12px;
    padding-right: 12px;
  }
  .func-panel { padding-left: 12px; padding-right: 12px; }
}

/* Round 39 item 3 -- .func-panel source order + mobile stacking.

   .func-panel__center moved ahead of the columns in the markup (it has to
   sit outside the carousel wrapper, and "headline first" is the order
   mobile wants). `order` restores the desktop reading -- left column,
   headline, right column -- across the 3-column grid. Only needed above
   the breakpoint; below it, source order is already correct. */
@media (min-width: 761px) {
  .func-panel__col--left { order: 1; }
  .func-panel__center { order: 2; }
  .func-panel__col--right { order: 3; }
}
@media (max-width: 760px) {
  /* Headline is pinned above the carousel rather than being slide 3 of 6,
     so it keeps reading as the centrepiece the desktop layout makes it. */
  .func-panel__center {
    font-size: 1.35rem;
    margin-bottom: 0.2rem;
  }
}

/* Round 39 item 3 -- pricing stages keep a sequence signal on mobile.

   The chevron clip-path is dropped below 900px (it needs horizontal room
   to read), which left three identical cards with nothing saying they run
   in order. A numbered rail restores that: a left border threading all
   three, and each card's stage number in a filled dot on it. Cheaper and
   far more legible at this width than trying to rotate the chevrons.
   The number comes from the existing .pricing-stage__step text ("Stage 1"),
   which is already in the markup -- nothing new to keep in sync. */
@media (max-width: 900px) {
  .pricing-stage-grid { position: relative; padding-left: 1.6rem; }
  .pricing-stage-grid::before {
    content: "";
    position: absolute;
    left: 0.42rem;
    top: 0.9rem;
    bottom: 0.9rem;
    width: 2px;
    background: var(--hairline);
  }
  .pricing-stage { position: relative; }
  .pricing-stage::before {
    content: "";
    position: absolute;
    left: -1.42rem;
    top: 1.1rem;
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    background: var(--agri-main);
    /* rings the dot in the panel fill so the rail reads as passing behind
       it rather than through it */
    box-shadow: 0 0 0 3px var(--surface-alt);
  }
}

/* =============================================================
   Round 40 -- mobile-only fixes. Everything here is inside a
   max-width media query; desktop is untouched by construction.
   ============================================================= */

/* Item 5 -- clicking the header logo hid the hero wordmark behind the
   header.

   Diagnosed rather than guessed: after the click, scrollY landed on 72,
   not 0. .site-header is `position: sticky`, which (unlike fixed) still
   occupies its 72px of normal flow ABOVE #home -- so scrolling #home's top
   to viewport y=0 means scrolling exactly the header's height down the
   page. The header then re-pins at top:0 over the hero, covering the first
   72px of it: measured, the hero logo runs y=28..121, so 44px of a 93px
   wordmark was hidden.

   scroll-margin-top pulls the anchor's landing point back up by the same
   amount, which for the first section on the page resolves to scroll 0.
   Sized off the real header height at this breakpoint (46px pill + the
   14px .site-header top padding = 60) with a little air. Desktop has the
   identical bug but is deliberately left alone this round -- flagged, not
   fixed. */
@media (max-width: 760px) {
  #home { scroll-margin-top: 72px; }
}

/* Round 40 item 4 -- "Intelligence process explained" reads as a process on
   mobile.

   Desktop (>=901px) draws this as a diagonal cascade with elbow connectors,
   which is where the sequence comes from. All of that is inside a
   min-width: 901px block, so below it the four steps were just four
   identical stacked cards with nothing saying they run in order -- the same
   gap the pricing stages had before round 39, and the reason this section
   stopped reading as a flow.

   A numbered rail restores it, and goes further than the round-39 dots
   because these steps are explicitly ordered: the marker carries the real
   step number, taken from a CSS counter on the <ol> rather than typed into
   the markup, so adding or reordering a step can't desync it. The rail is
   drawn on .process-flow (not per-card) so a single line threads all four,
   and it stops at the last marker instead of running past it. */
@media (max-width: 900px) {
  .process-flow {
    position: relative;
    counter-reset: pstep;
    padding-left: 2.9rem;
  }
  /* top/bottom inset to the first and last marker centres, so the line
     starts and ends ON the numbers rather than floating past them */
  .process-flow::before {
    content: "";
    position: absolute;
    left: 0.92rem;
    top: 1.55rem;
    bottom: 1.55rem;
    width: 2px;
    background: var(--hairline);
  }
  .process-step { counter-increment: pstep; }
  .process-step::before {
    content: counter(pstep);
    position: absolute;
    left: -2.9rem;
    top: 0.85rem;
    width: 1.85rem;
    height: 1.85rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--agri-main);
    color: #fff;
    font-family: var(--font-display);
    font-weight: 800;
    font-size: 0.85rem;
    line-height: 1;
    /* Rings the marker so the rail reads as passing behind it rather than
       through it. --surface, not --surface-warm: checked live rather than
       assumed -- .intel-process sets no background of its own, and the
       nearest ancestor that paints one is .accordion-item at #fff. A warm
       ring would have shown as a visible halo on white. */
    box-shadow: 0 0 0 3px var(--surface);
  }
  /* tighter stack now that the rail carries the "these belong together"
     signal that the old 1rem gap was doing on its own */
  .process-step + .process-step { margin-top: 0.7rem; }
}

/* Animated hero logo (transparent webm): the video carries its own glow, so
   the old wordmark sheen overlay and drop-shadow are off for it. */
.hero__logo-wrap:has(.hero__logo--video)::after { display: none; }
.hero__logo-wrap .hero__logo--video { filter: none; }

/* Animated engine figure (transparent webm), replaces the old SVG figure. */
.engine__video {
  display: block;
  width: 100%;
  max-width: 60rem;
  height: auto;
  margin: 0 auto;
}

/* Desktop: the video carries its own labels. Narrow (<=900px, the same
   breakpoint the old SVG figure used to stack its labels): swap to the
   triangle-only video, full width, with the labels as text below it. */
.engine__video--mobile,
.engine__labels { display: none; }
@media (max-width: 900px) {
  .engine__video--desktop { display: none; }
  .engine__video--mobile { display: block; max-width: 32rem; }
  .engine__labels {
    display: block;
    list-style: none;
    margin: 1.4rem auto 0;
    padding: 0;
    max-width: 32rem;
    text-align: center;
  }
  .engine__labels li + li { margin-top: 1.1rem; }
  .engine__labels strong {
    display: block;
    font-family: var(--font-display);
    font-stretch: expanded;
    font-weight: 800;
    font-size: 1.1rem;
    line-height: 1.2;
    color: var(--ink-on-dark);
  }
  /* The one-line descriptions under each label are not shown on mobile
     (owner request); the bold labels stay. */
  .engine__labels span { display: none; }
}

/* =============================================================
   Keyboard navigation
   ============================================================= */

/* "Skip to content": first Tab stop. Parked off-screen, slides into view
   only while it has keyboard focus; never visible to mouse users. */
.skip-link {
  position: fixed;
  top: 10px;
  left: 10px;
  z-index: 4000;
  padding: 0.7rem 1.1rem;
  border-radius: var(--radius-sm);
  background: var(--ink);
  color: #fff;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.95rem;
  text-decoration: none;
  transform: translateY(-250%);
}
.skip-link:focus { transform: translateY(0); outline: 2px solid var(--agri-main); outline-offset: 3px; }

/* <main> is only a programmatic focus target for the skip link. */
main:focus { outline: none; }

/* Keyboard-shortcuts help: built by js/main.js the first time "?" is
   pressed, hidden again on close. Nothing of it shows by default. */
.kb-help {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  background: rgba(23, 25, 31, 0.55);
}
.kb-help[hidden] { display: none; }
.kb-help__card {
  width: min(26rem, 100%);
  max-height: calc(100vh - 2.5rem);
  overflow-y: auto;
  padding: 1.4rem 1.5rem 1.5rem;
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 24px 60px rgba(23, 25, 31, 0.35);
}
.kb-help__head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
.kb-help__title { margin: 0; max-width: none; color: var(--ink); font-family: var(--font-display); font-stretch: expanded; font-weight: 800; font-size: 1.15rem; }
.kb-help__close {
  flex: 0 0 auto;
  width: 2.2rem;
  height: 2.2rem;
  border: 1px solid var(--hairline);
  border-radius: 50%;
  background: transparent;
  color: var(--ink);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}
.kb-help__list { display: grid; grid-template-columns: max-content 1fr; gap: 0.6rem 1rem; margin: 0 0 1.1rem; padding: 0; font-size: 0.95rem; }
.kb-help__list dt { margin: 0; }
.kb-help__list dd { margin: 0; color: var(--ink-soft); }
.kb-help kbd {
  display: inline-block;
  min-width: 1.7em;
  padding: 0.15em 0.45em;
  border: 1px solid var(--hairline);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--surface-alt);
  font-family: var(--font-mono);
  font-size: 0.85em;
  text-align: center;
}
.kb-help__note { margin: 0 0 1rem; max-width: none; font-size: 0.85rem; }
.kb-help__toggle {
  padding: 0.6rem 1rem;
  border: 1px solid var(--ink);
  border-radius: 999px;
  background: var(--ink);
  color: #fff;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.9rem;
  cursor: pointer;
}

/* =============================================================
   Mobile header + hero cards. Everything here is inside a
   max-width: 760px media query (the site's phone breakpoint), or
   behind a class only ever set at that width -- desktop and tablet
   are untouched by construction.
   ============================================================= */
@media (max-width: 760px) {
  /* Header: 36px pill (was 37px before this round's trim, 31px mid-round),
     inset 20px each side so its edges line up with the Home cards and page
     text, and less space above it (logo 21px and the burger's 44px tap
     overlay are unchanged). --chamfer scales with the bar height (18px on
     the original 37px pill -> 17px on 36px) so the angled ends keep the
     same proportions; left at 18px they would nearly cross over. */
  .site-header { padding: 10px 20px 0; --chamfer: 17px; }
  .site-header::before,
  body.past-home .site-header::before {
    top: 10px;
    left: 20px;
    right: 20px;
    /* the pill below now carries the translucent fill on its own; this
       backing plate would double it back up to ~99% white */
    background: transparent;
  }
  .site-header__inner,
  body.past-home .site-header__inner {
    padding: 7.5px 16px;
    background: rgba(255, 255, 255, 0.5);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .site-header__inner,
    body.past-home .site-header__inner { background: rgba(255, 255, 255, 0.92); }
  }

  /* Hero cards: titles only at the top of the page; js/main.js adds
     .hero-open once scrolled and removes it again back at the top. The
     descriptions are clipped, not removed, so screen
     readers still read them. .hero-collapse is only ever set by JS. */
  html.hero-collapse:not(.hero-open) .stream-card p {
    max-height: 0;
    opacity: 0;
    overflow: hidden;
  }
  html.hero-collapse:not(.hero-open) .stream-card__title { margin-bottom: 0; }
  html.hero-collapse.hero-animate .stream-card p {
    transition: max-height 300ms var(--ease), opacity 300ms var(--ease);
  }
  html.hero-collapse.hero-animate .stream-card__title {
    transition: margin-bottom 300ms var(--ease);
  }
  html.hero-collapse.hero-animate.hero-open .stream-card p { max-height: var(--hero-desc-h, none); }
  /* No scroll-anchoring nudges while the cards grow. */
  html.hero-expanding, html.hero-expanding body { overflow-anchor: none; }
}

/* Safari fallback player (js/main.js "iOS / Safari fixes"): the real <video>
   only feeds frames to a <canvas> and must stay invisible but still decode. */
.alpha-src {
  position: absolute;
  width: 2px;
  height: 2px;
  opacity: 0;
  pointer-events: none;
}

/* =============================================================
   Contact form -> /api/contact (js/contact.js). Everything here is either
   out of the layout flow or hidden until the form is actually submitted,
   so the form looks exactly as before until then.
   ============================================================= */
.contact-form__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
/* Turnstile mounts here. Out of flow (no grid row, no gap) unless Cloudflare
   asks the visitor to interact, in which case js adds .is-visible. */
.contact-form__turnstile {
  position: absolute;
  width: 300px;
  height: 65px;
  left: -9999px;
  overflow: hidden;
}
.contact-form__turnstile.is-visible {
  position: static;
  width: auto;
  height: auto;
  overflow: visible;
}
.contact-form__status {
  margin: 0;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.45;
  color: var(--ink);
}
.contact-form__status:focus { outline: none; }
.contact-form__status:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }
.contact-form__status[hidden] { display: none; }
#contact.band--alt .contact-form__status { color: var(--ink-on-dark); }
#contact.band--alt .contact-form__status--error { color: #ffb4a6; }
.contact-form button[type="submit"]:disabled { opacity: 0.6; cursor: progress; }

/* Contact details: the phone number with the WhatsApp / Telegram buttons
   beside it. The buttons are the same .social__link squares as LinkedIn. */
.contact-details__phone {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.45rem;
}
/* Phone rows: compact 28px icon buttons (the tap area stays 44px via the
   invisible ::after), with "(Fintech)" / "(Agri)" and the icons packed
   tight against the number. */
.contact-details__phone .social__link { position: relative; width: 28px; height: 28px; }
.contact-details__phone .social__link svg { width: 13px; height: 13px; }
.contact-details__phone .social__link::after { content: ""; position: absolute; inset: -8px; }

/* Fintech > Services: five service rows in two categories, each on ONE line
   on desktop (number | name | description | "Explore use case"), stacking on
   phones. Card material is the same gradient/border as Agri's .service-card;
   the link reuses .service-card__link so the shared carousel JS binds it. */
.fin-services__category {
  font-size: 1.15rem;
  margin: 0 0 0.9rem;
}
.fin-services__category:not(:first-child) { margin-top: 1.8rem; }
.fin-services__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.9rem;
}
.fin-service {
  display: grid;
  grid-template-columns: 2.2rem minmax(0, 1.35fr) minmax(0, 2.2fr) auto;
  align-items: center;
  gap: 0.4rem 1.4rem;
  background: linear-gradient(158deg, var(--surface) 0%, var(--surface-alt) 100%);
  padding: 1.1rem 1.5rem;
}
.fin-service__num {
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 1.5rem;
  color: var(--ink);
}
.fin-service .fin-service__name {
  margin: 0;
  font-size: 1.1rem;
  line-height: 1.25;
}
.fin-service .fin-service__desc {
  margin: 0;
  font-size: 0.92rem;
  line-height: 1.45;
  color: var(--ink-soft);
}
.fin-service .fin-service__link {
  margin-top: 0;
  padding-top: 0;
  white-space: nowrap;
}
.fin-service .fin-service__link:hover { color: var(--fintech-main); }
@media (max-width: 760px) {
  .fin-service {
    grid-template-columns: 2.2rem minmax(0, 1fr);
  }
  .fin-service .fin-service__desc,
  .fin-service .fin-service__link { grid-column: 1 / -1; }
  .fin-service .fin-service__link { padding-top: 0.5rem; }
}

/* Fintech's expanded view shows the services directly: the lone "Services"
   button is only the closed card's row (JS opens the item on expand). */
#fintech.is-expanded #fintech-services > .accordion-trigger { display: none; }

/* Closed Fintech card: "Services" heading + the five service names as
   plain cards (no numbers, descriptions or + buttons), instead of the
   Services row. Hidden once the panel is expanded, where the full rows show. */
#fintech:not(.is-expanded) #fintech-services { display: none; }
#fintech.is-expanded .fin-closed { display: none; }
.fin-closed-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
}
.fin-closed-list li {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  padding: 1.5rem 1.75rem;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.05rem;
  line-height: 1.25;
  color: var(--ink);
}

/* Fintech > Case 1 ("Four Markets, One Treasury"): same split layout as
   Agri's Case 1 (challenge / what we did on the left), with the outcome
   figures as four stat tiles where Agri has the laptop mockup. */
.case-card__services {
  margin: 0.25rem 0 0;
  font-size: 0.8rem;
  line-height: 1.35;
  color: var(--ink-soft);
}
#fintech .case-card__head--value { color: var(--fintech-main); }
.case-card__media .case-card__head--value { margin-top: 0; }
.case-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.9rem;
}
.case-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 1.3rem 1rem;
  text-align: center;
  background: var(--fintech-wash);
  border: 1px solid var(--fintech-tint);
  border-radius: var(--radius-md);
}
.case-stat__num {
  font-family: var(--font-display);
  font-stretch: expanded;
  font-weight: 800;
  font-size: 2.6rem;
  line-height: 1;
  color: var(--fintech-main);
}
.case-stat__label {
  font-size: 0.82rem;
  line-height: 1.35;
  font-weight: 600;
  color: var(--ink);
}
.case-stats--3 > .case-stat:last-child { grid-column: 1 / -1; }
/* A tile that is one sentence rather than a figure + caption. */
.case-stat--text .case-stat__label {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.1rem;
  line-height: 1.3;
  color: var(--fintech-main);
}

/* Phones: Fintech case cards as wide as the service rows above them (the
   block's 12px side padding made them narrower), and Value tiles sized so
   the figures fit inside them. */
@media (max-width: 760px) {
  #fintech.is-expanded #fintech-services-cases { padding-left: 0; padding-right: 0; }
  .case-stats { gap: 0.6rem; margin-inline: -1.25rem; }
  .case-stat { padding: 1rem 0.5rem; gap: 0.4rem; }
  .case-stat__num { font-size: 1.8rem; }
  .case-stat__label { font-size: 0.78rem; }
  .case-stat--text .case-stat__label { font-size: 0.95rem; }
}
@media (max-width: 380px) {
  .case-stat { padding: 0.85rem 0.3rem; }
  .case-stat__num { font-size: 1.5rem; }
  .case-stat__label { font-size: 0.74rem; }
  .case-stat--text .case-stat__label { font-size: 0.8rem; overflow-wrap: anywhere; }
}
@media (max-width: 340px) {
  .case-stat__num { font-size: 1.3rem; }
  .case-stat--text .case-stat__label { font-size: 0.74rem; }
}

/* Fintech > Pricing system: Agri's pricing panel look (grey panel with a
   corner cut, 1.5rem heading, green-pill lead lines -> brand-coloured here,
   white cards on the panel). */
#fintech.is-expanded .fin-pricing {
  margin-top: 1.5rem;
  padding: 1.4rem 1.6rem;
  border-top: 0;
  border-radius: var(--radius-md);
  clip-path: polygon(14px 0, 100% 0, 100% 100%, 0 100%, 0 14px);
  background: var(--surface-alt);
}
#fintech.is-expanded .fin-pricing > :is(h3, h4):first-child {
  font-size: 1.5rem;
  line-height: 1.15;
}
.fin-pricing .pill-list li::before { background: var(--fintech-main); }
.fin-pricing__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1rem;
}
.fin-pricing__card {
  background: var(--surface);
  border-radius: var(--radius-md);
  padding: 1.2rem 1.2rem 1.3rem;
}
.fin-pricing .fin-pricing__title {
  margin: 0 0 0.7rem;
  padding-bottom: 0.7rem;
  border-bottom: 1px solid var(--ink);
  font-size: 1.1rem;
  line-height: 1.2;
}
.fin-pricing__card p {
  margin: 0;
  font-size: 0.88rem;
  line-height: 1.5;
  color: var(--ink);
}
@media (max-width: 900px) {
  .fin-pricing__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
  #fintech.is-expanded .fin-pricing { padding-left: 12px; padding-right: 12px; }
}
@media (max-width: 520px) {
  .fin-pricing__grid { grid-template-columns: 1fr; }
}

/* Fintech's check-list ticks take the Fintech brand colour. */
#fintech .check-list > li::before {
  color: var(--fintech-main);
  border-color: var(--fintech-main);
}
.case-card .check-list { margin-bottom: 0.55rem; }

/* Agri case 3 has no mockup: the title block spans the card and the text
   splits into two columns that start on the same line (challenge / what
   we did | outcome / value). */
.case-card--text { align-items: start; }
.case-card--text > .case-card__top { grid-column: 1 / -1; }
.case-card--text .case-card__rule { margin-bottom: 0; }
@media (max-width: 640px) {
  .case-card--text > .case-card__top { grid-column: auto; }
}
.case-card--text { align-content: start; }

/* Section dividers: a 2px brand-tint line above AND below the Case studies
   block, in both streams (Fintech purple, Agri green), so the services,
   the use cases and the pricing panel read as three separate parts. */
#fintech.is-expanded #fintech-services-cases {
  border-bottom: 2px solid var(--fintech-tint);
  padding-bottom: 2rem;
}
#fintech.is-expanded #fintech-services-cases + .fin-pricing { margin-top: 2rem; }
#agri.is-expanded #agri-services-cases {
  border-top: 2px solid var(--agri-tint);
  border-bottom: 2px solid var(--agri-tint);
  padding-bottom: 2rem;
}
#agri.is-expanded #agri-services-cases + .info-block--part3 { margin-top: 2rem; }

/* Agri case 3: the Value statement as one tile in Agri colours, in the same
   style as Fintech's Value tiles (bold, brand-coloured, larger text). */
.case-card--text .case-card__line--after { margin-top: 0.55rem; }
.case-stats--agri { grid-template-columns: 1fr; }
.case-stats--agri .case-stat {
  background: var(--agri-wash);
  border-color: var(--agri-tint);
  padding: 1.6rem 1.4rem;
}
.case-stats--agri .case-stat--text .case-stat__label {
  color: var(--ink);
  font-size: 1.4rem;
  line-height: 1.25;
}
@media (max-width: 760px) {
  .case-stats--agri .case-stat--text .case-stat__label { font-size: 1.15rem; }
}
@media (min-width: 641px) {
  /* Desktop: the Value block sits at the vertical centre of its column. */
  .case-card--text > .case-card__body--aside {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
}

/* Fintech only: "Explore use case" as regular, underlined text (Agri keeps
   its bold link). */
.fin-service .fin-service__link {
  font-family: inherit;
  font-weight: 400;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Agri too: "Explore use case" as regular, underlined text. */
.service-card .service-card__link {
  font-family: inherit;
  font-weight: 400;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Footer: "Legal notice . Privacy" links (third line of the copyright block). */
.site-footer__legal a { color: inherit; text-decoration: none; }
.site-footer__legal a:hover,
.site-footer__legal a:focus-visible { text-decoration: underline; }

/* Legal pages (/legal/legal-notice, /legal/privacy): plain text pages in the
   site's own header, footer, fonts and colours. */
.legal { padding: clamp(7rem, 14vh, 9rem) 0 clamp(3rem, 8vh, 5rem); }
.legal__body { max-width: 72ch; }
.legal__title { font-size: clamp(1.9rem, 4vw, 2.6rem); line-height: 1.1; margin: 0 0 1.5rem; }
.legal__body h2 { font-size: 1.2rem; line-height: 1.25; margin: 2rem 0 0.6rem; }
.legal__body p { margin: 0 0 0.9rem; font-size: 1rem; line-height: 1.6; }
.legal__body ul { margin: 0 0 0.9rem; padding-left: 1.25rem; }
.legal__body li { margin: 0 0 0.45rem; font-size: 1rem; line-height: 1.6; }
.legal__body a { color: inherit; text-decoration: underline; text-underline-offset: 0.18em; }
.legal__body p, .legal__body li { color: var(--ink); }

/* iOS Low Power Mode blocks autoplay and WebKit then paints a grey "play"
   badge over every autoplay video -- including the decorative background
   beams. These videos are never controllable, so the badge is hidden. */
video::-webkit-media-controls,
video::-webkit-media-controls-panel,
video::-webkit-media-controls-overlay-play-button,
video::-webkit-media-controls-start-playback-button {
  display: none !important;
  -webkit-appearance: none;
}

/* A decorative video the browser refused to autoplay (see js/main.js): hidden
   (still able to start) so iOS draws no play badge; a still <img> of its poster
   is shown instead until it is really playing. */
video.is-autoplay-blocked { opacity: 0 !important; }

/* Phones: the contact details come first, then the "Request a call or demo"
   heading directly above the form (it used to sit under "Contact", above the
   details). Visual order only; the source order and desktop are unchanged. */
@media (max-width: 760px) {
  .contact-grid > .contact-details { order: 1; }
  .contact-grid > .contact-grid__heading { order: 2; }
  .contact-grid > .contact-form { order: 3; }
}

/* Landing point for the phone "Request a call" pill: the form heading sits
   just under the sticky header. */
@media (max-width: 760px) {
  .contact-grid__heading { scroll-margin-top: 84px; }
}

/* Oleksii's founder photo: transparent cut-out on the About band's own dark
   colour. Anchored to the top so the head keeps its space above on every
   card shape; any cropping falls on the arms at the bottom. */
.founder-card--agri .founder-card__avatar,
.founder-card--fintech .founder-card__avatar { background: var(--ink-deep); }
/* The image is taken out of flow so its own proportions never change the
   card's row height; the card keeps the size it had with the placeholder. */
.founder-card--agri .founder-card__avatar,
.founder-card--fintech .founder-card__avatar { position: relative; }
.founder-card--agri .founder-card__avatar img,
.founder-card--fintech .founder-card__avatar img { position: absolute; inset: 0; object-position: 50% 0; }
/* Initials tile: contain, not cover, so the letters are never cropped in a narrow frame
   (the tile's dark fill and the frame's --ink-deep background are the same colour). */
.founder-card--fintech .founder-card__avatar img { object-fit: contain; object-position: 50% 50%; }

/* Mobile founder photos 20% larger (both cards share this rule): was 60% / 190px. */
@media (max-width: 480px) {
  .founder-card__avatar { width: 72%; max-width: 228px; }
}

/* Equal founder photos: each photo is stretched to the height of the stat tiles
   beside it, and one founder's tiles wrap to more lines than the other's, so the
   two photos came out different heights (12px at 1440, 36px at 1100). The two
   cards now share their three rows (photo+stats / quote / link) through subgrid,
   so both photos take the taller column's height. Browsers without subgrid keep
   the previous flex layout. */
@supports (grid-template-rows: subgrid) {
  .founder-card {
    display: grid;
    grid-row: span 3;
    grid-template-rows: subgrid;
    row-gap: 1.1rem;
  }
}

/* Agri > Intelligence > Current functionality: the two left-hand tiles
   carry their icon in the bottom-right corner (50% larger) instead of above
   the bullet list. The right-hand feature tiles are untouched. */
.func-panel__col--left .func-tile { position: relative; }
.func-panel__col--left .func-tile__icon {
  position: absolute;
  right: 1.35rem;
  bottom: 1.35rem;
  margin: 0;
}
.func-panel__col--left .func-tile__icon svg { width: 39px; height: 39px; }

/* The page's videos are decorative (logo, beams, engine animation). Taking
   them out of pointer hit-testing stops browsers from showing their own
   hover toolbar (picture-in-picture / "enhance video") over them. */
video { pointer-events: none; }

/* Language switcher: "EN | UA" as plain text (no pill). Plain links (no
   redirect, no detection). It is a direct child of .site-header (not of the
   clipped pill), absolutely positioned, so the logo / nav / Contact button
   keep exactly the positions they have without it.
   - >=1080px: outside the header bar, to the left of it. Its text turns light
     (class .on-dark, set by js/main.js) when it is over a dark band.
   - 761-1079px: no room outside, so it sits inside the bar left of Contact.
   - <=760px: inside the bar, just left of the burger, always visible.
   The active language is bold with a dark (brand ink) underline. */
.site-header { --lang-right: 132px; --lang-top: 39px; }
html[lang="uk"] .site-header { --lang-right: 145px; }
.lang-switch {
  position: absolute;
  z-index: 2;
  top: var(--lang-top);
  left: calc(min(50% + var(--header-max) / 2, 100% - clamp(12px, 3vw, 28px)) - var(--lang-right));
  transform: translate(-100%, -50%);
  display: flex;
  align-items: center;
  gap: 0.35em;
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  line-height: 1;
  white-space: nowrap;
  color: var(--ink);
}
.lang-switch a {
  padding: 0.35em 0.1em;
  color: inherit;
  border-bottom: 2px solid transparent;
}
.lang-switch a[aria-current="true"] { font-weight: 800; border-color: var(--ink); }
.lang-switch a:hover { border-color: var(--ink); }
.lang-switch span { opacity: 0.45; }
.lang-switch.on-dark { color: var(--ink-on-dark); }
.lang-switch.on-dark a[aria-current="true"],
.lang-switch.on-dark a:hover { border-color: var(--ink-on-dark); }
@media (min-width: 1080px) {
  .lang-switch { left: calc(50% - var(--header-max) / 2 - 16px); }
}
@media (max-width: 760px) {
  .site-header { --lang-right: 66px; --lang-top: 28px; }
  html[lang="uk"] .site-header { --lang-right: 66px; }
  .lang-switch { left: calc(100% - 20px - var(--lang-right)); }
}

/* Header navigation: the current section is underlined in the dark ink
   colour on every link (it used to be green / purple). Hover is unchanged. */
.site-nav__list a[aria-current="true"],
.site-nav__list a.nav--fintech[aria-current="true"],
.site-nav__list a.nav--agri[aria-current="true"] { border-color: var(--ink); }

/* Ukrainian: "Our Approach" keeps four cards in one row (the longer text made
   the auto-fit grid drop the fourth card to a second row on narrower desktops
   and zoomed browsers). Equal-height cards, tops aligned; English unchanged. */
@media (min-width: 1000px) {
  html[lang="uk"] .our-values-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 1.4rem;
    row-gap: 1.4rem;
    align-items: stretch;
  }
  html[lang="uk"] .our-values-grid .value-card { padding: 1.6rem 1.25rem; }
}

/* Ukrainian: labels are longer, so tile text can run to a different number of
   lines tile by tile. Pin the figure to the top so the numbers in a row line up. */
html[lang="uk"] .case-stat:not(.case-stat--text) { justify-content: flex-start; }

/* Ukrainian "Our Approach": the hover-expand is half as strong (grows by 15%
   instead of 30%) -- the cards are closer together in the Ukrainian layout. */
@media (prefers-reduced-motion: no-preference) and (min-width: 761px) {
  html[lang="uk"] .our-values-grid .value-card:hover { transform: scale(1.15); }
}

/* Ukrainian: the Agri > Intelligence "Pricing system" lead line is longer than the
   global paragraph measure (62ch), which left its last two words alone on a second
   line. Let it use the full block width. */
html[lang="uk"] .pricing-block > p { max-width: none; }

/* Agri > Intelligence: the same divider line between its sub-sections that
   Agri > Services has (2px, Agri tint; 2rem of space on either side). The line
   sits under About the tool, under How it works and under Current functionality,
   i.e. above How it works, Current functionality and Pricing system. */
#agri.is-expanded #agri-intelligence-panel .about-tool-grid,
#agri.is-expanded #agri-intelligence-panel .intel-process,
#agri.is-expanded #agri-intelligence-panel .info-block--part1 {
  border-bottom: 2px solid var(--agri-tint);
  border-radius: 0;
  padding-bottom: 2rem;
}
#agri.is-expanded #agri-intelligence-panel .intel-process,
#agri.is-expanded #agri-intelligence-panel .info-block--part1,
#agri.is-expanded #agri-intelligence-panel .pricing-block { margin-top: 2rem; }

/* Mobile: Agri case 3's Value tiles are slimmer (less padding, 1rem text, tighter
   gap). Case 3 is the tallest slide in the case carousel and sets its height, so
   this shortens the empty space under cases 1 and 2. Desktop is unchanged. */
@media (max-width: 760px) {
  .case-stats--agri { gap: 0.6rem; }
  .case-stats--agri .case-stat { padding: 0.8rem 1rem; }
  .case-stats--agri .case-stat--text .case-stat__label { font-size: 1rem; line-height: 1.2; }
}

/* =============================================================
   Sample report button: a floating pill, bottom-LEFT (the "Request a call" pill owns the bottom-right
   corner). Modelled on .float-contact: position: fixed, 20px offsets, pill, same focus ring. It lives at
   the end of <body> because the open Intelligence panel is a scroll container with overflow:hidden
   ancestors (so position: sticky cannot work) and a transformed body (which would re-anchor any fixed
   child). js/sample-popup.js adds .is-visible only while the Agri panel is open, the Intelligence item is
   expanded, its content is in view and the popup is closed.
   Visual height 54px (>= the 44px tap minimum, so no extra tap padding). Fill is --agri-main (white text
   4.7:1, AA); glow is the same green, a CSS-only 3s pulse on box-shadow, static under reduced motion.
   ============================================================= */
.sample-float {
  position: fixed;
  left: calc(20px + env(safe-area-inset-left, 0px));
  bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  z-index: 205;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 54px;
  max-width: calc(100vw - 40px - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
  padding: 0 1.5em;
  border-radius: var(--radius-pill);
  background: var(--agri-main);
  color: #fff;
  font-weight: 700;
  font-size: 0.85rem;
  line-height: 1.2;
  text-align: center;
  white-space: nowrap;
  box-shadow: 0 0 16px 2px rgba(67, 130, 52, 0.45);
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease), visibility 0s linear var(--dur-fast);
}
.sample-float.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
  transition-delay: 0s;
}
.sample-float:hover { background: var(--agri-deep); }
@media (prefers-reduced-motion: no-preference) {
  .sample-float.is-visible { animation: sample-report-glow 3s ease-in-out infinite; }
  .sample-float.is-visible:hover,
  .sample-float.is-visible:focus-visible { animation-play-state: paused; }
}
@keyframes sample-report-glow {
  0%, 100% { box-shadow: 0 0 8px 0 rgba(67, 130, 52, 0.25), 0 0 0 0 rgba(67, 130, 52, 0); }
  50%      { box-shadow: 0 0 22px 4px rgba(67, 130, 52, 0.6), 0 0 0 6px rgba(67, 130, 52, 0.16); }
}
/* Room at the end of the Intelligence panel so the floating pill never covers the last content
   (54px pill + 20px offset + breathing space). */
#agri-intelligence-panel .accordion-panel__body { padding-bottom: calc(1.5rem + 94px); }

/* Sample report popup (<dialog id="sample-popup">, see js/sample-popup.js). ~900px wide, 90% of the
   viewport high. Opened only at >=700px; the dialog is display:none unless open. */
.sample-popup {
  width: min(900px, calc(100vw - 2rem));
  height: 90vh;
  height: 90dvh;
  max-width: none;
  max-height: none;
  margin: auto;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #fff;
  color: var(--ink);
  box-shadow: 0 24px 70px rgba(23, 25, 31, 0.45);
  overscroll-behavior: contain;
}
.sample-popup[open] { display: flex; flex-direction: column; }
.sample-popup::backdrop { background: rgba(23, 25, 31, 0.62); }
.sample-popup__bar {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0.75rem 0.5rem 1.1rem;
  border-bottom: 1px solid var(--hairline);
}
.sample-popup__title { font-weight: 700; font-size: 0.9rem; }
.sample-popup__actions { display: flex; align-items: center; gap: 0.6rem; }
.sample-popup__demo {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 1.2em;
  border-radius: var(--radius-pill);
  background: var(--agri-main);
  color: #fff;
  font-weight: 700;
  font-size: 0.88rem;
}
.sample-popup__demo:hover { background: var(--agri-deep); }
.sample-popup__close {
  min-height: 44px;
  padding: 0.5em 1.2em;
  border: 1px solid var(--ink);
  border-radius: var(--radius-pill);
  font-weight: 600;
  font-size: 0.88rem;
  color: var(--ink);
}
.sample-popup__close:hover { background: var(--ink); color: #fff; }
.sample-popup__frame { flex: 1 1 auto; width: 100%; min-height: 0; border: 0; background: #fff; }
/* Scroll lock while the popup is open. */
html.sample-popup-open,
html.sample-popup-open body { overflow: hidden; }
