/* =========================================================================
   ENSTACKED — SITE HEADER
   Full-bleed glass bar + one morphing mega-menu card + mobile drawer.

   Layout reproduces the enstacked2.0 navigation design: a full-width bar
   that is transparent at the top of the page and turns to glass once the
   user scrolls, a SINGLE stage card underneath that morphs its width and
   slides horizontally to sit under the active trigger, a blurred backdrop,
   and a full-card mobile drawer built on <details> accordions.

   Three deliberate differences from the reference, all for site uniformity:

     - every colour, radius, space and easing resolves through a design
       token, so the header survives the dark-mode block when it lands. The
       one exception is --esn-glass below, which has no token because the
       token set has no translucent surface yet;

     - the container caps at 1320px, not the reference's 1200px. 1320px is
       what `.es-container` and Bootstrap's `.container` cap at, so the logo
       and the CTA line up edge-for-edge with page content on wide screens.
       A 1200px bar would sit 60px inside the content column on both sides,
       which is visible on any page with a full-width hero;

     - the drawer takes over at 991.98px, not the reference's 900px, because
       that is the breakpoint every other component on this site flips at.

   Class prefix `esn-` throughout. The reference names (.brand, .eyebrow,
   .badge, .backdrop, .link-card, .acc) are generic enough to collide with
   the ~200 legacy pages still running the old stylesheet, so nothing here
   uses a bare name. The `data-*` hooks ARE the contract with nav.js.
   ========================================================================= */

/* Header-local layout constants, declared once and shared by all three
   fixed pieces. Not in :root and not in design-tokens.css: these are this
   component's geometry, not site-wide design tokens. The drawer needs them
   too (it hangs off the bottom of the bar) and it is a SIBLING of the
   header, not a descendant, so it cannot inherit them. */
.esn-header,
.esn-drawer {
  --esn-h: 64px; /* bar height                            */
  --esn-offset: var(--space-3); /* 12px — inset from the viewport edge   */
  --esn-gap: var(--space-2); /* 8px  — bar to stage card              */
  --esn-max: 1320px; /* matches `.es-container`               */

  /* Sweep motion --------------------------------------------------------
     The travelling highlight on a panel link row. Header-local for the
     same reason the geometry above is: these two are one interaction's
     feel, not a site-wide motion scale. The token set tops out at
     --duration-slow (400ms), which is the right length for a state change;
     this is a light crossing a surface and needs the longer arc to read as
     travel instead of as a flicker. Named once so the row, its pane and
     its ink cannot drift out of step. */
  --esn-sweep-dur: 650ms;
  --esn-sweep-ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* ── Frosted surfaces ──────────────────────────────────────────────
     Not tokens because design-tokens.css has no translucent surface yet.
     These four are SEMANTIC: the dark block further down only reassigns
     them, so `.esn-stage` and `.esn-drawer` never name a ground directly
     and the two themes cannot drift apart.

     ONE layer each, and the surface colour rather than white. There was a
     white gradient washed over a --gray-50 floor here; it made the pane read
     as a white card with a blur behind it rather than as glass, and the
     second layer bought nothing the alpha could not.

     0.84 on the panel is not a taste call, it is the floor. Light pages carry
     inset --gray-900 objects (the closing CTA panel, the bento tiles), and
     when one of those is behind the pane the ink lands on about #D6D5D3.
     --brand-700, the hover ink, needs 0.84 to hold 4.52:1 there; --text-muted
     needs 0.82. Below that the panel is glassier and the text fails AA. The
     bar sits at 0.80 because nothing on it is muted or brand-coloured: it
     carries --text-color at 11.66:1 and an opaque pill.

     No inset highlight on this theme. A white top edge is what makes a dark
     pane read as glass; on a near-white pane it is invisible, so --esn-edge
     resolves to nothing here and only the dark block gives it a value.

     --esn-panel IS NO LONGER GLASS (owner, 12 Sep 2026). The mega-menu card is
     an opaque surface; the alpha and the 32px blur are gone from it, and the
     glass moved down to the row a pointer is on. Everything the 0.84 floor
     above was protecting is moot once the ground is solid: --gray-50 is the
     exact colour that alpha used to composite to over a light page, so every
     ink value already measured against the panel still holds, and the ones
     that used to land on #D6D5D3 over an inset dark object now land on
     #FAFAF8. --esn-glass keeps its alpha: that is the BAR, which is still
     glass while the page is merely scrolled. */
  --esn-glass: rgba(250, 250, 248, 0.8);
  --esn-glass-solid: var(--esn-panel);
  --esn-panel: var(--gray-50);

  --esn-line: rgba(22, 20, 16, 0.1);
  --esn-edge: transparent;

  /* The dark-ground set: frosted glass with a white tint, not a black scrim.

     Each is TWO layers. A white gradient carries the tint and the top-lit
     falloff that makes a pane read as glass; a --gray-900 wash underneath is
     the safety net, because `backdrop-filter` on a descendant of an element
     that already has one is not reliable (the ancestor establishes a backdrop
     root and the descendant can sample nothing). With the wash, a panel whose
     blur fails to resolve is merely flat instead of see-through.

     The white tint lands the panel around #32302D over a --gray-900 hero,
     which is what every ink value in the on-dark block is measured against. */
  --esn-glass-dark:
    linear-gradient(
      180deg,
      rgba(255, 255, 255, 0.1),
      rgba(255, 255, 255, 0.04)
    ),
    rgba(22, 20, 16, 0.72);
  /* Opaque, for the same reason as --esn-panel. --gray-800 is a shade under
     the #32302D the two translucent layers used to composite to over a
     --gray-900 hero, so every ink value in the on-dark block keeps at least
     the contrast it was measured at. */
  --esn-panel-dark: var(--gray-800);

  --esn-line-dark: rgba(255, 255, 255, 0.18);
  --esn-edge-dark: rgba(255, 255, 255, 0.14); /* the lit top edge of a pane */

  /* Objects sitting ON the glass. An interactive surface LIFTS: the white
     stacks on top of the pane's own tint, so a hovered row composites to
     about #464542 against the pane's #32302D and the change is legible
     without a border. A plate or a well SINKS instead, which is the
     separation the old near-black panel got for free and this one has to
     draw on purpose. */
  --esn-fill-dark: rgba(255, 255, 255, 0.1);
  --esn-sunk-dark: rgba(22, 20, 16, 0.55);
  --esn-sunk-dark-2: rgba(22, 20, 16, 0.35);
}

/* ── Shell ───────────────────────────────────────────────────────────── */

.esn-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition:
    background var(--duration-slow) var(--ease-out),
    border-color var(--duration-slow) var(--ease-out),
    -webkit-backdrop-filter var(--duration-slow) var(--ease-out);
  transition:
    background var(--duration-slow) var(--ease-out),
    border-color var(--duration-slow) var(--ease-out),
    backdrop-filter var(--duration-slow) var(--ease-out);
}

/* Glass + hairline once the page has moved, or while a panel is open. */
.esn-header.is-scrolled,
.esn-header.is-open {
  background: var(--esn-glass);
  border-bottom-color: var(--esn-line);
  /* Same blur as the panel below it. The bar and the stage are two panes of
     the same glass and are visible together whenever a menu is open, so a
     different radius on each reads as a mistake. */
  -webkit-backdrop-filter: saturate(180%) blur(32px);
  backdrop-filter: saturate(180%) blur(32px);
}

/* Opaque while a panel is open, so the bar and the stage card below it
   read as one surface rather than two translucent sheets. The blur goes with
   it: nothing shows through an opaque ground, so the rule above would only be
   paying for a compositing layer that changes no pixel. */
.esn-header.is-open {
  background: var(--esn-glass-solid);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* ── Bar ─────────────────────────────────────────────────────────────── */

.esn-inner {
  position: relative;
  max-width: var(--esn-max);
  margin: 0 auto;
  height: var(--esn-h);
  padding: 0 var(--space-5);
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-6);
}

@media (min-width: 1280px) {
  .esn-inner {
    padding-left: var(--space-6);
    padding-right: var(--space-6);
  }
}

.esn-brand {
  display: inline-flex;
  align-items: center;
  height: 100%;
}

/* No hover treatment on the brand mark, on the owner's instruction,
   2 Sep 2026. The 1.02 scale-up and the transition that drove it are both
   gone; `transform-origin` went with them because it existed only to anchor
   that scale. The logo is a fixed-width wordmark, and nudging it on hover
   read as a wobble rather than as feedback. The link is already obvious. */
.esn-brand img {
  width: 140px;
  height: auto;
}

/* ── Primary nav ─────────────────────────────────────────────────────── */

.esn-nav {
  justify-self: center;
  height: 100%;
}

.esn-list {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  height: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

.esn-item {
  display: inline-flex;
  align-items: center;
  height: 100%;
}

.esn-trigger,
.esn-navlink {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 40px;
  padding: 0 var(--space-4);
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: var(--font-medium);
  line-height: 1;
  letter-spacing: var(--tracking-normal);
  color: var(--text-color);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    color var(--duration-normal) var(--ease-out),
    background-color var(--duration-normal) var(--ease-out);
}

/* --brand-700, not --brand-500 or --brand-600, for any brand-coloured TEXT
   in here. utilities.css already worked this out for `.es-eyebrow`: brand-600
   measures 4.47:1 on white and misses AA by 0.03, and brand-500 is 3.4:1.
   Making the panes translucent tightens it further, because a dark section
   scrolling under one lifts the ground the ink sits on. brand-700 holds
   4.95:1 even in that worst case.

   The brand-500 that is left in this file is all NON-text: the CTA's top
   hairline and the chevron glyphs. Those answer to the 3:1 bar for graphical
   objects, which brand-500 clears. */
/* The bar used to draw a sliding 2px brand rule under the hovered item. That
   is gone: the tinted pill is the whole indicator now. It is the same
   `--bg-sunken` ground `.esn-service` rows take on hover in the panel below,
   so the bar and the panel answer the pointer the same way. The watermark
   glyph those rows carry is deliberately NOT up here — it belongs to the
   links that point at a page, not to a bar item that opens a panel. */
.esn-trigger:hover,
.esn-navlink:hover,
.esn-trigger[aria-expanded="true"] {
  color: var(--brand-700);
  background: var(--bg-sunken);
}

.esn-caret {
  flex-shrink: 0;
  opacity: 0.6;
  transition:
    transform var(--duration-slow) var(--ease-out),
    opacity var(--duration-normal) var(--ease-out);
}

.esn-trigger:hover .esn-caret,
.esn-trigger[aria-expanded="true"] .esn-caret {
  opacity: 1;
}
.esn-trigger[aria-expanded="true"] .esn-caret {
  transform: rotate(-180deg);
}

/* ── Bar actions ─────────────────────────────────────────────────────── */

.esn-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  justify-self: end;
}

