/* NIX — /business/essentials
   ══════════════════════════════════════════════════════════════════════════
   The store for the services we configure. Built on the same tokens as the
   rest of the site — paper, panel, chip, rule, the two weights — so it reads
   as a room in the same house rather than a second shop with its own taste.
   No colour of its own: the site is monochrome by decision, and a price is
   not a reason to break that. The pill's shape is what makes a price read as
   a price.

   THE SHELL IS shell.css's, not this file's. What is here is the panel's
   contents: the window, the two rows of doors, the shelf, and the form on a
   service's own page. The rail, the ring and the grid around them belong to
   the sheet every page with a shell shares — the marketplace learned that the
   expensive way, by rewriting the shell under other names and going without
   everything the shell does.

   FOUR SHAPES, AND EACH ONE MEANS SOMETHING:
     a card   an industry section, or a category: a place to go
     a chip   an industry, or a tag on a service: a filter
     a row    one service: a thing for sale, with its price at the far edge
     the window   one service at the size of the room, so the page opens on
                  something for sale rather than on a list
   ══════════════════════════════════════════════════════════════════════════ */

/* The panel is its own scroller — the app's arrangement, and what lets the
   frame above stay exactly the height of the window. min-height:0 is what
   allows a grid item to shrink below its content far enough to scroll;
   without it the panel grows and pushes the frame open again. */
.es {
  min-height: 0;
}

.es-panel {
  background: var(--nix-panel);
  border-radius: var(--radius-xl);
  padding: var(--shell-pad);
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  /* THE SECTIONS STAND FURTHER APART THAN THE THINGS INSIDE THEM. At
     --space-3 the gap between Industries and Categories was the same 24px as
     the gap between two cards, so the page read as one long list with
     headings in it rather than as three sections. --space-6 is the next step
     that is unmistakably a break. */
  gap: var(--space-6);
  /* The same entrance the store makes, at the same token: a catalogue that
     appears fully formed reads as a page that was already there, which is
     exactly what a drill-in must not look like. */
  animation: es-fade var(--dur-3) var(--ease-out-expo) both;
}

@keyframes es-fade {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

/* NOTHING IN THIS COLUMN SHRINKS, and this is the rule the header bar was
   missing. .es-panel is a flex column with a definite height — it is a grid
   item that scrolls inside itself — and a flex item's default is
   `flex-shrink: 1`. So on a page with a lot in it the browser compresses the
   children BEFORE it lets the panel scroll: the bar lost its margins and part
   of its own height on the front page, while a category page with six rows on
   it had nothing to compress and drew the same bar correctly.
   That is exactly how it was reported — right when you navigate, wrong on All
   and Overview — and it looked like two different bars.

   `flex: 0 0 auto` on every child rather than on the bar alone: the hero's
   min-height and each section's gaps were being paid out of the same budget,
   and fixing the one that was noticed first is how the next one gets
   reported. */
.es-panel > * {
  flex: 0 0 auto;
}

.es-back:focus-visible,
.es-card:focus-visible,
.es-chip:focus-visible,
.es-item:focus-visible,
.es-input:focus-visible,
.es-submit:focus-visible,
.es-add:focus-visible,
.es-dock:focus-visible,
.es-quote__drop:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

/* ── The header: eight categories as a filter ───────────────────────────
   THE SHAPE IS .nix-tagbar's AND IT IS NOT COPIED HERE. That component is the
   header on /business/ai and /agent-base — the pill, the 22px between words,
   the surface and the lift, and the chip on the current item — and this bar
   wears it. Everything that was written here twice is gone with the copy: two
   bars with the same measurements in one app is two bars that drift, and the
   first version of this one had already drifted to a different font size.

   What is left is the two things this page adds. It sticks, and on a phone it
   starts after the ring.

   NOT .nix-sectionbar, which is the other half of that pair. components.css
   hides that class while a drilled rail is open, on the argument that a page
   giving its own bar to the rail is the page that loses the bar — true of a
   table of contents, false here. This rail carries Overview, Industries and
   Services and has never carried the categories, so hiding this would take
   the only filter off the page and put nothing in its place. */
.es-bar {
  position: sticky;
  top: 8px;
  z-index: 30;
}

/* THE BAR CLEARS THE RING ON A PHONE, for the reason components.css spells
   out for the other three: below 720px the rail covers the page, so the ring
   in the corner is the only way into the menu and it was sharing those pixels
   with this bar's first word. The same 56 and 24, and the same subtraction —
   max-width resolves against the containing block and margins are laid
   outside it, so without it the pill comes out its full width and then takes
   80px more. */
@media (max-width: 720px) {
  .es-bar {
    margin-inline: 56px 24px;
    max-width: calc(100% - 56px - 24px);
  }
}

/* ── Headings ───────────────────────────────────────────────────────────── */

/* The page's own name, on the six pages that have no window to carry it. */
.es-title {
  font-size: var(--text-xl);
  font-weight: 300;
  letter-spacing: var(--tracking-tight);
  line-height: 1.15;
}

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

.es-sec__title {
  font-size: var(--text-sm);
  font-weight: 400;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--nix-text-soft);
}

