﻿/*
 * Tesla Product Card — the single canonical general product card.
 * ---------------------------------------------------------------------------
 * Hand-authored plain CSS at a fixed, theme-version-independent path, following the
 * assets/qsm/qsm.css precedent. Deliberately NOT part of the style.less monolith: the card is
 * the general product presentation and must render identically regardless of which asset
 * generation ThemeCssVersionUrl selects.
 *
 * ONE card. Context differences are outer-layout modifiers on the SAME markup tree, never a
 * second card implementation:
 *
 *     .tc-grid            desktop / mobile product grid   (hairline separators)
 *     .tc-row             horizontal product row/slider   (dark section)
 *     --tc-media-ratio    media aspect, per context
 *
 * GEOMETRY IS MEASURED, NOT GUESSED. Values below come from the native-resolution references
 * in G:\Projects\Design\plp (all @2x):
 *
 *   PLP-FullPage @1920      card pitch 341.25 · 4 columns · 1px hairline between cards
 *                           grid 1365 wide · sidebar ~318 on the inline-start side
 *   plp-mobile-1 @414       card 195 · 2 columns · gap 8.5 · outer 8 · media 195x259 = 3:4
 *   products-row-slider     card 252 · gap 24 · media 252x301 = 5:6 · section background #000
 *   @1440
 *
 * Colour tokens follow the Ryan palette (assets/v6/css/variables.less):
 *   @color-error-200  #ED2E7E  discount %, special-sale label, selected swatch ring
 *   @color-success-200 #00BA88 free shipping
 *   @color-info-200   #18A0FF  expedited shipping
 *   @color-secondary  #2F2F2F  primary text
 *   @color-secondary-500 #979797 muted / old price
 *   @color-secondary-300 #E6E6E6 hairlines, swatch borders
 *   @color-warning-200 #F4B740 rating star
 *
 * RTL is the storefront default. Every directional rule uses logical properties so the LTR
 * sheet needs no card-specific counterpart.
 *
 * Typography: the card sets size/weight/line-height, and a font-family on the title only.
 * Everything else - prices, discounts, amounts - inherits the Ryan face untouched.
 */

/* ==========================================================================
   1. CARD
   ========================================================================== */

.tc-card {

    --tc-accent: #ED2E7E;
    --tc-success: #00BA88;
    --tc-info: #18A0FF;
    --tc-danger: #ED303D;
    --tc-ink: #2F2F2F;
    --tc-ink-soft: #5E5E5E;
    --tc-muted: #979797;
    --tc-hairline: #E6E6E6;
    --tc-star: #F4B740;
    /* The media frame's ground. White, not a grey tile: the catalogue's photography is
       cut out on white, and `object-fit: contain` means this colour is what fills the
       letterboxing on any image whose aspect does not match the frame - so a grey plate
       showed as two grey bands down the sides of most products. This is the media area
       only; the card plate, badges, dots, cart button and shadows keep their own colours,
       and every context that reuses this media owner inherits the change. */
    --tc-media-bg: #FFFFFF;

    /* Media aspect. 3:4 for grids; the row context overrides it. */
    --tc-media-ratio: 3 / 4;
    --tc-media-radius: 8px;

    /* Reserved region heights. These are what make every card in a row the same height and
       put the QSM host, title, swatches and price on shared baselines. Overridden per
       breakpoint below. */
    --tc-badge-h: 18px;
    /* Title metrics measured on plp-mobile-1 (card 195): glyph band 15.5 tall -> ~15px, and the
       first line sits 19.0 below the media edge. */
    --tc-title-lh: 22px;
    --tc-title-h: 44px;      /* exactly two lines */
    --tc-swatch-h: 24px;
    --tc-meta-h: 17px;
    --tc-price-h: 26px;
    /* 22, not 15: the row carries the discount percentage next to the struck price now, and 15
       was sized for the struck price alone - the 19px badge does not fit inside it. 20 contains
       that badge exactly; 22 was 2px too deep and put the hanging row on the deals countdown's
       top hairline. The row hangs out of flow (negative end margin below), so this is the height
       of the hanging band only and it adds nothing to the card's own height. */
    --tc-oldprice-h: 20px;
    /* --tc-pad-x is the card's own horizontal inset; --tc-text-pad-x insets the text columns
       relative to the media. On mobile the reference shows the media running the full card
       width (card 195 -> media 195 -> 260 tall at 3:4) with only the text inset, so the card
       inset is 0 there and the text carries its own. Desktop insets the media instead. */
    /* CARD CONTENT SAFE-AREA.
       --tc-pad-x insets the media; --tc-text-pad-x insets the text column inside it. The media
       may run the full card width, but the badge, title, swatches and price must never touch the
       card edge. Where the media is full-bleed (mobile, row) the text carries the whole inset. */
    --tc-pad-x: 0px;
    --tc-text-pad-x: 10px;
    /* Deep enough to hold the hanging old-price row (15) plus a little air. Uniform on
       every card, so it reads as card padding rather than a reserved discount slot. */
    --tc-pad-b: 20px;
    --tc-gap: 4px;

    /* Per-seam gaps. Each falls back to --tc-gap, so a context only has to name the seams it
       actually re-times. Measured seam-by-seam from the references rather than assumed to be
       one uniform rhythm - they are not. */
    /* Mobile seams, measured off plp-mobile-1 (card 195, media 195x259):
           media bottom 330 -> title glyph 352      seam ~4 on the box
           title bottom  385 -> swatch chips 403    seam 18
           swatch bottom 427 -> meta glyph  445     seam 20
           meta bottom   460 -> price glyph 470     seam 16
       These are structural gaps in the design system, so they are reserved even where this
       catalogue has no rating or discount to put in them - but nothing fake is rendered. */
    /* Box seam, not optical seam: the title's half-leading (line-height 22 over a 15px glyph)
       eats ~3.5px, so a 14px box seam rendered as 17.5px of visible air under the media and the
       title read as glued to it. 18 lands the glyph at ~21.5, which is the reference's 22. */
    --tc-gap-media-title: 18px;
    --tc-gap-title-swatch: 13px;
    --tc-gap-swatch-meta: 16px;
    --tc-gap-meta-price: 6px;

    /* Seams for the COLLAPSED neighbourings (MP5). When a section is absent the price must not
       inherit the seam of a section that is no longer there: 6px is the meta->price seam, and
       reading it as a swatch->price seam glues the price to the swatch rail. These are the
       section-level seams the price falls back to as its predecessor changes. They are seams,
       not placeholders - nothing is reserved and nothing is rendered. */
    --tc-gap-swatch-price: 18px;   /* swatches present, meta collapsed */
    --tc-gap-title-price: 14px;    /* swatches AND meta collapsed */

    /* Price-row direction.
       row          -> price on the inline-start (right in RTL), discount % on the inline-end.
                       This is plp-mobile-1.
       row-reverse  -> price on the inline-end, discount % on the inline-start.
                       This is products-row-slider.
       The two reference sets genuinely disagree here, so the arrangement is a context token on
       ONE markup tree rather than a second card. --tc-oldprice-align follows it so the old
       price always sits directly under the price it replaces. */
    --tc-price-dir: row;
    --tc-oldprice-align: flex-start;

    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    padding: 0 var(--tc-pad-x) var(--tc-pad-b);
    /* No card surface: both reference sets show the card sitting directly on the page/section
       background, separated by rules rather than by a tile. */
    background: transparent;
    color: var(--tc-ink);
    box-sizing: border-box;
}

.tc-card *,
.tc-card *::before,
.tc-card *::after { box-sizing: inherit; }

.tc-card a { text-decoration: none; color: inherit; }

/* ==========================================================================
   2. BADGE STRIP
   ========================================================================== */

/*
 * Always occupies its reserved height, badge or not, so the media top edge is identical on
 * every card in the row. This is the first of the shared baselines.
 */
.tc-card__badges {
    display: flex;
    align-items: center;
    justify-content: flex-start;   /* inline-start = right in RTL, as in both references */
    padding-inline: var(--tc-text-pad-x);
    height: var(--tc-badge-h);
    min-height: var(--tc-badge-h);
    flex: 0 0 auto;
}

/*
 * Sampled off products-row-slider (card 2 badge band, CSS y 91.0-103.5, media top 114):
 *     colour  #E7113D      glyph height 12.5  ->  ~13px
 *     flush against the card's inline-start edge, 10.5 above the media
 *
 * Note the reference uses a RED for this badge and the pink #ED2E7E only for the discount
 * percentage - two different colours. Using --tc-danger here keeps that separation, which also
 * reads correctly: Special Sale is not a discount.
 */
.tc-card__badge--special {
    font-size: 13px;
    font-weight: 700;
    line-height: 1;
    color: var(--tc-danger);
    white-space: nowrap;
}

/* ==========================================================================
   3. MEDIA
   ========================================================================== */

.tc-card__media {
    position: relative;
    width: 100%;
    aspect-ratio: var(--tc-media-ratio);
    overflow: hidden;
    border-radius: var(--tc-media-radius);
    background: var(--tc-media-bg);
    flex: 0 0 auto;
}

@supports not (aspect-ratio: 1 / 1) {
    .tc-card__media::before { content: ""; display: block; padding-top: 133.33%; }
    .tc-card__media > .tc-card__viewport,
    .tc-card__media > .tc-card__media-link { position: absolute; inset: 0; }
}

.tc-card__viewport,
.tc-card__media > .tc-card__media-link { width: 100%; height: 100%; }

.tc-card__slide,
.tc-card__media-link {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
}

.tc-card__img {
    display: block;
    width: 100%;
    height: 100%;
    max-width: 100%;
    /* CONTAIN, not cover. The media box is now a store-configurable aspect (section 9b), so the
       frame no longer matches the photography it is given: a square photo in a 3:4 frame, or a
       portrait photo in a 1:1 one, would lose its edges to a `cover` crop. A product card must
       show the whole product - a cropped shoe or a beheaded bottle is a broken card, not a
       stylistic choice - so the image is fitted inside the frame and --tc-media-bg fills what
       is left over. */
    object-fit: contain;
}

/*
 * Minimal Swiper structural CSS, scoped to the card.
 *
 * assets/v6/css/style.css pulls the Swiper bundle in only through a plain CSS @import on
 * line 5, AFTER other rules have already been emitted. A CSS @import is honoured only when it
 * precedes every rule except @charset, so browsers drop it and Swiper's layout rules never
 * arrive: .swiper-wrapper is not a flex row and the slides stack vertically, showing the next
 * image underneath the current one.
 *
 * Rather than reorder the shared sheet (which would touch every slider on the site), the card
 * carries the few structural rules it needs, scoped to .tc-card__viewport. Harmless if the
 * bundle ever does load.
 */
/* GESTURE OWNERSHIP.
 *
 * The gallery Swiper is live on touch (measured: 9/9 PLP grid cards initialised at 390px),
 * but the viewport declared no touch-action, so it inherited auto. The browser therefore kept
 * BOTH axes for native scrolling and surrendered the horizontal one only after its own
 * scroll-intent heuristic had watched the gesture for several frames - which is the lag that
 * reads as a slow, sticky, half-ignored swipe, and why a quick reverse flick is often dropped.
 *
 * pan-y hands the horizontal axis to Swiper immediately while leaving the vertical axis to the
 * page: a vertical drag still scrolls the PLP, a horizontal drag moves the photos with no
 * arbitration delay. The tap through to the product link is unaffected - touch-action governs
 * panning, not clicks.
 *
 * Scoped to .swiper so it applies only once the gallery has actually initialised; a
 * single-image card keeps plain auto behaviour. No threshold change, no longer transition,
 * no gesture JS - Swiper touchAngle 40 / threshold 5 stay put and now get a clean signal. */
/* GESTURE OWNERSHIP - PRODUCT ROW (mobile Swiper engine only).
 * Proven, not assumed. On a 15-card row at 390px with touch-action:auto the row Swiper saw
 * ONE touchMove out of sixteen and setTranslate never fired. With pan-y it sees 16/16,
 * sliderMove 16, setTranslate 16, touches.diff 270 and translate lands at 612 - the browser
 * stops arbitrating the horizontal axis and hands it straight to Swiper, while vertical stays
 * with the page.
 *
 * Scoped by the mode attribute that init() sets, so the native-rail fallback (which wants the
 * browser to own both axes) and desktop are untouched. */
.tc-row__track.swiper[data-tc-row-mode="swiper-mobile"] {
    touch-action: pan-y;
}

.tc-card__viewport.swiper {
    touch-action: pan-y;
    position: relative;
    display: block;
    height: 100%;
    margin: 0;
    padding: 0;
    overflow: hidden;
    list-style: none;
    z-index: 1;
}

.tc-card__viewport .swiper-wrapper {
    position: relative;
    display: flex;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    box-sizing: content-box;
    transition-property: transform;
    transform: translate3d(0, 0, 0);
    z-index: 1;
}

.tc-card__viewport .swiper-slide {
    position: relative;
    flex-shrink: 0;
    width: 100%;
    height: 100%;
    transition-property: transform;
}

/* ==========================================================================
   4. GALLERY DOTS
   ==========================================================================
   Refined to the reference language: small, circular, tightly and evenly spaced, subtle.
   Inactive reads as a soft ring, active as a solid ink dot. Deliberately not Swiper's
   default heavy blue bullets.
   ========================================================================== */

/*
 * MEASURED off products-row-slider (card 1 media 252x302 @1440, sampled at native @2x):
 *
 *     bullet diameter   4.0      bullet gap    3.0     bullet pitch  7.0
 *     bottom offset    12.5      side offset  12.5
 *
 * The rail sits on the media's inline-END (left in RTL), opposite the QSM host, in both
 * reference sets.
 */
.tc-card__dots {
    position: absolute;
    inset-block-end: 12px;
    inset-inline-end: 12px;        /* opposite the QSM host, as both references show */
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 3px;
    width: auto !important;
    line-height: 0;
    pointer-events: none;          /* the rail must never block the media link */
}

/*
 * Dots sit directly on product photography, which may be light or dark. A single-colour
 * treatment disappears on one of the two, so inactive is a light fill and active is ink,
 * each carrying a faint counter-coloured ring for separation. Reads on both.
 */
/* Inactive: light centre inside a thin visible ring. The ring is drawn as a real border so it
   reads at 4px; a box-shadow hairline optically vanished against product photography. */
.tc-card__dots .swiper-pagination-bullet {
    pointer-events: auto;
    width: 5px;
    height: 5px;
    margin: 0 !important;
    border-radius: 50%;
    background: rgba(255, 255, 255, .9);
    border: 1px solid rgba(47, 47, 47, .55);
    box-sizing: border-box;
    box-shadow: none;                       /* no glow */
    opacity: 1;
    transition: background-color .18s ease, border-color .18s ease;
}

/* Active: solid dark, same diameter, so the rail never shifts as slides change. The two states
   differ by fill, which is what makes them separable at this size. */
.tc-card__dots .swiper-pagination-bullet-active {
    background: #2F2F2F;
    border-color: #2F2F2F;
}