/* The header CTA is the reference navbar's compact pill, on the owner's
   instruction: a 36px capsule with an "ink wipe" hover (a solid fill rising
   from the bottom while a brand hairline draws across the top edge) and the
   up-right glyph.

   It is deliberately NOT `.es-btn--dark`, the site section button. That one is
   16px type in 14px-28px padding, which is 50px tall and 171px wide; sized for
   a section CTA it is the heaviest object in a 64px bar. The padding here is
   12px, down from that 28px, which is where the white space went.

   Colour, motion tokens and the brand hairline all still resolve through the
   design tokens, so only the geometry and the hover belong to the reference. */
.esn-cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 36px;
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-full);
  background: var(--gray-900);
  color: var(--text-inverse);
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: var(--font-semibold);
  line-height: 1;
  letter-spacing: var(--tracking-normal);
  white-space: nowrap;
  text-decoration: none;
  overflow: hidden;
  isolation: isolate;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition:
    transform var(--duration-slow) var(--ease-out),
    background-color var(--duration-slow) var(--ease-out),
    border-color var(--duration-slow) var(--ease-out),
    color var(--duration-normal) var(--ease-out);
}

/* Rising fill. z-index -1 inside the button's own stacking context paints it
   above the button's background and below its label. */
.esn-cta::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--gray-800);
  transform: translateY(101%);
  transition: transform var(--duration-slow) var(--ease-out);
}

.esn-cta::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1.5px;
  background: var(--brand-500);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--duration-slow) var(--ease-out);
}

/* The up-right glyph that sat beside the label was removed on the owner's
   instruction, 2 Sep, so `.esn-cta__icon` no longer exists in the markup and
   its rules are gone with it rather than being left to rot. The label keeps
   the stacking context it needs to sit above the rising fill. */
.esn-cta > span {
  position: relative;
  z-index: 1;
}

.esn-cta:hover,
.esn-cta:focus-visible {
  color: var(--text-inverse);
}
.esn-cta:hover::before,
.esn-cta:focus-visible::before {
  transform: translateY(0);
}
.esn-cta:hover::after,
.esn-cta:focus-visible::after {
  transform: scaleX(1);
}
.esn-cta:active {
  transform: translateY(1px);
}

/* The drawer's CTA is the one place it is a full-width block, where the
   compact height would read as an afterthought. */
.esn-cta--block {
  width: 100%;
  height: 52px;
  border-radius: var(--radius-lg);
  font-size: var(--fs-body-sm);
}

/* Hamburger */
.esn-toggle {
  display: none;
  position: relative;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: var(--radius-full);
  background: none;
  cursor: pointer;
  align-items: center;
  justify-content: center;
  transition: background-color var(--duration-normal) var(--ease-out);
}

.esn-toggle:hover {
  background: var(--bg-sunken);
}

.esn-toggle .esn-bar {
  position: absolute;
  left: 14px;
  width: 16px;
  height: 1.8px;
  border-radius: 2px;
  background: var(--text-color);
  transition:
    transform var(--duration-slow) var(--ease-out),
    opacity var(--duration-normal) var(--ease-out),
    top var(--duration-slow) var(--ease-out);
}

.esn-toggle .esn-bar:nth-child(1) {
  top: 16px;
}
.esn-toggle .esn-bar:nth-child(2) {
  top: 21px;
}
.esn-toggle .esn-bar:nth-child(3) {
  top: 26px;
}

.esn-toggle[aria-expanded="true"] .esn-bar:nth-child(1) {
  top: 21px;
  transform: rotate(45deg);
}
.esn-toggle[aria-expanded="true"] .esn-bar:nth-child(2) {
  opacity: 0;
}
.esn-toggle[aria-expanded="true"] .esn-bar:nth-child(3) {
  top: 21px;
  transform: rotate(-45deg);
}

/* ── Mega stage ──────────────────────────────────────────────────────────
   ONE card for every panel. nav.js unhides the incoming panel, narrows the
   card to that panel's `data-width`, slides it under the active trigger via
   --esn-stage-x, then writes the measured height. Everything below is the
   transition; no per-menu geometry lives in this file. */

.esn-stage {
  position: absolute;
  top: calc(var(--esn-h) + var(--esn-gap));
  left: var(--esn-offset);
  right: var(--esn-offset);
  margin: 0 auto;
  max-width: var(--esn-max);
  height: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  background: var(--esn-panel);
  border: 1px solid var(--esn-line);
  border-radius: var(--radius-xl);
  /* The inset hairline is the card's lit top edge. It read as the rim of a
     pane of glass while the card was translucent; on the opaque card it is
     still the highlight that stops the top edge going flat, and it resolves
     to nothing on the light theme either way. */
  box-shadow:
    var(--shadow-xl),
    inset 0 1px 0 var(--esn-edge);
  /* No `backdrop-filter`. The card is opaque (owner, 12 Sep 2026), so a blur
     here samples a page it then completely covers. */
  transform: translateX(var(--esn-stage-x, 0px));
  will-change: height, max-width, transform, opacity;
  transition:
    height var(--duration-slow) var(--ease-out),
    max-width var(--duration-slow) var(--ease-out),
    transform var(--duration-slow) var(--ease-out),
    opacity var(--duration-normal) var(--ease-out);
}

.esn-header.is-open .esn-stage {
  opacity: 1;
  pointer-events: auto;
}

.esn-stage-inner {
  position: relative;
  padding: 0 var(--space-6);
}

/* The footer rule already closes the panel, so the 48px that used to sit
   under it read as the card failing to end. */
.esn-panel {
  padding: var(--space-6) 0 var(--space-5);
}

/* The UA default for [hidden] is display:none, but Bootstrap's reboot and
   several legacy rules set display on generic selectors; restate it so a
   closed panel can never contribute height to the stage. */
.esn-panel[hidden] {
  display: none;
}

/* A panel's own width is pinned in pixels by nav.js on every open and every
   resize, so the height it measures is the height it will need once the
   card's width transition has finished. Nothing declares a panel width
   here: `data-width` in dynamic-header.php is the only source of it. */

