/* builder_capabilities: carousel */
/* CSS OWNS CAROUSEL LAYOUT — BEFORE AND AFTER HYDRATION.
   These rules are deliberately NOT gated on [data-ch-widget-mounted]. The host
   is the scroll track in both states, so the browser sizes the slides once and
   the same geometry survives the handoff. JS never writes a width.

   That is the whole point of the design: previously the pre-paint used these
   @container bands while the mounted carousel re-derived a count in JS from a
   measured box, and the two disagreed (different boxes, and react-multi-carousel
   only re-read its `responsive` prop at mount / window-resize, so a container
   change couldn't re-apply without a forced remount). Now there is ONE source of
   truth for column widths and it is this stylesheet.

   Below 620px: always 1 slide. Author's mobile count is intentionally ignored —
   sub-620 containers can't show >1 card without each becoming illegible.
   Hero variants are excluded; they use a plain block layout below.

   SPECIFICITY — READ BEFORE EDITING. This block is deliberately NOT wrapped in
   `:where()`. The doubled attribute selector is (0,4,0), which clears both
   competing sources in one move:
     * the global reset's `body div { margin: 0; padding: 0 }` (0,0,2)
       — _ui/lib/css/reset.css;
     * per-module block CSS, scoped by scopeBuilderCss to
       `#builder-module-<id> .rsg-carousel` (1,1,0) — ListingCarousel 2122 alone
       ships `padding:0`, `gap:clamp(11px,1.75vw,22px)` and
       `@media(min-width:881px){overflow-x:visible}`, every one of which broke a
       different part of this layout when it won.
   These properties are STRUCTURAL: the host is a scroll-snap track whose child
   widths are computed by the @container bands below, and each of the three
   authored overrides above silently desynced that computation. Authors tune the
   carousel through `--bv-carousel-gap` / `--bv-carousel-*` (which feed the calcs
   consistently), not by restyling the track.
   Cosmetics are NOT claimed here — background, border, padding-block, colour and
   so on stay authorable at normal specificity. */
[data-ch-widget="carousel"]:not([data-ch-carousel-type="hero"]):not([data-ch-widget-mounted="never"]) {
    /* `container-type: inline-size` applies `contain: … inline-size`, which
       computes this element's inline size AS IF IT HAD NO CONTENTS. That is what
       makes the bands safe: a 20-slide flex line is ~6000px wide, but its
       intrinsic contribution is zeroed, so it can never inflate the queried box —
       it simply overflows and scrolls. (Worst case under hostile authored CSS is
       "too narrow", never 6000px.)
       NOTE an element can never query its OWN container, so the bands below size
       the SLIDES (descendants), not this element. */
    container-type: inline-size;
    display: flex;
    flex-wrap: nowrap;
    /* The band calc()s below subtract exactly this value to size each slide, so
       the rendered gap and the gap they assume MUST be the same number. Left at
       `:where()` (0,0,0) an authored `gap` (e.g. ListingCarousel's
       `clamp(11px,1.75vw,22px)`) silently replaced the spacing WITHOUT changing
       what the calc subtracted, and the slides overflowed the track by the
       difference — measured 11px, which clipped the last visible card.
       Authors who want a different gap set `--bv-carousel-gap`; that keeps the
       two in agreement by construction. */
    gap: var(--bv-carousel-gap, 16px) !important;
    /* Native scroll, not overflow:hidden + transforms. A scroll container's
       automatic minimum size is 0, a second independent guarantee against
       content-driven inflation — and it gives us free momentum/touch scrolling,
       so the widget needs no drag implementation. Mirrors the shape
       ListEjectBase.php ships for every ejected slider.

       `!important` on the x axis is load-bearing. Block CSS is scoped per module
       (scopeBuilderCss → `#builder-module-<id>`), so an authored `.rsg-carousel`
       rule is (1,1,0) and outranks this `:where()` (0,0,0). ListingCarousel 2122
       ships `@media (min-width:881px){ .rsg-carousel{ overflow-x: visible } }` —
       correct when the host was a static row, fatal now that the host IS the
       scroller, because a non-scrollable track can't be paged by the arrows.
       (It currently survives only by accident: `overflow-y: hidden` forces the
       x axis to compute as `auto`. Don't depend on that side effect.) */
    overflow-x: auto !important;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}
:where([data-ch-widget="carousel"]:not([data-ch-carousel-type="hero"]))::-webkit-scrollbar {
    display: none;
}
/* NO ARROW GUTTER HERE — deliberately.

   This used to carry an unconditional
     @media (min-width:800px){ [data-ch-widget="carousel"]:not([type=hero]) { margin-inline:46px } }
   to keep the old absolutely-positioned overlay arrows off the first and last
   cards. It was right about the mechanism (margin, not padding: a scroll
   container's inline padding scrolls with its content and cannot hold a gutter)
   and wrong about the scope — it inset EVERY non-hero carousel, including
   single-slide ones that never built arrows at all.

   Moot now: those overlay arrows are gone entirely, replaced by the
   `.ch-pgbar` chrome bar that sits BELOW the track and costs it no width.
   Reserving inline space here would still be actively harmful: the host carries
   `container-type: inline-size` and the slide-count bands below query it, so a
   gutter would shrink the queried box and could drop a carousel a whole band
   (cost it a column). Nothing to reserve here. */
/* Default (mobile / unqueried): one slide fills the container.
   `:not(.chc-carousel)` excludes the static runtime's mounted SHELL. That
   runtime reparents every slide under a single `.chc-carousel` wrapper, after
   which `> *` matches THAT WRAPPER instead of the slides these rules are
   written for — so the wrapper wore the slide's `flex` basis and, worse, its
   `content-visibility`/`contain-intrinsic-size` (skipping the whole carousel's
   layout and reporting the 400px placeholder as its height). runtime.js already
   re-asserts both axes on `.chc-carousel` and wins on specificity; this guard
   is the belt to that pair of braces, so a FUTURE mount path that reparents
   slides can't silently inherit slide styling again. */
