/** Shopify CDN: Minification failed

Line 237:2 Unexpected "/"
Line 237:26 Expected identifier but found whitespace

**/
/* Badges live in calmy-cardsurface.css now - one pill, one position, for every
   card on the site. The rules that were here set background:none and loaded
   later, so they quietly turned the pill back into bare text. */
/* ============================================================================
   CALMY DESIGN TOKENS + SHARED PRIMITIVES + CARD SYSTEM

   OWNS: palette and spacing tokens, section grounds, the line-icon primitive, the
   information band, link/button variants and the product card.
   MUST NOT TOUCH: header, navigation, drawers, overlay, cart, PDP purchase column.

   GROUNDS - three neutrals, small steps. They should read as a change of air, not colour.
     --c-bg var(--t-page) ivory | --c-white var(--t-white) soft white | --c-surface var(--t-page) soft cream
   ============================================================================ */

:root{
  --c-serif:var(--font-heading--family,Georgia,'Times New Roman',serif);
  --c-sans:var(--font-body--family,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif);
  --c-eyebrow:.22em;--c-label:.16em;
  --c-white:var(--t-white);--c-clay:var(--t-accent);--c-clay-soft:var(--t-accent-mut);
  --c-espresso:var(--t-ink);--c-olive:var(--t-neutral);--c-cream:var(--t-page);
  --c-sp:64px;--c-sp-tight:48px;
}
@media(min-width:700px){:root{--c-sp:80px;--c-sp-tight:60px}}
@media(min-width:1000px){:root{--c-sp:104px;--c-sp-tight:76px}}

.csec--white{background:var(--c-white)}
.ccar--white{background:var(--c-white)}

/* ---------- Line icon primitive ----------
   One weight, one size, currentColor. Never coloured, never filled, never large. */
.cico{width:20px;height:20px;display:block;fill:none;stroke:currentColor;stroke-width:1.15;
  stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}

/* ---------- Information band ---------- */
.cinfb{width:100%}
.cinfb--white{background:var(--c-white)}
.cinfb--surface{background:var(--c-surface)}
.cinfb__row{display:grid;gap:2px}
.cinfb__i{display:flex;align-items:flex-start;gap:12px;text-decoration:none;color:inherit;min-width:0}
.cinfb__ic{flex:none;color:var(--c-clay);margin-top:1px}
.cinfb__b{display:flex;flex-direction:column;gap:5px;min-width:0}
.cinfb__h{font-size:10.5px;letter-spacing:var(--c-label);text-transform:uppercase;color:var(--c-fg);line-height:1.4}
.cinfb__t{font-size:12.5px;line-height:1.6;color:var(--c-muted);max-width:30ch}
a.cinfb__i:hover .cinfb__h{color:var(--c-clay)}

/* Compact: one quiet rule of labels. */
.cinfb--compact{border-top:1px solid var(--c-hair);border-bottom:1px solid var(--c-hair)}
.cinfb--compact .cinfb__row{grid-template-columns:1fr 1fr;gap:14px 20px;padding:18px 0}
@media(min-width:700px){.cinfb--compact .cinfb__row{grid-template-columns:repeat(4,1fr);gap:20px;padding:20px 0}}
.cinfb--compact .cinfb__i{align-items:center;gap:9px}
.cinfb--compact .cico{width:17px;height:17px}
.cinfb--compact .cinfb__h{font-size:9.5px}

/* Full: icon, heading, explanation. */
.cinfb--full{padding:var(--c-sp-tight) 0}
.cinfb--full .cinfb__row{grid-template-columns:1fr;gap:26px}
@media(min-width:700px){.cinfb--full .cinfb__row{grid-template-columns:repeat(3,1fr);gap:40px}}
.csec__head--c{justify-content:center;text-align:center}

/* ---------- Button and link variants ---------- */
.cbtn.cbtn--secondary{background:transparent;color:var(--c-fg);border:1px solid var(--c-line)}
.cbtn.cbtn--secondary:hover{border-color:var(--c-fg);opacity:1}
.csec--dark .cbtn.cbtn--secondary{color:var(--c-cream);border-color:rgb(var(--t-page-rgb) / .4)}
.clink{display:inline-flex;align-items:center;gap:9px;font-size:10px;letter-spacing:var(--c-eyebrow);text-transform:uppercase;color:inherit;text-decoration:none;min-height:44px;background:none;border:0;padding:0;cursor:pointer;font-family:inherit}
.clink:after{content:"\2192";transition:transform .22s var(--c-ease)}
.clink:hover:after{transform:translateX(4px)}

/* ---------- Product card -----------------------------------------------------
   One rounded card, no border, ground one step off the page. The product image
   floats directly on that ground with a soft shadow — there is no inner tinted
   frame any more.

   On a pointer device the card swaps to a full-bleed lifestyle image on hover,
   keeps the category word, fades the text block out and slides a pill up from
   the bottom edge. Nothing scales, nothing lifts, and the card's own shadow
   never changes.

   These rules override calmy-cardsurface.css, which frames the image as a
   cover-cropped panel with its own ground. Selectors are doubled so the result
   does not depend on which file loads last.
   --------------------------------------------------------------------------- */

/* min-width: 0 on the grid item itself for the same reason as the track: a
   grid item's automatic minimum is its min-content size, so without this the
   card can refuse to shrink into a column that is narrower than its longest
   unbreakable word. */
.ccard {
  min-width: 0;
  --card-pad: 18px;
  --pill-h: 46px;
  --pill-inset: 12px;
  --swap: 400ms;
  height: 100%;
}
@media (min-width: 1000px) { .ccard { --card-pad: 22px; } }

.ccard__inner {
  position: relative;
  height: 100%;
  display: flex;
  flex-direction: column;
  /* Clips the hover image to the card, corners included. */
  overflow: hidden;
  /* ONE CONTINUOUS CREAM SURFACE. The hairline and the 22px radius are gone:
     the card is now a single soft field with the media sitting on the same
     ground as the text under it, rather than a bordered tile with a framed
     photograph inside it. --t-card is the cream one step off the page, so the
     card still reads as a distinct surface on --t-page without an outline. */
  border: 0;
  border-radius: 16px;
  background: var(--t-card);
  padding: var(--card-pad);
  /* No pill reservation any more — the button is the last item in the foot,
     in normal flow, so it takes its own height and the card's bottom padding
     is simply the card's padding. */
  text-decoration: none;
  color: inherit;
  isolation: isolate;
}

/* ---- The hover image ------------------------------------------------------
   Absent from the DOM entirely when there is nothing to swap to, so hover can
   never reveal an empty frame. */
.ccard__hover {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity var(--swap) var(--c-ease, ease);
  pointer-events: none;
}

/* ---- Category word --------------------------------------------------------
   The card's headline: large, heavy, lowercase, muted. Sits above the hover
   image so it survives the swap. */
/* A small uppercase eyebrow at the top of the card.

   It used to be 26-30px display type laid over the photograph, which put a
   word across the product at exactly the size that competes with it. As an
   eyebrow it labels the card instead of decorating it, and the media area
   below gets the whole card width back. */
.ccard__mood {
  position: relative;
  z-index: 2;
  margin: 0 0 10px;
  font-family: var(--c-sans);
  font-size: 9.5px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  text-indent: 0.18em;
  color: var(--c-soft);
  min-height: 1.4em;
  transition: color var(--swap) var(--c-ease, ease);
}
.ccard__mood.ccard__mood:empty { display: block; }


/* Pinned to the CARD, not the image, at a fixed inset — its offset parent is
   .ccard__inner, so it cannot slide with the photograph or sit under it. z-2
   puts it above the hover image at z-1. The fill transition runs on the same
   clock as the image fade, so the pill grows in as the photograph darkens and
   there is no moment where dark text sits on a dark shot. */
.ccard__badge.ccard__badge {
  position: absolute;
  top: var(--card-pad);
  right: var(--card-pad);
  left: auto;
  z-index: 3;
  padding: 5px 10px;
  border-radius: 999px;
  transition: background-color var(--swap) var(--c-ease, ease),
              color var(--swap) var(--c-ease, ease);
}

/* ---- Product image --------------------------------------------------------
   Floats on the card ground. No inner box, no frame, no ground of its own. */
.ccard__media.ccard__media {
  position: relative;
  z-index: 0;
  display: grid;
  place-items: center;
  /* 4:5 portrait. Square cropped the taller products tight against the sides;
     a portrait box gives every one of them room above and below. The card
     grows to suit. */
  aspect-ratio: 4 / 5;
  /* min-height: 0 is load-bearing. As a flex item this defaults to `auto`,
     which floors the box at its content size — the image sized itself from
     max-width (86% of 251 = 216px), went square, and dragged the frame to
     216px tall regardless of the ratio. With the floor removed the ratio
     decides the height, and the image's max-height percentage finally has a
     definite height to resolve against. */
  min-height: 0;
  overflow: visible;
  /* No ground of its own — the card's shows through, so the frame is the card
     rather than a tile laid on it. */
  background: transparent;
  border-radius: 14px;
  box-shadow: none;
  padding: 0;
  transition: opacity var(--swap) var(--c-ease, ease);
}

/* Absolutely positioned and centred with `margin: auto`, not laid out in
   flow. In flow the image sized itself from max-width alone — max-height's
   percentage had no definite height to resolve against once min-height was
   zeroed, so a square photograph came out taller than its frame and, with
   overflow visible, printed straight over the product name beneath it.
   Against the frame's padding box both percentages resolve, and the image can
   never exceed the space reserved for it. */
/* Scoped to the PRODUCT image by class, not to any img in the box.

   As `.ccard__media img` it also caught the hover image the moment that moved
   inside the media box — at (0,3,1) it outranked the rule holding the swap
   hidden, so the second photograph sat permanently on top of the first and
   there was no hover left to see. It is the leftover of the contain-with-12%-
   inset treatment that cover replaced, so most of it is dead; what remains is
   the positioning. */
.ccard__media.ccard__media .ccard__img {
  position: absolute;
  inset: 0;
  margin: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 1;
} / .is-on { opacity: 1 }. This card renders one
     image and never sets is-on, so without this every photograph is invisible. */
  opacity: 1;
  /* NO mix-blend-mode. It used to be multiply, to pull the grey sweep the
     catalogue was shot on into the card's own hue. The featured images are
     transparent PNGs now, so there is no sweep to neutralise - and multiply
     against a cream card would darken the product itself, which is the one
     thing it must not do. Removed with the comment that justified it. */
}

