/* Appex — deliberate corrections, loaded AFTER the design's site-header.js so it wins.
 *
 * Everything here fixes something that is wrong in the design too (measured at 1440px),
 * or is an explicit improvement that was asked for. Nothing here is a re-implementation
 * of design behaviour — the design's own scripts still own all of that.
 */

/* ---------------------------------------------------------------------------
   1. Industries submenu clipped its longest label.
   Measured in the design: "Hospitality, Travel & Transportation" is 236px of text
   in a 220px column, because .nav-menu-grid is pinned to min-width:460px and split
   into two equal 1fr columns. Letting the columns size to their content fixes it
   without changing the two-column layout.
   --------------------------------------------------------------------------- */
.nav-menu-grid{
  display: grid !important;          /* .nav-menu-inner is a flex column; the grid has to win */
  grid-template-columns: repeat(2, max-content) !important;
  /* Column-first, so the list reads down the left column and then down the
     right. `--nav-rows` comes from the view, which counts the children: a fixed
     number here would strand items in a third implicit column the moment an
     industry is added. */
  grid-auto-flow: column;
  grid-template-rows: repeat(var(--nav-rows, 7), auto);
  min-width: 0 !important;
  column-gap: 10px;
}

/* ---------------------------------------------------------------------------
   1b. Submenu links hover exactly like their parent nav item.
   Parent items hover to `color:#b45f1e; border-bottom-color:#b45f1e; scale(1.08)`,
   but the dropdown links only changed colour and added a white tint — so the two
   levels of the same menu behaved differently. Matched to the parent here.

   The transparent border sits on the resting state so hovering never shifts layout,
   and the origin is left-centre because these are stacked rows rather than a
   horizontal bar (the design's own mobile panel scales from left for the same reason).

   `!important` is needed because each page's own <style> block is emitted after this
   stylesheet and carries `.nav-menu-inner a:hover` at equal specificity. The design's
   scopeHoverRules() then wraps this in `(hover:hover) and (pointer:fine)`, so it stays
   desktop-only and the mobile panel keeps its own treatment.
   --------------------------------------------------------------------------- */
/* Shared timing for both levels. Parent items carry an inline `transition:` and each
   page's own <style> sets `.nav-menu-inner a{transition:… .15s}` AFTER this file, which
   is why the submenu faded quicker than the parent — hence !important on both. */
[data-header-inner] nav a,
[data-header-inner] nav .nav-menu-inner a{
  transform-origin: left center;
  transition: color .18s ease,
              transform .22s cubic-bezier(0.34, 1.56, 0.64, 1),
              background .18s ease !important;
}

/* No underline on hover at either level. The parent's orange border-bottom comes from
   a compiled style-hover class (`.hXXX:hover`, specificity 0,2,0), so this needs both
   !important and a more specific selector to unset it. */
[data-header-inner] nav a:hover,
[data-header-inner] nav .nav-menu-inner a:hover{
  color: #b45f1e !important;
  border-bottom-color: transparent !important;
}

/* Submenu picks up the parent's lift; the white tint goes, since parents have none. */
[data-header-inner] nav .nav-menu-inner a:hover{
  transform: scale(1.08) !important;
  background: transparent !important;
}


/* ---------------------------------------------------------------------------
   1c. Dropdown panels were sized exactly to their longest label.
   With max-content columns (section 1) the Industries panel is as wide as its
   content and no wider, so "Hospitality, Travel & Transportation" sat 22px from
   the visible border of .nav-menu-inner while every shorter label had 115px+.
   Room is added to both insets so the panel grows evenly and the hover highlight
   grows with it: text-to-border goes 22px -> 42px, panel 504px -> ~575px.

   The column-gap here is also what section 1 was trying to set: its plain
   `column-gap:10px` loses to `gap:2px` in the page's own <style> (equal
   specificity, later in source order), which is why the measured gap was 2px.
   Row gap is deliberately left at the design's 2px for the stacked rows.
   --------------------------------------------------------------------------- */
/* Asymmetric on purpose. The hover is `scale(1.08)` from `transform-origin:
   left center` (section 1b), so a row only ever grows to the RIGHT — by 8% of
   the distance from its left edge to the end of its text. On the longest label
   that is around 20px, which is more than the 18px of panel padding it had, so
   the text crossed its own background on hover. Room goes where the growth goes;
   the panel is sized to its content, so the extra is invisible at rest. */
[data-header] .nav-menu-inner{
  padding: 8px 34px 8px 18px !important;
}
[data-header] .nav-menu-inner a{
  padding-left: 24px !important;
  padding-right: 24px !important;
}
/* The gap has the same job as the right padding above, between the columns
   instead of at the edge: the left column's widest row grows 8% to the right on
   hover and 14px was not enough to hold it, so a hovered "Επαγγελματικές
   Υπηρεσίες" ran into "Εστίαση, Τουριστικά & Μεταφορές" beside it. Sized for the
   growth of a full-width row rather than for the labels we have today, because
   the widest label changes with every translation. */
[data-header] .nav-menu-grid{
  column-gap: 44px !important;
}

/* ---------------------------------------------------------------------------
   2. The language picker's gap fix is GONE, and why.
   It read `[data-header-inner] > div[data-lang-picker][data-lang-picker]{gap:4px}`,
   with the attribute doubled to outrank a `> div:last-child{gap:clamp(10px,1.4vw,20px)}`
   that site-header.js injects at runtime. The picker was that last child. It is a
   nested child of the actions group now — and absolutely positioned out of that
   flex besides — so neither the injected rule nor any gap reaches it, and its own
   `gap` in the markup simply applies.
   --------------------------------------------------------------------------- */

/* 3. The `right`/`left` pins that used to sit here are still GONE, even though the
   picker hangs under the header again. They existed to argue with alignLangPicker()
   over where a detached picker sat; that aligner is deleted, and the markup's own
   `right:0` — against a header inner that is already `position:relative` — is the
   placement. A pin here would only be a second opinion. */

/* ---------------------------------------------------------------------------
   4. Selected-flag border.
   site-header.js paints the active flag with a #b45f1e border on click. That reads
   as an error state, and looked wrong inside the mobile panel. Keep the selected
   state as opacity only — which is how the picker already shows it at rest.
   --------------------------------------------------------------------------- */
[data-lang-picker] > a,
[data-lang-picker] > button{
  border-color: transparent !important;
}
/* In the mobile panel the flags are CLONES, not the picker itself — mobileNav() copies
   each anchor and writes `border:1px solid #b45f1e` inline on the active one. They are
   the only titled anchors in the panel, so this reaches them without catching nav links. */
[data-mnav-panel] a[title]{
  border-color: transparent !important;
}

/* ---------------------------------------------------------------------------
   4b. The picker was twice the size on /contact-us/ and /el/contact-us/ only.

   The header CTA is styled by href substring, in the page's inline <style>:

       [data-header-inner] a[href*="contact-us"]{
         padding: clamp(8px,0.95vw,10px) clamp(11px,1.5vw,18px) !important;
         font-size: clamp(11.5px,1.12vw,14px) !important; }

   On the contact page the picker's own flag links point at exactly /contact-us/ and
   /el/contact-us/, so they matched that selector and were dressed as buttons: each anchor
   went from 19x15 to 53x33, and the picker from 48x21 to 116x39. Position was never the
   problem — alignLangPicker() faithfully re-centred the bigger box under the partner badge
   (measured: picker centre 1226 = badge centre 1226 on every page), so it sat in the right
   place at the wrong size, which is why it read as "different" rather than misplaced.

   Re-assert what the markup already writes inline, and do it for ALL picker links rather
   than just the two that collide, so a future page whose slug happens to match another
   header-button selector cannot bring this back.

   Specificity matters here and is easy to get wrong: the design's rule is (0,2,1) AND
   !important, and its inline <style> comes AFTER this file in the document (site-fixes.css
   is line 28, the style block line 32), so an equal-specificity !important still loses.
   The trailing [href] takes this to (0,3,1) and settles it. */
[data-header-inner] [data-lang-picker] > a[href]{
  /* Matches the tile the markup writes. It used to re-assert a flat `1px`, which
     was the picker's padding while the selected language was shown by opacity
     alone; a filled tile needs room around the flag or the white sits on its
     edges. Changing one without the other silently flattens the tile again. */
  padding: 5px 8px !important;
  /* Concrete, not `inherit`. While the picker was two SVG flags the font size did
     not matter and inheriting was the cheapest way to unset the CTA's clamp; the
     labels are text now, and inheriting hands them the header's 14px body size
     instead of the 12px the markup asks for. These three have to track the tile
     in header.blade.php — changing one alone flattens it silently. */
  font-size: 12px !important;
  display: flex !important;
}

/* ---------------------------------------------------------------------------
   4c. The menu's "Contact Us" was wearing the CTA's padding.

   Same cause as 4b, one row up in the header: the design styles its header CTA by
   href substring, in the page's inline <style> —

       [data-header-inner] a[href*="contact-us"]{
         padding: clamp(8px,0.95vw,10px) clamp(11px,1.5vw,18px) !important; }

   — and the navigation menu has a "Contact Us" entry pointing at the same URL. It
   matched, took 11-18px of horizontal padding, and took none of the background that
   would have made it read as a button. So it sat in the row looking like the other
   five links with roughly twice the gap in front of it, which is how it was spotted.

   Scoped to `nav` because the real CTA is the sibling <div>, not in the menu, and it
   must keep its padding. Specificity: the design's rule is (0,2,1) !important and its
   inline <style> comes after this file, so an equal-specificity !important would lose;
   `nav a[href]` takes this to (0,2,2) and settles it. The value is the one the markup
   already writes inline on every top-level entry.
   --------------------------------------------------------------------------- */
[data-header-inner] nav a[href]{
  padding: 4px 0 !important;
}

/* ---------------------------------------------------------------------------
   5. Booking modal cut the Google Calendar header ("Faidon Giac…", Share button).
   The design caps the dialog at 900px; the embedded scheduler needs more room
   before it collapses its own header.
   --------------------------------------------------------------------------- */
[data-booking-modal] > div{
  max-width: 1080px !important;
  height: min(760px, 90vh) !important;
}

/* ---------------------------------------------------------------------------
   6. The marquees ("belts") must ease to a stop, not stop dead.
   The design does this with style-hover="animation-play-state:paused", which is
   instantaneous by definition. site-fixes.js ramps the animation playbackRate
   instead, so the compiled hover rule has to be neutralised here or it would
   still hard-pause on the way in.
   --------------------------------------------------------------------------- */
/* The doubled attribute selector is load-bearing. Each strip carries
   style-hover="animation-play-state:paused", compiled to `.hXXX:hover{…!important}` at
   specificity (0,2,0) — which outranked a plain `[data-marquee-eased]` (0,1,0), so the
   CSS hard-pause was still firing and beating the JS easing. Worse, the strip is
   `width:max-content`, so its box reaches well past the visible band and the pause
   triggered with the cursor apparently outside the belt. (0,3,0) settles it. */
[data-marquee-eased],
[data-marquee-eased][data-marquee-eased]:hover{
  animation-play-state: running !important;
}