:where([data-ch-widget="carousel"]:not([data-ch-carousel-type="hero"])) > *:not(.chc-carousel) {
    flex: 0 0 100%;
    min-width: 0;
    scroll-snap-align: start;
    /* MEMORY hint for long carousels: skip style/layout/paint/decode for slides
       outside the viewport. Added while chasing an iOS Safari OOM tab-kill on a
       99-card listing carousel (mclaneresidential.com).
       ⚠ MEASURED, so nobody re-litigates it: this does NOT reduce image FETCHES.
       An A/B on a faithful 99-slide replica loaded 5/99 images WITH it and 5/99
       WITHOUT — `loading="lazy"` already caps horizontal overflow on its own, so
       the "all slides share a vertical band, therefore all images load" theory
       is wrong at least in Chromium. Kept because it is provably free (geometry
       verified byte-identical: scrollWidth 40178 = 99×390 + 98×16, all slide
       widths equal, flex-basis untouched) and may still cut decode/layout cost
       on Safari, which could not be tested here. It is NOT the fix — the fetch
       clamp in WebsitePage::listingFetch is.
       Slides stay in the DOM with a real `src`, so crawlability and the
       pre-paint-matches-hydrated geometry contract this file protects are both
       unchanged. Virtualization would break that contract (server and client DOM
       would differ, and `track.scrollWidth`, which the arrow/dot math reads,
       would shift as you scroll). The inline axis stays owned by the `flex: 0 0 …`
       bands; `contain-intrinsic-size: auto` only supplies a placeholder for the
       skipped axis. Safe against the @container bands: `container-type:
       inline-size` above already zeroes the flex line's intrinsic contribution.

       ⚠ THE PLACEHOLDER SETS THE CAROUSEL'S HEIGHT — it is not "inert for
       layout". Slides are flex items in a nowrap row, so EVERY slide feeds the
       line's cross-size, and a SKIPPED slide feeds the placeholder instead of
       its real height. One 400px placeholder therefore sized the whole
       carousel. Measured on teamprice.com /cities/austin-tx/houses: 71px
       subdivision cards, slides 1-3 onscreen at 71px, slides 4+ skipped at
       400px → host 410px, ~340px of dead space under a one-line card.

       Fixed in two halves, and BOTH are load-bearing:

       1. `auto none` on the skipped slides, so a skipped slide contributes
          nothing and the tallest REAL slide wins.
       2. `content-visibility: visible` on the first 4 (`--bv-carousel-large`,
          the most that are ever visible at once), so at least one slide always
          lays out for real.

       Half 1 alone is WRONG and was measured wrong before this comment existed:
       with every slide skipped, a carousel that is offscreen on a COLD load has
       no remembered size for `auto` to reuse, so all slides report nothing and
       the carousel collapses to 0 — verified on the teamprice.com homepage
       listing carousel (380px cards, 0px host), which is a scroll-jump/CLS bug
       traded for the blank-space one. Pinning the leading slides gives `auto`
       a real height to report at all times: 380px cold and warm, on and off
       screen. The memory win is kept where it matters — a 99-card carousel
       still skips 95. */
    content-visibility: auto;
    contain-intrinsic-size: auto none;
}
/* See the two-halves note above: the leading slides must always lay out, or an
   offscreen carousel has no size on a cold load.

   -n+4 tracks the largest `--bv-carousel-*` count. Surveyed 2026-08-12 across
   every website_section_blocks + website_page_modules row: the max anywhere is
   4 (`--bv-carousel-large:4` / `data-ch-carousel-items="1,2,3,4"`), so 4 covers
   every carousel that ships today with no slide to spare. IF A WIDER BAND IS
   EVER ADDED, RAISE THIS TO MATCH — a 5-up carousel would skip its 5th visible
   slide, and if that slide is the tallest the carousel shrinks under its own
   content. Keep it >= the largest count, never below. */
:where([data-ch-widget="carousel"]:not([data-ch-carousel-type="hero"])) > *:not(.chc-carousel):nth-child(-n+4) {
    content-visibility: visible;
}
@container (min-width: 620px) {
    :where([data-ch-widget="carousel"]:not([data-ch-carousel-type="hero"])) > *:not(.chc-carousel) {
        flex: 0 0 calc((100% - var(--bv-carousel-gap, 16px) * (var(--bv-carousel-tablet, 2) - 1)) / var(--bv-carousel-tablet, 2));
    }
}
/* @container, NOT @media — and 801 / 1201, not 900 / 1200.
   Why the container: every builder carousel sits inside a horizontal wall
   (--ch-wall-horiz-width, default 1200px) plus padding, or an authored
   max-width, so the container is ALWAYS narrower than the viewport. Measuring
   the viewport picked a band the carousel had no room for — an 885px viewport
   chose the 3-up desktop band for a 728px container, giving 243px cards.
   With a default wall the container never exceeds 1200, so the wide slot is
   unreachable and a default site settles at the desktop count; widening the wall
   past 1200 is what earns the 4th column.
   (The bands used to read 900, which made 801-900px paint the TABLET count and
   then snap to desktop on hydration — same class of mismatch, viewport axis.) */
@container (min-width: 801px) {
    :where([data-ch-widget="carousel"]:not([data-ch-carousel-type="hero"])) > *:not(.chc-carousel) {
        flex: 0 0 calc((100% - var(--bv-carousel-gap, 16px) * (var(--bv-carousel-desktop, 3) - 1)) / var(--bv-carousel-desktop, 3));
    }
}
@container (min-width: 1201px) {
    :where([data-ch-widget="carousel"]:not([data-ch-carousel-type="hero"])) > *:not(.chc-carousel) {
        flex: 0 0 calc((100% - var(--bv-carousel-gap, 16px) * (var(--bv-carousel-large, 4) - 1)) / var(--bv-carousel-large, 4));
    }
}
/* NO POST-HYDRATION RESET — deliberately removed.
   There used to be a `[data-ch-widget-mounted="client"] { display:block
   !important; gap:normal !important; overflow:visible !important }` rule here. It
   existed for exactly one reason: react-multi-carousel replaced the host's
   children with a single wrapper div, and a lone flex item with no basis inside
   the authored `.rsg-carousel { display:flex }` collapsed to width 0, so the
   library measured 0 and rendered an empty track.

   The mounted carousel no longer replaces the host's children — the slides stay
   direct children of the host, which stays the flex/scroll track in both states.
   Resetting it to `display:block` would now BREAK the layout rather than rescue
   it. Blocks authoring `.rsg-carousel { display:flex; gap:…; overflow-x:auto }`
   at (1,1,0) are simply declaring what this bundle already wants, so their higher
   specificity is harmless — that authored shape IS the target shape.

   Do NOT confuse this with the superficially-similar rules in EjectSupport.php
   (~7863) and SectionBlocks.php (~7187). Those belong to the RUNTIME-LESS path
   (screenshots / no-JS export): they stamp mounted="client" and hide every slide
   but the first, so a static capture shows one clean card instead of a clipped
   row. That behaviour is still correct and is deliberately left alone.

   Also note there is deliberately NO post-hydration per-slide width override.
   A previous attempt forced flex/width/max-width:100% !important on mobile, but
   `100%` resolved against the FLEX TRACK (N x the container width) and blew each
   card out to thousands of px. Slide widths come from the @container bands above
   and nowhere else. */

/* ── The shell ──────────────────────────────────────────────────────────────
   Emitted by the server paint (BuilderCapabilities::wrapCarouselShell) and by
   the widget as a fallback. It wraps ONLY the track, and it is the containing
   block every piece of chrome positions against.

   WHY IT EXISTS: the widget used to adopt `track.parentElement` — whatever the
   section block wrapped the carousel in. On ListingCarousel that is
   `.lc-container`, which also carries the heading and the CTA footer, so
   `top:50%` centred the arrows on the whole section (measured 50px high) and
   appended dots after the CTA button. The shell's height IS the track's height,
   so both are correct by construction rather than by luck.

   NO PADDING HERE — deliberately. `container-type: inline-size` lives on the
   host and the slide-count bands (801/1201 below) query it, so insetting the
   host would shrink the queried box by the gutter and could drop a carousel a
   whole band (a 1283px host would fall under 1201 and lose its 4th column).
   The arrows instead hang OUTSIDE the track on negative offsets: the gutter is
   paid by the section's own horizontal padding, which every wall already has,
   and the column count is untouched. */
[data-ch-carousel-shell] {
    position: relative;
    /* A grid whose FIRST ROW is the track. The arrows are placed in that row
       (grid-row: 1) and stretched, so `top:50%` resolves against the CARDS —
       not against the shell, which also contains the dots row beneath them.
       Centring on the shell left them 12px low here and, in the old
       authored-wrapper world, 50px high. */
    display: grid;
    grid-template-columns: 1fr;
}

/* ── Chrome ─────────────────────────────────────────────────────────────────
   The overlay arrows and dots that used to live here are GONE. Both lanes now
   adopt the server-painted `.ch-pgbar` that BuilderCapabilities::buildCarouselBar
   emits (React: widgets/index.tsx; vanilla: runtime.js, whose buildArrows only
   adopts and whose buildDots is a no-op stub). Nothing has emitted
   `.ch-carousel-arrows` / `.ch-carousel-dots` elements since that switch, so
   their rules were dead weight shipping on every page and are removed.

   Hero keeps its own overlay arrows on a separate path (`.chc-hero_arrows`).

   `[data-ch-carousel-chrome]` stays: it is the pointer-events guard for genuine
   overlay chrome, and removing it would let a transparent layer eat track
   clicks. */
