/* ==========================================================================
   Mouldshop card surfaces — separator and hover, per surface.
   TASK-0029 built it. TASK-0030 fixed its SCOPE and gave it the column ladder.

   Ruling 12: ONE CARD EVERYWHERE, and the separator belongs to the surface, not to a
   second card template. Nothing here touches a card template; the card only supplies the
   `position: relative` it already had.

   MECHANISM — MEASURED OFF THE LIVE REFERENCE IN TASK-0025 (`49aaa80`), NOT INVENTED.
     grid   : a 1px rule painted by the CARD's own ::before / ::after, sitting in the
              container gutter. Container `gap` supplies the gutter; the rule is pulled
              into it with a negative offset.
     slider : a real `border-right` on the item. A DIFFERENT mechanism, deliberately —
              that is what the reference does, and it is what a horizontally scrolling
              track wants (the rule on the last item reads as "there is more to the right").
     hover  : shadow declared at ZERO ALPHA and only the alpha animates, plus a z-index
              lift. GRID ONLY — the reference's slider item has no hover and ours loses the
              one TASK-0017 gave it.
     js     : NONE. Nothing here has to re-run after a DOM swap, which is the entire reason
              this mechanism was chosen over a listener. See the section on N grids below.

   INVARIANT 12 — THE PALETTE SWAP IS THE :root BLOCK BELOW AND NOTHING ELSE.
   Every colour is declared once, as a custom property, with a PLACEHOLDER value. No rule
   past that block contains a colour literal. The reference's own rule colour is
   rgba(10,41,121,0.35) — a translucent navy — and it is deliberately NOT used here: it is
   Mouldshop's brand value, not our placeholder, and invariant 12 binds until the palette
   lands. Swapping is one block.

   INVARIANT 20 — REPRODUCE THE MECHANISM, NOT THE DEFECT.
   The reference's own outer-edge suppression is measurably broken (TASK-0025): at 4 columns
   every card paints a right-hand rule including the last, and at 3 columns only half the
   last-column cards are suppressed. Two independent causes, both avoided here:

     1. ITS SUPPRESSION CANCELS ITSELF. `article:nth-child(4n-3)::before{content:none}` is
        followed, at equal specificity, by a rule re-enabling `2n-1` — and every 4n-3 is odd.
        Avoided by having exactly ONE rule per edge per regime. NOTHING RE-ENABLES ANYWHERE,
        and that constraint survived the TASK-0030 fix: the scope defect below was closed by
        NARROWING the suppression selector, never by re-enabling a suppressed edge.

     2. ITS `nth-child` IS OFF BY ONE. The vendor `.grid` container's first child is a
        <script>, not an article, so `nth-child` counts from the wrong element. MEASURED ON
        THIS BOX AND IT IS THE SAME HERE. Worse than TASK-0029 knew: after a Load more the
        vendor STRIPS that script from the already-rendered list and ships a fresh one in the
        appended list, so the same page holds one grid whose first child is an ARTICLE and
        one whose first child is a SCRIPT — `nth-child` would be off by one in one grid and
        correct in the other, on the same screen. `nth-of-type` counts only among `article`
        siblings and is immune to all of it.

   AND ONE FURTHER DEPARTURE, WHICH IS AN IMPROVEMENT ON THE SOURCE.
   The reference draws its rules on the TRAILING edges (right, bottom) and suppresses the
   last column and last row. Suppressing a last row requires knowing the item count:
   `nth-last-of-type(-n+4)` is only the final row when the count is a multiple of 4. With 11
   products in 4 columns it silently strips the bottom rule from a card in the middle of the
   grid. THIS FILE DRAWS ON THE LEADING EDGES INSTEAD — left and top — and suppresses the
   FIRST column and the FIRST row, which `nth-of-type` locates EXACTLY, at any item count.
   Same picture, no arithmetic that can go wrong. Re-verified at 4, 3, 2 and 1 columns.

   ==========================================================================
   THE COMPONENT IS N GRIDS, NOT ONE — the TASK-0029 defect, and the fix.
   ==========================================================================
   The operator, at the TASK-0029 smoke: "I load more grid - and the row witch was the
   bottom, and now isnt still has now bottom border - witch means you cover the curent block
   of cards, not a ntire component."

   MEASURED, not assumed (/en-us/shop, 1440px, one Load more click):

     WHAT LOAD MORE APPENDS. The vendor form is
     `data-response-target-element=".product-list" data-swap="afterend"`
     (Swift-v2_ProductListItemRepeater.cshtml). It does NOT append articles into the existing
     grid. It inserts a WHOLE SECOND `.product-list` as a SIBLING, immediately after the
     first, sharing the paragraph wrapper as their parent. Each `.product-list` carries its
     own direct-child `.grid` with its own articles: 1 list / 12 articles becomes 2 lists /
     12 + 12. The vendor also strips the previous list's ProductListLoadMore block and its
     script element.

     WHICH RULE THE OPERATOR SAW. The FIRST-ROW TOP-RULE SUPPRESSION, RE-APPLIED TO THE
     APPENDED BATCH. `nth-of-type` counts within its own grid, so the appended list's first
     four articles each matched `:nth-of-type(-n+4)` and dropped their top rule — leaving the
     batch seam with no horizontal rule at all. Named exactly: it is a suppression wrongly
     re-applied, NOT a stray trailing rule on the previous batch.

     The first-COLUMN suppression is NOT affected and was left alone. Every appended grid
     starts a fresh row at column 1, so `nth-of-type(Nn + 1)` still lands on the true first
     column in every list. Measured: correct in both lists before any change.

   THE FIX, AND WHY IT NEEDS NO JS.
     - The top-rule suppression is scoped to the FIRST list only, via
       `.product-list:not(.product-list ~ .product-list)` — "a product list not preceded by a
       product list". Appended lists therefore never suppress, and the seam gets its rule.
       `:first-child` was REJECTED as the weaker form: it would stop matching the moment the
       paragraph wrapper gained any other child, and its failure direction is a stray rule
       above the very first row.
     - The left-rule suppression stays keyed on the grid, unscoped, because it is correct per
       grid.
     - Both are pure selectors. Nothing re-runs after a swap; an appended list is styled
       correctly the instant it is parsed. Still zero JS, still no listener (invariant 17).

   THE SEAM GUTTER. `.grid` carries `pb-3` (16px) and the two lists sit flush (measured:
   list0 bottom equals list1 top, no margins), so the seam gap was 16px where every internal
   row gap is 24px — the rule would have landed 4px off centre. Appended grids take that 8px
   back as padding-top, so the seam is a full gutter and the seam rule is centred exactly
   like every other row rule.

   ==========================================================================
   THE COLUMN LADDER — ruling 13, and WHERE THE COLUMN COUNT ACTUALLY LIVES.
   ==========================================================================
   It is NOT CSS. It is a PARAGRAPH SETTING, read in the vendor repeater template:

       GridLayoutDesktop : 4-columns -> g-col-lg-3 | 3-columns -> g-col-lg-4
                           2-columns -> g-col-lg-6 | 6-columns -> g-col-lg-2
                           list      -> g-col-lg-12
       GridLayoutMobile  : 2-columns -> g-col-6    | list      -> g-col-12

   Those are Bootstrap grid classes on the ARTICLE — `grid-column: auto / span N` against
   `.grid`'s 12 tracks. This box is set to desktop `4-columns` and mobile `list`, measured as
   `g-col-12 g-col-lg-3` on every article.

   THE SETTING IS THE SOURCE OF THE COUNT, BUT IT CANNOT BE THE SOURCE OF THE LADDER, AND
   THE AC'S "IF IT IS A SETTING, USE IT" DOES NOT SURVIVE THE EVIDENCE. Two independent
   reasons, both measured on this box rather than argued:

     1. IT EXPRESSES EXACTLY TWO REGIMES, split at Bootstrap `lg` (992px). Ruling 13 asks for
        FOUR, at 1400 / 1200 / 990. Nothing in the vendor's option list can say "three at
        1200". The middle of the ladder is unreachable from admin at any setting value.

     2. IT IS PER-PARAGRAPH, AND THIS BOX ALREADY HOLDS THREE DIFFERENT ANSWERS.
        ItemType_Swift-v2_ProductListItemRepeater carries SIX rows, referenced by ~20
        paragraphs across the listing, home and machine pages, in three combinations:
        `4-columns/list` (the /en-us/shop listing, paragraph 20182), `4-columns/2-columns`
        and `3-columns/list`. Ruling 13 is a SITE-WIDE ladder. Leaning on the setting for the
        end regimes would have given ruling-13 behaviour on one listing and something else on
        the others — and it would have looked correct wherever it was first checked.

   So the ladder is declared HERE, at all four regimes, and does not consult the setting:

     | viewport      | container | ruling 13 | span | vendor setting would give |
     | >= 1400       | 1288      | 4 cols    |  3   | 4 or 3, per paragraph     |
     | 1200 - 1399   | 1080      | 3 cols    |  4   | 4 or 3, per paragraph     |
     |  990 - 1199   | full      | 2 cols    |  6   | 4 or 3, and 1 below 992   |
     |  < 990        | full      | 1 col     | 12   | 1 or 2, per paragraph     |

   The setting is left exactly as it is. It still decides the count for any grid this file
   does not reach, and re-pointing ~20 paragraphs of vendor demo content to agree with a
   ruling they know nothing about would be a worse trade than four media queries.

   The 990 boundary is OURS, not Bootstrap's: between 990 and 992 the vendor says one column
   and this file says two.

   The container widths those regimes are anchored to are TASK-0031's, not this file's.
   ========================================================================== */