/* ==========================================================================
   5. QSM HOST  —  SLOT ONLY (see firewall note)
   ==========================================================================
   QSM FIREWALL.

   QSM (assets/qsm/qsm.js + qsm.css) owns everything inside this host: markup, classes,
   dimensions, colours, icon, radius, shadow, states and behaviour. The card owns ONLY the
   slot — where the host sits and how much room is reserved — so the control lands at the
   identical position on every card in a row.

   No rule here targets a QSM-owned selector (.vp-product-item-theme-v2,
   .vp-quantity-supermarket-v3, .vp-quantity-theme-v1, .vp-input, …). An earlier revision set
   background/border-radius/box-shadow on .vp-product-item-theme-v2; that was a firewall
   violation and has been removed.

   THE CARD ADAPTS AROUND QSM. QSM DOES NOT ADAPT TO THE CARD.
   ========================================================================== */

/*
 * POSITION ONLY. Nothing here touches how QSM looks or behaves.
 *
 * Target measured off products-row-slider (card 1, media 252x302 @1440):
 *     control 32x32, bottom offset 12.0, inline-start (right in RTL) offset 12.0
 *
 * The control itself is 35x35 and is drawn entirely by qsm.css - unchanged, and out of scope.
 * The host box measures 0x0 because the control inside it is `position: relative` and overflows
 * it, and it renders 16px ABOVE the host's own bottom edge. With the host pinned at 10px the
 * control therefore landed 26px above the media bottom instead of 12. The -4px cancels that
 * 16px overhang: -4 + 16 = 12. It is an anchor correction on the card-owned slot, not a QSM
 * change - verified by re-measuring the control's own box, which is still 35x35.
 */
.tc-card__quickadd {
    position: absolute;
    inset-block-end: -2px;
    inset-inline-start: 12px;
    z-index: 3;
    /* Anchored to the media box, so its position is identical on every card regardless of
       title length, swatches, rating or discount. */
}

/* ==========================================================================
   6. BODY  —  reserved regions
   ========================================================================== */

.tc-card__body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    padding-block-start: var(--tc-gap-media-title);
    padding-inline: var(--tc-text-pad-x);
}

/*
 * Region order is explicit so a context can re-sequence the SAME markup instead of forking it.
 * Default (plp-mobile-1 / grid):  title, swatches, meta, pricing.
 * Horizontal row (products-row-slider): title, meta, pricing, swatches - see section 8.
 */
.tc-card__title-link { display: block; order: 1; }
.tc-card__swatches   { order: 2; }
.tc-card__meta       { order: 3; }
.tc-card__pricing    { order: 4; }

.tc-card__title {
    margin: 0;
    /* Titles carry brand and model names - "XIAOMI Redmi Note 13", "ARTDECO All in One" - and
       the Persian face renders that Latin run with the wrong shapes. The English face goes
       first and the base Persian face follows, so a mixed title keeps each script correct.
       Scoped to the title on purpose: prices, discount percentages and amounts stay on the
       theme's own numeric face, which is what the design calls for. */
    font-family: yekanbakhen, yekanbakh, sans-serif;
    font-size: 15px;
    /* Regular weight: the product name is body copy, not a heading - the emphasis in the text
       block belongs to the price. */
    font-weight: 400;
    /* One fifth more air between the two lines. Expressed against the token rather than as a
       literal, so every context keeps its own measured metric and gains exactly the same 20%:
       22px -> 26.4, the desktop card's 23px -> 27.6, the row's 21px -> 25.2. */
    line-height: calc(var(--tc-title-lh) * 1.2);
    color: var(--tc-ink);
    text-align: start;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    overflow-wrap: anywhere;
    /* Two-line CEILING, not a two-line box. The clamp above already stops a long title at two
       lines; reserving the second line as height meant a one-line title carried an empty line of
       air into the seam below it. As a max-height the box is exactly as tall as the lines it
       has, and the grid still equalises neighbouring cards. */
    /* +4px of vertical room. The clamp, the line count and the reserve token are unchanged -
       only the ceiling the text is allowed to fill, which was cutting the descenders off the
       second Persian line. Stated once, at the single place the ceiling is applied, so every
       context inherits the same 4px from its own measured token. */
    max-height: calc(var(--tc-title-h) + 4px);
}

/* Swatches ---------------------------------------------------------------- */

.tc-card__swatches {
    display: flex;
    align-items: center;
    justify-content: flex-start;   /* inline-start, as in the references */
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
    height: var(--tc-swatch-h);
    min-height: var(--tc-swatch-h);
    flex: 0 0 auto;
    margin-block-start: var(--tc-gap-title-swatch);
}

/* COLLAPSED, not reserved. A product with no variations must not carry a swatch-sized hole:
   `display: none` takes the rail's height AND its seam margin with it, so the body closes up.
   Equal card height is still guaranteed - the grid stretches every card to the tallest cell
   (min-height: 100%) - but the slack now lands at the card's bottom instead of in the middle
   of the text block. */
.tc-card__swatches--empty { display: none; }

.tc-card__swatch {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
}

.tc-card__swatch-chip {
    display: block;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: 1px solid var(--tc-hairline);
    background: #FFFFFF;
    object-fit: cover;
}

.tc-card__swatches--image .tc-card__swatch-chip { border-color: rgba(0, 0, 0, .10); }

/* SELECTED-SWATCH RING — WITHDRAWN.
 *
 * The ring existed so that clicking a colour could select its related gallery image. That
 * interaction is not shipping in this round, so a card was painting a pink selection state that
 * nothing had selected and that the shopper could not change - it read as an accidental
 * highlight on one arbitrary chip per card.
 *
 * WHAT IS WITHDRAWN IS THE DECORATION, NOT THE STATE. `_ProductCard.Swatches` still emits
 * `is-selected` from `ProductAttributeValueModel.IsPreSelected`, the swatch data and
 * `data-tc-swatch-image` are untouched, and product-card.js still previews a swatch's picture.
 * When variation selection lands, restoring the treatment is re-declaring the box-shadow here -
 * one rule, one place - rather than re-plumbing markup or model data.
 *
 * Declared as `none` rather than deleted for exactly that reason: the hook stays visible in the
 * sheet, and it also overrides any older cached generation of this file that still paints a ring.
 * box-shadow costs no layout in either direction, so this is not a layout change. */
.tc-card__swatch.is-selected .tc-card__swatch-chip {
    box-shadow: none;
}

.tc-card__swatch-chip--blank {
    background: repeating-linear-gradient(45deg, #F7F7F7 0 4px, #EDEDED 4px 8px);
}

.tc-card__swatch-more {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 22px;
    padding-inline: 5px;
    border-radius: 11px;
    background: #F5F5F5;
    color: var(--tc-muted);
    font-size: 10px;
    font-weight: 600;
    line-height: 1;
}

/* Meta: rating on the inline-end, shipping/stock on the inline-start ------- */

.tc-card__meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-direction: row-reverse;   /* rating (first in DOM) lands on the inline-end */
    gap: 8px;
    height: var(--tc-meta-h);
    min-height: var(--tc-meta-h);
    flex: 0 0 auto;
    margin-block-start: var(--tc-gap-swatch-meta);
}

.tc-card__rating { display: inline-flex; align-items: center; gap: 4px; }
.tc-card__rating-star { width: 13px; height: 13px; fill: var(--tc-star); flex: 0 0 auto; }
.tc-card__rating-value { font-size: 12px; font-weight: 600; color: var(--tc-ink); }

.tc-card__shipping {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 11px;
    font-weight: 600;
    color: var(--tc-success);
}
.tc-card__shipping-icon { width: 15px; height: 15px; fill: var(--tc-success); flex: 0 0 auto; }

/* Pricing ----------------------------------------------------------------- */

.tc-card__pricing {
    display: flex;
    flex-direction: column;
    /* The price is the card's bottom anchor: neighbouring cards must share a price baseline even
       when one has swatches and the other does not. The auto margin absorbs whatever equal-height
       slack the grid hands the card, so the flexible space lands BETWEEN the optional content and
       the price - never below the price, and never as a reserved placeholder row. The collapsed
       seams above still apply as the minimum gap. */
    margin-top: auto;
    padding-block-start: var(--tc-gap-meta-price);
}

/* Row 1: price and discount % on opposite edges; which edge is which is the context token. */
.tc-card__price-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-direction: var(--tc-price-dir);
    gap: 8px;
    height: var(--tc-price-h);
    min-height: var(--tc-price-h);
}

/* flex: 0 0 auto keeps the amount and its currency mark welded together. Without it the price
   group is a flex item that can be stretched by the row, which pushes the currency mark to the
   far edge and leaves a gap in the middle of the price. */
.tc-card__price {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    flex: 0 0 auto;
    white-space: nowrap;
}

/* NO `:only-child` NUDGE HERE, DELIBERATELY.
 *
 * There used to be `.tc-card__price-row > .tc-card__price:only-child { margin-inline-end: auto }`.
 * It made sense while the discount percentage shared this row: with both elements present the
 * row balanced itself, and a lone price on an undiscounted card was pushed back under the title
 * instead of floating against the far edge.
 *
 * The percentage moved to the old-price row, so `.tc-card__price` is now the only child on EVERY
 * card, discounted or not - the selector no longer distinguishes anything, and the auto margin
 * fired everywhere. `.tc-row` cancelled it explicitly; the PLP grid did not, so a grid card put
 * its current price on one edge and its struck price and percentage on the other. Same markup,
 * two different alignments.
 *
 * Deleting it (rather than adding a second cancel for the grid) puts every context back on the
 * one owner: `--tc-price-dir` / `--tc-oldprice-align`. In row-reverse, `space-between` already
 * parks a single child on the correct edge - which is exactly what the `.tc-row` cancel said. */

/* products-row-slider price glyph band is 21.5 CSS tall (digits plus the comma descender), which
   puts the numeral around 19-20px - the price is one of the card's two visual anchors, alongside
   the title. 15px read as body copy.

   THE SINGLE SOURCE OF TRUTH FOR THE CURRENT PRICE SIZE.
   The desktop grid and the row each used to restate this at 23, and the mobile row at 19, so the
   same component showed three different price sizes depending on where it was rendered. Those
   three declarations are gone rather than restated at this size: with the contexts silent, every card -
   home rows, PLP grid, special offers, and any other host of this component - reads the one
   value below at both breakpoints. The contexts keep the values that are genuinely theirs (the
   currency mark's height, the mobile colour and weight). */
.tc-card__price-value {
    font-size: 20px;
    font-weight: 700;
    line-height: 1.2;
    color: var(--tc-ink);
    direction: ltr;
    unicode-bidi: embed;
    /* The number's glyph band sits high in its 20px box, so on the row's centre line it reads
       above the toman mark rather than level with it. Nudged down onto the mark - the same
       device the currency image itself already uses one line below (`position: relative;
       top: 1px`), so the row keeps one convention. Relative, so nothing reflows: the price row,
       the old-price row and the card's height are all measured from the unmoved box. */
    position: relative;
    top: 4px;
}

/* Official Toman asset (pro-toman.svg), scaled off the numeral so the two read as one unit:
   ~0.95x the price font-size, sitting on the digits' optical baseline. Held slightly back in
   weight so the amount stays dominant, exactly as the reference does. */
/* Specificity is deliberate: a theme-level `img { width: 100% }` was winning over `width: auto`
   here, so the mark's CSS box stretched to the whole price line (97.6px for a 24px-tall mark).
   preserveAspectRatio then centred the artwork inside that box, which is what pushed the mark
   ~37px away from the numeral and made it read as small and detached. */
.tc-card__price .tc-card__price-currency {
    width: auto;
    max-width: none;
    min-width: 0;
    /* The asset carries internal padding inside its 25x26 viewBox, so the VISIBLE wordmark is
       smaller than the CSS box. Sized off the rendered glyph, not the box, to sit next to the
       numeral at the reference's optical weight. */
    height: 26px;
    flex: 0 0 auto;
    align-self: center;
    position: relative;
    top: 1px;
    opacity: 1;
}

/* The percentage travels with the OLD price, not with the current one - the two together are
   one statement ("was X, that is Y% off"), and splitting them put the discount at the far edge
   of the price line where it read as unrelated to either number. This is also what the mobile
   row already does (section 13 places both on grid row 2), so desktop now matches rather than
   carrying a second arrangement.

   READS AS "50%", AND NOT BY ACCIDENT.
   This used to author the sign first and rely on `direction: rtl` to flip it after the digits.
   That works, but it makes the visual order a product of bidi resolution: anything that changes
   the inherited direction - or an older cached copy of this sheet without the declaration -
   renders the source order instead, i.e. "%50". The digits are authored first now and the badge
   declares `direction: ltr`, so the two spans are laid out in source order as their own inline
   boxes and the result is the same in any surrounding context.

   The percentage leads the row: authored FIRST, so in the RTL storefront it lands on the RIGHT
   of the struck price, which is the arrangement the reference shows.

   Two steps up from where it started (15 -> 19) so it reads at a glance, while staying three
   points under the current price (22px) - the price is still the loudest thing in the block.
   Colour is untouched: it is the card's discount token, not the brand colour, and it
   deliberately does not follow the primary brand. */
.tc-card__discount-badge {
    display: inline-flex;
    align-items: center;
    font-size: 19px;
    font-weight: 700;
    color: var(--tc-accent);
    direction: ltr;
    flex: 0 0 auto;
}

/* Row 2: old price, always reserved so discounted and undiscounted cards match in height.
   Aligned to the same edge the price sits on, so it reads as the value being replaced. */
.tc-card__oldprice-row {
    display: flex;
    align-items: center;
    justify-content: var(--tc-oldprice-align);
    /* The struck price and the percentage are one compact group, so the row gets a gap and
       stops distributing them to opposite edges. */
    gap: 8px;
    /* The row is a fixed 18px band that hangs out of flow, so its children must not carry
       their fonts' default half-leading - with `normal` the 17px badge builds a ~22px line box
       and spills past the band it is supposed to sit inside. Both children are single-line and
       vertically centred by the row, so collapsing the leading changes nothing but the box. */
    line-height: 1;
    height: var(--tc-oldprice-h);
    min-height: var(--tc-oldprice-h);
    /* The old price HANGS below the current price instead of lifting it. In ProductRowSample the
       current-price line of a discounted card is level with its neighbours' and the struck price
       sits underneath; if the old-price row consumed layout height it would push the bottom-
       anchored price block up by its own height and break that line. The negative end margin
       takes it out of the height calculation while leaving it in normal flow - no absolute
       positioning, and no reserved slot on cards that have no old price. */
    margin-block-end: calc(-1 * var(--tc-oldprice-h));
}

.tc-card__old-price {
    /* One step up (14 -> 15). The struck price shares its row with the percentage now and has to
       hold its own beside it; it still sits well under the current price, which is the anchor. */
    font-size: 15px;
    font-weight: 500;
    color: var(--tc-muted);
    text-decoration: line-through;
    direction: ltr;
    unicode-bidi: embed;
    /* The pair is one run: it may never break between the number and its percentage. */
    white-space: nowrap;
}

/* The percentage lives INSIDE the struck price now, so it would inherit the strike. It is not a
   struck value - it is the discount - so the line is switched off for it and it keeps its own
   colour and weight. `inline-flex` keeps its two glyphs glued at any font size. */
.tc-card__old-price .tc-card__discount-badge {
    display: inline-flex;
    align-items: baseline;
    text-decoration: none;
    white-space: nowrap;
}

