/* =========================================================================
   template-parts/sections/section-hero.php
   Moved out of the partial's inline <style id="es-hero-css"> on the owner's
   instruction, 10 Sep 2026. en_print_css() (inc/css-loader.php) prints this
   file's <link> where the <style> used to be, once per page, so every page
   that renders this section shares this one file.
   ========================================================================= */

/* ── Hero shell ─────────────────────────────────────────── */
.es-hero {
  position: relative;
  padding: var(--hero-pad-top) 0 var(--section-pad);
  overflow: hidden;
}

.es-hero--bg-tint {
  background:
    radial-gradient(ellipse 65% 55% at 50% -12%, rgba(232, 93, 32, 0.12), transparent 70%),
    linear-gradient(180deg, var(--brand-50) 0%, var(--bg-surface) 80%);
}

.es-hero--bg-surface { background: var(--bg-surface); }

/* ── `ground: 'none'` ────────────────────────────────────
   The default. Nothing is drawn, so the hero shows its `bg` and nothing
   else. `$hero_has_ground` already stops the layer from being printed at
   all, so this rule is a belt-and-braces guard for a caller that stamps
   the class some other way; it costs one declaration and removes a class
   of silent regression. */
.es-hero__bg--none { background-image: none; }

/* ══ Canvas hero ═══════════════════════════════════════════
   The light counterpart to --bg-dark: a warm, ambient stage for a
   centered composition rather than a backdrop for artwork.

   Two differences from --bg-tint, both deliberate:

   1. It returns to --brand-50 at the foot instead of fading to white
      and staying there. The tint variant reaches pure white by 80%,
      which is fine when the next thing down is an ordinary section
      and wrong here: this page hangs a white raised card across the
      hero's bottom edge, and a white card on a white floor has only
      its hairline border left to read by. Coming back to the warm
      tint gives the card a ground to lift off, and it puts the
      brightest part of the surface in the middle where the copy is,
      which is where the eye should land anyway.

   2. Two off-centre brand glows instead of one overhead ellipse, so
      the light reads as coming from behind the composition rather
      than from the top of the screen.

   Everything resolves through tokens except the two glow alphas,
   which are the brand hue at low opacity and are the same values
   the tint variant already uses. */
.es-hero--bg-canvas {
  background:
    radial-gradient(ellipse 46% 40% at 18% 12%, rgba(232, 93, 32, 0.10), transparent 68%),
    radial-gradient(ellipse 50% 44% at 84% 26%, rgba(232, 93, 32, 0.09), transparent 70%),
    linear-gradient(180deg, var(--brand-50) 0%, var(--bg-surface) 52%, var(--brand-50) 100%);
}

/* ── Entrance on load ───────────────────────────────────────
   A staggered fade and rise that runs at first paint, with no JS and
   no observer. `.reveal-up` cannot do this job here: it holds the
   element at opacity 0 until a deferred script has parsed, run and
   had an IntersectionObserver callback fire, and the element it would
   be holding is the H1, which is this page's LCP element.

   A CSS animation costs nothing on that path. The keyframe starts at
   opacity 0 but the element is only fully transparent for the length
   of its own animation-delay, and the longest delay in the ladder is
   0.30s on the trust row, which is not an LCP candidate. The headline
   sits at 0.06s.

   animation-fill-mode: both is what makes the pre-delay state opacity
   0 and holds the final state after. If animations are unavailable the
   rule simply does not apply and every element renders normally, which
   is the reason this is safer than the observer path, not just faster. */
@keyframes esHeroIn {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

.es-hero--enter .es-hero__enter {
  animation: esHeroIn var(--duration-slow) var(--ease-out) both;
  animation-delay: var(--rv-delay, 0s);
}

/* The tiles drift in from a little further out and a little later, so
   they read as settling around the copy rather than arriving with it. */
@keyframes esHeroTileIn {
  from { opacity: 0; transform: translateY(26px) scale(0.94); }
  to   { opacity: 1; transform: none; }
}

.es-hero--enter .es-hero__tile {
  animation: esHeroTileIn 700ms var(--ease-spring) both;
  animation-delay: var(--rv-delay, 0s);
}

/* ══ Dark hero ═══════════════════════════════════════════
   Full-bleed dark surface carrying background artwork.

   Colours are pinned to the inverse end of the token scale
   rather than the semantic aliases: this section is dark in
   BOTH themes by design, so `--text-heading` (dark ink in
   light mode) would be unreadable here.

   Measured against the --gray-900 base (#161410):
     #FFFFFF heading  18.39:1   AAA
     #D5CFC5 body     11.88:1   AAA
     #FF9D6B eyebrow   9.01:1   AAA
     #F47340 accent    6.46:1   AAA
     #B0A898 label     7.80:1   AAA
   ══════════════════════════════════════════════════════ */
.es-hero--bg-dark {
  background-color: var(--gray-900);
  padding-bottom: var(--section-pad-lg);
  isolation: isolate;
}

/* ── The backdrop layer needs its parent to be a stacking context ──
   `.es-hero__bg` is `z-index: -1`. A negative-z child paints after the
   background of its stacking context ROOT, and `.es-hero` is
   `position: relative` with `z-index: auto`, which does NOT create one:
   `overflow: hidden` does not either. So without an explicit context the
   root is some ancestor, the hero's own background is painted as an
   ordinary descendant background AFTER the negative child, and the
   backdrop ends up behind it.

   `--bg-dark` above already sets `isolation: isolate` for exactly this
   reason, which is why every backdrop before now happened to work: the
   layer was dark-hero-only. `grid` is the first ground that also paints
   under a LIGHT hero, and a light hero's `--bg-canvas` / `--bg-tint`
   gradient is opaque, so without this the mesh renders in the DOM,
   computes correctly, and is invisible on the page.

   On a dark hero this is the same property at the same value it already
   had, so nothing there changes. */
.es-hero--ground-grid { isolation: isolate; }

/* Artwork layer. Sits behind content, never intercepts clicks. */
.es-hero__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  background-position: var(--es-hero-bg-pos, top center);
  background-repeat: no-repeat;
  background-size: cover;
  pointer-events: none;
}

/* ── `ground: 'grid'` ────────────────────────────────────
   A blueprint mesh instead of the dot grids and arcs `deco` draws: fine
   ruled lines running the full surface, masked so they fade out before
   they reach any edge, under a warm centre glow and a brighter pool
   behind the headline.

   Its own ground rather than a variant of `deco` because it has to be
   unmistakably NOT the other pages. Every other service hero on the site
   runs the same confetti scatter on the same warm canvas, and the owner
   asked for this one to be different. A ruled mesh also happens to be the
   right metaphor here: it reads as a technical surface, which is what an
   app-development page is selling.

   Five layers, all gradients, so the whole thing is a few hundred bytes
   and no HTTP request. The mask is what keeps it from reading as graph
   paper: at full strength edge to edge the lines fight the headline.

   ── Contrast under the headline ─────────────────────────
   The two glows LIGHTEN the ground, which is the direction that costs
   contrast against white copy, so the pool is the layer that has to be
   checked rather than the mesh. Composited at the centre of the pool, the
   --gray-900 base (22,20,16) resolves to about rgb(81,40,21):
     #FFFFFF heading  12.6:1  AAA
     #D5CFC5 lede      8.1:1  AAA
   The pool is deliberately weaker than `deco`'s 0.30 centre glow, so this
   ground is no darker a place to put copy than the one About already runs.

   On the LIGHT hero this page actually ships, the direction reverses: the
   layers darken a near-white ground, which moves contrast against the
   dark copy the right way. --text-heading on the composited ground stays
   above 15:1, so the light form needs no separate check. */
