/* ══════════════════════════════════════════════════════════════════════════
   final/css/catalog.css — LANE 22, THE CATALOGUE GRID AND THE PRODUCT TILE.

   Owner, 2026-08-26, three complaints, all about product tiles:
     1. "product pages for mobile need to be two products wide not one"
     2. "i want the add to cart button on product tiles to have something to
         make them visible or stand out because they are white on a white
         backround."
     3. a tile composition: plate · category · name · price · action button.

   THIS SHEET LOADS LAST (final/lib.mjs linkCss), after theme.css, sf-base.css,
   sf-refresh.css AND tailwind.min.css. Nothing here needs !important; where a
   rule looks over-specified it is because it has to beat a specific opponent,
   and the opponent is named in the comment above it.

   ── THE GROUND, MEASURED OFF THE BUILT CSS ────────────────────────────────
   The catalogue is a WHITE PLATE, not the dark ground the brief describes.
   sf-refresh.css section H ("WHITE PLATES", 2026-08-20) re-pins the whole
   token set on `body #products-grid` — the section that wraps the grid — so
   inside it:

       --sf-bg      -> --cc-plate        #FFFFFF
       --sf-surface -> --cc-plate-soft   #F2F7FB   (the section ground)
       --sf-elevated-> --cc-plate        #FFFFFF   (the card)
       --sf-text    -> --cc-plate-ink    #04101F
       --sf-muted   -> --cc-plate-muted  #4A6076
       --sf-accent  -> --cc-plate-accent #0A5464

   EVERY COLOUR BELOW IS A var(), NEVER A LITERAL — the plate palette is solved
   in sf-refresh.css and a baked hex here would go stale the moment it is
   re-solved. The literals that appear are FALLBACKS inside var(), and they are
   the same values that file solves for today.
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   1. THE GRID IS TWO ACROSS ON A PHONE
   ══════════════════════════════════════════════════════════════════════════
   WHY IT WAS ONE. products.html ships the grid with tailwind's
   `grid-cols-2 lg:grid-cols-4`, and the page's own #product-grid-mobile-css
   block sizes every tile for a 2-up phone — but neither of them ever painted a
   column. theme.css carries

       body.layout-hero-product-forward .sf-product-grid {
         grid-template-columns: repeat(auto-fill, minmax(var(--sf-grid-min, 230px), 1fr));
       }

   at (0,2,1), which outranks a bare `.grid-cols-2` (0,1,0) whatever the source
   order. The 230px is only the FALLBACK — theme.css defines `--sf-grid-min:
   220px` for this client, so 220px is the track floor that actually applied.
   MEASURED: at 375px the grid box is 343px and two 220px tracks need 440px
   before the gap, so auto-fill returned ONE column — and it stayed one column
   all the way to ~505px. Every phone. The tile CSS that assumed 2-up (including
   final/css/mobile.css's "MEASURED at 375: tile 165px wide") was describing a
   layout that never rendered.

   THE FIX IS AN EXPLICIT COLUMN COUNT, not a smaller minmax(): auto-fill on a
   floor of N px is exactly the mechanism that dropped to one column, and it
   will do it again the next time a padding or a gap moves. `minmax(0, 1fr)`
   is what keeps a long compound name from pushing the track wider than its
   share — a bare `1fr` floors at min-content and overflows the page. */
#product-grid.sf-product-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-flow: row;
  gap: clamp(10px, 2.6vw, 22px);
  align-items: stretch;
}

/* 3-up from a large phone in landscape / small tablet, 4-up on desktop.
   COMPUTED tile widths, from the real section padding and the gap clamp:
     320 -> 143px · 375 -> 170px · 414 -> 190px · 480 -> 218px ·
     600 -> 178px (3-up) · 768 -> 227px (3-up) · 1280 -> 290px (4-up).
   The narrowest tile any phone sees is 143px at 320px, and §3e below shows the
   worst-case size-pill row measuring 118px inside its 123px card body there. */