[data-ch-carousel-chrome] { pointer-events: none; }
[data-ch-carousel-shell] > [data-ch-widget="carousel"] {
    grid-row: 1;
    grid-column: 1;
    min-width: 0;
}

/* builder_capabilities: tabs */
:where([data-ch-widget="tabs"][data-ch-widget-mounted="server"] [data-ch-tab-panel][hidden]) {
    display: none;
}
:where([data-ch-widget="tabs"][data-ch-widget-mounted="server"] [data-ch-tab][aria-selected="true"]) {
    font-weight: 600;
}

/* builder_capabilities: accordion */
:where([data-ch-widget="accordion"][data-ch-widget-mounted="server"]) > details {
    /* Native <details> handles open/close pre-JS. The client widget
       upgrades to single-mode (closes siblings on open) on hydrate. */
}
:where([data-ch-widget="accordion"][data-ch-widget-mounted="server"]) > details > summary {
    cursor: pointer;
    list-style: none;
}
:where([data-ch-widget="accordion"][data-ch-widget-mounted="server"]) > details > summary::-webkit-details-marker {
    display: none;
}

/* builder_capabilities: wall_horiz */
[data-ch-widget*="wall_horiz"][data-ch-widget-mounted="server"] {
    max-width: var(--ch-wall-horiz-width, 1200px);
    margin: 0 auto;
    width: 100%;
    padding-inline: var(--ch-wall-horiz-padding, clamp(16px, 3vw, 32px));
    position: relative;
    z-index: 1;
}

/* builder_capabilities: wall_vert */
[data-ch-widget*="wall_vert"][data-ch-widget-mounted="server"] {
    padding-top: var(--ch-wall-vert-padding-top, var(--ch-wall-vert-padding, clamp(40px, 6vw, 72px)));
    padding-bottom: var(--ch-wall-vert-padding-bottom, var(--ch-wall-vert-padding, clamp(40px, 6vw, 72px)));
    position: relative;
    z-index: 1;
}

/* builder_capabilities: grid-paginator */
[data-ch-widget="grid-paginator"][data-ch-widget-mounted="server"] {
    display: flex;
    align-items: center;
    /* NO justify-content here. The host is now also the chrome bar (it carries
       .ch-pgbar--sides|centered|left|right), and a `center` on this two-attribute
       selector would outrank every one of those single-class placement rules —
       the operator's choice would silently do nothing. Placement is the bar's
       job; this rule owns only the sticky strip's own box. */
    gap: 14px;
    margin-top: clamp(24px, 5vw, 40px);
    /* Sticky is PROGRESSIVE ENHANCEMENT, deliberately. It engages only when no
       ancestor clips: ~19 of the 44 blocks carrying this host set
       `overflow:hidden` on their own root (.bv-team-grid, .rsg-section,
       .testimonial-grid-section, .bv-articles-list …), which kills position:sticky
       outright. Those blocks keep exactly today's static bar — hence the margin
       above STAYS, so the fallback never jams the bar against the last row.
       The mounted guard matters: the host ships before the engine reveals the
       nav, and painting a background on an unmounted host would streak a bar
       across single-page grids (total <= limit) that never show a pager. */
    position: sticky;
    bottom: 0;
    z-index: 20;
    /* --ch-bg-color is stamped on the SECTION wrapper from the section's resolved
       background row (WebsiteSchemes ~1756), not just :root — so this tracks the
       block's own background rather than the page's. */
    background: var(--ch-bg-color, transparent);
    /* Inline padding, not just block. With `sides` placement the two halves are
       pushed to the strip's own edges, so a bare `8px 0` left the rail and the
       next button flush against it — the bar read as clipped rather than
       inset. 12px each side gives both ends the same breathing room. */
    padding: 8px 12px;
}
/* ABOVE THE GRID. expandGridPaginatorHosts moves the host in front of the row
   list and stamps `data-ch-paginator-above`, so the chrome bar — search, sort
   and the pager — sits at the TOP of the grid and sticks there while the
   visitor scrolls the cards, instead of below a screenful of rows.

   It sticks to `--nav-height` rather than 0 so it parks BELOW the site's own
   sticky nav instead of underneath it. --supernav-height is added when present
   (the same pair listPaginatorJs already subtracts when it scrolls a page into
   view); both fall back to 0px, so a site with no sticky nav gets `top: 0`.

   The margin flips with the position: above the grid the bar needs room BELOW
   it, not above, or it sits flush against the first row. */
[data-ch-widget="grid-paginator"][data-ch-widget-mounted="server"][data-ch-paginator-above] {
    top: calc(var(--nav-height, 0px) + var(--supernav-height, 0px));
    bottom: auto;
    margin-top: 0;
    margin-bottom: clamp(16px, 3vw, 24px);
}
/* BOTTOM-POSITIONED BAR. The `paginator_position` fetch filter picks which end
   of the grid the chrome bar rides; `bottom` restores the original layout, where
   the strip sits after the rows and pins to the VIEWPORT FLOOR as you scroll.

   A block that already owns a toolbar wants this: ListingSearchLocal ships
   Grid/Map/List tabs and its own search field, so a second strip above the cards
   duplicates chrome the visitor already has. A plain grid keeps `top`.

   Only the sticky EDGE and the margin flip here. The DOM move is skipped
   server-side for `bottom` (expandGridPaginatorHosts leaves the host where the
   block authored it, after the rows), so there is nothing to re-order — the base
   `bottom: 0` rule already applies and this just makes the intent explicit and
   overrides the `above` rule if both ever land together. */
[data-ch-widget="grid-paginator"][data-ch-widget-mounted="server"][data-ch-paginator-position="bottom"] {
    top: auto;
    bottom: 0;
    margin-top: clamp(24px, 5vw, 40px);
    margin-bottom: 0;
}
/* Fallback for a host that never got bar classes (an un-migrated block, or one
   whose paginator type could not be resolved): keep today's centred strip. */
[data-ch-widget="grid-paginator"][data-ch-widget-mounted="server"]:not(.ch-pgbar) {
    justify-content: center;
}
/* A nav that is hidden (single-page grid) must not reserve the sticky strip. */
[data-ch-widget="grid-paginator"][data-ch-widget-mounted="server"]:has(> nav[hidden]) {
    position: static;
    background: none;
    padding: 0;
}
/* The nav carries its own layout. Scoped on BOTH the grid's host wrapper and the
   bare `.ch-pg` class because a CAROUSEL emits the same nav with no
   [data-ch-widget="grid-paginator"] host around it — every rule below that was
   host-scoped simply did not match there, and the buttons collapsed to ~20px. */
[data-ch-widget="grid-paginator"] > nav,
.ch-pg {
    display: flex;
    align-items: center;
    gap: 14px;
    /* Containing block for the rail below, so an absolutely-positioned rail
       resolves against THIS nav on every variant rather than escaping to some
       distant positioned ancestor. `simple` also declares it for its own groove;
       stating it here makes the rail's containment variant-independent. */
    position: relative;
}
/* THE RAIL IS NEVER A FLEX ITEM. listPaginatorJs writes `width: <100/pages>%`
   and a translateX onto `.ch-pg_rail` for EVERY variant — it does not know or
   care which one is active. Only `simple` styles the rail into a travelling
   groove (position:absolute, below) and `slim` hides it outright; `modern` did
   neither, so the element kept its inline 12.5% width IN FLOW and claimed an
   eighth of the bar as blank space between the label and the next arrow.
   Taking it out of flow here makes that impossible for any variant, present or
   future: a variant that wants a visible rail positions it, and one that does
   not pays nothing. Reported on teddy.chl.local with the modern slab. */