.es-hero__bg--grid {
  background-image:
    /* pool behind the headline */
    radial-gradient(ellipse 54% 42% at 50% 30%, rgba(232, 93, 32, 0.20), transparent 70%),
    /* wide centre glow */
    radial-gradient(ellipse 80% 70% at 50% 44%, rgba(232, 93, 32, 0.10), transparent 72%),
    /* vertical rules */
    linear-gradient(90deg, rgba(232, 93, 32, 0.16) 1px, transparent 1px),
    /* horizontal rules */
    linear-gradient(180deg, rgba(232, 93, 32, 0.16) 1px, transparent 1px),
    /* horizon */
    linear-gradient(90deg, transparent, rgba(232, 93, 32, 0.30) 46%, transparent 78%);
  background-position: center, center, center, center, bottom;
  background-size: cover, cover, 64px 64px, 64px 64px, 100% 1px;
  background-repeat: no-repeat, no-repeat, repeat, repeat, no-repeat;

  /* ── The fade is on the WHOLE layer ────────────────────
     This used to mask a second `::after` mesh only, which meant the
     brand rules in `background-image` above ran edge to edge at full
     strength in both themes while the comment claimed they faded. On a
     light ground that is the difference between a texture and graph
     paper drawn across the headline.

     Masking the glows along with the mesh costs nothing, which is why
     the two no longer need separate layers: both glows are radial and
     have already fallen to transparent well inside where this mask
     starts to bite, so it never puts an edge on them. The horizon line
     is the one layer the mask does shorten, and a horizon that stops
     before the corners is the better drawing anyway. */
  -webkit-mask-image: radial-gradient(ellipse 82% 78% at 50% 40%, #000 34%, transparent 86%);
  mask-image: radial-gradient(ellipse 82% 78% at 50% 40%, #000 34%, transparent 86%);
}

/* On a light ground the same alphas read completely differently: 0.16 is
   a hairline on near-black and a drawn rule on near-white, so the mesh
   comes down. It does NOT come down as far as it first did, though. At
   0.07 the line differed from the ground by 2, 10 and 14 per channel,
   which is technically present and not a background element anybody
   would see. 0.11 is a soft warm rule that reads without competing with
   the copy over it. */
.es-hero:not(.es-hero--bg-dark) .es-hero__bg--grid {
  background-image:
    radial-gradient(ellipse 54% 42% at 50% 30%, rgba(232, 93, 32, 0.13), transparent 70%),
    radial-gradient(ellipse 80% 70% at 50% 44%, rgba(232, 93, 32, 0.07), transparent 72%),
    linear-gradient(90deg, rgba(232, 93, 32, 0.11) 1px, transparent 1px),
    linear-gradient(180deg, rgba(232, 93, 32, 0.11) 1px, transparent 1px),
    linear-gradient(90deg, transparent, rgba(232, 93, 32, 0.20) 46%, transparent 78%);
}

/* The mesh is the one background layer here that is a repeating pattern at
   a fixed pitch, and a 64px pitch on a 375px screen is six columns of
   lines behind a headline. Opened up so it stays a texture. */
@media (max-width: 767px) {
  .es-hero__bg--grid { background-size: cover, cover, 96px 96px, 96px 96px, 100% 1px; }
}

/* ── `ground: 'flow'` ────────────────────────
   A layered warm aurora for the Careers hero: soft brand pools out in the
   side margins, three sweeping arcs low in the frame, a clearing that keeps
   the centre column near-surface, and a fine grain pass over the lot.

   ── Why this is CSS and not the supplied picture ────
   It is the same artwork, at a few hundred bytes and no HTTP request,
   on a hero whose LCP element is the headline. The section this replaced
   shipped 868 KB of decorative SVG above the fold to say the same thing.
   A raster also cannot follow the dark-mode token flip, and it cannot
   scale from a 320px phone to a 1920px desktop without either tiling or
   cropping the curves off. If the owner does upload the file, the
   `background` argument takes over and this layer is never printed:
   see the `$hero_has_bgimg` branch in the markup.

   ── What this replaced, and why ────────────────
   The first version drew each curve as a band a few TENTHS of a percent
   wide. Against `background-size: cover` on an 850px box that is a stop
   under one device pixel, so the arcs antialiased away to nothing on most
   displays, and its strongest layer was white at 0.85 sitting on the
   #FFF3EE to #FFF wash: white on near-white. The composition was sound; it
   was drawn below the threshold at which anything renders. Bands here are
   ~0.9-1.1% and carry brand rather than white, which is what makes them
   appear at all.

   ── Contrast, measured rather than asserted ───────
   The old note claimed the mask confined every layer to the lower half
   and that the copy sat in the upper half, so nothing composited under
   text. That was not true of the rendered page. Measured against the hero
   box, the mask ran fully opaque from 62% to 92% while the CTA sat at
   55-62%, the feature pills at 66-71% and the trust lockup at 76-80%:
   three of the five copy elements were inside the drawing's strongest
   band, not clear of it.

   So the interest moves to where the copy is NOT. The hero is a centred
   column, so the side margins are free at every height, and the 41-55%
   gap between the headline and the CTA is free across the full width.
   The pools live out there, and the `clearing` layer below is listed
   FIRST, which puts it on top of everything and holds the centre column
   at close to page surface. That is what lets the pools be strong enough
   to see while the copy keeps its contrast: the drawing is brightest
   exactly where there is nothing to read. */
.es-hero__bg--flow {
  background-image:
    /* Clearing. Topmost, so it sits over the pools and the arcs and keeps
       the centre column near --bg-surface for the copy that runs down it.

       Sized from the measured INK, not from the copy elements' boxes. The
       headline, the feature row and the trust row are full-width flex
       containers with centred content, so their boxes run 19-81% while
       their glyphs run 31-69%, 36-66% and 59-71%. Sized to the boxes this
       would be far too wide and would flatten the pools; sized to the ink
       it holds every glyph at 0.55 clearing or better while both side
       pools, centred at 6% and 96%, sit past its 74% stop and keep their
       full strength. */
    radial-gradient(ellipse 56% 92% at 50% 52%, rgba(255, 255, 255, 0.94) 0%, rgba(255, 255, 255, 0.62) 42%, transparent 74%),

    /* The three sweeps. Centres well below the section, so only the top of
       each ellipse is in frame and the slice reads as a shallow curve. */
    radial-gradient(ellipse 148% 90% at 84% 126%, transparent 58.4%, rgba(232, 93, 32, 0.22) 59.3%, transparent 60.2%),
    radial-gradient(ellipse 166% 102% at 71% 138%, transparent 55.2%, rgba(232, 93, 32, 0.16) 56.2%, transparent 57.2%),
    radial-gradient(ellipse 188% 116% at 58% 152%, transparent 51.6%, rgba(232, 93, 32, 0.11) 52.7%, transparent 53.8%),

    /* Aurora pools, out in the margins the centred column never reaches.
       Two hues rather than one: brand-300 against brand-400 is what gives
       the wash depth instead of a single flat orange. */
    radial-gradient(ellipse 34% 50% at 6% 30%, rgba(255, 157, 107, 0.38), transparent 70%),
    radial-gradient(ellipse 36% 54% at 96% 22%, rgba(244, 115, 64, 0.30), transparent 72%),
    radial-gradient(ellipse 58% 42% at 60% 102%, rgba(232, 93, 32, 0.24), transparent 72%);
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;

  /* Edge fade only. The old mask started biting at 26%, which is why the
     pools could not reach up the sides; copy protection is the clearing
     layer's job now, not the mask's. */
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 12%, #000 86%, transparent 100%);
  mask-image: linear-gradient(180deg, transparent 0%, #000 12%, #000 86%, transparent 100%);
}

/* Grain. Large, soft, low-alpha gradients band into visible steps on an
   8-bit display; a little fractal noise over the top dithers the steps
   away. It is the difference between a gradient that looks printed and one
   that looks like a screenshot of a gradient. Inline SVG, so it is bytes in
   this file rather than a request, and it multiplies so it only ever
   darkens the wash a touch and never lifts it toward the copy. */
.es-hero__bg--flow::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
  background-repeat: repeat;
  opacity: 0.035;
  mix-blend-mode: multiply;
  pointer-events: none;
}

@media (prefers-reduced-transparency: reduce) {
  .es-hero__bg--flow::after { display: none; }
}

/* A phone is roughly half as wide as it is tall, so an ellipse sized as a
   percentage of the box comes out far steeper there and the sweeps turn
   into near-vertical hooks. Widened and dropped further below the section
   so the visible slice stays shallow.

   The side pools also come off here, and the whole composition quietens.
   On a phone the copy does not sit in a centred column with margins beside
   it, it fills the hero: measured at 375px the ink runs from 23% to 87% of
   the section, so there is no region left that is both large and empty.
   Pools kept out at the sides would sit under the headline rather than
   beside it.

   So the clearing goes nearly full height and the colour moves to the two
   bands the copy does not reach, above 23% and below 87%, with the pool
   centres pushed outside the box so only their falloff is in frame. The
   sweeps stay: they live low, where the mask is already fading. A phone
   hero carrying less background than the desktop one is the right answer
   here, not a compromise.

   1199.98px, not 767px. What decides which composition works is not the
   device, it is how wide the COPY runs relative to the section, because a
   clearing sized in percentages has to cover it. Measured, the headline's
   ink is:

       992px  9-91%      1200px  28-72%
      1100px  9-91%      1280px  29-71%
                         1440px  31-69%
                         1920px  36-64%

   The step between 1100 and 1200 is the headline re-wrapping once its
   reading cap engages inside the container. Below it the copy is
   effectively full width and there are no side margins to put pools in;
   above it there are. Sized for the wide form, the desktop clearing left
   the headline on bare pool at 1024 (0 clearing) and the trust row at
   0.33, which is why this bound is 1200 and not the 992 it first was.

   The .98 is the convention noted further down: a bare 1199 beside a
   container that widens at min-width 1200 leaves a hole fractional zoom
   lands in. */
@media (max-width: 1199.98px) {
  .es-hero__bg--flow {
    background-image:
      radial-gradient(ellipse 130% 86% at 50% 50%, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.58) 46%, transparent 78%),
      radial-gradient(ellipse 260% 70% at 84% 134%, transparent 58.4%, rgba(232, 93, 32, 0.22) 59.4%, transparent 60.4%),
      radial-gradient(ellipse 300% 78% at 71% 146%, transparent 55.2%, rgba(232, 93, 32, 0.16) 56.3%, transparent 57.4%),
      radial-gradient(ellipse 340% 88% at 58% 158%, transparent 51.6%, rgba(232, 93, 32, 0.11) 52.8%, transparent 54%),
      radial-gradient(ellipse 110% 30% at 50% -6%, rgba(255, 157, 107, 0.36), transparent 72%),
      radial-gradient(ellipse 140% 34% at 60% 108%, rgba(232, 93, 32, 0.26), transparent 74%);
  }
}

/* Token-built stand-in used until the real artwork is uploaded:
   centre brand glow, two dot grids, and concentric arcs. Same
   visual family, a few hundred bytes, no HTTP request. */
.es-hero__bg--deco {
  background-image:
    /* centre glow */
    radial-gradient(ellipse 42% 46% at 50% 52%, rgba(232, 93, 32, 0.30), transparent 68%),
    /* dot grid, right */
    radial-gradient(circle at center, var(--brand-500) 1.1px, transparent 1.2px),
    /* dot grid, left */
    radial-gradient(circle at center, var(--brand-500) 1.1px, transparent 1.2px),
    /* concentric arcs, bottom right */
    repeating-radial-gradient(circle at 78% 104%, transparent 0 30px, rgba(232, 93, 32, 0.20) 30px 31px),
    /* horizon line */
    linear-gradient(90deg, transparent, rgba(232, 93, 32, 0.35) 42%, transparent 72%);
  background-position:
    center,
    88% 34%,
    6% 74%,
    center,
    bottom;
  background-size:
    cover,
    22px 22px,
    22px 22px,
    cover,
    100% 1px;
  background-repeat:
    no-repeat,
    repeat,
    repeat,
    no-repeat,
    no-repeat;
  /* Clip each dot grid to a small block instead of tiling the section. */
  -webkit-mask-image:
    linear-gradient(#000, #000),
    linear-gradient(#000, #000),
    linear-gradient(#000, #000),
    linear-gradient(#000, #000),
    linear-gradient(#000, #000);
  mask-image:
    linear-gradient(#000, #000),
    linear-gradient(#000, #000),
    linear-gradient(#000, #000),
    linear-gradient(#000, #000),
    linear-gradient(#000, #000);
  -webkit-mask-size: 100% 100%, 150px 130px, 150px 130px, 100% 100%, 100% 100%;
  mask-size: 100% 100%, 150px 130px, 150px 130px, 100% 100%, 100% 100%;
  -webkit-mask-position: center, 88% 34%, 6% 74%, center, bottom;
  mask-position: center, 88% 34%, 6% 74%, center, bottom;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  opacity: 0.9;
}

/* ══ Plain warm ground, opt-in via `ground: plain` ═════════
   The dark hero's other backdrop. --deco above draws a centre glow, two dot
   grids and a set of concentric arcs; this draws the light and nothing
   else, for a composition where the surface is meant to be colour rather
   than a scene with things on it.

   Three glows on the same brand hue: a broad one centred behind the
   headline, a tighter hotter core inside it, and a wash rising from the
   foot so the surface does not go flat black where the slab meets the page.
   The ground colour underneath is --gray-900, set by .es-hero--bg-dark.

   The centre is at 32% of the section's height, not at its top edge. An
   earlier pass put the main glow at 2% and the foot wash at 106%, which
   centred both of them outside the section: the top glow sat behind the
   fixed header and the bottom one below the slab, so the middle third,
   which is exactly where the headline and the button sit, rendered as flat
   black. Measured on the CRO hero, the ground behind the H1 moved from
   rgb(24,21,17) to rgb(60,32,21) once the centre came down.

   No mask layer, unlike --deco, because there is nothing to clip.

   Alphas are held down deliberately. At 0.26 on the tight core the surface
   peaked at rgb(100,47,21) directly behind the eyebrow, which read as a
   spotlight rather than as a wash and put the brand-400 accent at 3.90:1
   there. Softened, the peak stays an obvious warm glow while the accent
   measures comfortably clear across the whole headline band. Measured on
   the rendered page, not estimated. */
.es-hero__bg--plain {
  background-image:
    radial-gradient(ellipse 42% 34% at 50% 34%, rgba(232, 93, 32, 0.17), transparent 66%),
    radial-gradient(ellipse 78% 62% at 50% 34%, rgba(232, 93, 32, 0.17), transparent 72%),
    radial-gradient(ellipse 95% 45% at 50% 99%, rgba(232, 93, 32, 0.13), transparent 76%);
}

/* The dark hero lays a legibility scrim over its backdrop, because artwork
   is unpredictable and white text has to clear AA over whatever was
   uploaded. This backdrop is two gradients written in this file, so there
   is nothing unpredictable to defend against, and the scrim's job here
   would only be to mute the warmth it was drawn for. Removed rather than
   softened.

   BOTH classes on the same element, exactly as the video scrim rule further
   down does it. `.es-hero--bg-dark .es-hero__bg--plain::after` looks more
   specific but is not: a pseudo-element counts as an element, not a class,
   so it ties the scrim rule at (0,2,0) and then loses on source order,
   because the scrim is declared later in this sheet. Measured with the tie
   in place, the scrim's 0.72 black was still painting over the glow and the
   ground behind the headline read rgb(34,23,16) against the base
   rgb(22,20,16): a 12-point lift where the design calls for a visible warm
   wash. Doubling the class up wins it outright. */
.es-hero--bg-dark .es-hero__bg.es-hero__bg--plain::after { content: none; }

/* ══ Slab, opt-in via `shape: slab` ════════════════════════
   Curves the section's two bottom corners so the surface reads as a slab
   laid on the page rather than as a full-bleed band running through it.
   What shows in the cut is the page ground behind the section, so this is
   only worth setting on a `bg` that differs from it.

   The section already carries `overflow: hidden` for its backdrop layer,
   which is what makes the radius actually clip the ground and the artwork
   rather than just rounding an invisible box.

   Sized off --radius-2xl, the largest radius in the token file, rather than
   a fresh number: one step of it at the floor, two at the ceiling, fluid
   between. A slab corner has to read at the scale of the section, not at
   the scale of a card, and 32px on a 700px-tall surface reads as a softened
   edge rather than as a curve.

   Anything the hero renders BELOW this section is unaffected: the seam
   proof card is its own section further down the document, so it lifts over
   the curve rather than being clipped by it. That is the arrangement the
   reference draws, where the product shot crosses the slab's bottom edge
   while the corners stay visible either side of it. */
.es-hero--slab {
  --es-slab-radius: clamp(var(--radius-2xl), 5vw, calc(var(--radius-2xl) * 2));
  border-bottom-left-radius: var(--es-slab-radius);
  border-bottom-right-radius: var(--es-slab-radius);
}

/* ── Background video ───────────────────────────────────────
   Fills the same layer a background still would, so the scrim,
   the stacking and the dark-surface text colours all carry over
   unchanged. */
.es-hero__bg-media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ── Right-aligned clip ─────────────────────────────────────
   The clip occupies the right of the section and the copy sits
   on the gradient to its left, but the two must read as one
   surface rather than two panels with a join down the middle.

   A hard-edged panel cannot do that. At the seam you would have
   dark base on one side and gradient-over-bright-footage on the
   other, and even under a 0.72 overlay that steps from about 12
   to about 66 in luminance, which is a visible line.

   So the clip's own left edge is masked away instead. It ramps
   from nothing to solid across the first 40% of the panel while
   the gradient above it ramps down over the same stretch, so the
   footage emerges out of the dark with no boundary anywhere. */
@media (min-width: 992px) {
  .es-hero__bg--right .es-hero__bg-media {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 58%;
    -webkit-mask-image: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.55) 18%, #000 40%);
    mask-image: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.55) 18%, #000 40%);
  }
}

/* Heavier, left-weighted scrim for footage. The section's own
   artwork is art-directed and predictable; a clip is not, and
   this one is a bright cream studio shot, so the generic scrim
   below is not enough to hold white text at AA.

   Left-weighted because the copy is left-aligned: the footage
   stays visible on the right where nothing sits on top of it.

   Stop positions and the 8,8,8 base are the owner's spec, used verbatim.
   Note this is the one colour in the section that is not a token: the dark
   surface token is --gray-900 (#161410), this is #080808.

   The radial sits above the linear, as specified, lifting the left edge
   just enough that the panel does not read as a flat black rectangle. */
/* Both classes on the same element: the generic scrim rule below carries
   the same specificity and sits later in this sheet, so it would otherwise
   win and drop the copy back to the weaker gradient. */
.es-hero--bg-dark .es-hero__bg.es-hero__bg--video::after {
  background:
    radial-gradient(
      circle at left center,
      rgba(255, 255, 255, 0.03),
      transparent 55%
    ),
    linear-gradient(
      90deg,
      rgba(8, 8, 8, 0.92) 0%,
      rgba(8, 8, 8, 0.88) 22%,
      rgba(8, 8, 8, 0.72) 42%,
      rgba(8, 8, 8, 0.42) 62%,
      rgba(8, 8, 8, 0) 100%
    );
}

/* The 42/58 split is a desktop idea. Below it the copy spans the full
   width, so a gradient that has faded to 0.42 by 62% leaves the end of
   every line sitting on bare footage: measured at 390px that put the
   subheadline at 3.88:1, a real AA failure. Held near the spec's opening
   value all the way across instead, so the clip stays visible but evenly
   dimmed and the text keeps its contrast. */
@media (max-width: 991px) {
  .es-hero--bg-dark .es-hero__bg.es-hero__bg--video::after {
    background:
      radial-gradient(
        circle at left center,
        rgba(255, 255, 255, 0.03),
        transparent 55%
      ),
      linear-gradient(
        90deg,
        rgba(8, 8, 8, 0.92) 0%,
        rgba(8, 8, 8, 0.86) 60%,
        rgba(8, 8, 8, 0.82) 100%
      );
  }
}

/* Legibility scrim: keeps AA contrast even if the uploaded
   artwork is brighter behind the headline than expected. */
.es-hero--bg-dark .es-hero__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 70% 60% at 50% 45%, rgba(22, 20, 16, 0.55), transparent 75%),
    linear-gradient(180deg, rgba(22, 20, 16, 0.72) 0%, rgba(22, 20, 16, 0.28) 38%, rgba(22, 20, 16, 0.85) 100%);
}

.es-hero--bg-dark .es-hero__eyebrow {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(255, 255, 255, 0.14);
  color: var(--brand-300);
  box-shadow: none;
  backdrop-filter: blur(6px);
}

.es-hero--bg-dark .es-hero__title { color: var(--text-inverse); }
.es-hero--bg-dark .es-hero__title .es-hl { color: var(--brand-400); }
.es-hero--bg-dark .es-hero__sub { color: var(--gray-300); }

/* 17 Sep 2026: the dark-hero glass secondary is gone; buttons keep the same
   solid fill on every ground (button colour scheme, utilities.css). */

/* ── Trust card on the dark surface ─────────────────────────
   The bare trust row cannot go on a dark ground. It is built from supplied
   brand assets, and both of the ones the service pages carry are drawn for
   light backgrounds: the Shopify Partners lockup is black wordmark on
   transparent and the Clutch mark is dark teal. On --gray-900 they render
   as near-invisible shapes, and there is no CSS fix that keeps them
   correct: a filter that lifts them to white destroys Shopify's green and
   Clutch's red dot, which is a brand asset being altered rather than
   restyled.

   So the row keeps its own light surface instead, via `trust_style: card`,
   and the pill is repainted here. Same problem and same answer as
   .es-hero__proof--card: the dark block above repaints
   every text element for white-on-dark, and inside a light pill each of
   those has to be put back or the score is white ink on a white ground.

   Three classes deep so these win on specificity, not source order. */
.es-hero--bg-dark .es-hero__trust--card {
  border-color: var(--border-subtle);
  background: var(--bg-surface);
}

.es-hero--bg-dark .es-hero__trust--card .es-hero__rating-score { color: var(--text-heading); }
.es-hero--bg-dark .es-hero__trust--card .es-hero__rating-source { color: var(--gray-600); }
.es-hero--bg-dark .es-hero__trust--card .es-hero__rating { border-left-color: var(--border-subtle); }
.es-hero--bg-dark .es-hero__trust--card .es-hero__trust-rule { background: var(--border-subtle); }

/* And the mark goes back to being itself. The rule further down inverts the
   Shopify Partners lockup to white so it survives the dark ground; inside
   the pill the ground is white, so the same invert would hide it just as
   thoroughly the other way. Three classes deep to beat that rule on
   specificity rather than on source order.

   This is the half that was missing. The block above was written for a
   dark hero carrying a card and no page combined the two, so the clash
   between it and the invert had never been drawn. */
.es-hero--bg-dark .es-hero__trust--card .es-hero__trust-logo { filter: none; }

.es-hero--bg-dark .es-hero__proof { border-top-color: rgba(255, 255, 255, 0.12); }
.es-hero--bg-dark .es-hero__stat-value { color: var(--text-inverse); }
.es-hero--bg-dark .es-hero__stat-value b { color: var(--brand-400); }
.es-hero--bg-dark .es-hero__stat-label { color: var(--gray-400); }

.es-hero--bg-dark .es-hero__media {
  border-color: rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
}

.es-hero--bg-dark .es-hero__caption {
  background: rgba(255, 255, 255, 0.04);
  border-top-color: rgba(255, 255, 255, 0.12);
  color: var(--gray-400);
}

/* Focus rings need to be visible against the dark surface. */
.es-hero--bg-dark a:focus-visible {
  outline: 2px solid var(--brand-300);
  outline-offset: 3px;
}

/* ── Copy block ─────────────────────────────────────────── */
.es-hero__copy { max-width: 900px; }

.es-hero--centered .es-hero__copy {
  margin-inline: auto;
  text-align: center;
}

/* Copy sitting on top of a full-section background, left aligned. Full
   width until the split breakpoint; the 42/58 text-to-video ratio is a
   desktop rule and is applied below. */
.es-hero--overlay .es-hero__copy { max-width: 100%; }

/* ── Compact stack ──────────────────────────────────────────
   This block shares the section with a moving image, so it has
   to read as one dense unit rather than a column of separately
   floating parts. Everything below tightens the rhythm without
   touching the type sizes, the colours or the component
   structure, and it is scoped to this layout so the centered
   hero on other templates keeps its original spacing.

   Every value is still on the token scale, one step down from
   the default in each case. */

/* 1.06 rather than the shared 1.1.
   Chosen off measured ink, not em-box metrics. At 64px the deepest real
   descender here is the "y" of Shopify at 14px and the tallest cap ascent
   is 49px, so 1.06 leaves 4.8px of clearance to the line below. 1.04 was
   tried first: it clears too (3.6px) and saves another 5px of page height,
   which is not worth a third less descender air on a headline this large.
   Held to display sizes; at the 38px mobile end of the clamp the shared
   1.1 is already tight enough. */
@media (min-width: 768px) {
  .es-hero--overlay .es-hero__title { line-height: 1.06; }
}

/* ── Two-line headline ──────────────────────────────────────
   The headline is the one element allowed out of the 42% column.

   "Shopify Development Services" sets 940px wide at the shared 64px, so
   inside a 469-554px column it can only ever break to three lines. Forcing
   it to fit there would mean dropping to about 35px, which is the mobile
   end of the hero clamp shown on a desktop, and would leave the headline
   less than twice the size of the body copy.

   Instead the headline alone runs wider than the column while the
   subheadline, features and buttons stay at 42%, and the size steps down
   one notch. The percentages are of the copy box, which is itself 42% of
   the container, so 140% lands at about 59% of the container.

   width: max-content is what actually lets it out. max-width alone only
   caps a block, it never expands one, so the headline just stayed at the
   column width and kept breaking to three lines.

   Sized off the container ladder rather than vw. A vw-based clamp breaks
   at 1399px, where the viewport is nearly 1400 but .es-container is still
   on its 1140 step, so the type grows while the box it has to fit in does
   not.

   Three lines: "CRO Enabled" on its own from the <br>, then the phrase
   breaking after "Development". The controlling measurement is not the
   whole phrase but "Shopify Development", which has to fit one line while
   the full phrase does not. Each step is sized against that, with margin:
     container  960 -> box 475, "Shopify Development" 437 at 40px
     container 1140 -> box 565, "Shopify Development" 525 at 48px
     container 1320 -> box 654, "Shopify Development" 612 at 56px

   The box is 118% of the copy column, so the headline runs a little past
   the body text but stays well inside the dark side of the gradient. */
@media (min-width: 992px) {
  .es-hero--overlay .es-hero__title {
    /* max-content is what lets the box reach 118%; max-width on its own
       only caps a block and would leave it at the column width. The cap
       then forces the wrap, so the used width is exactly 118%. */
    width: max-content;
    max-width: 118%;
    /* One fluid step replacing three hard breakpoints that set 40px at
       992, 48px at 1200 and 56px at 1400. This passes through all three
       of those exactly and interpolates between them, so a 1100px window
       no longer sits on the 992px size until it crosses 1200. Per the
       design system: no per-breakpoint font-size where a clamp will do. */
    font-size: clamp(40px, 3.92vw + 1.1px, 56px);
  }
}

/* ── Proof strip ────────────────────────────────────────────
   Held to the copy column and set two up rather than four across. Four in
   a row only fits by spanning the container, which would carry the last
   two numbers out over the bright side of the clip where white text has
   nothing to sit on. Two columns of ~272px also lands within 10% of the
   About hero's 298px stat cell, so the numbers keep the same proportions
   they have there. */
@media (min-width: 992px) {
  .es-hero--overlay .es-hero__proof {
    max-width: 42%;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--space-6);
  }

  /* Left, not centred: this hero's copy is left aligned, so centred
     numbers would float away from the column they belong to. */
  .es-hero--overlay .es-hero__stat { text-align: left; }
}

/* ── Standing height ────────────────────────────────────────
   The section holds a viewport-scale stage rather than collapsing onto its
   copy. Compacting the headline to two lines took roughly 250px out of the
   content, and without this the whole composition shrank with it and the
   clip lost its presence.

   Full screen. The site header is position: fixed and floats over this
   section, which starts at y=0, so the hero owns the whole viewport and
   there is no header height to subtract. The section's top padding is
   already larger than the header, so nothing runs underneath it.

   min-height, not height: content is never clipped, it just cannot make the
   section shorter than the viewport. No upper cap, because a cap is exactly
   what stops it being full screen on a tall monitor.

   The container takes the section's full height and centres its children as
   one block, so the copy and the proof strip below it read as a single unit
   sitting in the middle of the stage rather than the copy floating while the
   numbers are pinned to the floor. */
@media (min-width: 992px) {
  .es-hero--overlay {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }

  /* Column, not row: children stay block-level so their max-widths still
     resolve against the container the way they do elsewhere. */
  .es-hero--overlay > .es-container {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
}

.es-hero--overlay .es-hero__eyebrow { margin-bottom: var(--space-4); }
.es-hero--overlay .es-hero__title   { margin-bottom: var(--space-4); }
.es-hero--overlay .es-hero__features { margin-top: var(--gap-fluid-sm); }
.es-hero--overlay .es-hero__ctas     { margin-top: var(--space-6); }
/* margin-bottom resets the UA default on the <ul>. It is the last item in
   the centred block, so 16px of user-agent margin below it pushes the whole
   composition 16px above true centre. Scoped, so the About hero keeps the
   spacing it has today. */
.es-hero--overlay .es-hero__proof {
  margin-top: var(--gap-fluid-lg);
  margin-bottom: 0;
  padding-top: var(--gap-fluid-md);
}

@media (min-width: 992px) {
  /* 42% of the container, not of the viewport.
     Measuring the 42% line from the viewport edge looks right on paper but
     inverts on wide screens: .es-container caps at 1320 and centres, so its
     left edge moves right as the screen grows, and the room left inside the
     42% line shrinks. Measured, that gave a 533px column at 1435 but only
     494px at 1920, and the feature row wrapped at the wider size.

     The 42/58 read does not depend on this number anyway. It comes from the
     gradient, which is section-wide and therefore genuinely viewport
     relative. This keeps the copy a stable measure that stays aligned with
     the container like every other block on the page, and lands within
     about 2% of the target ratio across 1440 to 1920. */
  .es-hero--overlay .es-hero__copy {
    max-width: 42%;
  }
}

/* ── The chip ───────────────────────────────────────────────
   This used to be a RAISED WHITE capsule: --bg-surface fill, a brand-200
   hairline and a --shadow-sm lift, with brand-600 ink at 4.67:1. That was
   the right drawing while every hero had a warm canvas or a drawn backdrop
   under it, because the pill's job was to lift off artwork.

   The heroes are a plain white ground now, and a white pill on a white
   ground is a border and a shadow pretending to be a chip. So it takes the
   treatment the site's one chip already uses: `.es-eyebrow` in
   utilities.css, brand-50 ground and brand-700 ink at 6.09:1. That is the
   uniformity contract's rule ("never define a second chip"), which the
   raised variant was the last exception to, and it raises the ink's
   contrast rather than lowering it. */
.es-hero__eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-5);
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--brand-100);
  border-radius: var(--radius-full);
  background: var(--accent-subtle);
  box-shadow: none;
  color: var(--brand-700);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-bold);
  letter-spacing: 1.8px;
  text-transform: uppercase;
}

/* ── The separator between the two eyebrow labels ───────────
   A hairline rule, and deliberately not a dot.

   It used to be an 8px brand-500 circle with a box-shadow ripple
   running on a 2s infinite loop. Removed on the owner's instruction:
   a pulsing dot beside a label is the house style of every AI-built
   landing page of the last two years, and it was on eight pages of
   this site. It also drew the eye to punctuation, which is the one
   thing in the capsule with nothing to say.

   A rule separates the two labels without competing with them, and
   it cannot animate, so there is no reduced-motion case to carry
   either. Sized off the type rather than fixed: 1em tall against the
   eyebrow's 12px, so it tracks the label if the size ever changes.

   The class name is unchanged. It is wrong now, but it is referenced
   by the mobile split rule below and by the confetti and dark-hero
   variants further down, and renaming it across all of them buys
   nothing a comment does not. */
.es-hero__dot {
  width: 1px;
  height: 1em;
  flex-shrink: 0;
  border-radius: 0;
  /* brand-300 rather than brand-200: at 1px on the white capsule the
     lighter stop all but disappears, and a separator nobody can see is
     the run-on phrase this element exists to prevent. Still well below
     the label's brand-600, so it reads as punctuation, not as a third
     thing to look at. It also holds up on the dark and confetti hero
     variants, which repaint the capsule but not this. */
  background: var(--brand-300);
}

/* Brand mark inside the pill. Fixed box with explicit dimensions on the
   element too, so the capsule cannot resize as the file decodes. */
.es-hero__eyebrow-mark {
  display: block;
  width: auto;
  height: 18px;
  flex-shrink: 0;
  margin-right: var(--space-1);
}

/* The capsule carries two labels on desktop. Below the split it drops to
   the first one only: at 375px the pair sets to three lines and the pill
   stops reading as a badge. The second label is not information the page
   needs twice, so it is removed rather than wrapped. */
@media (max-width: 575px) {
  .es-hero__eyebrow-part + .es-hero__dot,
  .es-hero__eyebrow-part + .es-hero__dot + .es-hero__eyebrow-part { display: none; }
}

/* ── Eyebrow inside a confetti hero ─────────────────────────
   Kept, but no longer a difference. This variant asked for a tinted fill
   and no shadow while the shared eyebrow was a raised white capsule; the
   shared one now carries exactly these values, so this rule restates the
   base rather than overriding it. Left in place because it also drops the
   border to brand-100 explicitly and costs nothing, and removing it would
   make the confetti heroes depend on the base never changing back. */
.es-hero--confetti .es-hero__eyebrow {
  border-color: var(--brand-100);
  background: var(--accent-subtle);
  box-shadow: none;
}

/* ── Tagline, opt-in via `tagline` ──────────────────────────
   One bold line closing the copy block. Heading ink and weight against the
   subtitle's body ink, so it reads as the claim the paragraph above it was
   building to rather than as a third paragraph. Its own measure, wider than
   the subtitle's, because it is written to sit on one line. */
.es-hero__tagline {
  max-width: 46ch;
  margin: var(--space-5) 0 0;
  color: var(--text-heading);
  font-size: var(--fs-lede-lg);
  font-weight: var(--font-bold);
  line-height: var(--lh-subhead);
  letter-spacing: var(--tracking-normal);
}

.es-hero--centered .es-hero__tagline { margin-inline: auto; }

/* ── Trust card, opt-in via `trust_style` ───────────────────
   The same content as the row below, seated in a raised pill. A hero with
   no buttons ends on this, and a bare row of a logo and a number reads as
   two loose fragments at the foot of the composition; a card makes it one
   object and gives the eye somewhere to stop. */
/* Doubled class so this beats the base .es-hero__trust block, which is
   further down this stylesheet and would otherwise win on order alone and
   stretch the card to the full copy width.

   `width: fit-content` rather than `display: inline-flex` for the shrink:
   inline-flex would shrink it, but an inline box cannot then be centred
   with `margin-inline: auto`, and the copy column is left-aligned text on
   the split layout. fit-content keeps it a block that both hugs its
   content and centres. */
.es-hero__trust.es-hero__trust--card {
  display: flex;
  width: fit-content;
  max-width: 100%;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--gap-fluid-sm);
  margin-top: var(--gap-fluid-lg);
  /* Tight vertical padding: the pill is a lockup of two marks, and the
     height it wants is the height of those marks plus a little air. The
     --space-4 it started at made it read as a panel. */
  padding: var(--space-3) var(--space-6);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  box-shadow: var(--shadow-md);
}

/* Hairline between the partner lockup and the score, drawn as a real
   element rather than a border so it can be dropped on a phone where the
   card wraps to two rows and a vertical rule would point at nothing. */
.es-hero__trust-rule {
  width: 1px;
  align-self: stretch;
  min-height: 28px;
  flex-shrink: 0;
  background: var(--border-subtle);
}

/* The card hugs its content, so it needs its own centring rather than
   inheriting the row's `justify-content`, which only distributes children
   inside a full-width box. */
.es-hero--centered .es-hero__trust.es-hero__trust--card { margin-inline: auto; }

.es-hero__rating-logo {
  display: block;
  width: auto;
  height: 26px;
  flex-shrink: 0;
  object-fit: contain;
}

/* ── Rating inside the card ─────────────────────────────────
   The bare row stacks its rating in a column, which is right when it is
   the only thing there. In the card it has a brand mark in front of it and
   that column turned the pill into a 123px-tall box with the logo, the
   score and the stars on three separate lines.

   Laid out as a grid instead, so the mark sits to the left spanning both
   rows and the figure sits over its stars, which is the lockup the design
   draws. Doubled class to beat the base `.es-hero--centered .es-hero__rating`
   rule further down this stylesheet. */
.es-hero__trust--card .es-hero__rating.es-hero__rating {
  display: grid;
  grid-template-columns: auto auto;
  grid-template-areas:
    "logo score"
    "logo stars";
  align-items: center;
  justify-items: start;
  column-gap: var(--space-4);
  row-gap: 2px;
  /* The card draws its own divider as a real element, so the rating must
     drop the border it carries in the bare row or the pill shows two. */
  padding-left: 0;
  border-left: 0;
}

.es-hero__trust--card .es-hero__rating-logo { grid-area: logo; }
.es-hero__trust--card .es-hero__rating-score { grid-area: score; }
.es-hero__trust--card .es-hero__stars { grid-area: stars; }

/* The score is a figure beside a mark here, not a headline: at the shared
   --fs-rating (30px) it stood taller than the Clutch lockup next to it and
   set the height of the whole pill. Sized so the score-over-stars pair
   finishes level with the two lockups instead of overrunning them, which is
   what keeps the pill a single tight row. */
.es-hero__trust--card .es-hero__rating-score {
  font-size: var(--fs-body);
  line-height: 1;
}

.es-hero__trust--card .es-hero__stars svg { width: 11px; height: 11px; }

/* Both lockups to one height, so the pill reads as two marks of equal
   standing rather than a large one and a small one. */
.es-hero__trust--card .es-hero__trust-logo { height: 28px; }

/* On a phone the card would otherwise run past the viewport: two lockups,
   a rule and a score do not fit 375px on one line. It wraps to a centred
   stack and the rule goes, since a vertical hairline between stacked rows
   separates nothing. */
@media (max-width: 575px) {
  .es-hero__trust.es-hero__trust--card {
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-3) var(--space-4);
    padding: var(--space-4) var(--space-5);
    border-radius: var(--radius-lg);
  }

  .es-hero__trust.es-hero__trust--card .es-hero__trust-rule { display: none; }
}

/* ── Action row, opt-in via `trust_style: inline` ───────────
   One line carrying the primary control and the proof beside it, split by
   a hairline. The two blocks keep their own markup and their own classes,
   so everything below is layout only: nothing about how a button or a
   lockup is drawn changes when a hero opts into this.

   The hairline is a real element, the same `.es-hero__trust-rule` the card
   already draws between its two lockups, rather than a border on the proof
   block. The proof block here is the raised pill, which carries a border of
   its own on all four sides: a divider added to its left edge would land on
   top of that border and read as a thickened edge, not as a rule between
   two things. A separate element sits in the gap instead, and can be
   dropped on its own once the row wraps. */
.es-hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-5) var(--gap-fluid-md);
  margin-top: var(--gap-fluid-lg);
}