@keyframes esnPanelIn {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.esn-panel {
  animation: esnPanelIn var(--duration-slow) var(--ease-out) both;
}

/* ── Shared panel typography ─────────────────────────────────────────── */

/* ONE label for every panel, on the owner's instruction: the flat uppercase
   form the Shopify tab's group headings already used, not `.es-eyebrow`.

   This is not a second chip. The site chip stays exactly as it is and stays
   the eyebrow on every section of every page; the nav panels simply do not
   use it. Six brand pills inside one dropdown put more weight on the labels
   than on the links under them, which is what the owner was looking at when
   they called it. The typography is still the chip's (label face, 12px, bold,
   1.8px tracking, uppercase), so the two read as one family. */
.esn-label {
  display: block;
  margin: 0 0 var(--space-3);
  font-family: var(--font-label);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-bold);
  letter-spacing: 1.8px;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Over the service rows rather than beside them, so it takes their inner
   padding and the label sits on the same left edge as the links. */
.esn-grouplabel {
  margin-bottom: var(--space-1);
  padding-left: var(--space-4);
}

/* On the featured card's --gray-900 ground. --gray-400 is 7.80:1 there and is
   the label colour section-hero.php already measured for that surface. */
.esn-label--on-dark {
  color: var(--gray-400);
}

.esn-heading {
  margin: 0 0 var(--space-3);
  font-size: var(--fs-title-sm);
  font-weight: var(--font-bold);
  line-height: var(--lh-heading);
  letter-spacing: var(--tracking-tight);
  color: var(--text-heading);
}

.esn-sub {
  margin: 0;
  max-width: 38ch;
  font-size: var(--fs-body-sm);
  line-height: var(--leading-normal);
  color: var(--text-body);
}

/* ── Panel grid (White-Label, Company) ───────────────────────────────── */

.esn-grid {
  display: grid;
  gap: var(--gap-fluid-lg);
}

/* ── Panel layout: eyebrow / CTA on one row, body / links on the next ──────
   Used by White-Label, Insights and Company. Owner's instruction, 9 Sep 2026:
   the CTA moves out of the bottom of the lead column and into the empty space
   at the top right, the links line up with the HEADING rather than with the
   eyebrow above it, and a hairline separates the two.

   It is a real two-row grid rather than the CTA being nudged with margins,
   because that is what makes the alignment exact and keeps it exact: the
   eyebrow and the CTA share row 1, the lead copy and the links share row 2,
   so the top of the first link is the top of the heading by construction and
   stays there whatever the copy does at any breakpoint.

   `row-gap` is the eyebrow's old margin, moved onto the grid. `.esn-label`
   still carries `margin-bottom` for every other place it appears, so the
   panel eyebrow zeroes it rather than the token changing. */
.esn-grid--panel {
  grid-template-columns: 1.05fr 1fr;
  grid-template-rows: auto 1fr;
  column-gap: var(--gap-fluid-lg);
  row-gap: var(--space-3);
  align-items: start;
}

/* Company and Insights add a third rail. 1060 / 1000 on the panel's
   `data-width` buys the room; the rail is fixed so the two content columns
   keep the proportions they have in the two-column panels. */
.esn-grid--trio {
  grid-template-columns: 1.05fr 1fr var(--esn-rail, 252px);
}

/* Company's rail is a padded glass card now (10 Sep 2026), so it is wider
   than the bare 252px rail: the card's --space-5 padding would otherwise
   leave the review rows narrower than they were. The Insights rail that used
   to be sized here went with its "Latest" cards the same day. */
.esn-panel[data-panel="company"] .esn-grid--trio {
  --esn-rail: 280px;
}

.esn-panel__eyebrow {
  grid-column: 1;
  grid-row: 1;
  margin-bottom: 0;
  align-self: center;
}

/* The panel CTA used to sit here, top-right on the eyebrow's row. It moved
   into `.esn-panefoot` on 9 Sep 2026 when the owner asked for the Shopify
   panel's footer stripe on every tab: that stripe already carries a CTA on
   its right, and two calls to action pointing at neighbouring destinations in
   one 200px-tall panel is the opposite of the premium read that was asked
   for. One CTA per panel, in the same place on all five. */

.esn-panel__lead {
  grid-column: 1;
  grid-row: 2;
}

/* A link column, built to the Shopify panel on the owner's instruction
   (10 Sep 2026): its own small label on the eyebrow's row, the links on the
   heading's row, and the hairline down its left edge for the FULL height of
   the grid.

   It spans both rows and takes them as a subgrid, so the label still shares
   row 1 with the eyebrow and the first link still shares row 2 with the
   heading, while the border belongs to one box. The hairline used to sit on
   the link list alone, which started it at the heading and left the eyebrow
   row with no divider. Same token, same 1px and same --space-6 clearance as
   `.esn-group:nth-child(even)`: one rule for a divider in this nav. */
.esn-panel__col {
  grid-column: 2;
  grid-row: 1 / -1;
  display: grid;
  grid-template-rows: subgrid;
  align-items: start;
  min-width: 0;
  border-left: 1px solid var(--border-subtle);
  padding-left: var(--space-6);
}

/* The column's label. Link columns add `.esn-grouplabel`, whose inset puts
   the label on the link text's left edge, as it does over BUILD / SCALE. The
   Products column does not: its cards have no inner inset to line up with. */
.esn-panel__collabel {
  margin-bottom: 0;
  align-self: center;
}

.esn-panel__links {
  grid-row: 2;
}

/* The rail spans both rows: it is a block in its own right, not a column of
   the body, so it starts level with the eyebrow rather than with the links.
   Company's rail is a glass card (`.esn-reviews--card`), and a card's own
   edge is the divider, so it draws no hairline of its own. */
.esn-panel__reviews {
  grid-column: 3;
  grid-row: 1 / -1;
}

.esn-lead {
  display: flex;
  flex-direction: column;
  padding-right: var(--space-2);
}

/* `auto` columns, not `repeat(3, 1fr)`. Three equal columns gave the widest
   label the same room as the narrowest, so in the Company panel's trio layout
   "Years in eCommerce" and "Projects delivered" each broke to two lines while
   "Client retention" sat on one, and the row read as ragged rather than as a
   set. Sizing to content lets each label take what it needs; --space-5 keeps
   them apart without the grid having to force it. */
.esn-stats {
  display: grid;
  grid-template-columns: repeat(3, auto);
  justify-content: start;
  gap: var(--space-5);
  margin: var(--space-5) 0 0;
  padding: var(--space-4) 0;
  border-top: 1px solid var(--border-subtle);
}

.esn-stats > div {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.esn-stats > div + div::before {
  content: "";
  position: absolute;
  left: -8px;
  top: 4px;
  bottom: 4px;
  width: 1px;
  background: var(--border-subtle);
}

.esn-stats dt {
  font-size: var(--text-xl);
  font-weight: var(--font-bold);
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  color: var(--text-heading);
  font-feature-settings: "tnum", "lnum";
}

.esn-stats dd {
  margin: 0;
  font-size: var(--fs-eyebrow);
  color: var(--text-body);
}

.esn-leadlink {
  align-self: flex-start;
  margin-top: auto;
  padding-top: var(--space-5);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  color: var(--text-heading);
  text-decoration: none;
  transition:
    gap var(--duration-normal) var(--ease-out),
    color var(--duration-normal) var(--ease-out);
}

.esn-leadlink:hover {
  color: var(--brand-700);
}

/* Every text CTA in the header carries the site's signature CTA
   microinteraction: the triple chevron's three paths sweeping their opacity
   in sequence (arrowPulse1/2/3, defined in utilities.css). It replaces the
   translate(2px,-2px) nudge the reference navbar used, which appears nowhere
   else on this site. `gap` is not animated any more either: the site's own
   buttons hold their gap and move the glyph, not the box. */
.esn-link__icon {
  flex-shrink: 0;
  color: var(--brand-500);
}

.esn-leadlink .arrow-icon path,
.esn-panefoot__cta .arrow-icon path,
.esn-ghost .arrow-icon path {
  transition: opacity var(--duration-normal) ease;
}

.esn-leadlink:hover .arrow-icon path:nth-child(1),
.esn-panefoot__cta:hover .arrow-icon path:nth-child(1),
.esn-ghost:hover .arrow-icon path:nth-child(1) {
  animation: arrowPulse1 0.8s infinite;
}
.esn-leadlink:hover .arrow-icon path:nth-child(2),
.esn-panefoot__cta:hover .arrow-icon path:nth-child(2),
.esn-ghost:hover .arrow-icon path:nth-child(2) {
  animation: arrowPulse2 0.8s infinite;
}
.esn-leadlink:hover .arrow-icon path:nth-child(3),
.esn-panefoot__cta:hover .arrow-icon path:nth-child(3),
.esn-ghost:hover .arrow-icon path:nth-child(3) {
  animation: arrowPulse3 0.8s infinite;
}

/* Reads as a small sibling of `.es-eyebrow`: same label face, same brand-50
   ground, same brand ink. --fs-eyebrow is documented in design-tokens.css as
   the floor ("nothing on the site is smaller"); this carried 10px. */
.esn-badge {
  display: inline-flex;
  align-items: center;
  height: 20px;
  padding: 0 8px;
  border: 1px solid var(--brand-100);
  border-radius: var(--radius-full);
  background: var(--accent-subtle);
  font-family: var(--font-label);
  font-size: var(--fs-eyebrow);
  font-style: normal;
  font-weight: var(--font-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--brand-700);
}

/* ── Shopify panel ───────────────────────────────────────────────────── */

.esn-mega {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: var(--gap-fluid-lg);
}

.esn-groups {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-6);
  /* No row-gap. The space between the two rows of groups is padding on the
     groups instead, so the hairline below runs unbroken from BUILD to the
     last RUN link: a row-gap left a --space-5 hole in it between SCALE and
     RUN (owner, 10 Sep 2026). Same space, different owner. */
  row-gap: 0;
  align-content: start;
}

.esn-group {
  padding-bottom: var(--space-5);
}
.esn-group:nth-last-child(-n + 2) {
  padding-bottom: 0;
}

/* Hairline between the two lifecycle columns. */
.esn-group:nth-child(even) {
  border-left: 1px solid var(--border-subtle);
  padding-left: var(--space-6);
}

/* Group-label spacing lives on .esn-grouplabel itself. */

.esn-services {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.esn-service {
  /* The arrow's corner, declared on the row so the reserve and the arrow
     cannot drift apart: the right padding IS the arrow's inset plus its box
     plus one gap, so moving the arrow moves the text that has to clear it.

     It is load-bearing on the widest rows. At the 1320px cap the Insights
     and White-Label rows are 342px, and "Explore the full case study
     archive." runs to 302px of the 326px it would have without a reserve,
     which is past the arrow. With the reserve the tightest clearance across
     all seventeen rows is 6px and nothing wraps.

     Every row carries it, parked rows included, or the labels down a group
     would not line up with each other. */
  --esn-go: 16px;
  --esn-go-inset: var(--space-3);

  /* How far left of its corner the arrow starts its run (owner, 12 Sep
     2026, marked on a screenshot). 140px lands it just past the end of the
     description on the 438px Shopify rows, which is the point marked, and
     about 40% across the 299px Company rows. A px, not a percentage: a
     percentage on the arrow resolves against the ARROW's 16px box, not the
     row, and CSS has no way to read the row's width here. */
  --esn-go-travel: 140px;

  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-2)
    calc(var(--esn-go-inset) + var(--esn-go) + var(--space-2)) var(--space-2)
    var(--space-4);
  border-radius: var(--radius-md);
  overflow: hidden;
  isolation: isolate;
  text-decoration: none;
}

/* The pointer lifts a pane of GLASS under the row (owner, 12 Sep 2026). The
   row used to go flat grey; the glass that used to be the whole card is here
   now instead.

   Drawn, not sampled. The card behind the row is opaque, so a
   `backdrop-filter` here would cost a compositing layer and change no pixel
   (the same reasoning `.esn-reviews--card` carries further down). What reads
   on a near-white pane is a translucent surface falling off on the diagonal
   plus a hairline to give it an edge, so that is what this is.

   Its own element rather than a background on the row, because the state has
   to fade and a gradient cannot be transitioned while an opacity can.
   `z-index: -1` inside the row's own stacking context keeps the old order
   exactly: glass, then the orange watermark, then the label.

   No shadow, inset or otherwise, in either half. Hover feedback on this site
   is border colour, lift and tint (owner, 10 Sep 2026).

   The orange still lands on the watermark glyph rather than on the row. A
   brand-tinted pane would put the accent on 300px of surface to say one row
   is hovered, which is the loudest way to say the quietest thing. */
a.esn-service::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border: 1px solid var(--esn-line);
  border-radius: inherit;
  background: linear-gradient(
    160deg,
    color-mix(in srgb, var(--bg-surface) 94%, transparent),
    color-mix(in srgb, var(--bg-surface) 44%, transparent)
  );
  opacity: 0;
}

/* IN ONLY, NEVER OUT (owner, 12 Sep 2026). Every transition on this row
   lives on the :hover / :focus-visible rule, never on the resting rule.
   That is the whole mechanism: while the pointer is on the row the timed
   rule applies and the state eases in; the moment it leaves, that rule
   stops applying and the resting rule has no transition to run, so the row
   snaps back to clean white in one frame.

   It has to be done on EVERY animated property at once, or the row comes
   apart on the way out: whichever property kept its transition on the base
   rule would linger behind the ones that snapped. The four are this pane,
   the glow (::after), the ink, and the arrow.

   Do not "tidy" any of these back onto the base rule. */
a.esn-service:hover::before,
a.esn-service:focus-visible::before {
  opacity: 1;
  border-color: color-mix(in srgb, var(--brand-500) 15%, transparent);
  transition:
    opacity var(--duration-normal) var(--ease-out),
    border-color var(--duration-normal) var(--ease-out);
}

/* The travelling glow (owner, 12 Sep 2026). ONE small soft warm blob. It
   starts off the row's left edge, crosses while the pointer is on the row,
   and settles against the right edge. Leaving the row runs the same travel
   backwards, right to left, for free: a transition that is only a transform
   reverses itself.

   A BLOB, NOT A BAND, and that is the whole design (owner, 12 Sep 2026).
   There was a full-width linear wash under this that carried the tint
   across the entire row; it made the hovered row read as an orange card
   with a highlight on it. The row's ground is the clean white pane and
   nothing else, and the orange is a small light crossing it. If this ever
   grows back into a second full-width layer, it is wrong.

   Nothing fades. At rest the layer is parked outside the row and the
   `overflow: hidden` above clips it away, so the default row is the one
   that shipped, pixel for pixel.

   ONE pseudo-element and ONE animated property, both composited: the GPU
   slides a finished layer. Animating a gradient stop, a width or a `left`
   would repaint or relayout, and there are 40 of these rows inside a
   position: fixed header.

   Width is exactly the row's, which is what makes the blob's travel free
   of arithmetic: the blob sits at 84% of the LAYER, so `translateX(0)`
   puts it at 84% of the row (settled at the right edge) and
   `translateX(-100%)` puts it at -16% (clear of the left edge). Move the
   84% and both ends move together. The layer is oversized vertically
   instead, so the glow falls off past the row's top and bottom rather than
   stopping on a hard line.

   The blob peaks at 10% --brand-400. --text-body holds 9.2:1 and
   --brand-700 5.8:1 at the centre of it, which is the warmest pixel the
   row ever shows.

   `z-index: -1` keeps it inside the row's own stacking context: above the
   glass pane (::before, the same layer, painted earlier) and below the
   label, so the type stays sharp. */