/* ---------------------------------------------------------------------------
   Client logo belt: one row instead of two, logos 25% larger, gaps unchanged.
   The band holds two strips (80s forward, 67s reverse) carrying the IDENTICAL 18
   logos, so hiding the reverse row loses no client. Size comes from the link height
   and the image cap; the 48px margin-right is left alone, so the spacing between
   logos is exactly as before. Hover is already zoom-only (scale 1.15, no colour) and
   the per-item pause in site-fixes.js binds to these links like any other belt item.
   --------------------------------------------------------------------------- */
[data-client-marquee] > div:nth-of-type(2){
  display: none !important;
}
[data-client-marquee] a{
  height: 50px !important;              /* was 40px */
}
/* Baseline cap only; the real per-logo sizes are set by area further down (section 24). This is
   raised to the row-safe maximum so it can no longer cap those values - max-height beats height,
   which is exactly how the first attempt at section 24 came out half-applied. */
[data-client-marquee] img{
  max-height: 44px !important;
}

/* The per-logo knobs that used to live here (cardlink 39px, krikri 50.7px, boussias/epignosis/codehub
   26px against a 32.5px baseline) are GONE. They balanced by height, which is the wrong target for a
   logo wall: it left rendered areas spanning 1026-7816 px2, so wordmarks read as much larger than
   square marks. Sizing is now by area, per logo, in section 24 at the end of this file. */

/* Industry belt items hovered orange (#C85A12) AND zoomed. Zoom only — the colour is
   held at its resting white, while the scale(1.12) from the compiled style-hover class
   still applies. Only the industries belt uses text links inside a marquee strip, so
   this cannot reach the logo or certification belts. */
[style*="appexMarquee"] a:hover{
  color: #fff !important;
}

/* The home hero cell field used to be re-animated from here — field opacity, a 0.75 cell
   scale, a shorter sway throw and a faster twinkle. All of it now lives in the DESIGN source
   (design/Appex Home.dc.html and the EL twin), so it can be edited in the Design project like
   any other part of the page, and this layer no longer touches the hero at all.

   Removing it was mandatory, not cosmetic. The rule here pinned animation-name to
   `appexHeroSwayToMiddle`, a name invented by this layer because it could not edit the design's
   keyframes; baking the change into the design's own `appexHeroSway` made that name nonexistent.
   The matching JS (compressCellLattice) had to go for a sharper reason — it would have
   compressed the already-compressed lattice a second time on every page load. */

/* ---------------------------------------------------------------------------
   ONE CONTENT MEASURE SITE-WIDE.
   Section wrappers are declared inline as `max-width:Npx; margin:0 auto; padding:… 32px`
   with N varying per section — 1200, 1100, 1000, 900, 820, 760, 700. Because each is
   centred at its own width, each lands on a different left edge, so headings started at
   145 / 163 / 195 / 213 / 245 / 263 / 303 down a single page.

   The reference is the About page's "What We Do" section: max-width 1200 with 32px
   padding, i.e. a content edge of 145 at a 1440 viewport. Every wrapper is normalised
   to it, so every heading and first line starts at the same x on every page.

   The `32px` guard is the section-wrapper signature (its horizontal gutter). Inner
   blocks that merely cap their own text width carry no 32px gutter, so they are left
   alone and nothing that was never a section measure gets stretched.

   BOTH spellings of each max-width are listed on purpose. These are inline styles, and
   an attribute selector matches the literal attribute string — but the reveal system
   writes `el.style.transition` on any [data-reveal] wrapper, which makes the browser
   RE-SERIALISE the whole style attribute: `max-width:1000px` becomes `max-width: 1000px`
   and `margin:0 auto` becomes `margin: 0px auto`. Matching only the authored spelling
   silently missed every animated wrapper — which is why one heading stayed at 245.
   --------------------------------------------------------------------------- */
[style*="32px"][style*="max-width:1100px"], [style*="32px"][style*="max-width: 1100px"],
[style*="32px"][style*="max-width:1000px"], [style*="32px"][style*="max-width: 1000px"],
[style*="32px"][style*="max-width:900px"],  [style*="32px"][style*="max-width: 900px"],
[style*="32px"][style*="max-width:820px"],  [style*="32px"][style*="max-width: 820px"],
[style*="32px"][style*="max-width:760px"],  [style*="32px"][style*="max-width: 760px"],
[style*="32px"][style*="max-width:700px"],  [style*="32px"][style*="max-width: 700px"]{
  max-width: 1200px !important;
}

/* A few 1200px wrappers put their 32px gutter on the outer band instead of on the
   wrapper itself, so their content edge landed at 113 rather than 145 (the Ecosystem
   page's agent sections). Giving those the standard gutter brings them onto the same
   edge; wrappers that already declare 32px are excluded so nothing is double-padded. */
[style*="max-width:1200px"][style*="auto"]:not([style*="32px"]),
[style*="max-width: 1200px"][style*="auto"]:not([style*="32px"]){
  padding-left: 32px;
  padding-right: 32px;
}

/* Headings carried their own narrower caps — 900px (x26), 780px (x26), 820px (x12),
   860px (x8), 800px (x2) — so a headline wrapped long before the content edge and left
   a much wider gap on the right than on the left, with line breaks that read as
   arbitrary. Uncapping lets every heading run to the same measure as its section, so the
   right margin equals the left and the text breaks only where it actually must.
   Only 74 of 719 headings are capped, and all the capped values are section-headline
   widths rather than card widths, so nothing inside a card is affected. */
section h1[style*="max-width"],
section h2[style*="max-width"],
section h3[style*="max-width"]{
  max-width: none !important;
}

/* ---------------------------------------------------------------------------
   7. "The Salesforce Advantage" band on the home page sat as a 1200px white card
   on the dark hero, showing dark edges either side. Full-bleed the band and keep
   its content on the same 1200px measure as the rest of the page.
   --------------------------------------------------------------------------- */
section.fix-fullbleed{
  max-width: none !important;
  width: 100% !important;
  padding-left: 0 !important;
  padding-right: 0 !important;
}
/* Its inner block is 760px wide with no 32px gutter, so the site-wide measure rule above
   does not catch it. Bring it onto the same 1200px measure and gutter here, otherwise
   centring a 760px block puts its heading at 333 instead of the site's 145. */
section.fix-fullbleed > *{
  max-width: 1200px !important;
  margin-left: auto !important;
  margin-right: auto !important;
  padding-left: 32px;
  padding-right: 32px;
}

/* Body copy carried its own caps, narrower than the section it sits in.
   Paragraphs are declared with `max-width:Npx` — 800px (x154), 780px (x52), 720, 660,
   640, 620, 560, 460 — while the heading directly above them now runs to the section
   measure. So a heading reached x 1580 and the paragraph under it broke at ~1030,
   leaving right gaps of 461 / 481 / 501 / 605 against a left edge of 145: the early,
   arbitrary-looking line breaks. Uncapping puts body copy on the same measure as its
   heading, so the right margin equals the left and text breaks only where it must.

   Unlike the wrapper rules above, this selector names only the PROPERTY, not a value,
   so it is immune to the style-attribute re-serialisation problem — `max-width:800px`
   and `max-width: 800px` both contain the substring `max-width`. No second spelling
   is needed here, and listing one would be dead weight.

   `li` is included for the same reason h1/h2/h3 are listed together above: no list item
   is capped today, but body copy in a list would carry the identical fault. */
section p[style*="max-width"],
section li[style*="max-width"]{
  max-width: none !important;
}

/* Trailing <br> is handled in site-fixes.js, NOT here, and the reason is a trap worth
   remembering: **`br:last-child` counts only ELEMENT children, so a trailing text node does
   not stop a <br> from matching it.** In "4 systems to become<br>an Agentic Enterprise" the
   <br> is the only element child, so it matched `br:last-child`, got display:none, and the
   heading rendered as "becomean Agentic Enterprise" — the break gone and no space in its
   place. Only JS can tell a genuinely trailing <br> from a mid-sentence one, because only JS
   can look at childNodes. See stripTrailingBreaks(). */

/* One level deeper: a bare inner wrapper that caps the text block itself.
   The Ecosystem page puts its two section intros ("HOW APPEX FITS IN", and the
   Agentforce band) inside `<div style="max-width:820px">` — an inner block that carries
   no 32px gutter, so the section-measure rule above deliberately leaves it alone, and
   no `max-width` of its own on the heading or paragraph, so the two rules above cannot
   reach it either. The result was three paragraphs stranded at 820px inside a 1136px
   measure: a right gap of 461 against a left edge of 144.

   Identified by :has(> heading) rather than by a width, because the same shape recurs at
   820px, 660px and 640px, and hardcoding values missed the 660px one — which caps "A
   solution for every business function & need", a heading needing 744px that therefore broke
   across two lines. "A div that contains a heading" is what these wrappers actually are, and
   it also keeps the rule off cards and off the booking modal (a 900px capped div), so this
   stays a rule about section intros rather than a list of magic numbers. Naming only the
   PROPERTY also sidesteps the re-serialisation trap: `data-reveal` on these wrappers rewrites
   the attribute to `max-width: 660px`, which a value-matching selector would miss.

   Three guards. `auto` spares deliberately centred blocks — one 820px wrapper is
   `position:relative; max-width:820px; margin:0 auto`, the "Ready for the Next Step?" CTA
   band, which uncapping would stretch across the page. `760px` spares the home page's
   "Salesforce Advantage" block, which the fix-fullbleed rule below already holds at the
   1200px measure and which this rule would otherwise outrank. `width:100%` is a belt for
   the modal. */
section div[style*="max-width"]:has(> h1, > h2, > h3):not([style*="auto"]):not([style*="760px"]):not([style*="width:100%"]){
  max-width: none !important;
}

/* The job-ad article column used to be centred here, to match a hero that was itself centred on an
   840px measure. That hero now sits on the standard 1200px measure like every other page, so the
   centring has gone with it — the body and the hero share one left edge again, which is what made
   the centring look right in the first place.

   The 22 blog pages were unaffected either way: their article is a grid item that already fills its
   790px track, so auto margins had no free space to distribute and the rule was a no-op there. */


/* ---------------------------------------------------------------------------
   8. Buttons: one hover for every CTA, and no orange on the hero's ghost button.

   The design gives a button TWO hover behaviours at once: site-header.js tags each one
   `data-btn` and injects `[data-btn]:hover{transform:scale(1.045)}`, and the button also
   carries a compiled colour class. The zoom is consistent everywhere; the colour is not.

   Reference is the home hero's "Book a Meeting" — `.hdkbu23:hover` → background #c96f2a,
   matching border, white text — used by 44 of the 50 orange CTAs. The other 6 (careers
   "See Open Roles" and "Send Your CV", the booking-modal openers) carry `.h1ogoub:hover`,
   which changes only the TEXT and BORDER to #6fb0ff and leaves the background orange, so
   the label went pale blue on an orange field. Every element carrying that class has an
   orange background, so re-pointing it is safe.

   Targeted by the resting background rather than by class, deliberately: `.hdkbu23` /
   `.h1ogoub` are content-hashed names that change whenever the design's hover declarations
   change, whereas `background:#b45f1e` is the button's own definition of being a primary
   CTA. This also means a new orange CTA gets the right hover for free.
   --------------------------------------------------------------------------- */