:root {
  /* --- placeholders: the whole palette surface of this packet ------------------------
     Project placeholders are light #ffffff and dark #000000. The rule and the shadows need
     an alpha, so they are declared as complete rgba() values in this block rather than as
     bare hex — the literal still lives here and nowhere else, which is what invariant 12
     asks for. Replace the block; touch nothing below it. */
  --ms-card-surface:      #ffffff;              /* project placeholder, light */
  --ms-card-rule:         var(--ms-rule);       /* THE PALETTE LANDED (2026-09-06): the placeholder black is gone and this is the brand navy at 35%, from --ms-rule in mouldshop.css */

  /* ONE shadow, geometry identical at rest and on hover so only the colour interpolates.
     The rest state is the same shadow at ZERO alpha — that is the technique, not an
     oversight, and it is what makes the transition animate the alpha alone.

     RULING 18 (TASK-0036) RE-CUT THIS NARROW, and the operator supplied the value:
     `0px 60px 40px -7px` at 20% black, replacing a two-layer
     `0 100px 100px -20px / 0 30px 60px -30px`. HE THEN TIGHTENED IT AGAIN, same day, by
     direct instruction: `0 40px 24px -7px` at the same 20% black. Both surfaces, one
     declaration, and the room token below re-derived from the new profile — that is the
     standing pattern now, not a one-off.

     WHY IT IS ONE LAYER NOW. The second layer existed to put a tighter core under the first
     layer's wide haze. At 40px of blur the single layer IS the tight core, and a second one
     would only re-introduce the reach being removed.

     THE POINT OF THE NUMBERS, measured off the unclipped grid card rather than derived:
       lateral reach  57px -> 13px   (the gap between a card and its rule is 12px)
       downward reach 165px -> 91px
       shadow landing ON the neighbouring card across the rule:  34px -> none
     The old shadow crossed the rule and shaded the next card; this one stops inside the
     gutter. That was the operator's complaint, and it is a SIZE, which is why the fix is a
     size — invariant 27. */
  --ms-card-shadow-rest:  0 40px 24px -7px rgba(0, 0, 0, 0);
  --ms-card-shadow-hover: 0 40px 24px -7px rgba(0, 0, 0, .2);

  /* --- non-colour tokens --- */
  --ms-card-gutter:       1.5rem;   /* 24px — the Bootstrap `.grid` gap this box renders */
  --ms-card-grid-pb:      1rem;     /* 16px — the vendor `pb-3` on `.grid`, used at the seam */
  --ms-card-rule-inset:   15px;     /* the rule is shorter than the card; the reference's value */
  /* The operator's easing, given directly and applied to EVERY hover animation on the card
     — the shadow and the lift alike, which is what "all animations" asked for. The duration
     was .6s and he cut it to .35s on seeing it run; the curve is unchanged. */
  --ms-card-hover-dur:    .35s;
  --ms-card-hover-curve:  cubic-bezier(0.78, 0, 0.22, 1);
  --ms-card-hover-ease:   var(--ms-card-hover-dur) var(--ms-card-hover-curve);
  --ms-card-hover-z:      100;

  /* The lift, operator-specified: the card rises on hover. Set at 15px, cut to 10px on seeing
     it run. It is a token because BOTH surfaces use it AND because the slider reserves
     exactly this much room above its row (2a) while the wrapper gives exactly this much back
     (2e) — three places that have to agree. Changing this one value moves all three; a
     literal in any of them would silently re-introduce the top clip. */
  --ms-card-hover-lift:   10px;

  /* TASK-0040. THE SLIDER'S SNAP INSET, AND EVERY PIXEL IN IT IS LOAD-BEARING.
     Where a snapped row comes to rest, measured from the scrollport's leading edge to the
     leftmost card's left edge. It has to clear the separator, which is drawn OUTSIDE the card
     in the gutter, or the scrollport cuts it:

         half a gutter   12   the gap the rule sits in
       + the rule itself  1   it is 1px wide and its own pixel has to be inside the clip box
       + slack            1   card widths are fractional (297.75 at 1440), so a rule computed
                              exactly onto the clip edge lands a quarter-pixel outside it
                              about half the time and renders as a hairline or not at all
       = 14

     Used TWICE and the two must agree: as the track's `padding-left`, which sets the resting
     position at scrollLeft 0, and as its `scroll-padding-left`, which sets every SNAPPED
     position. If they disagree the row jumps by the difference on first paging. */
  --ms-slider-snap-inset: calc((var(--ms-card-gutter) / 2) + 2px);
}

/* ==========================================================================
   1. THE GRID SURFACE
   ========================================================================== */

/* The card is the paint surface. It must be opaque: the rules sit in the gutter and a
   transparent card lets the page show through wherever a rule is meant to stop.
   `position: relative` is already on the vendor card (`position-relative` in its class
   list, measured) — restated here so this file does not silently depend on a vendor class
   staying put. */
.product-list > .grid > article {
  position: relative;
  background-color: var(--ms-card-surface);
  box-shadow: var(--ms-card-shadow-rest);
  /* A NUMBER, not auto: `auto` cannot be transitioned, so with auto at rest the delayed drop
     below never ran and the card fell under its neighbours the instant the pointer left
     (operator, 2026-09-24, second report). 0 keeps DOM paint order among resting cards. */
  z-index: var(--ms-card-z, 0);   /* descending paint order, set per card by mouldshop-card-stack.js */
  transition: box-shadow var(--ms-card-hover-ease), transform var(--ms-card-hover-ease),
              z-index 0s linear var(--ms-card-hover-dur);
}

/* Vertical rule — painted on the card's RIGHT, pulled half a gutter out so it lands centred
   between this card and its right-hand neighbour.

   TASK-0043 / DEC-0020 ruling 21c. It used to be a LEFT rule with the first column suppressed.
   RIGHT, on every card with no suppression at all, gives the same interior lines AND closes the
   grid on the right — which ruling 21c requires and the left form could not express — and
   "no line left of the first column" now falls out for free, because nothing ever draws one.
   One declaration replaces one declaration plus four media-query suppressions. */
.product-list > .grid > article::before {
  content: "";
  position: absolute;
  right: calc(var(--ms-card-gutter) / -2);
  top: var(--ms-card-rule-inset);
  width: 1px;
  height: calc(100% - (var(--ms-card-rule-inset) * 2));
  background-color: var(--ms-card-rule);
  pointer-events: none;
}