a.esn-service::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  top: -55%;
  bottom: -55%;
  z-index: -1;
  pointer-events: none;
  transform: translateX(-100%);
  /* Radii, not diameters: 22% of the row's width and 62% of the layer's
     (oversized) height, so the blob is about 190 x 165 on a 438px row and
     reads as a soft round light rather than a stripe. */
  background: radial-gradient(
    22% 62% at 84% 50%,
    color-mix(in srgb, var(--brand-400) 10%, transparent) 0%,
    color-mix(in srgb, var(--brand-400) 6%, transparent) 38%,
    color-mix(in srgb, var(--brand-400) 2%, transparent) 68%,
    transparent 100%
  );
}

a.esn-service:hover::after,
a.esn-service:focus-visible::after {
  transform: translateX(0);
  transition: transform var(--esn-sweep-dur) var(--esn-sweep-ease);
}

.esn-service b {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-body-sm);
  font-weight: var(--font-semibold);
  letter-spacing: var(--tracking-normal);
  color: var(--text-heading);
}

/* `:not(.esn-service__go)` is load-bearing. The arrow is also a `> span` of
   the row, and this selector (0,1,1) outranks `.esn-service__go` (0,1,0),
   so without the exclusion it hands the arrow `position: relative` — which
   drops it back into the flow and stretches it across the row — plus the
   body ink. Any rule below that paints the description has to exclude it
   for the same reason. */
.esn-service > span:not(.esn-service__go) {
  position: relative;
  z-index: 1;
  font-size: var(--fs-caption);
  line-height: 1.45;
  color: var(--text-body);
}

/* Ink answers the pointer on its own clock, a quarter of the glow's, so
   the label has already changed by the time the light is halfway across.
   --brand-700 is the hover ink `.esn-leadlink` and `.esn-panefoot__cta`
   already use, so a link row reads the same as every other link in the
   panel; it holds 5.8:1 under the centre of the glow. */
a.esn-service:hover b,
a.esn-service:focus-visible b {
  color: var(--brand-700);
  transition: color var(--duration-normal) var(--ease-out);
}

.esn-service.is-soon b,
.esn-service.is-soon > span {
  color: var(--text-body);
}

/* Hover arrow (owner, 12 Sep 2026). `arrow-right`, the plain one the
   Products panel's go-button uses, NOT the triple chevron: the owner's
   ruling on nav panels is that they take the plain arrow, and the triple
   chevron stays on page CTAs.

   THE ROW'S RIGHT CORNER, centred on the row's height (owner, 12 Sep
   2026), which is where the glow settles: the light arrives and the arrow
   is what it arrives at. The row reserves the width for it up top, see
   --esn-go-inset above.

   It anchors to the ROW, so it must be a child of the row and not of `b`.
   `b` is `position: relative`, so an arrow inside it would anchor to the
   LABEL's box and sit wherever the label happens to end.

   IT TRAVELS (owner, 12 Sep 2026). At rest it is parked --esn-go-travel to
   the left of its corner and invisible; on hover it runs the whole distance
   back to the corner. `right` never changes: the corner is where it lives
   and the run is a transform, so the arrow's home is one number and its
   journey is another and neither can move the other.

   It runs on the GLOW's clock, not the ink's, because the two are one
   gesture: the light crosses the row and the arrow rides in with it to the
   place the light settles. The fade is --duration-fast so the arrow is
   visible for nearly all of the run rather than appearing part-way along
   it.

   `z-index: 1` puts it in the label's layer, above the glow, and it is the
   row's last child, so it passes over the description rather than under it
   on the narrower rows. `pointer-events: none` keeps a moving object out of
   the way of the pointer that started it. */
.esn-service__go {
  position: absolute;
  right: var(--esn-go-inset);
  top: 50%;
  z-index: 1;
  display: inline-flex;
  color: var(--brand-500);
  opacity: 0;
  transform: translate(calc(var(--esn-go-travel) * -1), -50%);
  pointer-events: none;
}

a.esn-service:hover .esn-service__go,
a.esn-service:focus-visible .esn-service__go {
  opacity: 1;
  transform: translate(0, -50%);
  transition:
    opacity var(--duration-fast) var(--ease-out),
    transform var(--esn-sweep-dur) var(--esn-sweep-ease);
}

/* ── Products panel ──────────────────────────────────────────────────────
   Varn and Caffenza are Enstacked's own app and theme, not client work, so
   they get their own panel rather than a teaser inside Shopify.

   Laid out to the owner's reference (11 Sep 2026): lead copy and two proof
   points on the left, two product cards on the right. The lead is centred
   against the cards' height below 1200px and spans it from 1200px up (see
   "Lead spans the cards" further down). The panel is 1180 wide (`data-width`), sized
   to its content so no dead band opens between the copy and the cards.

   Compacted on the owner's instruction (11 Sep 2026, "cards are bigger"):
   the card column is capped at 36rem, so each card is ~280 x 265 at the
   1320px cap, with one --space-5 inset on every side and a 64px mark.
   Nothing here sets a card height; the padding and the mark produce it. */

.esn-prodpanel {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 36rem);
  column-gap: var(--gap-fluid-lg);
  align-items: center;
}

.esn-prodpanel__eyebrow {
  margin-bottom: var(--space-4);
}

.esn-prodpanel__heading {
  margin: 0 0 var(--space-4);
  font-size: var(--fs-title-md);
  line-height: var(--lh-display);
}

/* The second line of the heading steps back a tone, as in the reference.
   --gray-500 on the pane is 3.9:1, which passes for text this large. */
.esn-prodpanel__heading span {
  display: block;
  color: var(--gray-500);
}

/* Two proof points, side by side, split by the nav's one divider rule. */
.esn-prodpoints {
  display: flex;
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
}

/* Spacing tightened when the panel narrowed (11 Sep 2026). With the 1180
   panel's ~490px lead column these leave each description ~150px, which
   holds both to two lines. The divider's padding comes out of the SECOND
   point's width, which is why that one is the tight one. */
.esn-prodpoint {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1 1 0;
  min-width: 0;
}

.esn-prodpoint + .esn-prodpoint {
  margin-left: var(--space-4);
  padding-left: var(--space-4);
  border-left: 1px solid var(--border-subtle);
}

.esn-prodpoint__icon {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: var(--space-7);
  height: var(--space-7);
  border-radius: var(--radius-md);
  background: var(--accent-subtle);
  color: var(--text-heading);
}

.esn-prodpoint__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.esn-prodpoint__text b {
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  color: var(--text-heading);
}

.esn-prodpoint__text span {
  font-size: var(--fs-eyebrow);
  line-height: 1.5;
  color: var(--gray-600);
}

/* ── Lead spans the cards (owner, 14 Sep 2026) ──
   The lead's description paragraph was removed, which left the eyebrow,
   heading and proof points floating mid-height with loose space above and
   below. From 1200px the lead now runs the full height of the cards and
   lines up with what is inside them:

     - top: the eyebrow is a row the height of the card's head row (the
       go-button, --space-6 + --space-2) with its text centred, so it sits on
       the same line as the "Shopify App" / "Shopify Theme" chips. Its bottom
       margin goes because the row already carries that space: the heading
       lands where it was.
     - bottom: the proof points are pushed down with an auto margin, so their
       bottom edge meets the bottom of the card descriptions.
     - inset: the same --space-5 padding the cards use, plus their 1px
       border, so both columns measure from the same lines.

   Not below 1200: at 992-1199 the proof points stack and the lead is taller
   than the cards' inner area (229px against 217px at 1100), so it cannot
   span. It stays centred there, as before. */
@media (min-width: 1200px) {
  .esn-prodpanel {
    align-items: stretch;
  }

  .esn-prodpanel__lead {
    display: flex;
    flex-direction: column;
    padding-block: calc(var(--space-5) + 1px);
  }

  .esn-prodpanel__eyebrow {
    display: flex;
    align-items: center;
    min-height: calc(var(--space-6) + var(--space-2));
    margin-bottom: 0;
  }

  .esn-prodpoints {
    margin-top: auto;
  }
}

/* ── Product cards ── */

.esn-products {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.esn-products > li {
  display: flex;
}

/* Each card carries its tag colours in two variables; the modifiers below
   re-point them and pick the card's background artwork. */
.esn-product {
  --esn-prod-chip-bg: var(--gray-100);
  --esn-prod-chip-ink: var(--gray-700);

  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  padding: var(--space-5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-xl);
  background: var(--bg-surface);
  box-shadow: var(--shadow-xs);
  overflow: hidden;
  isolation: isolate;
  text-decoration: none;
  transition:
    transform var(--duration-slow) var(--ease-out),
    border-color var(--duration-normal) var(--ease-out);
}

/* The card artwork: the owner's reference backgrounds (11 Sep 2026), redrawn
   as SVG in assets/svg so they stay a few KB and sharp at any density. The
   files anchor their ribbon to the right edge (xMaxYMid slice), so `cover`
   on a near-square card crops the empty left side, never the curve. Behind
   the content, never over it. */
.esn-product::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: no-repeat right center / cover;
  /* Owner, 11 Sep 2026: the artwork sits back behind the copy. One value for
     both cards, so their tints stay matched. */
  opacity: 0.6;
}

.esn-product--violet {
  --esn-prod-chip-bg: var(--violet-100);
  --esn-prod-chip-ink: var(--violet-600);
}

.esn-product--violet::before {
  background-image: url("../svg/product-wash-violet.svg");
}
.esn-product--warm::before {
  background-image: url("../svg/product-wash-warm.svg");
}

/* No box-shadow on hover: owner ruling, 10 Sep 2026. */
a.esn-product:hover {
  transform: translateY(-2px);
  border-color: var(--brand-200);
}

.esn-product.is-soon {
  cursor: default;
}

/* Tag left, go-button right, both on the card's own inset: every edge in
   the card lines up with the padding, nothing hangs into it.

   The row is held at the go-button's height even when it carries the
   shorter "Soon" chip instead, so the mark, name and copy start on the same
   line in every card. Without it Caffenza's plate sat 9px above Varn's. */
.esn-product__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  align-self: stretch;
  min-height: calc(var(--space-6) + var(--space-2));
}

.esn-product__tag {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--esn-prod-chip-bg);
  font-size: var(--fs-eyebrow);
  font-weight: var(--font-medium);
  line-height: 1;
  color: var(--esn-prod-chip-ink);
  white-space: nowrap;
}

.esn-product__go {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: calc(var(--space-6) + var(--space-2));
  height: calc(var(--space-6) + var(--space-2));
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  box-shadow: var(--shadow-sm);
  color: var(--text-heading);
}

/* The Products panel's plain arrow (owner, 11 Sep 2026), in the card button
   and the footer CTA. It holds still on hover: the owner asked for no
   transform on it, and it is a single stroke, so the triple chevron's
   opacity sweep has nothing to sweep either. */
.esn-arrow {
  flex-shrink: 0;
}

.esn-product__chip {
  flex-shrink: 0;
}

/* The mark. 64px square (--space-8). A letter plate paints its own dark
   rounded ground; a logo file already carries its own plate and radius, so
   its frame is transparent and sizes the image, never the other way round
   (owner, 9 Sep 2026: adding a logo must not resize a card). */