.es-sec__title a { color: inherit; text-decoration: none; }
.es-sec__title a:hover { color: var(--nix-text); }

.es-empty { color: var(--nix-text-soft); font-size: var(--text-base); }
.es-empty a { color: var(--nix-text); }

/* The way back out of a drill-in, in the corner the ring occupies on the
   pages that have one — the same gesture at the same target size, so moving
   between these pages never moves the way out. */
.es-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  padding: 6px 14px 6px 8px;
  border-radius: var(--radius-pill);
  background: var(--chip-tint);
  color: var(--nix-text-soft);
  text-decoration: none;
  font-size: var(--text-sm);
}

.es-back svg { width: 18px; height: 18px; }
.es-back:hover { background: var(--chip-hover); color: var(--nix-text); }

/* ── THE BACK PILL STARTS AFTER THE RING ─────────────────────────────────
   With the rail shut, shell.css parks the menu ring at the panel's top-left —
   absolutely, so it is not in the flow and nothing moves out of its way. This
   pill is the first thing in that flow and was drawn underneath it: one circle
   over one chevron, two controls sharing the same pixels, and a press there a
   coin toss between opening the menu and going back.

   The ring keeps the corner, on every page of this site, and the pill gives up
   the room — the same call components.css makes for the four bars on a phone,
   and for the same reason: the ring is in that corner everywhere and these
   pills are on four pages.

   36px. The ring's own left is 8 + 12 + 12 - 10 = 22 and it is 20 wide, so it
   ends at 42; the panel's padding already pays for 12 of the gap, and this
   puts the chevron at 48 with a thumb's clearance after the circle.

   Only while it IS shut. Open, the rail carries its own way back and the ring
   is not in the panel at all — an indent there would be a pill hanging off
   nothing. */
body.is-shut:not(.is-nav) .es-back { margin-left: 36px; }

/* ── The stage: heading, the apps, the sentence ─────────────────────────
   Three things stacked, and the middle one is the argument. The icons were a
   field BEHIND the copy with a veil over them, which made the one thing on
   the card that answers "what is this" the one thing nobody could quite see.
   They are the middle of the stack now, at full strength.

   The card is a band rather than a screen: a front window is a claim, and a
   claim does not need six hundred pixels to be made. */
.es-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  /* A FLOOR, NOT A FIXED HEIGHT. The card has to be tall enough that the row
     of icons sits in the middle of something rather than between two lines
     that crowd it, and it still has to grow if the heading wraps to three
     lines on a narrow panel. min-height does both; a height does neither. */
  min-height: 520px;
  padding: var(--space-7) var(--space-4);
  border-radius: var(--radius-xl);
  /* THE SURFACE STEP, NOT THE TINT ONE. This is the page's front window and
     it should read as the thing standing FORWARD of the panel; --chip-tint is
     the step used for wells and tiles, which sit back into it. In light that
     is white, and in dark it is the value the same decision resolves to
     there — the token moves with the theme, a literal would not. */
  background: var(--nix-surface);
  overflow: hidden;
}