/* Horizontal rule — THE CARD OWNS IT, DEC-0024 ruling 31, and it is inset at both ends
   exactly like the vertical rule above.

   THIS IS THE THIRD CONSTRUCTION AND THE RECORD OF WHY MATTERS MORE THAN THE CODE.

     1. TOP rule on every card, first row suppressed (ruling 21b's predecessor). The line
        between rows N and N+1 belonged to row N+1's CELLS, so a short last row left the row
        above it ruled only under the columns that row happened to fill.
     2. ONE full-grid-width line per row, drawn by the row's FIRST card (ruling 21c, shipped
        at `5c58fd2`). Built and measured correctly; the operator rejected the RESULT on
        sight: "bottom border - is one long line through entire row - not borders under each
        cell - with gaps".
     3. This one. Each card draws its own bottom rule, one card wide, inset at both ends by
        `--ms-card-rule-inset` — the same token the right rule uses for its top and bottom
        clearance. The row reads as ruled cells with gaps, and no rule ever meets another at
        a corner: the bottom rule stops `inset` short of the card edge while the vertical
        rules sit half a gutter OUTSIDE it, so the corner gap is `inset + gutter/2` on both
        sides, by construction rather than by tuning.

   WHAT DID NOT COME BACK WITH THE PER-CARD FORM. Construction 1's defect was that a card
   asked "is there a card below me?". This one never asks that. It asks "is my ROW the last
   row?" — a question about rows, which is what ruling 21c got right and is kept.

   THE TRAP, RATIFIED IN DEC-0021, RESTATED IN DEC-0024, AND WRITTEN HERE SO IT IS NOT
   RE-DERIVED: `:nth-last-of-type(n + C + 1)` — "cards that have a card directly below" —
   REPRODUCES CONSTRUCTION 1's DEFECT. A penultimate-row card standing over a column that a
   short last row leaves empty has nothing below it and would draw nothing.

   SO THE TEST IS PER COLUMN POSITION, in 1c. A card in column p of C is not in the last row
   exactly when at least `C - p + 1` cards follow it: that many siblings is what it takes to
   reach the start of the next row. Column 1 needs C, the last column needs 1. Every card of
   the penultimate row passes however short the last row is, and no card of the last row can
   pass whatever its column.

   `display: none` is the resting state and 1c turns it on. The alternative — always drawing
   and zeroing the width — cannot express "no rule" once both `left` and `right` are set,
   because width is then computed from them. */
.product-list > .grid > article::after {
  content: "";
  display: none;
  position: absolute;
  bottom: calc(var(--ms-card-gutter) / -2);
  left: var(--ms-card-rule-inset);
  right: var(--ms-card-rule-inset);
  height: 1px;
  background-color: var(--ms-card-rule);
  pointer-events: none;
}

/* --- 1a. THE BATCH SEAM ---------------------------------------------------------------
   Every `.product-list` after the first arrived from a Load more. Give its grid back the
   8px the previous grid's `pb-3` costs, so the seam gap equals a full gutter and the seam's
   top rule is centred exactly like every internal row rule. */
.product-list ~ .product-list > .grid {
  padding-top: calc(var(--ms-card-gutter) - var(--ms-card-grid-pb));
}

/* --- 1b. THE COLUMN LADDER — ruling 19 (DEC-0018) --------------------------------------
   `grid-column: auto / span N` against `.grid`'s 12 tracks, the same property the vendor's
   `g-col-*` classes set. Those are single-class selectors; every rule here is three, so these
   win at every width and the paragraph setting is never consulted.

   RULING 19 REPLACES RULING 13's 4 / 3 / 2 / 1 AT 1400 / 1200 / 990, for this surface:

     >= 1080      filter column + 3 product columns — FOUR EQUAL CELLS
     768 – 1079   3 product columns, filter is a modal
     576 – 767    2 product columns, filter is a modal
     < 576        1 product column,  filter is a modal

   It also ends the vendor Bootstrap `lg` flip at 991, which is where the sidebar used to stack
   and never lined up with our 990 step. That mismatch was live and unrecorded until DEC-0018.

   1080 IS A VIEWPORT WIDTH. It is NOT `--ms-page-width`'s 1080 container step. The two numbers
   coincide and mean different things; nothing here may be derived from the container ladder and
   no token is shared with it. Naming them apart is deliberate (DEC-0018).

   The product grid sits INSIDE the 9/12 content cell, so a "product column" is `span 4` of that
   inner grid's twelve tracks, not `span 3` of the outer row. */

/* < 576 : one column. */
@media (max-width: 575.98px) {
  .product-list > .grid > article { grid-column: auto / span 12; }
}

/* 576 - 767.98 : two columns. */
@media (min-width: 576px) and (max-width: 767.98px) {
  .product-list > .grid > article { grid-column: auto / span 6; }
}

/* >= 768 : three columns, at every width above it. Above 1080 the filter column takes the
   fourth cell; below it the filter is a modal and the three columns fill the row. The product
   count does not change across 1080 — only what sits beside them does. */
@media (min-width: 768px) {
  .product-list > .grid > article { grid-column: auto / span 4; }
}

/* --- 1b-i. FOUR EQUAL CELLS — ruling 20 ------------------------------------------------
   Ruling 20 is a claim about RENDERED WIDTHS, not about span arithmetic, and the arithmetic
   alone does not deliver it: measured at 1440 the outer row ran a 16px gap while the product
   grid ran 24px, so a 3/12 sidebar came out 310 against a 222.5 card. Same spans, different
   gutters, unequal cells.

   One declaration fixes it: give the OUTER listing row the same gutter the cards already use.
   At 1288 with a 24px gutter, a twelve-track row has 85.33px tracks, so span-3 = 304 and
   span-9 = 960; three span-4 columns inside that 960 are 304 each. Four cells, one gutter,
   equal by construction rather than by tuning. */
@media (min-width: 1080px) {
  /* `.grid:has(.product-list)` is the OUTER row — the only grid on the page that contains a
     product list rather than being one. The inner grid is inside `.product-list`, so it cannot
     match itself. A `> * >` child chain was rejected: it assumes how deeply the vendor nests
     the list inside its column, which is exactly the kind of positional assumption that broke
     the row rule. */
  .grid:has(.product-list) {
    gap: var(--ms-card-gutter);
  }
}

/* --- 1c. THE FOUR EDGES — ruling 21c (DEC-0020) ----------------------------------------
   no line above the first row · no line left of the first column · no line below the last row ·
   a line right of EVERY cell, including the last, so the grid is closed on the right.

   Three of the four need no rule at all now:
     - no left line   : nothing draws one; the vertical rule moved to the right.
     - no top line    : nothing draws one; the horizontal rule moved to the row's bottom.
     - no bottom line under the last row : every card of the last row fails its column's
       `:has()` test — there are not enough cards after it to reach a next row — so the whole
       row rests at `display: none`.
   The four media-query suppressions this block used to carry are DELETED, not relaxed — every
   edge they were protecting is now a property of the construction instead of an exception to it.

   What remains is WHICH CARDS DRAW — the last-row test, which is per column position and so
   per regime. The rule's width is no longer a per-regime value at all: it is the card, minus
   the two insets, at every step (ruling 31). */

/* < 576 : one column. Every card is its own row, so "not in the last row" is simply "a card
   follows". */
@media (max-width: 575.98px) {
  .product-list > .grid > article:has(+ article)::after {
    display: block;
  }
}

/* 576 - 767.98 : two columns. Column 1 needs 2 following cards to reach the next row,
   column 2 needs 1. */
@media (min-width: 576px) and (max-width: 767.98px) {
  .product-list > .grid > article:nth-of-type(2n + 1):has(+ article + article)::after,
  .product-list > .grid > article:nth-of-type(2n + 2):has(+ article)::after {
    display: block;
  }
}

