/*
 * Webshop overview: how this page's pieces sit together.
 *
 * Page-specific by design. Anything here that turns out to have a second
 * consumer should move to a component file at that point, not before.
 */

/* Filters beside results ------------------------------------------------------
   One column while the filters are a sheet; two once there is room for the
   panel and four cards beside it. This replaces the Foundation large-3 /
   large-9 float pair, and with it the source-order workaround that used to
   force the editable footer text into a row of its own. */

.shop-body {
    display: grid;
    gap: var(--shop-sidebar-gap);
}

@media (min-width: 1024px) {
    .shop-body {
        grid-template-columns: var(--shop-sidebar) minmax(0, 1fr);
        align-items: start;
    }
}

/* Stock header ----------------------------------------------------------------
   A different treatment from .shop-section__header above it — eyebrow,
   sentence-case title, subtitle, and controls on the right — so it is its own
   thing rather than a modifier of one. */

.inventory {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--shop-space-4) var(--shop-space-6);
    /* The catalogue's heading-to-content step, like every section's. */
    padding-block-end: var(--shop-rhythm-heading);
}

.inventory__text {
    display: grid;
    gap: 6px;
    min-width: 0;
    max-width: 640px;
}

.inventory__eyebrow {
    color: var(--shop-accent);
    font-size: var(--shop-text-sm);
    font-weight: var(--shop-weight-semibold);
    line-height: 1.3;
    letter-spacing: var(--shop-tracking-eyebrow);
    text-transform: uppercase;
}

/* Level with the hero's h1 on a phone rather than above it: at 26px this h2
   outranked the page's own title, and took two lines to say "our stock". */
.inventory__title {
    color: var(--shop-ink);
    font-size: var(--shop-text-xl);
    font-weight: var(--shop-weight-bold);
    line-height: var(--shop-leading-display);
    letter-spacing: -0.01em;
}

.inventory__subtitle {
    color: var(--shop-text-soft);
    font-size: var(--shop-text-body);
    line-height: 1.45;
}

/* Two equal columns on a phone: the result count over the filter button and
   the sort's label over its select, so the two small labels share a line and
   so do the two controls. A grid rather than flex: flex-basis 0 shares out the
   *content* width, so the button's own padding always left it 40px wider than
   the select. */
.inventory__controls {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: end;
    gap: var(--shop-space-3);
    width: 100%;
}