/* Both children own a top margin for the stacked arrangement. Inside the
   row the row itself sets that space, so theirs is cleared or the two
   blocks sit on different baselines. */
.es-hero__actions > .es-hero__ctas,
.es-hero__actions > .es-hero__trust {
  margin-top: 0;
}

/* min-width: 0 lets the pill shrink and wrap its own lockups before the
   flex line breaks, rather than forcing the break at the first width where
   the button and the pill no longer fit side by side. */
.es-hero__actions > .es-hero__trust { min-width: 0; }

/* ── Centred hero: centre the row too ──────────────────────
   The full-width action row was written for the SPLIT layout, where it
   runs edge to edge under two columns and starting at the container's
   left edge is correct. On a CENTRED hero everything above it (eyebrow,
   headline, capability row) is centred on the container, and the row
   inherited `justify-content: normal`, so the buttons sat hard left with
   the proof pill floating somewhere past the middle and a quarter of the
   row empty on the right. It read as a layout that had come apart.

   The small-screen block far below already centres this row; it was only
   ever unset on desktop, which is where it is most visible. */
.es-hero--centered.es-hero--actions-row .es-hero__actions { justify-content: center; }

/* ── and cancel the auto margin that was defeating it ──────
   `.es-hero--centered .es-hero__trust--card` carries `margin-inline: auto`
   so the pill centres itself when it owns a row. Inside the action row it
   is a flex item, and an auto margin on a flex item eats ALL the free
   space on that axis before `justify-content` gets to distribute any. The
   row was therefore still laying out as buttons-hard-left, pill-hard-right
   with the centring rule above computing correctly and doing nothing.

   Four classes deep so it beats the three-class rule it is undoing,
   regardless of which lands first in the sheet. */
.es-hero--centered .es-hero__actions > .es-hero__trust.es-hero__trust--card { margin-inline: 0; }

/* Held off the full row height. `align-self: stretch`, which is what the
   rule carries for the card, draws it the full height of the tallest thing
   on the row: measured, a 64px rule against a 55px pill and a 54px button,
   so it overshot both and read as a column edge rather than as a separator
   between two objects. Sized to sit inside them instead. */
.es-hero__actions > .es-hero__trust-rule {
  align-self: center;
  height: var(--space-7);
  min-height: 0;
}

/* ── Trust row ──────────────────────────────────────────────
   Partner lockup and review score, split by a hairline. Sits under the
   buttons as the last thing in the hero, so the composition finishes on
   proof rather than on a control. */
.es-hero__trust {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-5) var(--gap-fluid-md);

  /* --gap-fluid-md, not --lg. The row was 89px tall when this gap was set,
     and 48px of air over an 89px block is proportionate. Now the rating is
     a single line the row is 40px, where 48px above it reads as a hole.
     Every pixel here is also spent twice: it pushes the hero down, and the
     seam counter card under it along with the hero. See the fold note on
     the Shopify Development hero. */
  margin-top: var(--gap-fluid-md);
}

.es-hero--centered .es-hero__trust { justify-content: center; }

.es-hero__trust-logo {
  display: block;
  width: auto;
  height: clamp(30px, 3.4vw, 40px);
}

/* Divider between the two halves. A pseudo-element on the score block
   rather than a border on the row, so it disappears on its own when the
   row wraps to two lines and the rule would be pointing sideways at
   nothing. */
/* ── One line: mark, score, stars ───────────────────────────
   This stacked its three parts in a column, which put the Clutch mark, the
   figure and the stars on three separate rows and made the lockup 89px
   tall next to a 40px partner mark. Owner's instruction is that the rating
   reads as one line, so the row runs horizontally and the parts are
   centred against each other.

   It also pays for itself in layout: the trust row drops from 89px to the
   height of the tallest mark, which is the height the Shopify Development
   hero could not afford when its row was removed for pushing the seam
   counter card below the fold.

   `flex-wrap: wrap` so a narrow column drops the stars to their own line
   rather than overflowing; the phone rule below keeps them centred there. */
.es-hero__rating {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding-left: var(--gap-fluid-md);
  border-left: 1px solid var(--border-subtle);
}

/* The source line is the one part that must not sit on that row: it is a
   sentence, not a mark, so it takes the full width and drops underneath. */
.es-hero__rating .es-hero__rating-source { flex-basis: 100%; }

.es-hero--centered .es-hero__rating { justify-content: center; }

.es-hero__rating:first-child {
  padding-left: 0;
  border-left: 0;
}

.es-hero__rating-score {
  color: var(--text-heading);
  font-size: var(--fs-rating);
  font-weight: var(--font-extrabold);
  line-height: 1;
  letter-spacing: var(--tracking-normal);
  font-variant-numeric: tabular-nums;
}

.es-hero__rating-out { color: var(--text-muted); font-weight: var(--font-semibold); }

.es-hero__stars {
  display: flex;
  gap: 2px;
  color: var(--star-color);
}

/* --gray-600, not --text-muted. This is 13px body text and AA wants 4.5:1
   for it; --text-muted (--gray-500) measures 3.83:1 on --bg-surface, which
   is a fail. --gray-600 measures 6.51:1 and still reads as the quiet line
   under the score.

   Note for the wider migration: --text-muted at --fs-caption is used on
   light surfaces in several existing components and fails there for the
   same reason. Fixing that is a token-level decision, so it is flagged
   rather than changed from inside this partial. */
.es-hero__rating-source {
  display: block;
  color: var(--gray-600);
  font-size: var(--fs-caption);
  line-height: 1.35;
}

/* Whole score block is one link when a profile URL is set. Underline is
   kept off the number and put on the source line, which is the part that
   reads as the destination. */
a.es-hero__rating { text-decoration: none; }
a.es-hero__rating:hover .es-hero__rating-source,
a.es-hero__rating:focus-visible .es-hero__rating-source { color: var(--brand-600); text-decoration: underline; }

.es-hero--bg-dark .es-hero__rating { border-left-color: rgba(255, 255, 255, 0.14); }
.es-hero--bg-dark .es-hero__rating-score { color: var(--text-inverse); }
.es-hero--bg-dark .es-hero__rating-source { color: var(--gray-400); }

/* ── The partner mark on a dark ground ──────────────────────
   assets/images/ShopifyPlus.png is a solid-black monochrome lockup, drawn
   for the light `canvas` heroes the five sibling service pages run. On the
   dark ground it was black ink on a near-black surface, which is to say
   invisible: the row read as a floating "5.0" and a rule pointing at
   nothing.

   Flipped to white rather than swapped for a second asset. The artwork is
   pure black with no colour in it, so brightness(0) invert(1) gives a clean
   white lockup and there is only ever one partner file to keep current.
   This is the same treatment section-cta.php already applies to a
   solid-black tile mark on its dark panel, so the site has one answer to
   this problem rather than two.

   Scoped to the dark ground only. About is the site's one other dark hero
   and it passes no `trust` at all, so no existing page is touched by this.

   The Clutch mark beside it is deliberately NOT inverted: it is a
   multi-colour logo that already reads on dark, and inverting it would
   wreck its brand colour. */
.es-hero--bg-dark .es-hero__trust-logo {
  filter: brightness(0) invert(1);
}

/* The separator drawn between the buttons and the trust row when `trust`
   rides inline in the actions row. Its base colour is --border-subtle, a
   LIGHT token: on the dark ground that is not invisible, it is the reverse
   problem, a hairline brighter than the copy it divides.

   Set to the same rgba(255,255,255,0.14) the rating's left border and the
   capability row's dividers already take on this ground, so every hairline
   in a dark hero is one value.

   Deliberately lower specificity than the `--card` rule above, which keeps
   its own --border-subtle: inside the card the hairline sits on a light
   surface, not on the dark ground, so that one is already correct. */
.es-hero--bg-dark .es-hero__trust-rule { background: rgba(255, 255, 255, 0.14); }

@media (max-width: 575px) {
  .es-hero__rating {
    padding-left: 0;
    border-left: 0;
  }
}

/* ── Confetti, opt-in via `decor_style: confetti` ───────────
   A scatter of small outlined shapes across the whole section rather than
   two icon tiles beside the headline. Chosen for a hero whose headline is
   the only thing competing for attention: tiles carrying marks read as a
   second set of icons, where these read as paper texture.

   Every shape is drawn from borders and gradients, so the layer costs no
   request and has nothing to lay out. It is aria-hidden and inert.

   Positioned against the section, not the headline box, so the shapes fill
   the margins the centred column leaves and none of them can land on a
   glyph however the headline rewraps. */
.es-hero__confetti {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.es-hero--confetti > .es-container { position: relative; z-index: 1; }

.es-hero__cf {
  position: absolute;
  top: var(--cf-y);
  left: var(--cf-x);
  width: var(--cf-size, 14px);
  height: var(--cf-size, 14px);
  opacity: var(--cf-op, 0.55);
  transform: rotate(var(--cf-rot, 0deg));
}

/* Ring. */
.es-hero__cf--circle {
  border: 1.5px solid var(--brand-300);
  border-radius: 50%;
}

/* Filled dot, drawn at a third of the box so a dot and a ring set to the
   same --cf-size still read as the same family. */
.es-hero__cf--dot {
  background: var(--brand-400);
  border-radius: 50%;
  transform: scale(0.34) rotate(var(--cf-rot, 0deg));
}

/* Outlined triangle, built from a clip-path on a bordered box so it keeps
   the same stroke language as the rings rather than being a solid wedge. */
.es-hero__cf--tri {
  background: var(--brand-300);
  clip-path: polygon(50% 0, 100% 100%, 0 100%, 0 100%, 50% 18%, 88% 92%, 12% 92%, 0 100%);
}

/* Plus, drawn as two bars through a single box. */
.es-hero__cf--plus::before,
.es-hero__cf--plus::after {
  content: "";
  position: absolute;
  background: var(--brand-400);
  border-radius: 1px;
}

.es-hero__cf--plus::before {
  top: 50%;
  left: 0;
  width: 100%;
  height: 2px;
  margin-top: -1px;
}

.es-hero__cf--plus::after {
  left: 50%;
  top: 0;
  height: 100%;
  width: 2px;
  margin-left: -1px;
}

/* Squiggle: one period of a wave, drawn from two stacked radial arcs. */
.es-hero__cf--wave {
  height: calc(var(--cf-size, 14px) * 0.42);
  border: 2px solid var(--brand-300);
  border-color: var(--brand-300) var(--brand-300) transparent transparent;
  border-radius: 50% 50% 0 0 / 100% 100% 0 0;
}

/* ── The halo, removed ──────────────────────────────────────
   A ring of concentric brand circles used to be drawn here, centred behind
   the headline. It came out on the owner's ruling that heroes ship a plain
   white ground: it was a repeating brand gradient painting the surface the
   H1 sits on, which is exactly what that ruling covers.

   The scattered `.es-hero__cf` shapes above are deliberately KEPT. Those
   are discrete decorative vectors placed in the margins a centred column
   leaves, not a treatment of the ground, and the uniformity contract
   protects the site's icons and vectors. Three heroes carry them:
   Shopify Migration, Shopify Plus and UI/UX Design. */

/* Phones get the halo only. The scattered shapes are placed against the
   wide margins a desktop centred column leaves; at 375px there are no such
   margins, so every one of them would land on the copy. */
@media (max-width: 767px) {
  .es-hero__cf { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .es-hero__confetti { transition: none; }
}

/* ── Floating tiles ─────────────────────────────────────────
   Soft squircles carrying section marks, set in the margins either side
   of the copy. Decoration only: aria-hidden, no pointer events, and
   drawn from tokens so there is no request and nothing to lay out.

   Wide screens only. Below 1200px the headline sets to fewer, shorter
   lines and a tile lands on top of the words rather than beside them. */
.es-hero__tiles { display: none; }

@media (min-width: 1200px) {
  /* Anchor for the layer: the tiles are placed against the headline box. */
  .es-hero__stage { position: relative; }

  /* The layer paints above the headline, unlike the drawn marks, which sit
     under it. That is a hit-testing decision, not a visual one. A block
     element takes pointer events across its whole box whether or not it has
     a background, and the headline's box spans the full column, so a tile
     underneath it would only be hoverable along the sliver that sticks out
     past the column edge. The hover state is the point of these tiles, so
     they have to be the thing the pointer reaches.

     The safety that ordering gives up is bought back by placement instead:
     both tiles sit in the whitespace a centred, ragged headline leaves at
     its corners, clear of every glyph. If the headline is rewritten long
     enough to reach them, a tile will cover a letter, and the coordinates
     in the geometry block need revisiting.

     `isolation` makes this a stacking context so each tile's glow (z-index
     -1) stays below its own tile rather than escaping to the stage and
     ending up under the headline. */
  .es-hero__tiles {
    display: block;
    position: absolute;
    inset: 0;
    isolation: isolate;
    /* The layer is inert; the tiles inside it opt back in, so one can be
       hovered while the space around it stays transparent to the pointer
       and the headline underneath stays selectable. */
    pointer-events: none;
  }

  .es-hero__tile {
    position: absolute;
    top: var(--es-tile-y);
    left: var(--es-tile-x);
    width: var(--es-tile-size, 96px);
    height: var(--es-tile-size, 96px);
    pointer-events: auto;
  }

  /* Warm bloom under each tile, so they sit in the same light the
     background gradient casts rather than looking pasted on. */
  .es-hero__tile::before {
    content: "";
    position: absolute;
    inset: -30%;
    z-index: -1;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(232, 93, 32, 0.18), transparent 66%);
  }

  /* The visible squircle. Split from the positioned box above because the
     two carry transforms that must not overwrite each other: the outer box
     runs the entrance animation, this one holds the resting tilt and
     animates to the hover pose. One element cannot do both, because a
     running animation wins over the transform property outright. */
  .es-hero__tile-inner {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-xl);
    background: var(--bg-surface);
    box-shadow: var(--shadow-lg);
    color: var(--brand-500);
    transform: rotate(var(--es-tile-rot, 0deg));
    transition:
      transform var(--duration-slow) var(--ease-spring),
      box-shadow var(--duration-slow) var(--ease-out);
  }

  /* Hover: the tile straightens, lifts and grows a little, as though picked
     up off the page. The rotation is a fraction of the resting tilt rather
     than a fixed angle, so a tile leaning left settles back to the left and
     one leaning right settles back to the right, instead of both snapping
     through vertical in the same direction. */
  .es-hero__tile:hover .es-hero__tile-inner {
    transform:
      rotate(calc(var(--es-tile-rot, 0deg) * 0.3))
      translateY(-9px)
      scale(1.07);
  }

  /* Tinted variant. One tile in a pair carrying a wash of the accent stops
     the two reading as a matched set of identical white boxes, without
     going as dark as a solid brand fill would.

     A low-alpha rgba pair rather than the solid brand-400/600 gradient the
     primary button uses: at full strength the tile reads as a button, which
     competes with the actual CTAs below it for the eye. Tokens carry no
     alpha channel of their own, so the two stops are written out at 14% and
     9%, which is the brand hue at roughly the same weight the tint hero
     background already uses for its ambient glows. Icon and border stay on
     the solid brand scale so the mark itself keeps full contrast against
     the wash. */
  .es-hero__tile--brand .es-hero__tile-inner {
    border-color: var(--brand-200);
    background: linear-gradient(155deg, rgba(232, 93, 32, 0.14), rgba(232, 93, 32, 0.05));
    box-shadow: var(--shadow-lg);
    color: var(--brand-500);
  }

  /* Height is computed from the tile, not left to `auto`.

     `auto` looks like it should defer to the width/height attributes the
     render loop writes, but those are presentational hints and any author
     rule outranks them. So `height: auto` did not mean "use 30px", it meant
     "use the file's intrinsic height", and the bundled Shopify mark is a
     89x100 SVG: it rendered at full size and burst out of a 78px tile.

     The attributes stay on the element to reserve the right box before the
     file decodes; this rule is what actually sizes it. 38% of the tile,
     matching the ratio the render loop uses for the attributes, so the two
     agree and there is no reflow when the SVG lands. */
  .es-hero__tile-img {
    display: block;
    width: auto;
    height: calc(var(--es-tile-size, 96px) * 0.38);
    max-width: 100%;
  }

  /* Same treatment for the drawn marks, for the same reason. These are
     currently sized correctly by their own attributes because nothing
     overrides them, but that is luck rather than design: one global
     `svg { width: … }` anywhere in the cascade would break them exactly
     the way the image above broke. Sized here so both kinds of mark are
     held by the same rule. */
  .es-hero__tile-mark {
    width: calc(var(--es-tile-size, 96px) * 0.32);
    height: calc(var(--es-tile-size, 96px) * 0.32);
  }

  .es-hero--bg-dark .es-hero__tile-inner {
    border-color: rgba(255, 255, 255, 0.12);
    background: rgba(255, 255, 255, 0.06);
    color: var(--brand-400);
    backdrop-filter: blur(6px);
  }

  /* Dark hero keeps a stronger wash: against --gray-900 the light-mode 14%
     alpha all but disappears, so the two stops step up to 22% and 10%. The
     border and icon move to the lighter end of the brand scale for the
     same reason every other dark-hero accent does. */
  .es-hero--bg-dark .es-hero__tile--brand .es-hero__tile-inner {
    border-color: rgba(232, 93, 32, 0.32);
    background: linear-gradient(155deg, rgba(232, 93, 32, 0.22), rgba(232, 93, 32, 0.10));
    backdrop-filter: blur(6px);
    color: var(--brand-400);
  }
}

.es-hero__title {
  margin: 0 0 var(--space-5);
  color: var(--text-heading);
  font-size: var(--fs-hero);
  font-weight: var(--font-extrabold);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-normal);
  text-wrap: balance;
}

/* Extra breathing room for a headline carrying a drawn mark (applied by PHP
   via $hero_title_marked, above). The shared 1.1 display line-height is
   tuned for plain text and leaves almost no gap between lines; a mark's
   overhang (the ring reaches -0.14em above its own line, the underline sits
   0.08em below it) needs more than that gap has to spare. Below 992px, where
   the headline commonly wraps to put two marked phrases on adjacent lines,
   that shortfall reads as the ring cutting into the line above it.

   Scoped to the modifier class rather than raised for every headline this
   partial renders, so a plain title (the About page hero, for one) keeps
   its original, tighter rhythm. */
.es-hero__title--marked { line-height: 1.42; }

.es-hero__title .es-hl {
  color: var(--brand-500);
  font-style: italic;
}

/* Upright accent. The shared .es-hl is italic, which is right when the
   accent is a short phrase and the only emphasis in the line. Scoped to the
   hero headline, so .es-hl keeps its italic everywhere else.

   This originally existed to sit under a drawn underline, which the slant
   fought. Those marks are withdrawn (see the note below), but two pages
   still use this modifier on its own merits and both are deliberate:
   Shopify CRO Packages, where the accent runs three quarters of the
   headline and italic at that length reads as a mistake, and Shopify
   Development Packages, where the accent shares the line with a second
   tone in .es-quiet. Everything else is plain italic .es-hl. */
.es-hero__title .es-hl--straight { font-style: normal; }