/* >= 768 : three columns. Column 1 needs 3, column 2 needs 2, column 3 needs 1. */
@media (min-width: 768px) {
  .product-list > .grid > article:nth-of-type(3n + 1):has(+ article + article + article)::after,
  .product-list > .grid > article:nth-of-type(3n + 2):has(+ article + article)::after,
  .product-list > .grid > article:nth-of-type(3n + 3):has(+ article)::after {
    display: block;
  }
}

/* --- 1c-i. THE LOAD-MORE SEAM -----------------------------------------------------------
   `:has(+ article ...)` asks about SIBLINGS, and Load more does not append siblings — it
   inserts a whole second `.product-list` (see the header). So the last row of batch 1 has no
   further cards in its own grid, fails the test, and draws nothing — leaving the seam between
   the two batches unruled. The old construction hit the same wall from the other side and
   solved it by scoping its first-row suppression to the first list; this is that same problem
   in the new ownership, and it gets the same shape of answer.

   A `.product-list` FOLLOWED BY ANOTHER ONE has more rows after it by definition, so every row
   in it draws — including its last. `~` and not `+`: the vendor appends each batch after the
   previous, and by the third click the first list is two siblings away, not one.

   ONE DECLARATION, NO REGIMES. Under ruling 21c this block needed three, because the width it
   was turning on differed per column count. Ruling 31's rule is one card wide at every step,
   so "every card in a non-final batch draws" is the whole statement and the ladder is
   irrelevant to it.

   IT HAS NOTHING TO BEAT. This rule and 1c both turn the SAME property ON; the only `display:
   none` on this surface is the resting declaration in section 1, at (0,2,2), and this selector
   is (0,3,2) — `.product-list` + the `:has()` argument's class + `.grid`, then `article` and
   the pseudo-element. So it wins where it must and never fights 1c, which is (0,3,3) and also
   says `block`. Nothing here depends on source order either. */
.product-list:has(~ .product-list) > .grid > article::after { display: block; }

/* --- 1d. hover. Grid only. ------------------------------------------------------------
   The z-index is LOAD-BEARING, not decoration. The rule to a card's right belongs to its
   NEIGHBOUR's ::before, so without the lift the hover shadow is painted underneath the
   neighbour's rule and the rule appears to cut through the shadow. Do not remove it as a
   tidy-up. (Two different "lifts" live in this file: this one is the PAINT ORDER lift, and
   `--ms-card-hover-lift` below is the physical one. They are unrelated.)

   THE 15px RISE is the operator's, and it lands on both surfaces because ruling 12 says one
   card everywhere. It animates on the same curve as the shadow, from the same token, so the
   two cannot drift apart. */
.product-list > .grid > article:hover {
  box-shadow: var(--ms-card-shadow-hover);
  z-index: calc(var(--ms-card-hover-z) + var(--ms-card-z, 0));   /* +order: a card still fading out stays above a newly hovered card below it */
  transition-delay: 0s;   /* raise at once on hover-in; the base rule delays the drop on hover-out */
  transform: translateY(calc(var(--ms-card-hover-lift) * -1));
}

/* Reduced motion means no motion, not instant motion: the transition goes AND so does the
   translate. The shadow still changes — it is a colour change, not movement. */

/* --- 1e. THE RULES DO NOT TRAVEL WITH THE CARD ----------------------------------------
   Operator, on seeing the lift: "you move card with dividers - ::before and ::after as
   well.. its wrong". Correct — the separators are pseudo-elements OF the card, so the card's
   `translateY` carried them up with it and the whole row's ruling shifted on hover.

   HIS FIX WAS A WRAPPER: an extra div inside the card to hold the rules while the product
   moves inside it. THE RESULT IS RIGHT AND THE MECHANISM CANNOT BE — the card's markup is
   vendor (the page-5861 product components), and M4's halt-bar forbids editing a vendor
   template in place. There is no element to add without doing that.

   SO THE PSEUDO-ELEMENTS CANCEL THE MOVE INSTEAD. The card translates -15px; each rule
   translates +15px against it, on the SAME token, so the net is zero at every frame and not
   merely at the two ends — identical duration and identical curve is what makes that exact
   rather than approximate. The rule stays exactly where it was; the card lifts past it.

   This is the operator's proxy container, expressed as arithmetic instead of markup: the
   card is the holder, the transform is the thing being held still. */
.product-list > .grid > article::before,
.product-list > .grid > article::after {
  transition: transform var(--ms-card-hover-ease);
}

.product-list > .grid > article:hover::before,
.product-list > .grid > article:hover::after {
  transform: translateY(var(--ms-card-hover-lift));
}

/* Reduced motion means no motion, not instant motion: the transition goes AND so does the
   translate. The shadow still changes — it is a colour change, not movement. With the card
   held still there is nothing for the rules to cancel, so their counter-move goes too. */
@media (prefers-reduced-motion: reduce) {
  .product-list > .grid > article { transition: none; }
  .product-list > .grid > article:hover { transform: none; }
  .product-list > .grid > article::before,
  .product-list > .grid > article::after { transition: none; }
  .product-list > .grid > article:hover::before,
  .product-list > .grid > article:hover::after { transform: none; }
}

/* ==========================================================================
   2. THE SLIDER SURFACE — RULING 16 (TASK-0034): ONE SEPARATOR MECHANISM

   THE SLIDER NO LONGER HAS A SEPARATOR CONSTRUCTION OF ITS OWN. It is built the way the
   grid is built, from the same tokens and the same declarations: a gap between cards, and
   each card painting a 1px rule into that gap from its own `::before`. The real
   `border-left` / `border-right` TASK-0032 and TASK-0033 used is DELETED.

   WHY, AND WHOSE CALL IT WAS. Three packets chased the two constructions and each found a
   fresh divergence — TASK-0032 the clipped hover, TASK-0033 the vendor container gutter and
   the one-sided swiffy gap. **The operator called it: replace one construction rather than
   keep matching them.** That is ruling 16, and invariant 25 is the lesson.

   WHAT IS INHERITED, NOT RELAXED. Ruling 14 (both outer edges bounded) and ruling 15 (equal
   clearance, equal to the grid's) still bind. They are now satisfied BY CONSTRUCTION rather
   than by arithmetic: a 24px gap with a rule down its middle puts 12px either side of every
   rule without anything being computed to make it so.

   THE VISIBLE CHANGES, STATED SO THEY ARE NOT READ AS DEFECTS AT THE SMOKE:
     · the rule is now INSET 15px top and bottom (the grid's `--ms-card-rule-inset`), where
       the border ran the card's full height;
     · the cards are no longer contiguous — a 24px gap opens between them;
     · the card is NARROWER: swiffy divides the track by the item count AFTER subtracting the
       gaps, so restoring the gap takes width off every card. Measured at 1440: 312 -> 288.
   All three follow from adopting the grid's construction and none is optional.

   -------------------------------------------------------------------------
   THE THREE FACTS THE PACKET HANDED OVER, RE-CONFIRMED — AND ONE WAS WRONG.
   -------------------------------------------------------------------------
   1. **CORRECTED. The grid's gutter is a CONTAINER GAP, not the card's margin.** The packet
      said "container `gap` is 0 on the grid and the gutter is the card's own margin". Read
      off `.product-list > .grid` at 1440: `gap: 24px` (`column-gap` and `row-gap` both 24),
      and the article's `margin` is `0px`. The card-margin route was never in play. This
      matters because it is what makes the mechanism transferable at all: swiffy's track is
      also a `display: grid`, so the slider can take a real `gap` the same way.
   2. Confirmed. Card width comes from swiffy's `--swiffy-slider-item-width`,
      `calc((100% - reveal - gap*(count-1)) / count)`, resolved against the track's CONTENT
      box. TASK-0033 set the gap to 0, which is why `grid-auto-columns` read a flat `25%`.
      Restoring the gap is what shrinks the card, and it is expected.
   3. Re-derived, and TASK-0033's answer STANDS. `--dw-container-gutter: 0` inside the card
      stays. Its job never depended on where the rule lives: it makes the card's content sit
      flush to the card box, which is what the grid card does (vendor swift.css zeroes the
      gutter for containers nested in a `swift-v2_app` paragraph, and the product list is
      one; the slider paragraph is not). Content flush to the box + rule 12px outside the box
      = 12px clearance, on both surfaces, from the same two facts. Remove it and the
      clearance goes back to 28 on one side.

   WHAT TASK-0033 LEAVES BEHIND. Its `padding-inline` on the item is GONE: it existed to buy
   clearance from a rule drawn ON the card's edge, and the rule is no longer there. Keeping
   it would add 12px to the 12px the gap already supplies. Its swiffy-gap-of-zero is likewise
   reversed — that packet needed contiguous cards to make a one-sided gap symmetric; this one
   needs the gap itself.
   ========================================================================== */