/* With nothing published there is no row to run, and the two lines stand on
   their own. Not an empty frame: a card with a hole in it reads as a page
   that failed to load. */
.es-stage--bare { min-height: 340px; padding: var(--space-6) var(--space-4); }

.es-stage__copy {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* --space-3, not --space-4: the reel now carries 20px of its own above and
     below, and the two together were a hole between the heading and the row. */
  gap: var(--space-3);
  width: 100%;
  text-align: center;
}

.es-stage__title {
  font-size: var(--text-2xl);
  font-weight: 300;
  letter-spacing: var(--tracking-tight);
  line-height: 1.1;
  max-width: 20ch;
  text-wrap: balance;
}

/* The button sits under the sentence with a step more air than the gap the
   stack uses, because it is the end of the card rather than the next line of
   it — and the same ink-on-pill every other primary control here wears. */
.es-stage__cta {
  margin-top: var(--space-1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 120px;
  padding: 12px 26px;
  border-radius: var(--radius-pill);
  background: var(--nix-ink);
  color: var(--nix-on-ink);
  text-decoration: none;
  font-size: var(--text-base);
}

.es-stage__cta:hover { background: color-mix(in srgb, var(--nix-ink) 88%, var(--nix-paper)); }

.es-stage__cta:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
}

.es-stage__lede {
  max-width: 46ch;
  color: var(--nix-text-soft);
  font-size: var(--text-md);
  line-height: 1.45;
  text-wrap: pretty;
}

/* ── The endless row ────────────────────────────────────────────────────
   THE REEL IS THE WINDOW and the track is what moves through it. The window
   is the full width of the card and fades at both edges, so an icon enters
   and leaves rather than appearing and vanishing at a hard border — which is
   what makes the row read as continuing past the card rather than as ending
   at it. */
.es-stage__reel {
  width: 100%;
  /* THE PADDING IS WHAT SAVES THE SHADOWS. overflow:hidden is the only way to
     clip a track that is wider than the card, and CSS will not let it clip one
     axis and leave the other alone — overflow-y:visible beside an
     overflow-x:hidden is computed back to a scrolling value. So the room the
     lift needs is taken INSIDE the clip: 20px of it, above and below, where
     the tiles' shadows were being sliced flat against the top and bottom
     edges. The mask is horizontal and does not touch them. */
  padding: 20px 0;
  overflow: hidden;
  mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
}

/* HALF, EXACTLY. The markup writes the list twice, so translating the track
   by 50% of its own width lands the second copy's first icon where the first
   copy's was — the only offset at which the reset is invisible. A pixel
   figure here would drift the moment a service is published; a percentage of
   the track cannot. */
.es-stage__track {
  display: flex;
  align-items: center;
  gap: 22px;
  width: max-content;
  /* SLOW. At 64s this crossed the card fast enough to read as a ticker, which
     is a thing you are meant to keep up with; at 150 it is closer to the way a
     shelf turns — you notice it moved rather than watching it move. The
     duration is for the WHOLE track, which is the catalogue twice over, so
     publishing more services makes it slower still and never faster. */
  animation: es-marquee 150s linear infinite;
}

@keyframes es-marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* NOTHING HAPPENS UNDER THE POINTER. It used to hold, on the argument that a
   sliding row cannot be looked at — true at 64 seconds and not at 150, where
   an icon is under the cursor long enough to read without the page reacting.
   A hero that changes because a pointer crossed it is a hero asking to be
   played with, and this one is a claim. */

.es-stage__app {
  flex: 0 0 auto;
  width: 72px;
  height: 72px;
  padding: 11px;
  border-radius: 18px;
  background: var(--nix-surface);
  box-shadow: var(--lift-2);
  object-fit: contain;
}

/* NOBODY WHO ASKED NOT TO BE MOVED GETS MOVED. The track stops where it
   started, which is a legible row of the first icons rather than a frozen
   half-icon — the animation only ever offsets it, so removing the animation
   leaves the markup's own order in place. */