a[style*="background:#b45f1e"]:hover,
a[style*="background: #b45f1e"]:hover,
button[style*="background:#b45f1e"]:hover,
button[style*="background: #b45f1e"]:hover{
  background: #c96f2a !important;
  border-color: #c96f2a !important;
  color: #fff !important;
}

/* The hero's secondary "Our Services" is a ghost button — translucent white on the dark
   hero — but it shares `.hdkbu23`, so it turned solid orange on hover while its neighbour
   did the same thing, losing the primary/secondary distinction the two-button pair exists
   to make. Held at its resting fill and border so the only thing that happens is the
   `[data-btn]` zoom, which is what the pair needs to stay legible against the artwork. */
a[style*="rgba(255,255,255,0.08)"]:hover{
  background: rgba(255,255,255,0.08) !important;
  border-color: rgba(255,255,255,0.2) !important;
  color: #fff !important;
}

/* ---------------------------------------------------------------------------
   9. The featured blog card had no hover at all.
   The 11 cards below it carry `.h1wzuspd:hover` (lift 4px + a soft shadow), but the
   featured card at the top of /blog/ carries no hover class, so the one card most
   likely to be clicked was the only one that did not respond to the pointer. Matched to
   its siblings exactly — same distance, same easing, same shadow — rather than inventing a
   second treatment.

   `[data-featured]` is the hook, and it is the second one. The first was
   `a[style*="1.05fr"]` — the featured card was then the only element on the page laid out
   as a `1.05fr 1fr` grid, which described its structure instead of a class hash. Then the
   card had to stop collapsing on a phone, its columns became
   `repeat(auto-fit, minmax(320px, 1fr))`, and the substring went with them: the rule
   matched nothing and the hover was silently gone again, with the CSS still sitting here
   looking correct. A style-substring selector is a guess about markup that has no reason to
   hold still. The attribute is on the anchor in blade, put there to be selected.

   The `transition` below is a fallback, not the operative timing. Both this card and its
   siblings carry an inline `transition: opacity .7s …, transform .7s …` written by the
   reveal system, and an inline declaration outranks a stylesheet one — so in a normal
   browser the lift eases over 0.7s on both. That is the design's existing behaviour for the
   11 cards, and matching them was the point. This declaration only takes over if the reveal
   transition is absent (no JS), where it keeps the lift from snapping.
   --------------------------------------------------------------------------- */
a[data-featured]{
  transition: transform .22s ease, box-shadow .22s ease;
}
a[data-featured]:hover{
  /* !important is load-bearing here. The reveal system sets el.style.transform to "none" as an
     INLINE style when a card scrolls into view, and an inline declaration beats a plain stylesheet
     rule — so this card sat completely still on hover while the 11 below it moved. Those work
     because their compiled .h1wzuspd:hover carries !important; this now matches them. */
  transform: translateY(-4px) !important;
  box-shadow: 0 22px 44px -30px rgba(11,31,51,0.45) !important;
}

/* ---------------------------------------------------------------------------
   10. Hero/CTA glows banded on narrow viewports — the CLIP, not the fade.
   Each dark hero and CTA carries decorative radial glows, authored SQUARE (760x760,
   700x700, 600x600, 460x460) with `border-radius:50%`. The gradient inside is a
   `circle`, which defaults to farthest-corner sizing — for a square box that radius is
   `hypot(w/2,h/2)`, and at the authored 68-72% stop the paint fades to nothing at ~0.48w,
   comfortably inside the circular clip. Smooth, which is why desktop always looked right.

   On a narrow viewport site-header.js clamps the width itself: it walks every element with a
   fixed px width and, where that width exceeds the room available, writes `width:100%;
   max-width:100%` inline (that is what the `data-w0` attribute on these layers records — the
   original width, so it can be restored when the viewport widens). It does NOT touch
   `height`, so a 760x760 glow becomes a 400x760 box. Two things
   then go wrong at once: `border-radius:50%` makes the clip an ellipse whose narrow
   semi-axis is only 200px, while farthest-corner sizing GROWS the gradient radius to
   hypot(200,380)=429 — so the paint is still at ~30% strength where the ellipse chops it.
   That produces a hard edge across the band. Measured down the centre of the 400x340 About
   hero, composited luminance jumped 37 -> 42 in a single step (max step 4.8) at exactly
   that boundary, against 1.2 for the same section on desktop. That step is the "third
   colour" — it is a clipped edge, not a gradient stop.

   Restoring squareness fixes it at the source: the box is square again, farthest-corner
   sizing returns to hypot(200,200)=283, the paint completes at 192px inside a 200px
   semi-axis, and the clip has nothing left to cut. Re-measured: max step 2.0, no local
   maxima — desktop reads 1.2. The glow still contributes (profile range 21->34), so this
   dims nothing; it removes an artefact.

   A no-op on desktop, where the width is never clamped: height resolves from the same
   authored width, so 760 stays 760. Verified across all 212 glows: every one is authored
   square and anchored on ONE vertical edge (top or bottom, never both), so deriving height
   from width cannot stretch or move any of them.
   --------------------------------------------------------------------------- */
div[style*="border-radius:50%"][style*="radial-gradient"],
div[style*="border-radius: 50%"][style*="radial-gradient"]{
  aspect-ratio: 1;
  height: auto !important;
}

/* ---------------------------------------------------------------------------
   12. Header: bar +20% WIDER, logo +15%, nav type +10%.

   Measured baseline at 1440px: bar 1120px wide and 66px tall, logo 38px, nav and sub-items 14px.
   The +20% is on the WIDTH — max-width 1120 -> 1344. An earlier pass grew the height instead (inner
   padding 12 -> 14.4 and the partner badge 40 -> 48, since those two are what set the bar height);
   both are reverted here, so the bar is back to its authored height and only wider.

   The bar reaches 1344 only where there is room (1344 + 48px side padding = 1392), so below about
   1392px it fills the available width exactly as before and nothing overflows.

   Every selector outranks the rule it replaces: responsiveCSS() injects its <style> at runtime, so
   it lands after this file and is all !important — among !important declarations the higher
   specificity wins regardless of order, hence the extra attribute selector on each (and a repeated
   attribute where it would otherwise tie and lose on order).
   --------------------------------------------------------------------------- */
/* The bar sits flush to the top — no strip of page above it. The 3px scroll-progress bar is fixed
   at 0..3 and draws along the header's top edge (z-index 200 over the header's 100) rather than
   floating above it. This is page margin, not header size, so it is not scaled. */
[data-header][data-header]{
  padding-top: 0 !important;
}

/* Full width at ANY viewport, which is what the +20% was reaching for. Capped at 1344px it filled the
   row at a 1265px viewport (100% zoom) but left 280px of gutter at 1920 and 600px at 2560 - which is
   why it looked like a centred pill as soon as the browser was zoomed out. The outer 24px side padding
   is kept, so the bar looks the same as it does at 100% zoom today, at every width. */
[data-header] [data-header-inner]{
  max-width: none !important;
}

/* the Appex logo, +15% on its own clamp */
[data-header] [data-header-inner] > div:first-child > a:first-child img{
  height: clamp(29.9px, 3.565vw, 43.7px) !important;
}
/* top-level nav items, +10% */
[data-header] [data-header-inner] nav{
  font-size: clamp(12.65px, 1.232vw, 15.4px) !important;
}
/* dropdown sub-items, +10%. Their 14px comes from a .nav-menu-inner a rule in the page's own
   <style>, which is not !important, so a plain rule with more weight is enough. */
[data-header] .nav-menu-inner a{
  font-size: 15.4px !important;
}

/* The 781-900px band: the design drops nav type to a flat 11.5px so the row still fits on one line.
   Scaled rather than replaced — a blanket override would re-break that fit. The badge is left at
   the design's size here, as everywhere else. */
@media (min-width: 781px) and (max-width: 900px){
  [data-header] [data-header-inner] nav{ font-size: 12.65px !important; }   /* 11.5 x 1.1 */
}

/* Mobile: the nav is hidden and the viewport is far below the max-width, so only the flush-to-top
   offset applies. */
@media (max-width: 780px){
  [data-header][data-header]{ padding-top: 0 !important; }
}

/* ---------------------------------------------------------------------------
   20. Mobile tap targets and minimum text size.
   Measured at 390px: the mobile nav panel links are already 275x42 and the form
   submit is 161x50, so both are left alone. What was too small: footer text
   links at 18px tall with no padding, and the contact checkbox rows at 23px.
   Guidance is a 44px target and a 12px text floor.
   --------------------------------------------------------------------------- */
@media (max-width: 780px){
  /* Grow the hit area without loosening the footer: padding out, margin back in.
     Links holding an image (logo, social icons) are excluded - they are already
     large enough and padding them would distort the row. */
  footer a:not(:has(img)){
    padding-top: 11px !important;
    padding-bottom: 11px !important;
    margin-top: -7px !important;
    margin-bottom: -7px !important;
  }

  /* The label is already the tap target; it just was not tall enough. */
  form label:has(input[type="checkbox"]){
    padding-top: 10px !important;
    padding-bottom: 10px !important;
  }
  form input[type="checkbox"]{
    width: 20px !important;
    height: 20px !important;
  }

  /* 11px inline labels (service card eyebrows, client names) up to the 12px floor.
     They are class-less spans carrying inline styles, so the style attribute is the
     only handle, and !important is required to beat the inline value. */
  [style*="font-size:11px"]{ font-size: 12px !important; }
  [style*="font-size: 11px"]{ font-size: 12px !important; }
}

/* ---------------------------------------------------------------------------
   21. Light blue bands for the service FAQs and Open Roles.
   Both sections are CENTRED CONTAINERS (max-width 900/1200 with margin:0 auto),
   not full-bleed sections, so putting a background on them directly would paint a
   floating panel instead of a band like the rest of the site.

   The band is produced with a huge box-shadow spread plus clip-path: the shadow
   carries the colour out to the viewport edges, and inset(0 -100vmax) clips it
   vertically so it stops at the section. Deliberately NOT 100vw - that counts the
   scrollbar and would reintroduce the horizontal scroll fixed earlier. box-shadow
   never affects layout, so nothing can overflow.
   --------------------------------------------------------------------------- */
#faqs, #roles{
  position: relative;
  background: #f4f8fc;
  box-shadow: 0 0 0 100vmax #f4f8fc;
  clip-path: inset(0 -100vmax);
}

/* ---------------------------------------------------------------------------
   22. Read-more links hover orange and zoom, like the footer social icons.
   This REPLACES the previous rule here, which pinned these links to stay blue -
   the earlier instruction, now reversed.

   The family is marked at build time (markReadMoreLinks in convert.js) because CSS
   cannot select on text: "More about Salesforce", "More About the Salesforce Partner
   Program", "More About Our Services". Colour and scale copy the footer social icons
   (.h1i3qxj7:hover -> #b45f1e, scale 1.22).

   transform-origin is left centre, not the default centre: these links sit at the end
   of a text block on their own line, so growing from the left keeps the left edge
   anchored and the line does not appear to jump. display:inline-block is required -
   transform does not apply to a plain inline box.
   --------------------------------------------------------------------------- */