[id^="ComponentSliderProducts_"] .slider-container > li {
  /* `position: relative` comes from swiffy's own `.slider-container > *`; restated so this
     file does not silently depend on a vendor rule staying put — same reason section 1
     restates it for the grid card. */
  position: relative;
  background-color: var(--ms-card-surface);

  /* TASK-0017's frame, and TASK-0032/0033's rule-as-border, both removed. There is no
     border on this card at all now. */
  border: 0;
  border-radius: 0;

  /* TASK-0017 set `overflow: hidden` to clip content to the 12px radius. With the radius
     gone it only risks clipping a marker at a card corner — and the rules are drawn OUTSIDE
     the card now, so it would clip those too. */
  overflow: visible;

  /* the grid card's rest state, verbatim — zero-alpha shadow, only the alpha animates */
  box-shadow: var(--ms-card-shadow-rest);
  /* A NUMBER, not auto: `auto` cannot be transitioned, so with auto at rest the delayed drop
     below never ran and the card fell under its neighbours the instant the pointer left
     (operator, 2026-09-24, second report). 0 keeps DOM paint order among resting cards. */
  z-index: var(--ms-card-z, 0);   /* descending paint order, set per card by mouldshop-card-stack.js */
  transition: box-shadow var(--ms-card-hover-ease), transform var(--ms-card-hover-ease),
              z-index 0s linear var(--ms-card-hover-dur);
}

/* --- 2a. THE GAP, AND THE ROOM AT EACH END -------------------------------------------
   The grid gets its gutter from `gap: 24px` on the container. Swiffy's track is also a
   `display: grid`, but its gap is driven by a variable it also uses to compute the item
   width, so the gap is set THROUGH THAT VARIABLE rather than by declaring `gap` directly —
   declaring `gap` would open a visual gutter while swiffy went on sizing the cards as though
   there were none, and the row would overflow its track by exactly the gaps.
   `0rem`-style units matter here: swiffy multiplies the variable inside a `calc()` with
   lengths, so a unitless value is invalid CSS. `--ms-card-gutter` is a rem length already.

   THE TRACK PADDING IS WHAT LETS THE ROW HAVE OUTER EDGES. A grid row ends at the container
   and needs no rule there; ruling 14 requires the slider row to be bounded at both ends, so
   the end rules need somewhere to be drawn. Half a gutter of padding at each end gives the
   first card's `::before` and the last card's `::after` exactly the same 12px they would
   have if there were another card beyond them. The card content lands in the same place it
   did before this packet — TASK-0033's 12px item padding is simply moved onto the track. */
[id^="ComponentSliderProducts_"] .swiffy-slider {
  --swiffy-slider-item-gap: var(--ms-card-gutter);

  /* TASK-0040 - THE FIX, AND IT IS ONE LINE OF THE THREE.
     Swiffy snaps with `center` by default. Read section 2a-bis below before touching this. */
  --swiffy-slider-snap-align: start;
}

[id^="ComponentSliderProducts_"] .slider-container {
  /* Half a gutter at each end, so the row's own leading and trailing rules have the same
     room every internal rule has — plus, at the leading end, the one pixel that rule is
     shifted by (see the leading-edge block below). */
  padding-left: var(--ms-slider-snap-inset);
  scroll-padding-left: var(--ms-slider-snap-inset);
  padding-right: calc(var(--ms-card-gutter) / 2);

  /* ROOM FOR THE LIFT, and it is not optional. The card's top edge is flush with the top of
     this box, and this box is the clip box (2e) — so a card that rises 15px would have its
     top 15px cut off, which is the same defect ruling 17 fixed at the bottom, arriving from
     the other end. Exactly the lift, taken from the wrapper's own top padding below so the
     slider's height does not change by a pixel. */
  padding-top: 0;   /* operator 2026-09-24: "slider-container ... has to be 0". Was the lift room; a hovered card now rises 10px into the clip edge (see WORKLOG). */
}

/* --- 2a-bis. THE SNAP DEFECT, AND WHY THE FIX IS THREE DECLARATIONS -------------------
   TASK-0040. The operator: "when we slide - the leftmost element loosing it left border..
   And proper borders are anly in FIRST and list slide - all bnetween has broken position".

   ROOT CAUSE, READ OUT OF THE VENDOR STYLESHEET AND CONFIRMED BY MEASUREMENT.
   `swiffy-slider.css` line 5 sets `--swiffy-slider-snap-align: center`, and at `min-width:
   62rem` it stops snapping the slides themselves and snaps a 1px MARKER pseudo-element
   instead - `.slider-container > *::before`, at `left: calc((gap/2)*-1)`, carrying
   `scroll-snap-align: var(--swiffy-slider-snap-align)`.

   CENTRING A 1px BOX ALIGNS ITS CENTRE, WHICH SITS ON A HALF-PIXEL. Every resulting snap
   offset therefore lands on a fraction. The vendor formula, with this slider's numbers
   (gap 24, clientWidth 1288, pitch 321.75, card 1 at offsetLeft 13):

       scrollLeft = itemLeft - gap/2 + 0.5 - clientWidth/2

     card 3 -> 656.5 - 12 + 0.5 - 644 =   1.00   measured: settles at 1
     card 4 -> 978.25 - 12 + 0.5 - 644 = 322.75  measured: settles at 323, laid out at 322.75
     card 5 -> 1300 - 12 + 0.5 - 644 =  644.50   measured: clamped to the 644 maximum

   The leftmost card therefore came to rest 12px inside the scrollport while its incoming
   rule sits 13px to its left - one pixel outside the padding box, which IS the clip box. Cut,
   at every snapped position. Scroll 0 escaped because it is not a snap result; maximum scroll
   escaped because the browser CLAMPS it to an integer. That is precisely "first and last are
   fine and nothing in between is", and the two good positions are exactly the two the old
   harness measured.

   WHAT THIS IS NOT. The rule does not drift. A card is `position: relative`, so its own
   pseudo resolves against the CARD, not the scrollport, and scrolls with it - measured to
   within 0.5px at every offset. See the retraction in 2b.

   THE FIX, AND ALL THREE PARTS ARE REQUIRED.
     1. `--swiffy-slider-snap-align: start` (2a). Aligning a START edge removes the half-pixel
        centring arithmetic entirely. It also covers BELOW 62rem, where there is no marker and
        the vendor snaps the `li` itself - the variable drives both.
     2. `left: 0` on the marker, below. Without it the marker keeps its negative half-gap and
        the row rests half a gutter further in than the padding says.
     3. `--ms-slider-snap-inset` on BOTH `padding-left` and `scroll-padding-left` (2a). This is
        the part the vendored fix from the Shopify lane does NOT need: Nickerson's rule is a
        border ON the card, so flush against the scrollport is correct for it. Ours is a
        separator BETWEEN cards, 13px outside the card it belongs to, so the row has to rest
        far enough in for that ink to be inside the clip box.

   REJECTED, MEASURED, NOT ARGUED. Widening the marker to 2px only relocates the parity
   dependency (established in the Shopify lane). `scroll-padding-left` alone, without moving
   the marker, was measured GREEN at 1440/1200/390 and made scrollLeft 0 an invalid snap
   position at 990, so the row could not show card 1 - a worse defect than the one being
   fixed. A 15px inset instead of 14 fails one offset at 990. 14 is the measured value.

   `:not(:first-of-type)` is not tidiness. Card 1's `::before` is OUR leading rule, at
   -13px and full height; this declaration would flatten it against the card's edge.
   `:not(.slider-item-snapstart)` mirrors the vendor's own guard so this never fights swiffy's
   own snapstart mode. And the specificity is the whole reason this selector is so long: the
   vendor's marker rule is (0,3,1) and the obvious short form is (0,2,2), which LOSES - the
   first attempt at this fix changed nothing at all and read as a failed hypothesis rather
   than as a lost cascade. This selector is (0,5,2). */