/* RULE 0 — no image yet. One flat tone at the frame's exact size: no icon, no
   wireframe, no gradient, and the card keeps its height so nothing moves when
   a photograph arrives. */
.ccard__empty {
  position: absolute;
  inset: 0;
  margin: auto;
  display: block;
  width: 76%;
  height: 76%;
  border-radius: 2px;
  background: var(--c-sink, var(--t-card));
}

/* ---- Bottom block ---------------------------------------------------------
   Pinned to the bottom of the card so every card in a row ends level however
   long the name above it runs. */
/* ── The foot: one column, one spacing scale ───────────────────────────────
   Five items, top to bottom, every one a full-width block on the same left
   edge: rating, title, price, descriptor, swatches. Any of the three optional
   ones can be absent and the rest keep their rhythm, because each item owns
   the space ABOVE itself and the first one in the column zeroes it. That is
   what makes the arrangement independent of which items a given product has —
   there is no rule anywhere that says "if there is a descriptor, then".

   THREE STEPS, and only three. --gap-tight binds the rating to the title it
   belongs to, --gap-base is the ordinary step between lines of information,
   --gap-wide separates the swatches, which are a different kind of thing from
   the text above them:

     rating  -> title       6px   tight
     title   -> price       8px   base
     price   -> descriptor  8px   base
     desc    -> swatches   10px   wide

   ALIGNED TO THE PILL, not to the card's text padding. .ccard__inner pads by
   --card-pad (18px, 22px from 1000px) but the buy pill is positioned at
   --pill-inset (12px), so the foot's left edge sat 6-10px inside the pill's
   and nothing in the card lined up with the one control on it. The negative
   margin is the difference between the two tokens, so it stays correct at
   both card sizes and cannot drift if either token moves. */
.ccard__foot {
  /* Tightened. The information block is compact by design — title, subtitle
     and swatches are one unit under the photograph, not four separate rows —
     so the only step that stays generous is the one before the button, which
     needs air to read as a control rather than as another line of text. */
  --gap-tight: 4px;
  --gap-base: 8px;
  --gap-wide: 14px;

  position: relative;
  z-index: 0;
  /* BLOCK, not flex, and the reason is the title's clamp.

     .ccard__t declares BOTH clamping mechanisms: the legacy pair
     (display: -webkit-box + -webkit-line-clamp) and the modern `line-clamp`.
     Chrome takes the modern one, which makes the element a block container —
     its computed display reads flow-root, not -webkit-box, and that is the
     modern path working, not a failure. Safari has no `line-clamp` yet and
     falls back to the legacy pair, which only functions on a real -webkit-box.

     A flex parent blockifies its items' display, which would destroy that
     -webkit-box and leave Safari with no clamp at all while Chrome carried on
     looking correct. Block children are full width and left-aligned anyway,
     so the column loses nothing by not being a flex container.

     Sibling margins cannot collapse into each other here because every item
     below sets margin-top only and leaves margin-bottom at zero, and the foot
     is itself a flex item, which establishes a BFC, so nothing escapes it. */
  display: block;
  margin-top: auto;
  margin-inline: calc(var(--pill-inset) - var(--card-pad));
  transition: opacity var(--swap) var(--c-ease, ease);
}

/* Each item declares its own leading space; whichever lands first has none. */
.ccard__foot > .ccard__row { margin-top: var(--gap-tight); }
.ccard__foot > .ccard__sw { margin-top: var(--gap-base); }
.ccard__foot > .ccard__buy { margin-top: var(--gap-wide); }
.ccard__foot > :first-child { margin-top: 0; }

/* The star row. Rendered only when a product carries BOTH calmy.rating and
   calmy.rating_count — see the gate in snippets/calmy-product-card.liquid. It
   used to stand in with outline stars and a (0) so the row never appeared from
   nowhere later, but on a store with no review data that placeholder is what
   every card shows, and it reads as a product nobody has reviewed.

   Nothing here reserves its height. .ccard__foot is display: block and the row
   owns its own margin-bottom, so on a card without a rating the margin leaves
   with the element and the title closes up against the top of the foot. */
.ccard__rate {
  display: flex;
  align-items: center;
  gap: 6px;
  /* Zero. This carried `margin-bottom: 6px`, which stacked under the title's
     own 6px margin-top and set the two 12px apart. The foot's scale is the
     only thing allowed to space these. */
  margin: 0;
  line-height: 1;
}
.ccard__stars { display: inline-flex; gap: 1px; }
.ccard__star {
  /* 11px, and muted below. The rating is supporting information: it sits above
     the title and must not compete with it. */
  font-size: 11px;
  line-height: 1;
  /* Unfilled stars sit a step back from filled ones so a 3-of-5 reads at a
     glance rather than as five marks of equal weight — but at the light
     neutral they measured 1.41:1 against the card, which is not "a step
     back", it is invisible. The SAME neutral at 0.95 clears the 3:1 a
     non-text indicator needs while staying plainly lighter than a filled
     star. No new hue: one token at one alpha. */
  color: rgb(var(--t-neutral-rgb) / 0.95);
}
/* A lit star is --c-muted, not --c-fg: 6.0:1 on the card against the title's
   13.9:1, so the row reads as quieter without becoming faint. That narrows the
   gap to an unlit star to 1.73:1, which would be too little if colour were the
   only thing separating them — but the two are different GLYPHS, ★ against ☆,
   so colour is reinforcing a distinction that is already there in the shape. */
.ccard__star.is-on { color: var(--c-muted); }
.ccard__rcount { font-size: 11px; color: var(--c-soft); font-variant-numeric: tabular-nums; }

/* Title left, price hard right, on one line.

   baseline alignment, so a two-line title and a one-line price still sit on a
   shared first baseline rather than the price floating at the block's centre.
   The gap is the minimum air between them; the title is the only side that
   gives, and it wraps rather than colliding. */
.ccard__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

/* min-width: 0 because the title is a flex item, where min-width defaults to
   auto and floored it at its longest word — 275px for a run-together product
   name in a 169px card.

   CLAMPED AT TWO LINES AND RESERVED AT TWO LINES, at every width. The clamp
   alone only stops a long title from growing; min-height is what stops a short
   one from shrinking. Without the reservation a one-line title pulled
   everything beneath it up by one line, and the prices across a row sat at two
   different heights — which is the whole reason the row looked ragged.

   min-height is derived from the line-height rather than typed as a pixel
   figure, so the reserved box stays exactly two lines if the type changes.

   overflow-wrap is NORMAL, not `anywhere`. `anywhere` was here to stop a long
   word overflowing, and it did, but it also drops the element's min-content
   width to a SINGLE CHARACTER. Next to .ccard__p, which is `flex: none` and
   `white-space: nowrap` and therefore never yields a pixel, that made the
   title the only thing in the row that could absorb a shortfall — and it could
   absorb all of it. On a narrow card with a compare-at price the title was
   squeezed to a one-character column and set itself vertically, one letter per
   line. word-break: normal is stated alongside it so neither property can be
   reintroduced from elsewhere without it being obvious.

   Nothing needs `anywhere` any more: the grid track is minmax(0, 1fr) and
   .ccard is min-width: 0, so neither can be floored by an unbreakable word,
   and .ccard__inner is overflow: hidden, so a word too long for the card is
   clipped rather than pushing the layout. */
.ccard__t {
  /* Takes the remainder of the row and is the only side that shrinks. */
  flex: 1 1 auto;
  /* Explicitly zero. calmy-pdp.css:296 also styles .ccard__t and sets
     `margin: 0 0 3px`; this file loads after it and wins on equal specificity,
     but only for properties it actually declares — so without this line that
     3px survived and every title-to-price gap measured 11px where the foot's
     scale said 8. The foot owns all vertical rhythm; nothing else may add to
     it. */
  margin: 0;
  min-width: 0;
  overflow-wrap: normal;
  word-break: normal;
  --t-lh: 1.35;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  min-height: calc(var(--t-lh) * 2em);
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: var(--c-sans);
  font-size: 12.5px;
  font-weight: 600;
  line-height: var(--t-lh);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-fg);
}

/* One line, always: the current price at the card's full weight, the
   compare-at struck through and muted beside it, 8px apart. nowrap keeps the
   pair together rather than letting a long compare-at drop to a second line. */
.ccard__p {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: var(--gap-base, 8px);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--c-fg);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ccard__from { font-size: 9.5px; font-weight: 400; letter-spacing: var(--c-label); text-transform: uppercase; color: var(--c-soft); }
/* .ccard__p del in calmy-pdp.css:298 adds margin-left: 6px, which stacked on
   top of the row's 8px flex gap and set the pair 14px apart. Zeroed at (0,2,0)
   so the gap is the single source of that distance. */
.ccard__p .ccard__was { margin: 0; }
/* THE COMPARE-AT COLOUR, in one place. --t-accent is the palette's own warm
   red (#8A5A45, 5.42:1 on the page ground); --c-soft was the same warm grey as
   the rest of the secondary text, which left the struck figure reading as
   another quiet label rather than as the price that no longer applies. No new
   colour is introduced and the token stays on .ccard, so nothing outside a
   card can pick it up. */