/* The contact page's inline "book a slot here" shares this treatment - it had no hover at all. The
   :not() keeps the home page's orange CTA out of it: that one also carries data-booking-open, and
   giving it #b45f1e text would put dark orange on an orange background (see section 8). */
a[data-readmore],
a[data-booking-open]:not([style*="background"]){
  display: inline-block;
  transform-origin: left center;
  /* Plain ease-out, not the design's cubic-bezier(0.34,1.56,0.64,1): a control point above 1
     overshoots the target and settles back, which is what made this read as intense. */
  /* !important because these links carry their OWN inline transition (color .18s ease) on the About
     page, and an inline declaration beats a plain stylesheet rule - so the transform had no transition
     there and the zoom snapped instantly, while the home link (no inline transition) animated correctly. */
  transition: color .2s ease, transform .26s cubic-bezier(0.22, 0.61, 0.36, 1) !important;
}
a[data-readmore]:hover,
a[data-booking-open]:not([style*="background"]):hover{
  color: #b45f1e !important;
  transform: scale(1.06) !important;   /* was 1.22. !important for the same inline-style reason. */
}

/* ---------------------------------------------------------------------------
   23. Dropdowns open on keyboard focus, not only on hover.
   The Services and Industries parents are href-less <a> tags (correctly - neither
   is a page), so they were not in the tab order and the menus could not be opened
   by keyboard at all. They now carry tabindex="0" and role="button"; these rules
   mirror the design's own hover rules so focus behaves exactly like hover.

   :focus-within rather than :focus, so tabbing INTO the panel keeps it open and
   tabbing past the last item closes it - no open/close state to track.
   --------------------------------------------------------------------------- */
.nav-item:focus-within .nav-menu{
  opacity: 1 !important;
  visibility: visible !important;
  transform: translateY(0) !important;
  transition: opacity .18s ease, visibility 0s, transform .18s ease !important;
}
/* Industries is the centred panel, so it keeps its own X translation. More specific
   than the rule above, so it wins. */
.nav-item:nth-child(3):focus-within .nav-menu{
  transform: translateX(-50%) translateY(0) !important;
}

/* A keyboard user has to be able to see where they are. focus-visible only, so a
   mouse click never draws a ring. */
[data-header] .nav-item > a[role="button"]:focus-visible,
[data-header] .nav-menu-inner a:focus-visible{
  outline: 2px solid #0031BC !important;
  outline-offset: 3px;
  border-radius: 4px;
}

/* ---------------------------------------------------------------------------
   24. Client logo belt: even optical size, and no white gap before a CTA.

   The logos carried per-logo HEIGHTS, which is the wrong target for a logo wall:
   equal height makes a wordmark huge and a square mark tiny. Measured rendered
   areas ran 1026 px2 (imbm) to 7816 px2 (panelnetworks) - a 7.6x spread, which is
   why panelnetworks read as much larger than worldline.

   Each logo is now sized from its own aspect ratio to a common target area of
   3800 px2, clamped to 44px tall and 170px wide so nothing outgrows the 50px row.
   A square mark cannot reach a wordmark's area inside that row without being taller
   than the row, so a residual spread of about 2x remains by design.

   Spacing and alignment are untouched: each link hugs its image and carries a fixed
   48px margin-right, so ink-to-ink spacing stays constant as the images resize, and
   the flex row keeps them vertically centred.
   --------------------------------------------------------------------------- */
[data-client-marquee] img[src*="logo-boussias"]{ height: 23.3px !important; max-height: 23.3px !important; width: 163px !important; max-width: 163px !important; }
[data-client-marquee] img[src*="logo-krikri"]{ height: 44px !important; max-height: 44px !important; width: 61px !important; max-width: 61px !important; }
[data-client-marquee] img[src*="logo-cardlink"]{ height: 33.5px !important; max-height: 33.5px !important; width: 113px !important; max-width: 113px !important; }
[data-client-marquee] img[src*="logo-worldline"]{ height: 44px !important; max-height: 44px !important; width: 63px !important; max-width: 63px !important; }
[data-client-marquee] img[src*="logo-obrela"]{ height: 26.2px !important; max-height: 26.2px !important; width: 145px !important; max-width: 145px !important; }
[data-client-marquee] img[src*="logo-epignosis"]{ height: 22.7px !important; max-height: 22.7px !important; width: 167px !important; max-width: 167px !important; }
[data-client-marquee] img[src*="logo-codehub"]{ height: 22.9px !important; max-height: 22.9px !important; width: 166px !important; max-width: 166px !important; }
[data-client-marquee] img[src*="logo-signal"]{ height: 28.8px !important; max-height: 28.8px !important; width: 132px !important; max-width: 132px !important; }
[data-client-marquee] img[src*="logo-ocm"]{ height: 33.3px !important; max-height: 33.3px !important; width: 114px !important; max-width: 114px !important; }
[data-client-marquee] img[src*="logo-afi"]{ height: 44px !important; max-height: 44px !important; width: 86px !important; max-width: 86px !important; }
[data-client-marquee] img[src*="logo-proximed"]{ height: 37.4px !important; max-height: 37.4px !important; width: 102px !important; max-width: 102px !important; }
[data-client-marquee] img[src*="logo-africa-biosystems"]{ height: 43.5px !important; max-height: 43.5px !important; width: 87px !important; max-width: 87px !important; }
[data-client-marquee] img[src*="logo-neurolife"]{ height: 30.3px !important; max-height: 30.3px !important; width: 125px !important; max-width: 125px !important; }
[data-client-marquee] img[src*="logo-panelnetworks"]{ height: 22.7px !important; max-height: 22.7px !important; width: 168px !important; max-width: 168px !important; }
[data-client-marquee] img[src*="logo-medousa"]{ height: 30.3px !important; max-height: 30.3px !important; width: 125px !important; max-width: 125px !important; }
[data-client-marquee] img[src*="logo-otropay"]{ height: 28.6px !important; max-height: 28.6px !important; width: 133px !important; max-width: 133px !important; }
[data-client-marquee] img[src*="logo-imbm"]{ height: 44px !important; max-height: 44px !important; width: 43px !important; max-width: 43px !important; }
[data-client-marquee] img[src*="logo-pext"]{ height: 26.6px !important; max-height: 26.6px !important; width: 143px !important; max-width: 143px !important; }

/* Speed matched to the industry belt above. Both keyframes translate by 50% of the strip, so the
   distance per cycle is width/2: the industry belt covers 4997/2 in 46s = 54.3 px/s, while this one
   covered 6000/2 in 80s = 37.5 px/s. 3000/54.3 = 55.2s. */
[data-client-marquee] [style*="appexMarquee"]{
  animation-duration: 55.2s !important;
}
/* The dark CTA carries margin-top:96px. That was invisible white space when the
   section above was white; now that these two are coloured bands it reads as a
   stripe. Scoped to the adjacent sibling, so only the band-to-CTA case is affected. */
#faqs + section, #roles + section{ margin-top: 0 !important; }

/* ---------------------------------------------------------------------------
   25. One header colour, taken from the hero.
   Headings on light backgrounds came in two inline families: #0031BC (220 of
   them - the vivid blue on section headings, product-card titles and the blog
   category titles) and #152a45 (184 - dark navy). The vivid blue is replaced with
   #0c2b48, which is the hero gradient's own blue stop:
   linear-gradient(160deg,#040d18,#071a30,#0c2b48).

   #152a45 is deliberately untouched: rgb(21,42,69) against rgb(12,43,72) is not a
   distinguishable difference in text, so recolouring those 184 would be churn.

   FAQ titles and the white headings on dark bands are safe BY CONSTRUCTION rather
   than by exclusion - neither carries an inline colour (both inherit), so a rule
   keyed on the inline string cannot reach them.

   Scoped to h1-h3 only. #0031BC is also the body-link colour, and links must keep it.
   --------------------------------------------------------------------------- */
h1[style*="color:#0031BC"],
h2[style*="color:#0031BC"],
h3[style*="color:#0031BC"],
h1[style*="color: #0031BC"],
h2[style*="color: #0031BC"],
h3[style*="color: #0031BC"],
/* The reveal animation rewrites the style attribute, and the browser SERIALIZES colours when it does -
   #0031BC becomes rgb(0, 49, 188), so the hex substring stops matching. Four headings on each industry
   page stayed blue for exactly this reason. Any attribute-substring selector needs both forms. */
h1[style*="rgb(0, 49, 188)"],
h2[style*="rgb(0, 49, 188)"],
h3[style*="rgb(0, 49, 188)"]{
  color: #0c2b48 !important;
}

/* ---------------------------------------------------------------------------
   26. Home hero fills the viewport, so nothing below it peeks in.
   Measured before: 410px of the next section visible at 1920x1080, 142px at 1280x800, while at
   390x844 the hero OVERFLOWED by 205px and its own stats column fell below the fold. The hero is
   content-driven at ~660-670px whatever the screen height, so both a floor and a fit are needed.

   svh, not vh or dvh: svh is the smallest viewport height (mobile chrome visible), so nothing can
   hide under the toolbar and then appear; dvh resizes as the chrome hides and the section visibly
   jumps mid-scroll; 100vh on iOS is the largest viewport, which is how peeking happens.

   Every size below is min(width-term, height-term) so the tighter dimension wins - a short, wide
   window scales down on height instead of overflowing. Gated to >=560px tall because a landscape
   phone cannot hold this content at a readable size; under the gate it flows normally.
   Home only (EN + EL) - a full-screen hero on an article would bury the article.
   --------------------------------------------------------------------------- */