/* ── Drawn annotations ──────────────────────────────────────
   WITHDRAWN 5 SEP 2026, ON THE OWNER'S INSTRUCTION. NO PAGE USES EITHER
   MARK ANY MORE.

   Every headline that carried `es-mark--underline` (Shopify Plus, Shopify
   Migration, Shopify App Development, Shopify SEO and Industries) is back
   on plain `.es-hl`, which is the coloured italic every other heading on
   the site uses. `es-mark--circle` was never used on a page at all. The
   SCF instructions on the Shopify Development and Shopify Plus hero fields
   were updated at the same time so no editor is told to add one back.

   What went wrong: the sweep is absolutely positioned off the phrase it
   wraps, so it takes no part in layout and nothing can move out of its way.
   On the Shopify SEO hero the second line ran long enough to reach the
   floating Shopify tile in the right margin, and the stroke carried on
   underneath it. That is not tunable per page, because the collision
   depends on the headline's wrap point, which changes with the viewport.

   The CSS is kept rather than deleted: it is a capability of a shared
   partial, deleting it is an architecture call, and it costs nothing while
   no markup carries the class. DO NOT reintroduce either class without
   asking the owner first.

   ── How it worked, for whoever revisits this ──
   The sweep under a phrase and the ring around one, both drawn as if by
   hand rather than set as a border or a box.

   Technique: the shape is an SVG in a mask, and the colour is an ordinary
   background-color underneath it. That is the only way to get an arbitrary
   stroke while keeping the colour on a token, which matters here: a data
   URI cannot read a custom property, so an SVG used as background-image
   would have to carry a baked hex and would not survive dark mode. As a
   mask it carries no colour at all. The `%23000` inside each URI is a mask
   matte, not a colour: only its alpha is read.

   Both are pseudo-elements on an inline span, so they add no boxes to the
   line, cannot affect wrapping, and are invisible to assistive tech. The
   phrase itself carries the meaning.

   Sized in `em` throughout so a mark tracks the headline through the whole
   --fs-hero clamp instead of needing a breakpoint of its own. */
.es-hero__title .es-mark {
  position: relative;
  white-space: nowrap;
}

/* ── The mark cannot survive a phone ────────────────────────
   `nowrap` above is what keeps a drawn mark on one line, because the shape
   is one absolutely positioned box and a wrapped inline would leave it
   spanning from the start of the first line fragment to the end of the
   last, drawing through everything between.

   On a phone that guarantee turns into an overflow. --fs-hero bottoms out
   at its clamp minimum while the column keeps shrinking, so the phrase
   stops getting smaller and the container does not: the Shopify Migration
   H1 measured 482px of unbreakable text in a 351px column at 375px, 131px
   past the edge. Nothing scrolled, because the page clips horizontally
   further up, which meant the words were simply cut off rather than
   visibly broken.

   So below the phone breakpoint the phrase wraps like ordinary text and
   the drawn shape is dropped. The accent colour still carries the
   emphasis, which is what plain .es-hl does everywhere else on the site.
   575px is the partial's existing phone boundary, and the longest marked
   phrase across the service pages still fits at 576px with room to spare. */
@media (max-width: 575px) {
  .es-hero__title .es-mark { white-space: normal; }
  .es-hero__title .es-mark::after { content: none; }
}

/* The marks pass BEHIND the letters, the way a pen mark on paper sits under
   ink that was already there. Where the ring crosses a glyph the glyph
   wins, which is what stops it reading as a shape drawn on top of the word.

   Getting a mark behind the text takes two rules working together, and
   neither works alone:

     1. The headline is made a stacking context. A `z-index: -1` child
        resolves against the nearest ancestor stacking context, and the span
        is not one (position: relative with z-index auto does not create
        one), so without this the mark escapes all the way out and paints
        behind the section's own background, where it is invisible.

     2. The mark takes z-index -1. Inside a stacking context, negative
        descendants paint after that element's own background but before its
        in-flow and inline content, which is exactly the layer wanted: above
        the hero surface, below the words.

   `isolation: isolate` rather than a z-index on the headline, so no z-order
   is asserted against anything else in the section. */
.es-hero__title { isolation: isolate; }

.es-hero__title .es-mark::after {
  content: "";
  position: absolute;
  z-index: -1;
  background-color: var(--brand-300);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  pointer-events: none;
}

/* Sweep under the phrase, with the tail flicking back on itself the way a
   pen does when the hand lifts. Sits below the baseline, not on it, and
   overhangs the right edge slightly so it reads as drawn past the word. */
/* 1.08em, measured rather than reasoned. The containing block for an
   absolutely positioned child of an inline box is that box's content area,
   whose top sits above the cap height, so the baseline lands near 1.0em
   rather than at the bottom. Swept 0.92 to 1.18 against the real face at
   the real weight: 0.92 struck through the letters, 1.00 sat on them, and
   1.18 floated away from the phrase. */
.es-hero__title .es-mark--underline::after {
  left: -0.02em;
  right: -0.06em;
  top: 1.08em;
  height: 0.26em;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 26' preserveAspectRatio='none'%3E%3Cpath d='M4 15.5C72 6 160 3.5 243 7.6c30 1.5 50 4.8 53 10.4-9-7.2-31-9.6-60-8.4' fill='none' stroke='%23000' stroke-width='4.2' stroke-linecap='round'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 26' preserveAspectRatio='none'%3E%3Cpath d='M4 15.5C72 6 160 3.5 243 7.6c30 1.5 50 4.8 53 10.4-9-7.2-31-9.6-60-8.4' fill='none' stroke='%23000' stroke-width='4.2' stroke-linecap='round'/%3E%3C/svg%3E");
}

/* Ring around the phrase. Deliberately not closed: the stroke starts and
   ends at different points near the top, which is what stops it reading as
   an ellipse element and starts it reading as a circled word. */
/* Tight, now that the ring runs under the letters. The earlier 0.58em of
   overhang existed only to keep the arcs off the first and last glyph while
   the mark painted on top; with it behind the text there is nothing to
   avoid, and the reference draws its ring close in. Stroke thinned to match
   too: a heavy ring reads as a border, a fine one as a pen. */
.es-hero__title .es-mark--circle::after {
  left: -0.34em;
  right: -0.34em;
  top: -0.14em;
  bottom: -0.26em;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 96' preserveAspectRatio='none'%3E%3Cpath d='M158 7C77 5.5 9 24 9 48c0 24 64 41 145 41s137-17 137-41C291 26 234 9 172 7.2' fill='none' stroke='%23000' stroke-width='2.8' stroke-linecap='round'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 96' preserveAspectRatio='none'%3E%3Cpath d='M158 7C77 5.5 9 24 9 48c0 24 64 41 145 41s137-17 137-41C291 26 234 9 172 7.2' fill='none' stroke='%23000' stroke-width='2.8' stroke-linecap='round'/%3E%3C/svg%3E");
}

/* The marks arrive after the words they annotate, which is the order a
   person would draw them in. Their own animation rather than a share of
   the headline's: the headline is the LCP element and must not be held
   back to wait for its decoration.

   Fade only, no rise. The shared esHeroIn keyframe carries an 18px
   translate, which on an absolutely positioned mark would slide it off the
   phrase it is annotating instead of moving it with the line. */
@keyframes esHeroMarkIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.es-hero--enter .es-hero__title .es-mark::after {
  animation: esHeroMarkIn 520ms var(--ease-out) both;
  animation-delay: 0.62s;
}

.es-hero--bg-dark .es-hero__title .es-mark::after { background-color: var(--brand-500); }

/* The ring's overhang is a share of the font size, so it shrinks with the
   headline, but the container's gutter does not: it is a flat 12px at
   every width. Pulled in below the tablet step so the mark cannot reach
   past the gutter and be clipped by the section's own overflow. */
@media (max-width: 767px) {
  .es-hero__title .es-mark--circle::after {
    left: -0.22em;
    right: -0.22em;
  }
}

.es-hero__sub {
  margin: 0;
  color: var(--text-body);
  font-size: var(--fs-lede-lg);
  line-height: var(--lh-body);
  max-width: 62ch;
  text-wrap: pretty;
}

.es-hero--centered .es-hero__sub { margin-inline: auto; }

/* Highlighted phrase inside the subheadline. Weight rather than the title's
   italic: at body size an italic run reads as an aside instead of emphasis. */
.es-hero__sub .es-hl {
  color: var(--brand-600);
  font-weight: var(--font-semibold);
}

.es-hero--bg-dark .es-hero__sub .es-hl { color: var(--brand-400); }

.es-hero__ctas {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--gap-fluid-md);
}

.es-hero--centered .es-hero__ctas { justify-content: center; }

/* ── Lead form column ───────────────────────────────────────
   The card shell only. Field styling lives in the form partial that gets
   dropped in here, so the same form renders identically wherever else it
   is used and this sheet does not have to know its field names.

   Same panel language as .es-hero__media below (subtle border, xl radius,
   large shadow) so the two read as the same kind of object in the same
   slot. It parts company on the ground: a media panel is --bg-sunken so a
   screenshot sits IN it, while the form is a surface that sits ON the
   section, which is what a card holding inputs has to be. */
.es-hero__form {
  margin: var(--gap-fluid-lg) 0 0;
  padding: var(--space-7);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-2xl);
  background: var(--bg-surface);
  box-shadow: var(--shadow-xl);
}

/* The dark hero repaints the media panel's border further up this sheet.
   The form is a light surface on that ground by design (it is the
   conversion element and it should read as the one lit object in the
   composition), so it only needs its edge softened against the dark. */
.es-hero--bg-dark .es-hero__form {
  border-color: var(--border-default);
}

.es-hero__form-head {
  margin-bottom: var(--space-5);
}

/* Mark above the heading, not beside it: the heading wraps to two lines in
   the column widths this card actually gets, and a floated tile beside a
   wrapping heading leaves a ragged notch under itself. */
.es-hero__form-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  margin-bottom: var(--space-4);
  padding: var(--space-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
}

.es-hero__form-logo img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain;
}

.es-hero__form-title {
  margin: 0;
  color: var(--text-heading);
  font-family: var(--font-display);
  font-size: var(--fs-title-sm);
  font-weight: var(--font-bold);
  line-height: var(--lh-heading);
}

.es-hero__form-lede {
  margin: var(--space-2) 0 0;
  color: var(--text-muted);
  font-size: var(--fs-body-sm);
  line-height: var(--lh-body);
}

/* Reassurance under the fields. Real text rather than a colour or an icon
   alone, so it survives a screen reader and a monochrome display. */
.es-hero__form-badge {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  color: var(--text-muted);
  font-size: var(--fs-caption);
  line-height: var(--lh-body);
}

.es-hero__form-badge svg {
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--brand-600);
}

.es-hero--bg-dark .es-hero__form-badge svg { color: var(--brand-500); }

@media (min-width: 992px) {
  /* In the grid the card owns its cell, so the stacked margin comes off. */
  .es-hero--has-form .es-hero__form { margin-top: 0; }

  /* An even split. The near-even default is set for a screenshot beside
     copy; a form column narrower than its copy makes the inputs cramped
     while leaving the headline more width than a hero headline wants. */
  .es-hero--has-form .es-hero__grid {
    grid-template-columns: var(--es-hero-columns, 1fr 1fr);
    align-items: start;
  }

  /* The form is the tallest thing in the row and the copy should read from
     the top of it, not float against its middle. */
  .es-hero--has-form .es-hero__copy { align-self: start; }

  .es-hero__form { padding: var(--space-8); }
}

/* On a 375px screen the desktop padding is 48px a side, which leaves the
   inputs 254px of a 351px card. The card is the whole point of the column
   on a phone, so it gives the width back rather than framing itself. */
@media (max-width: 575px) {
  .es-hero__form {
    padding: var(--space-5);
    border-radius: var(--radius-xl);
  }
}

/* ── Embedded app console ────────────────────────────────
   A drawn reproduction of the Shopify Dev Dashboard, from the reference
   the owner supplied. It carries its OWN product top bar rather than
   browser chrome, because it is an app rather than a browser tab.

   ── WHY IT IS DRAWN AND NOT THE SCREENSHOT ────────────
   The reference is Shopify's own marketing asset. Three reasons it is not
   embedded here: it is their artwork on a commercial agency page; it
   carries their placeholder names, including a fictional person's, which
   would be a stranger's name on this site; and a bitmap in the hero would
   become the LCP element on a page whose LCP is currently the H1 text.
   Drawing it costs no request, scales at every width, and lets every
   label say something true about this business.

   ── Themed through local custom properties ────────────
   Every colour below resolves through a `--con-*` property declared once
   on the wrapper, so the whole panel re-themes by swapping that block
   rather than by writing a second selector for each of forty rules. The
   dark set is the one this page ships, on a light hero, which is the
   contrast the reference has.

   The base is --gray-900, the site's own warm near-black, NOT the green
   near-black in the reference. Reproducing Shopify's exact surface colour
   would make the panel read as a screenshot of their product; the site's
   own ground with the site's own accent on the selected row reads as
   Enstacked drawing a dashboard. Shopify green appears in exactly two
   places, both of which are true: their logo mark, and the status pill,
   which uses the shared success tokens.

   ── NO FIGURES, STILL ──────────────────────────────────
   The reference shows "0 installs". Nothing here states a count, a rate
   or a duration, because a number in a hero panel reads as a published
   result whatever it is attached to. See the note on `console` in the
   defaults above. */
.es-hero__console {
  position: relative;
  width: 100%;
  max-width: 560px;
  margin-inline: auto;
  margin-top: var(--gap-fluid-lg);

  --con-frame-bg:  var(--bg-surface);
  --con-frame-bd:  var(--border-subtle);
  --con-panel-bg:  var(--bg-sunken);
  --con-card-bg:   var(--bg-surface);
  --con-chip-bg:   var(--bg-sunken);
  --con-line:      var(--border-subtle);
  --con-ink:       var(--text-heading);
  --con-ink-body:  var(--text-body);
  --con-ink-mute:  var(--text-muted);
  --con-active-bg: var(--accent-subtle);
  --con-active-ink: var(--brand-700);
  --con-ok-bg:     var(--success-bg);
  --con-ok-ink:    var(--success-text);
  --con-shadow:    var(--shadow-lg);

  /* The two states the pointer paints. One step up from the resting
     surface and one step up from the resting hairline, in both themes, so
     every hover below is written once and re-themes with the panel. */
  --con-hover-bg:   var(--bg-sunken);
  --con-hover-line: var(--border-default);
}

.es-hero__console--dark {
  --con-frame-bg:  var(--gray-900);
  --con-frame-bd:  rgba(255, 255, 255, 0.12);
  --con-panel-bg:  rgba(255, 255, 255, 0.03);
  --con-card-bg:   rgba(255, 255, 255, 0.05);
  --con-chip-bg:   rgba(255, 255, 255, 0.08);
  --con-line:      rgba(255, 255, 255, 0.10);
  --con-ink:       var(--text-inverse);
  --con-ink-body:  var(--gray-300);
  --con-ink-mute:  var(--gray-400);
  --con-active-bg: rgba(232, 93, 32, 0.16);
  --con-active-ink: var(--brand-300);
  --con-ok-bg:     var(--success-bg-on-dark);
  --con-ok-ink:    var(--success-text-on-dark);
  --con-shadow:    var(--shadow-xl);
  --con-hover-bg:   rgba(255, 255, 255, 0.08);
  --con-hover-line: rgba(255, 255, 255, 0.22);
}

/* `aspect-ratio` on the frame, not a height derived from its contents, so
   the box reserves its exact space before a single row inside it has been
   laid out. That is what keeps this column off the CLS bill above the
   fold. Same reason the deck below states its ratio. */
.es-hero__console-frame {
  display: flex;
  flex-direction: column;
  aspect-ratio: 20 / 17;
  overflow: hidden;
  border: 1px solid var(--con-frame-bd);
  border-radius: var(--radius-xl);
  background: var(--con-frame-bg);
  box-shadow: var(--con-shadow);
}

/* ── Product top bar ─────────────────────────────────────
   The dashboard's own header: brand lockup on the left, theme control and
   store selector on the right. This is where the reference puts it, and
   it is why there are no traffic lights on this window: the dashboard is
   an app, not a browser tab, so there are no traffic lights on it. */
.es-hero__console-topbar {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--con-line);
}

/* ── The search field ────────────────────────────────────
   Furniture, not a control: there is no input in it and the panel it sits
   on is inert. It is here because a product top bar with a brand at one
   end, a store selector at the other and nothing between them is the one
   part of the reference that reads as a wireframe rather than an app.

   Capped rather than fluid so it stays a field and does not stretch into a
   bar across the whole window on a wide screen. */
.es-hero__console-search {
  display: inline-flex;
  flex: 0 1 300px;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  padding: 5px var(--space-3);
  border: 1px solid var(--con-line);
  border-radius: var(--radius-full);
  background: var(--con-chip-bg);
  color: var(--con-ink-mute);
  font-size: var(--fs-eyebrow);
  white-space: nowrap;
}

/* A drawn magnifier: ring on the pseudo before, handle on the one after.
   Its own span rather than a pseudo-element on the chip, because a glyph
   needs two of them and the chip's `after` is not adjacent to its text.
   Drawn rather than taken from `inc/icons.php` for the same reason the
   store awning and the theme control below are: the registry has nothing
   at this scale, and an 11px icon is line work, not artwork. */
.es-hero__console-mag {
  position: relative;
  flex: none;
  width: 11px;
  height: 11px;
  opacity: 0.8;
}

.es-hero__console-mag::before {
  content: "";
  position: absolute;
  inset: 0 3px 3px 0;
  border: 1.5px solid currentColor;
  border-radius: var(--radius-full);
}

.es-hero__console-mag::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: 0;
  width: 5px;
  height: 1.5px;
  border-radius: 1px;
  background: currentColor;
  transform: rotate(45deg);
  transform-origin: 100% 50%;
}

.es-hero__console-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.es-hero__console-logo {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-xs);
}

.es-hero__console-logo img {
  display: block;
  width: 15px;
  height: auto;
}

.es-hero__console-wordmark {
  overflow: hidden;
  color: var(--con-ink);
  font-size: var(--fs-body-sm);
  font-weight: var(--font-medium);
  letter-spacing: var(--tracking-normal);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.es-hero__console-wordmark b { font-weight: var(--font-bold); }

.es-hero__console-topright {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--space-3);
}

/* The theme control. Drawn as a small sun rather than pulled from the icon
   registry: the registry has no sun, and its marks are sized for buttons,
   so any of them here would outweigh the store chip beside it. */
.es-hero__console-sun {
  position: relative;
  flex: none;
  width: 12px;
  height: 12px;
  border-radius: var(--radius-full);
  background: var(--con-ink-mute);
}

.es-hero__console-sun::before,
.es-hero__console-sun::after {
  content: "";
  position: absolute;
  inset: -4px;
  border: 1.5px solid var(--con-ink-mute);
  border-radius: var(--radius-full);
  opacity: 0.45;
}

.es-hero__console-sun::after { inset: -7px; opacity: 0.22; }

.es-hero__console-store {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  max-width: 190px;
  overflow: hidden;
  padding: 5px var(--space-3);
  border: 1px solid var(--con-line);
  border-radius: var(--radius-sm);
  background: var(--con-chip-bg);
  color: var(--con-ink);
  font-size: var(--fs-caption);
  font-weight: var(--font-medium);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The little storefront mark on the chip. A drawn awning, again because
   the registry has nothing at this scale. */
.es-hero__console-store::before {
  content: "";
  flex: none;
  width: 13px;
  height: 13px;
  border: 1.5px solid currentColor;
  border-radius: 2px 2px 3px 3px;
  border-top-width: 4px;
  opacity: 0.75;
}

/* ── Body: sidebar beside the app ────────────────────── */
.es-hero__console-body {
  display: grid;
  flex: 1 1 auto;
  grid-template-columns: 152px 1fr;
  min-height: 0;
}

.es-hero__console-rail {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-4) var(--space-3);
  border-right: 1px solid var(--con-line);
  background: var(--con-panel-bg);
}

/* A top-level destination in the sidebar: Apps, Dev stores. Sits at the
   same indent as the app name and above the app's own pages. */
.es-hero__console-group {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2);
  color: var(--con-ink);
  font-size: var(--fs-caption);
  font-weight: var(--font-medium);
  line-height: 1.2;
}

.es-hero__console-group::before {
  content: "";
  flex: none;
  width: 12px;
  height: 12px;
  border: 1.5px solid currentColor;
  border-radius: 2px;
  opacity: 0.6;
}

/* The store mark on the second group, so the two are not the same glyph. */
.es-hero__console-group--store::before {
  border-radius: 2px 2px 3px 3px;
  border-top-width: 4px;
}

/* The selected app. A heading over its pages, not a link among them. */
.es-hero__console-appname {
  overflow: hidden;
  margin: var(--space-3) 0 var(--space-1);
  padding-inline: var(--space-2);
  color: var(--con-ink);
  font-size: var(--fs-caption);
  font-weight: var(--font-bold);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* One of the app's pages. Indented under the app name, with room on the
   left for the selected marker so nothing shifts when it moves. */
.es-hero__console-nav {
  position: relative;
  padding: 6px var(--space-3);
  margin-inline-start: var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--con-ink-mute);
  font-size: var(--fs-caption);
  line-height: 1.3;
}

/* The page the dashboard is open on. A raised chip plus the brand bar in
   the gutter, which is the treatment in the reference, in this site's
   accent rather than Shopify's green. */
.es-hero__console-nav.is-current {
  background: var(--con-active-bg);
  color: var(--con-active-ink);
  font-weight: var(--font-semibold);
}

.es-hero__console-nav.is-current::before {
  content: "";
  position: absolute;
  inset-block: 4px;
  inset-inline-start: calc(var(--space-3) * -1);
  width: 2px;
  border-radius: var(--radius-full);
  background: var(--brand-500);
}

.es-hero__console-main {
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: var(--space-4);
}

/* ── One dashboard page ──────────────────────────────────
   The rail switches between these. Every page is in the DOM; which one is
   shown is the `hidden` attribute, set by the script, so switching costs
   no request and nothing reflows outside the panel.

   `min-height: 0` on a flex child that scrolls its own overflow: without
   it the panel refuses to shrink below its content and pushes the frame
   past the aspect ratio it is supposed to hold. */
.es-hero__console-panel {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-3);
  min-height: 0;
}

/* The script sets `hidden`; this makes it stick against the flex display
   above, which would otherwise win and show every page at once. */
.es-hero__console-panel[hidden] { display: none; }

/* A tab that the script has made interactive. Cursor and hover only appear
   once the upgrade has run, so the static illustration never advertises
   behaviour it does not have. */
.es-hero__console-nav[role="tab"] { cursor: pointer; }