/* Optional rows collapse when the product genuinely has no data for them. Same rule as the
   swatch rail: no fake content, no reserved hole. */
.tc-card__meta--empty,
.tc-card__oldprice-row--empty { display: none; }

/* ...and the price re-times itself against whatever section actually precedes it. DOM order is
   title, swatches, meta, pricing, so the adjacent-sibling chain reads the collapsed state
   directly off the markup - no extra class on the pricing block, no JS, no reserved row. */
.tc-card__meta--empty + .tc-card__pricing {
    padding-block-start: var(--tc-gap-swatch-price);
}

.tc-card__swatches--empty + .tc-card__meta--empty + .tc-card__pricing {
    padding-block-start: var(--tc-gap-title-price);
}

/* Non-price states -------------------------------------------------------- */

.tc-card__state {
    display: inline-flex;
    align-items: center;
    /* `.tc-card__pricing` is a column flex container, so a bare inline-flex child still STRETCHES
       to the full card width - which rendered "ناموجود" as a full-width grey bar rather than as a
       pill. align-self makes it hug its own text.

       Which EDGE it hugs has to follow the price, and that differs by context: the mobile grid
       puts the price on the inline-start, the desktop grid and the row on the inline-end. That is
       already encoded in --tc-oldprice-align (the struck price tracks the same edge), so this
       reuses it rather than introducing a second token that could drift out of step. */
    align-self: var(--tc-oldprice-align);
    min-height: 24px;
    padding-inline: 8px;
    border-radius: 6px;
    font-size: 11px;
    font-weight: 600;
    line-height: 1;
}
.tc-card__state--out { background: #F5F5F5; color: var(--tc-muted); }
.tc-card__state--soon,
.tc-card__state--call { border: 1px solid var(--tc-success); color: var(--tc-success); }

/* ==========================================================================
   7. GRID CONTEXT
   ==========================================================================
   Desktop: 4 columns of 341.25 with 1px hairlines between them and no gutter — cards butt
   together and are separated by rules, exactly as PLP-FullPage shows.
   Mobile: 2 columns of 195 with an 8.5px gutter and no rules.
   ========================================================================== */

/*
 * The grid container is an existing theme element (.km-category-grid-product-list), so these
 * rules must neutralise the widths/floats the theme sets on .product-5 cells. Everything is
 * scoped under .tc-grid, so any list that does not opt in is untouched.
 */
.tc-grid {
    display: grid !important;
    grid-template-columns: repeat(var(--tc-cols, 2), minmax(0, 1fr));
    column-gap: var(--tc-col-gap, 12px);
    row-gap: var(--tc-row-gap, 24px);
    align-items: stretch;          /* equal card heights across the row */
    padding-inline: var(--tc-grid-pad, 16px);
    float: none;
}

/*
 * MOBILE GUTTER — the grid owns it, and it had to be taken back from the theme.
 *
 * BEFORE (measured, iPhone 12 and 414): column gap 8.5, outer inset 6.8 / 7.8, media gutter 8.5.
 * The card is flat and its media is full-bleed `object-fit: cover`, so 8.5px between two
 * photographs is not read as a gutter at all - the two columns fused into one band of imagery and
 * the outer 7px let that band run into the screen edge. plp-mobile-1's 8.5 was measured on a
 * reference whose cards still had a plate around them; once the plate went (section 10) the same
 * number stopped separating anything.
 *
 * AFTER: column gap 12, outer inset 16. 16 is not a new number - it is the CRI section inset that
 * `.vp-product-slider.tc-row` already uses on mobile, so the PLP grid and the product rows now
 * line up on one page margin. At 390 that leaves a 173px card, which lands on the mobile ROW card
 * width (172, section 9c): the two mobile product presentations are finally the same size.
 *
 * WHY THIS SELECTOR. The outer inset is not a specificity flourish. `.tc-grid` is (0,1,0) and the
 * theme's own `.km-category-grid-style .km-category-grid-product-list` (0,2,0) sets
 * `padding: 6px 0` inside `max-width: 959px` - which is why the declaration above computed to 0px
 * and the grid had no outer inset at all. Matching the theme's own shape and adding the opt-in
 * class is the minimum that actually wins; the gaps are left on `.tc-grid` because nothing
 * competes for those.
 */
@media only screen and (max-width: 991.98px) {
    /* MOBILE PLP GRID SPACING CONTRACT.
     *
     * One place defines every gap on this grid, so the next "too much whitespace" report is a
     * token change here rather than another one-off rule somewhere else:
     *
     *     outer gutter   4px   (--tc-grid-pad, plus the full-bleed below)
     *     column gap     0px   (--tc-col-gap)  cards meet on a seam
     *     row gap        4px   (--tc-row-gap)
     *     separation     1px   #EFEFEF hairlines drawn into the seams, unchanged
     *
     * The previous 12/16/16 was only half the story: the grid also inherits the site page gutter
     * from `.size` (98% width, ~7.8px a side), so the measured outer inset was 22.8px left and
     * 23.8px right - the wide bands in the report. Padding alone cannot reach 4px from there, so
     * the grid is widened to the viewport and given the gutter back as its own padding. Both
     * percentages resolve against the same containing block, so the pair cancels exactly and no
     * guessed negative margin is involved.
     *
     * Net effect on the card: 390 - 8 = 382 across two cells = 191 per cell, up from 165.7. */
    .tc-grid {
        --tc-col-gap: 0px;
        --tc-row-gap: 4px;
        --tc-grid-pad: 4px;
    }

    /* With a zero column gap the two cards share a seam, so the card needs its own inset to keep
       the title and price off that line. The media grows regardless - 175px of image against the
       165.7px it had when the grid was carrying 12px of gutter and 16px of padding. */
    .tc-grid .tc-card { --tc-pad-x: 8px; }

    /* THE FILLER WAS INSIDE THE CARD, NOT BETWEEN THE ROWS.
     *
     * `--tc-pad-b` reserves the space the DESKTOP composition needs for the hanging old-price row
     * (`.tc-card__oldprice-row` pulls itself out of the flow with a negative block-end margin, and
     * the padding is what stops it colliding with the card edge). Mobile does not compose that way
     * any more: the old price and the percentage are a real second grid row via `display: contents`,
     * so the negative margin is inert and nothing hangs. The grid card was still carrying the base
     * 20px reserve - measured as exactly 20px of empty card below the last painted element on every
     * cell - while the mobile ROW card had already been corrected to 12px.
     *
     * Set on `.tc-grid .tc-card` rather than on `.tc-grid`: the token is declared on `.tc-card`
     * itself, so an inherited value would lose to it. `.tc-row` and `.tc-deals` keep their own. */
    .tc-grid .tc-card { --tc-pad-b: 8px; }

    /* MOBILE PLP HAIRLINES.
     *
     * Desktop separates its four columns with real cell borders (see the min-width: 992px block).
     * Mobile cannot reuse those: the desktop grid butts its cards together with zero gutter, so a
     * border sits exactly on the seam, whereas the mobile grid has a 12px column and 16px row gap
     * and a border would land on the card edge rather than between the pair - and, with
     * border-box sizing, would take a pixel off every card.
     *
     * Drawn as pseudo-elements parked in the middle of the existing gutters instead: no box is
     * resized, no track changes, and the cards keep the exact geometry measured in the previous
     * pass. The negative insets place a decorative line into the gutter - they are not pulling
     * layout, and nothing is absolutely positioned that carries content.
     *
     * The token is restated here because the desktop declaration lives inside its own media query
     * and is not in scope on a phone. Same #EFEFEF, so both breakpoints separate with one colour. */
    .tc-grid { --tc-grid-rule: #EFEFEF; }

    .tc-grid > * { position: relative; }

    /* Vertical: once per row, on the inline-end of the first card, centred in the 12px gutter.
       Odd children only, so the outer edge of the row never gets a line. */
    .tc-grid > *:nth-child(odd)::after {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        inset-inline-end: calc(-1 * var(--tc-col-gap, 12px) / 2);
        width: 1px;
        background: var(--tc-grid-rule, #EFEFEF);
        pointer-events: none;
    }

    /* Horizontal: under each row, centred in the 16px row gutter. Drawn by both cells and extended
       half a gutter towards each other so the two halves meet and read as one continuous rule.
       The last row is excluded - a trailing line under the final products reads as a page edge. */
    .tc-grid > *:not(:nth-last-child(-n+2))::before {
        content: "";
        position: absolute;
        inset-inline: 0;
        bottom: calc(-1 * var(--tc-row-gap, 16px) / 2);
        height: 1px;
        background: var(--tc-grid-rule, #EFEFEF);
        pointer-events: none;
    }

    .tc-grid > *:nth-child(odd):not(:nth-last-child(-n+2))::before {
        inset-inline-end: calc(-1 * var(--tc-col-gap, 12px) / 2);
    }

    .tc-grid > *:nth-child(even):not(:nth-last-child(-n+2))::before {
        inset-inline-start: calc(-1 * var(--tc-col-gap, 12px) / 2);
    }

    /* Same selector shape as the theme's own `.km-category-grid-style
       .km-category-grid-product-list` (0,2,0), which is what actually governs this padding.
       The grid does NOT widen itself: the page gutter it used to inherit is removed at its real
       owner (the `.size` wrapper, in category.less), so 4px of padding here is the whole outer
       inset. A 100vw full-bleed was tried first and is deliberately not used - vw resolves against
       the initial containing block, which in this RTL page produced 8px of horizontal overflow
       (innerWidth 398 against a 390 client width) and shifted every measurement. */
    .km-category-grid-style .km-category-grid-product-list.tc-grid {
        padding-inline: var(--tc-grid-pad, 4px);

        /* The theme pulls this list 1px left (and 1px up) so its DESKTOP cell borders collapse
           onto one another. Mobile draws its separators as pseudo-elements in the seams and has
           no cell borders to collapse, so the pull is pure overflow here - it pushed the document
           1px wider than the viewport and made the two outer gutters asymmetric (measured 11px
           against -3px). Zeroed for the mobile grid only. */
        margin-inline: 0;
    }
}

.tc-grid > .tc-grid__cell,
.tc-grid > * {
    min-width: 0;
    width: auto !important;
    max-width: none !important;
    margin: 0 !important;
    padding: 0;
    float: none !important;
    display: flex;
}

/* min-height, not height: with `height: 100%` the card's height resolves against a grid row that
   was itself sized before the card's reserved regions were counted, so growing a region made the
   card overflow its cell instead of growing the row. min-height keeps the equal-height contract
   (every card still fills the tallest cell) while letting the row track the content. */
.tc-grid > * > .tc-card { min-height: 100%; }

/* Hairline separators are drawn by the cell, not the card, so the card stays context-free. */
.tc-grid__cell { border: 0 solid var(--tc-hairline, #E6E6E6); }

@media (min-width: 992px) {
    .tc-grid {
        --tc-cols: 4;
        --tc-col-gap: 0px;
        --tc-row-gap: 0px;
        --tc-grid-pad: 0px;
    }

    /* 1px hairline between columns, and between stacked rows.
     *
     * THE RULE BELOW USED TO DRAW NOTHING. It read `var(--tc-hairline)` with no fallback, and
     * `--tc-hairline` is declared on `.tc-card` - a DESCENDANT of the cell, not an ancestor -
     * so the cell never saw the token. An unresolved var() without a fallback is invalid at
     * computed-value time: the whole declaration was dropped and the cell kept the `border: 0`
     * from `.tc-grid__cell` above. Measured on the live PLP: every cell computed 0px on all
     * four sides, with the border colour falling through to the body ink.
     *
     * The separators get their OWN token, published on the grid where the cell can see it,
     * rather than shadowing `--tc-hairline`: the card keeps its #E6E6E6 for the seams it draws
     * inside itself, and the grid rules stay a shade lighter so a 4-column page reads as
     * separated without turning into a table of boxes. Fallbacks are stated so a future scope
     * slip degrades to a visible hairline instead of to nothing. */
    .tc-grid {
        --tc-grid-rule: #EFEFEF;
    }

    .tc-grid > * {
        border-inline-start: 1px solid var(--tc-grid-rule, #EFEFEF);
        border-block-end: 1px solid var(--tc-grid-rule, #EFEFEF);
    }
    .tc-grid > *:nth-child(4n) { border-inline-start: 0; }

    /* SOFT HOVER.
     *
     * Pointer devices only - `hover: hover` keeps it off touch, where :hover sticks after a tap.
     * A tinted plate plus a wide, low-opacity shadow, and no transform: lifting or scaling the
     * cell would move the media and re-flow the hairlines it shares with its neighbours. The
     * shadow is deliberately wider and fainter than the reference so it reads as a lift rather
     * than an outline.
     *
     * `position: relative; z-index: 1` is not decoration: cells paint in document order, so
     * without it the NEXT cell own hairline paints over the hovered cell shadow and cuts a hard
     * line through it. Scoped to the hover state, so nothing stacks at rest. */
    .tc-grid > * {
        transition: box-shadow .18s ease;
    }

    @media (hover: hover) {
        .tc-grid > *:hover {
            position: relative;
            z-index: 1;
            /* SHADOW ONLY - no background change.
             * This used to also tint the plate to #FCFCFD. Most of the catalogue is photographed
             * on white and the media frame is white (--tc-media-bg), so the tint stopped at the
             * image edge and drew a grey L around a white cut-out instead of lifting the card.
             * The cell keeps the white it already inherits from .container-plp-page at rest, and
             * hover adds nothing but the shadow. The transition drops background-color with it:
             * nothing animates that property any more. */
            box-shadow: 0 6px 24px rgba(0, 0, 0, .07);
        }
    }

    .tc-card {
        --tc-pad-x: 16px;
        --tc-text-pad-x: 0px;
        --tc-pad-b: 24px;   /* holds the hanging old-price row (18) */
        --tc-gap: 6px;
        /* Seams measured on the POPULATED PLP-FullPage cards (column 1, CSS @1920):
               title line 2 bottom 1076   -> meta band  1092.5   16.5
               meta bottom          1105  -> price      1118.5   13.5
               price bottom         1138.5-> old price  1150.5   12.0 */
        --tc-gap-title-swatch: 16px;
        --tc-gap-swatch-meta: 14px;
        --tc-gap-meta-price: 12px;
        --tc-gap-swatch-price: 18px;
        --tc-gap-title-price: 16px;
        --tc-title-lh: 23px;
        --tc-title-h: 46px;
        /* Same correction as mobile, one step wider: the desktop card is ~325 across, so the
           media edge needs a little more air under it than the 195 mobile card does. */
        --tc-gap-media-title: 20px;
        --tc-badge-h: 20px;
        --tc-swatch-h: 28px;
        --tc-meta-h: 20px;
        --tc-price-h: 28px;
        --tc-oldprice-h: 20px;   /* exactly contains the 19px discount badge; 22 pushed the hanging row onto the deals countdown hairline */
        --tc-media-radius: 6px;
    }

    .tc-card__title { font-size: 16px; }
    .tc-card__price-currency { height: 28px; }
    .tc-card__swatch-chip { width: 24px; height: 24px; }

    /* The POPULATED desktop cards in PLP-FullPage put the price on the inline-end and the pink
       percentage on the inline-start - the same arrangement as products-row-slider, and the
       opposite of plp-mobile-1. Two of the three reference sets agree, so the desktop grid
       follows them and mobile keeps its own. Same markup, one token. */
    .tc-card {
        --tc-price-dir: row-reverse;
        --tc-oldprice-align: flex-end;
    }
}

/* --------------------------------------------------------------------------
   7b. PLP LISTING WIDTH
   --------------------------------------------------------------------------
   PLP-FullPage @1920 puts the grid at x 122 - 1487: 1365 wide, pitch 341.25, sitting flush
   against the page container's margin edge rather than inside its 20px padding. The theme's
   own split (`.size` padding 20 + `grid-template-columns: 300px minmax(0,1fr)` + `gap: 16`)
   left the grid 1320 wide - a 3.2% shortfall that showed up as a visibly narrower card.

           reference   122 .. 1487   =  1365   pitch 341.25
           before      141 .. 1462   =  1321   pitch 330.30
           after       122 .. 1487   =  1365   pitch 341.25

   Deliberately scoped to the listing page shell (`.container-plp-page`) and to desktop only:
   `.size` is a site-wide container and this must not re-proportion any other page. The sidebar
   column keeps its 300px; the 45px comes from the container padding (40) and the column gap
   (16 -> 11), so no filter control is squeezed.
   -------------------------------------------------------------------------- */

@media (min-width: 992px) {
    .container-plp-page > .size { padding-inline: 0; }
    .container-plp-page .km-page-category-content { gap: 11px; }
}

/* ==========================================================================
   8. HORIZONTAL ROW CONTEXT
   ==========================================================================
   Applied by the eight shared row components (HomepageNewProducts, HomepageProducts,
   HomepageBestSellers, RelatedProducts, SimilarProducts, ProductsAlsoPurchased,
   CrossSellProducts, RecentlyViewedProductsBlock, SmartProductBasedUserViewedProducts). They
   all emit the same section/track markup, so opting in is one class per view and the row
   behaves identically everywhere.

   Everything here is an OUTER-CONTEXT override on the same card markup: no row-specific
   partial, no row-specific class on any card element.

   GEOMETRY — measured from products-row-slider.png (2880x1208 @2x, i.e. a 1440 viewport).
   Vertical anchors are relative to the media top; each was read off the reference's own
   band map, not interpolated from a single uniform gap:

       badge strip      22      glyph band 91.0 - 103.5
       media            302     114.0 - 414.5   (252 x 302.4 = 5:6)
       -> title         14      title line 1 glyph top 435.0
       title            42      two lines at 21
       -> meta          11      meta glyph band 485.5 - 501.0
       meta             20
       -> price          7      price glyph band 512.5 - 534.0
       price            26
       -> old price      4      old price glyph band 542.5 - 555.0
       old price        18
       -> swatches      14      swatch chips 575.0 - 594.0
       swatches         24      chips 20 diameter, 6 apart
                        ---
       card height     ~508     reference ~505 - 516

   Card width 252, card-to-card gutter 32. Because the card carries its own width, the row runs
   Swiper in `slidesPerView: 'auto'` - see TcProductRow in assets/product-card/product-card.js.

   ENGINE: SWIPER. The row is not an OwlCarousel and no `.owl-*` rule takes part in its layout
   or behaviour; sliders-rtl.js / sliders-ltr.js hand every .tc-card slider straight over.
   ========================================================================== */

/* Row-level tokens. Declared on the section so the card, the slide that measures it and the
   navigation all read the same numbers - and so none of them is a page-width decision. */
.tc-row {
    --tc-row-card-w: 252px;
}

.tc-row .tc-card {
    /* SQUARE media. Re-measured off ProductRowSample.png at native @2x: the hairline between
       two ProductBoxes runs y 230..1069, and its top edge IS the media top. The gallery dot rail
       sits at 12px above the media bottom (raw y 652..661 -> media bottom 685), so the media box
       is raw 230..685 = 455 tall against a 456-wide content box. That is 1:1, not the 5:6 this
       was carrying.

       The design set names the two ProductBox families directly and confirms it independently:
           productBox-moraba-*.png    ("moraba"  = square)    195 card, media 195x195  -> ROW
           productBox-mostatil-*.png  ("mostatil"= rectangle) 390 card, media 390x506  -> GRID (3:4)
       The grid already runs 3:4; the row is the square one. */
    /* Ratio is NOT restated here any more. The row pinned desktop to 1/1 while the mobile
       block below pinned the same cards to 3/4, so one card family rendered two different
       aspects purely by viewport - which is the defect. The base token (section 2,
       --tc-media-ratio: 3 / 4) is now the single default for grid and row at every width,
       and section 9b's .tc-media-square / .tc-media-portrait modifiers remain the supported
       way to set a different aspect per section without editing this file. */
    --tc-media-radius: 10px;

    --tc-pad-x: 0px;                 /* media runs the full card width, as the reference shows */
    --tc-text-pad-x: 10px;           /* ...but the text column keeps the card safe-area */
    --tc-pad-b: 24px;   /* holds the hanging old-price row (18) */

    --tc-badge-h: 22px;
    --tc-title-lh: 21px;
    --tc-title-h: 42px;
    --tc-meta-h: 20px;
    --tc-price-h: 26px;
    --tc-oldprice-h: 20px;   /* exactly contains the 19px discount badge; 22 pushed the hanging row onto the deals countdown hairline */
    --tc-swatch-h: 24px;

    /* Same half-leading correction the grid got: 14 rendered as ~17 of visible air and the title
       read as glued to the media. */
    --tc-gap-media-title: 18px;
    --tc-gap-title-swatch: 14px;     /* swatches sit last here, so this is the price seam */
    --tc-gap-swatch-meta: 11px;
    --tc-gap-meta-price: 7px;

    /* ProductRowSample reads as four separate groups - title, swatches, meta, price - so the
       seam under the swatch rail is a GROUP seam, not the 7px meta->price tick it was inheriting.
       That inheritance is what glued the price to the chips. */
    --tc-gap-swatch-price: 20px;
    --tc-gap-title-price: 16px;

    /* ProductRowSample.png is the row's authority. RE-MEASURED at native @2x rather than
       inherited from the previous reading, which had it backwards:

           card 3 (no discount)  content box x 831..1287   price ink x  831..1027   -> inline-END
           card 4 (discount)     content box x 1351..1807  price ink x 1351..1543
                                                           percentage x 1735..1805  -> inline-START
                                                           old price under the price, same edge

       So the CURRENT price sits on the inline-END (left in RTL) and the pink percentage on the
       inline-START (right), with the struck old price directly beneath the current price. In an
       RTL flex row that is `row-reverse`, and the old-price row follows it to `flex-end`. */
    --tc-price-dir: row-reverse;
    --tc-oldprice-align: flex-end;

    /* ONE source of truth for row card width. The Swiper slide reads the same token (see the
       Track section), so the card and the slide that measures it can never disagree. This is
       CARD geometry - it is not, and must not become, a page-width decision. */
    width: var(--tc-row-card-w);
}

/* The row uses the CANONICAL sequence - media, name, swatches, meta, price - exactly like the
   grid (ProductRowSample.png). It no longer re-orders the swatch rail below the price: the rail
   belongs with the product name, and the price is the bottom anchor in every context. Only
   sizing and seam values stay context-specific. */

.tc-row .tc-card__title { font-size: 15px; }
/* DESKTOP ONLY - AND SAID SO, WHICH IT WAS NOT.
 *
 * These two were written for the desktop row but sat at the top level, outside every media
 * query, so they applied on phones as well. At (0,2,0) they outrank the unqualified mobile
 * price rules in section 13, which is why the mobile row and the mobile PLP grid still
 * disagreed after the pricing selectors were unified: transplanting the identical price markup
 * into both hosts measured the same grid areas, the same justify-self and the same gaps, but a
 * 16px struck price and a 28px currency mark in the row against 15px and 21px in the grid.
 *
 * The values are unchanged; only their scope is corrected, so the shared mobile rules own
 * mobile in BOTH hosts and the desktop row keeps exactly what it had.
 *
 * Same theme `img { width: 100% }` fight as the grid, one specificity step higher because the
 * slider's own image rules also apply here. */
@media (min-width: 992px) {
    .tc-row .tc-card__price .tc-card__price-currency { width: auto; height: 28px; }
    .tc-row .tc-card__old-price { font-size: 16px; }
}
/* productBox-moraba-color.png (native 1x, card 195) measures the chips at 22 across with an 8
   gap. 20 read as a row of dots rather than as selectable variations. */
.tc-row .tc-card__swatch-chip { width: 22px; height: 22px; }
.tc-row .tc-card__swatches { gap: 6px; }

/* Track --------------------------------------------------------------------
   The row runs on SWIPER (TcProductRow in assets/product-card/product-card.js). Swiper's own
   bundle supplies the flex wrapper, the transform track and the touch behaviour; everything
   below is only what the CARD needs from the track.

   `slidesPerView: 'auto'` means the slide takes its width from the card inside it, so the
   canonical 252 card is the single source of card width at every container width.

   WIDTH OWNERSHIP. Nothing in this file gives the row a width. The chain is

       div.size2              max-width: 1336px + auto margins   <- OWNS THE PAGE WIDTH
         section.tc-row       100% of parent
           .tc-row__content   100% of parent
             .swiper          100% of parent, overflow hidden
               .tc-card       252px  <- card geometry, not page geometry

   Change the parent to .size (or anything else) and the row follows it with no edit here. */
/*
 * STRUCTURAL SWIPER CSS FOR THE ROW TRACK — and it is not optional.
 *
 * assets/v6/css/style.css pulls the Swiper bundle in through a plain CSS `@import` on line 5,
 * AFTER other rules have already been emitted. A CSS @import is honoured only when it precedes
 * every rule except @charset, so every browser drops it and Swiper's layout rules never arrive.
 * Without them .swiper-wrapper is not a flex row: the slides stack VERTICALLY, the track grows
 * to ~6200px tall, and every horizontal translate Swiper computes lands on nothing.
 *
 * The card gallery already carries its own copy of these rules for exactly this reason (see
 * section 3, scoped to .tc-card__viewport). The row carries the ones it needs, scoped to
 * .tc-row__track, rather than reordering the shared sheet - which would touch every slider on
 * the site. All of it is harmless if the bundle ever does load properly.
 */
.tc-row__track.swiper {
    position: relative;
    display: block;
    width: 100%;
    margin: 0;
    padding: 0;
    list-style: none;
    z-index: 1;
    /* The ONLY clip in the row, and it clips the track alone. The navigation is a sibling of
       this element precisely so it escapes it. */
    overflow: hidden;
}

.tc-row__track > .swiper-wrapper {
    position: relative;
    display: flex;
    flex-direction: row;
    width: 100%;
    /* height: auto, NOT 100% - the wrapper is sized by its tallest slide, which is what gives
       every card in the row a shared bottom and therefore a shared price baseline. */
    height: auto;
    margin: 0;
    padding: 0;
    box-sizing: content-box;
    transition-property: transform;
    transform: translate3d(0, 0, 0);
    z-index: 1;
}

.tc-row__track > .swiper-wrapper > .swiper-slide {
    flex-shrink: 0;
    transition-property: transform;
}

/* PARTIALLY VISIBLE EDGE CARD — never show a half-rendered price.
   ------------------------------------------------------------------------------------------
   The track is 1304 wide at a 284 pitch, so 4.59 cards fit and the last one is a 66%-visible
   "peek". The price sits on the card's inline-END, which is the edge the clip cuts, so on that
   card the track sliced the numeral mid-way and left just its last two zeros showing. At card
   scale that reads as an infinity symbol - and worse, it reads as a PRICE OF ZERO. `750,000`
   rendering as `۰۰` is misinformation, not a cosmetic blemish.

   ProductRowSample does the same thing structurally: its peek card's price is off-canvas
   entirely, so the peek shows imagery and a clipped title and no price at all. That is what is
   reproduced here - the media, title and swatch rail still peek and still signal "more to the
   left", but the price block is withheld until the card it belongs to is actually whole.

   TcProductRow marks the fully-visible slides (see markFullyVisible), re-evaluated on init,
   resize and every transition, so this tracks the real geometry rather than assuming which
   index is the edge. */
.tc-row__track > .swiper-wrapper > .swiper-slide:not(.tc-row__slide--whole) .tc-card__pricing {
    visibility: hidden;
}

/* SCOPING IS LOAD-BEARING. Every row rule below matches the row's OWN wrapper via the direct
   child chain `.tc-row__track > .swiper-wrapper > .swiper-slide`. A descendant selector such as
   `.tc-row__content .swiper-slide` also matches the CARD GALLERY's slides, because each card
   carries its own nested Swiper (.tc-card__viewport) inside the row - which handed every gallery
   slide the row's width, the row's stretch and the row's gutter separator. */
.tc-row__track > .swiper-wrapper { align-items: stretch; }

/* With `slidesPerView: 'auto'` Swiper measures the slide, so the slide must carry the width.
   It is the SAME token the card uses, so card width is stated once for the whole row. */
.tc-row__track > .swiper-wrapper > .swiper-slide {
    width: var(--tc-row-card-w);
    height: auto;
    display: flex;
}

.tc-row__track > .swiper-wrapper > .swiper-slide > .tc-card { min-height: 100%; }

/* Track width — CONTAINER-BASED, NOT FULL-BLEED ----------------------------
   The row track stays inside the normal centred Ryan container and lines up with every other
   homepage section. There is deliberately NO viewport break-out here.

   A previous revision pulled the track out to the viewport edges with
   `margin-inline: calc(50% - 50vw)` plus `overflow-x: clip`, on the reading that
   ProductRowSample shows partial cards bleeding off both edges. That is a property of the
   reference ARTBOARD, not of this storefront's layout: it made the product rows the only
   full-width sections on the page and broke their alignment with the banners, the category
   tiles and the section headings above them.

   Two things this section must NOT do, because they break navigation placement:
     * clip .tc-row or .tc-row__content on the x axis - the arrows live outside the track
     * pull the track full-bleed - the arrows would then have no side margin to sit in */
/* Header -------------------------------------------------------------------
   Reference composition: title on the inline-start, a view-all action on the opposite edge
   with a chevron, and the cards beginning below. The heading keeps the container inset while
   the track bleeds, which is what the reference shows. */
.tc-row__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
}

.tc-row__more {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    color: inherit;
}

.tc-row__more-chevron { width: 7px; height: 12px; fill: currentColor; flex: 0 0 auto; }

/* Row surface --------------------------------------------------------------
   ProductRowSample.png is the authority: the ProductBox row sits on the page's own LIGHT
   surface, not on a black band. The former dark treatment (full-bleed black pseudo-element,
   inverted ink tokens, held-back title, inverted Toman mark, light-on-dark swatch rings and
   carousel arrows) was deleted rather than recoloured, and the surface now belongs to EVERY row
   rather than to a `--dark` modifier. `.tc-row--dark` is still emitted by the HomepageNewProducts
   view; it carries nothing of its own now, so the class is inert and the two homepage rows are
   byte-identical in styling. */
.tc-row {
    position: relative;
    padding-block: 24px 28px;
}

/* Row surface: the RYAN PANEL, owned by the theme -----------------------------
   `.vp-product-slider { background: #fff; border-radius: 16px }` (assets/css/style.css:20399)
   is the theme's own product-row panel. The nine row views used to opt out of it with `no-bg`
   and this file re-asserted it; the views now simply do not opt out, so the surface comes from
   the theme unaided and nothing is duplicated here.

   This replaced a full-bleed `::before` that used `inset-inline: calc(50% - 50vw)`. That band
   spanned the viewport, which is what made the rows read as the only full-width sections on the
   homepage. The panel now ends exactly where the `.size2` container ends. */

/* The track runs edge to edge of that panel, so the panel needs an inset or the first card's
   media sits flush against the rounded corner - and Ryan's -22px navigation has nothing to be
   mounted on. Row layout only; it carries no page-width decision. */
.vp-product-slider.tc-row { padding-inline: 16px; }

/* Separation between neighbouring ProductBoxes ------------------------------
   The separator belongs to the GUTTER, not to the card. Drawn on the card it read as a border
   welded to the product image. Swiper puts the `spaceBetween` gutter in each slide's own margin,
   so the rule is drawn by the SLIDE, centred in that margin and inset vertically so it spans the
   card body rather than running the full media height.

   `inset-inline-start: -16px` is half the gutter on the slide's leading edge, which is the side
   Swiper actually margins in both directions - so one logical rule serves RTL and LTR.

   GUTTER WIDTH. ProductRowSample @2x: hairline pitch 520, card content box 456 -> gutter 64, i.e.
   32 CSS with the rule 16 from each neighbour. `spaceBetween: 32` in TcProductRow
   (assets/product-card/product-card.js) is the other half of this pair - the two must move
   together, and a change to either alone puts the rule off-centre. */
.tc-row__track > .swiper-wrapper > .swiper-slide { position: relative; }

.tc-row__track > .swiper-wrapper > .swiper-slide::before {
    content: "";
    position: absolute;
    inset-inline-start: -16px;
    top: var(--tc-badge-h, 22px);
    bottom: 0;
    width: 1px;
    background: var(--tc-hairline, #E6E6E6);
    pointer-events: none;
}

/* The leading edge of the first slide is the row's own edge, not a gutter between two cards. */
.tc-row__track > .swiper-wrapper > .swiper-slide:first-child::before { content: none; }

/* Carousel navigation ------------------------------------------------------
   RYAN'S PRODUCT-SLIDER CONTROL, driven by Swiper.

   THE AUTHORITY IS NOT THE GENERIC RYAN SLIDER NAV. Our rows carry `nav-slider product`, and
   the theme has a dedicated control for exactly that pair - a TALL ROUNDED PILL, not a square:

       assets/css/style.css:16308  .nav-slider .owl-nav [class*='owl-'] i
           width 40 · border-radius 16 · colour #5E5E5E · font-size 34
           background rgba(255,255,255,.3) · backdrop-filter blur(15px)
           box-shadow 0 0 8px rgba(0,0,0,.08)
       assets/css/style.css:16404  .nav-slider.product .owl-nav [class*='owl-'] i
           height 81                       <-- the product-slider override
       :16330/:16346  owl-prev { right: -22px }   owl-next { left: -22px }
       :16337/:16353  960-1199 -> -18px          :16341/:16357  <960 -> 0
       :16322  disabled { opacity: 0; visibility: hidden }
       :16326  :hover i { background: #fff }
       :16333/:16349  prev i { padding-left: 3px }  next i { padding-right: 3px }

   Measured on a live Owl `nav-slider product` host still on this page, the glyph plate computes
   40 x 81 - which is why an earlier revision that forced `height: auto` on it, and the 40x40
   square that replaced it, both read as foreign controls. The pill IS the Ryan product-slider
   navigation.

   Swiper cannot match those `.owl-*` selectors, so the declarations are restated against the
   row's own elements. Nothing here is invented; the only deliberate deviation from the Owl rule
   is the vertical anchor, which centres on the MEDIA band rather than on 50% of the whole card
   (the card is much taller than a legacy Ryan slide, so 50% put the control down by the title).

   The buttons are siblings of .swiper (see TcProductRow.buildNav) because .swiper is
   overflow:hidden; inside it, a control mounted outside the track edge would be clipped away. */
.tc-row {
    /* Ryan's product-slider plate: 40 wide, 81 tall, radius 16. */
    --tc-nav-w: 40px;
    --tc-nav-h: 81px;
    /* Ryan hangs it off the track edge. The row's white panel is inset 16px from the track, so
       -22px lands the pill mounted on the panel's edge, which is how Ryan's own product sliders
       read. Ryan's own breakpoint steps are kept. */
    --tc-nav-offset: -22px;
    /* The card's own tokens live on .tc-card, which is a DESCENDANT of the nav's ancestor chain,
       not an ancestor of it - so they are out of scope here and are restated. Keeping them as
       row-level tokens is what makes the calc below resolve at all; referencing --tc-badge-h
       here would silently invalidate the whole declaration. */
    --tc-nav-badge-h: 22px;   /* badge strip above the media, mirrors --tc-badge-h */
    /* Vertical centre of the MEDIA band, measured from the track's top edge. The media is 1:1
       and runs the full card width, so its height IS the card width token. */
    --tc-nav-top: calc(var(--tc-nav-badge-h) + var(--tc-row-card-w) / 2 - var(--tc-nav-h) / 2);
}

@media only screen and (min-width: 960px) and (max-width: 1199px) {
    .tc-row { --tc-nav-offset: -18px; }
}

@media only screen and (max-width: 959px) {
    .tc-row { --tc-nav-offset: 0px; }
}

/* The nav is positioned against the TRACK's box, so the anchor is stated rather than inherited
   from whatever the theme happens to set on .vp-content. */
.tc-row__content { position: relative; }

.tc-row__nav {
    position: absolute;
    top: var(--tc-nav-top);
    z-index: 4;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--tc-nav-w);
    height: var(--tc-nav-h);
    padding: 0;
    border: 0;
    border-radius: 16px;
    background: rgba(255, 255, 255, .3);
    -webkit-backdrop-filter: blur(15px);
    backdrop-filter: blur(15px);
    box-shadow: 0 0 8px rgba(0, 0, 0, .08);
    color: #5E5E5E;
    cursor: pointer;
    transition: all .3s ease-in-out;
}

.tc-row__nav i {
    font-size: 34px;
    line-height: 1;
    color: inherit;
}

/* Ryan nudges the chevron 3px against its own side so it reads optically centred in the pill. */
.tc-row__nav--prev { inset-inline-start: var(--tc-nav-offset); }
.tc-row__nav--next { inset-inline-end: var(--tc-nav-offset); }
.tc-row__nav--prev i { padding-inline-start: 3px; }
.tc-row__nav--next i { padding-inline-end: 3px; }

/* Ryan's own hover: the frosted plate goes solid. */
.tc-row__nav:hover { background: #FFFFFF; color: #5E5E5E; }

/* Swiper writes these classes (see the navigation config in TcProductRow). `--disabled` is an
   edge; `--locked` is a row short enough not to need navigation at all. Ryan hides rather than
   greys out, so both go away completely. */
.tc-row__nav--disabled,
.tc-row__nav--locked {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
}

/* ==========================================================================
   8b. DEALS CONTEXT  (پیشنهاد شگفت‌انگیز / تخفیف‌های ویژه)
   ==========================================================================
   The Deals section was the last general product presentation still running the
   pre-Haxamanesh `.product_box.offer` card. Themes/Ryan/Views/Shared/_HomepageDealsProductBox
   now renders the canonical _ProductCard, so everything below is OUTER CONTEXT on the same
   card - no deals card partial, no deals-only class on any card element. The only markup
   this context adds is the countdown, which the canonical card has no concept of.

   Membership: the desktop track is a `.vp-slider` holding `.tc-card`, so TcProductRow claims
   it and it becomes a Swiper row like every other; the mobile branch is a native scroller and
   carries `tc-row` in the markup for the same tokens.

   WHY A PLATE HERE AND NOWHERE ELSE. Every other row sits on the theme's white
   `.vp-product-slider` panel, so the canonical card is deliberately transparent. This section
   paints its OWN decorative background (an admin-chosen image or colour on
   `.vp-special-discount`), and card text on top of that artwork is unreadable. The plate is
   therefore container presentation for this surface - the card inside it is untouched.
   ========================================================================== */

/* Neither host owns page width; both sit inside the section's own padding. */
.tc-deals.tc-row { padding-block: 0; }

/* The row must be able to SHRINK to its container.
 *
 * The mobile Deals section (`.amazing-offer`) is a flex container in the theme's own CSS, so this
 * row host is a flex item - and a flex item defaults to `min-width: auto`, meaning it refuses to
 * shrink below its content's min-content width. This item's content is a Swiper track whose
 * natural width is the sum of every slide (2 x 252 + 32 = 568 on this catalogue), so the row blew
 * the section out to 568px inside a 390px viewport and the whole page scrolled sideways.
 *
 * `min-width: 0` is the flexbox opt-out of that floor, not a magic number: it lets `width: 100%`
 * on `.tc-row__track.swiper` finally resolve against the space the container actually has. The row
 * still owns no page width - it takes whatever `.amazing-offer` / `.size2` give it. */
.tc-deals {
    min-width: 0;
    width: 100%;
}

/* EQUAL HEIGHT is inherited, never declared.
 *
 * `min-height`, deliberately not `height: 100%`. A flex item with a definite cross size is not
 * stretched, so `height: 100%` made the plate opt OUT of any equalising parent and size to its own
 * content - which is exactly how the mobile row ended up with an 18px plate-height spread and two
 * different timer baselines. With min-height the plate takes the row's line height when there is
 * one and still fills its slide, so no fixed height and no per-product rule is needed anywhere. */
.tc-deal {
    display: flex;
    flex-direction: column;
    width: 100%;
    min-height: 100%;
    padding: 10px 10px 14px;
    border-radius: 16px;
    background: #FFFFFF;
    box-sizing: border-box;
}

/* The card fills the plate; the plate, not the card, carries the width. Without this the
   252px on `.tc-row .tc-card` would fight the plate's own padding and overflow it. */
.tc-deal .tc-card {
    width: 100%;
    flex: 1 1 auto;
    /* the plate supplies the safe-area, so the card's own text inset would double it */
    --tc-text-pad-x: 0px;
    /* the countdown sits below, so the card no longer needs to reserve the hanging row's air */
    --tc-pad-b: 10px;
}

/* The slide carries the row's line height - `.tc-row__track > .swiper-wrapper` already stretches
   every slide to the tallest one, in section 8 - so the plate only has to fill the slide it is
   given. That single rule is the whole equalisation: no fixed height, no per-product rule, and
   nothing that has to know which products have a rating or a discount.
   Both breakpoints run this same path now - the mobile branch emits the same .vp-slider markup -
   which is why there is no mobile-only equalisation rule here. */
.tc-deals .tc-row__track > .swiper-wrapper > .swiper-slide > .tc-deal { height: 100%; }

/* No gutter hairline in this context: the plates already separate one deal from the next, and
   a rule drawn between two white tiles on a photographic background reads as damage. */
.tc-deals .tc-row__track > .swiper-wrapper > .swiper-slide::before { content: none; }

/* Countdown ---------------------------------------------------------------
   Same rail the legacy deals card showed - digits, clock glyph, sale progress bar - re-seated
   under the canonical card. Colours are the card's own danger token, not a new palette. */
.tc-deal__timer {
    flex: 0 0 auto;
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--tc-hairline, #E6E6E6);
}

.tc-deal__timer .vp-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    color: var(--tc-danger, #ED303D);
}

.tc-deal__timer .vp-discount-timer-time-v2 {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    font-size: 15px;
    font-weight: 700;
    line-height: 1;
    direction: ltr;
}

.tc-deal__timer .vp-row i { font-size: 15px; line-height: 1; }

.tc-deal__timer .vp-progress {
    height: 6px;
    margin-top: 8px;
    border-radius: 999px;
    background: #FADBDD;
    overflow: hidden;
}

.tc-deal__timer .vp-progress-bar {
    height: 100%;
    border-radius: inherit;
    background: var(--tc-danger, #ED303D);
}

/* ==========================================================================
   9. RESPONSIVE / ACCESSIBILITY
   ========================================================================== */

@media (max-width: 991.98px) {
    /* Larger touch target for the dots without changing how they look. */
    .tc-card__dots .swiper-pagination-bullet { position: relative; }
    .tc-card__dots .swiper-pagination-bullet::after { content: ""; position: absolute; inset: -8px; }
}

@media (prefers-reduced-motion: reduce) {
    .tc-card__viewport.swiper .swiper-wrapper { transition-duration: 0ms !important; }
    .tc-card__dots .swiper-pagination-bullet { transition: none; }
}

/* ==========================================================================
   9b. MEDIA RATIO — CONFIGURABLE PER ROW
   ==========================================================================
   The card's media aspect has always been a token (`--tc-media-ratio`); what was missing was a
   supported way to SET it per section without editing this file. These modifiers are that way.

   Put one on the row section - or on any ancestor of the cards - and every card inside follows:

       <section class="vp-product-slider tc-row tc-row--media-portrait"> ...
       <section class="vp-product-slider tc-row tc-row--media-square">  ...

   They are context classes on the SAME card markup - not a second card, not a per-product flag -
   so a row switches ratio by changing one class in its view, and a future third ratio is one
   more modifier here. The `.tc-media-*` aliases exist so a non-row container (a widget zone, a
   landing block) can opt in with the same vocabulary.

   Written after the row and grid context blocks and matched at (0,2,0), so a modifier beats the
   context default it sits inside, at every breakpoint.

   STORE-LEVEL RATIO. `<body>` carries one of these two classes on every storefront page,
   emitted by Themes/Ryan/Views/Shared/_Root.Head.cshtml from the store-scoped
   StoreInformationCustomFrameworkSettings.ProductCardImageAspectRatio setting. Body is simply
   the outermost opt-in container, so this needs no rule of its own: a store selects Square or
   Portrait in the admin and every .tc-card on the page follows. Portrait is the default and is
   identical to the base token, so a store that has never touched the setting renders exactly as
   it did before the setting existed.

   A narrower modifier still wins, by design - a section that deliberately sets its own aspect
   (the mobile deals plate at section 16, at (0,3,0)) keeps it whatever the store chooses.
   ========================================================================== */

.tc-row--media-portrait .tc-card,
.tc-media-portrait .tc-card { --tc-media-ratio: 3 / 4; }

.tc-row--media-square .tc-card,
.tc-media-square .tc-card { --tc-media-ratio: 1 / 1; }

/* ==========================================================================
   9c. MOBILE PRODUCT ROW  —  native rail, single image, no arrows
   ==========================================================================
   BREAKPOINT 991.98px. This is not a free choice: it is the same edge as
   CONFIG.desktopMinWidth (992) in the gallery controller and ROW_DESKTOP_MIN_WIDTH (992) in
   TcProductRow, and the same edge the cart-recommendation sheet switches its own rail at. A
   viewport between two different edges would get JS from one mode and CSS from the other.

   INTERACTION MODEL. One model for the whole mobile storefront: a native overflow-x rail with
   scroll-snap. The cart-recommendation sheet already browses products this way, so the rows do
   too instead of running Swiper here. TcProductRow adds `.tc-row__track--rail` and leaves the
   .swiper-wrapper / .swiper-slide markup alone, so the rail reuses the same boxes and crossing
   the breakpoint is only a class swap.

   GEOMETRY comes from the CRI card, which is the agreed benchmark: 172 wide, 3:4 portrait
   media, 8px between neighbours.
   ========================================================================== */

@media only screen and (max-width: 991.98px) {

    /* CRI card geometry, adopted wholesale. 252 was a desktop number: on a 390px screen it
       showed one and a half products; 172 shows two and a bit, which is what the sheet does. */
    .tc-row {
        --tc-row-card-w: 172px;
        --tc-row-rail-gap: 8px;
    }

    /* PORTRAIT is the mobile default, matching the CRI card. The row's desktop default is
       square; that stays. An explicit .tc-row--media-square still wins - it is declared in 9b
       against `.tc-card`, one specificity step above this.
       Radius 0 for the same reason: the CRI media is a flat square-cornered image, and a 10px
       rounded tile next to it reads as a different card family. */
    .tc-row .tc-card {
        /* Ratio is NOT restated here. It used to repeat the base token's own 3/4, which was
           harmless while 3/4 was the only default but pinned mobile rows to portrait the moment
           the ratio became store-configurable: this rule is (0,2,0) and declared after section
           9b, so it beat the store's own .tc-media-* modifier and a Square store would have kept
           portrait rows on mobile only. Dropping the declaration lets the base token - and so the
           store setting - own the aspect at every width, which is what section 9's comment
           already claims. Radius still differs on mobile and stays. */
        --tc-media-radius: 0px;
    }

    /* ---- the rail ---- */
    .tc-row__track--rail {
        display: block;
        overflow-x: auto;
        overflow-y: hidden;
        -webkit-overflow-scrolling: touch;
        overscroll-behavior-x: contain;
        scroll-snap-type: x proximity;
        scrollbar-width: none;
    }

    .tc-row__track--rail::-webkit-scrollbar { display: none; }

    /* Swiper's transform must not survive a mode swap; `width: max-content` is what lets the
       flex line grow past the viewport so it can actually be scrolled. */
    .tc-row__track--rail > .swiper-wrapper {
        display: flex;
        flex-direction: row;
        align-items: stretch;
        width: max-content;
        gap: var(--tc-row-rail-gap);
        transform: none !important;
        transition: none !important;
    }

    .tc-row__track--rail > .swiper-wrapper > .swiper-slide {
        flex: 0 0 auto;
        width: var(--tc-row-card-w);
        height: auto;
        /* Swiper writes the gutter as a slide margin; the rail uses `gap` instead, so any
           leftover inline margin would double it. */
        margin: 0 !important;
        scroll-snap-align: start;
    }

    /* The gutter hairline belongs to the Swiper gutter, which the rail does not have. */
    .tc-row__track--rail > .swiper-wrapper > .swiper-slide::before { content: none; }

    /* ---- no arrows on mobile ----
       Touch-first: the rail is dragged, not clicked. TcProductRow does not build the buttons in
       rail mode; this also covers a row left over from a desktop->mobile resize. */
    .tc-row__nav { display: none !important; }

    /* ---- single image per card in a row ----
       The gallery controller skips row cards on mobile (see product-card.js), so the server's
       multi-slide markup is left inert. Show the first slide, drop the rest and the dot rail.
       Scoped by `.vp-slider` - the row track - so the PLP / category grid keeps its galleries. */
    .vp-slider .tc-card__media--gallery .tc-card__viewport > .swiper-wrapper {
        display: block;
        width: 100%;
        transform: none !important;
    }

    .vp-slider .tc-card__media--gallery .tc-card__viewport > .swiper-wrapper > .swiper-slide {
        width: 100% !important;
        margin: 0 !important;
    }

    .vp-slider .tc-card__media--gallery .tc-card__viewport > .swiper-wrapper > .swiper-slide + .swiper-slide {
        display: none;
    }

    .vp-slider .tc-card__dots { display: none; }

    /* ======================================================================
       CRI IS THE VISUAL AUTHORITY FOR THE WHOLE MOBILE ROW
       ======================================================================
       Not just the card box - the section header, its seams, the ink and the price
       composition too. Every number below was read off the rendered
       .tc-cart-rec__section on an iPhone 12, so the two read as one component family:

           section inline padding   16 / 16
           section block padding    14 top
           section title            15px / 700 / #292D32, start-aligned
           head padding             0 0 12       (the 12 IS the head->rail seam)
           card title               13px / 400 / line-height 1.75 / 2 lines reserved
           ink                      #292D32      (the row was #2F2F2F)
           old price                #BCBCBC 12/400
           discount %               #ED2E7E 12/700
           price block              price on line 1, old price + % TOGETHER on line 2
       ====================================================================== */

    /* --- section shell --- */
    .vp-product-slider.tc-row {
        padding-block: 14px 20px;
    }

    /* --- section header ---
       The theme's .header-section carries its own 16px padding and an m-b-16 margin; CRI's head
       has neither - its only seam is a 12px bottom padding, which is also the head->rail gap. */
    .tc-row .tc-row__head {
        padding: 0 0 12px;
        margin: 0;
    }

    /* `h2` as well as `h3`: the head selector was written against the row views that emit an
       h3, but SimilarProducts/Default.cshtml emits `<h2 class="vp-title">`. It therefore missed
       that one heading, which fell through to a generic rule and rendered #4A5F73 / 27px while
       every other product-row heading on both pages measured #292D32 / 24px. Adding the tag is
       what makes the shared contract actually shared - no new colour is introduced here.

       Font size is deliberately left to lose: `.vp-product-slider .vp-header > .vp-title`
       (0,3,0) keeps its 18px over the 15px below, exactly as it already does for the home rows'
       `h3.vp-title`. So the corrected heading matches the HOME `.vp-title` headings it belongs
       with - 18px / 700 / 24px / #292D32 / YekanBakhEn - rather than being forced into the
       `.header-section__title` family it is not part of. */
    .tc-row .header-section__title,
    .tc-row .tc-row__head h2,
    .tc-row .tc-row__head h3 {
        font-size: 15px;
        font-weight: 700;
        line-height: 24px;
        color: #292D32;
    }

    /* CRI's restrained secondary text (#8f8f8f / 12px) is the model for the row's view-all
       action - it is a supporting link, not a second heading competing with the title. */
    .tc-row .tc-row__more {
        font-size: 12px;
        font-weight: 500;
        color: #494A5C;
    }

    .tc-row .tc-row__more-chevron { width: 6px; height: 10px; }

    /* --- card ink + type, from the CRI card --- */
    .tc-row .tc-card {
        --tc-ink: #292D32;
        --tc-muted: #8F8F8F;
        --tc-gap-media-title: 10px;
        --tc-gap-title-swatch: 8px;
        /* SWATCH -> PRICE SEAM.
           Measured before: the swatch rail's bottom sat 10px above the price glyph, and with the
           chips being circles the optical distance was smaller still - the price read as a seventh
           chip on the same rail rather than as the next region of the card.
           The other three contexts already separate them properly (mobile grid 18, desktop row 21,
           deals 34), so this was the mobile row alone drifting, not a system-wide value.
           18 puts it on the mobile GRID's number, which is the one measured off plp-mobile-1, and
           inside the 16-20 band. The pair below sums to the same 18 for the rating case, so the
           seam does not change depending on whether a product happens to have reviews.
           No card grows a fixed height: the seam is a minimum, .tc-card__pricing still carries
           `margin-top: auto`, and the rail still stretches every slide - so the price stays
           bottom-anchored and neighbouring price baselines are unchanged. */
        --tc-gap-swatch-meta: 12px;
        --tc-gap-meta-price: 6px;
        --tc-gap-swatch-price: 18px;
        --tc-gap-title-price: 10px;
        --tc-pad-b: 12px;
        /* CRI reserves two 22.75px lines for the title so neighbouring cards keep one baseline */
        --tc-title-lh: 23px;
        --tc-title-h: 46px;
    }

    /* 400, not 600: CRI's product name is regular weight. The heavier row title was the single
       most obvious "different design system" tell next to the sheet. */
    .tc-row .tc-card__title {
        font-size: 13px;
        font-weight: 400;
        line-height: 23px;
        color: #292D32;
        /* CRI RESERVES the two-line box (min-height 46) rather than treating it as a ceiling.
           In a rail of independent cards that reserve is what keeps every price on one
           baseline - without it a one-line title lifts its card's price above its neighbour's.
           The desktop row keeps the max-height-only behaviour. */
        min-height: var(--tc-title-h);
    }

    /* ONE MOBILE PRICE SYSTEM.
     *
     * These four declarations, and the composition block below them, used to be scoped to
     * `.tc-row`. That left mobile with TWO price presentations from one markup: a home-row or
     * special-offer card laid its price, struck price and percentage out on a two-row grid,
     * while the SAME card in the PLP grid fell through to the desktop composition - price row
     * plus a hanging old-price row - at different sizes, weights and greys. The `.tc-row`
     * qualifier is dropped rather than duplicated for `.tc-grid`, so every mobile host of this
     * component (PLP grid, home rows, special offers, search overlay, related rails) reads one
     * system. Desktop is untouched: all of this sits inside the mobile query.
     *
     * Current price drops two steps, 20 -> 16. The struck price and the percentage keep the
     * sizes they already had, so the gap between the anchor and the secondary numbers narrows
     * rather than the whole block shrinking - the price is still the loudest thing in the card,
     * and 16px stays well clear of the 13px title.
     *
     * The colour moves from a hardcoded #292D32 to the card ink token it was a near-duplicate
     * of, which is what makes this one system rather than two that happen to look alike. */
    /* The digits read ~4px high against the Toman mark: the two are centred as boxes by the
       row's `align-items: center`, but the numeral's box carries half-leading above and below
       its glyphs while the mark's box is the artwork, so optical centres do not coincide with
       box centres. Corrected optically, on the digits only. `transform` is the right tool here
       precisely because it does NOT participate in layout - the mark, the gap, the struck price
       and the percentage all keep the positions they already had, and nothing reflows. It
       applies because the value is a flex item of `.tc-card__price` and is therefore
       blockified. Mobile only: this whole block is inside the max-width 991.98px query. */
    .tc-card__price-value { font-size: 16px; font-weight: 700; color: var(--tc-ink); transform: translateY(4px); }
    .tc-card__price .tc-card__price-currency { height: 21px; }
    .tc-card__old-price { font-size: 15px; font-weight: 400; color: #BCBCBC; }
    .tc-card__discount-badge { font-size: 16px; font-weight: 700; color: #ED2E7E; }
    .tc-row .tc-card__swatch-chip { width: 18px; height: 18px; }
    .tc-row .tc-card__swatches { gap: 6px; }

    /* --- price composition, rebuilt to CRI's ---
       CRI:  line 1  price + Toman, on the inline-START
             line 2  struck old price and the % TOGETHER, gap 8
       The row put the % on the price line at the opposite edge and the old price alone beneath.
       `display: contents` lifts the two markup rows out of the way so all four elements become
       items of one grid - so the composition changes with NO change to the shared card markup,
       and the desktop/PLP arrangement is untouched. */
    .tc-card__pricing {
        display: grid;
        grid-template-columns: auto auto 1fr;
        grid-template-rows: auto minmax(var(--tc-oldprice-h, 17px), auto);
        column-gap: 8px;
        row-gap: 4px;
        align-items: center;
    }

    .tc-card__price-row,
    .tc-card__oldprice-row { display: contents; }

    .tc-card__price {
        grid-column: 1 / -1;
        grid-row: 1;
        justify-self: start;
        margin-inline-end: 0;
    }

    /* One grid item on row 2, not two. The percentage is nested inside the struck price, so a
       track of its own would have nothing to place - and placing them in separate tracks is
       exactly what let the percentage drift away from the number it belongs to. */
    .tc-card__old-price { grid-column: 1 / -1; grid-row: 2; justify-self: start; }

    /* MOBILE PLP: THE PRICING BLOCK SITS ON THE OTHER EDGE.
     *
     * Rows and special offers keep the price on the inline-start (the right, in this RTL
     * storefront); the PLP grid card puts the whole block on the opposite edge instead. That is a
     * PLACEMENT change only - same two-row structure, same grid areas, same 8px column gap and
     * 4px row gap, same 16/15/16 sizes, same colours, strike and percentage.
     *
     * Done by moving the `1fr` filler from the last column to the first, so the two sized columns
     * are pushed to the far edge, and flipping the price row to `justify-self: end`. The old price
     * stays on the inline-start of the pair and the percentage on its inline-end, exactly as in the
     * rows - the pair travels together rather than being re-ordered. Scoped to `.tc-grid`, which is
     * the PLP host: `.tc-row` and `.tc-deals` are untouched, and this whole block is inside the
     * mobile query so desktop PLP keeps its own composition. */
    /* MOBILE PLP TITLE, one step down.
     *
     * The grid card inherited the base 15px/22px while the mobile ROW card already runs 13px, so
     * the PLP title read heavier than the price beside it and heavier than the same component in
     * a home rail. 13px/20px puts the two mobile presentations on one size and restores the
     * hierarchy: the 16px price is again the loudest thing in the card.
     *
     * Size and leading only - the English-first family, the 600 weight and the 2-line clamp are
     * untouched, and `.tc-row` / `.tc-deals` keep their own title rules. Desktop PLP is outside
     * this query and stays at 16px. */
    /* One step down from 13/20. The PLP card is the densest context - two per row, against the
       row card's 182px - and the title read larger than the reference at that width.
       Mobile PLP only: `.tc-row` and `.tc-deals` keep their own size. */
    .tc-grid .tc-card__title { font-size: 12px; line-height: 18px; }

    /* THE EMPTY BAND UNDER A PLP CARD WAS A RESERVED PRICE ROW, NOT PADDING.
     *
     * `.tc-card__pricing` reserves its second row at `minmax(20px, auto)` so that in a RAIL every
     * card keeps one price baseline whether or not it carries an old price. The PLP grid does not
     * need that: `align-items: stretch` already equalises the cells, so the reserve only bought a
     * permanently empty 20px row plus its 4px row-gap. Measured: 31px of empty card below the last
     * painted element against an 8px padding-bottom - the other 23px was this.
     *
     * `auto auto` lets the row collapse when there is nothing in it and size to content when there
     * is, so discounted cards are unchanged. Scoped to `.tc-grid`; `.tc-row` and `.tc-deals` keep
     * the reserve their rails depend on. */
    .tc-grid .tc-card__pricing {
        grid-template-columns: 1fr auto auto;
        grid-template-rows: auto auto;
    }
    .tc-grid .tc-card__price { justify-self: end; }
    .tc-grid .tc-card__old-price { grid-column: 1 / -1; justify-self: end; }

    /* HOME / RAIL CARDS: the pricing block sits on the same edge as the PLP grid card.
     *
     * `.tc-card__price` defaults to `justify-self: start`, which in this RTL storefront is the
     * RIGHT edge - measured on a home rail card the price box ended flush with the card's right
     * side (0px of gap). The requested treatment is the opposite edge, matching the PLP card that
     * already flips it. Same mechanism as `.tc-grid` above: the `1fr` filler moves to the first
     * column so the two sized tracks are pushed across, and both rows flip together so the struck
     * price and its percentage travel with the current price rather than splitting.
     *
     * Placement only - no size, colour, gap or structure changes - and scoped to `.tc-row`, so
     * `.tc-deals` and the PLP grid keep exactly what they have. Inside the mobile query, so
     * desktop rails are untouched. */
    .tc-row .tc-card__pricing { grid-template-columns: 1fr auto auto; }
    .tc-row .tc-card__price { justify-self: end; }
    .tc-row .tc-card__old-price { grid-column: 1 / -1; justify-self: end; }

    /* SWATCH ROW MUST NOT LEAVE THE CARD. The markup now emits at most six items (five chips plus
     * a "+N"), and this is the belt to that braces: one line, no wrapping, and anything that still
     * would not fit is clipped at the card's own padding box rather than painting outside it. */
    .tc-card__swatches {
        flex-wrap: nowrap;
        overflow: hidden;
        max-width: 100%;
        min-width: 0;
    }

    /* The non-priced states replace the whole block, so they must not inherit the grid. */
    .tc-card__pricing > .tc-card__state {
        grid-column: 1 / -1;
        justify-self: start;
    }

    /* A native rail has no clipped peek card - every slide is marked whole by the controller -
       so the desktop peek-price guard must not linger and blank a price here. */
    .tc-row__track--rail > .swiper-wrapper > .swiper-slide .tc-card__pricing { visibility: visible; }
}

/* ==========================================================================
   10. MOBILE FLAT PRESENTATION
   ==========================================================================
   MOBILE ONLY. Listing items are presented FLAT - no frame, no plate, no shadow, no outer
   radius - so the content owns the width instead of a chrome box owning it.

   BREAKPOINT. 959.98px is the theme's own mobile/tablet boundary, not a new number: it is the
   edge `.justMobileTablet` / `.justDesktop` switch on, and the same edge the row navigation
   already changes offset at (assets/css/style.css:16341/16357). Everything in this section is
   inside the query, so DESKTOP IS UNTOUCHED - there is no rule here that can reach it.

   WHAT COUNTS AS A FRAME. Measured on an iPhone 12, these were the containers painting a
   surface around listing items:

       section.vp-product-slider.tc-row    #fff, radius 16, padding-inline 16   (homepage rows)
       div.tc-deal                         #fff, radius 16, padding 10/10/14    (deal item)
       .vp-selected-brands .vp-item        #fff, radius 16, 1px border, shadow  (brand item)
       .vp-post-item-theme                 #fff, radius 16, shadow              (post item)

   SECTION vs ITEM. Where a section carries a background ON PURPOSE - the Deals section paints an
   admin-chosen image or colour, and the brand section is the page's own panel - the section is
   left alone and only the ITEM boxes inside it are flattened. Where the panel IS the frame and
   nothing else needs it, as on the product rows, the panel goes.

   WHAT IS NOT TOUCHED. Gutters, seams and alignment are untouched everywhere: spacing is what
   keeps flat items readable, so removing it would trade one defect for another. Banner sliders
   keep their radius - a rounded photograph is artwork, not a card frame.
   ========================================================================== */

@media only screen and (max-width: 959.98px) {

    /* --- homepage product rows -------------------------------------------
       The white panel IS the frame here: the cards inside are already transparent, and nothing
       else in the section depends on the surface. Dropping it and its 16px inline padding hands
       32px of width back to the cards. `.tc-row` raises specificity over the theme's own
       `.vp-product-slider` rule (assets/css/style.css:20399) so this does not depend on which
       stylesheet loads last. Vertical rhythm is kept - only the box goes. */
    .vp-product-slider.tc-row {
        background: transparent;
        border-radius: 0;
        box-shadow: none;
        /* 16px, not 0 - CRI is the authority for the mobile section's horizontal inset and its
           .tc-cart-rec__section uses 16/16. An edge-to-edge row read as a different component
           from the sheet, which was the point of the correction. The 32px the panel's own
           padding used to waste is still gone; this is the sheet's inset, not the panel's. */
        padding-inline: 16px;
    }

    /* --- product rows KEEP the panel on the PDP and the HOME PAGE ---------
       EXCEPTION to the flat presentation directly above.

       The flat rule was written on the premise that a white panel on those pages was pure
       chrome. That held for pages whose own ground is white; it does not hold here. Both of
       these pages sit on `body { background: #f2f2f2 }` (assets/css/style.css), and on both of
       them every OTHER block - the PDP's gallery, price box, features and specs; the home page's
       banners and category tiles - already reads as a white card floating on that grey. A
       transparent product row was the only block on either page with no surface, so its heading
       sat on bare grey while the cards below it floated unattached. The panel is what makes the
       row read as one section instead of a heading and some loose cards.

       SCOPE, and why it is these two class names ------------------------------
       `.page-product`   ViewBag.BodyCssClass, Views/Product/ProductTemplate.*.cshtml
       `.html-home-page` the home page's own body class

       `.page-home` and `.page-index` are NOT usable here even though the home body carries them:
       the PDP body carries them too (measured: `page-extra page-product page-index page-home
       vp-has-mini-header`), so they are not home-page markers at all and scoping to them would
       be a selector that means something other than what it says. `.html-home-page` appears on
       the home body and nowhere else, so it is both the narrowest and the honest choice.

       Every other page - category, search, manufacturer, tag, cart - is untouched and keeps the
       flat presentation. Both selectors raise specificity over the flat rule above without a
       single `!important`, so this does not depend on rule order within the block.

       WHICH ROWS. One selector covers every canonical row on both pages: the three PDP
       components (Views/Shared/Components/{SimilarProducts,RelatedProducts,ProductsAlsoPurchased})
       and the home rows, so they cannot drift apart. The deals section is NOT matched - it is
       `section.vp-special-discount > .vp-content.tc-deals`, not a `.vp-product-slider`, so its
       admin-chosen campaign artwork is untouched, exactly as the deals note below intends. The
       promotional banner pairs between home rows are not `.vp-product-slider` either.

       WHAT IS NOT CHANGED. The 16px inline padding above is the section's inset and is already
       correct, so it is inherited rather than restated; the 14/20 block padding (section 9) and
       the 24px bottom margin (`.vp-product-slider`, assets/css/style.css) are likewise the
       existing rhythm and are left to their owners - that 24px is the grey gap that keeps the
       panels reading as separate blocks. Only the surface is added back. The heading is already
       inside <section> in every one of these views, so it - and the home rows' `همه` view-all
       link and chevron - come onto the panel with no markup change.

       RADIUS. 16px is the theme's own `.vp-product-slider` radius and the same number every
       other white block on these pages uses - not a new value invented for this row.

       SCROLL. The panel paints a background; it sets no overflow. The only clip in the row is
       `.tc-row__track { overflow: hidden }` / the `.scrollMode` overflow-x on the rail, both
       INSIDE the panel and both untouched, so the swipe and the card geometry are exactly as
       they were. */
    /* CATEGORY and CART join the list, they are not a new treatment.
       `.vp-product-slider.tc-row` is `background: transparent` on mobile, and white is re-asserted
       only for the pages named here. Category and Cart were missing, so every rail on them - the
       "recently viewed" row in particular - painted nothing and the page's own #f2f2f2 ground read
       straight through it. Measured on the category page: the recently-viewed section computed
       `rgba(0,0,0,0)` against `rgb(255,255,255)` for the identical markup on Home and PDP, which is
       the grey block in the report. Adding the two page classes gives those rails the exact same
       surface, full-bleed and inset the normal rows already have - no new rule, no redesign, and
       the cards themselves are untouched. */
    body.page-product .vp-product-slider.tc-row,
    body.html-home-page .vp-product-slider.tc-row,
    body.page-category .vp-product-slider.tc-row,
    body.page-cart .vp-product-slider.tc-row {
        background: #fff;

        /* FLAT, NOT A CARD. The flat presentation above is kept in full - the ONLY thing this
           exception changes is the surface COLOUR. No radius, no shadow, no border, so the
           section reads as a band of the page rather than a plate sitting on it. */
        border-radius: 0;
        box-shadow: none;
        border: 0;

        /* FULL BLEED. Ryan's own break-out pattern, the same four declarations the PDP gallery
           already uses (assets/css/style.css:36239, from css/product.less). It is safe against
           page overflow because `body { max-width: 100%; overflow-x: hidden }` (css/base.less)
           is the page's own guard - measured after this change, scrollWidth still equals
           innerWidth on both pages.

           Needed because the section's container carries a gutter that differs per page - .size2
           on home left an 8px band each side, .size on the PDP ~15px - so a white surface that
           stopped at the container read as an inset card, and read as a DIFFERENT WIDTH card on
           each page. Breaking out of the container is what makes one rule produce one result. */
        width: 100vw;
        max-width: 100vw;
        margin-right: calc(50% - 50vw);
        margin-left: calc(50% - 50vw);

        /* The rail takes the whole width. The 16px section inset above exists to hold cards off
           a ROUNDED CORNER and to give the desktop nav arrows something to mount on; neither
           applies here - the corner is gone and `.tc-row__nav { display: none !important }`
           (section 9) removes the arrows at this breakpoint. Keeping it would spend 32px of a
           390px viewport on nothing, so the first card now starts at the right edge and the last
           finishes at the left. The card-to-card gutter is untouched and remains the only
           separation between cards. */
        padding-inline: 0;
    }

    /* The HEADING does not follow the rail to the edge - text needs a reading inset, and its
       16px matches the page's other headings. This is the one place where head and rail are
       deliberately given DIFFERENT horizontal padding. Block padding stays with the CRI rule in
       section 9: `padding-inline` is a side-only shorthand, so the 12px head->rail seam there
       is untouched. */
    body.page-product .vp-product-slider.tc-row > .tc-row__head,
    body.html-home-page .vp-product-slider.tc-row > .tc-row__head,
    body.page-category .vp-product-slider.tc-row > .tc-row__head,
    body.page-cart .vp-product-slider.tc-row > .tc-row__head {
        padding-inline: 16px;
    }

    /* The RAIL takes 8px, not the section's old 16 and not the 0 it briefly had. 0 read as a
       collision - the card media met the screen edge with no air at all - while 16 spent 32px of
       a 390px viewport on nothing. 8 is the smallest inset that still reads as deliberate.

       It sits on `.tc-row__content`, the rail's WRAPPER, not on the section and not on the
       scroller: the section must stay edge-to-edge white, and padding on the scroller itself
       behaves differently at the two scroll extremes. With the wrapper inset the geometry is
       symmetric by construction - the first card starts 8px from the right edge at the RTL
       start, and the last card finishes 8px from the left edge at the far end.

       Card-to-card spacing is untouched: this is an edge inset, not a gutter change. */
    body.page-product .vp-product-slider.tc-row > .tc-row__content,
    body.html-home-page .vp-product-slider.tc-row > .tc-row__content,
    body.page-category .vp-product-slider.tc-row > .tc-row__content,
    body.page-cart .vp-product-slider.tc-row > .tc-row__content {
        padding-inline: 8px;
    }

    /* --- deals -----------------------------------------------------------
       SEPARATED PLATES, not one continuous ground.

       This sub-block used to do the opposite: it flattened every `.tc-deal` to
       transparent and moved a single opaque `background: #FFFFFF` onto `.tc-deals`,
       the list they share. That was done for legibility - the section paints an
       administrator's campaign artwork, and card text on artwork is unreadable - and
       it did solve legibility. What it cost was the section itself: measured at 390px
       the white ground ran the full 374px width and 501px height of the list, so below
       the heading band the artwork was completely covered and the two deals, sitting
       8px apart on one white slab, read as a single undivided row rather than as two
       products on a campaign surface.

       The plates are the treatment that satisfies BOTH. Section 8b already defines them
       - `background: #FFFFFF; border-radius: 16px; padding: 10px 10px 14px` - and every
       word of that block's reasoning ("this section paints its OWN decorative
       background ... card text on top of that artwork is unreadable ... the plate is
       container presentation for this surface") applies at this breakpoint exactly as it
       does above it. So mobile no longer overrides them at all: each deal gets its own
       opaque tile, text stays on white, and the artwork shows through the gutter between
       the tiles and around the list. One definition, both breakpoints.

       The three declarations removed here are removed as a set, because each existed only
       to serve the flattening:
         .tc-deal { background/border-radius/box-shadow/padding }  the flattening itself
         .tc-deals { background/border-radius/padding-block }      the substitute ground
         .tc-deal .tc-card { --tc-text-pad-x: 10px }               the substitute text inset
       That last one has to go with them: section 8b sets `--tc-text-pad-x: 0px` on the
       card precisely BECAUSE the plate supplies the safe-area, so leaving the 10px here
       would inset the card's text twice inside a plate that already pads it.

       Equal height is untouched: `.tc-deal` is still `min-height: 100%` inside a slide
       the row stretches (section 8b), and the padding restored is uniform across every
       plate, so all plates grow by the same amount and the timer and progress-bar
       baselines stay level with each other. */

    /* The rail takes an 8px edge inset so the plates do not touch the section's sides.
       Without it the first plate's edge landed exactly on the section's right edge
       (measured: plate right 382, section right 382) and read as a crop rather than a
       card. 8px is not a new number - it is the same edge inset the home and PDP rows
       already take on `.tc-row__content` a few rules above, and it matches the 8px
       card-to-card gutter the row itself sets, so the artwork shows in an even rhythm
       around and between the tiles.

       It sits on the rail WRAPPER, which is what `.tc-deals` is (it computes to
       `.vp-content.tc-deals.tc-row.tc-row__content`), not on the scroller inside it -
       padding on a scroller behaves differently at the two scroll extremes, while on the
       wrapper the geometry is symmetric by construction.

       Block padding stays 0 from `.tc-deals.tc-row` in section 8b: the section's own
       `padding: 16px 0` and its heading's `margin-bottom: 24px` (style.css,
       `.amazing-offer`) already open and close the list, so adding more here would just
       widen the band of artwork above and below without adding separation. */
    .tc-deals {
        padding-inline: 8px;
    }
    /* --- brand / manufacturer items --------------------------------------
       A white bordered tile inside a white panel - the frame was the only thing separating it
       from its own background. Margins stay, so the items keep their spacing and never touch.

       The selector list and the two !important flags are NOT padding: the theme paints this item
       from three places, and the strongest of them wins on both counts.
           style.css  .vp-selected-brands .vp-item                       box-shadow / radius !important
           style.css  .vp-selected-brands.vp-all .vp-content .vp-item    (0,4,0) bg / shadow / radius
           style.css  same selector inside max-width:959px               1px border
       Matching (0,4,0) and re-declaring only the two properties the theme itself marked important
       is what it takes to actually turn the frame off; a weaker rule silently loses. */
    .vp-selected-brands .vp-item,
    .vp-content-brands .vp-item,
    .vp-selected-brands.vp-all .vp-content .vp-item,
    .vp-selected-brands.vp-all .vp-content-brands .vp-item {
        background: transparent;
        border: 0;
        border-radius: 0 !important;
        box-shadow: none !important;
    }

    /* --- post / article listing items ------------------------------------
       Same boxed treatment, same listing-slider role, so it follows the same rule. Background and
       radius are won on load order alone; only the shadow needs !important, because the theme's
       own mobile rule declares it that way (style.css, max-width:959px). */
    .vp-post-item-theme {
        background: transparent;
        border-radius: 0;
        box-shadow: none !important;
    }
}

/* ==========================================================================
   11. STACKING
   ==========================================================================
   ONE HIERARCHY, AND THE CARD IS AT THE BOTTOM OF IT.

   The storefront's page-level layers are already numbered, and they are not ours to renumber:

       .vp-bottom-sheet / .vp-toolbar / mini header        20     persistent chrome
       .km-category-compare-products                       12     persistent PLP utility widget
       .before-class                                      145     modal backdrop
       div[class*="bottomsheet-"]                         150     bottom-sheet / dialog content
       .tc-cart-rec                                      1300     recommendation sheet

   The card's own overlays are numbered relative to EACH OTHER, and only to each other:

       media / gallery viewport                             1
       gallery dots                                         2
       QSM host                                             3     (QSM's own control sits at 5
                                                                   INSIDE the host - see the
                                                                   firewall note in section 5)
       row navigation (.tc-row__nav, section 8c)            4     row-scoped, not card-scoped

   THE DEFECT THIS PREVENTS. `.tc-card` is `position: relative` with `z-index: auto`, which is NOT
   a stacking context. Every number in the second list therefore resolved against the ROOT stacking
   context, side by side with the page-level numbers in the first - the two scales were competing
   in one namespace, and the card's numbers only stayed underneath by being small. Anything that
   raised one of them (a QSM state, a promoted Swiper wrapper, a future badge or overlay, a theme
   rule reaching in) would have painted a card control straight through a modal backdrop, and no
   amount of raising the backdrop would fix it for the next one.

   `isolation: isolate` closes that namespace. It makes each card a stacking context whose children
   are sealed inside it, so the whole card composites as ONE layer at its natural document position:
   card overlays can never outrank page chrome, a backdrop, or a dialog, whatever number they carry
   internally, and the internal ordering above keeps working exactly as before.

   Deliberately `isolation`, not `z-index: 0`: it creates the stacking context without giving the
   card a paint order of its own, so cards keep compositing in document order and nothing in the
   grid or the row changes position. It also costs no layout and no repaint.

   The corollary is that no rule outside this file needs a bigger number. The backdrop stays at 145
   and the dialog at 150; the persistent widgets stay at 12 and 20 and now genuinely outrank every
   card that scrolls behind them, because a card's ceiling is its own box. */
.tc-card { isolation: isolate; }

/* The row's navigation is row chrome, not card chrome, so it sits OUTSIDE the card's context and
   keeps its own number - but it is scoped to `.tc-row__content`, which is `position: relative`
   with `z-index: auto`. Sealing it the same way keeps the arrows above the track they belong to
   and below everything the page puts on top of the row. */
.tc-row__content { isolation: isolate; }

/* ============================================================================
 * EMPTY RESULT — the grid's own child, not a card.
 *
 * ArchiveProducts emits the "no products" section as a direct child of this
 * grid, so `.tc-grid > *` was laying it out as a single 1fr flex cell: on
 * desktop a ~200px column with the icon, headline and helper text stacked into
 * narrow vertical strips. It is not a card - it spans the whole track.
 *
 * The !important declarations answer `.tc-grid > *`, which sets width, max-width
 * and margin with !important of its own. Mobile Search already corrects this in
 * assets/mobile-search-header/mobile-search-header.css and keeps its own 340px
 * column; this rule owns the desktop side, where the grid is 4 columns wide.
 * ============================================================================ */
@media (min-width: 992px) {
    .tc-grid > .km-category-grid-product-list-empty,
    .km-category-grid-style .km-category-grid-product-list.tc-grid > .km-category-grid-product-list-empty {
        grid-column: 1 / -1;
        display: block;
        width: 100% !important;
        max-width: 560px !important;
        margin: 0 auto !important;
        padding: 56px 16px 64px;
        min-height: 0;
        text-align: center;
    }
}

/* ============================================================================
 * 14. MOBILE ROW DENSITY — issues 1 and 3 of the final mobile hotfix.
 *
 * Appended as its own section rather than edited into sections 9/12, so the
 * whole of this round is one reversible block and the reasoning stays with the
 * numbers. Everything here is inside the same 991.98px breakpoint the mobile row
 * rules already use, and every selector is prefixed with `.tc-row`: the PLP grid
 * (`.tc-grid`) is a sibling composition and is not touched by any of it.
 * ========================================================================== */

@media only screen and (max-width: 991.98px) {

    /* --- ISSUE 1: cards read as vertically stretched -----------------------
       `.tc-card__pricing { margin-top: auto }` (section 5) is the cause, and it
       is correct on desktop: slides in a Swiper track are equal height, the auto
       margin absorbs that slack, and neighbouring cards therefore share a price
       baseline instead of each floating its price at its own height.

       On a 390px row the same mechanism reads as a defect. The row's height is
       set by its tallest card - typically a two-line title plus swatches - and
       every shorter card in the row has that entire difference injected as one
       empty band between its title and its price, which is exactly the gap in
       the reference screenshot. The card is not padded too generously; it is
       being stretched, and the stretch is pooling in one place.

       Two lines, both scoped to `.tc-row`:

       - releasing the auto margin stops the slack pooling mid-card, so the
         pricing block follows the meta at its normal `--tc-gap-meta-price` seam;
       - `align-items: flex-start` on the track stops the slack existing at all,
         so each card ends at its own content and the row is as tall as its
         tallest card rather than every card being as tall as the tallest.

       Order matters between them: without the second line the released margin
       would simply move the same empty band from mid-card to below the price.
       The price baseline the auto margin was protecting is a desktop contract -
       desktop keeps it, untouched, outside this query. */
    .tc-row .tc-card__pricing {
        margin-top: 0;
    }

    .tc-row .tc-row__track.swiper > .swiper-wrapper,
    .tc-row .tc-row__track > .swiper-wrapper {
        align-items: flex-start;
    }

    /* --- ISSUE 3: -16px after the title, -16px after the row ---------------
       Both reductions are taken from the section/row spacing owners in section
       12, never from card internals.

       AFTER THE ROW: `.vp-product-slider.tc-row { padding-block: 14px 20px }`.
       The trailing 20px is the entire space between the last card and whatever
       section follows, so 20 -> 4 is the -16 exactly, with 4px left so the row
       still closes rather than colliding with the next section.

       AFTER THE TITLE: all 16px come out of the title->cards gap, and none out
       of the space above the title. That gap is owned by a single declaration,
       `.tc-row .tc-row__head { padding: 0 0 12px }`, and 12 is everything it
       has - so 12px come off there and the last 4 are pulled by the track.

       The -4px is safe, and specifically it does not clip the heading. The head
       lays out at `font-size: 15px / line-height: 24px` (section 12), so its
       line box carries (24 - 15) / 2 = 4.5px of half-leading BELOW the last
       glyph before the box even ends. The pull consumes that empty leading and
       stops 0.5px short of the descender line; nothing overlaps the text. It is
       applied to `.tc-row__content`, the track's wrapper, so it moves the cards
       and not the head - the head keeps its own box and a long or wrapped title
       still pushes the cards down normally.

       The row's leading padding is deliberately left at its original 14px. An
       earlier revision of this block took the last 4px from there instead; that
       removed the right total but from the wrong side of the title. */
    .tc-row .tc-row__head {
        padding-bottom: 0;
    }

    .tc-row > .tc-row__content {
        margin-top: -4px;
    }

    .vp-product-slider.tc-row {
        padding-block: 14px 4px;
    }

    /* `.tc-deals.tc-row` zeroes its own padding-block in section 12 and lays its
       plates out itself, so the row rule above cannot reach it. The deals plate
       is the composition in the reference screenshot, so it takes the same -16
       explicitly. The `.tc-row > .tc-row__content` rule above already covers its
       track, since the deals section carries `tc-row` too. */
    .tc-deals.tc-row .tc-row__head {
        padding-bottom: 0;
    }
}

/* ============================================================================
 * 15. DEALS PLATE DENSITY - mobile only, .tc-deals only.
 *
 * Measured on the running homepage at 390: the plate is 484px tall for a 152px-wide
 * card, and the band between the title and the price is 64px. That band is NOT
 * empty - it is the swatch row (24px + 8px margin) and the meta row (20px + 12px
 * margin) - so nothing here removes content; it removes the air around it, which is
 * what makes the plate read as stretched next to its own image.
 *
 * Every selector is prefixed .tc-deals and every rule sits inside the mobile query,
 * so the product rows, the PLP grid and the desktop deals plate are untouched. The
 * card's own component rules in sections 5 and 9 keep their values; these override
 * only within the deals context.
 * ========================================================================== */

@media only screen and (max-width: 991.98px) {

    /* The plate's own frame. 10/10/14 was set when the countdown still needed room to
       breathe under the card; the countdown carries its own top spacing now, so the
       bottom 14 is doubling up. */
    .tc-deals .tc-deal {
        padding: 8px 8px 10px;
    }

    /* Swatches sit directly under the title and read as part of it - 8px of separation
       is a paragraph break where a line break is wanted. */
    .tc-deals .tc-card__swatches {
        margin-top: 4px;
    }

    /* The meta row is a single 20px line; 12px above it on a 152px-wide card is the
       single largest piece of slack in the stack. */
    .tc-deals .tc-card__meta {
        margin-top: 6px;
    }

    /* --tc-gap-meta-price is the seam between the meta row and the price block. The
       meta row above already carries its own margin, so this is the second half of the
       same gap. */
    .tc-deals .tc-card__pricing {
        padding-block-start: 4px;
    }
}

/* ============================================================================
 * 16. DEALS MEDIA IS SQUARE ON MOBILE.
 *
 * The plate measures 472px tall at 390, and 208 of that - 44% - is the image. The
 * card's aspect is already a token (`--tc-media-ratio`, section 2), so this is a
 * one-value change in the deals context rather than any new geometry: 3/4 portrait
 * becomes the 1/1 the theme already defines for square media at section 1453. On a
 * 152px-wide plate that is 208px -> 152px, and the image stays large enough to read
 * because the plate is narrow to begin with.
 *
 * Nothing structural moves. The countdown keeps its place inside the plate, the
 * min-height equalisation from section 8 is untouched so neighbouring plates still
 * share a baseline, and the swiper is not involved. Desktop keeps 3/4: the rule is
 * inside the mobile query and scoped to .tc-deals, so .tc-row, the PLP grid and the
 * ordinary product card are all out of its reach.
 *
 * The `@supports not (aspect-ratio)` fallback at section 2 still hard-codes 133.33%
 * for every card. Browsers without aspect-ratio therefore keep the tall deals media;
 * that path is years out of support and is left alone rather than forked here.
 * ========================================================================== */

@media only screen and (max-width: 991.98px) {

    .tc-deals .tc-deal .tc-card {
        --tc-media-ratio: 1 / 1;
    }
}

/* ============================================================================
 * 17. SPECIAL OFFERS DENSITY - .tc-deals only.
 *
 * Two independent changes, one per breakpoint, both made at the value that already
 * OWNS the geometry rather than as an override fighting it.
 * ========================================================================== */

@media only screen and (max-width: 991.98px) {

    /* MOBILE: closer together, and each plate wider.
     *
     * Both numbers are the rail's own tokens from section 13 - `--tc-row-rail-gap` is the
     * `gap` on the rail's flex line and `--tc-row-card-w` is the slide width - so this is a
     * two-value change at the owner, not a set of overrides. Scoped to `.tc-deals`, so the
     * ordinary product rows keep the 172/8 CRI geometry they share with the PLP.
     *
     *   gap    8 -> 4      half the space between neighbouring plates
     *   width  172 -> 198  +15%, which is what the reclaimed gap plus a little of the
     *                      next plate's peek pays for
     *
     * The rail is a native overflow-x scroller, so nothing about swipe, snap, RTL or the
     * countdown is involved: the line simply gets wider and scrolls further. */
    .tc-deals.tc-row {
        /* --tc-row-space is the live one: mobile runs Swiper (see initMobileSwiper in
           product-card.js), and Swiper writes the gutter as a slide margin, so the number has to
           reach it from CSS through rowSpaceBetween(). 32 -> 16 is the halving.
           --tc-row-rail-gap is the native-rail fallback that runs only when Swiper is
           unavailable. It carries its own base (8, from the CRI geometry), so it is halved
           against that rather than against 32 - each path halves what it actually renders. */
        --tc-row-space: 16px;
        --tc-row-rail-gap: 4px;
        --tc-row-card-w: 198px;
    }
}

@media (min-width: 992px) {

    /* DESKTOP: the band between the title and the price.
     *
     * MEASURED CAUSE, on the running homepage at 1440 (60 deals in the row):
     *
     *   59 of 60 plates    title 42, meta collapsed, pricing margin-top AUTO = 22px, seam 16
     *    1 of 60 plates    (index 44) meta VISIBLE - it is the only deal with a rating -
     *                      pricing margin-top 0, seam 7
     *
     * So the row is 22px taller than its own content needs, and that 22px is not padding
     * anywhere: it is equal-height slack. One deal has a rating row, the slide track stretches
     * every slide to that tallest plate, and `.tc-card__pricing { margin-top: auto }` (section
     * 5) - which is correct, it is what gives neighbouring cards a shared price baseline -
     * pools the whole difference into a single empty band between the title and the price on
     * the other 59. The band therefore measured 22 slack + 16 seam = 38px.
     *
     * The fix is to stop one outlier from setting the height of sixty plates, so the deals
     * plate does not render the meta row. Nothing is being forced to a smaller size: with no
     * outlier there is no slack to pool, the auto margin resolves to 0 on its own, and the band
     * falls to the 16px seam - 38 -> 16, and the plate with it (462 -> 440).
     *
     * Deliberately NOT done: releasing `margin-top: auto`, which would move the same slack
     * below the price instead of removing it, and pinning a height, which would break the
     * equal-height contract this row depends on. Equal heights, the media, the title, the
     * price, the discount, the countdown, the progress bar, the cart control and the slider
     * are all untouched - every plate simply stops carrying an empty band.
     *
     * Mobile is out of scope by the query: section 15 tunes the deals meta row there
     * deliberately, and item 7 is a desktop change only. */
    .tc-deals .tc-card__meta {
        display: none;
    }

    /* DESKTOP GUTTER: 32 -> 24, exactly 0.75x.
     *
     * The owner is Swiper's `spaceBetween`, which it writes as a margin on each slide, so the
     * number reaches it from here through rowSpaceBetween() in product-card.js - the same route
     * the mobile deals gutter already uses. Nothing compensates with a negative margin, the card
     * width is untouched, and the deals row suppresses the slide separator hairline anyway
     * (`.tc-deals ... .swiper-slide::before { content: none }`), so there is no rule left
     * measured against the old 32. Scoped to `.tc-deals`, so ordinary product rows keep 32. */
    .tc-deals.tc-row {
        --tc-row-space: 24px;
    }
}