@media (min-height: 560px){
  body[data-route="/"] #top,
  body[data-route="/el/"] #top{
    /* Divided by the wide-screen zoom (see fitWideScreens in site-fixes.js): viewport-unit
       lengths are multiplied by an ancestor zoom, so a plain 100svh hero would render z times
       the screen. Unzoomed screens have no --wide-zoom and the fallback keeps this a no-op. */
    min-height: calc(100svh / var(--wide-zoom, 1));
    display: flex;
    align-items: center;
  }
  /* The wrapper is a grid item now, so it must be told to span the full width - otherwise it
     shrinks to its content and the max-width:1200px + margin:0 auto centring is lost. */
  body[data-route="/"] #top > [data-hero-grid],
  body[data-route="/el/"] #top > [data-hero-grid]{
    width: 100%;
    /* The two paddings are named so the offset below can be derived from them rather than repeating the
       expressions - they are viewport-derived clamps, and two copies would drift apart. */
    --hero-pad-top: clamp(88px, 13vh, 120px);
    --hero-pad-bottom: clamp(36px, 8vh, 96px);
    padding-top: var(--hero-pad-top) !important;
    padding-bottom: var(--hero-pad-bottom) !important;
    gap: clamp(28px, 6vh, 72px) !important;
    /* The hero keeps the shared 1200px measure. A 1320px measure was tried here to widen the text
       column to 972px, on the theory that the break needed more room - it did not; the break was
       blocked by the specificity bug fixed below. Reverted, so the hero content edge lines up with
       every section beneath it again. */
  }
  /* authored: clamp(34px,4.6vw,68px) */
  body[data-route="/"] #top h1,
  body[data-route="/el/"] #top h1{
    font-size: clamp(30px, min(4.6vw, 7.4vh), 68px) !important;
    margin-bottom: clamp(12px, 2.6vh, 26px) !important;
  }
  /* authored: 17px fixed, margin-bottom 36px. Type is 50% larger by request: 17 -> 25.5px, and the
     height term and the floor scale with it (2.15vh -> 3.22vh, 14 -> 21px) so the fit behaviour on
     short screens is preserved rather than reset. */
  body[data-route="/"] #top p,
  body[data-route="/el/"] #top p{
    /* 25.5px is the +50% target and it is what a desktop column gets. The 4.9vw term is what stops it
       on a phone: at 390px wide the column is 354px, and 25.5px there gives a 28-character line (ideal is
       45-75) and pushed 59px of hero content below the fold. 4.9vw lands ~19px / 37 characters instead. */
    /* Max is 21.5px, not the 25.5px first tried. Measured: line one - "...solutions for organizations" -
       is 821px wide at 22px, against an 819px column, so 25.5px could never hold it and the forced break
       orphaned the word on any window tall enough to reach the cap. 21.5px fits at every desktop width.
       This changes nothing in a short window, where 3.22vh already binds first (19.7px at 612px tall). */
    font-size: max(17px, min(21.5px, 3.22vh, 4.9vw)) !important;
    margin-bottom: clamp(16px, 3.6vh, 36px) !important;
  }
  /* the stat column: numbers are 36px, labels 12px - cap the numbers only, the labels are at the
     12px readability floor already */
  body[data-route="/"] #top [data-hero-grid] > div:last-child div[style*="font-size:36px"],
  body[data-route="/el/"] #top [data-hero-grid] > div:last-child div[style*="font-size:36px"]{
    font-size: min(36px, 4.6vh) !important;
  }
  /* buttons keep their 16px label; only the vertical padding gives way */
  body[data-route="/"] #top a[style*="padding:15px 26px"],
  body[data-route="/el/"] #top a[style*="padding:15px 26px"]{
    padding-top: clamp(10px, 1.8vh, 15px) !important;
    padding-bottom: clamp(10px, 1.8vh, 15px) !important;
  }
}

/* ---------------------------------------------------------------------------
   27. The page must not move on its own during a reflow.
   Scroll anchoring lets the browser change the scroll position when content above the viewport
   changes height - and a desktop-to-mobile switch does exactly that (the home hero grows and the
   document measured 4019px -> 4481px). That is the textbook cause of a page jumping to a lower
   section by itself. Anchoring exists to stop late content shifting the page, but every image now
   carries width/height, so it has nothing left to protect and is safe to switch off.
   --------------------------------------------------------------------------- */
html, body{ overflow-anchor: none; }

/* ---------------------------------------------------------------------------
   28. Mobile header: hamburger colour and alignment, and tap feedback in the panel.

   Colour: the toggle was #0031BC while every section heading was unified to #0c2b48 in section 25.

   Alignment: measured at 390x844 the partner badge centres at y=28 and the toggle at y=36 - 8px low.
   The toggle is 44px tall and position:fixed, so top:6px puts its centre on 28. The badge centre is
   stable at 28 at the top of the page AND scrolled (the header condenses without moving it), so one
   fixed value holds in both states.

   Tap feedback: site-header.js gives the panel links scale(1.045)+orange on :hover - which a touch
   screen never fires - and its only :active state SHRINKS to 0.985 and stays grey. So :active mirrors
   the desktop link hover instead. Those rules are injected in a runtime <style>, which sits after this
   file in the cascade, so `html body` is needed to outrank them rather than just !important.
   --------------------------------------------------------------------------- */
[data-mobile-toggle]{
  color: #0c2b48 !important;
  top: 6px !important;
}
html body [data-mnav-panel] a:active,
html body [data-macc]:active{
  color: #b45f1e !important;
  transform: scale(1.06) !important;
}
/* the CTA button in the panel is a filled block - it keeps its own colour treatment and just zooms,
   otherwise the orange text would sit on the orange fill */
html body [data-mnav-panel] a[data-mnav-cta]:active{
  color: #0031BC !important;
  background: #60C8FC !important;
  transform: scale(1.04) !important;
}

/* ---------------------------------------------------------------------------
   29. Hero vertical rhythm, and the optional paragraph break.
   Measured at 1920x1080 before: H1 -26px- lede -40.8px- body -36px- buttons. The 40.8px was a full
   blank line from an authored <br><br>, so the gap between two body sentences exceeded the gap under
   the headline. The lede is now its own <p> and takes the same gap as the H1, leaving the larger gap
   only before the CTAs: 26 / 26 / 36. The sections below run heading -20px- body on a 30.4px line
   height, i.e. about two thirds of a line - the same proportion this now uses.
   --------------------------------------------------------------------------- */
@media (min-height: 560px){
  body[data-route="/"] #top p:not([data-hero-body]),
  body[data-route="/el/"] #top p:not([data-hero-body]){
    margin-bottom: clamp(12px, 2.6vh, 26px) !important;
  }
}
/* The break after "organizations" is UNCONDITIONAL. Two earlier attempts to gate it failed for the same
   underlying reason: the width at which the break is needed depends on the FONT SIZE, and the font size
   is itself viewport-dependent (min(25.5px, 3.22vh, 4.9vw)). A fixed 960px threshold is therefore
   meaningless - at 25.5px line one needs 951px, at 19.7px it needs 735px, and a short window puts the
   font at 19.7px while leaving the column at ~810px, so the gate could never open. Forcing it is what was
   actually asked for: the second sentence always starts a new line.
   The source is "organizations <br>across" - space OUTSIDE the tag - so nothing fuses if the break is ever
   removed again. */

/* SUPERSEDED by a container query - see below. The break is no longer gated on the viewport at all. */

/* The break is gated on the COLUMN, which is what the comment above says a viewport number cannot do.
   Section 31 caps the font at 2.6cqw, and that closes the arithmetic: line one is 37.28em, so when the
   cqw term binds it measures 37.28 x 0.026C = 0.969C and fits ANY column by construction; when the 17px
   floor binds instead (C < 654) it is a fixed 634px. So the break is safe exactly when C >= 634 - one
   number, no font-size dependency. 640 for margin.

   Measured at 900px wide: column 521, font on its 17px floor, line needs 634 - two lines are impossible
   at a readable size there, and the forced break was orphaning "organizations" onto its own line.

   Default off, container query switches it on. That covers narrow widths for free: below 781px section
   31 never creates the container, and an unmatched @container never applies - so the break stays off
   with no second breakpoint to keep in sync. Source is "organizations <br>across", space outside the
   tag, so nothing fuses when the break is off. */
body[data-route="/"] #top p[data-hero-body] br,
body[data-route="/el/"] #top p[data-hero-body] br{
  display: none;
}
@container (min-width: 640px){
  body[data-route="/"] #top p[data-hero-body] br,
  body[data-route="/el/"] #top p[data-hero-body] br{
    display: inline;
  }
}

/* ---------------------------------------------------------------------------
   30. Zoomed wide screens: no content-sized grid tracks.

   Under the >=1441px body zoom (fitWideScreens), Chrome intermittently mis-resolves INTRINSIC grid
   track sizes: observed twice on the home hero grid (authored `minmax(0,1fr) auto`) - once the auto
   KPI track ballooned 252 -> 909px, and once, on the user's machine, the 1fr column collapsed to one
   character per line. The authored template was intact both times; only the USED sizes were wrong,
   and a reload cleared it. The fix is to leave nothing content-sized for the race to mis-measure
   while zoom is active:
   - hero: the auto track becomes fixed 264px (its natural width is 252.25px at every healthy load,
     so nothing visibly moves; the KPI labels wrap exactly as before).
   - footer (the only other content-sized grid in the build, every page): max-content columns become
     equal quarters; justify-items:start keeps the columns' left edges in place.
   Below 1441px the zoom is off, the race has never appeared, and the authored templates stand.
   --------------------------------------------------------------------------- */
@media (min-width: 1441px){
  body[data-route="/"] #top [data-hero-grid],
  body[data-route="/el/"] #top [data-hero-grid]{
    /* 300px, not 264: at 264 the long stat labels ("Client Rating on Salesforce AgentExchange" needs
       273px at 12px) wrapped to two lines, pushing the stats column natural height to 521 against a 451
       text column - so the stats hung ~80px BELOW the buttons and no height pinning could fix it
       (min-height:auto floors a grid item at its content). 300 keeps the track determinate for the
       zoom-corruption guard while letting every label sit on one line. */
    grid-template-columns: minmax(0, 1fr) 300px !important;
  }
  footer > [data-stack-mobile]{
    grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
  }
  /* The hex-icon background tile is a TEXTURE, not content - it should not grow with the zoom.
     Every one of its 113 usages across the build carries the same inline background-size (auto 1400px),
     so dividing by the zoom renders it at exactly 1400 real px on any screen. The attribute selector is
     the only handle CSS has on a background-image url; below 1441px the var is absent and the calc
     resolves to the authored 1400px - a true no-op. */
  [style*="hex-wide"], [data-hex-tile]{
    /* Halved from the authored 1400px per user feedback on the big screen ("reduce cell size in 50%"):
       hexes and the gaps between them are one tile, so both halve together. Wide screens only - below
       1441px the approved laptop rendering keeps the design's own 1400px tile. */
    /* The ORIGINAL tile at 350px. The 'dense' variant it replaces was broken: halving the <use>
       coordinates without the <defs> paths broke the honeycomb tiling, rendering 54.3px icons only
       28.9px apart - a MINUS 25px edge gap, i.e. overlapping. Measured (269 hex frames, real
       nearest-neighbour distances), scaling the original uniformly satisfies both requests at once:
       icon 27.1px = half the previous 54.3, centre distance 28.9px = 75% below the original 115.5,
       and the edge gap is +1.7px instead of negative. Size and spacing are locked by the honeycomb;
       they cannot be dialled independently without redrawing the pattern. */
    background-image: url('/assets/hex-wide.svg') !important;
    background-size: auto calc(350px / var(--wide-zoom, 1)) !important;
  }
}

/* ---------------------------------------------------------------------------
   31. The hero paragraphs fit their column, whatever width it ends up.

   Section 26 caps them at 21.5px because line one ("...solutions for organizations") is 821px at
   22px against what was an 819px column. Section 30 then fixed the stats track at 264px to stop the
   zoomed-grid corruption, which narrowed the text column to 800px - and the line needs 801px at
   21.5px. It missed by ONE pixel, wrapped, and the forced <br> after "organizations" made it three
   lines on every wide screen.

   Rather than restore the pixel (a tolerance that thin breaks again on the next layout change, or
   under the fallback font before Hanken loads), the cap is now container-relative. The line is
   37.28em wide, so 100/37.28 = 2.68cqw fits exactly; 2.6cqw leaves ~3% margin at any column width.

   Desktop only: the <br> is dropped below 780px so wrapping is meant to be natural there, and on a
   354px phone column 2.6cqw would compute 9.2px and drag the paragraph to its 17px floor - worse
   than the 19.1px it gets today. Both paragraphs, so the lede and the body stay matched.
   --------------------------------------------------------------------------- */