.inventory__filter,
.inventory__sort {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

/* With no results there is no sort control, and the button takes the row. */
.inventory__filter:only-child {
    grid-column: 1 / -1;
}

.inventory__count,
.inventory__sort label {
    color: var(--shop-text-soft);
    font-size: var(--shop-text-xs);
    white-space: nowrap;
}

.inventory__count {
    font-variant-numeric: tabular-nums;
}

/* Foundation draws its own arrow as a background image; this replaces it. */
select.inventory__select {
    box-sizing: border-box;
    width: 100%;
    height: 48px;
    padding-inline: var(--shop-space-3) var(--shop-space-8);
    border: 1px solid var(--shop-border);
    border-radius: var(--shop-radius-md);
    appearance: none;
    background-color: var(--shop-surface);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%231e1b4b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6l5 5 5-5'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--shop-space-3) center;
    background-size: 14px;
    color: var(--shop-ink);
    font-size: var(--shop-text-control);
    font-weight: var(--shop-weight-bold);
}

select.inventory__select:focus {
    border-color: var(--shop-accent);
}

/* The opener for the filter sheet. Keeps the .webshop-filter-button class,
   which selectie.js and the Playwright harness both rely on; the legacy rules
   that used to show and hide that class were deleted with the markup that
   needed them, so visibility is owned here. */
.inventory__filter-button {
    min-height: 48px;
}

/* A phone shows "Filters", because the sentence wraps to two lines in half a
   row; from 640px the sentence. display rather than clipping, so whichever is
   out of sight is also out of the accessible name. */
.inventory__filter-label {
    display: none;
}


@media (min-width: 640px) {
    .inventory__title {
        font-size: var(--shop-text-3xl);
    }

    .inventory__subtitle {
        font-size: var(--shop-text-lg);
    }

    /* Still bottom-aligned, from the base rule: centred, the button sat 13px
       above the select, whose label stands over it. */
    .inventory__controls {
        display: flex;
        width: auto;
        /* On the right also without the text beside it (a search page). */
        margin-inline-start: auto;
    }

    .inventory__filter-label {
        display: inline;
    }

    .inventory__filter-short {
        display: none;
    }

    /* Wraps without this: the subtitle's max-content width plus the controls
       came to 7px more than the track. Letting the text shrink keeps the sort
       control on the heading's line, which is where the design puts it. */
    .inventory__text {
        flex: 1 1 auto;
    }

    /* The button carries the count in its own label from here. */
    .inventory__count {
        display: none;
    }

    .inventory__filter,
    .inventory__sort {
        flex: none;
    }

    /* The label stays above the select rather than beside it, as in the design -
       which also keeps the pair narrow enough to share the heading's line. */
    .inventory__sort label {
        font-size: var(--shop-text-md);
    }

    select.inventory__select {
        width: auto;
    }
}

/* The panel is a column from here, so there is nothing to open. Placed after
   the base rule that makes this wrapper a flex column, which would otherwise
   win on source order. */
@media (min-width: 1024px) {
    .inventory__filter {
        display: none;
    }
}

/* iOS zooms the page when a field under 16px takes focus, and a select is a
   field. */
@media (pointer: coarse) {
    select.inventory__select {
        font-size: var(--shop-text-base);
    }

    /* The same size as the select beside it. */
    button.inventory__filter-button {
        font-size: var(--shop-text-base);
    }
}

/* Waiting for a filter -------------------------------------------------------
   A filter click is answered by the server, and on a slow connection that is
   most of a second away (measured on this tree with Chrome's own throttling:
   ~730ms on "Slow 4G", ~1.7s on "3G" - playwright/filter-latency.js). The
   option itself ticks immediately, because selectie.js flips aria-pressed
   before the request goes out; this is the results saying that what is on
   screen is still the previous answer.

   A bar above the results rather than dimming them. Dimming was tried first and
   is wrong: opacity composites the whole subtree against the page, and at 0.55
   every string in the grid falls below AA - the card title to 3.74:1, the meta
   line to 2.62:1, the price to 1.64:1 - while the cards deliberately stay
   clickable (below). That is live, purchasable content made illegible for up to
   1.7s on every click. No opacity rescues it either: --shop-text-soft only
   reaches 3.97:1 even at 0.9. A bar touches no text, and it is just as visible
   when the grid is empty, where there was nothing to dim at all.

   Two parts, which is what keeps it from reading as a measurement. A pale track
   the width of the region, and a short accent segment travelling across it: the
   segment is a quarter of the width and never rests anywhere, so there is no
   edge for the eye to read as "this far along". A single saturated line filling
   the region is the thing to avoid - that is a bar at 100%.

   The track carries no information on its own, which is why it is allowed to be
   as pale as it is - measured at 1.19:1 on white, and nothing subtle enough to
   be a track ever reaches the 3:1 that 1.4.11 asks of a graphic that means
   something. The accent is always the part that informs.

   So reduced motion gets its own accent, not the track by itself. The block in
   base.css neutralises animation-duration inside #content with !important and
   reaches *::before and *::after, so a travelling segment would stop at whatever
   position it was declared with and read as exactly the percentage this must not
   suggest. Instead the segment is declared only where motion is welcome, and
   where it is not the track is dashed in full accent: the same colour at the
   same 8.98:1, uniform from end to end, so there is no leading edge to read as
   progress and no motion to object to.

   It costs no layout: every part is absolutely positioned inside a region that is
   already the grid's own box (measured: 756px of a 1440px page, 360 of 390), so
   nothing moves when they appear, and dropping aria-busy removes them in the same
   frame.

   The results are the only region that gets it. The panel carried the same bar
   for a while, on the reasoning that with the sheet open the results are inert
   and behind it; but inside the sheet the bar lands on the top edge of the
   search field - .filters starts exactly where that input does - and all that
   escapes the input is a lilac rule along its border and a purple nub at each
   rounded corner. It read as a stray line on the field, not as waiting. The
   sheet says it in its footer instead, where the visitor's finger already is:
   the two buttons take the spinner (filter-sheet.css). It lives here rather than
   in filter-panel.css because aria-busy on the results is this page family's
   doing (selectie.js), not something a component has. */
@keyframes shop-waiting-appear {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* One mark, at both widths, hung off the grid itself ---------------------------
   It goes in the band of whitespace the stock header already leaves between its
   subtitle and the first row of cards, spanning the grid and nothing else - not
   the sidebar, not the header's full width. A rule any higher than this sits
   among the eyebrow, the heading and the panel's top edge, which are already
   horizontal lines, and stops reading as something happening.

   Hung off the grid rather than off the results column, so its ends are the
   first and last card's by construction rather than by arithmetic: the
   <ul class="product-grid"> measures 464..1220 at 1440 and so do the cards in
   its first row. The empty state swaps that list for .shop-empty at the same
   box, which is the one case with no cards to be level with and the one where a
   visitor most needs telling that something is happening - so both are hosts.

   One rule for both breakpoints. It was two for a while and the narrow one had
   drifted: no delay, so it flashed on every tap; hosted on .shop-results, whose
   first child is the inventory header, so it drew a 2px rule across that header
   204px above the first card rather than above the tiles; and no pointer-events,
   which the wide one had. The only thing that genuinely differs by width is
   --shop-rhythm-heading, and that is a token, so the offset below follows it
   with no media query at all: 16px of rhythm puts the mark 9px above the grid,
   20px puts it 11px above.

   Nothing moves when it appears: the mark is absolutely positioned into
   whitespace that is already there, half of --shop-rhythm-heading above the
   grid, which is the same token that put the whitespace there.

   The delay is the whole point of the timing and it is an animation-delay rather
   than a timer. A filter answers in ~130ms on a usable connection, and a mark
   that appears and vanishes inside that is worse than none - so nothing is
   painted for the first 180ms. The delay also settles the hard part by
   construction: there is no timer to leak, so a request that was superseded or
   abandoned cannot bring an indicator back for itself. aria-busy is on for as
   long as the page is waiting on the newest request and off the moment it
   settles (content-swap's onPending), so a reply inside 180ms removes the element
   before it has drawn anything, and a burst keeps one unbroken wait rather than
   restarting the clock per click. */
.shop-results :is(.product-grid, .shop-empty) {
    position: relative;
}

.shop-results[aria-busy="true"] :is(.product-grid, .shop-empty)::before {
    /* A fifth of the grid, which the travel below is derived from. */
    --shop-waiting-length: 20%;
    /* The colour the mark is painted in, named so forced colours can restate it
       once rather than restating every gradient that uses it. */
    --shop-waiting-ink: var(--shop-accent);

    content: "";
    position: absolute;
    /* Centred in the gap the header's padding-block-end leaves, so it is clear
       of what is above and of the cards below by the same amount. */
    inset-block-start: calc(var(--shop-rhythm-heading) / -2 - 1px);
    /* The element is the grid's full width and the mark is painted inside it,
       rather than the element being the mark and sliding. A moving box is not
       clipped by anything here - the grid does not hide its overflow, and must
       not, because the cards' focus rings and labels sit proud of it - so a mark
       travelling in from the left crosses the gutter and the filter card on its
       way, and on a phone it runs out past the right edge of the page. A
       background is clipped to the box that paints it, for nothing, which keeps
       every pixel of this over the tiles at both widths. */
    inset-inline: 0;
    block-size: 2px;
    background-color: transparent;
    background-repeat: no-repeat;
    /* backwards, so the delay is spent at the first keyframe - transparent, and
       off the left end - instead of at the element's own values. */
    animation: shop-waiting-appear 150ms var(--shop-ease) 180ms backwards;
    /* Drawn over the results' own whitespace; it must never take a pointer. */
    pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
    .shop-results[aria-busy="true"] :is(.product-grid, .shop-empty)::before {
        /* Two stops of one colour: a flat block with hard ends, because what is
           wanted is a mark rather than a smudge. */
        background-image: linear-gradient(var(--shop-waiting-ink) 0 100%);
        background-size: var(--shop-waiting-length) 100%;
        animation:
            shop-waiting-appear 150ms var(--shop-ease) 180ms backwards,
            shop-waiting 1.5s linear 180ms infinite backwards;
    }

    /* A background-position percentage is of the room the image has to move in -
       the grid less the mark, so 80% of the grid for a mark of 20%. That makes 0%
       the mark against the left edge and 100% against the right; -25% and 125%
       are one mark-width beyond each, where it is entirely outside the box and so
       painted nowhere. The turn therefore happens with nothing on screen, which
       is what stops the loop snapping. Tied to the length above: the overshoot is
       length / (100 - length). */
    @keyframes shop-waiting {
        from { background-position: -25% 0; }
        to { background-position: 125% 0; }
    }
}

@media (prefers-reduced-motion: reduce) {
    .shop-results[aria-busy="true"] :is(.product-grid, .shop-empty)::before {
        /* No travel to strand halfway, so the mark is the whole width of the
           grid, dashed: the same accent at the same 8.98:1, uniform end to end,
           so there is no leading edge to read as progress. The 180ms delay still
           applies - base.css neutralises animation-duration, not animation-delay
           - so a fast reply shows nothing here either. */
        background-image: repeating-linear-gradient(
            90deg,
            var(--shop-waiting-ink) 0 5px,
            transparent 5px 10px
        );
    }
}

/* Forced colours drops background-image and repaints background-color, and this
   mark is nothing but background - so without this there is no indicator at all
   in High Contrast, at either width and in either motion setting. Opting out of
   the override is the only way to keep a painted gradient, so the colour is
   restated as a system one: Highlight follows the user's own theme, which is the
   point of the mode. Same opt-out as the colour swatches in filter-panel.css,
   for the same reason - here the mark is the information. */
@media (forced-colors: active) {
    .shop-results[aria-busy="true"] :is(.product-grid, .shop-empty)::before {
        forced-color-adjust: none;
        --shop-waiting-ink: Highlight;
    }
}

/* Below the grid --------------------------------------------------------------
   "Load more" centred, the counter to its right, the previous-page link to its
   left. Pagination chrome belongs to the page, not to the grid component. */

.shop-results__footer {
    display: grid;
    grid-template-columns: 1fr;
    justify-items: center;
    align-items: center;
    gap: var(--shop-space-3);
    margin-block-start: var(--shop-space-8);
}

.shop-results__previous a,
.shop-results__previous a:hover,
.shop-results__previous a:focus {
    display: inline-flex;
    text-decoration: none;
    align-items: center;
    min-height: var(--shop-tap);
    color: var(--shop-accent);
    font-size: var(--shop-text-base);
    font-weight: var(--shop-weight-semibold);
}

/* The one soft button that is the whole point of its row rather than one of a
   pair: taller, larger, a wider radius to match. */
.shop-results__more {
    width: 100%;
    min-height: 52px;
    padding-inline: 28px;
    border-radius: var(--shop-radius-lg);
    font-size: 17px;
}

.shop-results__count {
    color: var(--shop-text-soft);
    font-size: var(--shop-text-md);
    line-height: 1.4;
    font-variant-numeric: tabular-nums;
}

@media (min-width: 640px) {
    .shop-results__footer {
        grid-template-columns: 1fr auto 1fr;
        gap: var(--shop-space-3) var(--shop-space-4);
    }

    .shop-results__previous {
        grid-column: 1;
        justify-self: start;
    }

    .shop-results__more {
        grid-column: 2;
        width: auto;
        min-width: 320px;
    }

    .shop-results__count {
        grid-column: 3;
        justify-self: end;
    }
}

/* Empty state ------------------------------------------------------------------ */

.shop-empty {
    padding-block: var(--shop-space-8);
    text-align: center;
}

.shop-empty__title {
    margin-block-end: var(--shop-space-4);
    color: var(--shop-ink);
    font-size: var(--shop-text-lg);
}

/* What a search can be: shown when it found nothing at all. */
.shop-empty__hint {
    max-width: 48ch;
    margin: 0 auto var(--shop-space-4);
    color: var(--shop-text);
    font-size: var(--shop-text-base);
    line-height: var(--shop-leading-normal);
}

/* After a search that found no tiles: where to go instead, as quiet links. */
.shop-empty__browse {
    display: grid;
    justify-items: center;
    gap: var(--shop-space-2);
    margin-block-start: var(--shop-space-4);
}

.shop-empty__label {
    margin: 0;
    color: var(--shop-text-soft);
    font-size: var(--shop-text-sm);
}

.shop-empty__browse .guide-links {
    justify-items: center;
}

/* Search ------------------------------------------------------------------------
   A search page opens on its heading (page-header.css) where other overview pages
   have the hero, with the section rhythm above it. Inside the results, the summary
   names the term and lists what the tile grid cannot show: the matching landing
   pages (guide-links.css) and accessories (product-chip.css). */

.search-page-header {
    padding-block-start: var(--shop-rhythm-section);
}

.search-summary {
    display: grid;
    gap: var(--shop-space-4);
    padding-block-end: var(--shop-rhythm-heading);
}

.search-summary__title {
    margin: 0;
    color: var(--shop-ink);
    font-family: var(--shop-font);
    font-size: var(--shop-text-lg);
    font-weight: var(--shop-weight-semibold);
    line-height: var(--shop-leading-snug);
    overflow-wrap: anywhere;
}

.search-summary__group {
    display: grid;
    gap: var(--shop-space-1);
}

.search-summary__label {
    margin: 0;
    color: var(--shop-text-soft);
    font-family: var(--shop-font);
    font-size: var(--shop-text-xs);
    font-weight: var(--shop-weight-semibold);
    letter-spacing: var(--shop-tracking-eyebrow);
    text-transform: uppercase;
}

.search-summary__chips {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
    gap: var(--shop-space-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Back to the filters ----------------------------------------------------------
   Shown at every width, once the filters have scrolled out of reach. Below
   1024px it opens the sheet; above it the panel is an ordinary column in the
   page, so it takes you back to it instead. selectie.js decides which, and
   when: from the moment the stock header's controls leave the screen until the
   tiles end.

   Centred at the bottom in the sheet range, where the thumb is. It sits over
   the gap between the two card columns, and the contact button bottom-right
   is 44px wide, so the two stay apart down to 320px. The radius is the large
   control radius Load More uses: the two thumb-sized controls on this page
   read as one family. */

.shop-refilter {
    position: fixed;
    inset-block-end: var(--shop-fixed-bottom);
    inset-inline-start: 50%;
    z-index: var(--shop-z-scrim);
    border-radius: var(--shop-radius-lg);
    translate: -50% 0;
    box-shadow: 0 4px 16px color-mix(in srgb, var(--shop-ink) 28%, transparent);
    /* Out of the tab order as well as out of sight until it is offered. */
    visibility: hidden;
    opacity: 0;
    transition:
        opacity var(--shop-duration) var(--shop-ease),
        visibility var(--shop-duration) var(--shop-ease);
}

.shop-refilter.is-visible {
    visibility: visible;
    opacity: 1;
}

/* It would cover the sheet's own action bar. Only the opacity goes: the button
   is inert while the sheet is open, which already takes it out of reach of the
   pointer and of assistive technology, and leaving it visible to the layout
   means focus can return to it the instant the sheet closes. Hiding it with
   visibility would have that focus call land on a still-hidden element, where
   the browser silently drops it. */
#web-shop.active .shop-refilter {
    opacity: 0;
}

@media (min-width: 1024px) {
    /* Aligned with the filter column it leads back to, rather than floating over
       the results, and the button it was there. */
    .shop-refilter {
        inset-inline-start: max(var(--shop-gutter), calc(50% - var(--shop-container) / 2));
        border-radius: var(--shop-radius-md);
        translate: none;
    }
}