.es-hero__console-nav[role="tab"]:hover:not(.is-current) {
  background: var(--con-chip-bg);
  color: var(--con-ink);
}

.es-hero__console-nav[role="tab"]:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}

/* ── Titled list card ────────────────────────────────────
   The shape Versions, Monitoring, Logs and Settings all take: a heading
   with an optional control, then rows. One block type rather than four
   bespoke panels, because the real pages differ in their content and not
   in their furniture. */
.es-hero__console-list {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-3);
  min-height: 0;
  padding: var(--space-4);
  border: 1px solid var(--con-line);
  border-radius: var(--radius-md);
  background: var(--con-card-bg);
}

/* Takes the card's free height so the rows below can share it out. Without
   this the rows sit at their content height at the top of a card that has
   been stretched to the full height of the page, which is the gap under
   every short list the owner flagged. */
.es-hero__console-listrows {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-2);
  min-height: 0;
  overflow: hidden;
}

/* ── Why a row grows, and why only so far ────────────────
   `1 0 auto` lets a row take a share of whatever the card has spare, so a
   four-row list on a 1600px screen is four comfortable rows rather than
   four tight ones with a hole under them. No shrink, ever: a flex item
   allowed to shrink below its content is how the topic list vanished on a
   phone the last time this panel was tuned.

   The cap is what stops the same rule turning a two-row list into two
   bands. Past roughly this height a row stops reading as a row of a table
   and starts reading as a card, and the leftover space belongs at the
   bottom of the list, which is where a real table leaves it. */
.es-hero__console-listrow {
  display: flex;
  flex: 1 0 auto;
  align-items: center;
  gap: var(--space-2);
  max-height: 56px;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--con-line);
  border-radius: var(--radius-sm);
  color: var(--con-ink-body);
  font-size: var(--fs-caption);
  line-height: 1.3;
}

/* The row's leading label, e.g. the setting name. Takes the remaining
   width so the code and the pill after it stay pinned right. */
.es-hero__console-listrow > .es-hero__console-rowlabel {
  flex: 1 1 auto;
  overflow: hidden;
  color: var(--con-ink);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The event-type chip on a log row: Webhook, Function, App Event. Its own
   class rather than the status pill, because a type is not a state and
   colouring it green would say the row succeeded. */
.es-hero__console-tag {
  flex: none;
  padding: 2px var(--space-2);
  border-radius: var(--radius-xs);
  background: var(--con-chip-bg);
  color: var(--con-ink-mute);
  font-family: var(--font-label);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-semibold);
  letter-spacing: var(--tracking-wide);
}

.es-hero__console-listrow code {
  overflow: hidden;
  padding: 2px 6px;
  border-radius: var(--radius-xs);
  background: var(--con-chip-bg);
  color: var(--con-ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-eyebrow);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Trailing note, e.g. "Released just now". Pushed right, and the first
   thing dropped when the row runs out of width. */
.es-hero__console-note {
  flex: none;
  margin-inline-start: auto;
  color: var(--con-ink-mute);
  font-size: var(--fs-eyebrow);
  white-space: nowrap;
}

/* ── Plot card ───────────────────────────────────────────
   Monitoring only. Restored from the version of this panel that drew the
   Monitoring page, because that page really is a chart. */
.es-hero__console-plotcard {
  display: flex;
  flex: none;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--con-line);
  border-radius: var(--radius-md);
  background: var(--con-card-bg);
}

/* Height stays DEFINITE. Each bar is a percentage, and a percentage height
   resolves against its parent only when that parent's height is definite:
   with `auto` the parent depends on its children and the children on the
   parent, which resolves to zero and collapses every bar onto the floor. */
.es-hero__console-plot {
  display: flex;
  align-items: flex-end;
  gap: 5px;
  height: clamp(64px, 9vw, 132px);

  /* Four rules behind the bars, drawn from the panel's own hairline. They
     carry no scale and no figures: they are what makes a row of bars read
     as a plot rather than as a bar graphic, which is the same job the axis
     strip below does. A gridline with a number against it would be a
     measurement, and this chart deliberately states none. */
  background-image: repeating-linear-gradient(
    to top,
    var(--con-line) 0,
    var(--con-line) 1px,
    transparent 1px,
    transparent 25%
  );
}

.es-hero__console-bar {
  flex: 1 1 0;
  height: var(--es-plot, 50%);
  border-radius: 3px 3px 0 0;
  background: linear-gradient(180deg, var(--brand-400) 0%, var(--brand-200) 100%);
}

.es-hero__console-bar:last-child {
  background: linear-gradient(180deg, var(--brand-600) 0%, var(--brand-400) 100%);
}

/* The x axis. One label per bar, on the same track widths as the bars, so
   a tick sits under the column it belongs to rather than near it. */
.es-hero__console-plotaxis {
  display: flex;
  gap: 5px;
  margin-top: calc(var(--space-2) * -1);
  color: var(--con-ink-mute);
  font-size: var(--fs-eyebrow);
  line-height: 1.2;
}

.es-hero__console-plotaxis span {
  flex: 1 1 0;
  overflow: hidden;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Summary cards ───────────────────────────────────── */
.es-hero__console-cards {
  display: grid;
  flex: none;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
}

.es-hero__console-card,
.es-hero__console-activity {
  padding: var(--space-4);
  border: 1px solid var(--con-line);
  border-radius: var(--radius-md);
  background: var(--con-card-bg);
}

/* Two cards side by side are as tall as the taller one, so the shorter one
   has room left over. Column layout with the row pushed to the bottom puts
   that room between the note and the action instead of under both, which
   is what makes the pair read as one row of cards rather than as one card
   and one card with a gap in it. */
.es-hero__console-card {
  display: flex;
  flex-direction: column;
}

.es-hero__console-card .es-hero__console-rowbtn {
  margin-top: auto;
}

.es-hero__console-activity {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-3);
  min-height: 0;
}

.es-hero__console-cardhead {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
}

.es-hero__console-activity .es-hero__console-cardhead {
  align-items: center;
  margin-bottom: 0;
}

.es-hero__console-cardtitle {
  color: var(--con-ink);
  font-size: var(--fs-body-sm);
  font-weight: var(--font-bold);
  letter-spacing: var(--tracking-normal);
}

.es-hero__console-cardnote {
  grid-column: 1;
  color: var(--con-ink-mute);
  font-size: var(--fs-caption);
  line-height: 1.35;
}

/* The small square control in a card's top-right corner. Two variants: an
   icon button, and a wider labelled one. */
.es-hero__console-cardbtn {
  display: inline-flex;
  grid-row: 1 / span 2;
  grid-column: 2;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px var(--space-2);
  border: 1px solid var(--con-line);
  border-radius: var(--radius-sm);
  background: var(--con-chip-bg);
  color: var(--con-ink);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-medium);
  white-space: nowrap;
}

.es-hero__console-cardbtn svg {
  width: 13px;
  height: 13px;
}

/* The row inside a card that behaves like a button: a value on the left
   and a chevron pinned right. */
.es-hero__console-rowbtn {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--con-line);
  border-radius: var(--radius-sm);
  color: var(--con-ink-body);
  font-size: var(--fs-caption);
}

.es-hero__console-rowbtn code {
  overflow: hidden;
  padding: 2px 6px;
  border-radius: var(--radius-xs);
  background: var(--con-chip-bg);
  color: var(--con-ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-eyebrow);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.es-hero__console-rowbtn .es-hero__console-go {
  margin-inline-start: auto;
  color: var(--con-ink-mute);
}

.es-hero__console-go svg {
  display: block;
  width: 14px;
  height: 14px;
}

/* Status pill. The shared success tokens, so it is the same green the rest
   of the system uses rather than a colour picked to match a screenshot. */
.es-hero__console-pill {
  flex: none;
  padding: 2px var(--space-2);
  border-radius: var(--radius-xs);
  background: var(--con-ok-bg);
  color: var(--con-ok-ink);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-semibold);
}

/* The sort control on the activity card. */
.es-hero__console-select {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px var(--space-3);
  border: 1px solid var(--con-line);
  border-radius: var(--radius-sm);
  background: var(--con-chip-bg);
  color: var(--con-ink);
  font-size: var(--fs-eyebrow);
  white-space: nowrap;
}

.es-hero__console-select::after {
  content: "";
  width: 0;
  height: 0;
  border-inline: 3px solid transparent;
  border-top: 4px solid currentColor;
  opacity: 0.6;
}

/* ── Activity timeline ───────────────────────────────── */
.es-hero__console-timeline {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 0;
  padding-inline-start: var(--space-5);
}

/* The rule the event dots are threaded onto. Stops short at both ends so
   it reads as a run between events, not as a border on the card. */
.es-hero__console-timeline::before {
  content: "";
  position: absolute;
  inset-block: 6px;
  inset-inline-start: 3px;
  width: 1px;
  background: var(--con-line);
}

.es-hero__console-day {
  color: var(--con-ink-mute);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.es-hero__console-event {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  color: var(--con-ink-body);
  font-size: var(--fs-caption);
  line-height: 1.4;
}

.es-hero__console-eventtext {
  flex: 1 1 auto;
  min-width: 0;
}

/* The stamp on an event. A relative time, never a duration: see the note
   on figures in the partial's console defaults. */
.es-hero__console-eventtime {
  flex: none;
  color: var(--con-ink-mute);
  font-size: var(--fs-eyebrow);
  white-space: nowrap;
}

/* The group is a grouping only. Its children lay out as though they were
   direct children of the timeline, which is what keeps one flex column and
   one rule running behind every dot. */
.es-hero__console-run {
  display: contents;
}

/* Every day group after the first opens a run of its own, so the dated
   headings do not sit flush against the event above them. */
.es-hero__console-day:not(:first-child) {
  margin-top: var(--space-2);
}

.es-hero__console-event::before {
  content: "";
  position: absolute;
  top: 5px;
  inset-inline-start: calc(var(--space-5) * -1 + 1px);
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--con-ink-mute);
}

.es-hero__console-event b {
  color: var(--con-ink);
  font-weight: var(--font-semibold);
}

.es-hero__console-event code {
  padding: 1px 4px;
  border-radius: var(--radius-xs);
  background: var(--con-chip-bg);
  color: var(--con-ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--fs-eyebrow);
}

/* ── The filter strip ────────────────────────────────────
   The segmented control an index page carries over its rows. One chip is
   in force and the rest are available, which is the state a real page is
   in when you land on it.

   It scrolls sideways rather than wrapping, for the reason the rail does
   at phone widths: a two-row segmented control stops reading as one. */
.es-hero__console-filters {
  display: flex;
  flex: none;
  gap: var(--space-1);
  overflow-x: auto;
  margin-top: calc(var(--space-1) * -1);
  scrollbar-width: none;
}

.es-hero__console-filters::-webkit-scrollbar { display: none; }

.es-hero__console-filter {
  flex: none;
  padding: 3px var(--space-3);
  border-radius: var(--radius-full);
  color: var(--con-ink-mute);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-medium);
  white-space: nowrap;
}

.es-hero__console-filter.is-on {
  background: var(--con-chip-bg);
  color: var(--con-ink);
}

/* ── Row state marker ────────────────────────────────────
   A dot, not a second pill: a row that already carries a status pill on
   its right does not need the same word again on its left, and a list
   where every row is a green pill stops distinguishing anything.

   Three states, and all three resolve to colours the panel already has:
   the success ink it paints pills with, the accent, and its own muted
   ink. No new colour enters the system for a 6px disc. */
.es-hero__console-dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--con-ink-mute);
}

.es-hero__console-dot--ok   { background: var(--con-ok-ink); }
.es-hero__console-dot--warn { background: var(--brand-400); }
.es-hero__console-dot--idle { background: var(--con-ink-mute); opacity: 0.55; }

/* ── A row's second line ─────────────────────────────────
   Rows that carry one are the only rows allowed to wrap, which is why the
   class is on the row and not just on the line. Left as one line with
   `nowrap` on everything else, a long detail would push the note off the
   end of the row instead of dropping under it. */
/* Two lines of content, so the ceiling is two lines higher. */
.es-hero__console-listrow--sub {
  flex-wrap: wrap;
  max-height: 78px;
  row-gap: 2px;
}