@media (min-width: 781px){
  body[data-route="/"] #top [data-hero-grid] > div:first-child,
  body[data-route="/el/"] #top [data-hero-grid] > div:first-child{
    container-type: inline-size;
  }
  body[data-route="/"] #top p,
  body[data-route="/el/"] #top p{
    font-size: max(17px, min(21.5px, 3.22vh, 4.9vw, 2.6cqw)) !important;
  }
}

/* ---------------------------------------------------------------------------
   32. Wide screens: the site measure scales, so every section keeps its share of the width.

   MEASURED at 1440 (clientWidth 1425): every heading sits at x=145, set by an inner wrapper with
   computed max-width 1200px and a 32px gutter - exactly 9 elements carry that measure and all 9 have
   the gutter. 1200/1440 = 83.333% and 32/1440 = 2.222%, so scaling both by the viewport reproduces
   145px exactly at 1440 and holds the same fraction at any width. Divided by --wide-zoom because vw
   resolves against the real window and the render is then multiplied by the zoom (a bare 10.556vw
   measured 12.39% instead of 10.56%).

   TARGETS THE CARRIERS, NOT THE SECTIONS. Sections are already full-bleed - computed max-width:none
   even where their inline style says 1200px - and every one paints a background, so re-capping them
   would shrink those backgrounds to the measure. :not(section) keeps them out. An earlier attempt
   uncapped the sections instead and never moved the content: headings stayed at 16.3% of the width
   while the hero alone moved to 10.56%.

   Scaling the measure rather than removing it leaves centring and nesting untouched - only the two
   numbers change. Both inline spellings are listed for the reason documented above section 6: the
   reveal system writes el.style.transition on [data-reveal] wrappers, and the browser re-serialises the
   whole style attribute, so `max-width:760px` becomes `max-width: 760px`.

   This replaces the hero-only version, which had left the hero at 211 and the sections at 325.
   >=1441px only, so mobile and laptop are untouched by construction.
   --------------------------------------------------------------------------- */
@media (min-width: 1441px){
  :not(section)[style*="max-width:1200px"],
  :not(section)[style*="max-width: 1200px"],
  /* ...and the five SECTIONS that carry the measure themselves rather than wrapping
     a carrier: About (What we do, Our services), Careers, Privacy, Contact. `:not(section)`
     above is right about bands — re-capping a section that paints a background would shrink
     the paint to the measure — but these five paint nothing and hold the 32px gutter, which
     is the carrier signature. Left out, they alone stayed at a literal 1200px while every
     other wrapper scaled, so "What we do" sat at 523 against the tabs at 270 on a 2560
     screen, and drifted further the wider the display. The background guard is what keeps
     the bands out; it is a property of the element, not a list of pages. */
  section[style*="32px"][style*="max-width:1200px"]:not([style*="background"]),
  section[style*="32px"][style*="max-width: 1200px"]:not([style*="background"]),
  [style*="32px"][style*="max-width:1100px"], [style*="32px"][style*="max-width: 1100px"],
  [style*="32px"][style*="max-width:1000px"], [style*="32px"][style*="max-width: 1000px"],
  [style*="32px"][style*="max-width:900px"], [style*="32px"][style*="max-width: 900px"],
  [style*="32px"][style*="max-width:820px"], [style*="32px"][style*="max-width: 820px"],
  [style*="32px"][style*="max-width:760px"], [style*="32px"][style*="max-width: 760px"],
  [style*="32px"][style*="max-width:700px"], [style*="32px"][style*="max-width: 700px"],
  section.fix-fullbleed > *{
    max-width: calc(83.333vw / var(--wide-zoom, 1)) !important;
    padding-left: calc(2.222vw / var(--wide-zoom, 1)) !important;
    padding-right: calc(2.222vw / var(--wide-zoom, 1)) !important;
  }
}

/* ---------------------------------------------------------------------------
   34. The stats column ends level with the buttons.

   MEASURED at 1999x1015, identically on dist/ and dist-demo/, so this is the shared hero and not a demo
   artefact: the stats column takes its own natural height - 4 blocks of 68 plus 3 gaps of 26 = 350
   layout px - and ends 45px ABOVE the buttons, because the grid is align-items:start so each column
   sizes to its own content.

   align-self:stretch makes the column take the ROW height, and the row height is the taller item: the
   text column, which ends at the buttons. space-between then distributes the four stats across exactly
   that height, so the last one lands on the buttons baseline with no height hard-coded anywhere.

   Desktop only. On mobile the stats are a two-by-two grid and stretching would pull its rows apart;
   the row gap there is deliberate.
   --------------------------------------------------------------------------- */
/* REWORKED: `align-self: stretch` aligns the column to the ROW, and the row is the taller column. With
   the authored 26px row-gap the stats' own natural height (4 blocks + 3 gaps + the 8px margin = 356 at
   1512x760) EXCEEDED the 350px text column, so the row was sized by the stats and the buttons ended 6px
   above the last one - more wherever a label wraps. Dropping the gap and letting space-between do the
   spacing puts the natural height back to the sum of the blocks alone (275px), so the row is always the
   text column's height and a stretched box's bottom edge IS the buttons' baseline.
   (An earlier attempt used height:0 + min-height:100%. A percentage min-height on a grid item resolves
   against the grid area, which is indefinite here, so it computed to 0 and the stats collapsed 75px
   above the buttons. Do not reach for that again.) */
@media (min-width: 781px){
  body[data-route="/"] #top [data-hero-grid] > div:last-child,
  body[data-route="/el/"] #top [data-hero-grid] > div:last-child{
    align-self: stretch !important;
    justify-content: space-between !important;
    row-gap: 0 !important;
  }
}
/* ---------------------------------------------------------------------------
   35. Nothing overflows to the right on a narrow phone.

   MEASURED at 281x561 on dist/ (so this is the shared hero, not a demo artefact):
     - the H1 reached x=321 against a 281px viewport, clipped mid-word. Its floor is 30px at ANY width
       (the override lives in `@media (min-height: 560px)`, which has no width term) and the design's
       site-header.js keeps `white-space: nowrap` on the "powered by Salesforce" span for as long as its
       scrollWidth fits the 36px-inset room - so the line could not break. The size now follows the
       viewport below ~349px and wrapping is always allowed. At 353+ the 30px floor still wins and the
       span still fits on one line, so mainstream phone widths are unchanged.
     - the two hero buttons squeezed to 106px and 97px with their labels spilling 33px and 23px past
       their boxes, because the row is nowrap and the anchors are shrinkable. The anchors now never
       shrink and keep their label on one line; the ROW wraps instead, stacking them at full width.
       One row still holds down to ~320px, so nothing changes on a normal phone.
   --------------------------------------------------------------------------- */
@media (max-width: 780px){
  body[data-route="/"] #top h1,
  body[data-route="/el/"] #top h1{
    font-size: max(19px, min(30px, 8.6vw)) !important;
    white-space: normal !important;
    overflow-wrap: break-word !important;
  }
  body[data-route="/"] #top h1 span,
  body[data-route="/el/"] #top h1 span{ white-space: normal !important; }

  /* Side by side and the same size, per the user — reversing the column this
     section used to impose, and reversing it safely.

     The column was the answer to a real fault: the row is `nowrap` and the
     anchors were shrinkable, so at 281px they squeezed to 106px and 97px with
     their labels spilling 33px and 23px past their own boxes. Stacking them
     avoided the squeeze by giving each its natural width.

     Equal halves solve the same fault without stacking. `flex: 1 1 0` sizes both
     boxes from the ROW rather than from the length of their words, so "Book a
     Meeting" and "Our Services" match at every width — which is the ask, and also
     why Chrome and Safari disagreed before: the pair fitted in one browser and
     wrapped in the other. `white-space: normal` is what makes it safe where the
     column was needed: below ~330px the longer label wraps to two lines inside
     its half instead of spilling out of it. */
  body[data-route="/"] #top [data-hero-grid] [data-hero-cta],
  body[data-route="/el/"] #top [data-hero-grid] [data-hero-cta]{
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: nowrap !important;
    align-items: stretch !important;
    gap: 12px !important;
  }
  body[data-route="/"] #top [data-hero-grid] [data-hero-cta] > a,
  body[data-route="/el/"] #top [data-hero-grid] [data-hero-cta] > a{
    flex: 1 1 0 !important;
    min-width: 0 !important;
    padding-left: 10px !important;
    padding-right: 10px !important;
    text-align: center !important;
    white-space: normal !important;
  }
}

/* ---------------------------------------------------------------------------
   36. Mobile header: inset pill at rest, full-bleed square on scroll - as on desktop.

   site-header.js's update() already writes the desktop behaviour inline (padding 0 / max-width 100% /
   border-radius 0 past 60px of scroll, and 16px 24px 0 / 1120px / 6px at rest), but its own injected
   sheet overrides it on mobile with `!important` - `[data-header]{padding:10px 14px 0}`,
   `body[data-scrolled] [data-header]{padding:8px 14px 0}` and `[data-header-inner]{border-radius:8px}`.
   An !important declaration beats a non-important inline one, so on a phone the bar was 14px-inset
   with 8px corners in BOTH states: the change never happened, and 14px of a 529px viewport reads as
   full width. These drive the two states from data-scrolled instead, and outrank those rules on
   specificity (three attributes plus body, against one).

   Only the side padding is set: padding-top stays 0 (section 19), so the bar is flush to the top in
   both states and nothing shifts vertically.

   The hamburger is position:fixed on <body> (site-header.js puts it outside React's subtree on
   purpose), so it does not travel with the header's padding - and at right:22px it poked 2px past the
   pill's edge. The offsets below are the bar's own content edge in each state: 24 + 14 = 38 at rest,
   0 + 14 = 14 scrolled, where 24/0 is the header's side padding and 14 the inner's. Vertically
   section 28's top:6px already centres it on the 56px bar, which is flush to the top either way.
   --------------------------------------------------------------------------- */