.ccard { --ccard-was: var(--t-accent, #8A5A45); }
.ccard__was { font-size: 11.5px; font-weight: 400; color: var(--ccard-was); text-decoration: line-through; text-decoration-thickness: 1px; }

/* The price hangs on the foot's RIGHT edge, which is also the pill's right
   edge — both sit --pill-inset from the card — so the price and the control
   under it share one vertical. */
.ccard__p { justify-content: flex-end; }

/* Explicit, because calmy-carousel.css orders this row with
   `.ccard__p > span:first-of-type { order: 2 }` — written when the price was
   the first span. Here the FROM label is first, so it collected order 2 and
   the row read "$39 From $80.95", which is backwards. */
/* Compare-at first, so it sits to the LEFT of the figure that replaced it:
   struck through, then the live price. FROM stays immediately before the
   current price. */
.ccard__p.ccard__p .ccard__was { order: 1; }
.ccard__p.ccard__p .ccard__from { order: 2; }
.ccard__p.ccard__p .ccard__now { order: 3; }

/* ── Colour swatches ───────────────────────────────────────────────────────
   A row of dots under the descriptor, rendered by calmy-product-card.liquid
   only when the product has a colour option with more than one value.

   Small on purpose. This is a "comes in five finishes" cue, not a picker —
   choosing happens on the product page — so the dots are read as a group
   rather than as five separate controls, and they carry no hit target. */
.ccard__sw {
  display: flex;
  align-items: center;
  gap: 6px;
  /* The row is exactly one swatch tall whether it holds four or one. */
  min-height: 14px;
}

/* AND IT KEEPS ITS ROW WHEN IT IS NOT THERE AT ALL. A product with no colour
   option renders no .ccard__sw — that is deliberate, an empty row of nothing
   is not a swatch row — but the space it would have taken still has to be
   reserved, or a card without finishes is 24px shorter in its foot than the
   card beside it and every line in the two feet stops agreeing.

   :has() lets the foot ask whether the row exists and stand in for it when it
   does not, so the reservation lives with the layout rather than being faked
   with an empty element in the markup. The height and the margin are the same
   two values the real row uses, so the two cases cannot drift apart. */
/* The reservation that stood in for a missing swatch row is gone. It was an
   ::after, so with the button now the last item in the foot it would have sat
   BELOW the button rather than where the swatches would have been. It is also
   no longer needed for what it was for: the foot is bottom-anchored inside a
   flex column whose media absorbs the slack, so the button lands on the same
   line in every card whether or not the card above it has swatches. */

/* Scoped through .ccard__sw so these beat `.ccard__sw span` in
   calmy-pdp.css:302 — an older implementation of this same row that is still
   in the file. At a single class mine lost, so the dots were sized 11px with a
   1px border by that rule, and the "+N" was being squashed into an 11px circle
   along with them. */
.ccard__sw .ccard__swatch {
  flex: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  /* A real 1px hairline in --c-line, the same colour the card's own edge uses.
     .calmy * is border-box, so the 14px is the outer size and the border does
     not push the row wider. This replaces an inset box-shadow: with a fill
     that can now be a photograph, a ring drawn inside the circle was landing
     on top of the image rather than around it. */
  border: 1px solid var(--c-line);
  /* A variant photograph arrives as a background-image. cover crops it to fill
     the circle at any aspect ratio, centred. */
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
}

/* The overflow count reads as one more item in the row: the same 14px height
   and the same hairline as a swatch, so the row has a single rhythm rather
   than four circles and a stray number. Width is auto — `.ccard__sw span` in
   calmy-pdp.css:302 pins any span in here to 11px square, which clipped the
   digits. */
.ccard__sw .ccard__swmore {
  flex: none;
  width: auto;
  min-width: 14px;
  height: 14px;
  padding: 0 5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--c-line);
  border-radius: 999px;
  font-family: var(--c-sans);
  font-size: 9.5px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--c-muted);
  font-variant-numeric: tabular-nums;
}

/* ---- The pill -------------------------------------------------------------
   Positioned, not in flow, so it can slide without resizing the card. Solid
   light fill, inset from the card edges. Visible at rest on touch; on a
   pointer device it waits below the edge until hover. */
/* IN FLOW, not positioned. It used to be absolutely placed against the card's
   bottom inset, which meant the card had to reserve height for it and the gap
   above it was a padding calculation rather than part of the foot's rhythm.
   As the last item in the foot it simply takes its own height, and the foot's
   own bottom-anchoring keeps it level across a row. */
.ccard__buy {
  /* relative, NOT static. static cancels the .cb component's own
     `position: relative`, and .cb__fill (`position: absolute; inset: 0`,
     painted --cb-wipe = near-black) then resolved against .ccard__inner and
     blacked out the entire card on hover. See assets/calmy-cardhover.css. */
  position: relative;
  z-index: 2;
  width: 100%;
  /* 46px is the design height; 44 is the floor a finger needs. min-height
     rather than height so a wrapped label can never crush it below that. */
  height: auto;
  min-height: max(var(--pill-h), 44px);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border-radius: 999px;
  /* Transparent, so the one continuous cream reads straight through the
     button and only the outline separates it. */
  background: transparent;
  /* A hairline as well as the fill. On alternating bands the card ground is
     itself --c-white, so fill alone left the pill invisible on touch, where
     it is the one control on the card and is on screen the whole time. Over
     the hover photograph the fill carries it and the hairline disappears
     into the edge. */
  /* A DARK hairline, not --c-line. The old border was the neutral the card's
     own edge used, which against the cream left the button barely drawn; the
     button is the one control on the card and has to be unmistakable. */
  border: 1px solid var(--t-ink);
  color: var(--c-fg);
  transition: opacity var(--swap) var(--c-ease, ease),
              transform 260ms var(--c-ease, ease),
              background-color 250ms var(--c-ease, ease),
              border-color 250ms var(--c-ease, ease),
              color 250ms var(--c-ease, ease);
}
/* ── The pill as a .cb ──────────────────────────────────────────────────────
   The pill is positioned by its four insets, and the button component's own
   sizing fought every one of them: .cb sets min-height: 50px over the 46px
   --pill-h, and cb--full sets width: 100%, which resolves against the CARD
   rather than the inset box and pushed the pill a full --pill-inset past the
   right edge. That is why it read as oversized, full-bleed and hanging off
   the card's bottom corner.

   cb--full is no longer passed at all. An absolutely positioned box with
   both left and right set and width: auto fills exactly the space between
   them, which is the geometry this pill was designed with. */
.ccard__buy.cb {
  min-height: 0;
  height: var(--pill-h);
  width: auto;
  padding-inline: 14px;
  /* .ccard__buy sets gap: 6px for the old three-span layout. The layers are
     absolute or visually hidden now, so a gap only pads the flow copy. */
  gap: 0;
}

/* ── Truncating the name, and only at rest ─────────────────────────────────
   .cb__label is the copy in normal flow, so it is the one that has to fit.
   Made a flex row so the name can be the only part that gives: BUY and the
   price hold their size, the name takes what is left and ellipses.

   padding-inline-start replaces the text-indent the non-flex label used —
   the tracking is trailing space on the last letter, and without the
   matching lead the label sits visibly left of centre.

   The .cb__i track copies are deliberately NOT covered by this. They are
   max-content and nowrap, so the marquee scrolls the FULL title: the pill
   fits at rest and completes itself on hover. */