.es-hero__console-rowsub {
  flex-basis: 100%;
  overflow: hidden;
  color: var(--con-ink-mute);
  font-size: var(--fs-eyebrow);
  line-height: 1.35;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The caption under a card's rows: what the list is showing, or what the
   chart is measured over. Pushed to the bottom of the card so it sits on
   the floor of the panel rather than floating under the last row. */
.es-hero__console-foot {
  margin-top: auto;
  padding-top: var(--space-1);
  color: var(--con-ink-mute);
  font-size: var(--fs-eyebrow);
  line-height: 1.3;
}

/* ── Two-column page ─────────────────────────────────────
   A page whose blocks belong side by side: the primary object in the wide
   column, its secondary panel in the narrow one. Wide screens only. Below
   the point where the narrow column stops being usable the blocks stack
   in source order, which is the single-column page they already were.

   Declared on the BAND only. The panel form is 560px at its widest, and
   two columns inside that is two cards too narrow to read. */
@media (min-width: 992px) {
  .es-hero--console-band .es-hero__console-panel--split {
    display: grid;
    grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
    gap: var(--space-3);
  }

  /* The wide column's block is placed, so the second one has nowhere to go
     but the narrow column. Split is limited to two blocks in the partial
     above for exactly this reason. */
  .es-hero--console-band .es-hero__console-panel--split > :first-child {
    grid-column: 1;
    grid-row: 1;
  }

  /* A chart in a stretched column takes the column. Its card is the full
     height of the page either way, so the choice is between a chart that
     fills it and a chart sitting on a lot of nothing.

     The bars stay percentages of the plot, and the plot's height stays
     DEFINITE: it is resolved by flex layout against a grid item that has
     been stretched to a known height, which is not the auto-height case
     the note above warns about. `min-height` is the height it had before
     this rule, so the card can never be shorter than it used to be. */
  .es-hero--console-band .es-hero__console-panel--split .es-hero__console-plot {
    flex: 1 1 auto;
    height: auto;
    min-height: clamp(64px, 9vw, 132px);
  }
}

/* ══ Pointer response ═════════════════════════════════════
   What stops the panel reading as a screenshot. Every surface in it
   answers the pointer, and none of it pretends to be a control.

   ── THE TWO RULES THIS IS BUILT ON ──────────────────
   1. Nothing here gets `cursor: pointer` except the rail, which is the one
      thing in the panel that really does something. A row that invites a
      click and then swallows it is worse than a row that never invited
      one; the hover states below say "this surface is live", not "press
      me". For the same reason there are no active states, no ripples and
      no press depressions anywhere in this block.

   2. All of it is gated on `:not([inert])`. section-hero.php ships the
      panel `inert`, and es-sections.js removes that attribute only once it
      has wired the tablist up. So the illustration a visitor gets before
      the script runs, or with scripts blocked, is exactly as inert as it
      looks: no hover, no highlight, nothing that suggests it responds.
      This is the same rule the rail's own `[role="tab"]` hover follows,
      reusing an attribute the script already toggles rather than adding a
      class for it.

   ── AND ONE SHAPE ───────────────────────────────────
   Hovering a thing lifts THAT thing and settles the things around it. The
   row you are on takes a surface and its hairline comes up, the rows
   either side fall back; the bar you are on holds, the rest of the chart
   recedes. One move, applied to rows, to chart columns and to feed events
   alike, which is what makes the panel read as one instrument rather than
   as a page of separate widgets.

   ── AND NO COLOUR IN ANY OF IT ──────────────────────
   Every state below is built from the panel's own greys: a surface, a
   hairline, and ink. No accent anywhere. Colour in an admin means
   something specific, and all three of the things it means here are
   already spoken for: the rail paints the page you are ON, the pill paints
   a status, and the dot paints a state. A fourth use of it for "your mouse
   is here" would devalue the other three, and the accent it would have to
   borrow is this site's orange, sitting on a drawing of somebody else's
   product. The one coloured thing that moves with the pointer is the
   pointer itself.

   Durations are the house `--duration-fast` on the house `--ease-out`. */

/* ── A real pointer, or none of it ───────────────────────
   `hover: hover` rather than a set of undo rules for touch. A finger has
   no hover state to leave: it taps, the state lands, and on a panel where
   nothing is clickable it stays landed on whatever was touched last, so a
   row sits highlighted with no way to clear it and no meaning if it did.
   Gating the whole block means a touch device is never given a state it
   cannot get out of, rather than being given one and then talked out of
   it rule by rule. */
@media (hover: hover) {

  /* ── No cursor rule ──────────────────────────────────────
     Four rules used to sit here, drawing a custom cursor over the frame
     and a pressed variant of it on mousedown. They are gone on the owner's
     instruction, with the four asset files they named.

     Nothing replaces them, and that is the point: the panel takes the
     system cursor, and the rail takes the system hand from the
     `[role="tab"]` rule further up the sheet. The browser's own two
     cursors say which part of this is a control, which is what they are
     for and what the drawn pair was standing in front of.

     The hover states below still do the work the custom cursor was also
     doing, which is telling a visitor this is not a screenshot. */

  /* ── Card surfaces ───────────────────────────────────────
     The whole card answers first, before anything inside it. A hairline and
     a half-step of ground: enough that moving across the panel feels like
     moving across something, not enough to compete with the row highlight
     that lands a moment later. */
  .es-hero__console-list,
  .es-hero__console-plotcard,
  .es-hero__console-activity,
  .es-hero__console-card {
    transition:
      border-color var(--duration-fast) var(--ease-out),
      background   var(--duration-fast) var(--ease-out);
  }

  .es-hero__console:not([inert]) .es-hero__console-list:hover,
  .es-hero__console:not([inert]) .es-hero__console-plotcard:hover,
  .es-hero__console:not([inert]) .es-hero__console-activity:hover,
  .es-hero__console:not([inert]) .es-hero__console-card:hover {
    border-color: var(--con-hover-line);
  }

  /* ── Rows ────────────────────────────────────────────────
     Ground, hairline and ink. Nothing else, and nothing coloured.

     This carried a 2px brand bar in the gutter for a while, borrowed from
     the rail's selected-page marker. It looked expensive and it was wrong:
     an accent bar is how a real admin says THIS ROW IS SELECTED, and
     hovering a row does not select it. Painting the accent under the
     pointer taught the panel a vocabulary the rest of it does not use, and
     on a page whose accent is orange it also put the site's brand on top
     of a drawing of somebody else's product.

     What a real dashboard does under the pointer is exactly this: the row
     takes a neutral surface a half-step off the card, its hairline comes
     up, and its text goes to full heading ink. */
  .es-hero__console-listrow {
    transition:
      background   var(--duration-fast) var(--ease-out),
      border-color var(--duration-fast) var(--ease-out),
      opacity      var(--duration-fast) var(--ease-out),
      color        var(--duration-fast) var(--ease-out);
  }

  /* ── How far the other rows settle, and why not further ──
     0.85, and the number is a contrast floor rather than a taste call. The
     rows carry muted text at --fs-eyebrow, which sits at 6.91:1 on the card
     at rest. Dimming the row dims that text with it: measured on this
     panel's dark ground, 0.72 took it to 4.27:1, which is under AA, and 0.8
     to 4.93:1. 0.85 holds it at 5.39:1 with room for the light theme, where
     the same ink is darker and loses more per step.

     It reads as more than 15% because the dim is not doing the work on its
     own: the row under the pointer takes a ground, a brighter hairline and
     heading ink at the same moment. */
  .es-hero__console:not([inert]) .es-hero__console-listrows:hover .es-hero__console-listrow {
    opacity: 0.85;
  }

  .es-hero__console:not([inert]) .es-hero__console-listrow:hover {
    background: var(--con-hover-bg);
    border-color: var(--con-hover-line);
    color: var(--con-ink);
    opacity: 1;
  }

  /* The state dot on the hovered row comes up to full. An idle dot is held
     at 0.55 at rest, so this is the one place the effect adds information:
     the row you are reading shows its state at full strength. */
  .es-hero__console-dot {
    transition: opacity var(--duration-fast) var(--ease-out);
  }

  .es-hero__console:not([inert]) .es-hero__console-listrow:hover .es-hero__console-dot {
    opacity: 1;
  }

  /* ── Chart ───────────────────────────────────────────────
     A real chart answers the pointer per column, so this one does too. The
     rest of the plot recedes and the column under the pointer keeps its
     colour and takes a light cap.

     Nothing MOVES, nothing changes height and nothing changes hue: these
     bars are an abstract shape rather than a measurement, and a bar that
     grows or lights up under the pointer is a shape making a claim about
     itself. The column is picked out by what happens to the OTHER columns,
     which is how a real chart does it and costs no new colour. */
  .es-hero__console-bar {
    transition: opacity var(--duration-fast) var(--ease-out);
  }

  .es-hero__console:not([inert]) .es-hero__console-plot:hover .es-hero__console-bar {
    opacity: 0.42;
  }

  .es-hero__console:not([inert]) .es-hero__console-bar:hover {
    opacity: 1;
  }

  /* ── Activity ────────────────────────────────────────────
     Same shape again, one line at a time: the run settles and the event
     under the pointer holds, with its dot growing on the thread. */
  .es-hero__console-event {
    transition:
      opacity var(--duration-fast) var(--ease-out),
      color   var(--duration-fast) var(--ease-out);
  }

  .es-hero__console-event::before {
    transition:
      transform  var(--duration-fast) var(--ease-out),
      background var(--duration-fast) var(--ease-out);
  }

  .es-hero__console:not([inert]) .es-hero__console-timeline:hover .es-hero__console-event {
    opacity: 0.85;
  }

  .es-hero__console:not([inert]) .es-hero__console-event:hover {
    color: var(--con-ink);
    opacity: 1;
  }

  /* The dot comes up to heading ink and grows. It was the accent here for
     the same bad reason the row marker was: an event under the pointer is
     being read, not selected. */
  .es-hero__console:not([inert]) .es-hero__console-event:hover::before {
    background: var(--con-ink);
    transform: scale(1.45);
  }

  /* ── The chip-shaped things ──────────────────────────────
     The sort control, the corner buttons, the filter strip, the store
     selector and the search field. They are drawn as controls, so they are
     the elements a pointer is most likely to test, and a chip that stays
     dead under the pointer is what makes a panel read as a picture of a
     dashboard. They brighten. They still do not take a pointer cursor,
     because they still do nothing. */
  .es-hero__console-select,
  .es-hero__console-cardbtn,
  .es-hero__console-filter,
  .es-hero__console-store,
  .es-hero__console-search,
  .es-hero__console-rowbtn {
    transition:
      background   var(--duration-fast) var(--ease-out),
      border-color var(--duration-fast) var(--ease-out),
      color        var(--duration-fast) var(--ease-out);
  }

  .es-hero__console:not([inert]) .es-hero__console-select:hover,
  .es-hero__console:not([inert]) .es-hero__console-cardbtn:hover,
  .es-hero__console:not([inert]) .es-hero__console-store:hover,
  .es-hero__console:not([inert]) .es-hero__console-search:hover,
  .es-hero__console:not([inert]) .es-hero__console-rowbtn:hover {
    background: var(--con-hover-bg);
    border-color: var(--con-hover-line);
    color: var(--con-ink);
  }

  /* The filter that is not the one in force previews what choosing it would
     look like: it takes the ground the selected chip has. The selected one
     is already there and does not move. */
  .es-hero__console:not([inert]) .es-hero__console-filter:hover:not(.is-on) {
    background: var(--con-chip-bg);
    color: var(--con-ink);
  }

  /* The chevron on a card's action row travels, which is the microinteraction
     every CTA on this site runs. Same direction, a third of the distance:
     this one is a drawing of a link, not a link. */
  .es-hero__console-go svg {
    transition: transform var(--duration-fast) var(--ease-out);
  }

  .es-hero__console:not([inert]) .es-hero__console-rowbtn:hover .es-hero__console-go svg {
    transform: translateX(3px);
  }

  .es-hero__console:not([inert]) .es-hero__console-rowbtn:hover .es-hero__console-go {
    color: var(--con-ink);
  }
}

/* ── Reduced motion ─────────────────────────────────────
   Not all of the block goes, because not all of it is motion. What goes is
   the animation and the two embellishments that are pure travel: the
   chevron no longer moves and the event dot no longer grows.

   The dims go too, and that is the one judgement call here. A dim with no
   transition under it is not a settle, it is a flicker: the neighbouring
   rows snap two shades darker and back on every pixel the pointer crosses.
   Instant is worse than absent.

   What stays is everything that is a STATE rather than a movement: the
   ground, the hairline and the ink on the row under the pointer, and the
   cursor over the panel. Those land instantly, which is exactly what this
   preference asks for, and taking them away would leave a visitor who
   asked for less motion with a panel that answers them not at all. */
@media (prefers-reduced-motion: reduce) {
  .es-hero__console-list,
  .es-hero__console-plotcard,
  .es-hero__console-activity,
  .es-hero__console-card,
  .es-hero__console-listrow,
  .es-hero__console-dot,
  .es-hero__console-bar,
  .es-hero__console-event,
  .es-hero__console-event::before,
  .es-hero__console-select,
  .es-hero__console-cardbtn,
  .es-hero__console-filter,
  .es-hero__console-store,
  .es-hero__console-search,
  .es-hero__console-rowbtn,
  .es-hero__console-go svg {
    transition: none;
  }

  .es-hero__console:not([inert]) .es-hero__console-listrows:hover .es-hero__console-listrow,
  .es-hero__console:not([inert]) .es-hero__console-timeline:hover .es-hero__console-event,
  .es-hero__console:not([inert]) .es-hero__console-plot:hover .es-hero__console-bar {
    opacity: 1;
  }

  .es-hero__console:not([inert]) .es-hero__console-event:hover::before {
    transform: none;
  }

  .es-hero__console:not([inert]) .es-hero__console-rowbtn:hover .es-hero__console-go svg {
    transform: none;
  }
}

/* ── On the .98 in every width below ─────────────────────
   Bootstrap's convention, and it is here because the container this panel
   sits in is Bootstrap's. A rule written `max-width: 767px` beside a
   container that widens at `min-width: 768px` leaves a hole: a viewport
   that lands between them, which fractional zoom and a scaled display both
   produce, matches NEITHER. Measured in that hole, the container had
   already dropped to 540px while the two summary cards were still side by
   side, which is the 144px card the note further down describes, and the
   panel drew itself 1110px tall.

   The `min-width` queries stay round, because 768 and up is exactly what
   they mean. */

/* ── Narrow screens ──────────────────────────────────────
   The sidebar used to be DROPPED here. It cannot be any more: the rail is
   now the control that changes the page, so hiding it on a phone would
   take the interaction away from exactly the visitors most likely to poke
   at it.

   It turns instead: the vertical sidebar becomes a horizontal strip of
   tabs above the panel, which is how a real admin behaves at this width
   and keeps every page reachable. The strip scrolls sideways rather than
   wrapping, because a two-row tab strip stops reading as a rail.

   The two decorative rows that bracket the app in the sidebar (Apps, Dev
   stores) go: they are navigation to somewhere else in the dashboard, not
   pages of this app, and on one line they would sit among the tabs and
   look selectable. */
@media (max-width: 639.98px) {
  .es-hero__console-frame { aspect-ratio: 5 / 4; }
  .es-hero__console-body  { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
  .es-hero__console-main  { padding: var(--space-3); }
  .es-hero__console-store { max-width: 120px; }

  .es-hero__console-rail {
    flex-direction: row;
    gap: var(--space-1);
    align-items: center;
    padding: var(--space-2) var(--space-3);
    border-right: 0;
    border-bottom: 1px solid var(--con-line);
    overflow-x: auto;
    scrollbar-width: none;
  }

  .es-hero__console-rail::-webkit-scrollbar { display: none; }

  .es-hero__console-group,
  .es-hero__console-appname { display: none; }

  .es-hero__console-nav {
    flex: none;
    margin-inline-start: 0;
    white-space: nowrap;
  }

  /* The selected marker is a bar in the left gutter on the vertical rail.
     Laid out horizontally there is no gutter, so it moves under the tab. */
  .es-hero__console-nav.is-current::before {
    inset: auto 0 -6px 0;
    width: auto;
    height: 2px;
  }
}

/* Two cards side by side stop working before the sidebar does. They stack
   instead. The second card used to be DROPPED here, which took half of
   what the Home page is off the phone entirely; it is kept now that the
   frame's height is set by its content rather than by a ratio.

   The card is the constraint here, not the viewport: the labelled button
   is a constant 124px in its own corner, so whenever the card head falls
   under about 300px the heading beside that button wraps one word per line
   and the card grows to several hundred pixels tall.

   991px, not 767px. The bound was measured at 767 on the assumption that
   the 540 container was the narrowest case, but the card head stays pinned
   at 200px right across 768-991: the container steps up to 720/960 while
   the sidebar takes the difference, so `Installs` rendered at 64px with its
   note wrapped over four lines on iPad portrait (768) and iPad Air (820)
   exactly as it had at 640. 991.98px is the real upper bound of the band
   that produces a card too narrow for the labelled button; at 992 the head
   reaches 299px and the note settles onto two lines. */
@media (max-width: 991.98px) {
  .es-hero__console-cards { grid-template-columns: 1fr; }
}

/* The log-row reflow below stays on the 767 bound. It is a remedy for a
   card that is genuinely narrow, and once the cards stack they are full
   width, so applying it across 768-991 would wrap a row that has room. */
@media (max-width: 767.98px) {
  /* ── A log row on a narrow card ──────────────────────
     Five things on one line: a state dot, an entry-type chip, the entity,
     its status and when it happened. On a card this narrow the entity is
     the one that loses, and it is the only one of the five that cannot be
     guessed from the others: `discount-calculator` was rendering as about
     four characters and an ellipsis.

     So it takes a line of its own, under the row it belongs to, and the
     four short things keep the first line. `order` rather than a second
     markup path: the row is built once and re-flowed here. */
  /* The row grows with the card, and a wrapped flex row spreads its lines
     across whatever height it is given. That pushed the chips to the top
     of the row and the code to the bottom with a hole between them, which
     is the loose rhythm in the owner's reference. The two lines are one
     entry and belong together in the middle of the row. */
  .es-hero__console-listrow--tagged {
    flex-wrap: wrap;
    align-content: center;
    max-height: 78px;
    row-gap: 2px;
  }

  /* ── The break, and the token ────────────────────────
     `flex-basis: 100%` on the code was doing two jobs: putting it on a
     line of its own, and making it as wide as the row. The first is
     right. The second turned a token into a full-width slab with its text
     in the left end of it, which reads as an empty input field rather
     than as the name of the thing the row is about.

     A zero-height item carrying the full basis takes the line break on
     itself, so the code goes back to being the width of the code. It is a
     pseudo-element rather than markup because the break is a consequence
     of this width and exists at no other width.

     `row-gap` drops to 2 because there are three flex lines now and only
     two of them have height, so the gap between the visible pair is paid
     twice. Two gaps of 2px is the 3px the pair had before, near enough. */
  .es-hero__console-listrow--tagged::after {
    content: "";
    flex: 0 0 100%;
    height: 0;
    order: 0;
  }

  .es-hero__console-listrow--tagged > code {
    flex: 0 1 auto;
    order: 1;
    max-width: 100%;
  }
}

@media (max-width: 479.98px) {
  .es-hero__console-wordmark { display: none; }
}

/* The search field is the first thing out of the top bar. It is the one
   element up there that says nothing: the brand names the product and the
   chip names the store, and a field with no room to be a field is just a
   pill with a magnifier on it. */
@media (max-width: 991.98px) {
  .es-hero__console-search { display: none; }
}

/* ══ Console: BAND form ═══════════════════════════════════
   The centered layout's shape. Full container width under the centred
   copy, in the media panel's slot and with its top gap, so a page that
   swaps one for the other keeps its rhythm.

   Wider than it is tall, because it is a picture of a desktop dashboard.
   The ratio is close to the media panel's 1710/702 rather than the tall
   panel ratio the split column uses. */
.es-hero--console-band .es-hero__console {
  max-width: none;
  margin-top: var(--gap-fluid-xl);
}

.es-hero--console-band .es-hero__console-frame { aspect-ratio: 1710 / 800; }

.es-hero--console-band .es-hero__console-body { grid-template-columns: 210px 1fr; }

.es-hero--console-band .es-hero__console-main { padding: var(--space-5); }

/* The container ladder, not the design, is what forces this step. Crossing
   992px downward drops the container from 960 to 720 in one jump, so at a
   fixed 1710/800 the frame loses height at that single pixel and the body
   inside it goes from roomy to cramped with nothing else having changed. */
/* The same problem one step earlier, and for the same reason: 1200px
   drops the container from 1140 to 960, and at a fixed 1710/800 the frame
   loses 90px of height at that single pixel. The pages inside it are now
   dense enough that those 90px are the difference between a card with room
   under its last row and a card clipping it. */
@media (max-width: 1199.98px) {
  .es-hero--console-band .es-hero__console-frame { aspect-ratio: 16 / 9; }
}

/* ── Below the split, the rail narrows ───────────────────
   992px is where a two-column page stops fitting two columns, so it is also
   where the tallest page stops being the width of one card and starts being
   the height of two. The frame stopped holding a ratio here for that
   reason, and it now stops holding one a step earlier still: see the 1400px
   block further down, which is where the ratio, the clip and the flexing
   children all come off in one place for both console forms. All this
   block still owns is the narrower rail. */
@media (max-width: 991.98px) {
  .es-hero--console-band .es-hero__console-body { grid-template-columns: 168px 1fr; }
}

/* ── Nothing is dropped at any width ─────────────────────
   Four rules used to live here, and between them they took the tail of
   every list, the second card off every split page and two thirds of the
   Home feed away from anyone on a narrow screen. Each was defensible on its
   own: the frame was pinned to a ratio, the ratio ran out of height first
   on the smallest screens, and dropping the tail of a list is what a real
   table does when it is short of room.

   They are gone on the owner's instruction. Every row, every card and every
   run in the panel now prints at every width, which means the frame can no
   longer be pinned to a ratio anywhere the restored content does not fit
   inside one. See `aspect-ratio: auto` below 1400px further down, and the
   single-cell stack that keeps the frame from resizing when the page
   changes.

   The rules and the widths they used to fire at, so nobody reinstates them
   by halves:
     ·  992-1399   the two-line `--sub --extra` tails on Versions/Settings
     ·  below 992  the second block of a `--split` page,
                   every `--extra` row,
                   every list row past the fourth
     ·  below 768  every `console-run` past the first, on Home */

/* `layout => 'split'` now means ONE thing: these blocks sit side by side on
   a wide screen and stack on a narrow one. It used to mean a second thing
   as well, that the block in the narrow column was a wide-screen affordance
   a narrow screen would never see, and a caller had to know both. Nothing
   goes missing below 992px any more, so nothing is off limits in the second
   block either.

   ── The frame that used to make this necessary ─────────
   Measured stacked, Versions and Settings both drew about 785px on a 696px
   frame, and switching to Logs then resized the window by 350px. The first
   half of that is now the point (the frame is sized by its tallest page);
   the second half is fixed by the single-cell stack below, which stops the
   frame resizing when the page changes at all. */

/* ── One cell, every page, so the box never resizes ──────
   The five pages used to be laid out one at a time: the shown page was the
   only box in the flow, so the frame was as tall as whichever page was
   open, and every tab was a step change in the height of an element above
   the fold. Measured at 375px it ran 348px on Monitoring against 603px on
   Home, so a tap moved everything under the hero by a quarter of a screen.

   They all sit on the same grid cell now. A grid row is as tall as the
   tallest thing in it whatever is visible, so the frame is the height of
   the tallest page from first paint and stays there: switching pages
   changes what is drawn in the box and never the box.

   ── Why `visibility` and not `display` ─────────────────
   `display: none` is what took the closed pages out of the flow, which is
   exactly the behaviour being removed, so the `hidden` attribute the script
   writes has to hide them a different way. `visibility: hidden` keeps the
   box and its height, and is equally absent from the accessibility tree and
   from the tab order, which is the half of `hidden` that has to survive.

   Scoped to the widths where the frame is content-sized. At 1400px and up
   the ratio fixes the height on its own and there is nothing to stabilise. */
@media (max-width: 1399.98px) {
  .es-hero__console-main {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
  }

  .es-hero__console-panel {
    grid-area: 1 / 1;
  }

  /* !important because Bootstrap's Reboot ships
     `[hidden] { display: none !important }` and it still loads on every
     template that has legacy markup left in it. Without it the closed pages
     collapse to nothing, the grid row is the height of the open page alone,
     and the stack buys nothing. Drop the !important once Bootstrap is
     dequeued site-wide, not before: the same trap took the panels out of
     section-service-tabs.php. */
  .es-hero__console-panel[hidden] {
    display: flex !important;
    grid-area: 1 / 1;
    visibility: hidden;
    pointer-events: none;
  }
}

/* ── A closed split page is still a split page ───────────
   The `!important` above is a blunt instrument: it beats the rule that
   makes a `--split` page two columns as well, so a closed Versions or
   Settings laid its two cards out one under the other while the open one
   put them side by side. It was still setting the height of the stack, so
   the frame was sized for a page nobody would ever see: 990px at 1280
   against the 569px the same pages actually draw, and it moved by 45px
   whenever a split page was the one open.

   So the split rule needs the same weight. This says nothing new about the
   layout; it only re-states it loudly enough to be heard over the line
   above. Both go together the day Bootstrap is dequeued. */
@media (min-width: 992px) and (max-width: 1399.98px) {
  .es-hero--console-band .es-hero__console-panel--split[hidden] {
    display: grid !important;
  }
}

/* ── The ratio comes off wherever the content no longer fits it ──
   With every row restored, the tail of a two-line list is past the bottom
   of its card at 992-1399 (the fifth row on Versions by 39px, on Settings
   by 25px, measured before those rows were dropped), and below 992 the
   pages are stacks rather than columns and are taller again. A ratio and a
   full page cannot both be honoured, and the page is what the owner asked
   for, so the frame is sized by its contents from 1399.98px down.

   No CLS cost, for the reason the note further down gives in full: nothing
   in this panel arrives after the first layout pass, so a box measured from
   its contents is measured just as early as a box measured from a ratio.
   And it is now measured from the TALLEST page rather than the open one, so
   it does not move when a tab is tapped either.

   The clip goes with it. `overflow: hidden` on the rows existed to hide
   what a fixed-height card could not show; with the height following the
   content there is nothing to hide, and leaving it on would keep cutting
   the last row whenever a page grew by a line. */
@media (max-width: 1399.98px) {
  .es-hero__console-frame,
  .es-hero--console-band .es-hero__console-frame { aspect-ratio: auto; }

  .es-hero__console-listrows { overflow: visible; }

  /* Grow, never shrink. It was `flex: none`, which was the right answer
     while the frame was the height of the open page and there was never
     any slack: the shrink is what let a card fall below its own content
     and let the card's `overflow: hidden` finish the job silently, and
     that is how the Monitoring topic list once collapsed to zero.

     The frame is now the height of the TALLEST page, so every page except
     that one has slack, and `none` left it as a black hole under the last
     card. `1 0 auto` fills it and still cannot shrink, so the collapse the
     old value was guarding against is still impossible. */
  .es-hero__console-panel,
  .es-hero__console-cards,
  .es-hero__console-card,
  .es-hero__console-activity,
  .es-hero__console-plotcard,
  .es-hero__console-list {
    flex: 1 0 auto;
    min-height: 0;
  }
}

/* ── Phones and small tablets ────────────────────────────
   The ratio used to come off here, because below 640px the frame ran out of
   height first and cut content without saying so: Home lost the last line
   of its activity feed, Logs lost a row, and on Monitoring the plot took
   its fixed height first and squeezed the topic list to ZERO, so both of
   its rows vanished. That is now handled for every width under 1400px in
   the one block above, along with the clip and the flexing children, so
   what is left here is the phone's own layout. */
@media (max-width: 639.98px) {
  .es-hero--console-band .es-hero__console-body { grid-template-columns: 1fr; }
  .es-hero--console-band .es-hero__console-main { padding: var(--space-3); }

  .es-hero__console-body { grid-template-rows: auto auto; }

  /* The plot keeps a height, but a smaller one: it is the only element in
     here that cannot be sized by its content, because its bars are
     percentages of it. */
  .es-hero__console-plot { height: 84px; }
}

/* ── Card deck ───────────────────────────────────────────
   A stack of cards on one another, the front one at rest and the rest
   stepped back and to the right so the pile reads as a pile. Which card
   is in front is driven by `.is-active`, so advancing is a class swap on
   two elements and nothing in the stack is ever inserted, removed or
   re-ordered. The box therefore cannot reflow while it runs, which is
   what keeps this off the CLS bill in a column that sits above the fold.

   Height comes from the aspect ratio, not from the cards: they are all
   absolutely positioned, so without this the container would collapse. */
/* ── The carousel ────────────────────────────────────────────
   One card in the middle at full size and in colour, the card either side of
   it stepped out, shrunk, greyed and clipped by the deck's own edge. The
   treatment is section-team.php's row, which is where the design for this
   came from: same idea of a centre that is current and neighbours that are
   visibly not.

   It is NOT that component. That one is 309 lines of cloned slides, drag and
   an infinite loop, bound to its own class names; this is three states on a
   stack the deck script already drives. Porting it would have meant editing
   a component the team section runs on live to gain a swipe this deck has
   never had. */
.es-hero__deck {
  position: relative;
  width: 100%;

  /* --es-hero-deck-w is the FRONT CARD's width and --es-hero-deck-peek is how
     much of each neighbour shows. The box is the card plus, on each side, the
     gap between the cards and that peek, so its bounds contain everything
     that should be seen and clip everything that should not. */
  max-width: calc(
    var(--es-hero-deck-w, 396px)
    + (var(--es-hero-deck-gap) + var(--es-hero-deck-peek, 56px)) * 2
  );
  margin: var(--gap-fluid-xl) auto 0;

  /* The neighbours are wider than the slice of them that belongs on screen,
     so the deck cuts them off. This is also what keeps a peek from turning
     into sideways scroll on a narrow column. */
  overflow: hidden;
}

/* The card's own width, capped so a column narrower than the card does not
   get an overhang. Read by the spacer and by every card, so the two cannot
   disagree about how wide a card is. */
.es-hero__deck { --es-hero-deck-cw: min(var(--es-hero-deck-w, 396px), 100%); }

/* HOW FAR A NEIGHBOUR STEPS OUT, and it is derived rather than picked.

   A neighbour has to clear the front card completely: half the front card,
   then a gap, then half of its own scaled-down self. Land it short of that
   and it sits BEHIND the front card, which means the slice on screen is a
   vertical band cut out of its middle, with its rounded corners either
   clipped off at the deck edge or hidden behind the card. It reads as a
   rectangle with square corners, because that is what it is.

   Percentages resolve against the card's own width, and `translateX(x)
   scale(s)` applies the scale to the point first, so the step is NOT scaled
   with it. That is why half the neighbour is `50% * scale` here. */
.es-hero__deck {
  --es-hero-deck-scale: 0.86;
  --es-hero-deck-gap: var(--space-3);
  --es-hero-deck-step: calc(
    50%
    + 50% * var(--es-hero-deck-scale)
    + var(--es-hero-deck-gap)
  );
}

/* Height source. Every card is absolutely positioned, so without something
   in flow the deck would collapse to nothing.

   A spacer rather than `aspect-ratio` on the deck itself: the deck is wider
   than the card by both peeks, so an aspect ratio set on the deck would be
   the CAROUSEL's proportion, and the card, which is the thing anyone
   actually looks at, would land at whatever fell out of it. This is sized to
   the card's width and given the card's ratio, so 5 / 7 means the card is
   5 / 7. */
.es-hero__deck::before {
  content: "";
  display: block;
  width: var(--es-hero-deck-cw);
  margin-inline: auto;
  aspect-ratio: var(--es-hero-deck-ratio, 5 / 7);
}

/* Centred by `left` and a negative margin rather than by a transform, so the
   transform is free to carry the state and nothing has to re-derive the
   centring every time the carousel steps. */
.es-hero__deck-card {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: var(--es-hero-deck-cw);
  margin: 0 0 0 calc(var(--es-hero-deck-cw) / -2);
  overflow: hidden;
  border-radius: var(--radius-xl);

  /* The ground under a photograph that has not decoded yet, and under a
     transparent one. gray-900 rather than a surface token on purpose: the
     caption is white and has to stay legible over whatever is behind it. */
  background: var(--gray-900);
  box-shadow: var(--shadow-lg);

  /* Resting state: parked in the centre, hidden. A card only becomes visible
     by being given one of the three roles below. */
  transform: translateX(0) scale(var(--es-hero-deck-scale, 0.86));
  opacity: 0;
  z-index: 0;

  /* `transform` and `opacity` only: both are compositor properties, so the
     step animates without laying anything out again. */
  transition: transform var(--es-hero-deck-fade, 450ms) var(--ease-out),
              opacity   var(--es-hero-deck-fade, 450ms) var(--ease-out);
}

/* Only three of the thirteen are ever drawn. The rest sit at opacity 0 in the
   centre, so stepping the carousel never animates a card in from somewhere
   it could not have come from. */
.es-hero__deck-card:not(.is-active):not(.is-prev):not(.is-next) {
  opacity: 0;
  pointer-events: none;
}

/* The two neighbours. Clear of the front card, so their inner edge and both
   of its rounded corners are on screen, and only their OUTER half is cut off
   by the deck. Scale alone marks them as not-current: they keep their own
   colour, so the photograph beside the one being read is still a photograph
   of that industry rather than a grey slab. */
.es-hero__deck-card.is-prev,
.es-hero__deck-card.is-next {
  opacity: 1;
  z-index: 1;
}

.es-hero__deck-card.is-prev {
  transform: translateX(calc(var(--es-hero-deck-step) * -1))
             scale(var(--es-hero-deck-scale, 0.86));
}

.es-hero__deck-card.is-next {
  transform: translateX(var(--es-hero-deck-step))
             scale(var(--es-hero-deck-scale, 0.86));
}

.es-hero__deck-card.is-active {
  transform: translateX(0) scale(1);
  opacity: 1;
  z-index: 2;
}

.es-hero__deck-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* A card whose industry has no artwork yet. A brand-tinted panel carrying
   the same caption in the same place, the same treatment an image-less tile
   gets in section-industries.php, so a missing upload reads as a designed
   placeholder rather than a hole in the stack. */
.es-hero__deck-card--blank {
  background: linear-gradient(155deg, var(--brand-700) 0%, var(--gray-900) 78%);
}

/* Caption. The scrim is part of the caption rather than a separate layer, so
   there is one fewer box in a stack that already has thirteen. 0.85 at the
   bottom edge over gray-900 clears AA for white against a fully white
   photograph, which is the worst case a stock image can present. */
.es-hero__deck-cap {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-10) var(--space-5) var(--space-5);
  background: linear-gradient(180deg,
              rgba(22, 20, 16, 0) 0%,
              rgba(22, 20, 16, 0.55) 45%,
              rgba(22, 20, 16, 0.85) 100%);
}

/* --fs-card-title and --fs-small, NOT --fs-lg and --fs-sm. Those two do not
   exist: the token file defines --fs-body/-body-sm/-small/-caption/-eyebrow
   and no -lg or -sm. Both were the only use of either name in this file, so
   both silently fell back to the inherited 16px and the caption's two lines
   came out the same size as each other, with only the weight separating
   them. It went unseen because the note line renders only when a card
   carries one, and until the industries deck got its written defaults on
   31 Aug 2026 no card ever did.

   Sized against the narrowest box the caption ever gets: 224px, on the
   stacked layout at 390px. Every one of the industries deck's thirteen
   titles and notes holds one line there at these two values. */
.es-hero__deck-title {
  color: var(--text-inverse, #fff);
  font-size: var(--fs-card-title);
  font-weight: 600;
  line-height: 1.25;
}

.es-hero__deck-note {
  color: rgba(255, 255, 255, 0.78);
  font-size: var(--fs-small);
  line-height: 1.35;
}

/* ── Reduced motion ──────────────────────────────────────
   The script stops advancing the deck under this preference, so the stack
   holds on its first card. The transition is dropped here as well, because
   the class swap the script performs on hover-out would otherwise still
   animate. */
@media (prefers-reduced-motion: reduce) {
  .es-hero__deck-card { transition: none; }
}

@media (max-width: 991px) {
  /* Centred under the copy when the columns stack, and smaller: a full-width
     5/7 card on a phone is taller than the viewport and would push the CTA
     and everything under it off screen. */
  .es-hero__deck {
    --es-hero-deck-cw: min(var(--es-hero-deck-w-sm, 300px), 100%);
    /* A narrower peek on a narrower card, so the neighbours stay the same
       share of it rather than growing as the card shrinks. */
    --es-hero-deck-peek: 40px;
    max-width: calc(var(--es-hero-deck-w-sm, 300px) + (var(--es-hero-deck-gap) + 40px) * 2);
  }
}

@media (max-width: 575px) {
  /* A narrower peek: 40px a side off a 320px screen is width the card itself
     needs more than the carousel needs to show what is coming. */
  .es-hero__deck {
    --es-hero-deck-cw: min(260px, 100%);
    --es-hero-deck-peek: 22px;
    --es-hero-deck-gap: var(--space-2);
    max-width: calc(260px + (var(--space-2) + 22px) * 2);
  }

  .es-hero__deck-cap { padding: var(--space-9) var(--space-4) var(--space-4); }
}

/* ── The stack variant ───────────────────────────────────────
   `'variant' => 'stack'`. Same script, same timer, same three roles: what
   changes is that only ONE neighbour is drawn, on the trailing edge, so the
   pair reads as a deck of cards rather than as a row stepping along.

   ONE SIDE, NOT TWO. The carousel has to show where the last card went as
   well as where the next one is coming from, so its box is the card plus a
   peek on EACH side. A stack has no leading slot: that left peek was a
   slice of a photograph with nothing to explain it, and on a phone it was
   the thing that made the composition read as broken. So the box is the
   card plus ONE peek and the card is anchored to the leading edge of it.

   `--es-hero-deck-step` still lands the neighbour exactly one card width
   plus one gap along, because that distance is measured from the front
   card's own box and the front card has not changed size.

   ── AFTER THE BREAKPOINTS, AND IT HAS TO BE ───────────────
   The 991px and 575px blocks above set `max-width` on `.es-hero__deck`
   directly, at the same specificity this selector has. Written before them
   the one-sided box was simply overridden at both widths and the phone got
   the two-sided carousel box back: measured 320px where 290px was wanted,
   which showed 52px of the second card instead of 22px. The calc below is
   written in the same custom properties those blocks set, so it picks up
   each breakpoint's card width, gap and peek on its own. */
.es-hero__deck--stack {
  max-width: calc(
    var(--es-hero-deck-cw)
    + var(--es-hero-deck-gap)
    + var(--es-hero-deck-peek, 56px)
  );
}

/* Anchored to the leading edge of that box rather than centred in it, which
   is what leaves the whole peek on one side. The margin the carousel uses to
   centre itself in the column is inherited unchanged, so the pair still sits
   where the carousel sat. */
.es-hero__deck--stack .es-hero__deck-card {
  left: 0;
  margin-left: 0;
}

/* THE OUTGOING CARD SINKS, IT DOES NOT SLIDE OFF.
   In the carousel `is-prev` is a visible slot: the card that has just been
   replaced steps out to the left and stays on screen. There is no left slot
   here, so left as it was, every step threw the card that had just been read
   sideways out of a box that gets clipped, which is a lot of motion for
   something nobody is meant to look at.

   Given the resting transform instead, it shrinks back into the pile under
   the card replacing it and fades while it does. Behind, because z-index
   drops to 0 and the incoming card lands at z-index 2 directly over it, so
   what is actually seen is the top card of the deck being taken off.

   Specificity: three classes here against the carousel rule's two, so this
   wins wherever both match and the carousel is untouched by it. */
.es-hero__deck--stack .es-hero__deck-card.is-prev {
  transform: translateX(0) scale(var(--es-hero-deck-scale, 0.86));
  opacity: 0;
  z-index: 0;
}

/* ── Media ──────────────────────────────────────────────── */
.es-hero__media {
  margin: var(--gap-fluid-xl) 0 0;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-xl);
  background: var(--bg-sunken);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}

.es-hero__media img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--es-hero-media-ratio, 1710 / 702);
  object-fit: cover;
}