@media (max-width: 780px){
  /* The design reserves a 38px slot for the menu button (the badge carries margin-right:38px in a
     flex-end group), but the button is 44px - which is why it used to be parked outside the pill at
     right:22px. 52px = 44 + 8 of gap, so it fits on the content edge with room to spare. */
  /* The repeated attribute is load-bearing: site-header.js injects the SAME selector with the SAME
     !important at 38px, and its runtime <style> lands after this file, so an identical selector loses
     on order. Among !important declarations specificity decides regardless of order (see section 19). */
  /* 40, not 52: the ask is badge-to-hamburger == hamburger-to-edge. The icon ends 15px inside the bar
     (matching the logo) and is 24px wide, so it starts 39px inside; a matching 15px gap puts the badge's
     end at 54px inside, and the badge ends at (bar - 14 inner padding) - margin. The 44px tap box still
     clears the badge by 5px. */
  [data-header-inner] a[href*="appexchange"][href*="appexchange"]{ margin-right: 40px !important; }

  /* The header animates its padding (`transition: padding .28s` inline), so the logo and badge glide
     when the bar goes full-bleed - the fixed button used to jump. Same duration and easing; both
     offsets are linear in the header padding, so the two curves match. */
  /* Doubled attribute: buttonHover() injects `[data-btn]{transition: ... !important}` in a LATER sheet
     at the same specificity, which was dropping `right` from the list entirely. The button's own hover
     transitions are restated because `transition` is a single shorthand - winning it means owning it. */
  [data-mobile-toggle][data-mobile-toggle]{
    transition: right .28s ease,
                transform .22s cubic-bezier(.34, 1.56, .64, 1),
                background .18s, background-color .18s, color .18s, border-color .18s, box-shadow .18s
                !important;
  }

  /* at rest: the desktop resting geometry */
  body:not([data-scrolled]) [data-header][data-header]{
    /* section 19 pins this too, but at (0,2,0) it loses to site-header.js's
       `body[data-scrolled] [data-header]{padding:8px 14px 0 !important}` at (0,2,1) - which left the
       scrolled bar 8px off the top and its icons 8px below the fixed hamburger. */
    padding-top: 0 !important;
    padding-left: 24px !important;
    padding-right: 24px !important;
  }
  body:not([data-scrolled]) [data-header] [data-header-inner][data-header-inner]{
    border-radius: 6px !important;
  }
  /* The ICON is what has to line up with the logo, not the button box: the box is 44px (tap target)
     around a 24px icon, so the icon sits 10px further in than the box edge. The logo is 15px inside the
     bar, so the box goes 5px inside it - i.e. the header's own side padding plus 5. Measured at 466:
     icon 15px from the bar edge in both states, box fully inside the bar, 18px clear of the badge. */
  body:not([data-scrolled]) [data-mobile-toggle]{ right: 29px !important; }

  /* scrolled: full-bleed, square */
  body[data-scrolled] [data-header][data-header]{
    padding-top: 0 !important;
    padding-left: 0 !important;
    padding-right: 0 !important;
  }
  body[data-scrolled] [data-header] [data-header-inner][data-header-inner]{
    border-radius: 0 !important;
  }
  body[data-scrolled] [data-mobile-toggle]{ right: 5px !important; }   /* 0 + 5 - the same icon inset */
}

/* ---------------------------------------------------------------------------
   37. In-page anchors land below the fixed header.

   A fragment jump aligns the target's top edge with the viewport top, which is behind the fixed header -
   so the first 56px (mobile) / 70px (desktop) of a section was hidden under the bar. This is the
   fallback for the moment before site-fixes.js measures the real header height (the logo is a vw clamp,
   so the bar is taller on wide screens); the script then sets the exact value inline.
   --------------------------------------------------------------------------- */
html{ scroll-padding-top: 70px; }
@media (max-width: 780px){
  html{ scroll-padding-top: 56px; }
}

/* ---------------------------------------------------------------------------
   38. The booking calendar's loading veil.

   The iframe used to be fetched on page load, so the modal always had content ready. Now it loads on
   request (see deferBookingCalendar in convert.js), and a first open can show an empty frame for a
   moment - this is what makes that read as loading rather than broken. Nothing paints it after the
   frame's load event hides the veil.
   --------------------------------------------------------------------------- */
@keyframes appexBookingSpin{ to { transform: rotate(360deg); } }
[data-booking-spinner]{ animation: appexBookingSpin .9s linear infinite; }
@media (prefers-reduced-motion: reduce){
  [data-booking-spinner]{ animation: none; }
}

/* ---------------------------------------------------------------------------
   39. The skip link.

   There was none on any page, and the nav is six items plus two dropdowns - a keyboard user tabbed
   through all of it before reaching content, on every page (WCAG 2.4.1, Level A).

   Hidden by position, not by display:none or visibility:hidden - both of those remove an element from
   the tab order, which would defeat the point. It appears on focus, above the fixed header (z-index
   1000 there, so 1001 here).
   --------------------------------------------------------------------------- */
[data-skip-link]{
  position: fixed;
  top: 0; left: 0;
  transform: translateY(-120%);
  /* Off-screen is not out of sight when the thing below it has a backdrop filter.
     `translateY(-120%)` on a 44px link with an 8px margin leaves its bottom edge
     0.8px above the viewport - and the header's `backdrop-filter: blur(20px)`
     samples a band far wider than that, so it smeared this blue box across the
     top-left of the navigation on every page. It read as a pale rounded box
     behind the header with a ghost of the logo in it, which is exactly what it
     was. `opacity: 0` fixes it at the source: a fully transparent element paints
     nothing, so there is nothing for the filter to pick up - and unlike
     `visibility: hidden` or `display: none` it keeps the link in the tab order,
     which is the whole reason the hiding is done by position in the first place. */
  opacity: 0;
  z-index: 1001;
  margin: 8px;
  padding: 12px 18px;
  background: #0031BC;
  color: #fff;
  font-size: 15px;
  font-weight: 700;
  text-decoration: none;
  border-radius: 8px;
  box-shadow: 0 8px 24px -8px rgba(0,0,0,0.45);
  transition: transform .18s ease, opacity .18s ease;
}
[data-skip-link]:focus,
[data-skip-link]:focus-visible{
  transform: translateY(0);
  opacity: 1;
  outline: 3px solid #f5a623;
  outline-offset: 2px;
}
/* The skip target carries tabindex="-1" so it can actually RECEIVE focus - without it the browser moves
   the scroll but not the keyboard, and the next Tab goes straight back into the nav, which defeats the
   whole link. Its focus ring is suppressed because it is a programmatic target, not a control: a 1200px
   outline around the page reads as a rendering fault. */
main[id="main"]{ scroll-margin-top: 0; }
main[id="main"]:focus{ outline: none; }

/* ---------------------------------------------------------------------------
   40. The home hero cell field: gaps between cells, and icon weight.

   The cells sit on a 64.87-unit honeycomb lattice but the hex path is 81.4 wide flat-to-flat, so they
   OVERLAPPED. Scaling each cell to 0.76 about its own centre leaves a gap of exactly one border width
   (1.5 units), which is what was asked for.

   Done in CSS because the markup is not uniform - the field carries three copies of the cell set for the
   seamless drift, and the uses differ in shape (some carry a transform attribute, some are positioned by
   a parent group). A build-time regex over the markup reached 81 of 366 cells and left two cell sizes
   mixed on screen. A rule cannot miss.

   `scale` is the INDIVIDUAL transform property and composes with the `transform` attribute, so each cell
   keeps its own translate. Using `transform` would replace the attribute and destroy the positioning -
   in SVG2 they are the same property. `transform-box: fill-box` puts the origin at the cell's own centre.

   The stroke is held at its authored 1.5 by vector-effect on the #hex definition, so shrinking the cell
   does not thin its border - the gap and the border stay equal.
   --------------------------------------------------------------------------- */
/* The cell scale used to live here as `scale: 0.76` on the <use>. Removed: it computed correctly and
   was not rendered, leaving the cells overlapping in the user's browser. The geometry is baked into the
   #hexHero path in convert.js instead, where no renderer can decline it. */
/* 0.8 -> 0.96, the "20% less transparent" icons. On every icon, not the 84% a regex happened to reach. */
[data-hero-field] use[href^="#ic-"]{
  opacity: 0.96;
}

/* 41. No empty hexes in the hero field.

   The design's cell manager hoists a hex into the icon group whenever an icon sits on that slot, and
   leaves the rest as bare outlines running the plain keyframe. Two problems with that here: the bare cells
   are the "empty cells" being reported, and their keyframe is a second, unrelated rhythm next to the
   manager's eased fades - which is what makes the field look unsettled.

   Hiding an un-hoisted hex fixes both. A hosted hex sits at [data-hero-icon-layer] > g > g > use, so this
   selector cannot reach it. The [data-hero-tile] ancestor is deliberate: it exists only after the manager
   has tiled the field, so if the manager fails to run this rule matches nothing and the field keeps its
   plain keyframe appearance instead of vanishing. */
[data-hero-field] [data-hero-tile] > g > use[href^="#hex"] { opacity: 0; }

/* 42. The hero block sits centred between the header and the fold.

   #top is 100svh with align-items:center, so the block was centred on the WHOLE viewport while the fixed
   header covers its top 70px - measurably half a header height too high (139/173 at 1280x700, 208/242 at
   1990x1000, always leaning toward the header, and proportionally worse the shorter the window).

   Two corrections, both derived rather than tuned:
     +35px   half the header height, which is the whole error when the block is centred on the viewport;
     -asym/2 the grid's own padding asymmetry already pushes the CONTENT below its box centre, so half of
             that has to come back off. 15px at 1000 tall, 17.5px at 700, 23px at 1400.

   Applied as `top` on the already-relative grid, deliberately NOT as padding: redistributing the padding
   also centres it, but it moves the grid's internal edges, and with `minmax(0,1fr) auto` tracks the stats
   column resizes, the H1 column takes the difference and the headline re-wraps onto fewer lines - which
   reads as the type getting bigger. An offset changes position and nothing else.

   781px is the site's own desktop breakpoint (DESKTOP in hero-belt.js). Gating this at 901 left a 120px
   band where the sticky 100svh hero was live and the centring was not - exactly where browser zoom lands a
   wide window, which is how zoom appeared to "break" the position.

   The scrolled and short-screen cases are NOT here: they need the fitted scale, so they live in the hero
   controller (see centreHeroStates in convert.js). */
@media (min-width: 781px){
  body[data-route="/"] #top > [data-hero-grid],
  body[data-route="/el/"] #top > [data-hero-grid]{
    position: relative;
    top: calc(35px - (var(--hero-pad-top, 88px) - var(--hero-pad-bottom, 36px)) / 2);
  }
}

/* ---------------------------------------------------------------------------
   §46  Article typography, moved out of the markup

   The design styles an article entirely with inline styles — 35 identical <p>
   rules on one page, 9 identical <li>, 8 identical <a>. That was fine while the
   prose lived in the design and nobody edited it.

   It is not fine now the body is editable: Filament's rich editor strips inline
   styles on save, so the first time anyone fixed a typo the article lost its
   typography — 63 style attributes down to zero, paragraphs falling back to the
   browser default.

   The declarations are uniform per element, so they belong here. The stored body
   is clean semantic HTML that an editor cannot break, and the look is the
   design's. Values are copied verbatim from the built article.
   --------------------------------------------------------------------------- */
[data-article] p {
  font-size: 17px;
  line-height: 1.75;
  color: #3d4f61;
  margin: 0 0 20px;
}

[data-article] a {
  color: #2f6fd0;
  font-weight: 600;
  transition: color .18s ease;
}

[data-article] h2 {
  font-size: clamp(26px, 3.4vw, 34px);
  font-weight: 800;
  letter-spacing: -0.025em;
  line-height: 1.18;
  margin: 38px 0 16px;
  color: #152a45;
}

[data-article] h3 {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.015em;
  margin: 28px 0 12px;
  color: #0031BC;
}

[data-article] ul,
[data-article] ol {
  margin: 0 0 22px;
  padding-left: 22px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

[data-article] li {
  font-size: 17px;
  line-height: 1.7;
  color: #3d4f61;
}

/* The hero keeps its own inline styles — it is emitted by the build, not stored
   in the body, so nothing can strip them. This covers in-article images. */
[data-article] img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 10px;
  border: 1px solid #e4ebf3;
  margin: 8px 0 26px;
}