.ccard__buy .cb__label {
  display: flex;
  align-items: baseline;
  gap: 0.5ch;
  min-width: 0;
  max-width: 100%;
  text-indent: 0;
  padding-inline-start: var(--t-track, 0.2em);
}
.ccard__buy .cb__label .cb__nm {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The pill's resting fill. The component's outline variant is transparent by
   design; on a card the pill needs its own ground, because at rest it sits
   over a photograph. Hover is left entirely to .cb__fill — an override here
   would repaint the ground the face label is measured against and put white
   text on a cream pill for the length of the wipe, which is the exact fault
   the two-layer fill was built to remove. */
.ccard__buy.cb {
  background: transparent;
  border-color: var(--t-ink);
}

/* ---- Hover, desktop pointers only -----------------------------------------
   THREE conditions, and the width one is not redundant. Some Android browsers
   and any phone paired with a stylus or mouse report `hover: hover` and
   `pointer: fine`, and on those devices a tap LATCHES :hover until you tap
   elsewhere — the card fell into its hover state and stayed there, showing the
   full-bleed image and the pill with the name, price and descriptor faded out.
   That is exactly the "card content is missing on mobile" report.

   The width floor means a phone can never enter this state whatever it claims
   about its pointer, so the resting content is permanent there. */
@media (hover: hover) and (pointer: fine) and (min-width: 1000px) {
  /* THE IMAGE SWAP IS THE ONLY HOVER BEHAVIOUR.

     It used to be five at once: the foot faded out, the category turned white,
     the badge changed colour, the buy pill slid up from below the card edge and
     the product shot faded away. Each was defensible on its own and together
     they meant pointing at a card rearranged it. All removed.

     The pill is consequently visible at rest here, as it already was on touch,
     so the card reads the same way on every device. */
  .ccard--swap .ccard__inner:hover .ccard__hover,
  .ccard--swap .ccard__inner:focus-visible .ccard__hover { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .ccard__hover { transition: none; }
}

/* ---------- Grid rhythm ---------- */
.ccar__track,.cgrid{gap:34px 14px}
@media(min-width:700px) and (max-width:999px){.ccar__track,.cgrid{grid-template-columns:repeat(3,minmax(0,1fr));gap:48px 20px}}
@media(min-width:1000px){.ccar__track,.cgrid{grid-template-columns:repeat(4,minmax(0,1fr));gap:64px 28px}}

/* ---------- Section headings: one scale, 28 / 38 ---------- */
.ccar__h,.csec__h,.cstory__h,.ced__h,.ccol__h,.csg__h{font-family:var(--c-serif);font-weight:400}
.ccar__h{font-size:28px;letter-spacing:-.026em;line-height:1.08}
@media(min-width:1000px){.ccar__h{font-size:38px}}
.ccar__head{margin-bottom:26px;align-items:baseline}
@media(min-width:1000px){.ccar__head{margin-bottom:44px}}
.ccar{padding:var(--c-sp-tight) 0}
.cstory{padding:var(--c-sp-tight) 0}
.ccar__all{font-size:9px;letter-spacing:var(--c-eyebrow)}
.ccar--surface{background:var(--c-surface)}
.chl__h{font-size:10px;letter-spacing:var(--c-eyebrow);text-transform:uppercase;color:var(--c-soft);margin:0 0 14px;font-weight:400}

/* ---------- Collection toolbar ---------- */
.ccol__head{padding:20px 20px 12px}
@media(min-width:1000px){.ccol__head{padding:48px 40px 24px}}
.ccol__h{letter-spacing:-.026em;font-size:28px;line-height:1.08}
@media(min-width:1000px){.ccol__h{font-size:38px}}
.ccol__d{font-size:13.5px;line-height:1.7;margin:12px auto 0;max-width:46ch}
@media(min-width:1000px){.ccol__d{font-size:14px}}
.ccol__bar{border-top:1px solid var(--c-hair);border-bottom:1px solid var(--c-hair);padding-block:6px;margin-bottom:22px;align-items:center}
@media(min-width:1000px){.ccol__bar{padding-block:8px;margin-bottom:32px}}
.ccol__count{letter-spacing:var(--c-label);font-size:10px;color:var(--c-soft)}
.ccol__tools{gap:22px}
.ccol__btn,.ccol__sel{letter-spacing:var(--c-label);font-size:10px;transition:color .18s var(--c-ease)}
.ccol__btn:hover,.ccol__sel:hover{color:var(--c-clay)}
.ccol__sel{background-image:linear-gradient(45deg,transparent 50%,var(--c-soft) 50%),linear-gradient(135deg,var(--c-soft) 50%,transparent 50%);background-position:calc(100% - 6px) calc(50% + 1px),calc(100% - 2px) calc(50% + 1px);background-size:4px 4px,4px 4px;background-repeat:no-repeat;padding-right:16px}

/* ---------- Benefit band grounds ---------- */
.cben--ivory{background:var(--c-bg);color:var(--c-fg)}
.cben--div.cben--ivory,.cben--div.cben--cream{border-top:1px solid var(--c-hair);border-bottom:1px solid var(--c-hair)}
.cben--ivory .cben__t,.cben--cream .cben__t{color:var(--c-muted);opacity:1}
.cben--ivory .cben__h,.cben--cream .cben__h{color:var(--c-fg)}


/* ═══ RANGE CARD EVENNESS ══════════════════════════════════════════════════ */

/* Equal heights, with the title and price bottom-aligned. The rail's items
   stretch, the inner is a column, and the foot takes the leftover — so a card
   with a short title and one with a long one still put their prices on the
   same line. */
.crange__grid > * { align-self: stretch; }
.crange__grid > * > .ccard,
.crange__grid .ccard { height: 100%; }
.ccard__foot { margin-top: auto; }

/* THE WHOLE PRODUCT, UNCROPPED.

   Back to contain, deliberately. cover filled the frame but cropped the
   jewellery, and a ring with its band cut off is the one thing a product card
   cannot do. contain shows the piece whole at its true proportions; the small
   amount of ground it leaves is the card's own cream, so the media does not
   read as a separate panel inside the card.

   The padding that used to sit around a contained image is gone with it — it
   was what made the product look lost — so the piece fills as much of the
   frame as its own aspect ratio allows. */
.ccard__media.ccard__media.ccard__media {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  display: block;
  overflow: hidden;
  /* No radius and no separate ground: the media sits ON the card surface, so
     there is nothing to round off and nothing to distinguish from it. */
  border-radius: 0;
  background: transparent;
}
.ccard__media .ccard__img.ccard__img.ccard__img,
.ccard__media .ccard__hover.ccard__hover.ccard__hover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  object-fit: contain;
  object-position: center;
}


/* ═══ CARD HOVER: THE SWAP, AND NOTHING ELSE ══════════════════════════════ */

/* Both images in the same fixed-ratio frame, both absolute, so neither can
   contribute height and the swap cannot move the card by a pixel. */
.ccard__media .ccard__hover {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity 250ms var(--c-ease, ease);
  pointer-events: none;
}

/* The card itself never responds. Stated rather than assumed: these were
   measured as already absent, and naming them stops the next change quietly
   reintroducing one. */
.ccard__inner,
.ccard__inner:hover,
.ccard__inner:focus-visible {
  transform: none;
  box-shadow: none;
  transition: none;
}

/* ── Below 700px: the title and the price stack ────────────────────────────
   Side by side, the row gives the price whatever it asks for (flex: none,
   nowrap) and the title whatever is left. On a 169px column with a compare-at
   price that left the title around 60px, which is not a column you can set a
   product name in. Stacked, the title gets the full card width and the price
   sits under it, and neither has to negotiate with the other.

   Above 700px this rule does not apply and the side-by-side row is unchanged. */
@media (max-width: 699px) {
  /* The pill drops the product name. It was ellipsing to "BUY SU… · $39",
     which spends the pill's width on a fragment that identifies nothing — the
     title is directly above it on the card. The separator lives inside .cb__nm
     so hiding the name does not leave a stray "·" behind, and the
     visually-hidden copy that carries the accessible name is a different
     element and still says the whole thing.

     Everything else that used to live in this block — the two-line clamp, the
     right-aligned price, the compare-at ordering — is now unconditional, so
     a card has the same structure at every width. */
  /* WAS display:none - see calmy-range.css. The title is never hidden now;
     it is the only part of the label allowed to shrink, and it ellipses. */
  .ccard__buy .cb__nm { display: inline; }
}



/* ═══════════════════════════════════════════════════════════════════════════
   EDITORIAL CARD
   Resting: a word, a badge, the product on the card ground, then the facts.
   Hover:   the lifestyle shot takes the whole card and the buy pill rises.

   WHY IT IS AT THE END OF THIS FILE, WITH DOUBLED CLASSES. Sixteen
   stylesheets carry .ccard rules, and three of them - collection, range,
   carousel - load AFTER this one in the body. Single-class rules here would
   lose to them on document order. Doubling the class is the theme's own
   existing answer (.ccard__media.ccard__media.ccard__media already ships) and
   it wins on specificity regardless of load order.

   Colours, radius and spacing are the theme's tokens throughout. Nothing new
   is introduced: --t-page is the card surface, --t-card the section ground it
   sits on, --t-neutral the muted warm grey, --t-ink the dark.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ---- Surface -------------------------------------------------------------
   --t-page, a shade LIGHTER than the --t-card ground a grid sits on, so the
   card lifts by surface rather than by shadow. */
.ccard.ccard .ccard__inner {
  position: relative;
  /* Sampled from the photographs - see --calmy-card-surface in calmy-cozy.css.
     The card is the ONLY thing that changes ground; the page keeps its cream. */
  background: var(--calmy-card-surface);
  /* 0.75, up from 0.55. The card surface and the page cream are now within a
     few points of each other, so the hairline is the only thing left saying
     "this is a card" - it has to carry that on its own. Radius unchanged. */
  border: 1px solid rgb(var(--t-neutral-lt-rgb) / 0.75);
  border-radius: 16px;
  padding: 16px;
  overflow: hidden;
  isolation: isolate;
  box-shadow: none;
}
@media (min-width: 750px) {
  .ccard.ccard .ccard__inner { padding: 18px; }
}

/* ---- The word ------------------------------------------------------------
   Large, lowercase, tight, and quiet. It is the card's headline, so it is set
   in the display face at a size that reads as type rather than as a label -
   but in --t-neutral, so it never competes with the product title below. */
.ccard.ccard:has(.ccard__badge) .ccard__mood { padding-right: 52px; }

/* 20px on the phone grid, where two cards share the width; the card is wide
   enough for the full 26/30px from 750px up. */
.ccard.ccard .ccard__mood {
  position: relative;
  z-index: 2;
  margin: 0 0 6px;
  font-family: var(--c-serif);
  font-weight: 400;
  font-size: 20px;
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: lowercase;
  text-indent: 0;
  color: var(--t-neutral);
  min-height: 1em;
  /* The badge is absolutely positioned top-right, so the word has to be told
     to stop before it. Reserved ONLY when a badge is actually there - :has()
     means a card without one gets the full width back rather than a permanent
     hole. Ellipsis because on a 2-up phone grid the card is ~125px wide and no
     size of display type fits a long word AND a pill; truncating is the one
     outcome that neither overlaps nor silently shrinks the type. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* Reads over the lifestyle shot once that arrives. Same clock as the fade. */
  transition: color 250ms var(--c-ease, ease);
}
@media (min-width: 750px) {
  .ccard.ccard .ccard__mood { font-size: 26px; }
}
@media (min-width: 1000px) {
  .ccard.ccard .ccard__mood { font-size: 30px; }
}
.ccard.ccard .ccard__mood:empty { display: block; }

/* ---- Badge ---------------------------------------------------------------
   Subtle ground, not a shout. Absent from the DOM when no badge resolves. */
.ccard.ccard .ccard__badge {
  top: 16px;
  right: 16px;
  z-index: 3;
  background: rgb(var(--t-ink-rgb) / 0.06);
  color: var(--t-neutral);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: lowercase;
}
@media (min-width: 750px) {
  .ccard.ccard .ccard__badge { top: 18px; right: 18px; }
}

/* ---- The product ---------------------------------------------------------
   Square and contained, with room around it. contain, not cover: this is the
   object on its own ground, and cropping a ring to fill a box loses the ring. */
.ccard.ccard .ccard__media.ccard__media {
  position: relative;
  z-index: 0;
  aspect-ratio: 1 / 1;
  /* flex: none, or the ratio is decoration. .ccard__inner is a column flex
     container and the media inherited flex: 1 1 auto, so it grew to fill the
     leftover height and the square came out 251x282. aspect-ratio only decides
     the box when nothing else is stretching it. */
  flex: 0 0 auto;
  min-height: 0;
  margin: 4px 0 14px;
  /* Nothing behind the product. calmy-cardsurface.css gives .ccard__media a
     background AND an `inset 0 0 0 1px` hairline; the background was already
     overridden here, the inset was not, and it was the faint box still drawn
     round every photograph. Both are off now, so the card's cream runs
     straight under the PNG. */
  background: none;
  border: 0;
  box-shadow: none;
  overflow: visible;
}
.ccard.ccard .ccard__media .ccard__img.ccard__img {
  position: static;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  /* Breathing room inside the square rather than a smaller square. */
  /* 2%, down from 6%. The new PNGs are 1600x1600 with the product centred and
     already evenly padded, so the wrapper was adding a second margin on top of
     the first and the ring sat small in its square. The product fills more of
     the card now and still never reaches the card's edge or the text block. */
  padding: 2%;
  transition: opacity 250ms var(--c-ease, ease);
}

/* ---- The lifestyle shot --------------------------------------------------
   A child of .ccard__inner, so inset:0 is the whole card and the card's own
   overflow:hidden clips it to the 16px radius. Absolutely positioned in both
   states: revealing it cannot move anything. */
.ccard.ccard .ccard__hover.ccard__hover {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  padding: 0;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
  transition: opacity 250ms var(--c-ease, ease);
}

/* ---- The facts -----------------------------------------------------------
   Rating (when there is one), then title and price on one row, then the
   subtitle. Each is absent from the DOM when its data is, so nothing here
   needs an empty state. */
.ccard.ccard .ccard__foot {
  position: relative;
  z-index: 2;
  margin-top: auto;
  transition: opacity 250ms var(--c-ease, ease);
}
.ccard.ccard .ccard__sub {
  display: block;
  margin-top: 6px;
  font-family: var(--c-sans);
  font-size: 12px;
  line-height: 1.45;
  color: var(--t-neutral);
  /* 0.85, measured: --t-neutral on --t-page is 6.66:1 and 0.85 of it is
     4.65:1, still past AA for 12px. 0.7 looked right and measured 3.4:1. */
  opacity: 0.85;
}

/* ---- The buy pill --------------------------------------------------------
   TOUCH FIRST. With no hover there is nothing to reveal, so the pill is in
   normal flow at the foot of the card and always visible. The pointer rules
   below are the exception, not the base. */
.ccard.ccard .ccard__buy {
  position: static;
  margin-top: 12px;
  width: 100%;
  opacity: 1;
  transform: none;
}

/* ═══ Pointer devices only ═══════════════════════════════════════════════ */
@media (hover: hover) and (pointer: fine) {
  /* Out of flow, so showing it cannot change the card's height. */
  .ccard.ccard .ccard__buy {
    position: absolute;
    left: 18px;
    right: 18px;
    bottom: 18px;
    z-index: 4;
    width: auto;
    margin-top: 0;
    opacity: 0;
    transform: translateY(calc(100% + 18px));
    transition: transform 250ms var(--c-ease, ease), opacity 250ms var(--c-ease, ease);
  }
  /* Cream ground, dark text - legible on a photograph, which is what it lands
     on once the lifestyle shot is up. */
  .ccard.ccard .ccard__buy.cb {
    background: var(--t-page);
    border-color: var(--t-page);
    color: var(--t-ink);
  }
  .ccard.ccard:hover .ccard__buy,
  .ccard.ccard:focus-within .ccard__buy {
    opacity: 1;
    transform: translateY(0);
  }

  /* THE SWAP, AND NOTHING ELSE - and the swap is one-sided.

     What was here: on :hover of the whole card, the second image faded in,
     the FIRST IMAGE faded out, the foot faded out, the eyebrow turned cream
     and the badge inverted. All five are gone.

       · the first image must never be hidden. Hiding it was only safe while
         the same condition showed the second one; the moment the reveal moved
         to the image box and the hiding stayed on the card, the two disagreed
         and the picture area went blank between them. One-sided means the
         failure cannot recur: the resting photograph is always underneath.
       · the foot, the eyebrow and the badge belong to the card, not to the
         picture, and the picture no longer covers them.

     .is-over-media is set by calmy-cardswap.js from the pointer's position
     against the image box - the one condition both sides of this swap use. */
  .ccard--swap.is-swap-ready.is-over-media .ccard__hover,
  .ccard--swap.is-swap-ready:focus-within .ccard__hover { opacity: 1; }
}

/* Nothing moves for anyone who asked for that. */
@media (prefers-reduced-motion: reduce) {
  .ccard.ccard .ccard__buy,
  .ccard.ccard .ccard__hover,
  .ccard.ccard .ccard__img,
  .ccard.ccard .ccard__foot,
  .ccard.ccard .ccard__mood { transition-duration: 0.01ms; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   EDITORIAL CARD — corrections
   1  the lifestyle shot covers the WHOLE card, not the media stage
   2  the buy pill, including its sold-out state, and the swipe disc above it
   3  equal heights, and a title that never breaks mid-word
   ═══════════════════════════════════════════════════════════════════════════ */

/* ---- 1. Edge to edge -----------------------------------------------------
   display is declared HERE because leaving it out is what let
   calmy-range.css's `.crange__grid .ccard__hover { display: none }` win on
   that one property while every other declaration of ours applied. A rule
   only wins the properties it actually names. */
.ccard.ccard .ccard__hover.ccard__hover {
  display: block;
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  object-fit: cover;
  /* Inherited from .ccard__inner, so the photograph is clipped to the card's
     own corners rather than squaring them off. */
  border-radius: inherit;
  opacity: 0;
  pointer-events: none;
  transition: opacity 250ms var(--c-ease, ease);
}

/* The whole image box and the whole foot used to fade to 0 on hover, so the
   lifestyle shot could have the card. Removed with the rest of the takeover:
   fading .ccard__media took the resting photograph with it and was the other
   half of the blank-card bug. */

/* ---- 2. The pill ---------------------------------------------------------
   Sold out: the same geometry, greyed, and inert. It is a <span>, so there is
   nothing to disable - pointer-events is what stops it answering. */
.ccard--soldout.ccard--soldout .ccard__buy {
  opacity: 0.55;
  pointer-events: none;
}
@media (hover: hover) and (pointer: fine) {
  .ccard--soldout.ccard--soldout:hover .ccard__buy,
  .ccard--soldout.ccard--soldout:focus-within .ccard__buy { opacity: 0.55; }
}

/* THE SWIPE DISC AND THE PILL, AND WHY THERE IS NO CSS RULE HERE.

   I previously lifted `.cswipe`'s bottom edge to keep the disc off the pill.
   That was the wrong tool and it is what produced the reported bug: .cswipe is
   a CLIP LAYER, so raising its bottom does not move the disc, it SLICES it —
   the disc carried on tracking the cursor and got cut in half at the new
   boundary the moment the pointer went low.

   The clip layer is back to inset:0, the card's own box. Keeping the two
   apart is calmy-swipecursor.js's job and always was: it fades the disc to
   zero near the pill. That rule is in the script because it needs the pointer
   position, which CSS does not have. */

/* ---- 3. Equal heights, whole words --------------------------------------- */
.ccard.ccard { height: 100%; }
.ccard.ccard .ccard__inner { height: 100%; }

/* Two lines maximum, clamped, and broken only BETWEEN words - no
   "Clarity Spinner Rin...". word-break:normal and overflow-wrap:normal are
   both restated because several stylesheets set `anywhere` on card text, and
   `anywhere` is exactly what puts the ellipsis mid-word.

   min-height reserves both lines whether or not the second is used, so a
   one-line title and a two-line title leave their prices on the same baseline
   and the cards line up. */
.ccard.ccard .ccard__t {
  display: block;
  overflow: hidden;
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
  /* Two lines reserved, stated against the title's OWN line-height rather than
     a guessed multiplier. calc(2 * 1.25em) assumed a 1.25 line-height the
     title does not have, so it under-reserved by ~4px and a one-line title
     ("Bead Roller Ring") left its card's foot 4px shorter than the two-line
     titles beside it - which pushed that card's title 3px down the row. */
  line-height: 1.3;
  min-height: 2.6em;
}

/* ---- Corrections, part two -----------------------------------------------
   Two things the block above got wrong, both of them cascade errors of my own
   making rather than anything the theme was doing.

   1  The reveal. `.ccard--swap:hover .ccard__hover` and the resting
      `.ccard.ccard .ccard__hover.ccard__hover { opacity: 0 }` are BOTH four
      classes, and the resting one is written later in this file - so it won
      the tie and the lifestyle shot never faded in. Measured: opacity stayed
      at 0 through hover while the word and the foot both reacted. Five
      classes settles it, rather than moving rules around and hoping.

   2  The pill's width. calmy-range.css sets `.crange__grid .ccard__buy.cb
      { width: 100% }` at three classes and loads after this file, so an
      absolutely positioned pill anchored with left+right had its width
      re-imposed from its content box and came out 221px inside a 295px card.
      Anchoring and width are restated together at four classes. */
.ccard--swap.is-swap-ready.is-over-media .ccard__hover.ccard__hover,
.ccard--swap.is-swap-ready:focus-within .ccard__hover.ccard__hover { opacity: 1; }

@media (hover: hover) and (pointer: fine) {
  .ccard.ccard .ccard__buy.ccard__buy {
    left: 16px;
    right: 16px;
    bottom: 16px;
    width: auto;
    max-width: none;
    display: flex;
  }
  @media (min-width: 750px) {
    .ccard.ccard .ccard__buy.ccard__buy { left: 18px; right: 18px; bottom: 18px; }
  }
}
/* Touch keeps it in flow and full width. */
@media not all and (hover: hover) and (pointer: fine) {
  .ccard.ccard .ccard__buy.ccard__buy { width: 100%; max-width: none; }
}

/* ---- The rating row's reserved space -------------------------------------
   One line, always, directly above the title. calmy-rating emits nothing
   without a real score and count, so this wrapper is what keeps an unreviewed
   card the same height as a reviewed one and the row's titles and prices on a
   single baseline. Sized in em off the row's own font-size rather than a fixed
   pixel height, so it follows the type scale. */
.ccard.ccard .ccard__rate {
  display: block;
  font-size: 12px;
  min-height: 1.35em;
  line-height: 1.35;
}
.ccard.ccard .ccard__rate:empty { display: block; }

/* The title and the price start on the same line, not on the same baseline.

   With align-items:baseline the row aligns the FIRST baseline of each side, so
   a price carrying the "From" label sets a taller box and pushes the title
   down with it - measured 3px on Bead Roller Ring against the five cards
   beside it, which is exactly the misalignment the reserved rating row was
   added to remove. flex-start puts both boxes on the row's top edge, so the
   title's position no longer depends on what the price happens to say. */
.ccard.ccard .ccard__row { align-items: flex-start; }

/* ---- The word's contrast --------------------------------------------------
   MEASURED, not eyeballed. On the cream card the word was --t-neutral, which
   is 6.66:1 against --t-page - legible by the letter of AA and, at 20-30px of
   light-weight serif, still reading as something half-erased. The product
   title beside it is --t-ink at 15.46:1.

   --t-ink at 0.85 lands on 9.78:1: unmistakable at a glance, and still a clear
   step below the title, so the word stays the quiet label it is meant to be
   rather than becoming a second headline. Same token, same hue - only the
   amount of it changes.

   Font, size, letter-spacing and position are untouched; this rule sets
   colour and nothing else. */
.ccard.ccard .ccard__mood { color: rgb(var(--t-ink-rgb) / 0.85); }

@media (hover: hover) and (pointer: fine) {
  /* THE EYEBROW KEEPS ITS INK. It used to turn cream with an ink halo on
     hover, because the lifestyle photograph came up behind it and covered the
     card. The photograph is confined to the image box now and the word sits
     on the card's own cream ground in every state - so turning it cream on
     hover left it washed out against the very surface it has to read on.
     Last of the whole-card takeover to go. */
}

/* ---- A ground for the word on hover ---------------------------------------
   MEASURED, and the reason this exists. Cream on the photograph only works if
   the photograph is dark there. Sampling the top-left region of the four
   lifestyle shots actually in use:

     everyday   rgb(14,13,14)     cream reads at 18.15:1
     focus      rgb(241,240,240)  cream reads at  1.06:1
     evening    rgb(249,249,249)  cream reads at  1.01:1
     restless   rgb(238,238,239)  cream reads at  1.09:1

   Three of the four are near-white where the word sits, so a cream word on
   them is invisible, and no text-shadow rescues 1.01:1 - a soft halo behind a
   pale letterform on a pale ground just greys the edge.

   So the word gets its own ground: a soft dark wash in the TOP-LEFT CORNER
   ONLY, fading to nothing well before the middle of the card, sitting between
   the photograph (z-1) and the word (z-2). On the dark shot it is invisible
   because it is already dark there; on the pale shots it is what makes the
   word readable. It appears only on hover, on the same 250ms clock, and it is
   inert.

   On .ccard__inner rather than on .ccard__mood: the word is clipped for its
   ellipsis, so a pseudo-element on it would be cut off at the text box. */
.ccard.ccard .ccard__inner::before {
  content: "";
  position: absolute;
  inset: 0 auto auto 0;
  width: 78%;
  height: 38%;
  /* 2, not 1. The photograph is a real child at z-index 1 and a ::before is
     painted before its element's children, so at the same z-index the photo
     covered the scrim completely - measured 1.18:1, i.e. no effect at all.
     The word goes to 3 to stay above this. */
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  /* Strength set by measurement, not by eye. At 0.62/0.34 the word measured
     2.96:1 over the palest shot - under the 3:1 that 30px text needs, and only
     just over it on the next one. These stops put the ground under the word at
     roughly rgb(112,110,108), which carries cream past 4.5:1 with room to
     spare, while still fading to nothing by 78% so it never reads as a bar
     across the corner. */
  background: radial-gradient(
    125% 105% at 0% 0%,
    rgb(var(--t-ink-rgb) / 0.86) 0%,
    rgb(var(--t-ink-rgb) / 0.62) 42%,
    rgb(var(--t-ink-rgb) / 0) 78%);
  transition: opacity 250ms var(--c-ease, ease);
}
/* The scrim carried the eyebrow over a photograph that covered the card. The
   photograph stays in its box now, so it never comes up. */

/* The word rides above its own scrim. */
.ccard.ccard .ccard__mood { z-index: 3; }

/* ---- The buy pill's label -------------------------------------------------
   BUY <TITLE> - <PRICE> on one line, always. The title is the only part that
   gives: it shrinks and ellipses. The hyphen and the price are flex:none, so
   the price is never the thing that gets cut. */
.ccard.ccard .ccard__buy .cb__nm {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ccard.ccard .ccard__buy .cb__sep,
.ccard.ccard .ccard__buy .cb__from,
.ccard.ccard .ccard__buy .cb__b {
  flex: 0 0 auto;
  white-space: nowrap;
}
/* The price carries the weight; BUY and the title stay regular, so the number
   is what the eye lands on. */
.ccard.ccard .ccard__buy .cb__b { font-weight: 700; }
.ccard.ccard .ccard__buy .cb__from { font-weight: inherit; }

/* ═══════════════════════════════════════════════════════════════════════════
   THE CARD'S TEXT BLOCK — three stacked rows, left aligned
     1  stars + (count), always occupying its line
     2  TITLE ......... PRICE, one line, space-between
     3  the subtitle, quieter and smaller
   Applies to every product card on the store, not just the range carousel:
   these are .ccard rules in the shared stylesheet, doubled so they also beat
   calmy-range.css and calmy-collection.css, which load after this file.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ---- Row 1: rating -------------------------------------------------------
   Height held whether or not a rating exists, so rows 2 and 3 sit on the same
   line across every card in a row. calmy-rating still emits nothing without a
   real score and count - the space is reserved, the stars are never faked. */
.ccard.ccard .ccard__rate {
  display: block;
  font-size: 11px;
  line-height: 1.4;
  min-height: 1.4em;
  margin: 0;
  color: var(--t-ink);
}
/* The count, one step quieter than the stars but the same size. */
.ccard.ccard .ccard__rate .crate__n,
.ccard.ccard .ccard__rate .crate__count { opacity: 0.6; }

/* ---- Row 2: title and price on one line ---------------------------------- */
.ccard.ccard .ccard__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-top: 6px;
}
.ccard.ccard .ccard__t {
  /* One line. The full title is not the card's job - the pill and the product
     page both carry it - and a second line here is what pushed the price and
     the subtitle out of alignment between cards. */
  display: block;
  min-width: 0;
  min-height: 0;
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--c-sans);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.04em;
  line-height: 1.35;
  text-transform: uppercase;
  color: var(--t-ink);
}
/* The price matches the title exactly in size and weight, and never gives:
   flex:none and nowrap mean a long title ellipses and the price does not. */
.ccard.ccard .ccard__p {
  flex: 0 0 auto;
  white-space: nowrap;
  font-family: var(--c-sans);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.04em;
  line-height: 1.35;
  color: var(--t-ink);
}
.ccard.ccard .ccard__p .ccard__now,
.ccard.ccard .ccard__p .ccard__from { font-size: inherit; font-weight: inherit; letter-spacing: inherit; }
/* No opacity: at 0.55 the struck price was 2.4:1 on the card ground, which is
   under the floor for text this size. The colour carries the demotion instead. */
.ccard.ccard .ccard__p .ccard__was { font-size: inherit; font-weight: 400; color: var(--ccard-was); }

/* ---- Row 3: subtitle ----------------------------------------------------- */
.ccard.ccard .ccard__sub {
  display: block;
  margin-top: 6px;
  font-family: var(--c-sans);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: 0;
  text-transform: none;
  color: var(--t-neutral);
  /* 0.85 measured at 4.65:1 on the cream card - quieter than the title and
     still past AA. Absent entirely when a product has no subtitle, so the card
     simply ends after the price without disturbing rows 1 and 2. */
  opacity: 0.85;
}

/* Row 3's height is reserved ONLY where the media stage flexes.

   calmy-range.css makes .ccard__media `flex: 1 1 auto`, so the stage absorbs
   whatever the text block does not use. A one-line subtitle therefore makes
   that card's whole text block shorter, the stage taller, and rows 1 and 2
   drop - measured 17px between a one-line and a two-line subtitle in the same
   row, which is exactly the misalignment this block is meant to remove.

   Two lines reserved there puts every foot at the same height and every title
   on one line across the row. Everywhere else the media is a fixed 1/1 square,
   so the rows already align and a missing subtitle is allowed to let the card
   sit shorter, as intended. */
.crange__grid .ccard.ccard .ccard__sub { min-height: 2.9em; }

/* ---- FIX 2: the stars, readable on cream ---------------------------------
   MEASURED against the card's --t-page surface:
     filled   was var(--crate-star, #B08968)  2.97:1  - washed out at 11px
     unfilled was var(--c-line)   #DBD1C0     1.41:1  - all but invisible

   Filled goes to --t-accent, the theme's own warm clay, at 5.42:1: clearly
   there at a glance and still warm rather than a black glyph on cream. The
   empty track lifts to 1.80:1 - visible as a track, and far enough below the
   filled stars that the proportion reads instantly.

   Scoped to the card. The PDP and the collection hero set their own star
   colours against their own grounds and are left alone. Colour only: size,
   spacing and the row's position are untouched, and the count in parentheses
   keeps the treatment it already had. */
.ccard.ccard .ccard__rate .crate__star { color: rgb(var(--t-neutral-rgb) / 0.38); }
.ccard.ccard .ccard__rate .crate__starf { color: var(--t-accent); }

/* ---- No tone filter on product cards -------------------------------------
   calmy-cardtone.js samples every photograph at runtime and writes --tone-b
   and --tone-s onto .ccard__media, which calmy-cardsurface.css turns into
   `filter: brightness() saturate()`. Measured on the live card:
   brightness(0.979) - the product was being darkened ~2% to make its baked-in
   sweep sit closer to the card.

   That is the compensation this whole exercise removes. It darkens silver,
   shifts gold and eats the highlights the brief asks to protect, and once a
   cut-out sits on a matching ground there is nothing left to compensate for.
   The script still runs and still measures; its result simply no longer paints. */
.ccard.ccard .ccard__media[data-tone] img,
.ccard.ccard .ccard__media[data-tone="tinted"] img { filter: none; }

/* ---- The card's star row -------------------------------------------------
   Five separate SVGs in a tight row, dark charcoal filled against a light grey
   remainder. Sized in px so the row height is fixed and the count sits on the
   same optical centre line. */
.ccard.ccard .crate__stars--svg {
  --crate-on: var(--t-ink);
  --crate-off: rgb(var(--t-neutral-rgb) / 0.22);
  display: inline-flex;
  align-items: center;
  gap: 1px;
  line-height: 0;
}
.ccard.ccard .crate__s { display: block; width: 16px; height: 16px; flex: none; }
@media (min-width: 750px) {
  .ccard.ccard .crate__s { width: 18px; height: 18px; }
  .ccard.ccard .crate__stars--svg { gap: 2px; }
}
/* The row: stars, then the count 8px after them, both vertically centred. */
.ccard.ccard .ccard__rate .crate {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}
.ccard.ccard .ccard__rate .crate__txt {
  font-size: 13px;
  line-height: 1;
  color: var(--t-neutral);
  white-space: nowrap;
}
@media (min-width: 750px) {
  .ccard.ccard .ccard__rate .crate__txt { font-size: 14px; }
}
/* No trace of the old two-layer track. */
.ccard.ccard .crate__stars--svg .crate__star,
.ccard.ccard .crate__stars--svg .crate__starfill { display: none; }


/* EVERY swap rule above is gated on .is-swap-ready, which calmy-cardswap.js
   adds only once the second image has decoded. Hovering a card whose second
   image is missing, still in flight or broken therefore changes nothing at all
   - the product stays visible instead of being faded out to reveal a gap. */

/* ═══ THE CARD LINK AND THE CART BUTTON ═════════════════════════════════════
   The card used to be one <a> wrapping everything, so paint order never
   mattered: every part of the card was inside the link. It is a <div> now
   with .ccard__link overlaying it, which makes the stacking explicit for the
   first time. Written out, inside .ccard__inner's own isolate:

     0  .ccard__media          the photograph (homepage)
     1  .ccard__hover          the lifestyle shot
     2  .ccard__mood           the eyebrow
     3  .ccard__link           THE CARD'S LINK - over the pictures and the
                               eyebrow, so a press anywhere on them opens the
                               product
     3  .ccard__badge          later in the tree, so it paints over the link
     4  .ccard__foot           the words, and on a collection card the
                               photograph too - both carry the cart button
     4  .ccard__buy            the pill
     5  .cswipe                the swipe disc, still on top of all of it

   Everything at 4 sits ABOVE the link, which would make the link unreachable
   under the card's own text - so everything at 4 that is not the button is
   pointer-events: none, and the press falls through to the link beneath.
   Only .ccard__add takes its clicks back. */
.ccard__link {
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  text-decoration: none;
  color: inherit;
}
/* NOTHING IN THE CARD TAKES A PRESS EXCEPT THE LINK AND THE BUTTON.

   Stated as one inverted rule rather than as a list of exceptions. Inside the
   old anchor every span - the eyebrow, the badge, the pill, the swatches, the
   words - was part of the link by construction. Now that the link is an
   overlay, any of them painted above it swallows the press instead, and which
   ones those are depends on a z-index scattered across four files: the badge
   at 3 and the eyebrow at 3 both sit over it, and naming them one at a time
   only holds until the next one is added.

   pointer-events inherits, so the two elements that ARE controls take theirs
   back - .ccard__link as a direct child, .ccard__add from wherever its
   placement puts it. */
.ccard .ccard__inner > * { pointer-events: none; }
.ccard .ccard__link,
.ccard .ccard__add { pointer-events: auto; }
.ccard.ccard .ccard__foot { z-index: 4; }

/* KEYBOARD. The ring used to be the anchor's own :focus-visible. The anchor is
   an overlay now, and an outline drawn on a box with inset: 0 is the same
   rectangle - but the pill's filled state has to follow the ancestor, which is
   what :has does here. */
.ccard .ccard__link:focus-visible { outline: 2px solid var(--t-ink); outline-offset: 2px; }
.ccard .ccard__inner:has(.ccard__link:focus-visible) .ccard__buy.cb {
  background-color: var(--t-ink);
  border-color: var(--t-ink);
  color: var(--t-page);
}
@media (hover: hover) {
  .ccard--swap .ccard__inner:has(.ccard__link:focus-visible) .ccard__hover { opacity: 1; }
}

/* ── The button ─────────────────────────────────────────────────────────────
   A white disc with the shared line-icon cart in it, 40px across on every
   card at every width. The 44px touch target is a transparent ::after that
   overhangs it by 2px a side, so the pressable area meets the minimum without
   the drawn circle growing to meet it and without the button taking 44px of
   layout from the row it sits in. */
.ccard__add {
  position: relative;
  z-index: 5;
  pointer-events: auto;
  flex: none;
  inline-size: 40px;
  block-size: 40px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--t-neutral-lt, rgb(0 0 0 / 0.14));
  border-radius: 50%;
  background: var(--t-white, #FFFFFF);
  color: var(--t-ink, #231E19);
  cursor: pointer;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.10), 0 4px 10px rgb(0 0 0 / 0.06);
  transition: background-color 0.18s var(--c-ease, ease), border-color 0.18s var(--c-ease, ease), color 0.18s var(--c-ease, ease);
}
.ccard__add::after { content: ''; position: absolute; inset: -2px; border-radius: 50%; }
.ccard__add .cico { inline-size: 19px; block-size: 19px; stroke: currentColor; fill: none; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }

/* HOVER IS A LIFT, NOT AN INVERSION. It filled with --t-ink and turned the
   glyph cream, which on a white circle sitting in a photograph read as a
   black disc appearing on the picture. The circle stays white-ish, takes a
   warmer ground one step off the page, a slightly firmer edge and a little
   more shadow - and the cart stays dark, which is the part that has to be
   legible whatever is behind it. */
@media (hover: hover) {
  .ccard__add:hover {
    background: var(--t-card, #F1EBE0);
    border-color: rgb(var(--t-ink-rgb) / 0.22);
    color: var(--t-ink, #231E19);
    box-shadow: 0 1px 3px rgb(0 0 0 / 0.12), 0 6px 14px rgb(0 0 0 / 0.10);
  }
}
.ccard__add:active { background: var(--t-card, #F1EBE0); }
/* The keyboard's own mark, and the same on all three placements. */
.ccard__add:focus-visible {
  outline: 2px solid var(--t-ink);
  outline-offset: 2px;
  background: var(--t-card, #F1EBE0);
  border-color: rgb(var(--t-ink-rgb) / 0.22);
  color: var(--t-ink, #231E19);
}

/* The three glyphs share one cell, so the box cannot change size between
   states and the button never moves under the finger mid-press. */
.ccard__add-g { grid-area: 1 / 1; display: grid; place-items: center; line-height: 0; }
.ccard__add-g--done,
.ccard__add-g--busy { opacity: 0; }
.ccard__add[data-state="busy"] .ccard__add-g--rest,
.ccard__add[data-state="done"] .ccard__add-g--rest { opacity: 0; }
.ccard__add[data-state="busy"] .ccard__add-g--busy { opacity: 1; animation: ccard-add-spin 0.7s linear infinite; }
.ccard__add[data-state="done"] .ccard__add-g--done { opacity: 1; }
.ccard__add[data-state="done"] {
  background: var(--t-card, #F1EBE0);
  border-color: rgb(var(--t-ink-rgb) / 0.35);
  color: var(--t-ink, #231E19);
}
.ccard__add[data-state="error"] { border-color: var(--t-clay, #A8503C); color: var(--t-clay, #A8503C); }
.ccard__add[disabled] { cursor: default; }
@keyframes ccard-add-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .ccard__add[data-state="busy"] .ccard__add-g--busy { animation: none; }
  .ccard__add { transition: none; }
}

/* ── Homepage placement: the price's line ───────────────────────────────────
   The row keeps everything it had; the wrapper beside it is what reserves the
   button's space, so the title wraps instead of running under the control. */
.ccard__priceline {
  display: flex;
  align-items: flex-end;
  gap: 12px;
}
.ccard__priceline .ccard__row { flex: 1 1 auto; min-inline-size: 0; }

/* ── THE IMAGE'S PRODUCT LINK, on the homepage range cards ──────────────────
   Elsewhere .ccard__link is an overlay at inset: 0 and the whole card is the
   target. Here the anchor is the image box itself, in normal flow, taking the
   space .ccard__media used to take - so the cart mark inside it can be placed
   against the photograph's own corner rather than against the card's.

   That is the reason for the restructure rather than a purely visual one: the
   range card's media is flex: 1 1 auto with aspect-ratio: auto, so its height
   is whatever is left after the heading and the information block. There is
   no ratio to mirror and no way to reach it from a sibling, so the link has
   to contain it. */
.ccard__link--media {
  position: relative;
  inset: auto;
  z-index: 3;
  /* A plain wrapper that stands where .ccard__media stood: same flex
     behaviour, no box of its own. The media keeps its ratio, its sizing and
     the 10px it carries below itself, and the mark is positioned against the
     media rather than against this, so none of those numbers live here. */
  display: block;
  flex: 0 0 auto;
  min-block-size: 0;
}

/* The badge is absolute against the card and used to out-paint the media by
   sitting at the same z-index later in the DOM. The link now comes after it
   and carries the photograph, so the badge is lifted one rung to stay on top
   of it. Keyed on the markup, so only the cards that restructured are moved. */
.ccard__inner:has(.ccard__link--media) .ccard__badge { z-index: 4; }

/* ── The cart mark ─────────────────────────────────────────────────────────
   A white disc 12px off the photograph's bottom-right corner, painted at
   every width and on every device - it has no hover state that brings it into
   existence and nothing hides it again.

   pointer-events: none is the load-bearing line. The mark is decoration, so
   the press it receives belongs to the anchor underneath it: one target, one
   destination, no control inside a link. */
.ccard__cart {
  position: absolute;
  inset-block-end: 12px;
  inset-inline-end: 12px;
  /* Above both photographs and above anything the media box does to them. It
     is not inside that box, so this is a rung in the link's own stacking
     order rather than a z-index war with the hover shot. */
  z-index: 2;
  inline-size: 40px;
  block-size: 40px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--t-white, #FFFFFF);
  color: var(--t-ink, #231E19);
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.10), 0 4px 10px rgb(0 0 0 / 0.06);
  /* It takes its own pointer events so it can answer :hover. It is still a
     span inside the anchor, so the press it receives is the anchor's press -
     nothing is nested and nothing new is clickable. */
  pointer-events: auto;
  /* transform, not width or inset: the lift cannot move anything around it. */
  transition:
    transform 200ms var(--c-ease, ease),
    box-shadow 200ms var(--c-ease, ease),
    background-color 200ms var(--c-ease, ease);
  will-change: transform;
}
/* THE 44px TARGET. An invisible 2px ring rather than padding, so the disc
   stays 40px while the thing a finger has to hit is 44. It is part of the
   mark's own box, so it answers :hover with it. */
.ccard__cart::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 50%;
}

/* THE LIFT IS THE MARK'S OWN HOVER, NOT THE CARD'S. Keyed on .ccard__cart
   itself, so pointing at the photograph, the title or the price leaves it
   exactly where it is - only the pointer actually over the disc moves it. */
@media (hover: hover) {
  .ccard__cart:hover {
    transform: scale(1.08);
    background: var(--t-page, #FDFBF7);
    box-shadow: 0 2px 6px rgb(0 0 0 / 0.14), 0 8px 18px rgb(0 0 0 / 0.10);
  }
}

/* KEYBOARD. The span cannot take focus - the anchor does - so the anchor's
   focus ring is drawn on the link and the mark takes the same lift, which is
   what tells a keyboard user which card they are on. */
.ccard__link--media:focus-visible {
  outline: 2px solid var(--t-ink, #231E19);
  outline-offset: 2px;
  border-radius: 14px;
}
.ccard__link--media:focus-visible .ccard__cart {
  transform: scale(1.08);
  background: var(--t-page, #FDFBF7);
  box-shadow: 0 2px 6px rgb(0 0 0 / 0.14), 0 8px 18px rgb(0 0 0 / 0.10);
}

/* REDUCED MOTION. The mark still brightens and still takes its shadow - the
   state stays legible - it just stops moving. */
@media (prefers-reduced-motion: reduce) {
  .ccard__cart { transition: none; will-change: auto; }
  .ccard__cart:hover,
  .ccard__link--media:focus-visible .ccard__cart { transform: none; }
}

/* The swipe follower hides over this mark and the ordinary arrow comes back -
   see the BUY list in calmy-swipecursor.js. Stated here too because a cursor
   is the one thing CSS and that script both have an opinion about. */
.ccard__cart { cursor: pointer; }

.ccard__cart .cico {
  inline-size: 19px;
  block-size: 19px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ═══ THE HOMEPAGE CARD'S HOVER, CONFINED TO ITS PHOTOGRAPH ═════════════════
   What this replaces. The homepage card answered a hover ANYWHERE on it by
   taking the whole card over: the lifestyle shot covered the card edge to
   edge, the words faded out, a dark scrim came up under the eyebrow, the
   badge inverted, and a pill slid in. Pointing at the price hid the price.

   The brief now asks for the opposite, and it is the better behaviour: the
   picture reacts to the picture, and the title, rating, price, description
   and cart button stay put and readable the entire time.

   HOW THE TRIGGER WORKS. .is-over-media is set by calmy-cardswap.js from the
   pointer's position against the media box's own rectangle, and only for a
   mouse or a pen. :hover cannot do it - the card's link is an overlay above
   the media and every other child is pointer-events: none, so the media box
   never sees a pointer event - and a media query cannot do it either, because
   the question is where in the card the cursor is, not what kind of screen it
   is. A touch device never gets the class at all, which is how hover-only
   effects stay off there while the rail still scrolls.

   Each rule below is stated at the specificity of the rule it overrides, so
   nothing here needs !important. */

/* The counter-overrides that used to sit here are gone. They existed to undo
   the whole-card takeover rule by rule - foot, eyebrow, badge, scrim, first
   image - which is exactly the stacking this file already had too much of.
   The rules they fought have been removed at source instead, above. */

/* The keyboard keeps its reveal: focus is not a pointer, and a customer
   tabbing to a card should still see the second photograph. */
.ccard--swap.is-swap-ready .ccard__inner:has(.ccard__link:focus-visible) .ccard__hover.ccard__hover { opacity: 1; }

/* 3 · BOUNDED BY THE PICTURE. The lifestyle shot is a child of .ccard__media
      in the Liquid now, so inset: 0 resolves against the image box; this
      states the clip and the corner so the swap can never paint outside it,
      and object-fit keeps the two photographs the same size as each other.
      The media box's own dimensions are untouched, so nothing reflows. */
.ccard.ccard .ccard__media { overflow: hidden; }
.ccard.ccard .ccard__media .ccard__hover.ccard__hover {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  border-radius: inherit;
}

/* 4 · NO HOVER EFFECTS ON A TOUCH SCREEN, stated as well as relied upon. */
@media (hover: none) {
  .ccard--swap .ccard__hover.ccard__hover { opacity: 0; }
}

/* ── BALANCE SPINNER RING: a little more room in the frame ──────────────────
   Its photograph is landscape (744x698) and the card's media box is portrait,
   so object-fit: cover was cropping the sides and clipping the outer ring.
   The image is scaled to 84% of the box and switched to `contain`, which
   shows the whole photograph, centred, with a margin of background around it
   - about a 16% zoom out, and the card's own dimensions are untouched.

   Scoped by the card's own product link rather than by a class, so it follows
   this product to every place its card renders - the collection grid, search,
   the 404 rail, recommendations - and reaches nothing else. One product, one
   rule, and removing the rule removes the whole change.

   THE EXPOSED GROUND MATCHES THE PHOTOGRAPH. Sampled from the file itself,
   column by column: the studio sweep is not flat and not linear either - it
   runs rgb(220) along the top edge, climbs to rgb(245) by the middle, holds
   there, then settles to rgb(240) along the bottom edge, where the sweep
   meets the prop's shadow. The stop values are the colours the browser
   actually paints at card size, read back off a screenshot rather than taken
   from the full-resolution file - downscaling blends the bottom shadow band
   upward by about ten levels, and the file's own rgb(250) bottom row would
   have left a bright lip under the photograph.

   The stops below are positioned in the MEDIA BOX's coordinates, not the
   photograph's. Contained at 84% the photograph covers only the middle
   18.6%-81.4% of the box vertically, so its top edge sits at 18.6% and its
   bottom edge at 81.4%; the gradient is held flat outside that span and
   traces the sampled sweep inside it. Matching the sweep in the photograph's
   own coordinates instead - the first attempt, a plain three-stop ramp - put
   rgb(234) beside an rgb(246) edge and left a visible seam down both sides. */
/* SPECIFICITY, STATED RATHER THAN GUESSED. The collection card's own rule is
   `.ccard--plain.ccard.ccard .ccard__media .ccard__img.ccard__img` - six
   classes - and it sets object-fit: cover, inline/block-size 100% and
   margin: 0. Anything less specific than that loses silently, which is
   exactly what happened on the first attempt. Repeating .ccard and
   .ccard__img here clears it, and clears the range card's five-class rule
   too, so this one block works in every placement. No !important. */
.ccard.ccard.ccard:has(a[href*="/products/duo-roller-ring"]) .ccard__media {
  /* background-IMAGE, not the `background` shorthand: the shorthand would
     reset background-color to transparent and throw away the card surface
     underneath. */
  background-image: linear-gradient(
    180deg,
    #DCDCDC 0%,
    #DCDCDC 18.5%,
    #E8E8E8 31%,
    #F2F2F2 44%,
    #F5F5F5 56%,
    #F5F5F5 62%,
    #F0F0F0 81.5%,
    #F0F0F0 100%
  );
}
/* THE RESTING SHOT ONLY. The hover lifestyle photograph is deliberately left
   alone: it covers the frame on every card in the theme, it is a hand rather
   than the pair of rings, so nothing about it needs to be zoomed out, and it
   is shot on a warm white that the grey sweep below would not have matched.
   Contained at 84% it would have sat in a visibly wrong ground. Covering, it
   hides the ground entirely while it is showing, and the resting shot is the
   only thing the gradient ever has to match. */
.ccard.ccard.ccard:has(a[href*="/products/duo-roller-ring"]) .ccard__media .ccard__img.ccard__img.ccard__img {
  object-fit: contain;
  /* inset + margin:auto centres it inside the box it no longer fills. */
  position: absolute;
  inset: 0;
  margin: auto;
  inline-size: 84%;
  block-size: 84%;
  width: 84%;
  height: 84%;
  max-inline-size: 84%;
  max-block-size: 84%;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CARD TITLES WRAP IN FULL

   Five stylesheets style .ccard__t and three of them clamp it, by three
   different mechanisms and at three specificities:

     calmy-cards.css       display:-webkit-box + -webkit-line-clamp
     calmy-cards.css       min-height: 2.6em, two lines reserved
     calmy-cards.css       white-space:nowrap + text-overflow:ellipsis, ONE line
     calmy-collection.css  -webkit-line-clamp: 3
     calmy-collection-card.css  (0,4,0) — the strongest selector in play

   Unpicking them one by one would leave the next reader to work out which of
   the five is in force on which card. This is one rule at (0,5,0), which beats
   every one of them on specificity rather than on load order, so it holds
   wherever the card appears - homepage, collection, search, 404 and the
   recommendations row.

   It undoes all three mechanisms at once: display:block takes the element out
   of -webkit-box (which is what makes -webkit-line-clamp work at all),
   white-space:normal and text-overflow:clip undo the single-line ellipsis, and
   the height trio releases the reserved two lines.

   Nothing about font-size, weight, colour or margin is touched, so each card
   variant keeps its own type. Titles grow downward in normal flow, so a
   three-line name pushes the subtitle and price down rather than over them. */
.ccard.ccard.ccard .ccard__t.ccard__t {
  display: block;
  -webkit-line-clamp: none;
  line-clamp: none;
  overflow: visible;
  text-overflow: clip;
  white-space: normal;
  min-height: 0;
  max-height: none;
  height: auto;
}

/* ═══════════════════════════════════════════════════════════════════════════
   PRODUCT CARDS SHOW THE WHOLE PRODUCT

   The card's media box is portrait (4:5) and most of these photographs are
   square. With object-fit: cover that difference came off the SIDES: a single
   ring survived it, but a set of three laid out across the frame lost its
   outer pieces, which is the one thing a card for a set has to show.

   contain, at 84% of the box, centred: the whole photograph, its own
   proportions, and an even 8% of breathing room on every side. The card's
   dimensions, corners, cart mark, title, rating and price are all untouched -
   only what happens inside the media box changes.

   BOTH PHOTOGRAPHS. The resting image and the hover image get the same
   treatment; a hover that re-crops the product would undo the fix the moment
   a pointer arrives.

   SPECIFICITY, COUNTED NOT GUESSED. The collection card's own rule is
   `.ccard--plain.ccard.ccard .ccard__media .ccard__img.ccard__img` - six
   classes - and calmy-cardsurface.css styles `.ccard__media img` from a file
   that may load after this one. Four .ccard repeats plus two .ccard__media
   clears both without !important, and the bare-img selector carries an
   element on top so it wins by type as well as by count.

   SCOPED TO .ccard. The product page's own gallery is .cpgal__* and is not
   touched by anything here - it keeps cover, and it should: there the frame
   is the composition.
   ═══════════════════════════════════════════════════════════════════════════ */
.ccard.ccard.ccard.ccard .ccard__media.ccard__media img,
.ccard.ccard.ccard.ccard .ccard__media.ccard__media .ccard__img.ccard__img,
.ccard.ccard.ccard.ccard .ccard__media.ccard__media .ccard__hover.ccard__hover {
  object-fit: contain;
  object-position: center;
  /* inset + margin:auto centres it inside the box it no longer fills. */
  position: absolute;
  inset: 0;
  margin: auto;
  /* THE FULL MEDIA AREA, no inset of ours at all.

     84% then 92% both left a margin of card between the frame's edge and the
     photograph - and because these photographs carry their own pale studio
     ground, that margin read as a second box: a grey rectangle pasted inside a
     cream one. 100% removes the inner margin entirely; the photograph reaches
     the frame's edge and the 14px radius clips it, so there is one shape
     instead of two.

     STILL contain, NOT cover. The frame is 4:5 and most of these photographs
     are square, so covering would crop a tenth off each side - and a set of
     rings laid out across the frame loses its outer pieces first. Whatever
     difference is left between the frame and the photograph is the file's own
     background, not a margin this stylesheet adds. */
  inline-size: 100%;
  block-size: 100%;
  width: 100%;
  height: 100%;
  max-inline-size: 100%;
  max-block-size: 100%;
  /* Nothing scales the photograph any more: a zoom on a contained image is a
     crop with extra steps. */
  transform: none;
}