.esn-product__plate {
  display: grid;
  place-items: center;
  width: var(--space-8);
  aspect-ratio: 1;
  margin-top: var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--gray-900);
  overflow: hidden;
}

.esn-product__plate--logo {
  background: transparent;
  border-radius: 0;
}

.esn-product__logo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.esn-product__mono {
  font-size: var(--text-2xl);
  font-weight: var(--font-bold);
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  color: var(--text-inverse);
}

.esn-product__name {
  margin-top: var(--space-4);
  font-size: var(--fs-body);
  font-weight: var(--font-bold);
  line-height: var(--lh-subhead);
  letter-spacing: var(--tracking-normal);
  color: var(--text-heading);
}

.esn-product__desc {
  margin-top: var(--space-1);
  font-size: var(--fs-caption);
  line-height: var(--leading-normal);
  color: var(--gray-600);
}

/* ── Featured card ───────────────────────────────────────────────────── */

.esn-feature {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  background: var(--gray-900);
  color: var(--text-inverse);
  overflow: hidden;
  isolation: isolate;
  text-decoration: none;
  transition:
    transform var(--duration-slow) var(--ease-out),
    box-shadow var(--duration-slow) var(--ease-out);
}

.esn-feature::before {
  content: "";
  position: absolute;
  inset: -40% -30% auto auto;
  width: 80%;
  aspect-ratio: 1;
  background: radial-gradient(
    closest-side,
    rgba(232, 93, 32, 0.28),
    transparent 70%
  );
  z-index: -1;
  pointer-events: none;
}

/* No hover on the card. It stopped being a link on 12 Sep 2026: it holds two
   of them now (the archive, and the study it is showing) and each answers the
   pointer for itself. */

/* The label row. The CTA moved up beside the label (owner, 12 Sep 2026), so
   the card opens with "CASE STUDIES ... View all" on one line.

   `align-self: stretch` because the card is `align-items: flex-start`: a
   shrink-wrapped row would have nothing to push the CTA against and
   `space-between` would do nothing. */
.esn-feature__head {
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

/* `.esn-label` carries the gap it needs when it is stacked above something.
   In this row the row owns the spacing. */
.esn-feature__head .esn-label {
  margin: 0;
}

.esn-feature__title {
  margin: 0 0 var(--space-2);
  font-size: var(--fs-card-title);
  font-weight: var(--font-bold);
  line-height: var(--lh-subhead);
  letter-spacing: var(--tracking-tight);
  color: var(--text-inverse);
}

/* No `.esn-feature__desc`. The summary line came off the card on 12 Sep 2026;
   the picture below says what it used to say. */

/* The most recent case study's own screenshot, taking whatever the card has
   left under the headline.

   `flex: 1` with `min-height: 0` is what keeps the CARD the size it was: its
   height is the Shopify group column's height, set by the grid, and the
   picture fills the remainder rather than adding to it. A picture with an
   intrinsic height here would push the card past its row.

   The link, the frame and the two lines under it. `flex: 1` with
   `min-height: 0` on both this and the frame inside it is what keeps the CARD
   the size it was: its height is the Shopify group column's, set by the grid,
   and everything here divides that remainder rather than adding to it. */
.esn-feature__study {
  align-self: stretch;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-4);
  text-decoration: none;
  color: inherit;
}

/* The picture is MATTED, not cropped (owner, 12 Sep 2026). `contain` inside a
   padded frame, so the whole screenshot is there: these are full-page store
   captures around 1920x1682, and every crop that fits one into a portrait
   frame loses either the sides of the page or the foot of it. The frame's own
   ground fills whatever contain leaves, which is a few pixels top and bottom
   at the card's usual width, and reads as matting rather than as a gap.

   Thinner mat and no border, on the owner's instruction (14 Sep 2026): the
   padding drops from --space-2 to --space-1 and the 12% white hairline is
   gone, so the screenshot sits closer to the card edge. The translucent
   ground stays, because it is what fills the contain gaps. */
.esn-feature__shot {
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--space-1);
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.05);
  transition: background-color var(--duration-normal) var(--ease-out);
}

.esn-feature__shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: var(--radius-sm);
}

/* Never a shadow (owner, 10 Sep 2026). With the border gone the hover lifts
   the mat's ground instead of brightening a hairline. */
.esn-feature__study:hover .esn-feature__shot {
  background: rgba(255, 255, 255, 0.1);
}

/* The whole card is the link, so its CTA is a text CTA, not a second button
   inside a button. It was a bordered glass pill, which is the reference
   navbar's "ghost" control and is not a shape this site uses. Now it reads
   like `.esn-leadlink` on a dark ground and takes the same chevron sweep.

   No `margin-top: auto`. That held it on the floor of the card while it was
   the last thing in the column; it now sits in `.esn-feature__head`. */
.esn-ghost {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  color: var(--text-inverse);
  transition: color var(--duration-normal) var(--ease-out);
}

/* brand-300 is the dark ground's brand ink (9.01:1 on --gray-900), the same
   value the dark hero gives its own eyebrow. brand-500 would be 4.36:1. */
.esn-ghost .arrow-icon {
  flex-shrink: 0;
  color: var(--brand-300);
}

.esn-ghost:hover {
  color: var(--brand-300);
}

/* ── Panel footer ────────────────────────────────────────────────────── */

/* ── Panel footer stripe ──────────────────────────────────────────────────
   On EVERY panel as of 9 Sep 2026, not just Shopify. It is one band rather
   than a rule with text under it: a proof line on the left, the panel's one
   CTA on the right.

   Full-bleed by construction. `.esn-stage-inner` insets the panel by
   --space-6 and `.esn-panel` closes with --space-5 of bottom padding; the
   negative margins cancel exactly those two, so the band reaches the pane's
   edges, and the padding puts its text back on the same left edge as the
   content above it. The stage already carries `overflow: hidden` and
   --radius-xl, so the band's bottom corners are clipped to the card's own
   radius with nothing to declare here — which is what stops it reading as a
   rectangle pasted onto a rounded pane. */
.esn-panefoot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin: var(--space-5) calc(var(--space-6) * -1) calc(var(--space-5) * -1);
  padding: var(--space-4) var(--space-6);
  border-top: 1px solid var(--border-subtle);
  background: var(--esn-panel);
}

.esn-panefoot__meta {
  font-size: var(--fs-eyebrow);
  color: var(--text-body);
}

.esn-panefoot__meta b {
  color: var(--text-heading);
  font-weight: var(--font-semibold);
}

/* The Products footer leads with the panel's own label, inline, then a dot. */
.esn-panefoot__tag {
  display: inline;
  margin: 0;
}
.esn-panefoot__dot {
  margin: 0 var(--space-3);
  color: var(--text-muted);
}

.esn-panefoot__cta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  color: var(--text-heading);
  text-decoration: none;
  white-space: nowrap;
  transition:
    gap var(--duration-normal) var(--ease-out),
    color var(--duration-normal) var(--ease-out);
}

.esn-panefoot__cta:hover {
  color: var(--brand-700);
}

/* ═════════════════════════════════════════════════════════════════════════
   THE HEADER ON A DARK GROUND

   The bar is transparent at the top of the page, so its ink belongs to
   whatever the page paints underneath it. On a dark band the ink, the caret
   and the near-black CTA pill all disappear. This block is the whole answer,
   and the rule behind it is:

     the navbar does not have a second theme. It borrows the ink the rest of
     the site already uses on a dark ground.

   The ink is the scale `section-hero.php` measured for `.es-hero--bg-dark`,
   so the bar, the panel and the hero they stand on are one piece of design
   rather than three guesses at "white-ish". The ratios below are re-measured
   against the ground the FROSTED PANE actually composites to (about #32302D
   over a --gray-900 hero), not against --gray-900 itself, because the white
   tint lifts the ground and every ratio with it:

     --text-inverse  #FFFFFF  13.13:1   headings, primary ink, hamburger
     --gray-300      #D5CFC5   8.48:1   nav labels, body copy, parked rows
     --gray-400      #B0A898   5.57:1   captions, group labels, caret
     --brand-300     #FF9D6B   6.43:1   hover and open ink, focus ring
     --brand-400     #F47340   4.61:1   the rule under an open trigger

   Every text value clears AA on the pane, on the bar (#2D2C28) and on the
   recessed plates (#23211D), which are the three grounds ink lands on. The
   4.61 belongs to a 2px decorative rule, which is held to 3:1, not 4.5:1.

   --gray-500 measures 3.43:1 here and 4.22:1 on the darkest of the three, so
   it fails on all of them. Nothing below uses it; a parked row is set one step
   down from white instead.

   NOTHING here flips to a light surface. An earlier version scoped all of it
   `:not(.is-open)`, which sent the bar back to white the moment a menu opened
   and dropped a white card onto a black hero. The panel is now the same
   smoked glass as the bar, and every element inside it is repainted.

   Which pages get the class, and when, is decided in two places and neither
   of them is here: inc/nav-theme.php renders it for the first paint, and
   assets/js/nav.js adds and removes it as `[data-nav-invert]` bands pass
   under the bar. nav.js mirrors it onto the drawer, which is a SIBLING of the
   header and so cannot inherit it.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Chrome ──────────────────────────────────────────────────────────────
   Both grounds are --gray-900 at different alphas, the same colour the hero's
   own scrim uses, so the bar densifies into the hero rather than sitting on
   top of it.

   Only the BAR is still one of those alphas. The mega-menu card is opaque as
   of 12 Sep 2026, so the old worry here is gone with it: there is no longer a
   descendant blur that could sample nothing, and no failure mode where a card
   whose blur did not resolve reads as a near-transparent panel. */
/* The whole dark theme, as far as the CHROME is concerned, is these four
   reassignments. `.esn-stage` and `.esn-drawer` name only the semantic
   surfaces, so neither has a second rule here and the two themes cannot
   drift apart. Only the shadow is restated, because a shadow tuned for a
   pane on a white page does nothing on a black one. */
.esn-header--on-dark,
.esn-drawer--on-dark {
  --esn-glass: var(--esn-glass-dark);
  --esn-panel: var(--esn-panel-dark);
  --esn-line: var(--esn-line-dark);
  --esn-edge: var(--esn-edge-dark);
}

.esn-header--on-dark.is-scrolled,
.esn-header--on-dark.is-open {
  border-bottom-color: var(--esn-line-dark);
}

.esn-header--on-dark .esn-stage,
.esn-drawer--on-dark {
  box-shadow:
    0 24px 64px -24px rgba(0, 0, 0, 0.65),
    inset 0 1px 0 var(--esn-edge);
}

/* ── Bar ─────────────────────────────────────────────────────────────── */

/* Logo. Both files are in the document on a page that opens dark (see
   dynamic-header.php), so this is a state change, not a request. */
.esn-brand__mark--dark {
  display: none;
}
.esn-header--on-dark .esn-brand__mark--light {
  display: none;
}
.esn-header--on-dark .esn-brand__mark--dark {
  display: block;
}