.ch-pg_rail {
    position: absolute;
    pointer-events: none;
}
/* Chevron glyphs — the static lane ships inline SVG, the React lane an entity. */
.ch-pg button svg {
    width: 20px;
    height: 20px;
    stroke: currentColor;
    fill: none;
    stroke-width: 2.25;
}
[data-ch-widget="grid-paginator"] > nav[hidden] {
    display: none !important;
}
/* The 999px-radius circular buttons this used to draw are RETIRED — that was the
   legacy grid look and is no longer one of the three options. Every paginator is
   now the joined module below: a bordered group with the progress rail along its
   top edge. */
[data-ch-widget="grid-paginator"] button,
.ch-pg button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--ch-text-color-accent, var(--ch-text-color, rgba(15, 28, 46, 0.16)));
    border-radius: 0;
    background: var(--ch-card-bg, transparent);
    color: var(--ch-card-text-color, var(--ch-text-color, inherit));
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}
/* Mask icons are PAINTED via background-color, not `color` (see WebsiteSchemes'
   moduleStylesGlobal icon accent rule) — so the accent has to land on background,
   and a bare `background:` shorthand here would wipe the mask itself. */
/* Mask icons are PAINTED via background-color, not `color`, so the accent has to
   land on background and a bare `background:` shorthand would wipe the mask. All
   three lanes now emit `<i data-mask-icon="chevron_left|chevron_right">` — the
   React carousel used a `&#8249;` text entity and the static runtime an inline
   SVG, which is why one control looked different depending on who drew it.
   Sized here, once, for every lane. */
[data-ch-widget="grid-paginator"] button i[data-mask-icon],
.ch-pg button i[data-mask-icon] {
    background-color: var(--ch-text-color-accent, var(--ch-text-color, currentColor));
    width: 18px;
    height: 18px;
}
[data-ch-widget="grid-paginator"] button:hover:not(:disabled),
.ch-pg button:hover:not(:disabled) {
    border-color: var(--ch-text-color-accent, var(--ch-color-primary, currentColor));
    color: var(--ch-text-color-accent, var(--ch-color-primary, currentColor));
}
[data-ch-widget="grid-paginator"] button:disabled,
.ch-pg button:disabled {
    opacity: 0.4;
    pointer-events: none;
}
[data-ch-widget="grid-paginator"] button[hidden],
.ch-pg button[hidden] {
    display: none !important;
}
/* NO min-width. The label holds anything from "2 of 5" to "1-18 of 240", so a
   fixed floor sized for one form leaves the other visibly padded — it collapses
   to whatever it needs instead, and `nowrap` is the only guarantee required.
   `tabular-nums` keeps the digits from jittering as the count changes width. */