@media (prefers-reduced-motion: reduce) {
  .es-stage__track { animation: none; }
}

@media (max-width: 640px) {
  .es-stage { min-height: 420px; padding: var(--space-6) var(--space-2); }
  .es-stage__title { font-size: var(--text-xl); }
  .es-stage__lede { font-size: var(--text-base); }
  .es-stage__app { width: 56px; height: 56px; padding: 9px; border-radius: 14px; }
  .es-stage__track { gap: 14px; }
}

/* ── One service: the head ──────────────────────────────────────────────
   The App Store's arrangement, because it is the right one here: the mark,
   the name BESIDE it rather than under it, and the one action on the same
   line. A reader knows what they are looking at before reading a word. */
.es-head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-3);
}

.es-head__art {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 104px;
  height: 104px;
  border-radius: var(--radius-lg);
  background: var(--chip-tint);
}

.es-head__icon { width: 76px; height: 76px; object-fit: contain; }

.es-head__text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }

.es-head__name {
  font-size: var(--text-2xl);
  font-weight: 300;
  letter-spacing: var(--tracking-tight);
  line-height: 1.1;
  text-wrap: balance;
}

/* The category, where a subtitle goes and doing a subtitle's job — it says
   what kind of thing this is, and it is the way back to the others like it. */
.es-head__cat {
  color: var(--nix-text-soft);
  font-size: var(--text-md);
  text-decoration: none;
  width: max-content;
}

.es-head__cat:hover { color: var(--nix-text); }

/* ONE ACTION, AND IT SAYS WHICH STATE IT IS IN. The label is written by the
   script; the ink fill is what "already in your quote" looks like, which is
   the same inversion every other current thing on this page uses. */
.es-add {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 22px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--nix-ink);
  color: var(--nix-on-ink);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: 400;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-2) var(--ease-out-expo);
}

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

.es-add:hover { background: color-mix(in srgb, var(--nix-ink) 88%, var(--nix-paper)); }

.es-add.is-on {
  background: var(--chip-tint);
  color: var(--nix-text);
}

.es-add.is-on svg { transform: rotate(45deg); }

/* The trade's own button, beside its heading rather than under it: it is about
   the whole page, and a control that acts on everything belongs on the line
   that names everything. */
.es-head--trade {
  grid-template-columns: 1fr auto;
  align-items: center;
}

/* Add-all never toggles off — a second press does nothing — so it must not
   wear the same fill as the per-service button, which does. The quiet
   treatment says "already done" without offering to undo it. */
.es-add--all.is-on { background: var(--chip-tint); color: var(--nix-text-soft); }
.es-add--all.is-on svg { display: none; }

/* The paragraph the page is built around. One measure, so it reads as prose
   rather than as a wide band of text. */
.es-copy {
  max-width: 68ch;
  color: var(--nix-text-soft);
  font-size: var(--text-md);
  line-height: 1.6;
  white-space: pre-line;
}

/* ── Preview ────────────────────────────────────────────────────────────
   Odoo's own screenshots, in a row that slides. Fixed height and auto width,
   so shots of different proportions line up on their baseline instead of
   stepping up and down the row. */
.es-shots {
  display: flex;
  gap: var(--shell-gap);
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 4px;
}

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

.es-shot {
  flex: 0 0 auto;
  height: 300px;
  width: auto;
  max-width: 92%;
  border-radius: var(--radius-lg);
  background: var(--chip-tint);
  object-fit: cover;
}

.es-shot--video { width: 520px; height: 300px; }

/* ── The basket ─────────────────────────────────────────────────────────
   A pill in the corner of the panel, over whatever is scrolling under it, and
   absent entirely while the basket is empty. */
.es-dock {
  position: fixed;
  right: var(--space-3);
  bottom: var(--space-3);
  z-index: 40;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 12px 20px;
  border-radius: var(--radius-pill);
  background: var(--nix-ink);
  color: var(--nix-on-ink);
  text-decoration: none;
  font-size: var(--text-base);
  box-shadow: var(--lift-3);
}

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

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