/* Filament's rich editor writes a colour as a CSS custom property and a class —
   `<span class="color" style="--color:#8a0808">` — and expects the panel's own
   stylesheet to turn that into a colour. The site does not ship Filament's CSS,
   so an editor picked a colour, saved, and the word stayed black. */
[data-article] .color,
[data-article] [data-color] {
  color: var(--color, inherit);
}

/* The same editor marks highlights this way. */
[data-article] mark {
  background: var(--color, #fff3bf);
  padding: 0 .15em;
  border-radius: 2px;
}

/* ---------------------------------------------------------------------------
   §47  The blog hub's category bands

   `[data-cat-bands]` lays the four categories side by side, and each band stacks
   its own posts one per row. With eleven posts spread unevenly — five in one
   category, one in another — the bands ended up wildly different heights, their
   coloured left-borders ran the full height of the tallest, and the page
   finished with a large empty area cut by four vertical lines.

   Each band takes the full width now and lays its posts out as a grid, so the
   cards fill the row instead of queueing down one side of it. Reading order,
   the band colours and the anchors are untouched.

   Targeted on `[data-cat-bands]` specifically: an earlier attempt at
   `main > div > div` was loose enough to catch the card wrapper as well and
   squeezed every card into a narrow column.

   The featured slider above this (CMS-SPEC §10) is still unbuilt.
   --------------------------------------------------------------------------- */
[data-cat-bands] {
  grid-template-columns: 1fr !important;
  row-gap: clamp(28px, 3.5vw, 44px);
}

[data-cat-bands] [data-reveal-group] {
  /* auto-fill, not auto-fit: a category holding one post keeps a normal-sized
     card rather than one stretched across the whole row. */
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)) !important;
  align-items: start;
}

@media (max-width: 600px) {
  [data-cat-bands] [data-reveal-group] {
    grid-template-columns: 1fr !important;
  }
}

/* ---------------------------------------------------------------------------
   §48  The article's "On this page"

   Styling lives here rather than inline on every entry: the list is generated,
   so an inline style would be repeated once per heading — sixteen times on the
   pricing article — and could not express :hover or the active state at all.

   The active entry is set by the scrollspy in the article template. It marks
   where the reader IS, which the old build could not do: it baked the state into
   the HTML, so the first entry was highlighted whatever you were reading.
   --------------------------------------------------------------------------- */
[data-toc] a {
  display: block;
  font-size: 13px;
  line-height: 1.5;
  color: #3c5470;
  padding: 7px 0 7px 12px;
  border-left: 2px solid transparent;
  transition: color .16s ease, border-color .16s ease;
}

[data-toc] a:hover {
  color: #0031BC;
}

[data-toc] a[aria-current="true"] {
  color: #0031BC;
  font-weight: 700;
  border-left-color: #0031BC;
}

/* An anchor jump must not land the heading under the sticky header. */
[data-article] :is(h2, h3)[id] {
  scroll-margin-top: 110px;
}

/* ---------------------------------------------------------------------------
   47. Heading sizes an editor put on a string (.ie-h1 … .ie-h4).

   The on-page editor cannot drop a real <h2> inside the designed markup — the
   parser re-parents block tags out of a <p> — so a heading applied to a string
   is a span carrying the design's own heading scale. Visitors see these, so
   they live here with the rest of the site's CSS, not in the editor.
   --------------------------------------------------------------------------- */
.ie-h1{display:block;font-size:clamp(32px,4.4vw,50px);line-height:1.1;font-weight:800;letter-spacing:-0.03em;}
.ie-h2{display:block;font-size:clamp(28px,3.8vw,38px);line-height:1.16;font-weight:800;letter-spacing:-0.022em;}
.ie-h3{display:block;font-size:clamp(22px,2.6vw,27px);line-height:1.2;font-weight:700;letter-spacing:-0.015em;}
.ie-h4{display:block;font-size:clamp(18px,2vw,21px);line-height:1.3;font-weight:700;}

/* ---------------------------------------------------------------------------
   50. The mobile hero: two buttons on one row, an even rhythm, and a fold that
   lands on them.

   Three things asked for on a phone, and all three are the same section.

   1. THE BUTTONS. The row is `flex-wrap:nowrap` inline, but each button carries
      `white-space:nowrap` and 26px of side padding, so at 390px the pair needs
      more than the column has and the browser overflows or drops the second one
      onto its own line — Chrome and Safari disagreed about which, which is why
      the two screenshots differ. `flex:1 1 0` makes them share the row in equal
      halves and stop depending on the length of their own words: "Book a Meeting"
      and "Our Services" get the same box on every device, in every browser.

   2. THE RHYTHM. The heading sat 26px above the first paragraph while the last
      paragraph sat 36px above the buttons, so the block read as lopsided. One
      value for all three gaps on a phone.

   3. THE FOLD. The stats belong below it: the first screen should end on the
      buttons and the rest should arrive with the scroll. The text column gets a
      minimum height of one screen less the grid's own top padding, so the KPI
      block — the grid's second item, and on a phone its second row — starts at
      the bottom edge of the viewport. `svh` and not `vh` deliberately: `vh` is
      the height with the browser's chrome hidden, so a `vh` measurement puts the
      buttons under Safari's address bar on first paint, which is exactly the view
      this is meant to fix.
   --------------------------------------------------------------------------- */
@media (max-width: 780px){
  /* The column is already `display:flex; flex-direction:column` on a phone. Give
     it the screen to fill and let flexbox do the spacing: `space-between` puts
     the heading at the top and the buttons at the bottom, and shares what is left
     equally between every pair — which is the ask, stated as a rule rather than
     as a number that would only be right on one device.

     `gap` is the floor, not the spacing: with `space-between` it is the minimum
     each pair gets, and any extra room is added on top of it equally. So a short
     screen keeps 26px and never less, and a tall one opens up without the block
     drifting apart at one seam only.

     The column has THREE children, not four: the two paragraphs are wrapped in
     `[data-hero-copy]` so they stay a block. Sharing the height between four
     would open a third gap between them and break the copy apart, which is not
     what "even" means here — the heading, the copy and the buttons are the three
     things, and the paragraphs keep the design's own spacing inside their group.

     The bottom margins that touch a seam have to go, or they would add to one
     gap only — which is the lopsidedness this replaces. The margin BETWEEN the
     two paragraphs is left exactly as the design set it. */
  body[data-route="/"] #top [data-hero-grid] > div:first-child > h1,
  body[data-route="/el/"] #top [data-hero-grid] > div:first-child > h1,
  body[data-route="/"] #top [data-hero-grid] [data-hero-copy] > p:last-child,
  body[data-route="/el/"] #top [data-hero-grid] [data-hero-copy] > p:last-child{
    margin-bottom: 0 !important;
  }

  body[data-route="/"] #top [data-hero-grid] > div:first-child,
  body[data-route="/el/"] #top [data-hero-grid] > div:first-child{
    /* 120px is the grid's own top padding, which is what sits between the top of
       the viewport and the top of this column. */
    min-height: calc(100svh - 120px);
    justify-content: space-between !important;
    gap: 26px !important;
  }

}

/* ---------------------------------------------------------------------------
   51. The hero's hexagon field stops growing past the design width.

   The field is sized in viewport units — `width: 220vw`, `height: max(167vw, 100%)`,
   `right: -58vw` — so every hexagon scales linearly with the window. That is right
   up to the width the design was drawn at and wrong past it: at 1440px the field
   renders 3168px wide, at 2545px it renders 7487px, and the same hexagon is twice
   the size. On a phone the proportion is exactly as intended, which is the tell —
   nothing is broken, the field simply has no ceiling.

   `min()`/`max()` give it one at the 1440px values, so nothing changes at or below
   the design width and the artwork holds its size above it. Capping `right` with
   the rest keeps the field anchored where it was: it is positioned from the right
   edge, and leaving the overhang uncapped while capping the width would drag the
   whole pattern off to one side.
   --------------------------------------------------------------------------- */
@media (min-width: 1441px){
  [data-hero-field]{
    width: min(220vw, 3168px) !important;
    height: max(min(167vw, 2405px), 100%) !important;
    right: max(-58vw, -835px) !important;
  }
}

/* ---------------------------------------------------------------------------
   52. The stats do not jump on a phone.

   `[data-kpis]` carries `data-reveal`, and the design's reveal moves an element
   `translateY(34px) -> none` over 0.7s as it enters — and, by its own comment,
   "re-animates both directions". On a desktop the stats are far below the fold
   and it plays once. On a phone they now sit exactly ON the fold, so every small
   scroll crossed the threshold and the block slid a few pixels, again and again.

   The transform is written inline by the script, so only `!important` can reach
   it. The reveal stays everywhere else — this is the one block whose position
   makes it misfire.
   --------------------------------------------------------------------------- */
@media (max-width: 780px){
  body[data-route="/"] #top [data-kpis],
  body[data-route="/el/"] #top [data-kpis]{
    transform: none !important;
    opacity: 1 !important;
  }
}

/* ---------------------------------------------------------------------------
   53. The testimonials are a carousel on a phone.

   Nine cards stacked one under another is nine screens of scrolling for a
   section most readers skim. One card at a time, snapped, with the strip moving
   on by itself and a swipe to take it back.

   Built here rather than left to `cardSwipe()` in the design's own script: that
   one bails on this container, because the mobile rule collapses the grid to a
   single track and it only takes over a grid with two or more. It also has no
   notion of advancing on its own, which is the half that was asked for.

   `data-cardswipe` goes on the strip from the script — not for the styling, but
   because `reflow()` skips anything carrying it. Without that it would find a
   nowrap flex row with several children and helpfully wrap it, which is exactly
   the carousel undone.
   --------------------------------------------------------------------------- */
@media (max-width: 780px){
  [data-testimonials]{
    display: flex !important;
    flex-wrap: nowrap !important;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    gap: 14px !important;
  }
  [data-testimonials]::-webkit-scrollbar{ display: none; }

  [data-testimonials] > div{
    /* The strip carries the page's side padding, so a card is the full width
       minus that — one card fills the screen and the next one is a swipe away
       rather than peeking, which is what makes the snap feel like a slide. */
    flex: 0 0 100% !important;
    /* `!important`, because the design's own `cardSwipe()` writes
       `scroll-snap-align: start` inline on cards it takes over, and `start`
       pins the card to the left edge of the scrollport: measured 0px of margin
       on the left and 36px on the right, which is what read as "not centred".
       `center` puts the same margin on both sides at every width. */
    scroll-snap-align: center !important;
    min-width: 0;
  }

  [data-testimonial-dots]{
    display: flex;
    justify-content: center;
    gap: 8px;
    margin: 16px 0 0;
  }
  [data-testimonial-dots] button{
    width: 7px; height: 7px; padding: 0;
    border: 0; border-radius: 50%;
    background: #b8cdf0;
    cursor: pointer;
    transition: background .2s ease, transform .2s ease;
  }
  [data-testimonial-dots] button[aria-current="true"]{
    background: #0031BC;
    transform: scale(1.35);
  }
}

/* Nothing of the above on a desktop, where the three-column grid stands. */
@media (min-width: 781px){
  [data-testimonial-dots]{ display: none; }
}
