/* ============================================================
   COLLECTION PRIMITIVES
   The shared vocabulary behind every filtered listing on the
   site: the blog index today, the case study index next. It is
   deliberately not blog CSS and not case study CSS, because
   both archives are the same page twice over: a head, a
   control row, a filtered grid and a load more button, and the
   second one to want an object should not have copied the
   first.

   What lives here:

     .es-cover      whole-box link
     .es-card       the card shell every listing tile is built on
     .es-panel      the same box without the hover lift
     .es-media      fixed ratio picture frame
     .es-chip       category pill, and the one that sits on art
     .es-archive    the archive page shell and its head
     .es-toolbar    the control row over a collection
     .es-filters    the category rail and its chips
     .es-search     the predictive search field and its results
     .es-field      a labelled control and its live status line
     .es-count      the result line
     .es-empty      the no-results panel
     .es-actions    the row that closes a collection

   Everything is written in semantic tokens. There is no colour
   in this file, so a ground that repaints the tokens repaints
   the components standing on it.

   The container, the section rhythm, the eyebrow chip, the
   buttons and the scroll entrance are already global in
   utilities.css. Nothing here restates them.
   ============================================================ */

/* -- Whole-box link --------------------------------------
   Makes a card clickable through a single anchor: one tab
   stop, real link semantics, and the destination read out
   once rather than three times.

   Anything that has to stay clickable inside the box needs
   `position: relative` and a z-index above 1. */

.es-cover { position: relative; }

.es-cover__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* The covering link takes its colour from the heading around
   it, in both states.

   `color: inherit` on the anchor is not enough on its own.
   style.css carries a site-wide `body a:hover { color:
   var(--text-color) }`, and that selector is one element and
   one pseudo-class deeper than `.es-post__title a` or
   `.es-case__title a`, so it wins the cascade on hover. The
   effect was that a card's heading turned brand-600 exactly as
   written while the anchor inside it, which is the text
   actually on screen, was painted back to near-black: the
   hover state was in the stylesheet and invisible on the page.

   Two classes and a pseudo-class here outrank it without an
   `!important` and without touching the global rule, which
   every legacy page still depends on. `:focus-within` is
   included so a card reached by keyboard reads the same as one
   under a cursor. */
.es-card:hover .es-cover__link,
.es-card:focus-within .es-cover__link {
  color: inherit;
}

/* The focus ring goes on the box, because the box is what the
   visitor sees as the control; the anchor is only a text run
   inside it. It cannot go on the covering pseudo element
   instead: `.es-card` clips its overflow, which would clip the
   ring with it. */
.es-cover:has(.es-cover__link:focus-visible) {
  outline: 2px solid var(--brand-500);
  outline-offset: 3px;
}

.es-cover:has(.es-cover__link:focus-visible) .es-cover__link:focus-visible {
  outline: none;
}

/* -- Card ------------------------------------------------
   The shared shell behind every post card and case card.
   Components style what goes inside it, not the box itself. */

.es-card {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-xl);
  background: var(--bg-surface);
  transition: transform var(--duration-normal) var(--ease-out),
              border-color var(--duration-normal) var(--ease-out),
              box-shadow var(--duration-normal) var(--ease-out);
}

.es-card:hover,
.es-card:focus-within {
  transform: translateY(-4px);
  border-color: var(--brand-200);
  box-shadow: var(--shadow-lg);
}

/* -- Panel -----------------------------------------------
   A plain surface: border, radius, padding, no hover lift.
   The quiet counterpart to `.es-card`, which is a control.

   Children are spaced by the owl rather than each carrying its
   own margin, so a panel can hold any mix of title, copy, list
   and button and the rhythm stays the same. */

.es-panel {
  padding: var(--panel-pad, var(--gap-fluid-sm));
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-xl);
  background: var(--bg-surface);
}

.es-panel > * + * { margin-top: var(--space-4); }

/* -- Media frame -----------------------------------------
   Fixed ratio box so a card never reflows while its image
   decodes. Set the ratio per component with --es-media-ratio. */