.esn-header--on-dark .esn-trigger,
.esn-header--on-dark .esn-navlink {
  color: var(--gray-300);
}

.esn-header--on-dark .esn-trigger:hover,
.esn-header--on-dark .esn-navlink:hover,
.esn-header--on-dark .esn-trigger[aria-expanded="true"] {
  color: var(--brand-300);
  /* Same swap `.esn-service` makes on this ground: --bg-sunken (a light-mode
     tint) would vanish here, --esn-fill-dark is the dark-ground equivalent. */
  background: var(--esn-fill-dark);
}

/* Lifted from 0.6: the caret is a hairline glyph, and the same opacity that
   reads as "quiet" on white reads as "missing" on --gray-900. */
.esn-header--on-dark .esn-caret {
  color: var(--gray-400);
  opacity: 0.75;
}

/* CTA. A --gray-900 pill on a --gray-900 hero is an invisible button, so on
   dark it takes the glass treatment the dark hero already gives its own
   secondary button (`.es-hero--bg-dark .es-btn--secondary`), and the ink-wipe
   fill rises to white with the label flipping dark. The brand hairline is
   unchanged: --brand-500 reads on the white fill it draws over. */
.esn-header--on-dark .esn-cta {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.22);
  color: var(--text-inverse);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.esn-header--on-dark .esn-cta::before {
  background: var(--bg-surface);
}

.esn-header--on-dark .esn-cta:hover,
.esn-header--on-dark .esn-cta:focus-visible {
  border-color: var(--bg-surface);
  color: var(--gray-900);
}

.esn-header--on-dark .esn-toggle .esn-bar {
  background: var(--text-inverse);
}
.esn-header--on-dark .esn-toggle:hover {
  background: var(--esn-fill-dark);
}

/* ── Panel typography ────────────────────────────────────────────────── */

.esn-header--on-dark .esn-label {
  color: var(--gray-400);
}
.esn-header--on-dark .esn-heading {
  color: var(--text-inverse);
}
.esn-header--on-dark .esn-sub {
  color: var(--gray-300);
}

/* ── Panel components ────────────────────────────────────────────────── */

.esn-header--on-dark .esn-group:nth-child(even) {
  border-left-color: var(--esn-line-dark);
}

/* The column hairline and the reviews card's own rules take the dark-ground
   hairline, the same swap every other border in this block makes. */
.esn-header--on-dark .esn-panel__col {
  border-left-color: var(--esn-line-dark);
}

/* The reviews card as glass on the dark pane: a white tint with the lit top
   edge, which is how every dark pane in this header reads as glass. */
.esn-header--on-dark .esn-reviews--card {
  border-color: var(--esn-line-dark);
  background:
    radial-gradient(
      circle at 100% 0%,
      color-mix(in srgb, var(--brand-500) 14%, transparent),
      transparent 55%
    ),
    linear-gradient(
      160deg,
      color-mix(in srgb, var(--gray-0) 10%, transparent),
      color-mix(in srgb, var(--gray-0) 3%, transparent)
    );
  box-shadow: inset 0 1px 0 var(--esn-edge-dark);
}

/* The footer band sinks on a dark ground rather than lifting: --bg-sunken is
   a light-mode tint and would read as a white slab across the bottom of the
   pane. --esn-sunk-dark is the dark-ground equivalent the plates already use. */
.esn-header--on-dark .esn-panefoot {
  border-top-color: var(--esn-line-dark);
  background: var(--esn-sunk-dark-2);
}

.esn-header--on-dark .esn-reviews__list {
  border-color: var(--esn-line-dark);
}
.esn-header--on-dark .esn-reviews__score,
.esn-header--on-dark .esn-reviews__rating {
  color: var(--text-inverse);
}
/* --gray-400, not --gray-600: the quiet line has to climb on a dark ground,
   not drop. Same value the panel's other secondary copy takes. */
.esn-header--on-dark .esn-reviews__note,
.esn-header--on-dark .esn-reviews__foot,
.esn-header--on-dark .esn-reviews__rating span {
  color: var(--gray-400);
}
.esn-header--on-dark .esn-reviews__stars {
  color: var(--brand-300);
}

.esn-header--on-dark .esn-service b {
  color: var(--text-inverse);
}
.esn-header--on-dark .esn-service > span:not(.esn-service__go) {
  color: var(--gray-400);
}
/* The same pane, tinted the way an interactive surface lifts on this ground:
   white stacked over the panel rather than a darker wash. The lit top edge
   reads here, which is the thing it was always for.

   0.10 at the top is a contrast floor, not a taste call. The row's second
   line is --gray-400, and white over --gray-800 puts it at 4.59:1 at 0.10 and
   4.45:1 at 0.11, so 0.10 is the last step that clears AA. The flat
   --esn-fill-dark this replaces was 0.10 for the same reason, and it only
   reached 4.05:1 because the panel under it was lighter; the opaque
   --gray-800 ground is what buys the half point back. */
.esn-header--on-dark a.esn-service::before {
  border-color: var(--esn-line-dark);
  border-top-color: var(--esn-edge-dark);
  background: linear-gradient(
    160deg,
    rgba(255, 255, 255, 0.1),
    rgba(255, 255, 255, 0.04)
  );
}
/* --gray-500 measured 4.4:1 on the old near-black panel and 3.7:1 on this
   lighter one, so it fails AA either way. A parked row is set apart by being
   one step down from the live rows' white, not by going under the floor:
   --gray-300 is 8.6:1 here and still reads as the quieter of the two. */
.esn-header--on-dark .esn-service.is-soon b {
  color: var(--gray-300);
}
.esn-header--on-dark .esn-service.is-soon > span {
  color: var(--gray-400);
}

/* The watermark sits on a dark ground now, so it takes the brand ink that
   ground uses and can carry a little more opacity without shouting. */
/* The sweep, re-mixed for this ground, and the one place the two themes
   could not share a decision.

   Every orange in the ramp is LIGHTER than --gray-800, so on this pane the
   glow lightens rather than warms, and the row's second line is what pays
   for it. --gray-400 clears AA on the hovered pane by 0.07 of a point
   (4.57:1, the floor the block above was tuned to) BEFORE any glow is laid
   over it, and drops to 3.8:1 under the centre of the blob. So the glow
   keeps its weight and the line goes up one step to --gray-300 on hover
   instead: 5.8:1 at that same centre, and the same "ink brightens under
   the pointer" the light theme gets from --brand-700.

   The label stays white rather than taking the brand ink: --brand-300 under
   the blob is 4.35:1, under AA for 14px semibold, where white is 8.9:1.
   That rule also has to exist to outrank the light theme's --brand-700,
   which carries one pseudo-class more than the base on-dark colour and
   would otherwise win here. */
.esn-header--on-dark a.esn-service::after {
  background: radial-gradient(
    22% 62% at 84% 50%,
    color-mix(in srgb, var(--brand-300) 11%, transparent) 0%,
    color-mix(in srgb, var(--brand-300) 7%, transparent) 38%,
    color-mix(in srgb, var(--brand-300) 2%, transparent) 68%,
    transparent 100%
  );
}

.esn-header--on-dark a.esn-service:hover::before,
.esn-header--on-dark a.esn-service:focus-visible::before {
  border-color: color-mix(in srgb, var(--brand-300) 18%, transparent);
  border-top-color: color-mix(in srgb, var(--brand-200) 22%, transparent);
}

.esn-header--on-dark .esn-service__go {
  color: var(--brand-300);
}

.esn-header--on-dark a.esn-service:hover b,
.esn-header--on-dark a.esn-service:focus-visible b {
  color: var(--text-inverse);
}

.esn-header--on-dark a.esn-service:hover > span:not(.esn-service__go),
.esn-header--on-dark a.esn-service:focus-visible > span:not(.esn-service__go) {
  color: var(--gray-300);
  transition: color var(--duration-normal) var(--ease-out);
}

/* Badge. The same translucent brand wash utilities.css gives `.es-eyebrow` on
   a dark section, so the two chips stay one component. */
.esn-header--on-dark .esn-badge,
.esn-drawer--on-dark .esn-badge {
  border-color: rgba(255, 157, 107, 0.28);
  background: rgba(232, 93, 32, 0.14);
  color: var(--brand-300);
}

.esn-header--on-dark .esn-stats {
  border-top-color: var(--esn-line-dark);
  border-bottom-color: var(--esn-line-dark);
}

.esn-header--on-dark .esn-stats > div + div::before {
  background: var(--esn-line-dark);
}
.esn-header--on-dark .esn-stats dt {
  color: var(--text-inverse);
}
.esn-header--on-dark .esn-stats dd {
  color: var(--gray-400);
}

.esn-header--on-dark .esn-leadlink,
.esn-header--on-dark .esn-panefoot__cta {
  color: var(--gray-300);
}
.esn-header--on-dark .esn-leadlink:hover,
.esn-header--on-dark .esn-panefoot__cta:hover {
  color: var(--brand-300);
}
.esn-header--on-dark .esn-link__icon {
  color: var(--brand-300);
}

.esn-header--on-dark .esn-panefoot {
  border-top-color: var(--esn-line-dark);
}
.esn-header--on-dark .esn-panefoot__meta {
  color: var(--gray-400);
}
.esn-header--on-dark .esn-panefoot__meta b {
  color: var(--text-inverse);
}

/* The featured card and the product plates keep a DARK ground on a dark page.
   On the near-black panel this block used to draw they had to invert to a
   white wash, because a dark card on a dark card is one shape. The panel is
   frosted glass now, so the relationship is the right way up again: the pane
   is the light object, the plate is the well cut into it, and their contents
   were written for a dark surface to begin with. */
.esn-header--on-dark .esn-feature,
.esn-header--on-dark .esn-feature.es-bg-dark {
  background: var(--esn-sunk-dark);
  border: 1px solid var(--esn-line-dark);
}

.esn-header--on-dark .esn-product {
  background: rgba(255, 255, 255, 0.05);
  border-color: var(--esn-line-dark);
  box-shadow: none;
}

.esn-header--on-dark a.esn-product:hover {
  background: var(--esn-fill-dark);
  border-color: rgba(255, 157, 107, 0.32);
}

/* The light washes would read as a white smear on glass; keep a trace. */
.esn-header--on-dark .esn-product::before {
  opacity: 0.12;
}

.esn-header--on-dark .esn-product__plate {
  background: var(--esn-sunk-dark);
}
.esn-header--on-dark .esn-product__plate--logo {
  background: transparent;
}
.esn-header--on-dark .esn-product__name {
  color: var(--text-inverse);
}
.esn-header--on-dark .esn-product__desc {
  color: var(--gray-400);
}
.esn-header--on-dark .esn-product__tag {
  background: var(--esn-fill-dark);
  color: var(--gray-300);
}
.esn-header--on-dark .esn-product--violet .esn-product__tag {
  color: var(--violet-200);
}