/* ── Unframed panel, opt-in via `media: array( 'frame' => false )` ──
   For a cut-out render rather than a screenshot. The panel exists to give a
   rectangular image an edge; artwork on a transparent ground already has
   one, and the frame then draws a box around the empty pixels around it.

   `contain`, not the panel's `cover`: with no frame to fill there is
   nothing to crop to, and a render is the one kind of hero image where
   losing an edge is losing part of the subject. */
/* The dark hero repaints the panel's border and ground further up this
   sheet at a higher specificity, so it has to be answered here or a plain
   panel comes back as a framed one on a dark surface. */
.es-hero__media--plain,
.es-hero--bg-dark .es-hero__media--plain {
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  overflow: visible;
}

.es-hero__media--plain img { object-fit: contain; }

.es-hero__caption {
  margin: 0;
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--border-subtle);
  background: var(--bg-surface);
  color: var(--text-muted);
  font-family: var(--font-label);
  font-size: var(--fs-caption);
  letter-spacing: 0.5px;
  text-align: center;
}

/* ── Video panel ────────────────────────────────────────────
   Same box as the still image, so a template can swap one for
   the other without touching layout. Muted, looping and inert:
   it is decoration behind the headline, never a thing to
   operate, so it ships without controls and out of the a11y
   tree. The poster is what paints first and is therefore the
   element LCP measures. */
.es-hero__video {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: var(--es-hero-media-ratio, 16 / 9);
  object-fit: cover;
  background: var(--bg-sunken);
}

/* ── Bleeding media panel ───────────────────────────────────
   Desktop only. The panel is lifted out of the grid and pinned
   to the section's right edge so it runs to the viewport rather
   than stopping at the container. Absolute rather than a
   negative margin because .es-container steps through a fixed
   max-width ladder: a margin sized for one step is wrong at the
   next, while `right: 0` is correct at every width. */
@media (min-width: 992px) {
  /* One number drives both halves. The panel is sized in vw because it is
     measured from the viewport edge, while the copy lives inside the
     container, so the copy's limit has to subtract the container's own
     offset: (100vw - 100%) / 2, where 100% is the container's inner width.
     Without that the two are measured against different boxes and the copy
     runs under the panel at every width. */
  .es-hero--bleed {
    position: relative;
    --es-hero-media-w: 52vw;
  }

  /* Both classes, deliberately: the two-column rule for .es-hero--split sits
     later in this sheet at the same specificity and would otherwise win,
     leaving the copy in a half-width column beside an empty one. */
  .es-hero--split.es-hero--bleed .es-hero__grid { grid-template-columns: 1fr; }

  .es-hero--bleed .es-hero__media {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 0;
    width: var(--es-hero-media-w);
    margin: 0;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: none;
  }

  .es-hero--bleed .es-hero__video,
  .es-hero--bleed .es-hero__media img {
    height: 100%;
    aspect-ratio: auto;
  }

  /* The panel is pinned to the section's edges, so the shared reveal's 28px
     rise would drag it off its own bottom edge and flash the section colour
     through. It fades in place instead; the copy beside it still rises. */
  .es-hero--bleed .es-hero__media.reveal-up { transform: none; }

  /* Copy sits above the panel and stops short of it. */
  .es-hero--bleed .es-hero__copy {
    position: relative;
    z-index: 1;
    /* 720 rather than the 900 the centered hero allows: --fs-hero is 64px at
       this width and fits roughly two words per line, so a wider box buys
       another word, not another line. The subtitle keeps its own 62ch limit
       so the reading measure does not stretch with it. */
    max-width: min(720px, calc(100vw - var(--es-hero-media-w) - var(--gap-fluid-lg) - (100vw - 100%) / 2));
  }

  /* Feathered edge, not a wash. The panel's left edge starts at the flat
     section colour and clears to fully transparent inside the first third,
     so the text block's black background reads as one continuous surface
     that dissolves into the footage, and the remaining two thirds of the
     clip play at full brightness with nothing over them.

     The copy never reaches this gradient: its max-width below stops it
     short of the panel entirely, so the blend is doing pure visual
     blending, not carrying any text contrast. */
  .es-hero--bleed .es-hero__media::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
      linear-gradient(90deg,
        var(--gray-900) 0%,
        rgba(22, 20, 16, 0.86) 7%,
        rgba(22, 20, 16, 0.42) 17%,
        rgba(22, 20, 16, 0.12) 26%,
        transparent 34%),
      /* Just enough at the foot to seat the stats card against the clip. */
      linear-gradient(180deg, transparent 68%, rgba(22, 20, 16, 0.42) 100%);
  }
}

/* ── Feature row ────────────────────────────────────────────
   Short capability labels under the subtitle. Hairline dividers
   rather than boxes, so the row reads as one line of proof and
   not as three competing cards. */
.es-hero__features {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4) 0;
  margin: var(--gap-fluid-md) 0 0;
  padding: 0;
  list-style: none;
}

.es-hero__feature {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-right: var(--space-4);
  padding-left: var(--space-4);
  border-left: 1px solid var(--border-subtle);
}

.es-hero__feature:first-child {
  padding-left: 0;
  border-left: 0;
}

/* Centred hero: the row is a caption under the composition, so it centres
   with everything else and the labels sit on one line each rather than
   being forced to two by the 11ch cap the overlay layout needs. */
.es-hero--centered .es-hero__features { justify-content: center; }

/* ── Pill capability row, opt-in via `features_style => 'pill'` ──
   The same three claims as chips instead of hairline-divided cells.

   It exists for a hero written as a SaaS product landing page: no
   paragraph, headline straight into proof, and the proof reading as
   discrete product facts rather than as one continuous caption. The
   hairline row is the house treatment on five service heroes, so a page
   that has to look unlike them cannot use it and stay unlike them.

   Deliberately NOT interactive. These are statements, not filters, and a
   chip that lifts on hover promises a destination it does not have. */
.es-hero--features-pill .es-hero__features {
  gap: var(--space-3);
  margin-top: var(--gap-fluid-md);
}

/* `border` in full, so the hairline the row treatment puts on the left
   edge is replaced rather than added to. The first-child rule below has to
   be restated for the same reason: the row zeroes its left edge, and here
   every chip needs all four. */
.es-hero--features-pill .es-hero__feature {
  gap: var(--space-2);
  padding: 8px var(--space-4) 8px 8px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  box-shadow: var(--shadow-xs);
}

.es-hero--features-pill .es-hero__feature:first-child {
  padding-left: 8px;
  border: 1px solid var(--border-subtle);
}

/* The mark shrinks to a disc that fits inside a pill. At the row's 34px
   square the chip would be 50px tall, which is taller than the buttons
   under it and reads as a control rather than as a label. */
.es-hero--features-pill .es-hero__feature-icon {
  width: 28px;
  height: 28px;
  border-radius: var(--radius-full);
}

.es-hero--features-pill .es-hero__feature-icon--img img {
  width: 18px;
  height: 18px;
}

/* The row treatment caps the label to force two-word labels onto two
   lines, which is right for a divided cell and wrong for a chip: a pill
   wrapping to two lines stops reading as one token. */
.es-hero--features-pill .es-hero__feature-label {
  max-width: none;
  font-size: var(--fs-small);
  line-height: 1.3;
  white-space: nowrap;
}

.es-hero--bg-dark.es-hero--features-pill .es-hero__feature {
  border-color: rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.06);
  box-shadow: none;
}

/* Below the point where three chips fit on one line they wrap, and a
   nowrap label on a narrow phone would push the widest chip past the
   gutter. The cap comes back off and the longest chip takes two lines,
   which is the lesser of the two problems. */
@media (max-width: 575px) {
  .es-hero--features-pill .es-hero__feature-label { white-space: normal; }
  .es-hero--features-pill .es-hero__feature { padding-right: var(--space-3); }
}

/* ── Stacked capability row, opt-in via `features_layout => 'stack'` ──
   Two classes deep, so it outranks the small-screen grid further down this
   sheet wherever it is declared, rather than depending on source order. */
.es-hero--features-stack .es-hero__features {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
}

/* The hairline divided items sitting side by side. Stacked, it would run
   down the left of a list that is already flush left, so it goes and the
   gap does the separating. */
.es-hero--features-stack .es-hero__feature {
  padding-left: 0;
  padding-right: 0;
  border-left: 0;
}

/* The label is capped at 11ch further down this sheet, which is what keeps
   three items sitting side by side to a similar width. Stacked, that cap is
   the thing doing the damage: it holds a five-word label to about 97px and
   wraps it to three lines while the whole column width sits unused beside
   it. One item per line has the room, so the cap comes off. */
.es-hero--features-stack .es-hero__feature-label { max-width: none; }
.es-hero--centered .es-hero__feature-label { max-width: none; }

.es-hero__feature-icon {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--brand-200);
  border-radius: var(--radius-md);
  background: var(--brand-50);
  color: var(--brand-500);
}

/* A third-party brand logo is artwork, not a line mark, so its tile drops
   the brand tint and hairline that frame an en_icon() glyph and simply
   holds the image. `contain` because these arrive at whatever aspect the
   vendor publishes and must not be cropped to a square. */
.es-hero__feature-icon--img {
  border-color: transparent;
  background: none;
}

.es-hero__feature-icon--img img {
  width: 24px;
  height: 24px;
  object-fit: contain;
}

.es-hero__feature-label {
  /* Forces the two-word labels onto two lines. At 13ch they stayed on one
     line, the row measured 3px wider than the copy column on a 1280 laptop,
     and the third item dropped to a second row. */
  /* 17 Sep 2026: widened from 11ch. At 11ch a three-word label such as
     "Domain-specific builds" or "Built to your compliance rules" broke to
     three lines. Measured in the browser: 14ch sets all three Industries
     labels on exactly two lines (15ch and up lets "Full-stack delivery"
     collapse to one), and `balance` keeps the two lines close in length. */
  max-width: 14ch;
  text-wrap: balance;
  color: var(--text-body);
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  line-height: 1.35;
}

.es-hero--bg-dark .es-hero__feature { border-left-color: rgba(255, 255, 255, 0.14); }
.es-hero--bg-dark .es-hero__feature-label { color: var(--gray-300); }

.es-hero--bg-dark .es-hero__feature-icon {
  border-color: rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.06);
  color: var(--brand-400);
}

/* ── Play CTA ───────────────────────────────────────────────
   Disc sits inside the existing secondary pill rather than
   replacing it, so the button keeps the site's shared height,
   radius and hover lift. */
.es-btn__play {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  margin-right: var(--space-1);
  margin-left: calc(var(--space-3) * -1);
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--brand-500);
  color: var(--text-inverse);
  transition: transform var(--duration-normal) var(--ease-out);
}

.es-btn:hover .es-btn__play { transform: scale(1.08); }

@media (prefers-reduced-motion: reduce) {
  .es-btn:hover .es-btn__play { transform: none; }
}

/* ── Proof strip ────────────────────────────────────────── */
/* Wrapper for the house stats card (section-stats.php, `embed`), which every
   hero proof strip renders through since 17 Sep 2026. It only spaces and
   stacks the card; the card draws itself. */
.es-hero__proof-card {
  position: relative;
  z-index: 1;
  margin-top: var(--gap-fluid-xl);
  text-align: left;
}

.es-hero__proof {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-5);
  margin-top: var(--gap-fluid-xl);
  padding-top: var(--gap-fluid-lg);
  border-top: 1px solid var(--border-subtle);
  list-style: none;
}

/* Card variant: the strip lifts off the section as a raised panel. Used
   where the hero runs to a hard edge and a hairline rule would be lost
   against the artwork behind it. Surface tokens, so it inverts with the
   rest of the page in dark mode instead of staying a white slab. */
.es-hero__proof--card {
  position: relative;
  z-index: 1;
  margin-top: var(--gap-fluid-xl);
  padding: var(--gap-fluid-md) var(--gap-fluid-lg);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-xl);
  background: var(--bg-surface);
  box-shadow: var(--shadow-lg);
}

.es-hero__proof--card .es-hero__stat {
  padding-left: var(--space-6);
  border-left: 1px solid var(--border-subtle);
  text-align: left;
}

.es-hero__proof--card .es-hero__stat:first-child {
  padding-left: 0;
  border-left: 0;
}

/* The card is a light surface even inside the dark hero, so the dark-hero
   overrides further up must not follow the stats onto it. */
.es-hero--bg-dark .es-hero__proof--card { border-color: var(--border-subtle); }
.es-hero--bg-dark .es-hero__proof--card .es-hero__stat-value { color: var(--text-heading); }
.es-hero--bg-dark .es-hero__proof--card .es-hero__stat-label { color: var(--text-muted); }

.es-hero__stat { text-align: center; }

/* Same --fs-stat as the standalone stats band. The two were byte-identical
   raw clamps in separate files, so a stat in the hero and a stat in the
   proof band could drift apart on any edit to one of them. */
.es-hero__stat-value {
  display: block;
  color: var(--text-heading);
  font-size: var(--fs-stat);
  font-weight: var(--font-extrabold);
  line-height: 1;
  letter-spacing: var(--tracking-normal);
  font-variant-numeric: tabular-nums;
}

/* Affixes take the heading ink, not the brand orange, so a figure reads as
   one number rather than as a black part and an orange part. Owner's
   instruction, 9 Sep 2026, applied site-wide; section-stats.php already
   made the same trade. The prefix and suffix are still their own elements,
   since the counter needs the number on its own to animate; only the
   colour is shared now. */
.es-hero__stat-value b,
.es-hero__stat-affix {
  color: inherit;
  font-weight: inherit;
}

/* Counting number. Width is reserved from the target's digit count so the
   row does not reflow as 0 climbs to 550. tabular-nums keeps every digit
   the same advance, so the reservation stays accurate mid-count. Any
   residual variance is absorbed inside the fixed grid cell, so it can
   never move the surrounding layout.

   Right aligned rather than centred, for the reason set out at the same
   rule in section-stats.php: `ch` measures the proportional zero, which is
   wider than this font's tabular digit, so the reservation always overshoots
   and centring put half that surplus between the figure and its suffix. Kept
   in step with that partial, since the two draw the same counter. */