.es-dock__count {
  min-width: 22px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: var(--nix-on-ink);
  color: var(--nix-ink);
  font-size: var(--text-xs);
  text-align: center;
}

/* ── The quote page ─────────────────────────────────────────────────────
   The list the script draws, above the form. Rows rather than cards: this is
   a receipt of what you picked, not a shelf to browse again. */
.es-quote__list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: var(--space-2);
}

/* Mark, name, and the way to take it out — the same three-part row the
   shelves use, at the size of a receipt line rather than a shelf item. */
.es-quote__row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-2);
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  background: var(--nix-chip);
}

.es-quote__art {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-sm);
  background: var(--chip-tint);
}

/* CONTAIN and inset, like every other app icon on these pages: these are
   drawings with their own margins and cropping one cuts the drawing. */
.es-quote__icon {
  width: 30px;
  height: 30px;
  object-fit: contain;
}

.es-quote__name { font-size: var(--text-base); }

.es-quote__drop {
  border: 0;
  background: none;
  padding: 4px 8px;
  border-radius: var(--radius-pill);
  color: var(--nix-text-quiet);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  cursor: pointer;
}

.es-quote__drop:hover { color: var(--nix-text); background: var(--chip-tint); }

.es-quote__form[hidden], .es-quote__empty[hidden] { display: none; }

@media (max-width: 640px) {
  .es-head { grid-template-columns: auto 1fr; }
  .es-head__art { width: 72px; height: 72px; }
  .es-head__icon { width: 52px; height: 52px; }
  .es-head__name { font-size: var(--text-xl); }
  /* The button drops under the pair rather than squeezing the name to nothing. */
  .es-add { grid-column: 1 / -1; justify-content: center; }
  .es-shot { height: 220px; }
  .es-shot--video { width: 100%; height: 200px; }
}

/* ── Cards: a place to go ───────────────────────────────────────────────
   The eleven industry sections on the front page, and the eight categories on
   their own. Portrait rather than square: a square tile reads as one more app
   icon, and these are rooms, not products. */
.es-cards {
  display: flex;
  gap: var(--shell-gap);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-bottom: 2px;
}

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

/* THE EDGE FADES ONLY WHEN THERE IS SOMETHING PAST IT. An unconditional mask
   leaves the last card permanently half-faded on the common case where they
   all fit; the class is written by the page's own script after measuring. */