@media (min-width: 600px) {
  #product-grid.sf-product-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1000px) {
  #product-grid.sf-product-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* THE 2x2 FEATURE TILE COMES OUT, AT EVERY WIDTH.
   theme.css features `.sf-product-grid > *:first-child` at `span 2 / span 2`
   for this layout. Two reasons it cannot stay: in a 2-column grid it eats the
   whole first row and the "two products wide" the owner asked for is gone on
   the first row of the catalogue; and the tiles are RE-ORDERED at runtime by
   the live-stock pass (low stock > featured > in stock > coming soon > out),
   so "first child" is whatever sorted first this page load — a feature slot
   that changes with the stock table is not a composition, it is an accident.
   (0,3,1) opponent, (1,1,0) here, so the id carries it.) */
#product-grid.sf-product-grid > * {
  grid-column: auto;
  grid-row: auto;
  min-width: 0;
}

/* A phone spends 32px of a 375px screen on section padding. 12px each side
   buys ~13px per tile at 2-up, which is a whole size pill. Opponent:
   `body.layout-hero-product-forward .sf-section` (0,2,1). */
@media (max-width: 479px) {
  #products-grid.sf-section { padding-left: 12px; padding-right: 12px; }
}


/* ══════════════════════════════════════════════════════════════════════════
   2. THE ADD-TO-CART BUTTON — WHITE ON WHITE, AND THE ARITHMETIC THAT FIXES IT
   ══════════════════════════════════════════════════════════════════════════
   WHY IT WAS INVISIBLE, exactly. sf-refresh.css section D gives the tile's one
   control

       background: var(--sf-bg);  color: var(--sf-text);
       border: 1px solid var(--sf-bg);

   which was correct when it was written: on the dark ground --sf-bg was the
   near-black navy #060E1A. Then section H re-pinned --sf-bg to --cc-plate on
   `body #products-grid`, and the button became #FFFFFF fill + #FFFFFF border
   on a #FFFFFF card. The LABEL still measured 19.03:1 and every contrast gate
   passed — what disappeared was the BUTTON, which no ink-vs-ground probe looks
   for. That is precisely what the owner reported.

   THE FILL IS SOLVED, NOT PICKED, and it is the darkest value the plate has:
   --cc-plate-ink. Two ratios matter and both are computed, not eyeballed
   (WCAG 2.x relative luminance, sRGB):

       L(#04101F) = 0.004953      L(#FFFFFF) = 1.0      L(#F2F7FB) = 0.922865

       label  #FFFFFF on #04101F  = 1.05 / 0.054953  = 19.11:1   (need 4.5:1)
       button #04101F vs the card #FFFFFF            = 19.11:1
       button #04101F vs the plate #F2F7FB           = 17.70:1

   So the label clears AA by 4.2x and the button itself is the highest-contrast
   object on the tile — it cannot read as "not a button".

   NOT THE ACCENT, DELIBERATELY. sf-refresh's own note on this control says the
   tile CTA is "one navy across the grid, never the lighter var(--sf-accent) —
   that blue belongs to the price figure". That decision is kept; only the
   token it reads is changed, from one that follows the plate to one that does
   not. Same technique, same file, as the .coa-cert island.

   Opponent: `#product-grid .tile-actions .tile-btn-details` (1,2,0). These are
   (1,2,0) too and this sheet loads later, so they win on order with no
   !important. */
#product-grid .tile-actions .tile-btn,
#product-grid .tile-actions .tile-btn-details {
  background: var(--cc-plate-ink, #04101F);
  color: var(--cc-plate, #FFFFFF);
  border: 1px solid var(--cc-plate-ink, #04101F);
  box-shadow: 0 1px 2px rgb(4 16 31 / 0.18);
  font-weight: 800;
}

/* Full width of the action row, bottom of the card — the reference's shape.
   sf-refresh already sets flex:1 1 100% here; restated so the composition does
   not depend on a rule in a file this lane does not own. */
#product-grid .tile-actions .tile-btn-details {
  flex: 1 1 100%;
  width: 100%;
}

/* 44px AT EVERY WIDTH, not just on a phone. final/css/mobile.css already
   forces 44px with !important below 860px; this carries the same target up to
   the desktop, where the button was 36px. */
#product-grid .tile-actions .tile-btn {
  min-height: 44px;
  padding: 10px 12px;
  line-height: 1.1;
}