.es-hero__stat-value .rv-counter {
  display: inline-block;
  min-width: calc(var(--es-digits, 1) * 1ch);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.es-hero__stat-label {
  display: block;
  margin-top: var(--space-3);
  color: var(--text-muted);
  font-family: var(--font-label);
  font-size: var(--fs-caption);
  letter-spacing: 1.1px;
  text-transform: uppercase;
}

/* ── Split layout ───────────────────────────────────────── */
@media (min-width: 992px) {
  /* The ratio comes through a custom property set on the section, so a hero
     can change its own proportions without this shared sheet growing a
     per-page selector. Unset falls back to the original near-even split. */
  .es-hero--split .es-hero__grid {
    display: grid;
    grid-template-columns: var(--es-hero-columns, 1.05fr 1fr);
    gap: var(--gap-fluid-xl);
    align-items: center;
  }

  .es-hero--split .es-hero__media   { margin-top: 0; }
  .es-hero--split .es-hero__deck    { margin-top: 0; }
  .es-hero--split .es-hero__console { margin-top: 0; }

  /* The console is a picture of a wide interface, so it wants the wider
     half of the split rather than the narrower one the default ratio gives
     the second column. A caller can still override it: `columns` sets
     --es-hero-columns on the section, which is a more specific place than
     this rule and therefore wins. */
  .es-hero--has-console .es-hero__grid {
    grid-template-columns: var(--es-hero-columns, 1fr 1.12fr);
  }

  /* The pill only exists where there is room beside the frame for it to
     hang into. Below this width the column is the full container and it
     would overhang the gutter. */
  .es-hero__console-pill { display: inline-flex; }

  /* ── Full-width action row on the split layout ────────────
     Copy and artwork share the top row; the buttons and the proof run
     underneath both, edge to edge of the container. Named areas rather
     than auto-placement because the row sits between the copy and the
     artwork in the markup, which is the order it has to read in when the
     columns stack, and auto-placement would put it in the second cell of
     the first row.

     Its own row gap rather than the column gap: --gap-fluid-xl is set for
     the distance between two columns and is far more air than a headline
     block wants above its own button. */
  .es-hero--split.es-hero--actions-row .es-hero__grid {
    grid-template-areas:
      "copy    media"
      "actions actions";
    row-gap: var(--gap-fluid-lg);
  }

  .es-hero--split.es-hero--actions-row .es-hero__copy    { grid-area: copy; }
  .es-hero--split.es-hero--actions-row .es-hero__media   { grid-area: media; }
  .es-hero--split.es-hero--actions-row .es-hero__deck    { grid-area: media; }
  .es-hero--split.es-hero--actions-row .es-hero__console { grid-area: media; }
  .es-hero--split.es-hero--actions-row .es-hero__actions { grid-area: actions; }

  /* The grid's row gap sets this distance now. Left on the element for the
     stacked arrangement, where there is no grid to set it. */
  .es-hero--split.es-hero--actions-row .es-hero__actions { margin-top: 0; }
}

/* ── Small screens ──────────────────────────────────────── */
@media (max-width: 991px) {
  /* Below the split breakpoint the panel is back in the flow, so it needs
     its frame and its own aspect ratio again. */
  .es-hero--bleed .es-hero__media {
    margin-top: var(--gap-fluid-lg);
    border-radius: var(--radius-xl);
    overflow: hidden;
  }

  .es-hero__feature {
    padding-right: var(--space-5);
    padding-left: var(--space-5);
  }
}

@media (max-width: 767px) {
  /* Two per row rather than a wrapping line, so the dividers still line up. */
  .es-hero__features { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }

  /* Centred hero keeps the flowing row. A two-column grid leaves an odd
     third item stranded in the left column under a centred composition,
     which reads as a mistake rather than a layout. */
  .es-hero--centered .es-hero__features {
    display: flex;
    gap: var(--space-4) var(--space-5);
  }

  .es-hero__feature {
    padding: 0;
    border-left: 0;
  }

  .es-hero__proof--card {
    padding: var(--gap-fluid-md) var(--space-5);
  }

  .es-hero__proof--card .es-hero__stat {
    padding-left: 0;
    border-left: 0;
  }
}

@media (max-width: 991px) {
  /* Below the split breakpoint the row is inside a single narrow column and
     has broken to two lines, so the vertical rule between the button and the
     proof is now pointing sideways at nothing. Measured: the button and the
     pill need about 790px on one line, and the container's 720px step leaves
     696. 991 is therefore the last width at which the rule separates two
     things rather than sitting under one of them.

     The pill keeps its own internal hairline, which is inside a box that has
     not wrapped and is still doing its job. */
  .es-hero__actions > .es-hero__trust-rule { display: none; }

  /* A cut-out render given the full column width on a stacked layout is not
     the same picture the split shows, it is a poster. At 900px wide the
     artwork stood 522px tall under the buttons and was the largest thing in
     the hero by some way. Held to something closer to the size it is drawn
     at on a desktop, and centred in the column it no longer fills.

     Only the plain variant: a framed screenshot genuinely does want the full
     column, since its content is small and its frame is the point. */
  .es-hero__media--plain {
    max-width: 460px;
    margin-inline: auto;
  }

  /* ── Stacked order for the full-width action row ──────────
     Artwork between the headline and the controls, per the mobile design,
     rather than last. Stacked, the row is no longer a band under two
     columns, so leaving it above the artwork would put the button in the
     middle of the composition with the picture stranded below it, which is
     a different page from the one the design draws.

     Done with `order` rather than by moving the figure in the markup. The
     artwork is not focusable and carries supporting alt text, so tab order
     is unaffected and the reading sequence (headline, controls, then the
     picture that illustrates them) still makes sense to a screen reader.
     Moving it in the markup instead would fix the visual order here and
     break the grid areas on the desktop layout above. */
  .es-hero--actions-row .es-hero__grid {
    display: flex;
    flex-direction: column;
  }

  .es-hero--actions-row .es-hero__copy    { order: 1; }
  .es-hero--actions-row .es-hero__media   { order: 2; }
  .es-hero--actions-row .es-hero__deck    { order: 2; }
  .es-hero--actions-row .es-hero__console { order: 2; }
  .es-hero--actions-row .es-hero__actions { order: 3; }

  /* Controls centred under a left-aligned headline, per the design. The
     copy keeps its own alignment: a two-line headline centred over a
     narrow column reads as a poster, and this is still a page. */
  .es-hero--actions-row .es-hero__actions { justify-content: center; }
}

@media (max-width: 575px) {
  .es-hero__ctas > .es-btn { width: 100%; }
  .es-hero__proof { grid-template-columns: 1fr 1fr; gap: var(--space-5) var(--space-4); }

  /* A full-width button cannot share a line with anything, so the row is
     made an explicit stack rather than left to wrap into one. Stretch, not
     the row's default centre alignment: centred, the proof would sit
     middle-aligned under a full-bleed button and read as unrelated to it. */
  .es-hero__actions {
    flex-direction: column;
    align-items: stretch;
    gap: var(--gap-fluid-md);
  }

  .es-hero__actions > .es-hero__ctas { width: 100%; }

  /* ── Phone treatment for the full-width action row ────────
     The design draws the button at its own width, centred, with the proof
     running the full measure underneath it. So the partial's full-bleed
     button is undone here and only here: the shared rule exists because a
     hero with two buttons wants them as equal full-width blocks, and this
     hero has one button under a picture.

     align-items: center rather than the stretch above, so the button hugs
     its label. The proof is put back to full width on its own, since it is
     a row of lockups and reads as a bar rather than as a control. */
  .es-hero--actions-row .es-hero__actions { align-items: center; }

  .es-hero--actions-row .es-hero__ctas > .es-btn { width: auto; }

  .es-hero--actions-row .es-hero__actions > .es-hero__ctas { width: auto; }

  .es-hero--actions-row .es-hero__actions > .es-hero__trust { width: 100%; }

  /* One line, not the two the pill wraps to by default at this width.
     The shared phone rule lets the card wrap and centre, which is right
     when the card is the last thing in a centered hero and has a full
     column to itself. Here it is a proof bar under a button and the
     design draws it as a single row, so the lockups are stepped down
     instead until they fit: measured at 375px, 22px marks and the tighter
     padding leave the pair on one line with room to spare. */
  .es-hero--actions-row .es-hero__trust.es-hero__trust--card {
    flex-wrap: nowrap;
    justify-content: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-full);
  }

  .es-hero--actions-row .es-hero__trust--card .es-hero__trust-logo { height: 22px; }
  .es-hero--actions-row .es-hero__trust--card .es-hero__rating-logo { height: 21px; }

  /* The internal hairline comes back: this pill has not wrapped, so it is
     still separating two lockups that sit side by side. */
  .es-hero--actions-row .es-hero__trust.es-hero__trust--card .es-hero__trust-rule {
    display: block;
  }

  .es-hero--actions-row .es-hero__trust--card .es-hero__rating.es-hero__rating {
    column-gap: var(--space-2);
  }
}

@media (prefers-reduced-motion: reduce) {

  /* The entrance is cancelled rather than shortened. `both` has already
     written opacity 0 into the pre-delay state, so the animation cannot
     simply be removed: the final state has to be restored explicitly or
     the whole hero stays invisible. */
  .es-hero--enter .es-hero__enter,
  .es-hero--enter .es-hero__tile,
  .es-hero--enter .es-hero__title .es-mark::after {
    animation: none;
    opacity: 1;
    transform: none;
  }

  /* The hover pose still applies, it just arrives instantly. Removing the
     transition rather than the hover rule keeps the tile responsive to the
     pointer without anything sliding across the screen, which is the
     distinction the preference is actually about. The resting tilt is a
     static transform and stays. */
  .es-hero__tile-inner { transition: none; }
}

/* ══ Left-ranged hero, opt-in via `align: 'left'` ═════════
   The centered layout keeps its single column and simply ranges its
   contents left against the container instead of centring them.

   This is what is LEFT of the old `panel` variant. That variant seated the
   whole composition in a raised --bg-surface card with a hairline border,
   --radius-2xl corners and --shadow-lg, and the left alignment came along
   with it because a box gives a column an edge to sit against. The card is
   gone on the owner's instruction; the alignment stayed, so it is its own
   argument now rather than a side effect of a surface.

   Everything removed with the card was surface: the card's own fill,
   border, radius, shadow and inner padding; the warm --bg-canvas ground
   written to give a white card something to lift off; the block that put
   the dark hero's white-on-dark ink back to dark inside the card; and the
   phone rule that softened the card's radius. None of it has anything left
   to paint now that heroes are a plain white ground and no card exists.

   What is here is alignment and nothing else, so a hero can range left
   without becoming a different component. */
/* The column ranges left against the container. `max-width: 100%` and
   `margin-inline: 0` undo the centered layout's measure and auto margins,
   so the copy starts at the container's left edge and runs to its right
   one, which is the same edge the header logo and every section heading
   below already sit on. That is what replaces the card as the thing the
   column lines up against. */
.es-hero--align-left .es-hero__copy {
  max-width: 100%;
  margin-inline: 0;
  text-align: left;
}

.es-hero--align-left .es-hero__trust,
.es-hero--align-left .es-hero__ctas,
.es-hero--align-left .es-hero__features,
.es-hero--align-left .es-hero__rating { justify-content: flex-start; }

/* ── One step tighter, kept ─────────────────────────────────
   These two were written as a card rule: a hero on open ground has to space
   its parts generously because nothing else groups them, and a card does
   that job with its own edges, so the same rhythm inside one reads as slack.

   They are kept anyway, now that the card is gone, because the second half
   of the reasoning survives it. Measured at 1440x900, the project's
   reference desktop: these two steps are what keep the four figures and
   their labels inside the first screen. Without them the proof row's labels
   fall past the fold on this composition, which is the one row on the page
   that has to be read as a set. Re-measure here after any change that adds
   a row to this hero.

   Each is one step down the fluid ladder, not a new value. */
.es-hero--align-left .es-hero__trust { margin-top: var(--gap-fluid-sm); }
.es-hero--align-left .es-hero__proof--display { margin-top: var(--gap-fluid-lg); }

.es-hero--align-left .es-hero__tagline { margin-inline: 0; }
.es-hero--align-left .es-hero__trust.es-hero__trust--card { margin-inline: 0; }

/* ── Quiet second line in the headline ──────────────────────
   The reference sets its headline in two tones: the claim in heading ink,
   the qualifier under it in grey. Scoped to the hero title rather than
   defined as a global utility, because outside a display size this colour
   does not clear AA.

   The scope matters because the token has two live values. design-tokens.css
   defines --text-muted as --gray-500 (#8A8177), which measures 3.82:1 on
   --bg-surface: a fail for body copy, a pass for large text, which under
   WCAG is anything at or above 24px. What actually renders today is
   #595959 at 7.00:1, because assets/css/style.css line 12141 redefines the
   token on :root from inside a block extracted from the agency case study
   page, and it loads after the token file.

   That override is a real foundation bug and is flagged separately: it is
   site-wide, it is not this section's to fix, and several partials already
   carry workarounds written against the documented value rather than the
   rendered one.

   This rule is safe under either value. It only ever applies inside
   .es-hero__title, whose smallest size anywhere is the 38px floor of
   --fs-hero, so even the weaker of the two clears the large-text bar with
   room to spare. */
.es-hero__title .es-quiet {
  color: var(--text-muted);
  font-weight: inherit;
}

.es-hero--bg-dark .es-hero__title .es-quiet { color: var(--gray-400); }

/* ── Display proof, opt-in via `stats_style: display` ───────
   The figures become the closing statement of the hero rather than a
   supporting band under it: no rule above them, no panel around them, set
   large and left aligned on the composition's own baseline grid.

   Left aligned, and it stays that way now the card has come off. The two
   pages that run this both set `align => 'left'`, so the headline, the
   buttons and the trust lockup above these figures all range against the
   container's left edge; the figures line up on the same edge and the
   column reads straight down. The card used to supply that edge. The
   container supplies it now.

   Four across on a desktop and held there by an explicit track count. The
   strip's `auto-fit, minmax(160px, 1fr)` is right for a variable-length
   list and wrong here: at these sizes it drops to three and orphans the
   fourth on its own row, which reads as an afterthought rather than as the
   fourth quarter of a set. */
/* `padding: 0`, not `padding-top: 0`. The base .es-hero__proof sets only
   padding-top, so the <ul> keeps the user agent's padding-inline-start.
   Measured here at 32px, which pushed the whole proof row that far right of
   the headline and the buttons it is supposed to line up under. The card
   variant never showed this because it sets the padding shorthand.

   Scoped to this variant deliberately. The base strip carries the same
   indent on every hero that runs it, but those are centred compositions
   where it reads as the row sitting slightly off-centre rather than as a
   broken left edge, and correcting it there would shift proof rows on pages
   that are not in scope for this pass. Flagged, not fixed. */
.es-hero__proof--display {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--gap-fluid-lg) var(--gap-fluid-md);
  margin-top: var(--gap-fluid-xl);
  padding: 0;
  border-top: 0;
}

.es-hero__proof--display .es-hero__stat {
  text-align: left;
  padding-left: 0;
  border-left: 0;
}

/* --fs-hero, not --fs-stat. These are the largest thing in the composition
   after the headline and are meant to read at the same scale, which is the
   whole idea of the reference: the numbers ARE the second headline. */
.es-hero__proof--display .es-hero__stat-value {
  font-size: var(--fs-hero);
  font-weight: var(--font-extrabold);
  letter-spacing: var(--tracking-tight);
  line-height: 1;
}

/* ── No width reservation on the counter here ───────────────
   The shared rule gives .rv-counter a `min-width` of one `ch` per digit and
   right-aligns inside it, so the row cannot reflow while 0 climbs to its
   target. `ch` measures the proportional zero, which is wider than this
   font's tabular digit, so that reservation always overshoots slightly and
   the surplus lands to the LEFT of the number.

   At --fs-stat that surplus is a couple of pixels. At --fs-hero it is about
   20px, and since these figures are left-aligned against a label directly
   underneath, it shows up as every number sitting indented from its own
   label. Measured on this page: "500" started 20px right of "STORES BUILT".

   The reservation is not needed in this variant anyway. It exists to stop
   the count reflowing the row, and the grid here is four fixed tracks, so
   any width change during the count is absorbed inside the cell and cannot
   move anything around it. That is the same reasoning the shared rule
   already relies on for its own residual variance. */
.es-hero__proof--display .es-hero__stat-value .rv-counter {
  min-width: 0;
  text-align: left;
}

/* The unit mark drops back against the figure rather than matching it.
   Sized in `em` so it tracks the clamp above through every breakpoint
   instead of needing its own. Baseline aligned, so "+" leads at the
   figure's foot and "%" trails at it, the way the reference draws them. */
.es-hero__proof--display .es-hero__stat-value b,
.es-hero__proof--display .es-hero__stat-affix {
  font-size: 0.42em;
  font-weight: var(--font-bold);
  letter-spacing: var(--tracking-normal);
}

/* --gray-600, not --text-muted. Same reason set out at
   .es-hero__rating-source further up: this is 13px text, AA wants 4.5:1
   for it, and --text-muted measures 3.82:1 on --bg-surface. --gray-600
   measures 6.51:1 and still reads as the quiet line under the figure. */
.es-hero__proof--display .es-hero__stat-label {
  margin-top: var(--space-4);
  color: var(--gray-600);
}

/* Source note under a single figure, for a number that cannot stand on its
   own without saying where it came from. Renders only when `note` is
   passed, so a stat without one is unchanged.

   --fs-eyebrow, which the token file marks as the floor: nothing on the
   site is smaller. The reference draws this line smaller still, at roughly
   10px, and that is not a size this design system has. It is the reference
   that gives here, not the scale.

   --gray-600 for the same reason as the label above it: at 12px AA wants
   4.5:1, and --text-muted measures 3.82:1 on --bg-surface. */
.es-hero__stat-note {
  display: block;
  max-width: 30ch;
  margin-top: var(--space-2);
  color: var(--gray-600);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-regular);
  line-height: 1.4;
  letter-spacing: var(--tracking-normal);
  text-transform: none;
}

/* ── Panel, tablet ──────────────────────────────────────────
   Four figures at --fs-hero need roughly 1000px of card to stay on one
   row. Below that they go two up, which keeps them paired rather than
   letting the grid strand one. */
@media (max-width: 991px) {
  .es-hero__proof--display { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ── Panel, phone ───────────────────────────────────────────
   The card loses its outer inset and most of its corner: at 390px a 24px
   inset either side takes 12% of the screen away from a headline that is
   already at the floor of its clamp. It becomes a full-width surface with
   a softer radius instead, which is what every other card on the site does
   at this width.

   The figures stay two up rather than dropping to one. Stacked, four
   numbers at display size run past a phone screen on their own and the
   proof stops being readable as a set. */
@media (max-width: 575px) {
  .es-hero__proof--display .es-hero__stat-value { font-size: var(--fs-stat); }
}

/* Art-directed hero ground, moved from a per-instance <style> in the partial
   (10 Sep 2026). The two images arrive as custom properties on the element.
   !important stands in for the #id specificity the old scoped rule had. */
.es-hero__bg.es-hero__bg--art { background-image: var(--es-hero-bg-sm) !important; }
@media (min-width: 768px) {
  .es-hero__bg.es-hero__bg--art { background-image: var(--es-hero-bg-lg) !important; }
}

/* ── Shared trust lockup, 17 Sep 2026 ────────────────────────
   The trust slot now holds template-parts/trust-lockup.php and nothing else,
   so the slot only positions it. Doubled class to beat the old card rules. */
.es-hero__trust.es-hero__trust--lockup {
  display: flex;
  width: auto;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  margin-top: var(--gap-fluid-md);
}

.es-hero--centered .es-hero__trust.es-hero__trust--lockup { justify-content: center; }

/* ══════════════════════════════════════════════════════════
   CLEAN SKIN, owner's instruction 17 Sep 2026
   Every page hero matches the new Home hero (template-parts/home/
   hero-split.php): white ground, centred copy, the same eyebrow pill,
   headline scale and upright orange accent, a quiet subtitle, the house
   button pair, and the proof row set off by a hairline.
   PHP already normalises the ground, artwork and alignment (see the clean
   skin block in section-hero.php); these rules carry the typography.
   Switch: Appearance, Customize, Hero banners (inc/hero-skin.php).
   ══════════════════════════════════════════════════════════ */
.es-hero--clean {
  background: var(--bg-surface);
  padding-bottom: var(--section-pad-sm, var(--section-pad));
}

.es-hero--clean .es-hero__eyebrow {
  margin-bottom: var(--space-6);
  padding: 9px 22px;
  border-color: var(--brand-100);
  background: var(--accent-subtle);
  box-shadow: none;
  color: var(--brand-700);
  font-family: var(--font-label);
  font-size: var(--text-xs);
  letter-spacing: 1.8px;
  line-height: 1.2;
}

.es-hero--clean .es-hero__title {
  margin-bottom: var(--space-5);
  font-size: clamp(2.25rem, 3.7vw + 0.2rem, 3.5rem);
  font-weight: var(--font-extrabold);
  line-height: 1.12;
  letter-spacing: -0.025em;
}

.es-hero--clean.es-hero--centered .es-hero__title {
  max-width: 16em;
  margin-inline: auto;
}

/* Upright solid accent, as on Home. */
.es-hero--clean .es-hero__title .es-hl {
  color: var(--brand-500);
  font-style: normal;
}

.es-hero--clean .es-hero__sub {
  max-width: 620px;
  color: var(--text-body);
  font-size: var(--text-base);
  line-height: 1.6;
  letter-spacing: 0.01em;
}

.es-hero--clean .es-hero__ctas {
  margin-top: var(--space-7);
}

/* Proof set off by a full-width hairline, like the Home proof row. */
.es-hero--clean.es-hero--centered .es-hero__trust.es-hero__trust--lockup {
  width: 100%;
  margin-top: var(--gap-fluid-lg);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border-subtle);
}

.es-hero--clean .es-hero__proof-card {
  margin-top: var(--gap-fluid-lg);
}

/* Media, form and deck panels: a lighter frame on the plain ground. */
.es-hero--clean :is(.es-hero__media, .es-hero__form) {
  box-shadow: var(--shadow-md);
}

@media (max-width: 575px) {
  .es-hero--clean .es-hero__ctas { flex-direction: column; align-items: stretch; }
  .es-hero--clean .es-hero__ctas .es-btn { width: 100%; }
}