.es-cards.is-scrollable {
  mask-image: linear-gradient(to right, #000 calc(100% - 48px), transparent);
}

/* THE PICTURE IS THE CARD. It used to sit in a 96px well inside one — a
   photograph inside a box inside a row of boxes, three edges deep before you
   reached the image. What is left is the image and its name under it: no
   ground behind the pair, no padding around them, nothing drawn that is not
   either the photograph or the word. */
.es-card {
  flex: 0 0 auto;
  scroll-snap-align: start;
  width: 168px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  color: var(--nix-text);
  text-decoration: none;
}

/* The lift is on the picture, not on the pair. Raising the name with it would
   move a line of type off its own baseline every time a pointer crossed the
   row, which is the sort of motion a page has to earn. */
.es-card:hover .es-card__art {
  transform: translateY(-2px);
  box-shadow: var(--lift-2);
}

/* The card itself now: a 4:3 window with the monogram behind it, which is what
   stands there while the photograph loads and what is left if a section has
   none. overflow:hidden is what makes the picture take these corners rather
   than square them off. The radius steps up from a chip's to a panel's,
   because this stopped being a well inside a card and became the card. */
.es-card__art {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 126px;
  border-radius: var(--radius-lg);
  background: var(--chip-tint);
  font-size: 40px;
  overflow: hidden;
  transition: transform var(--dur-3) var(--ease-out-expo),
              box-shadow var(--dur-3) var(--ease-out-expo);
}

/* COVER, NOT CONTAIN — the opposite of the app icons above. An icon is a
   drawing that must be seen whole; a photograph is a window, and letterboxing
   one inside a tile puts two bands of ground where the picture should be. */
.es-card__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ── The stack: three apps as a category's face ─────────────────────────
   A category has no photograph, so its card shows three of the services
   inside it. Each icon sits on its own small tile with a lift under it, which
   is what makes three overlapping pictures read as three objects rather than
   as one busy image.

   FOUR ARRANGEMENTS, and the template picks by position so a row of eight has
   a rhythm. Every one of them places the same three elements; only the
   transforms differ, so nothing here has to know how many arrangements exist.
   The later icon is the higher one — a stack is built up, not down. */
.es-stack {
  position: absolute;
  inset: 0;
  display: block;
  pointer-events: none;
}

.es-stack__i {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 54px;
  height: 54px;
  padding: 7px;
  border-radius: var(--radius-chip);
  background: var(--nix-surface);
  box-shadow: var(--lift-2);
  object-fit: contain;
  /* The translate(-50%, -50%) is in every rule below rather than here,
     because a transform is one property: a variant that set its own would
     drop the centring and stack all three in the corner. */
}

.es-stack__i--0 { z-index: 1; }
.es-stack__i--1 { z-index: 2; }
.es-stack__i--2 { z-index: 3; }

/* FIVE ARRANGEMENTS. Four over eight categories put the same shape on two
   cards in one row — Finance and Sales were both the flat vertical stack —
   and five shares no factor with eight, so no two cards here repeat.

   Every one places the same three elements and only the transforms differ.
   The centring translate is repeated inside each rule rather than sitting on
   the base: transform is a single property, and a variant that set its own
   would silently drop the centring and pile all three in a corner.

   --lean is what the hover opens. Each rule multiplies its own offsets by it,
   so one property animates five different arrangements and none of them needs
   a second copy of itself for the open state. */
.es-stack { --lean: 1; }

/* Fanned, the way three cards are held. */
.es-stack--fan .es-stack__i--0 { transform: translate(calc(-50% - 34px * var(--lean)), -50%) rotate(calc(-9deg * var(--lean))); }
.es-stack--fan .es-stack__i--1 { transform: translate(-50%, calc(-50% - 4px)); }
.es-stack--fan .es-stack__i--2 { transform: translate(calc(-50% + 34px * var(--lean)), -50%) rotate(calc(9deg * var(--lean))); }

/* Down and to the right, like a pile pushed sideways. */
.es-stack--cascade .es-stack__i--0 { transform: translate(calc(-50% - 26px * var(--lean)), calc(-50% - 18px * var(--lean))); }
.es-stack--cascade .es-stack__i--1 { transform: translate(-50%, -50%); }
.es-stack--cascade .es-stack__i--2 { transform: translate(calc(-50% + 26px * var(--lean)), calc(-50% + 18px * var(--lean))); }

/* One in front, two behind and to the side: a hero with its company. */
.es-stack--corner .es-stack__i--0 { transform: translate(calc(-50% + 30px * var(--lean)), calc(-50% - 20px * var(--lean))) scale(0.78); }
.es-stack--corner .es-stack__i--1 { transform: translate(calc(-50% + 30px * var(--lean)), calc(-50% + 20px * var(--lean))) scale(0.78); }
.es-stack--corner .es-stack__i--2 { transform: translate(calc(-50% - 26px * var(--lean)), -50%) scale(1.08); }

/* One above another, leaning: the flat version of this was the one that read
   as a mistake on Finance and Sales, so the column has a tilt down it. */
.es-stack--column .es-stack__i--0 { transform: translate(calc(-50% - 12px * var(--lean)), calc(-50% - 24px * var(--lean))) rotate(calc(-5deg * var(--lean))); }
.es-stack--column .es-stack__i--1 { transform: translate(-50%, -50%); }
.es-stack--column .es-stack__i--2 { transform: translate(calc(-50% + 12px * var(--lean)), calc(-50% + 24px * var(--lean))) rotate(calc(5deg * var(--lean))); }

/* Two below, one above and between them. */
.es-stack--trio .es-stack__i--0 { transform: translate(calc(-50% - 28px * var(--lean)), calc(-50% + 16px * var(--lean))); }
.es-stack--trio .es-stack__i--1 { transform: translate(calc(-50% + 28px * var(--lean)), calc(-50% + 16px * var(--lean))); }
.es-stack--trio .es-stack__i--2 { transform: translate(-50%, calc(-50% - 18px * var(--lean))) scale(1.04); }

/* THE STACK OPENS UNDER THE POINTER. One number does it: --lean pushes every
   offset and every angle out by a fifth, so the three icons spread and settle
   rather than jumping to a second hand-written layout. --dur-3 with the
   expo-out is the token for a control changing shape.

   THE TRANSITION IS ON THE PIECES, not on the group. Scaling the group would
   take the lift under each tile with it, and a shadow that shrinks stops
   separating the three — which is the whole job those shadows do. */
/* NOBODY WHO ASKED NOT TO BE MOVED GETS MOVED, and the way to say that is to
   gate the rule rather than to write a second one undoing it: an override that
   sets --lean back to the value it already had declares nothing, which is what
   check_css.py fails on and it is right to. */
@media (prefers-reduced-motion: no-preference) {
  .es-stack__i { transition: transform var(--dur-3) var(--ease-out-expo); }
  .es-card:hover .es-stack { --lean: 1.22; }
}

/* Under the picture and on the page's own ground — a caption, not a label on
   a box. Centred would fight the left edge of the image above it. */
.es-card__name {
  font-size: var(--text-base);
  line-height: 1.25;
  text-wrap: balance;
}

/* The band over a section's own page. Short: it is a heading's companion, not
   a hero — the page under it is a list of services, and a full-height picture
   would push the first one off the screen. */
.es-banner {
  height: 180px;
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--chip-tint);
}

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