@media (min-width: 62rem) {
  [id^="ComponentSliderProducts_"] .swiffy-slider:not(.slider-item-snapstart)
  .slider-container > li:not(:first-of-type)::before {
    left: 0;
  }
}

/* --- 2b. THE RULES — the grid's declaration, on the two elements that can carry it -----
   Same tokens, same 1px, same `--ms-card-rule-inset`, same `--ms-card-rule`, same
   `pointer-events: none` as the grid's `::before`. Two things about the SLIDER's DOM force
   differences, and both were found by measurement rather than assumed:

   1. **`::before` IS NOT FREE ON A SWIFFY CARD — IT IS SWIFFY'S SCROLL-SNAP ANCHOR.**
      `.slider-item-show4:not(.slider-item-snapstart) .slider-container > ::before` declares
      `content: " "; position: absolute; left: calc(gap / -2); top: 0; width: 1px;
      height: 1px; scroll-snap-align: var(--swiffy-slider-snap-align)`. It is the 1px marker
      that makes a gapped track snap on the card AND its gap rather than on the card alone.
      Its specificity is (0,3,0) against this file's (0,2,1), so the first draft of this
      packet lost `top` and `height` to it and painted a 1px dot per card instead of a rule —
      caught by `CSS.getMatchedStylesForNode`, not by reading the file. **The rule therefore
      lives on `::after`, which is free (only the universal `box-sizing` touches it), and
      swiffy's anchor is left exactly as it is.** Do not "unify" the two onto `::before`:
      overriding that anchor changes where the track snaps.

   2. **THE CARD PAINTS ITS TRAILING RULE, NOT ITS LEADING ONE — and that is the opposite of
      the grid.** On the grid, leading edges are paintable and the first column is suppressed.
      Here the row needs a rule at BOTH ends (ruling 14) and there is one pseudo per card, so
      N cards can only paint N of the N+1 rules. Which N is not a free choice:
        · Painting TRAILING rules puts the last one at the row's end for free, and leaves the
          row's START missing — and the start is expressible from the track (see below).
        · Painting LEADING rules would leave the row's END missing, and the end is NOT
          expressible: an absolutely-positioned pseudo inside a scroll container resolves
          `right` against the padding box, whose right edge is the SCROLLPORT's, not the
          content's. That rule would sit at the visible edge and slide across the cards as
          the row scrolls — a false edge, exactly what TASK-0032 refused to draw.
      So the direction is forced by the scroll container, not chosen.

   AND THE GRID'S HORIZONTAL RULE NEVER REACHES HERE. Its `::after` is a top rule for the row
   below, keyed on the grid selector — nothing to suppress, it simply never matches, and a
   single row has no use for one. The `::after` below is a different rule doing a different
   job. */
[id^="ComponentSliderProducts_"] .slider-container > li::after {
  content: "";
  position: absolute;
  right: calc(var(--ms-card-gutter) / -2);
  top: var(--ms-card-rule-inset);
  width: 1px;
  height: calc(100% - (var(--ms-card-rule-inset) * 2));
  background-color: var(--ms-card-rule);
  pointer-events: none;
}

/* The row's leading edge. There is no card to the left of card 1, so card 1 paints it —
   and it has to be card 1 rather than the track, for a reason that was MEASURED after the
   first attempt got it wrong:

     AN ABSOLUTELY-POSITIONED PSEUDO WHOSE CONTAINING BLOCK *IS* THE SCROLL CONTAINER DOES
     NOT SCROLL WITH THE CONTENT. It is positioned against the padding box, which stays put
     as the content scrolls. `.slider-container::before { left: 0 }` therefore sat at the
     visible left edge and slid across the cards as the row scrolled — measured: leading ink
     at 88.5 before AND after a chevron click that moved the row 313px. That is precisely the
     false edge this packet refused to draw at the trailing end, arrived at from the other
     direction. The harness assertion that caught it is kept, and now asserts the opposite:
     the leading ink must move BY the scroll distance.

   So the leading rule goes on CARD 1's `::before`, which is also where swiffy puts its
   scroll-snap anchor. The declaration is written to STAND ALONE rather than to patch that
   anchor, because the anchor is not always there: swiffy's rule is keyed on
   `.slider-item-show2 / show4 / show6`, and below 990 this slider carries none of them — a
   patch-only version painted nothing at all at 989 and narrower, which the ladder sweep
   caught. Where the anchor does exist this rule simply wins (specificity (0,3,1) against
   (0,3,0), on the class-plus-pseudo-class count, not by `!important`) and leaves
   `scroll-snap-align` untouched, so the anchor keeps doing its job. The axis is
   `scroll-snap-type: x mandatory`, so a taller snap area cannot change where the row snaps —
   verified by measurement, not by argument: the chevron still moves the row 313px and it
   still returns to 0.

   THE EXTRA PIXEL. Every internal rule sits 11px from the card on its left and 12px from the
   card on its right, because a 1px rule cannot be centred in a 24px gutter. At `-12px` the
   leading rule would sit 11px from card 1 — the one rule in the row with a different
   relationship to the card it precedes. `-13px` makes it 12, so every rule in the row reads
   the same pair, and the track's leading padding carries the same extra pixel so the ink
   still lands inside the scrollport. The grid has the identical artefact, mirrored: it paints
   leading rules, so its cards read 11 in and 12 out where the slider reads 12 in and 11 out. */
[id^="ComponentSliderProducts_"] .slider-container > li:first-of-type::before {
  content: "";
  position: absolute;
  left: calc((var(--ms-card-gutter) / -2) - 1px);
  top: var(--ms-card-rule-inset);
  width: 1px;
  height: calc(100% - (var(--ms-card-rule-inset) * 2));
  background-color: var(--ms-card-rule);
  pointer-events: none;
}

/* --- 2c. THE VENDOR CONTAINER GUTTER — kept from TASK-0033, see fact 3 above ----------
   Page 5861's rows wrap their paragraph in `[data-swift-container]`, which vendor swift.css
   gives `--dw-container-gutter: calc(2rem)`. The grid card never shows it; the slider card
   did, per row (`data-dw-container-width="3"` binds its cap, `"2"` does not). Zeroing it is
   what makes the two cards render page 5861 identically, and it is independent of where the
   separator lives. */
[id^="ComponentSliderProducts_"] .slider-container > li [data-swift-container] {
  --dw-container-gutter: 0;
}