.es-media {
  position: relative;
  overflow: hidden;
  aspect-ratio: var(--es-media-ratio, 16 / 10);
  background: var(--bg-sunken);
}

.es-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--duration-slow) var(--ease-out);
}

.es-card:hover .es-media img { transform: scale(1.04); }

@media (prefers-reduced-motion: reduce) {
  .es-card,
  .es-media img { transition: none; }

  .es-card:hover,
  .es-card:focus-within { transform: none; }

  .es-card:hover .es-media img { transform: none; }
}

/* A picture frame with nothing in it. The archive is driven by
   editors, and a post without a featured image should read as a
   card with a quiet ground rather than as a broken one. */
.es-media--empty {
  display: grid;
  place-items: center;
  color: var(--text-muted);
}

.es-media--empty svg {
  width: 34px;
  height: 34px;
  opacity: .45;
}

/* -- Chip ------------------------------------------------
   Category pill. Neutral by default, `--brand` for the one
   that carries emphasis. */

.es-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  background: var(--bg-sunken);
  color: var(--text-body);
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  line-height: 1.2;
  white-space: nowrap;
}

.es-chip--brand {
  border-color: var(--eyebrow-border);
  background: var(--eyebrow-bg);
  color: var(--eyebrow-ink);
}

/* A chip that sits on artwork rather than in a card body: top
   left of the media frame, on its own dark wash so it is
   legible over whatever the picture happens to be there. It
   carries its own ground rather than the theme's translucent
   one, which composited against a light photograph measures
   4.33:1.

   Scoped to the modifier, not to the media frame, so a chip
   placed inline inside a card body is never torn out of the
   flow by a rule meant for this one. */
/* Top right rather than top left. The artwork on this library is a banner
   with its headline set on the left, so a chip in that corner sat on top of
   the words; the right of the frame is the side with room on it. */
.es-chip--over {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  /* Above the covering link's pseudo element, which sits at 1. Without this
     the card's own link swallows the chip and the category is unclickable. */
  z-index: 2;
  max-width: calc(100% - var(--space-4) * 2);
  overflow: hidden;
  border-color: transparent;
  background: color-mix(in srgb, var(--gray-900) 72%, transparent);
  color: var(--gray-0);
  text-decoration: none;
  text-overflow: ellipsis;
  backdrop-filter: blur(6px);
  transition: background-color var(--duration-fast) var(--ease-out);
}

/* A chip that is a link says so on hover, and the ground is what says it
   rather than an underline: the label is already at 12px on artwork and an
   underline at that size on a translucent ground reads as noise. */
a.es-chip--over:hover,
a.es-chip--over:focus-visible {
  background: var(--brand-500);
  color: var(--text-inverse);
}

/* -- Inline link -----------------------------------------
   The quiet "Read" affordance in a card foot. It is not a
   control in its own right on a covered card, which is why the
   markup renders it `aria-hidden`. */

.es-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--brand-600);
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
}

.es-link svg {
  width: 16px;
  height: 14px;
  transition: transform var(--duration-normal) var(--ease-out);
}

.es-card:hover .es-link svg { transform: translateX(4px); }

/* -- Archive shell ---------------------------------------
   The blog index and the case study index are the same page
   twice over. The shell is declared here so that neither owns
   it and each layout keeps its own block name on the same
   elements. */

/* Closes the page, since the last thing in it is a button
   rather than a section with its own padding. */
.es-archive {
  padding-bottom: var(--section-pad);
  background: var(--bg-surface);
}

/* Top padding is the fixed nav clearance every other revamped
   page uses, so the archive head starts where a hero starts.
   The floor is a step down from the section rhythm because the
   head, the toolbar and the first row have to share the first
   screen, and this is the part of that stack that is spacing
   rather than content. */
.es-archive__head {
  padding-block: var(--hero-pad-top) var(--gap-fluid-sm);
}

.es-archive__head h1 {
  max-width: 18ch;
  margin: 0;
  color: var(--text-heading);
  font-size: var(--fs-hero);
  font-weight: var(--font-extrabold);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-tight);
}

.es-archive__head .es-lede {
  max-width: 58ch;
  margin-top: var(--space-5);
}