@media (max-width: 640px) { .es-banner { height: 120px; } }

/* ── Chips: a filter ────────────────────────────────────────────────────
   An industry inside its section, and the tags on a service. Small, wrapped,
   and the same shape wherever they appear — a reader learns one thing. */
.es-chip {
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  background: var(--chip-tint);
  color: var(--nix-text);
  text-decoration: none;
  font-size: var(--text-sm);
}

/* A CHIP THAT CARRIES A MARK IS STILL A CHIP. The icon sits inside the pill at
   the height of the word beside it and the left padding comes in to meet it —
   so a row of industries with icons is the same row, not a row of small cards.
   The modifier rather than .es-chip itself, because the categories are chips
   too and they have no picture. */
.es-chip--tag {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding-left: 7px;
}

/* 20px: the cap height of the label, near enough. Bigger and the pill grows to
   the icon rather than the word, which is what turns a filter into a button. */
.es-chip__icon {
  width: 20px;
  height: 20px;
  border-radius: 5px;
  object-fit: contain;
  flex: 0 0 auto;
}

.es-chip:hover { background: var(--chip-hover); }

/* ── Rows: a thing for sale ─────────────────────────────────────────────
   Two columns on a wide screen, one on a narrow one. Mark, name, one line of
   description, the category it is filed under, and the price held out to the
   right where a scanning eye expects it. */
.es-grid {
  display: grid;
  /* 300, not 340. That width was sized for a service's three lines — name,
     description, category — and an industry row carries one, so the same
     minimum left a column of half-empty rows on a wide panel. */
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: var(--shell-gap);
}

.es-item {
  display: grid;
  grid-template-columns: 64px 1fr auto;
  align-items: center;
  gap: var(--space-2);
  padding: var(--shell-pad);
  border-radius: var(--radius-lg);
  background: var(--nix-chip);
  color: var(--nix-text);
  text-decoration: none;
  transition: background var(--dur-2) var(--ease-out-expo);
}

.es-item:hover { background: var(--chip-hover); }

/* THE MARK IS A STACK: the monogram, and the app's icon over it. The icon is
   what a reader recognises; the monogram is what is there while it loads, and
   what stays if it never does — the img removes itself on error, so nothing
   here has to be hidden or revealed. position:relative is what lets the icon
   cover the letter exactly. */