.esn-header--on-dark .esn-product__go {
  background: var(--esn-fill-dark);
  box-shadow: none;
  color: var(--text-inverse);
}

.esn-header--on-dark .esn-prodpanel__heading span {
  color: var(--gray-400);
}
.esn-header--on-dark .esn-prodpoint + .esn-prodpoint {
  border-left-color: var(--esn-line-dark);
}
.esn-header--on-dark .esn-prodpoint__icon {
  background: var(--esn-fill-dark);
  color: var(--text-inverse);
}
.esn-header--on-dark .esn-prodpoint__text b {
  color: var(--text-inverse);
}
.esn-header--on-dark .esn-prodpoint__text span {
  color: var(--gray-400);
}

/* ── Mobile drawer ───────────────────────────────────────────────────────
   A sibling of the header, so nav.js mirrors the class onto it rather than it
   inheriting one. Same smoked glass as the panel. */

.esn-drawer--on-dark .esn-acc,
.esn-drawer--on-dark .esn-acclink,
.esn-drawer--on-dark .esn-acc--nested,
.esn-drawer--on-dark .esn-social {
  border-color: var(--esn-line-dark);
}

.esn-drawer--on-dark .esn-acc > summary,
.esn-drawer--on-dark .esn-acclink,
.esn-drawer--on-dark .esn-drawer__label {
  color: var(--text-inverse);
}

.esn-drawer--on-dark .esn-acc--nested > summary,
.esn-drawer--on-dark .esn-acc ul li a,
.esn-drawer--on-dark .esn-contact a {
  color: var(--gray-300);
}

.esn-drawer--on-dark .esn-acc ul li a:hover,
.esn-drawer--on-dark .esn-contact a:hover {
  color: var(--brand-300);
}

.esn-drawer--on-dark .esn-acc ul li .esn-acc-soon {
  color: var(--gray-400);
}
.esn-drawer--on-dark .esn-acc-caret {
  color: var(--gray-400);
}
.esn-drawer--on-dark .esn-acc[open] > summary .esn-acc-caret {
  color: var(--brand-300);
}
.esn-drawer--on-dark .esn-acc > summary::after {
  background: var(--brand-400);
}
.esn-drawer--on-dark .esn-acc__icon,
.esn-drawer--on-dark .esn-contact__icon {
  color: var(--brand-300);
}
.esn-drawer--on-dark .esn-acc ul li a.esn-acc-cta {
  color: var(--gray-300);
}
.esn-drawer--on-dark .esn-acc-cta__arrow {
  color: var(--gray-400);
}
.esn-drawer--on-dark .esn-acc-cta:hover .esn-acc-cta__arrow {
  color: var(--brand-300);
}
.esn-drawer--on-dark .esn-acc-mono {
  background: var(--esn-sunk-dark);
}
.esn-drawer--on-dark .esn-acc-mono--alt {
  background: var(--esn-sunk-dark-2);
}
.esn-drawer--on-dark .esn-acc-prod b {
  color: var(--text-inverse);
}
.esn-drawer--on-dark .esn-acc-prod span {
  color: var(--gray-400);
}

.esn-drawer--on-dark .esn-cta {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.22);
  color: var(--text-inverse);
}

.esn-drawer--on-dark .esn-cta::before {
  background: var(--bg-surface);
}
.esn-drawer--on-dark .esn-cta:hover {
  border-color: var(--bg-surface);
  color: var(--gray-900);
}

/* --brand-500 is 4.36:1 on --gray-900, which clears WCAG 2.2's 3:1 for a focus
   indicator but sits close to the floor on the one surface where the ring
   matters most. --brand-300 is 9.01:1 and is already this ground's brand ink. */
.esn-header--on-dark :focus-visible,
.esn-drawer--on-dark :focus-visible {
  outline-color: var(--brand-300);
}

/* ── Backdrop ────────────────────────────────────────────────────────── */

.esn-backdrop {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: transparent;
  opacity: 0;
  pointer-events: none;
  -webkit-backdrop-filter: blur(8px) saturate(115%);
  backdrop-filter: blur(8px) saturate(115%);
  transition: opacity var(--duration-slow) var(--ease-out);
}

.esn-backdrop.is-on {
  opacity: 1;
  pointer-events: auto;
}

/* ── Mobile drawer ───────────────────────────────────────────────────── */

.esn-drawer {
  position: fixed;
  top: calc(var(--esn-h) + var(--esn-gap));
  left: var(--esn-offset);
  right: var(--esn-offset);
  bottom: var(--esn-offset);
  z-index: 95;
  background: var(--esn-panel);
  border: 1px solid var(--esn-line);
  border-radius: var(--radius-xl);
  box-shadow:
    var(--shadow-xl),
    inset 0 1px 0 var(--esn-edge);
  /* No `backdrop-filter`, for the same reason as `.esn-stage`: the drawer
     shares --esn-panel and that ground is now opaque. */
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  transform: translateY(-12px) scale(0.985);
  transform-origin: 50% 0;
  opacity: 0;
  pointer-events: none;
  transition:
    transform var(--duration-slow) var(--ease-out),
    opacity var(--duration-normal) var(--ease-out);
}

.esn-drawer.is-open {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}

.esn-drawer__inner {
  max-width: 720px;
  margin: 0 auto;
  padding: var(--space-4) var(--space-5) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.esn-acc-wrap {
  display: flex;
  flex-direction: column;
}

.esn-acc,
.esn-acclink {
  border-bottom: 1px solid var(--border-subtle);
}

.esn-acc > summary,
.esn-acclink {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-1);
  font-size: var(--text-md);
  font-weight: var(--font-semibold);
  letter-spacing: var(--tracking-normal);
  color: var(--text-heading);
  text-decoration: none;
  cursor: pointer;
  list-style: none;
}

.esn-acc > summary::-webkit-details-marker {
  display: none;
}

.esn-acc > summary::after {
  content: "";
  position: absolute;
  left: var(--space-1);
  bottom: 10px;
  width: 36px;
  height: 2px;
  border-radius: 1px;
  background: var(--brand-500);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--duration-slow) var(--ease-out);
}

.esn-acc[open] > summary::after {
  transform: scaleX(1);
}

/* Level 2: the Shopify lifecycle groups. Smaller and indented, so the two
   levels never read as the same rank. */
.esn-acc--nested {
  border-bottom: 0;
  border-top: 1px solid var(--border-subtle);
}

.esn-acc[open] > .esn-acc--nested:first-of-type {
  border-top: 0;
}

.esn-acc--nested > summary {
  padding: var(--space-3) var(--space-1) var(--space-3) var(--space-4);
  font-size: var(--fs-body-sm);
  font-weight: var(--font-medium);
  color: var(--text-body);
}

.esn-acc--nested > summary::after {
  left: var(--space-4);
  width: 28px;
}

.esn-acc-caret {
  flex-shrink: 0;
  opacity: 0.6;
  color: var(--text-body);
  transition:
    transform var(--duration-slow) var(--ease-out),
    color var(--duration-normal) var(--ease-out),
    opacity var(--duration-normal) var(--ease-out);
}

.esn-acc[open] > summary .esn-acc-caret {
  transform: rotate(-180deg);
  color: var(--brand-500);
  opacity: 1;
}

/* The nested caret starts pointing right and swings down, so a group reads
   as a branch rather than a second top-level section. */
.esn-acc-caret--sub {
  transform: rotate(-90deg);
  opacity: 0.5;
}
.esn-acc[open] > summary .esn-acc-caret--sub {
  transform: rotate(0deg);
}

.esn-acc ul {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0 var(--space-1) var(--space-4);
  list-style: none;
}

.esn-acc--nested ul {
  padding-left: var(--space-4);
  padding-bottom: var(--space-3);
}

.esn-acc ul li a,
.esn-acc ul li .esn-acc-soon {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 10px 0 10px var(--space-4);
  font-size: var(--fs-body-sm);
  color: var(--text-body);
  text-decoration: none;
  transition: color var(--duration-fast) var(--ease-out);
}

.esn-acc ul li a:hover {
  color: var(--brand-700);
}
.esn-acc ul li .esn-acc-soon {
  color: var(--text-muted);
}

.esn-acc__icon {
  flex-shrink: 0;
  color: var(--brand-500);
  opacity: 0.7;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.esn-acc ul li a:hover .esn-acc__icon {
  opacity: 1;
}

/* Drawer product rows: the same monogram plate at row scale. */
.esn-acc-list--products .esn-acc-product,
.esn-acc-list--products .esn-acc-soon--product {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) 0 var(--space-2) var(--space-4);
}

.esn-acc-mono {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 42px;
  height: 42px;
  border-radius: var(--radius-md);
  background: var(--gray-900);
  color: var(--text-inverse);
  font-size: var(--text-lg);
  /* 700, not 800: the variable font's weight axis stops at 700. */
  font-weight: var(--font-bold);
  font-style: italic;
  line-height: 1;
  letter-spacing: var(--tracking-tight);
}

.esn-acc-mono--alt {
  background: var(--gray-800);
}

.esn-acc-logo {
  display: block;
  flex-shrink: 0;
  width: 42px;
  height: 42px;
  border-radius: var(--radius-md);
  object-fit: contain;
}