/* -- Toolbar ---------------------------------------------
   The control row over a collection: the category rail on one
   side, the search field on the other. Stacked below 992px,
   because a rail of chips and a 420px field do not share a
   narrow line. */

.es-toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--gap-fluid-sm);
  margin-bottom: var(--gap-fluid-md);
}

@media (min-width: 992px) {
  .es-toolbar {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--gap-fluid-md);
  }

  /* The rail takes the room that is left and the search keeps
     its own width, rather than the two fighting over the row.
     `min-width: 0` is what lets the rail actually scroll: a
     flex item's automatic minimum size is its content, so
     without this the track is as wide as every chip in it and
     the row pushes the container. */
  .es-toolbar > .es-filters {
    flex: 1 1 auto;
    min-width: 0;
  }

  .es-toolbar > .es-search { flex: 0 0 auto; }
}

/* -- Category rail ---------------------------------------
   The chips are a slider rather than a wrapping block. An
   editor can add a category at any time, and a block that
   wraps grows a row every time they do, pushing the first card
   further down the page; a rail is the same height whether it
   holds four categories or forty.

   The buttons are progressive enhancement. The track is a
   native scroll container and already works with a finger, a
   trackpad and the keyboard, so with no script it is still a
   usable control and the arrows simply never appear.

   The chips themselves are buttons, not links: they change
   what is on the page, they do not navigate. `aria-pressed` is
   what a screen reader reads as the on state. */