/* THE SMALLEST PHONE STILL HOLDS THE LONGEST LABEL. The script writes one of
   two labels into this control and never a third: "Add to cart" when the SKU
   is buyable, "View details" when it is not. At 320px the card body is 123px;
   "VIEW DETAILS" at 12px/700/0.04em measures ~95px and the 12px gutters take
   it to 119px — inside, but with 4px to spare, and 12px is a floor
   (final/css/mobile.css sets that font-size !important below 860px). Trading
   6px of gutter and half the tracking buys ~19px of headroom, so neither label
   can wrap to a second line on any phone that exists. */
@media (max-width: 374px) {
  #product-grid .tile-actions .tile-btn { padding: 10px 6px; letter-spacing: 0.02em; }
}

/* HOVER IS THE BRAND ACCENT, and it is the plate's own solved accent rather
   than a second navy: white on #0A5464 measures 1.05/0.123246 = 8.52:1, which
   is sf-refresh's own published figure for this value. A real state change
   (navy -> teal) instead of navy-on-navy, which that file already called "too
   quiet to read". Opponent (1,3,0); this is (1,3,0), later. */
#product-grid .tile-actions .tile-btn:hover,
#product-grid .tile-actions .tile-btn-details:hover {
  background: var(--cc-plate-accent, #0A5464);
  border-color: var(--cc-plate-accent, #0A5464);
  color: var(--cc-plate, #FFFFFF);
  box-shadow: 0 8px 18px -10px rgb(4 16 31 / 0.65);
}

/* THE "ADDED" FLASH KEEPS ITS EMERALD AND LOSES THE WHITE INK, because white
   on it FAILS: L(#059669) = 0.228617, so #FFFFFF on it is 1.05/0.278617 =
   3.77:1. The plate ink on the same green is 0.278617/0.054953 = 5.07:1 and
   PASSES. Only the colour is restated — the emerald is sf-refresh's and stays
   sf-refresh's, so the "added" green is still one green across the site.
   The .is-limit state needs nothing: its ground is --sf-muted (#4A6076 on the
   plate) and the white ink above measures 1.05/0.161275 = 6.51:1. */
#product-grid .tile-actions .tile-btn.is-added,
#product-grid .tile-actions .tile-btn-details.is-added {
  color: var(--cc-plate-ink, #04101F);
}

/* Nothing here is hover-only: the control is rendered on every tile on every
   load, and wireTileActions only ever swaps its LABEL between "Add to cart"
   and "View details". The active nudge is dropped on touch by the page's own
   `@media (hover:none)` rule. */
#product-grid .tile-actions .tile-btn:focus-visible {
  outline: 2px solid var(--cc-plate-accent, #0A5464);
  outline-offset: 2px;
}


/* ══════════════════════════════════════════════════════════════════════════
   3. TILE COMPOSITION — plate · category · name · price · sizes · button
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 3a. THE PHOTO SITS ON A SOFT PLATE AGAIN ─────────────────────────────
   Section H turned the card white, and --sf-photo-bg was already white, so
   the media box lost its edge and sf-refresh put a hairline back "because the
   vial has to sit ON something". A tinted plate is the better answer and the
   one the owner's reference shows. VERIFIED FIRST: every product frame is a
   genuine cut-out (sharp: 600x600 RGBA, corner alpha 0 on every sample), so a
   tint shows through instead of framing a white rectangle.

   The tint is SOLVED off the brand: 7% of the plate accent into the plate, a
   1.12:1 step below the card — one notch, the same distance repo doctrine
   gives --sf-band. --sf-surface is re-pinned on the same element because the
   RUO overlay's scrim gradient and the loading skeleton are both built from
   it; leave it white and the scrim floats over the tint.
   Opponent: `body #product-grid .sf-card-media` (1,1,1) in sf-refresh, which
   pins --sf-surface:#FFFFFF. Matched at (1,1,1), later file. */
body #product-grid .sf-card-media {
  --sf-surface: color-mix(in srgb, var(--cc-plate-accent, #0A5464) 7%, var(--cc-plate, #FFFFFF));
  background: color-mix(in srgb, var(--cc-plate-accent, #0A5464) 7%, var(--cc-plate, #FFFFFF));
}

/* ── 3b. THE CATEGORY LINE ────────────────────────────────────────────────
   Written into the markup by 22-catalog.mjs from the tile's own
   data-category, and rendered through the ENGINE'S OWN tile idiom: sf-base.css
   already carries `body .sf-card-meta` ("Category/meta line on tiles: visible
   by default") at 0.66rem / 700 / uppercase / 0.1em. This is that line, not a
   section eyebrow — it precedes no <h1>-<h3>, wears no class named for the
   idiom, and carries information the product name does not.

   TRACKING IS 0.1em ON PURPOSE. section-kickers.mjs treats 0.12em as the floor
   where uppercase becomes "tracked out"; every functional label in the engine
   sits at 0.06-0.10em and so does this one.

   Ink: --sf-muted -> #4A6076 on the #FFFFFF card = 6.51:1. */
#product-grid .cc-tile-cat {
  display: block;
  font-family: inherit;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--cc-plate-muted, #4A6076);
  margin: 0 0 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 769px, NOT the 600px the columns step at, and every type/padding step below
   uses the same number. The page's own #product-grid-mobile-css block ends at
   768px, which is where .sf-card-body goes from 10px of gutter to 18/20px — so
   769 is where a tile actually gets roomier, and a step at 600 would put 20px
   of action-row gutter under a 10px card body for a whole breakpoint. The
   3-up tile at 600-768px measures ~178px, which is a 375px 2-up tile: it wants
   the compact treatment, not the roomy one. */
@media (min-width: 769px) {
  #product-grid .cc-tile-cat { font-size: 11px; margin-bottom: 6px; }
}

/* ── 3c. THE NAME SURVIVES A 165px TILE ───────────────────────────────────
   Longest name in this catalogue is "Bacteriostatic Water" (20 chars), which
   wraps to two lines at 2-up. Clamped at two so a long name can never push the
   price and the button out of alignment across a row, and `anywhere` so a
   single unbroken token (a future "Semaglutide/Tirzepatide" style name) breaks
   instead of widening the track. The inline style attribute owns weight,
   colour, tracking and margin — none of which is touched here. */
#product-grid .tile-name {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.25;
  overflow-wrap: anywhere;
  max-width: 100%;
}

/* ── 3d. NOTHING OVERFLOWS THE TILE ───────────────────────────────────────
   A flex/grid child's default min-width is auto (= min-content), which is how
   a 4-pill size row silently grew wider than a 150px card body and was CLIPPED
   by the article's overflow:hidden. Every box in the chain is floored at 0 and
   the pill row is allowed to wrap. */
#product-grid > article,
#product-grid .sf-card-body,
#product-grid .tile-meta-row,
#product-grid .tile-price-row { min-width: 0; max-width: 100%; }

#product-grid .tile-variant-pills {
  flex-wrap: wrap;
  max-width: 100%;
  min-width: 0;
  row-gap: 4px;
}

/* ── 3e. THE SIZE PILLS ARE THUMB-SIZED AND READ AS A CHOICE ──────────────
   They were 19px tall and the active one differed from the inactive ones only
   by a border, on a white card, at 11px. The active pill now takes the same
   navy the action button carries (one "chosen" colour on the tile) and the
   quiet ones sit as outlines.

       active   #FFFFFF on #04101F  19.11:1
       inactive #4A6076 on #FFFFFF   6.51:1

   WORST-CASE FIT, computed at the NARROWEST phone (320px): the card body is
   123px wide; the widest tile in this catalogue carries four sizes, and four
   10px pills ("5mg" 25px, "10mg"/"20mg"/"30mg" 27px each) plus three 4px gaps
   measure 118px. It fits on one row there and has 27px of slack at 375px — and
   flex-wrap above catches any future SKU that does not.

   30px, NOT 44px, and that is a deliberate deviation stated out loud: four
   44px pills would wrap to two rows and add ~60px to every tile in a 45-tile
   catalogue. The pill row sits inside a container that stops click propagation,
   so a miss costs a re-tap and never a wrong navigation. */
#product-grid .tile-size-pill {
  min-height: 30px;
  padding: 0 8px;
  font-size: 10px;
  border-radius: var(--cc-radius, 8px);
  background: var(--cc-plate, #FFFFFF);
  color: var(--cc-plate-muted, #4A6076);
  border: 1px solid color-mix(in srgb, var(--cc-plate-muted, #4A6076) 42%, transparent);
}
#product-grid .tile-size-pill:hover {
  border-color: var(--cc-plate-accent, #0A5464);
  color: var(--cc-plate-accent, #0A5464);
}
#product-grid .tile-size-pill.active,
#product-grid .tile-size-pill.active:hover {
  background: var(--cc-plate-ink, #04101F);
  border-color: var(--cc-plate-ink, #04101F);
  color: var(--cc-plate, #FFFFFF);
}
@media (min-width: 769px) {
  #product-grid .tile-size-pill { min-height: 32px; padding: 0 10px; font-size: 11px; }
}

/* The price is the accent and stays the accent — #0A5464 on #FFFFFF = 8.52:1.
   Only its footprint is set, so a 2-up tile does not put the figure on the
   same optical weight as the name. */
#product-grid .tile-price-row { line-height: 1.2; }
#product-grid .sf-price { font-size: 15px; }
@media (min-width: 769px) {
  #product-grid .sf-price { font-size: 17px; }
}

/* The action row takes the CARD BODY'S OWN GUTTER at both widths, so the button
   sits on the same left edge as the name above it. That is why this steps at
   769px: .sf-card-body is `10px 10px 0 !important` up to 768 and the tile's
   inline `padding:18px 20px 0` above it. */
#product-grid .tile-actions { padding: 0 10px 12px; margin-top: 12px; }
@media (min-width: 769px) {
  #product-grid .tile-actions { padding: 0 20px 20px; }
}


/* ══════════════════════════════════════════════════════════════════════════
   4. THE HOME PAGE'S FEATURED STRIP IS NOT THIS LANE'S, AND THAT IS A FINDING
   ══════════════════════════════════════════════════════════════════════════
   It has the SAME bug: index.html declares `.hero-products` as
   `grid-template-columns: 1fr 1fr` at (0,1,0), the same theme.css layout rule
   (0,2,1) beat it, and the strip was one tile per row on every phone with the
   first of four featured 2x2.

   A block that fixed it from here was WRITTEN AND THEN REMOVED, because the
   home lane (final/css/homepage.css) had already rebuilt that element as a
   snap-scrolling flex rail with its own --cc-fp-cols / --cc-fp-gap and 44px
   arrows. This sheet links after theirs, and
   `#featured-strip .hero-products.sf-product-grid` (1,2,0) outranks their
   `body #featured-strip .hero-products` (1,1,1) — so the "fix" would have
   silently overridden their gap at every width. Two lanes solving one element
   is how a green run ships a broken rail.

   Nothing in this sheet touches #featured-strip. Its tile class names do not
   collide either: the catalogue's category line is .cc-tile-cat, theirs is
   .cc-fp-cat, and both are scoped to their own container.
   ══════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   5. REDUCED MOTION
   The page already drops .tile-btn transitions under the query; the two
   transitions introduced here (the pill and the button's shadow) go with them.
   Nothing in this sheet animates on its own, so there is no reveal that could
   end invisible.
   ══════════════════════════════════════════════════════════════════════════ */
#product-grid .tile-size-pill {
  transition: background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease;
}
@media (prefers-reduced-motion: reduce) {
  #product-grid .tile-size-pill,
  #product-grid .tile-actions .tile-btn { transition: none; }
}