/* --- 2e. ROOM FOR THE SHADOW — ruling 17 (TASK-0035) ----------------------------------
   The shadow declaration is NOT touched here. Nothing in this block changes a colour, an
   offset, a blur, a spread, a timing or an alpha. It changes the box the shadow is allowed
   to paint into, which is the only thing that was ever wrong.

   WHAT CLIPS, AND AT WHAT HEIGHT — the chain, measured at 1440:

     LI.d-flex flex-column                    h=394   overflow visible
     UL.slider-container                      h=394   overflow auto / hidden   <-- THE CLIP
     DIV.swiffy-slider                        h=394   overflow visible
     DIV.grid.gap-0                           h=394   overflow visible
     DIV#ComponentSliderProducts_             h=458   padding 24px top / 40px bottom
     DIV.w-100.h-100 > DIV.ms-block-col       h=458

   The track is the clip box and it is EXACTLY the card box — 394 == 394 — so the shadow is
   cut at the card's own bottom edge. Note the row below it: the component wrapper already
   carries 40px of space under the track, and that space is OUTSIDE the clip, which is why
   it never helped.

   HOW BIG THE CUT IS. Measured on the GRID card, which nothing clips, by capturing a 3px
   column below a hovered card at rest and on hover and differencing the luminance. Both
   shadows, so the re-cut is visible in the same table:

     px below              +0   +10   +20   +30   +40   +60   +80  +100  +140  +165
     TASK-0032, two-layer   85    77    68    59    52    40    29    20     6     2
     ruling 18, 60/40       51    50    48    43    36    17     4     0     0     0
     tightened, 40/24       51    49    42    28    12     0     0     0     0     0

   The slider was cutting the shadow at its STRONGEST point — an 85/255 step at a hard
   horizontal line. That is what the operator saw. RULING 18 then made the shadow itself
   smaller, twice, so the room needed shrank with it: perceptible (>= 2/255) to +165px, then
   +83px, now +50px. `--ms-slider-shadow-room` is that measurement plus the same small
   headroom, and it has followed it down 170 -> 90 -> 55 in the same commit as each change.
   It is a measurement, not a taste, and it is re-derived whenever the shadow changes.

   THE ROUTES, AND WHY THIS ONE (invariant 26 — the rejected options are part of the report):

     A. CHOSEN — padding at the bottom of the TRACK, so the clip box is taller than the row
        of cards. The cards do not move, the page below is not overlapped, and the space is
        the slider's own. The slider block gets taller; that is the whole cost.
     B. REJECTED — the same padding plus an equal negative margin, so the slider's height
        does not change. TASK-0032 rejected this for eating the clicks of whatever sits
        below, and THAT REASON DOES NOT SURVIVE MEASUREMENT: hit-tested at +60, +120 and
        +160px below the row, the element returned is the FOLLOWING section, not the track —
        the next block is later in document order and neither is positioned, so it wins.
        The route is rejected for a different and worse reason, also measured: THE SHADOW
        DISAPPEARS COMPLETELY. Same capture, same hover, delta 0/255 at every depth from +0
        to +165, against 89 / 63 / 42 / 21 / 6 / 2 for the route above. Pulling the next
        block up over the track's padding puts it in front of the shadow. Note what that
        means in general: the route works or fails according to what happens to follow the
        slider on the page, which is not a property of the slider at all.
     C. REJECTED — shrink the CARD inside the existing track height. It needs ~165px off a
        394px card whose content already measures 375, so it would have to come out of the
        product image. That is a design change to the card, which ruling 12 does not permit
        and this packet forbids.

   WHAT THE OPERATOR IS TOLD BEFORE HE SMOKES IT: the row of cards is unchanged — same
   height, same width, same position. The empty space below the row grows, but far less than
   TASK-0035 first made it: with ruling 18's shadow the slider block measures 458px before
   and 473px after at 1440, +15px, because the 40px the component wrapper was holding below
   the track is given to the track instead of being added to it. (TASK-0035 shipped this at
   588px against the old shadow; the two tightenings give 115px of that back.)
   IF THAT IS TOO MUCH SPACE, IT IS ONE TOKEN. The bottom row above is the price list: at
   today's shadow, 40px of room leaves a 12/255 step and 30px leaves 28. Nothing else
   changes. */
:root {
  /* Measured, not chosen, and RE-DERIVED EVERY TIME THE SHADOW CHANGES. The unclipped
     shadow fades to nothing at +50px below the card; 55px keeps the same small headroom over
     the measurement that 170px kept over 165px and 90px kept over 83px. The history is the
     point: 170 -> 90 -> 55, tracking 165 -> 83 -> 50. Leaving it high parks dead space in the
     slider for a shadow that no longer goes there, so it moves in the same commit. */
  --ms-slider-shadow-room: 47px;
  /* TASK-0112 (operator: "bottom = minimum that never clips the hover shadow") - now
     DERIVED, not measured (browser measurement is off the table, CLAUDE.md 2026-09-22):
       hover shadow  0 40px 24px -7px   ->  shape edge 40 - 7 = 33px below the card,
                                            blur reaches its full radius, 24px, further = 57px
       hover lift    10px (--ms-card-hover-lift) raises the card, so below the RESTING card
                                            edge the shadow ends at 57 - 10 = 47px.
     47px is that arithmetic with no headroom. The 55px it replaces kept 5px over the old
     browser reading of ~50px. If a sliver ever shows at the bottom, the reading was right
     and this goes back to 50. */
}

[id^="ComponentSliderProducts_"] .slider-container {
  padding-bottom: var(--ms-slider-shadow-room);
}

/* The component wrapper's own bottom padding moves INSIDE the clip rather than adding to it.
   It was `1.5rem 1.25rem 2.5rem` — 24px top, 20px sides, 40px bottom — and the 40px sat
   below the track where the shadow could never reach it. The sides and the top are
   unchanged; only the bottom is surrendered, so the net height cost is 130px and not 170. */
[id^="ComponentSliderProducts_"] {
  padding-bottom: 0;

  /* The wrapper's own top padding was 1.5rem (24px), sitting OUTSIDE the clip where the
     lifted card cannot use it. 15px of it moves inside, onto the track, so the row keeps its
     position and the slider block keeps its height: 24 outside becomes 9 outside + 15 in. */
  padding-top: 0;   /* operator 2026-09-24, after TASK-0112: "not section but ComponentSliderProducts_ - remove top padding". What remains above the cards is the TRACK's lift-room (--ms-card-hover-lift, 10px), which keeps a hovered card's 10px lift from being clipped. */
}

/* The same cancellation on this surface — see 1e. Two selectors rather than one because the
   slider's rules live on different pseudo-elements: `::after` carries every card's trailing
   rule, and card 1's `::before` carries the row's leading edge (it is swiffy's snap anchor,
   re-used — see 2b). Both must stand still while the card rises. */
[id^="ComponentSliderProducts_"] .slider-container > li::after,
[id^="ComponentSliderProducts_"] .slider-container > li:first-of-type::before {
  transition: transform var(--ms-card-hover-ease);
}

[id^="ComponentSliderProducts_"] .slider-container > li:hover::after,
[id^="ComponentSliderProducts_"] .slider-container > li:first-of-type:hover::before {
  transform: translateY(var(--ms-card-hover-lift));
}

@media (prefers-reduced-motion: reduce) {
  [id^="ComponentSliderProducts_"] .slider-container > li { transition: none; }
  [id^="ComponentSliderProducts_"] .slider-container > li:hover { transform: none; }
  [id^="ComponentSliderProducts_"] .slider-container > li::after,
  [id^="ComponentSliderProducts_"] .slider-container > li:first-of-type::before { transition: none; }
  [id^="ComponentSliderProducts_"] .slider-container > li:hover::after,
  [id^="ComponentSliderProducts_"] .slider-container > li:first-of-type:hover::before { transform: none; }
}

/* --- 2d. THE HOVER — ruling 14, and RE-MEASURED on this construction ------------------
   The packet required this to be re-measured rather than inherited, because moving the rule
   out of the border changes the geometry the shadow paints into. Measured, and the answer is
   in two halves:

     UNCHANGED: the downward drop is still clipped. The track is still a scroll container
     (`overflow-x: auto` from swiffy + `overflow-y: hidden` from vendor swift.css) and its
     clip box is still exactly the card box — track height and item height both 392 at 1440.
     Adding horizontal padding does not give the shadow vertical room, and nothing in this
     packet could have.

     CHANGED, AND FOR THE BETTER: there is now a 24px gap either side of a hovered card, so
     the lateral bleed lands in open track before it reaches a neighbour. The z-index lift is
     still what carries it ONTO the neighbour, so it stays — A/B measured again on this
     construction, not carried over from TASK-0032.

   THE LIFT REPLACES A SUPPRESSION. Until now this rule carried `transform: none`, whose job
   was to hold down the 2px `translateY` TASK-0017's slider skin declares — the record said
   "the hover is shadow-only" and that was correct while no lift was wanted. The operator has
   now specified a 15px rise on both surfaces, so the suppression is not weakened, it is
   REPLACED: this declaration still wins over TASK-0017's by load order, and the 2px value
   can no more appear than it could before. Anything that reads 2px here is a regression, and
   the harness says so. */