[data-ch-widget="grid-paginator"] [data-ch-page-label],
.ch-pg [data-ch-page-label] {
    text-align: center;
    font-weight: 600;
    color: var(--ch-text-color, inherit);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ── PAGINATOR VARIANTS ─────────────────────────────────────────────────────
   One markup shape, three looks. The engine (EjectSupport::listPaginatorJs)
   binds [data-ch-page] and [data-ch-page-label] and is variant-blind, so these
   are pure presentation and the static export lane needs no extra code.

   `simple` is the historical bar and adds nothing — an un-migrated block, or one
   whose operator never picked a style, renders byte-identical to today.

   PLACEMENT IS A SEPARATE AXIS from the look. A grid keeps the sticky, centered
   strip above; a carousel gets the static, right-aligned form via
   .ch-pg--incarousel. The same variant therefore reads the same in both
   contexts and only its position differs. */

/* SIMPLE (default) — the approved "rail as the top edge" control, and the
   LIGHTEST of the three: there is NO container border. Only the buttons are
   outlined; the count sits to their LEFT as bare text, and the progress rail
   runs above the whole control.

   Source order is prev / label / next (one markup for every variant, so the
   engine and the export lanes never branch), and `order` moves the label ahead
   of both buttons — the buttons end up together on the right, which is what
   makes this read as "a count, then a control" rather than a segmented widget.

   The rail is a travelling WINDOW: 1/pages wide, sliding to (page-1)/pages, so
   BOTH its edges move. A bar that only grew from the left would read as "how
   full" rather than "where am I". */
.ch-pg--simple , [data-ch-prop-paginator_variant="simple"] .ch-pg {
    position: relative;
    /* 16px, not the bar's default 8: with no container border the count and the
       buttons are two loose objects, and at 8px they read as one crowded clump.
       Double the gap gives the label room to be its own thing. */
    gap: 16px;
    padding-top: 11px;              /* rail height + breathing room */
    /* fit-content AND flex-none: inside the chrome bar the nav is a flex item and
       would otherwise stretch, dragging its groove across the whole row instead
       of sitting over the control it measures. */
    width: fit-content;
    flex: none;
}
/* the unfilled groove, spanning the whole control */
.ch-pg--simple::before , [data-ch-prop-paginator_variant="simple"] .ch-pg::before {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 3px;
    border-radius: 2px;
    background: var(--ch-text-color, rgba(15, 28, 46, 0.16));
    opacity: 0.22;
}
/* the travelling window; width/transform are set by the runtime */
.ch-pg--simple .ch-pg_rail , [data-ch-prop-paginator_variant="simple"] .ch-pg .ch-pg_rail {
    position: absolute;
    top: 0; left: 0;
    height: 3px;
    border-radius: 2px;
    background: var(--ch-text-color-accent, var(--ch-text-color, currentColor));
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Count first, buttons together after it. */
.ch-pg--simple [data-ch-page-label] , [data-ch-prop-paginator_variant="simple"] .ch-pg [data-ch-page-label] {
    order: 1;
    /* Flush LEFT against the rail's own left edge — the label and the groove
       above it share a starting line, which is what ties the two together as one
       control. No right padding: the bar's 8px gap already separates it from the
       buttons, and anything more reads as a hole. */
    padding: 0;
    border: 0;
}
.ch-pg--simple button[data-ch-page="prev"] , [data-ch-prop-paginator_variant="simple"] .ch-pg button[data-ch-page="prev"] { order: 2; }
.ch-pg--simple button[data-ch-page="next"] , [data-ch-prop-paginator_variant="simple"] .ch-pg button[data-ch-page="next"] { order: 3; }
/* Buttons carry the only borders on this variant, and sit flush together. */
.ch-pg--simple button , [data-ch-prop-paginator_variant="simple"] .ch-pg button {
    border-radius: 8px;
    border-width: 1px;
}
.ch-pg--simple button[data-ch-page="prev"] , [data-ch-prop-paginator_variant="simple"] .ch-pg button[data-ch-page="prev"] { margin-right: -4px; }

/* ── SIMPLE IS THE DEFAULT, INCLUDING FOR UNSTAMPED INSTANCES ───────────────
   The variant rules above key on `data-ch-prop-paginator_variant`, which only
   exists once an operator has opened the switcher and saved. Every instance that
   nobody has touched carries NO stamp — measured 20 of 21 in production — and
   matched none of the three variants, so it painted with base button styling
   only: no rail, no groove, no radius. That is the "unstyled" bar reported on
   teddy.chl.local.

   `simple` is the product default, so an unstamped bar must render as `simple`
   rather than as nothing. Repeating the declarations here (instead of moving
   them onto the base `.ch-pg`) is deliberate: `modern` and `slim` override most
   of simple's rules but NOT `padding-top` or the `::before` groove, so basing
   them would leak an 11px gap and a stray groove into both.

   Scoped with `:has()` on the SECTION root — the element that carries the
   variant stamp alongside `data-ch-preset-source-id` — so this matches only a
   genuinely unstamped instance and never competes with an explicit choice.
   `:has()` is already relied on elsewhere in this bundle. */
section:not([data-ch-prop-paginator_variant]) .ch-pgbar > .ch-pg {
    position: relative;
    gap: 16px;
    padding-top: 11px;
    width: fit-content;
    flex: none;
}
section:not([data-ch-prop-paginator_variant]) .ch-pgbar > .ch-pg::before {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 3px;
    border-radius: 2px;
    background: var(--ch-text-color, rgba(15, 28, 46, 0.16));
    opacity: 0.22;
}
section:not([data-ch-prop-paginator_variant]) .ch-pgbar > .ch-pg .ch-pg_rail {
    position: absolute;
    top: 0; left: 0;
    height: 3px;
    border-radius: 2px;
    background: var(--ch-text-color-accent, var(--ch-text-color, currentColor));
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
section:not([data-ch-prop-paginator_variant]) .ch-pgbar > .ch-pg [data-ch-page-label] {
    order: 1;
    padding: 0;
    border: 0;
}
section:not([data-ch-prop-paginator_variant]) .ch-pgbar > .ch-pg button[data-ch-page="prev"] { order: 2; margin-right: -4px; }
section:not([data-ch-prop-paginator_variant]) .ch-pgbar > .ch-pg button[data-ch-page="next"] { order: 3; }
section:not([data-ch-prop-paginator_variant]) .ch-pgbar > .ch-pg button {
    border-radius: 8px;
    border-width: 1px;
}

/* MODERN — one slab: shared borders, square corners except the bottom two, and
   a flat top edge. The segments must touch, so the gap collapses and the
   buttons lose their pill radius. */
.ch-pg--modern , [data-ch-prop-paginator_variant="modern"] .ch-pg {
    gap: 0 !important;
    border-radius: 0 0 8px 8px;
    /* clips the square segments into the slab's own rounded bottom corners */
    overflow: hidden;
    width: fit-content;
}
.ch-pg--modern button, [data-ch-prop-paginator_variant="modern"] .ch-pg button,
.ch-pg--modern [data-ch-page-label] , [data-ch-prop-paginator_variant="modern"] .ch-pg [data-ch-page-label] {
    border-radius: 0;
    border: 1px solid var(--ch-text-color-accent, var(--ch-text-color, rgba(15, 28, 46, 0.16)));
    height: 48px;
}
/* Adjacent segments share one edge so the seams do not double up. */
.ch-pg--modern button + button, [data-ch-prop-paginator_variant="modern"] .ch-pg button + button,
.ch-pg--modern button + [data-ch-page-label], [data-ch-prop-paginator_variant="modern"] .ch-pg button + [data-ch-page-label],
.ch-pg--modern [data-ch-page-label] + button , [data-ch-prop-paginator_variant="modern"] .ch-pg [data-ch-page-label] + button { border-left: 0; }
.ch-pg--modern [data-ch-page-label] , [data-ch-prop-paginator_variant="modern"] .ch-pg [data-ch-page-label] {
    display: inline-block;
    /* Slab height minus the top border, so the text sits optically centred. */
    line-height: 47px;
    /* Enough to clear the shared borders on both sides, no more — the slab
       should hug "2 of 5" rather than reserve room for a longer string. */
    padding: 0 12px;
    background: var(--ch-card-bg, transparent);
    /* Match the chevrons rather than body text: the label is part of the
       control, and the accent is what the arrows already paint with. Falls back
       to inherited text colour on a scheme that sets no accent. */
    color: var(--ch-text-color-accent, inherit);
}
/* Concentric corners: the slab clips at 8px, so a square segment underneath has
   its own 1px border sliced through at the curve. The inner radius must be the
   outer MINUS the border width or the border visibly breaks at the corner. */
/* Inner radius = outer MINUS the border width, or overflow:hidden slices each
   segment's own border at the curve. All four corners now, since modern has no
   rail keeping its top edge flat. */
.ch-pg--modern button:first-of-type , [data-ch-prop-paginator_variant="modern"] .ch-pg button:first-of-type {
    border-top-left-radius: 7px;
    border-bottom-left-radius: 7px;
}
.ch-pg--modern button:last-of-type , [data-ch-prop-paginator_variant="modern"] .ch-pg button:last-of-type {
    border-top-right-radius: 7px;
    border-bottom-right-radius: 7px;
}

/* SLIM — the same parts as the others, UNFUSED: separate outlined buttons with a
   gap between them, the count between the arrows. No container, no rail, no
   shared borders. The lightest of the three, and the only one where the two
   buttons are not adjacent. */
.ch-pg--slim , [data-ch-prop-paginator_variant="slim"] .ch-pg { gap: 8px; }
.ch-pg--slim .ch-pg_rail , [data-ch-prop-paginator_variant="slim"] .ch-pg .ch-pg_rail { display: none; }
.ch-pg--slim button , [data-ch-prop-paginator_variant="slim"] .ch-pg button {
    border-radius: 8px;
    border-width: 1px;
}
.ch-pg--slim [data-ch-page-label] , [data-ch-prop-paginator_variant="slim"] .ch-pg [data-ch-page-label] {
    padding: 0 4px;
    border: 0;
}

/* ── THE CHROME BAR ─────────────────────────────────────────────────────────
   One row beneath a carousel's track, or inside a grid's sticky strip, holding
   up to two halves: the block's buttons and its paginator. Three axes describe
   it (see WebsitePage::paginatorBarClasses, which resolves them):

     ordering   ch-pgbar--buttons_first | --paginator_first | --stacked
     placement  ch-pgbar--sides | --centered | --left | --right
     variant    ch-pg--simple | --modern | --slim   (on the nav itself)

   Ordering is expressed with `order`, not by reordering the DOM: the paginator
   must stay after the buttons in source order for tab sequence and for the
   Mustache token's position in authored HTML. */
.ch-pgbar {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    margin-top: 18px;
    width: 100%;
    /* DEFAULT PLACEMENT lives on the base rule, not on a `--sides` modifier.
       The axes are per-instance and arrive as `data-ch-prop-paginator_*` stamps,
       which most instances do not carry (only an operator who has opened the
       switcher has one). Defaulting here means an unstamped bar renders exactly
       as it always has, and the stamps below are pure overrides rather than a
       requirement. */
    justify-content: space-between;
}
.ch-pgbar > .ch-pgbar_buttons { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* The bar is a real row, never an overlay, so everything in it is clickable.
   Stated explicitly because `[data-ch-carousel-chrome]` sets pointer-events:none
   for the OLD overlay chrome — if the bar ever lands inside one of those
   subtrees, or an authored wrapper sets it, the buttons and the paginator go
   dead while still looking perfect. */
.ch-pgbar,
.ch-pgbar * { pointer-events: auto; }
/* except a disabled arrow, which must stay inert */
.ch-pgbar button:disabled { pointer-events: none; }

/* ORDERING. buttons_first is source order and needs no rule. */
.ch-pgbar--paginator_first > .ch-pgbar_buttons { order: 2; }
.ch-pgbar--paginator_first > .ch-pg { order: 1; }
.ch-pgbar--stacked {
    flex-direction: column;
    align-items: stretch;
}
/* Stacked puts the PAGINATOR on top, whatever the source order. */
.ch-pgbar--stacked > .ch-pg { order: 1; }
.ch-pgbar--stacked > .ch-pgbar_buttons { order: 2; }

/* PLACEMENT. `sides` pushes the halves apart; the rest collapse the group and
   move it as a unit. On a stacked (column) bar these align the column instead. */
.ch-pgbar--sides    { justify-content: space-between; }
.ch-pgbar--centered { justify-content: center; }
.ch-pgbar--left     { justify-content: flex-start; }
.ch-pgbar--right    { justify-content: flex-end; }
/* GATHERED placements need more air than `sides` does. When the two halves are
   pushed to opposite edges the whole row separates them; collapsed together they
   are touching, and the bar's 16px reads as one run of controls rather than two
   groups. 22px (+40%) keeps them related but distinct. Not applied to `sides`,
   where the gap does nothing. */
.ch-pgbar--centered,
.ch-pgbar--left,
.ch-pgbar--right { gap: 22px; }

/* STACKED runs vertically, and vertical distance reads much larger than the same
   number horizontally — two rows 22px apart look like two unrelated blocks. Pull
   them back to 7px (-40% from the row's 12px) so the paginator and the buttons
   read as one stacked unit.
   This MUST come after the gathered rule above: `.ch-pgbar--stacked` and
   `.ch-pgbar--right` are both single-class selectors, so on stacked+right the
   later one wins — without this the 22px horizontal value would leak into the
   column. */
.ch-pgbar--stacked { gap: 7px; }

.ch-pgbar--stacked.ch-pgbar--centered { align-items: center; }
.ch-pgbar--stacked.ch-pgbar--left     { align-items: flex-start; }
.ch-pgbar--stacked.ch-pgbar--right    { align-items: flex-end; }
/* paginatorBarClasses() rewrites stacked+sides to stacked+centered, so this pair
   is never emitted — but authored HTML can carry any classes, and `sides` on a
   column would otherwise fall through to `align-items: stretch` and read as
   left-aligned. Defensive, not dead. */
.ch-pgbar--stacked.ch-pgbar--sides {
    justify-content: flex-start;
    align-items: center;
}

/* ── THE FILTERS BAR — search + sort, a THIRD child of the chrome bar ───────
   WebsitePage::gridFiltersBarXml injects `.ch-fbar` into the same host as the
   paginator when an operator enables Show Search / Show Sort. It is OPT-IN, so
   every rule here is inert on the grids that do not carry one.

   Source order is buttons -> filters -> paginator. The visual order below is
   `order` only, never a DOM move — same contract the ordering axis already
   keeps, so tab sequence follows the markup on every arrangement. */
.ch-fbar {
    display: flex;
    align-items: center;
    /* MATCH THE PAGER'S BUTTONS, which are an explicit 40px square (.ch-pg
       button). The pill has no height of its own — it was derived from its
       content: 20px line-height + 8px padding x2 + 1px border x2 = 38px, so it
       sat 2px shorter than the arrows beside it and the bar read as misaligned.
       Setting it HERE, once, keeps the two halves honest: both fill the pill via
       `align-self: stretch` below rather than each restating a magic number.
       box-sizing is stated because this bundle ships onto pages whose reset we
       do not control — under content-box the border would push this to 42px. */
    box-sizing: border-box;
    /* Tracks the pager's button size rather than restating it: `simple` and
       `slim` are 40px, `modern` is 48px, and a pill frozen at 40 would sit
       visibly short beside a modern pager. The variants set --ch-pg-btn on the
       bar; 40px is the fallback for a bar that carries no variant at all. */
    height: var(--ch-pg-btn, 40px);
    /* ONE grouped pill: the search half and the sort half share a single
       rounded container with a divider between them, rather than reading as two
       separate controls that happen to sit next to each other. */
    /* MATCHES THE PAGER'S BUTTONS, deliberately: same border token, same square
       corners, same surface. The capsule shape and faded hairline this used to
       carry made the search read as borrowed from another design system when it
       sits inches from the arrows. `--ch-text-color-accent` with these exact two
       fallbacks is what `.ch-pg button` declares, so the two track any scheme
       together. */
    border: 1px solid var(--ch-text-color-accent, var(--ch-text-color, rgba(15, 28, 46, 0.16)));
    /* 8px is what the buttons actually RESOLVE to: `.ch-pg button` declares
       `border-radius: 0`, but every shipping variant overrides it — `simple` and
       `slim` to 8px, `modern` to square segments inside a rounded slab. The
       value to match is the variant's, not the base rule's. */
    border-radius: 8px;
    background: var(--ch-card-bg, transparent);
    color: var(--ch-card-text-color, var(--ch-text-color, inherit));
    transition: border-color 0.15s ease;
    overflow: hidden;
    max-width: 100%;
}
/* SIDES is the default spread, and there the filters pill belongs immediately
   LEFT of the pager: buttons keep the left edge, filters + paginator group at
   the right. `margin-left:auto` on the filters pill pushes both of them over as
   one unit while `space-between` still holds the buttons at the far edge. */
.ch-pgbar--has_filters > .ch-fbar { order: 2; }
.ch-pgbar--has_filters > .ch-pg   { order: 3; }
.ch-pgbar--has_filters.ch-pgbar--sides > .ch-fbar { margin-left: auto; }
/* …UNLESS there are no buttons. With three children the auto-margin groups the
   filters + pager against the right while the buttons anchor the left. With only
   TWO, nothing holds the left edge, so that same margin collapsed the group and
   `space-between` then split them the wrong way round — the pager stranded alone
   on the LEFT and the search pushed to the far right. Dropping the margin lets
   `space-between` do the obvious thing: search on the left, pager on the right. */
.ch-pgbar--has_filters.ch-pgbar--no_buttons.ch-pgbar--sides > .ch-fbar { margin-left: 0; }
/* paginator_first swaps the two halves; the filters pill travels WITH the
   paginator, staying on its left. */
.ch-pgbar--has_filters.ch-pgbar--paginator_first > .ch-fbar { order: 1; }
.ch-pgbar--has_filters.ch-pgbar--paginator_first > .ch-pg   { order: 2; }
.ch-pgbar--has_filters.ch-pgbar--paginator_first > .ch-pgbar_buttons { order: 3; }
.ch-pgbar--has_filters.ch-pgbar--paginator_first.ch-pgbar--sides > .ch-fbar { margin-left: 0; }
/* STACKED gives the pill its own row, between the paginator and the buttons. */
.ch-pgbar--has_filters.ch-pgbar--stacked > .ch-pg   { order: 1; }
.ch-pgbar--has_filters.ch-pgbar--stacked > .ch-fbar { order: 2; margin-left: 0; }
.ch-pgbar--has_filters.ch-pgbar--stacked > .ch-pgbar_buttons { order: 3; }

/* BREATHING ROOM BETWEEN THE PILL AND THE PAGER. The bar's 16px is tuned for
   two groups held apart by `space-between`; where the filters pill and the pager
   end up ADJACENT they read as one undifferentiated run of controls, and the
   search's end sits almost against the item-count label. The bar's own gap is
   14px, so +20% is 16.8px — applied as a 2.8px margin on the seam that needs it.

   Applied as a margin on the pill rather than raising the bar's `gap`, because
   the gap is shared with the buttons half: bumping it would also push the
   buttons away from whatever they sit beside, in every placement. `column-gap`
   on the pill's own trailing edge only affects the seam that actually needs it.

   `paginator_first` puts the pager BEFORE the pill, so the space belongs on the
   pill's left edge there instead. */
.ch-pgbar--has_filters:not(.ch-pgbar--stacked) > .ch-fbar { margin-right: 2.8px; }
.ch-pgbar--has_filters.ch-pgbar--paginator_first:not(.ch-pgbar--stacked) > .ch-fbar {
    margin-right: 0;
    margin-left: 2.8px;
}

/* THE PAGER'S BUTTON SIZE, published for the FiltersBar to match.
   `simple` and `slim` are 40px squares; `modern` runs 48px segments. Declared on
   the BAR (not the nav) because the pill is the nav's sibling and cannot read a
   variable set on it. Both channels are covered: the `ch-pg--<variant>` class
   stamped into markup, and the `data-ch-prop-paginator_variant` attribute that
   carries a per-instance choice to the export lanes. */
.ch-pgbar { --ch-pg-btn: 40px; }
.ch-pgbar:has(.ch-pg--modern),
[data-ch-prop-paginator_variant="modern"] .ch-pgbar { --ch-pg-btn: 48px; }

/* OPTICAL ALIGNMENT WITH THE PAGER. Matching heights is not enough on its own:
   the `simple` variant reserves an 11px gutter above its buttons for the
   progress rail, so its nav box is ~51px while its BUTTONS are 40px. Centring
   the bar's children then lines up the two BOXES and leaves the pill floating
   visibly above the arrows.

   Aligning the bar's items to their BOTTOM edge puts the pill on the same
   baseline as the buttons — the rail keeps its gutter above, and the controls
   read as one row. Scoped to a bar that actually has a filters pill, so a
   buttons+pager bar keeps the centring it has always had. */
.ch-pgbar--has_filters:not(.ch-pgbar--stacked) { align-items: flex-end; }
/* The buttons half inherits the same bottom alignment — centring it instead
   would measure it against the TALLER nav box (the rail gutter again) and float
   it above the pill and the arrows. Stated explicitly rather than left to
   inherit so the intent survives someone changing the parent's align-items. */
.ch-pgbar--has_filters:not(.ch-pgbar--stacked) > .ch-pgbar_buttons { align-self: flex-end; }

/* THE SEARCH HALF. A <label>, so the whole padded area is a click target and a
   press anywhere in it focuses the input — the input alone would leave the
   padding and the icon dead. */
.ch-fbar_search {
    display: flex;
    align-items: center;
    gap: 8px;
    /* Vertical padding is gone: the pill sets the height and this stretches to
       it, so a padding value here would only fight that and re-introduce the
       2px drift. Horizontal padding still owns the click target's width. */
    padding: 0 14px;
    align-self: stretch;
    cursor: text;
    min-width: 0;
}
.ch-fbar_search > i[data-mask-icon] {
    /* Sized with a rule, never inline: an inline width/height would beat any
       author selector trying to restyle it. */
    width: 16px;
    height: 16px;
    flex: none;
    opacity: 0.5;
}
/* The input is stripped to nothing — no border, no ring, no background, no
   platform search decorations — so the PILL is the visible control and the
   input is just the text inside it. */
.ch-fbar_search > input[data-ch-grid-search] {
    appearance: none;
    -webkit-appearance: none;
    border: 0;
    outline: 0;
    background: none;
    padding: 0;
    margin: 0;
    min-width: 0;
    /* 14ch (~+25% over the original 11ch): enough to read a typical query back
       without the pill crowding the pager on a mid-width grid. The mobile rule
       below overrides this to 100% once the pill takes its own row. */
    width: 14ch;
    color: inherit;
    font: inherit;
    font-size: 14px;
    line-height: 20px;
}
.ch-fbar_search > input[data-ch-grid-search]::-webkit-search-decoration,
.ch-fbar_search > input[data-ch-grid-search]::-webkit-search-cancel-button {
    -webkit-appearance: none;
}
.ch-fbar_search > input[data-ch-grid-search]::placeholder {
    color: inherit;
    opacity: 0.55;
}

/* THE SORT HALF, divided from the search half by a hairline rather than a gap. */
.ch-fbar_sort {
    position: relative;
    display: flex;
    align-items: center;
    align-self: stretch;
    flex: none;
}
.ch-fbar_search + .ch-fbar_sort {
    border-left: 1px solid color-mix(in srgb, currentColor 18%, transparent);
}
.ch-fbar_sort > select[data-ch-grid-sort] {
    appearance: none;
    -webkit-appearance: none;
    border: 0;
    outline: 0;
    background: none;
    /* Right padding leaves room for the chevron, which is positioned over it.
       No vertical padding — the select stretches to the pill's height instead
       (see .ch-fbar), so the two halves and the pager all land on 40px. */
    padding: 0 30px 0 14px;
    height: 100%;
    margin: 0;
    color: inherit;
    font: inherit;
    /* 11px, smaller than the search half's 14px on purpose: the sort is a
       secondary control the visitor sets once, not the one they type into, and
       dropping it a step keeps the pill from reading as two equal-weight fields.
       `ch` is relative to THIS font-size, so 18ch here is a tighter absolute
       width than 18ch would be at 14px — it buys room for the longer option
       labels ("Fewest Listings", "Most Listings") without widening the pill as
       much as the number suggests. */
    font-size: 11px;
    line-height: 20px;
    cursor: pointer;
    max-width: 18ch;
    text-overflow: ellipsis;
}
/* The dropdown LIST is painted by the OS, not by this stylesheet, and inherits
   nothing from the pill — on a dark block it would otherwise render dark text on
   a dark ground in some engines. */
.ch-fbar_sort > select[data-ch-grid-sort] option {
    color: initial;
    background: initial;
}
.ch-fbar_sort > i[data-mask-icon] {
    position: absolute;
    right: 11px;
    width: 14px;
    height: 14px;
    opacity: 0.5;
    pointer-events: none;
}

/* ACTIVE STATES, mirroring `.ch-pg button:hover` — the accent lands on the
   border so the pill and the arrows light up the same way.

   `:focus-within`, not `:focus`: the focusable node is the <input> or the
   <select> INSIDE the pill, so focusing either has to light the whole container
   or the ring would wrap half the control. This also restores a visible focus
   state that did not exist before — the input's own outline is suppressed (it is
   deliberately stripped chrome), which left keyboard users with no indication at
   all of where they were. */
@media (hover: hover) {
    .ch-fbar:hover {
        /* Same halving as focus below — a full-strength accent on hover made the
           pill flash darker than the buttons, which only shift their own border. */
        border-color: color-mix(in srgb, var(--ch-text-color-accent, var(--ch-color-primary, currentColor)) 50%, var(--ch-text-color, rgba(15, 28, 46, 0.16)));
    }
}
.ch-fbar:focus-within {
    /* HALF-STRENGTH, deliberately. Painting the accent on the border AND a full
       1px ring on top stacked into ~2px of solid dark — the pill visibly gained
       weight and jumped forward against the arrows beside it. The border now
       moves only halfway to the accent and the ring is a 50%-alpha hairline, so
       focus still reads clearly at a glance without the control appearing to
       thicken. Still inset rather than a wider border: growing border-width
       would add 2px to the box and break the height match with the buttons. */
    border-color: color-mix(in srgb, var(--ch-text-color-accent, var(--ch-color-primary, currentColor)) 50%, var(--ch-text-color, rgba(15, 28, 46, 0.16)));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ch-text-color-accent, var(--ch-color-primary, currentColor)) 50%, transparent);
}

/* The client-rendered empty state (listPaginatorJs writes this when a search
   matches nothing — the block's authored Mustache empty branch is server-only
   and cannot fire on a re-fetch). Spans the whole grid, whatever its columns. */
.ch-grid_empty {
    grid-column: 1 / -1;
    width: 100%;
    padding: 32px 0;
    text-align: center;
    opacity: 0.7;
    font-size: 15px;
}

/* NARROW SCREENS. The pill takes its own full-width row rather than crushing
   the pager: below ~560px a search box, a sort dropdown, two arrows and a label
   cannot share a line without every one of them becoming unusable. */
@media (max-width: 560px) {
    .ch-pgbar--has_filters > .ch-fbar {
        order: 1;
        width: 100%;
        margin-left: 0;
        justify-content: space-between;
    }
    /* The divider between the halves only reads correctly at full height once
       the pill spans the row. */
    .ch-fbar_search { min-width: 0; }
    .ch-pgbar--has_filters > .ch-pgbar_buttons { order: 2; }
    .ch-pgbar--has_filters > .ch-pg { order: 3; }
    .ch-fbar_search { flex: 1 1 auto; }
    .ch-fbar_search > input[data-ch-grid-search] { width: 100%; }
}

/* ── PER-INSTANCE AXES — driven by the section-root stamps ──────────────────
   Everything above is keyed on classes stamped into the MARKUP. That works only
   as a default, because the chrome bar is also baked into SHARED block content
   (migration 2250) and shared content cannot carry a per-instance value: two
   pages using the same block legitimately want different placements.

   So ordering and placement are ALSO expressed here against the
   `data-ch-prop-paginator_*` attributes the render stamps on the section root
   (an ancestor of every bar). Those attributes ride the markup on every lane —
   React, PHP static export and Astro — and this whole bundle ships on all of
   them, so the axes resolve identically everywhere.

   THIS IS THE ONLY MECHANISM THAT REACHES THE STATIC LANE. StaticPageRender
   never calls BuilderCapabilities::transform(), so a render-time class restamp
   corrects React and silently does nothing in the export. CSS is the one channel
   both lanes share.

   These are (0,2,0) — an attribute plus a class — so they outrank the (0,1,0)
   class rules above without !important, and any classes already baked into
   stored content degrade to a harmless floor.

   BOTH axes are converted together on purpose: the `gap` rules above resolve a
   stacked-vs-gathered tie by SOURCE ORDER between two equal-specificity
   selectors, so converting only one axis would break that tie. */
[data-ch-prop-paginator_ordering="buttons_first"] .ch-pgbar > .ch-pgbar_buttons { order: 1; }
[data-ch-prop-paginator_ordering="buttons_first"] .ch-pgbar > .ch-pg { order: 2; }
[data-ch-prop-paginator_ordering="paginator_first"] .ch-pgbar > .ch-pgbar_buttons { order: 2; }
[data-ch-prop-paginator_ordering="paginator_first"] .ch-pgbar > .ch-pg { order: 1; }
[data-ch-prop-paginator_ordering="stacked"] .ch-pgbar {
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
}
[data-ch-prop-paginator_ordering="stacked"] .ch-pgbar > .ch-pg { order: 1; }
[data-ch-prop-paginator_ordering="stacked"] .ch-pgbar > .ch-pgbar_buttons { order: 2; }

/* THE FILTERS PILL IN THE ATTRIBUTE CHANNEL.
   The class rules above (`.ch-pgbar--has_filters > .ch-fbar|.ch-pg`) are only
   half the story: these attribute selectors carry one more class's worth of
   specificity (0,3,0 vs 0,2,0), so on any instance stamped with
   `data-ch-prop-paginator_ordering` they OUTRANK the class rules and re-order
   the nav out from under them.

   That is exactly what put the pager on the LEFT and the search on the RIGHT on
   a live page: `buttons_first` set the nav to order 2, tying it with the pill's
   own order 2, and a flex tie falls back to SOURCE order — where the nav comes
   first, because expandGridPaginatorHosts appends the bar before the filters.

   So every ordering value needs the pill placed explicitly here too, using the
   same sequence the class rules establish: buttons, filters, pager. */
[data-ch-prop-paginator_ordering="buttons_first"] .ch-pgbar--has_filters > .ch-pgbar_buttons { order: 1; }
[data-ch-prop-paginator_ordering="buttons_first"] .ch-pgbar--has_filters > .ch-fbar { order: 2; }
[data-ch-prop-paginator_ordering="buttons_first"] .ch-pgbar--has_filters > .ch-pg { order: 3; }
/* paginator_first: the pill travels WITH the pager, staying on its left. */
[data-ch-prop-paginator_ordering="paginator_first"] .ch-pgbar--has_filters > .ch-fbar { order: 1; }
[data-ch-prop-paginator_ordering="paginator_first"] .ch-pgbar--has_filters > .ch-pg { order: 2; }
[data-ch-prop-paginator_ordering="paginator_first"] .ch-pgbar--has_filters > .ch-pgbar_buttons { order: 3; }
/* stacked: pager on top, pill between it and the buttons. */
[data-ch-prop-paginator_ordering="stacked"] .ch-pgbar--has_filters > .ch-pg { order: 1; }
[data-ch-prop-paginator_ordering="stacked"] .ch-pgbar--has_filters > .ch-fbar { order: 2; }
[data-ch-prop-paginator_ordering="stacked"] .ch-pgbar--has_filters > .ch-pgbar_buttons { order: 3; }

[data-ch-prop-paginator_placement="sides"]    .ch-pgbar { justify-content: space-between; }
[data-ch-prop-paginator_placement="centered"] .ch-pgbar { justify-content: center; }
[data-ch-prop-paginator_placement="left"]     .ch-pgbar { justify-content: flex-start; }
[data-ch-prop-paginator_placement="right"]    .ch-pgbar { justify-content: flex-end; }
/* Gathered placements need the wider gap, same reasoning as the class rules. */
[data-ch-prop-paginator_placement="centered"] .ch-pgbar,
[data-ch-prop-paginator_placement="left"]     .ch-pgbar,
[data-ch-prop-paginator_placement="right"]    .ch-pgbar { gap: 22px; }
/* …but a stacked column keeps the tighter vertical rhythm. (0,3,0) so it wins
   over the gathered pair above regardless of source order — the class layer
   relied on ordering alone, which is the fragile part this replaces. */
[data-ch-prop-paginator_ordering="stacked"][data-ch-prop-paginator_placement] .ch-pgbar { gap: 7px; }

[data-ch-prop-paginator_ordering="stacked"][data-ch-prop-paginator_placement="centered"] .ch-pgbar { align-items: center; }
[data-ch-prop-paginator_ordering="stacked"][data-ch-prop-paginator_placement="left"]     .ch-pgbar { align-items: flex-start; }
[data-ch-prop-paginator_ordering="stacked"][data-ch-prop-paginator_placement="right"]    .ch-pgbar { align-items: flex-end; }
/* A column has no sides — the operator's `sides` on a stacked bar reads as
   centered rather than falling through to `align-items: stretch`. Mirrors the
   rewrite paginatorBarClasses() used to do in PHP. */
[data-ch-prop-paginator_ordering="stacked"][data-ch-prop-paginator_placement="sides"] .ch-pgbar {
    justify-content: flex-start;
    align-items: center;
}

/* A BAR WITH ONE CHILD cannot be ordered and cannot spread. This replaces the
   "no buttons" branch in paginatorBarClasses(): a lone paginator pinned
   against an empty edge by `space-between` looked like a layout bug.
   `:has()` is already relied on in this same bundle (see the grid-paginator
   rule above); browsers without it simply keep the spread, which is cosmetic. */
.ch-pgbar:not(:has(.ch-pgbar_buttons)):not(.ch-pgbar--has_filters),
[data-ch-prop-paginator_placement] .ch-pgbar:not(:has(.ch-pgbar_buttons)):not(.ch-pgbar--has_filters) {
    justify-content: flex-end;
}
/* …but a buttonless bar WITH a FiltersBar still has two children to spread, and
   they are not interchangeable: the search is what the visitor reaches for, so it
   takes the left edge and the pager keeps the right. This rule exists because the
   one above predates FiltersBar — back then "no buttons" meant a LONE paginator,
   where `space-between` really did pin it against an empty edge. */
.ch-pgbar--has_filters.ch-pgbar--no_buttons.ch-pgbar--sides,
[data-ch-prop-paginator_placement="sides"] .ch-pgbar--has_filters.ch-pgbar--no_buttons {
    justify-content: space-between;
}

/* Inside a bar the nav is a flex ITEM: its own margin-auto would fight the
   bar's justify-content and always win, pinning it right regardless of
   placement. */
.ch-pgbar > .ch-pg { margin-left: 0; margin-right: 0; }

/* CAROUSEL PLACEMENT — static and right-aligned, never sticky. A carousel's bar
   sits in its own chrome row under the track, opposite the block's buttons; the
   grid's sticky strip would fight that row and pin to the viewport. */
.ch-pg--incarousel {
    position: static !important;
    background: none !important;
    padding: 0 !important;
    margin-top: 0 !important;
    justify-content: flex-end !important;
    margin-left: auto;
}

/* builder_capabilities: modal */
:where([data-ch-modal-body]) {
    /* Belt-and-braces: the [hidden] attribute already hides this,
       but some legacy global CSS overrides display on divs. */
    display: none;
}