.es-filters {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.es-filters__track {
  display: flex;
  gap: var(--space-3);
  overflow-x: auto;
  scroll-behavior: smooth;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Room for the focus ring, which the scroll container would
     otherwise clip on the first and last chip. */
  padding: var(--space-2) var(--space-1);
  margin: calc(var(--space-2) * -1) calc(var(--space-1) * -1);
}

.es-filters__track::-webkit-scrollbar { display: none; }

@media (prefers-reduced-motion: reduce) {
  .es-filters__track { scroll-behavior: auto; }
}

.es-filters__track > * { scroll-snap-align: start; }

/* Scoped to the track, not to `.es-filters`: the arrows are buttons in
   the same box, and a rule written on the wrapper reached them with the
   chip's own padding and squeezed the mark inside to nothing. */
.es-filters__track button {
  flex: 0 0 auto;
  padding: var(--space-2) var(--space-5);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  color: var(--text-body);
  font-family: inherit;
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  line-height: 1.5;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.es-filters__track button:hover {
  border-color: var(--brand-300);
  color: var(--text-heading);
}

.es-filters__track button[aria-pressed="true"] {
  border-color: var(--brand-500);
  background: var(--brand-500);
  color: var(--text-inverse);
}

/* The arrows. 40px rather than 44px because they sit on a
   40px chip line and a taller control would set the height of
   the whole row; they are an enhancement over a track that is
   already operable by finger and keyboard, not the only way
   to reach a category. */
.es-filters__nav {
  display: none;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  color: var(--text-heading);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              opacity var(--duration-fast) var(--ease-out);
}

/* Shown only once the script has measured that the track has
   somewhere to scroll to. */
.es-filters--scrollable .es-filters__nav { display: inline-flex; }

.es-filters__nav:hover:not(:disabled) {
  border-color: var(--brand-500);
  color: var(--brand-600);
}

.es-filters__nav:disabled {
  opacity: .35;
  cursor: default;
}

.es-filters__nav svg {
  width: 18px;
  height: 18px;
}

.es-filters__nav--prev svg { transform: rotate(180deg); }

/* -- Search ----------------------------------------------
   A real search form: method GET, a real submit, and a real
   destination. With no script it posts to the site search and
   the page it lands on is the one WordPress already renders.
   The script upgrades the same field into a combobox that
   answers as you type. */

.es-search {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
  max-width: 420px;
}

.es-search__icon {
  position: absolute;
  left: var(--space-5);
  width: 18px;
  height: 18px;
  color: var(--text-muted);
  pointer-events: none;
}

/* 13px of vertical padding on a 14px field gives a 48px
   control, which clears the 44px touch target floor. */
.es-search input[type="search"] {
  width: 100%;
  /* Gutter, icon, gutter again on the left; room for the clear
     control on the right. */
  padding: 13px calc(var(--space-5) * 2 + 18px) 13px calc(var(--space-5) * 2 + 18px);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  color: var(--text-heading);
  font-family: inherit;
  font-size: var(--fs-small);
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}

.es-search input[type="search"]::placeholder { color: var(--text-muted); }

.es-search input[type="search"]:focus-visible {
  outline: none;
  border-color: var(--brand-500);
  box-shadow: var(--focus-ring);
}

/* The engines disagree about what the native control looks
   like and whether it can be styled at all, so the field ships
   without it and the button below is the reset. */
.es-search input[type="search"]::-webkit-search-cancel-button { display: none; }

/* One control in the right gutter, and it is the submit until
   the script arrives. Without a script it has to be a submit,
   because that is the only way the field goes anywhere; with
   one it becomes the clear control, because the results are
   already on the page by then. */
.es-search__action {
  position: absolute;
  right: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.es-search__action:hover {
  background: var(--bg-sunken);
  color: var(--text-heading);
}

.es-search__action svg { width: 16px; height: 16px; }

/* The two marks are wrapped so the script can swap them with the `hidden`
   property rather than build an SVG at runtime. Flex, so the wrapper has no
   baseline of its own and the mark sits centred in the disc. */
.es-search__action > [data-es-search-icon] { display: inline-flex; }

/* -- The collapsed search --------------------------------
   The field is a 40px circular button at the end of the row
   until it is clicked or focused, and the category rail takes
   everything that is left. When clicked, it expands smoothly
   towards the left, revealing the full search input. */

.es-toolbar.es-toolbar--compact {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--space-3);
  position: relative;
}

/* The rail takes all remaining room. min-width: 0 is what lets
   it scroll rather than push the row. */
.es-toolbar.es-toolbar--compact > .es-filters {
  flex: 1 1 auto;
  min-width: 0;
  transition: flex 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Collapsed circular search button on the right */
.es-toolbar.es-toolbar--compact > .es-search {
  position: relative;
  display: flex;
  align-items: center;
  flex: 0 0 40px;
  width: 40px;
  height: 40px;
  min-width: 40px;
  max-width: none;
  padding: 0;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  box-sizing: border-box;
  overflow: hidden;
  cursor: pointer;
  transition: width 0.35s cubic-bezier(0.16, 1, 0.3, 1),
              flex-basis 0.35s cubic-bezier(0.16, 1, 0.3, 1),
              border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}

.es-toolbar.es-toolbar--compact > .es-search:hover {
  border-color: var(--brand-500);
  background: var(--bg-sunken);
}

.es-toolbar.es-toolbar--compact > .es-search:hover .es-search__icon {
  color: var(--brand-600);
}

/* Centered search icon in collapsed 40px circle */
.es-toolbar.es-toolbar--compact .es-search__icon {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 18px;
  color: var(--text-heading);
  pointer-events: none;
  flex-shrink: 0;
  transition: left 0.35s cubic-bezier(0.16, 1, 0.3, 1),
              color var(--duration-fast) var(--ease-out);
  z-index: 2;
}

/* Input reset for compact container: hidden when collapsed, full width when expanded */
.es-toolbar.es-toolbar--compact .es-search input[type="search"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0 38px 0 38px;
  border: 0;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--text-heading);
  font-family: inherit;
  font-size: var(--fs-small);
  opacity: 0;
  pointer-events: none;
  cursor: pointer;
  transition: opacity 0.2s ease;
  box-sizing: border-box;
}

.es-toolbar.es-toolbar--compact .es-search input[type="search"]:focus,
.es-toolbar.es-toolbar--compact .es-search input[type="search"]:focus-visible {
  outline: none;
  box-shadow: none;
  border: 0;
}

.es-toolbar.es-toolbar--compact .es-search input[type="search"]::-webkit-search-cancel-button {
  display: none;
}

/* Action / close button on the right inside expanded search */
.es-toolbar.es-toolbar--compact .es-search__action {
  position: absolute;
  right: 3px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
  z-index: 3;
}

.es-toolbar.es-toolbar--compact .es-search__action:hover {
  background: var(--bg-sunken);
  color: var(--text-heading);
}

.es-toolbar.es-toolbar--compact .es-search__action svg {
  width: 16px;
  height: 16px;
}

.es-toolbar.es-toolbar--compact .es-search__action > [data-es-search-icon] {
  display: inline-flex;
}

/* Expanded state: when active, clicked, focused, or holding search query */
.es-toolbar.es-toolbar--compact > .es-search.is-expanded,
.es-toolbar.es-toolbar--compact > .es-search.is-open,
.es-toolbar.es-toolbar--compact > .es-search:focus-within {
  flex: 0 0 380px;
  width: 380px;
  border-color: var(--brand-500);
  box-shadow: var(--focus-ring);
  background: var(--bg-surface);
  cursor: auto;
  overflow: visible;
}

.es-toolbar.es-toolbar--compact > .es-search.is-expanded .es-search__icon,
.es-toolbar.es-toolbar--compact > .es-search.is-open .es-search__icon,
.es-toolbar.es-toolbar--compact > .es-search:focus-within .es-search__icon {
  left: 12px;
  color: var(--text-muted);
}

.es-toolbar.es-toolbar--compact > .es-search.is-expanded input[type="search"],
.es-toolbar.es-toolbar--compact > .es-search.is-open input[type="search"],
.es-toolbar.es-toolbar--compact > .es-search:focus-within input[type="search"] {
  opacity: 1;
  pointer-events: auto;
  cursor: text;
}

.es-toolbar.es-toolbar--compact > .es-search.is-expanded .es-search__action,
.es-toolbar.es-toolbar--compact > .es-search.is-open .es-search__action,
.es-toolbar.es-toolbar--compact > .es-search:focus-within .es-search__action {
  opacity: 1;
  pointer-events: auto;
}

@media (max-width: 991.98px) {
  .es-toolbar.es-toolbar--compact > .es-search.is-expanded,
  .es-toolbar.es-toolbar--compact > .es-search.is-open,
  .es-toolbar.es-toolbar--compact > .es-search:focus-within {
    flex: 0 1 min(100%, 320px);
    width: min(100%, 320px);
  }
}

@media (max-width: 575.98px) {
  .es-toolbar.es-toolbar--compact > .es-search.is-expanded,
  .es-toolbar.es-toolbar--compact > .es-search.is-open,
  .es-toolbar.es-toolbar--compact > .es-search:focus-within {
    flex: 1 1 100%;
    width: 100%;
  }

  .es-toolbar.es-toolbar--compact:has(.es-search.is-expanded) > .es-filters,
  .es-toolbar.es-toolbar--compact:has(.es-search.is-open) > .es-filters,
  .es-toolbar.es-toolbar--compact:has(.es-search:focus-within) > .es-filters {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .es-toolbar.es-toolbar--compact > .es-search,
  .es-toolbar.es-toolbar--compact .es-search__icon,
  .es-toolbar.es-toolbar--compact .es-search__action {
    transition: none;
  }
}

/* -- Predictive results ----------------------------------
   The listbox the combobox opens. It is built by the script
   and never rendered on the server, so a page without
   JavaScript has no empty popup sitting in the document.

   Absolutely positioned rather than in flow: the toolbar is a
   flex row on desktop and a panel that pushed the grid down
   every time somebody typed would move the page under the
   pointer. */

.es-search__panel {
  position: absolute;
  z-index: 40;
  top: calc(100% + var(--space-3));
  left: 0;
  right: 0;
  max-height: min(60vh, 420px);
  overflow-y: auto;
  padding: var(--space-3);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
  box-shadow: var(--shadow-lg);
}

.es-search__panel[hidden] { display: none; }

.es-suggest {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  width: 100%;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  color: var(--text-heading);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

/* Hover and keyboard land on the same treatment, so the list
   never shows two different "current" rows at once. The script
   moves `aria-selected` with the arrow keys and leaves focus in
   the field, which is what a combobox is supposed to do. */
.es-suggest:hover,
.es-suggest[aria-selected="true"] {
  background: var(--bg-sunken);
}

.es-suggest__thumb {
  flex: 0 0 auto;
  width: 52px;
  height: 40px;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--bg-sunken);
}

.es-suggest__thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.es-suggest__text {
  min-width: 0;
  display: grid;
  gap: 2px;
}

.es-suggest__title {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  font-size: var(--fs-small);
  font-weight: var(--font-semibold);
  line-height: var(--lh-subhead);
}

/* The matched run, marked up as a `<mark>` so the emphasis is
   in the document and not only in the paint. Weight as well as
   ground, so it is not carried by colour alone. */
.es-suggest__title mark {
  background: var(--eyebrow-bg);
  color: var(--eyebrow-ink);
  font-weight: var(--font-bold);
}

.es-suggest__meta {
  overflow: hidden;
  color: var(--text-muted);
  font-size: var(--fs-caption);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The row that closes the list: everything the field found, handed to the
   archive under it.

   It is a `<button>`, so it arrives carrying the theme's own button styling:
   Bootstrap's reboot plus the site's border and radius rules drew it as an
   outlined pill spanning the panel, which is a second call to action at the
   foot of a list of results. Every one of those is reset here rather than
   overridden piecemeal, and what is left is a row that matches the
   suggestions above it. */
.es-search__all {
  display: block;
  width: 100%;
  margin: var(--space-2) 0 0;
  padding: var(--space-4) var(--space-3);
  border: 0;
  border-top: 1px solid var(--border-subtle);
  border-radius: 0;
  background: none;
  box-shadow: none;
  color: var(--brand-600);
  font: inherit;
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  line-height: 1.4;
  text-align: left;
  text-decoration: none;
  text-transform: none;
  letter-spacing: normal;
  cursor: pointer;
  appearance: none;
}

.es-search__all:hover,
.es-search__all:focus-visible {
  background: var(--bg-sunken);
  color: var(--brand-700);
  text-decoration: none;
}

.es-search__note {
  padding: var(--space-4) var(--space-3);
  color: var(--text-muted);
  font-size: var(--fs-caption);
}

/* -- Form field -------------------------------------------
   A labelled control and the live line that answers it.
   Shared, because the audit request on the blog promo and the
   next contact form are the same object.

   Every colour is indirected, so the field also works on a
   dark or brand ground: such a ground repoints the tokens and
   nothing here is restated there. The ring is its own token
   for that reason, since brand-500 on a brand panel is the
   accent on its own darker self. */

.es-field { display: grid; gap: var(--space-2); }

.es-field > label {
  color: var(--text-heading);
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
}

.es-field input {
  width: 100%;
  /* 12px on a 20px line box is a 46px control, clear of the
     44px touch target floor. */
  padding: 12px var(--space-5);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  background: var(--bg-sunken);
  color: var(--text-heading);
  font: inherit;
  font-size: var(--fs-small);
  transition: border-color var(--duration-fast) var(--ease-out);
}

.es-field input::placeholder { color: var(--text-muted); }

.es-field input:hover { border-color: var(--border-default); }

/* Lifts off the recessed ground onto the card's own as it takes focus, so the
   field the visitor is in is the one lit rather than the one merely ringed. */
.es-field input:focus-visible {
  outline: 2px solid var(--field-ring, var(--brand-500));
  outline-offset: 2px;
  border-color: transparent;
  background: var(--bg-surface);
}

.es-field input[aria-invalid="true"] { border-color: var(--error-text); }

/* Out of the flow until there is something to report, on the
   owner's instruction: the line used to hold a reserved 20px
   whether or not it had anything to say. The trade is that the
   button moves down by a line the first time a field is wrong.

   It stays a `role="status"` live region, and the error is not
   carried by this line alone: the script also marks the field
   `aria-invalid` and moves focus to it, so the problem is
   announced whether or not the region is picked up. */
.es-form-status {
  display: none;
  margin: 0;
  font-size: var(--fs-caption);
  line-height: 1.4;
}

.es-form-status[data-state="busy"],
.es-form-status[data-state="error"],
.es-form-status[data-state="success"] { display: block; }

/* In flight. Muted, because it is a note about the request rather than an
   answer to it, and it is on screen for as long as the network takes. */
.es-form-status[data-state="busy"] { color: var(--text-muted); }

/* Weight as well as colour, so the one line that reports a
   problem is not reporting it by hue alone. */
.es-form-status[data-state="error"] {
  color: var(--error-text);
  font-weight: var(--font-semibold);
}

.es-form-status[data-state="success"] { color: var(--success-text); }

/* -- Count, empty state and actions -----------------------  */

/* A live region, so it keeps its line whether or not it has
   anything to say. Without the reserved height the grid below
   jumps up by a line the first time a filter is used. */
.es-count {
  min-height: 20px;
  margin: 0 0 var(--space-5);
  color: var(--text-muted);
  font-size: var(--fs-caption);
}

.es-empty {
  padding: var(--gap-fluid-xl) var(--gap-fluid-sm);
  border: 1px dashed var(--border-default);
  border-radius: var(--radius-xl);
  color: var(--text-muted);
  text-align: center;
}

.es-empty[hidden] { display: none; }

.es-empty strong {
  display: block;
  margin-bottom: var(--space-2);
  color: var(--text-heading);
  font-size: var(--fs-card-title);
}

.es-empty p { margin: 0; }

/* The row that closes a collection, holding the load more
   control. Centred, because it is the one thing on its line. */
.es-actions {
  display: flex;
  justify-content: center;
  margin-top: var(--gap-fluid-lg);
}

.es-actions[hidden] { display: none; }

/* -- Pagination -------------------------------------------
   Real links on first paint, so the whole archive is
   reachable and crawlable with no script at all. The script
   intercepts the clicks and redraws this row in place; where
   a category or a search is on there is no URL for page three
   of that, so it draws buttons instead of links and says so
   by rendering a different element, not by styling one.

   Both elements take the same rules, which is why every
   selector here names the class rather than the tag. */

.es-pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

.es-page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 40px square, so a page number and an arrow are the same
     control at the same size and the row does not step. */
  min-width: 40px;
  height: 40px;
  padding: 0 var(--space-3);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-full);
  background: var(--bg-surface);
  color: var(--text-body);
  font-family: inherit;
  font-size: var(--fs-caption);
  font-weight: var(--font-semibold);
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

.es-page:hover,
.es-page:focus-visible {
  border-color: var(--brand-300);
  color: var(--text-heading);
  text-decoration: none;
}

/* The page you are on is not a control, so it is a `<span>` in
   the markup and reads as a state here: filled, and carrying
   `aria-current="page"` for anything not looking at it. */
.es-page.is-current {
  border-color: var(--brand-500);
  background: var(--brand-500);
  color: var(--text-inverse);
  cursor: default;
}

/* The gap between two runs of numbers. Not a control either,
   and not a border: a bordered pill holding an ellipsis reads
   as a page you can go to. */
.es-page--gap {
  min-width: 0;
  border-color: transparent;
  background: none;
  color: var(--text-muted);
  cursor: default;
}

.es-page__icon { width: 16px; height: 16px; }

.es-page--prev .es-page__icon { transform: rotate(180deg); }

.es-page[disabled],
.es-page[aria-disabled="true"] {
  opacity: .35;
  cursor: default;
}

/* On a phone a middle page's row is nine pills, which is two
   lines of them. Two lines is the right answer: the row wraps,
   and every page it was going to offer is still offered. Hiding
   the numbers instead leaves a current page, a gap and an arrow,
   which is a control that has lost the thing it controls. */

/* -- Busy state ------------------------------------------
   While a filter or a page is in flight the grid stays where
   it is and dims, rather than collapsing to a spinner. The
   page keeps its height, so nothing under the pointer moves
   and the scroll position survives the swap. */

[data-es-busy="true"] {
  opacity: .45;
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--ease-out);
}