.esn-acc-prod {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.esn-acc-prod b {
  font-size: var(--fs-body-sm);
  font-weight: var(--font-semibold);
  color: var(--text-heading);
}

.esn-acc-prod span {
  font-size: var(--fs-caption);
  color: var(--text-muted);
}

.esn-acc ul li a.esn-acc-cta {
  display: inline-flex;
  gap: 5px;
  padding-left: var(--space-4);
  font-size: var(--fs-caption);
  font-style: italic;
  font-weight: var(--font-medium);
  color: var(--text-heading);
  white-space: nowrap;
}

.esn-acc-cta__arrow {
  display: inline-block;
  font-style: normal;
  color: var(--text-body);
  transition:
    transform var(--duration-normal) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.esn-acc-cta:hover .esn-acc-cta__arrow {
  color: inherit; /* the label is already brand-700 on hover */
  transform: translate(2px, -2px);
}

/* ── Drawer rows, mega-menu style (owner, 14 Sep 2026) ──────────────────
   `es_nav_drawer_list( $items, 'go' )`: the name, and the nav's plain arrow
   on the right edge, no glyph. The arrow is always on screen because a phone
   has no hover to reveal it; it keeps the old glyph's brand tint and 0.7
   opacity, so the row weighs what it did. Its right edge sits on the list's
   --space-1 inset, the same line as the accordion carets above it.
   Shopify first; the other sections move over one at a time. */
.esn-acc ul li a.esn-acc-go,
.esn-acc ul li .esn-acc-go {
  justify-content: space-between;
}

.esn-acc-go__arrow {
  flex-shrink: 0;
  color: var(--brand-500);
  opacity: 0.7;
  transition: opacity var(--duration-fast) var(--ease-out);
}

.esn-acc ul li a.esn-acc-go:hover .esn-acc-go__arrow,
.esn-acc ul li a.esn-acc-go:focus-visible .esn-acc-go__arrow {
  opacity: 1;
}

.esn-drawer--on-dark .esn-acc-go__arrow {
  color: var(--brand-300);
}

/* The Shopify panel's case-study card in the drawer: the same component,
   es_nav_case_card(), lined up with the group rows (left on their --space-4
   indent, right on the carets' --space-1 inset). On the desktop stage the
   card is a grid column and the picture fills the height the column has
   left; the drawer has no column height to fill, so the matted screenshot
   takes its own proportions at the drawer's width. */
.esn-acc-feature {
  padding: var(--space-4) var(--space-1) var(--space-4) var(--space-4);
  border-top: 1px solid var(--border-subtle);
}

.esn-feature--drawer .esn-feature__study,
.esn-feature--drawer .esn-feature__shot {
  flex: none;
}

.esn-feature--drawer .esn-feature__shot img {
  height: auto;
}

.esn-drawer--on-dark .esn-acc-feature {
  border-color: var(--esn-line-dark);
}

.esn-drawer--on-dark .esn-feature.es-bg-dark {
  background: var(--esn-sunk-dark);
  border: 1px solid var(--esn-line-dark);
}

/* Drawer footer: block CTA, then the contact block the legacy drawer
   carried. Kept because it is the only place on a phone that surfaces the
   phone number, the inbox and the social profiles. */
.esn-drawer__footer {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding-top: var(--space-4);
}

.esn-contact {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* A <p>, not an <h3>. The header renders before every page's <h1>, so any
   heading element in here lands out of order in the document outline and
   costs the site its Lighthouse accessibility score on all ~200 pages. The
   panels carry role="region" + aria-label instead, which is what actually
   names them for a screen reader. */
.esn-drawer__label {
  margin: 0 0 var(--space-1);
  font-size: var(--fs-small);
  font-weight: var(--font-bold);
  color: var(--text-heading);
}

.esn-contact a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-small);
  color: var(--text-body);
  text-decoration: none;
}

.esn-contact__icon {
  flex-shrink: 0;
  color: var(--brand-500);
}
.esn-contact a:hover {
  color: var(--brand-700);
}

.esn-social {
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-subtle);
}

.esn-social ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

.esn-social ul li a {
  display: inline-flex;
}
.esn-social ul li a img {
  width: 22px;
  height: 22px;
}

/* ── Responsive ──────────────────────────────────────────────────────── */

/* Six nav items now, and "Case Studies" is more than twice the width of the
   "Work" it replaced, so the bar is tighter than it was at every desktop
   width. The two blocks below buy that back by trimming the trigger padding
   and the bar gaps, which are the only slack in the row: the labels, the logo
   and the CTA all have a floor. */
@media (max-width: 1199.98px) {
  .esn-inner {
    gap: var(--space-4);
    padding-left: var(--space-4);
    padding-right: var(--space-3);
  }
  .esn-trigger,
  .esn-navlink {
    padding: 0 10px;
  }
  .esn-products {
    gap: var(--space-3);
  }
  /* Products at 992-1199: the lead column is ~350px, so the heading drops a
     step to keep "Built by Enstacked," on one line, the two proof points
     stack, and the cards give back some padding. */
  .esn-prodpanel {
    grid-template-columns: minmax(0, 1fr) minmax(0, 32rem);
    column-gap: var(--gap-fluid-md);
  }
  .esn-prodpanel__heading {
    font-size: var(--fs-title-sm);
  }
  .esn-prodpoints {
    flex-direction: column;
    gap: var(--space-3);
  }
  .esn-prodpoint + .esn-prodpoint {
    margin-left: 0;
    padding-left: 0;
    border-left: 0;
  }
  .esn-product {
    padding: var(--space-4);
  }
  .esn-grid {
    gap: var(--gap-fluid-md);
  }
  .esn-mega {
    grid-template-columns: minmax(0, 1fr) 250px;
    gap: var(--gap-fluid-md);
  }
  .esn-groups {
    column-gap: var(--space-5);
  }
  .esn-group:nth-child(even) {
    padding-left: var(--space-5);
  }
}

/* The narrowest band the desktop bar has to survive: 992px with six items.
   Nothing is dropped or wrapped here, only tightened. */
@media (max-width: 1099.98px) {
  .esn-inner {
    gap: var(--space-3);
  }
  .esn-list {
    gap: 0;
  }
  .esn-trigger,
  .esn-navlink {
    padding: 0 var(--space-2);
  }
  .esn-brand img {
    width: 128px;
  }
}

/* Drawer takes over at the site's existing nav breakpoint, so the header
   flips at the same width every other component on the site does. */
@media (max-width: 991.98px) {
  .esn-nav,
  .esn-actions .esn-cta {
    display: none;
  }
  .esn-actions .esn-cta--block {
    display: inline-flex;
  }
  .esn-toggle {
    display: inline-flex;
  }
  .esn-stage {
    display: none;
  }

  /* Two items left, so drop the three-column grid. With the nav and the CTA
     both display:none the grid's trailing `auto` track resolves to 0 and the
     hamburger lands a gap-width short of the padding edge; flex puts it
     exactly on it. */
  .esn-inner {
    display: flex;
    justify-content: space-between;
    padding: 0 var(--space-2) 0 var(--space-4);
    gap: var(--space-3);
  }
}

@media (max-width: 575.98px) {
  .esn-inner {
    padding: 0 var(--space-2) 0 var(--space-3);
  }
  .esn-brand img {
    width: 120px;
  }
  .esn-drawer__inner {
    padding: var(--space-3) var(--space-4) var(--space-5);
  }
}

/* ── Motion + focus ──────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .esn-header *,
  .esn-header *::before,
  .esn-header *::after,
  .esn-drawer *,
  .esn-backdrop {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }

  /* The chevron sweep is an infinite animation, so shortening its duration
     would spin it instead of stopping it. utilities.css kills it the same way
     for `.es-btn`. */
  .esn-header .arrow-icon path,
  .esn-drawer .arrow-icon path {
    animation: none !important;
  }
  .esn-header .es-btn:hover {
    transform: none;
  }
}

.esn-trigger:focus-visible,
.esn-navlink:focus-visible,
.esn-cta:focus-visible,
.esn-toggle:focus-visible,
.esn-service:focus-visible,
.esn-ghost:focus-visible,
.esn-feature__study:focus-visible,
.esn-product:focus-visible,
.esn-leadlink:focus-visible,
.esn-panefoot__cta:focus-visible,
.esn-acc > summary:focus-visible,
.esn-acclink:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ── Reviews rail (Company panel) ─────────────────────────────────────────
   Every figure in here is read from `en_review_platforms()` and the headline
   average from `en_review_average()`, so this block cannot disagree with the
   testimonial strips the same source draws on ~15 pages. Nothing is typed. */

.esn-reviews {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

.esn-reviews > .esn-label {
  margin-bottom: calc(-1 * var(--space-4));
}

/* The reviews in one glass container, on the owner's instruction
   (10 Sep 2026). It stretches to the grid's height so its edges line up with
   the panel's other columns.

   It is also the tallest thing in the Company panel (316px against the
   Explore column's 288px of links), so its own box sets the panel's height.
   The bottom padding is removed on the owner's instruction (14 Sep 2026):
   no white space under the "projects delivered" line, and a shorter panel.
   The line's own line box is the only room left under it. The top keeps
   --space-5 so the label still clears the card's lit edge.

   Glass without a blur of its own. The pane behind it already carries a
   `backdrop-filter`, which makes it a backdrop root: a descendant's blur can
   only sample the pane's own contents, not the page, so it would cost a
   compositing layer and show nothing (see the note on --esn-glass-dark). The
   effect is built from what does read on a near-white pane instead: a
   translucent surface falling off on the diagonal, a lit top edge, a hairline
   and a soft shadow, with a faint brand bloom in the top corner like the
   Shopify panel's case-study card. */
.esn-reviews--card {
  align-self: stretch;
  padding: var(--space-5) var(--space-5) 0;
  border: 1px solid var(--esn-line);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(
      circle at 100% 0%,
      color-mix(in srgb, var(--brand-500) 8%, transparent),
      transparent 55%
    ),
    linear-gradient(
      160deg,
      color-mix(in srgb, var(--bg-surface) 96%, transparent),
      color-mix(in srgb, var(--bg-surface) 50%, transparent)
    );
  box-shadow:
    var(--shadow-sm),
    inset 0 1px 0 var(--bg-surface);
}

.esn-reviews__head {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.esn-reviews__stars {
  display: inline-flex;
  gap: 2px;
  color: var(--brand-500);
}

.esn-reviews__star {
  flex-shrink: 0;
}

/* The one big number. --fs-rating is the token the review score already uses
   elsewhere on the site, so the nav does not invent a second display size. */
.esn-reviews__score {
  margin: 0;
  font-size: var(--fs-rating);
  font-weight: var(--font-bold);
  line-height: 1.05;
  letter-spacing: var(--tracking-tight);
  color: var(--text-heading);
  font-variant-numeric: tabular-nums;
}

/* --gray-600, not --text-muted: this is 13px text, AA wants 4.5:1 for it, and
   --text-muted measures 3.82:1 on --bg-surface. Same trade section-hero.php
   documents for its own caption line. */
.esn-reviews__note {
  margin: 0;
  font-size: var(--fs-eyebrow);
  color: var(--gray-600);
}

.esn-reviews__list {
  display: flex;
  flex-direction: column;
  gap: 0;
  margin: 0;
  padding: var(--space-3) 0 var(--space-2);
  border-top: 1px solid var(--border-subtle);
  border-bottom: 1px solid var(--border-subtle);
  list-style: none;
}

.esn-reviews__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  text-decoration: none;
  transition: opacity var(--duration-normal) var(--ease-out);
}

.esn-reviews__row:hover {
  opacity: 0.7;
}

/* Fixed box with `contain`, so five logos drawn at five different aspect
   ratios still line up in one column. Same treatment the testimonial strip's
   badges give them. */
.esn-reviews__logo {
  width: 72px;
  height: 18px;
  object-fit: contain;
  object-position: left center;
  flex-shrink: 0;
}

.esn-reviews__rating {
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  color: var(--text-heading);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The "/5.0" drops back so the score reads first. Written out to one decimal
   on both sides on the owner's instruction, matching the review badges. */
.esn-reviews__rating span {
  font-weight: var(--font-regular);
  color: var(--gray-600);
}

/* The card's --space-4 flex gap is cancelled and replaced with --space-2, so
   the line sits a short, deliberate step under the list's divider. The full
   cancel it had before put the text flush against the rule (0px measured). */
.esn-reviews__foot {
  margin: calc(var(--space-2) - var(--space-4)) 0 0;
  font-size: var(--fs-eyebrow);
  color: var(--gray-600);
}

/* The Insights panel's "Latest" cards (.esn-latest, .esn-insight) were
   removed with their markup on the owner's instruction, 10 Sep 2026. */