[id^="ComponentSliderProducts_"] .slider-container > li:hover {
  box-shadow: var(--ms-card-shadow-hover);
  z-index: calc(var(--ms-card-hover-z) + var(--ms-card-z, 0));   /* +order: a card still fading out stays above a newly hovered card below it */
  transition-delay: 0s;   /* raise at once on hover-in; the base rule delays the drop on hover-out */
  transform: translateY(calc(var(--ms-card-hover-lift) * -1));
}

/* ==========================================================================
   3. CARD CONTENT PADDING, AND THE IMAGE SCALE (operator, 2026-09-06)

   The defect, measured on both surfaces before anything was written: the card carries
   `padding: 0px/0px` and EVERY text element renders at inset 0 - title, price and the
   "excl. VAT" line all sit hard against the card's own edge. Identical on the grid card
   (222.5px wide) and the slider card (297.5px), so this is the CARD, not the slider.

   Ruling 12 - ONE CARD EVERYWHERE - so both selectors carry the same declaration and the two
   surfaces stay identical. If a later change makes these two disagree, that is the defect.

   THREE ELEMENTS, THREE DIFFERENT TREATMENTS, as specified:
     content text   20px inline padding
     image          padding unchanged, scaled down 25%
     badges         the same 20px as the content (in mouldshop-card-markers.css)

   `:not(:has(figure))` is what separates them. The vendor card is a stack of <section>s: the
   first holds the <figure> with the image, the rest hold the title and the price. Selecting on
   CONTENT rather than on position means an extra section, or a reordered one, still lands on
   the right side of the split - `:nth-of-type` would not. */
:root {
  --ms-card-content-pad: 20px;

  /* The image renders at 75% of its box. `transform` rather than `width`, deliberately: a
     width change would shrink the figure's height too and every card in the row would get
     shorter, moving the title and price up. A transform leaves the layout box exactly where it
     is and only paints smaller, which is what "the image does not have to change, it just
     needs to be scaled down" asks for. */
  --ms-card-image-scale: .75;
}

/* 20px on EVERY side of the content block, not just the sides. The block is several vendor
   <section>s stacked, so "20px all round" was three rules, not one: the sides on every content
   section, the top on the first one after the image (removed 2026-09-24, see below), and the
   bottom on the last. Padding the
   block on all four sides with a single rule would put 20px between the title and the price
   as well, which is a gap inside the content, not around it. */
.product-list > .grid > article > section:not(:has(figure)),
[id^="ComponentSliderProducts_"] .slider-container > li > section:not(:has(figure)) {
  padding-inline: var(--ms-card-content-pad);
}

/* The TOP padding on the first section after the image (the product name) is GONE - operator,
   2026-09-24: "next element is product name ... padding-top: var(--ms-card-content-pad) -
   remove this". The rule that set it is deleted, not zeroed. */

/* The image's <figure> keeps no bottom margin (operator, 2026-09-24: "figure element from image
   - there is bottom margin - remove this (margin: 0 0 1rem) - but only here"). The 1rem is
   Bootstrap reboot's `figure { margin: 0 0 1rem }`, which applies to every figure on the site;
   this selector reaches only the card's own image figure, on both card surfaces. */
.product-list > .grid > article > section figure.item_swift-v2_productdefaultimage,
[id^="ComponentSliderProducts_"] .slider-container > li > section figure.item_swift-v2_productdefaultimage {
  margin-bottom: 0;
}

.product-list > .grid > article > section:not(:has(figure)):last-of-type,
[id^="ComponentSliderProducts_"] .slider-container > li > section:not(:has(figure)):last-of-type {
  padding-bottom: var(--ms-card-content-pad);
}

.product-list > .grid > article figure img,
[id^="ComponentSliderProducts_"] .slider-container > li figure img {
  transform: scale(var(--ms-card-image-scale));
}

/* ==========================================================================
   4. THE FILTER'S FLIP POINT — ruling 19 / ruling 25 (DEC-0018, DEC-0021)

   VENDOR SWIFT ALREADY SHIPS THE WHOLE FILTER BEHAVIOUR and none of it is rebuilt here.
   `Swift-v2_ProductListFacets.cshtml` renders a desktop form at `d-none d-lg-block`, a mobile
   block at `d-block d-lg-none` carrying the trigger button, and `#FacetsModal` as a
   `modal-fullscreen` Bootstrap modal with Clear / Update — on Bootstrap's own modal JS, so it
   is focus-managed and keyboard-reachable without a line of ours. Verified by measurement:
   the desktop form computes `display: flex` at 992 and `none` at 991, the mobile block the
   exact inverse.

   THE ONLY MISMATCH IS THE FLIP POINT — vendor `lg` at 992 against ruling 19's 1080 — and the
   template is VENDOR, which is never edited in place (DEC-0017 ruling 1). So the correction is
   this band and nothing else: between 992 and 1079.98 the desktop column is hidden and the
   modal trigger is shown, which is what the vendor already does below 992 and what ruling 19
   wants up to 1080.

   `!important` IS REQUIRED AND IS NOT LAZINESS. Bootstrap's display utilities are themselves
   declared `!important`, so a normal declaration — at any specificity — loses to `.d-lg-block`.
   This is the documented way to override them from outside.

   NO JS. The modal, its trigger, its focus handling and its close button are all the vendor's,
   untouched. Only which of the two blocks is visible changes, and only inside this band. */
@media (min-width: 992px) and (max-width: 1079.98px) {
  form[id^="FacetsForm_Desktop_"] { display: none !important; }
  [class*="FacetsForm_Mobile_"]   { display: block !important; }

  /* AND THE ROW ITSELF, which TASK-0044 missed and the operator's smoke found at 1062:
     "its changing on around 990".

     Moving the FACETS to 1080 hid the desktop form across this band but left the OUTER ROW
     still flipping at Bootstrap `lg` = 992 — so between 992 and 1079 the 3/12 cell was still
     there, empty, and the three product columns were squeezed into the 9/12 beside it. Two
     flip points on one layout, 88px apart. The facets' visibility and the row's shape are one
     decision and they move together.

     Both cells to span 12, which is what `g-col-12` already gives below 992: the facets cell
     becomes a full-width row holding the Filter trigger, the product grid takes the whole row
     under it. Identical to the <=991 layout, which is the point — nothing should happen at
     `lg` on this row any more.

     Scoped to `.grid:has(.product-list)` — the outer row is the only grid on the page that
     CONTAINS a product list rather than being one, so Bootstrap's `lg` is untouched everywhere
     else. (0,2,0) against `g-col-lg-3`'s (0,1,0), so no `!important` is needed here; the
     display overrides above need it only because Bootstrap's display utilities carry it. */
  .grid:has(.product-list) > * { grid-column: auto / span 12; }
}

/* ==========================================================================
   CARD PAINT ORDER - operator, 2026-09-24: "indexes of cards should lower from up
   to down". Hovering the card BELOW while the one above was still fading out cut
   the upper card's shadow (equal z-index, later card wins). Each card now carries
   --ms-card-z = count - index (mouldshop-card-stack.js): top rows above lower
   rows, left above right. At rest z-index = that; on hover 100 + that. The
   shadow only falls down (and ~17px sideways), so it always lands on a card that
   paints below it.

   ISOLATED, so these numbers can never compete with anything outside the list -
   in particular the sticky header (z-index 1000): a 120-product listing would
   otherwise put its first card at 120 and a hovered one at 220 in the page's
   own stacking order.
   ========================================================================== */
/* The LISTING is isolated at the parent that holds its .product-list blocks, not per grid:
   "Load more" appends the next page as a second .product-list beside the first, and an
   isolated grid is one layer - the whole second grid painted over the first grid's last
   row (operator, 2026-09-24). One shared layer + one numbering run across all loaded pages
   (mouldshop-card-stack.js) keeps the seam in order. */
:has(> .product-list),
[id^="ComponentSliderProducts_"] .slider-container {
  isolation: isolate;
}