.es-item__art {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  border-radius: var(--radius-chip);
  background: var(--chip-tint);
  font-size: 26px;
}

/* THE ICON KEEPS ITS OWN SHAPE. Odoo's app icons are square PNGs with their
   own rounded corners drawn in; a second radius on top of that clips the
   artwork, so the tile's radius is on the tile and the icon sits inside it
   with a little air. object-fit because a module that shipped a non-square
   icon must not be stretched into one. */
.es-item__icon {
  position: absolute;
  inset: 8px;
  width: calc(100% - 16px);
  height: calc(100% - 16px);
  object-fit: contain;
}

.es-item__body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-width: 0;
}

.es-item__name { font-size: var(--text-base); }

/* One line, and it stops at one. A description_sale is free text an operator
   typed; three of them at four lines each turn the shelf into a document. */
.es-item__sum {
  font-size: var(--text-sm);
  color: var(--nix-text-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.es-item__cat {
  font-size: var(--text-xs);
  color: var(--nix-text-quiet);
}

/* The price, as a label on the row rather than a second control: the row IS
   the link, and a button inside a link is two targets for one gesture. */
.es-get {
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  background: var(--nix-surface);
  color: var(--nix-text);
  font-size: var(--text-sm);
  white-space: nowrap;
}

.es-form { display: flex; flex-direction: column; gap: var(--shell-gap); }

/* Two boxes side by side above 640px, stacked under it. The pair is a name
   and the pair after it is an address and a company: neither reads as two
   questions when they are on one line. */
.es-form__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--shell-gap);
}

.es-field { display: flex; flex-direction: column; gap: 4px; }

.es-field__label {
  font-size: var(--text-xs);
  color: var(--nix-text-soft);
}

.es-input {
  width: 100%;
  padding: 10px 14px;
  border: 0;
  border-radius: var(--radius-chip);
  background: var(--nix-surface);
  color: var(--nix-text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: 400;
}

.es-input--area { resize: vertical; min-height: 96px; }

.es-input::placeholder { color: var(--nix-text-quiet); }

/* THE HONEYPOT IS HIDDEN FROM EYES, NOT FROM BOTS. display:none is the one
   thing a naive crawler does check for, so this is moved off the canvas
   instead — and aria-hidden with tabindex="-1" on the markup keeps a screen
   reader and a keyboard from ever reaching it. */
.es-pot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.es-submit {
  align-self: flex-start;
  min-width: 120px;
  padding: 12px 24px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--nix-ink);
  color: var(--nix-on-ink);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: 400;
  cursor: pointer;
}

.es-submit:hover { background: color-mix(in srgb, var(--nix-ink) 88%, var(--nix-paper)); }

/* The answer to a submit, in the same box the form is in so the reader's eye
   does not have to travel to find out what happened. */
.es-note {
  padding: 10px 14px;
  border-radius: var(--radius-chip);
  background: var(--nix-surface);
  color: var(--nix-text);
  font-size: var(--text-base);
}

/* Not a red box. The design system has no error colour and inventing one here
   would be the first, on the least important surface in the site; the word
   soft against the same ground is what marks it. */
.es-note--bad { color: var(--nix-text-soft); }

/* ── Narrow ─────────────────────────────────────────────────────────────
   At 900px the page scrolls instead of the panel and the panel grows to its
   contents — the reverse of the desktop arrangement and the honest one on a
   phone. shell.css makes that switch; what is here is what the contents do
   inside it. */
@media (max-width: 900px) {
  .es-panel { overflow-y: visible; }
    }

@media (max-width: 640px) {
  .es-form__row { grid-template-columns: 1fr; }
  .es-grid { grid-template-columns: 1fr; }
  .es-card { width: 140px; }
  .es-card__art { height: 105px; }
  /* Only the tile size, and the offsets follow: they are multiples of
     --lean, not literals restated per breakpoint. */
  .es-stack__i { width: 44px; height: 44px; padding: 6px; }
  .es-stack { --lean: 0.78; }
  .es-card:hover .es-stack { --lean: 0.95; }
}
