/* ==========================================================================
   Product card — Phase 3A
   One definition, both surfaces. The home grid and the archive grid used to
   draw the same plant two different ways: a full-width "للسلة" button here, a
   quiet ＋ mark there; a one-line ellipsised name here, a wrapping one there;
   grayscale when sold here, desaturated there. This file is now the only
   place the card is described, and index.html's grid renders the same
   pwBuildCard() markup the catalog renders.

   Loaded by index.html, which means catalog.html and every /plants/ page
   inherit it through the build scripts. catalog.css keeps what is genuinely
   catalogue-only: the masthead, the rail, the strip, the list view and the
   skeletons.
   ========================================================================== */

/* ---------- the card system's hook: .pw-cards ----------
   Put `.pw-cards` on any container whose children are pwBuildCard() cards, and
   the card system below applies to it. Four containers carry it today:

     index.html      #grid                    home grid
     cat-main.html   #grid.cat-grid           catalogue grid (and its list view)
     plant-main.html #pdRelatedFamilyGrid     product page — related, family
     plant-main.html #pdRelatedPairedGrid     product page — related, paired

   Why this exists. The card rules were written `#grid:not(.is-list) …`, an ID
   selector — and the two related grids on the product page are NOT `#grid`,
   they are `#pdRelatedFamilyGrid` and `#pdRelatedPairedGrid`. So the same
   component rendered two different ways: grid-composed with an edge-to-edge
   plate on home and the catalogue, block-stacked with an 11px inset on the
   product page. Nobody chose that; it fell out of the selector.

   The hook is a class, so it reaches any container that opts in. Specificity
   drops from (1,2,0) to (0,3,0), which is still above every rule that competes
   for these properties — the loose `.cat-caption` defaults above (0,1,0) and
   the pre-migration `.card{padding:11px}` in index.html's <style> (0,1,0).
   Nothing targets the caption's internals at ID weight, which is what makes
   the migration safe. Verified: identical computed values on home and the
   catalogue at 390 / 834 / 1440, both directions.

   NOT migrated, deliberately — the grid-CONTAINER rules below, and the list
   view and plate-spread rules in catalog.css, keep `#grid`. They fight
   identity-v2's own `#grid` rules for gap and columns, and an ID always beats
   a class no matter how many you stack; demoting them to `.pw-cards` would
   hand ≤760px back to identity-v2's `gap: 28px 12px`. They describe the grid,
   not the card, so they are a separate concern and stay until identity-v2's
   `#grid` block is migrated with them. */

/* ---------- grid rhythm ----------
   Both grids are #grid; only the class differs. They carried different row
   gaps (home 28px on a phone, archive 40px) so the same plants breathed
   differently depending on the page. One rhythm now. Column counts are left
   alone — the home grid's four-up on a wide desktop is its own editorial
   choice, not an inconsistency.
   #grid.<class> is ID+class, which is what it takes to outrank identity-v2's
   own #grid rules. */
/* D1 gives the home grid a 34px column gutter against D2's 18 — `four columns
   at the 34/54 gutter ratio`, in the sheet's own words. The home card is the
   editorial one and is given room to breathe; the catalogue is an index and
   packs tighter. Both keep 54 between rows.

   Split into two rules for exactly that reason. The related grids on the
   product page are not `#grid`, so they take identity-v2's `.grid` gap of
   54/18 — which is what D3 draws for them, and why the 34 must not be written
   on `.grid` itself. */
#grid.grid { gap: 54px 34px; }
#grid.cat-grid { gap: 54px 18px; }

@media (max-width: 1040px) {
  #grid.grid, #grid.cat-grid { gap: 44px 14px; }
}
@media (max-width: 760px) {
  /* D2. The sheet's phone frames set `gap:34px 14px` on the two-up product
     grid; the desktop sheet sets 54/18, which is what the wider ranges already
     carry. This was 40/16 — a B6 correction that widened the column gap from
     12px because rows floated 40px apart. Claude Design settles both numbers
     at once, and 34/14 keeps the same vertical-over-horizontal rhythm at a
     2.4:1 ratio. */
  #grid.grid, #grid.cat-grid { gap: 34px 14px; }
}

/* card entrance — archive language, once, subtle */
.cat-card {
  position: relative;
  animation: cat-enter 500ms cubic-bezier(.22, .72, .18, 1) both;
  animation-delay: var(--cd, 0ms);
}
@keyframes cat-enter {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.cat-link {
  display: block;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.cat-link:focus-visible { outline: 2px solid var(--archive-clay); outline-offset: 3px; }

.cat-card .card-media { display: block; }

/* ---------- the rarity seal, per surface ----------
   Drawn four times, and it grows with the card it sits on:

     01 Home / 02 Catalogue, phone .... 32 at 9,  Mono 12/600
     D2 Desktop catalogue ............. 32 at 10, Mono 12/600
     D1 Desktop home .................. 36 at 11, Mono 13/600

   The phone values were already right; the two desktop surfaces were both
   still drawing the phone's 32-at-9. D3's related card carries no seal at all
   in the sheet — ours keeps it, because it is the card's rarity legend and
   dropping it would remove a fact, not a decoration. It follows D2's numbers,
   which is the sheet its caption already follows. */
@media (min-width: 761px) {
  .pw-cards.cat-grid:not(.is-list) .badge-seal {
    top: 10px;
    inset-inline-start: 10px;
  }
  .pw-cards.grid:not(.cat-grid):not(.is-list) .badge-seal {
    width: 36px;
    height: 36px;
    top: 11px;
    inset-inline-start: 11px;
    font-size: 13px;
  }
}

/* Sold out reads the same on both grids now. The home grid used
   `grayscale(1)` at 45% opacity, which drained the specimen of the colour
   that identifies it; the archive's lighter desaturation says "not available"
   without pretending the plant is a different plant. */
.cat-card.is-sold .card-img {
  opacity: 0.6;
  filter: saturate(0.4);
}

/* ---------- caption = mounted herbarium label ----------
   Phase 3B. The caption used to stack five blocks with gaps of 5, 0, 7 and 5px
   — no scale, and the name sat directly on its binomial with nothing between
   them. Everything is now a multiple of 4: 4 inside a group, 8 between groups,
   12 to the rule above. */
.cat-caption {
  --cap-step: 4px;
  display: block;
  padding: calc(var(--cap-step) * 3) 0 0;  /* 12 */
  border-top: 1px solid var(--archive-line-soft);
  margin-top: calc(var(--cap-step) * 3);   /* 12 */
}

/* D2(a). Three sheets draw this plate identically — 01 Home, 02 Catalogue and
   D2 Desktop catalogue — and all three run the photograph edge to edge inside
   the card's hairline, with the caption's rule spanning the full width and the
   padding applied to the caption alone: `padding:11px 12px` on the phone,
   `13px 15px` in the desktop grid.

   What we had instead was an 11px inset on the card itself, which came from
   the pre-migration `.card` rule still living in index.html's <style> — never
   a reading of any sheet. It floated the image off the border and pulled the
   caption rule 11px in from both ends, so the one hairline that is supposed to
   run the width of the plate stopped short on every card on both grids.

   Scoped to the grid views: the list view is a different composition (a 120px
   media column in a subgrid row) and keeps the caption it has. */
.pw-cards:not(.is-list) .cat-card { padding: 0; }
.pw-cards:not(.is-list) .cat-caption {
  padding: 11px 12px;
  margin-top: 0;
}

/* The caption is padded to its surface, like everything else on this card:
   D1's home card at `16px 18px`, D2's and D3's at `13px 15px`, and both phone
   sheets at `11px 12px`.

   The boundary moves from 1041 to 761 here, which is a correction rather than
   a new decision: the name, binomial, code and price all switch to the desktop
   scale at 761, so leaving the padding until 1041 left the 761–1040 band with
   a 21px name inside an 11px caption. One card, one boundary, and it sits
   where the sheets' own phone/desktop split sits. */
@media (min-width: 761px) {
  .pw-cards.grid:not(.cat-grid):not(.is-list) .cat-caption { padding: 16px 18px; }
  .pw-cards.cat-grid:not(.is-list) .cat-caption { padding: 13px 15px; }
}

/* ---------- B5: one metadata row ----------
   `SP-001 · شائعة .......... 80 د.ك` on a single line, without touching the
   card builder. The accession code and the tier/price row are siblings, so a
   grid places them on the same track: code in the first column, the existing
   .cat-meta row in the second, still pushing the price to the far end.

   .cat-meta is left intact as a real flex box — dissolving it with
   display:contents was the first attempt and it was wrong twice over: the
   caption is a column, so flex-wrap wraps into columns rather than rows, and
   the list view styles .cat-meta as a box whose max-width would have vanished.

   Scoped to the grid views; the list view keeps the stacked caption it has. */
.pw-cards:not(.is-list) .cat-caption {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas:
    "name  name"
    "latin latin"
    "code  meta"
    "avail avail"
    "chip  chip";
  align-items: baseline;
  column-gap: calc(var(--cap-step) / 2);  /* 2 — the code sits against its row */
  row-gap: calc(var(--cap-step) * 2);     /* 8 */
}
.pw-cards:not(.is-list) .cat-caption > .name-ar      { grid-area: name; }
.pw-cards:not(.is-list) .cat-caption > .name-en      { grid-area: latin; }
.pw-cards:not(.is-list) .cat-caption > .cat-sp       { grid-area: code; }
.pw-cards:not(.is-list) .cat-caption > .cat-meta     { grid-area: meta; }
.pw-cards:not(.is-list) .cat-caption > .cat-avail    { grid-area: avail; }
.pw-cards:not(.is-list) .cat-caption > .fulfill-chip { grid-area: chip; }

/* A name and its binomial are one thought, so they sit at 4 from each other
   against the 8 that separates the groups. */
.pw-cards:not(.is-list) .cat-caption > .name-en { margin-top: calc(var(--cap-step) * -1); }

/* The grid owns the rhythm now, so the row's own 7px top margin — one of the
   four loose values this phase replaced — has to stand down. */
.pw-cards:not(.is-list) .cat-meta { margin-top: 0; }

/* The separator belongs to the pairing, not to either label. */
.pw-cards:not(.is-list) .cat-tier::before {
  content: '·';
  margin-inline-end: calc(var(--cap-step) / 2);
  opacity: 0.55;
}

/* ---------- keeping the price on the line ----------
   The accession code takes the first column, so the tier and the price share
   what is left — about 118px on a 164px card. `غير شائعة` plus `80 د.ك` wanted
   roughly 122, and the row wrapped on all thirteen cards.

   The few pixels come back from the gaps, and the two sides are given
   different orders of priority: the price never shrinks and never breaks, and
   the tier is allowed to wrap inside itself first. So the line yields in the
   order it should — spacing, then the tier, and only then the price. */
.pw-cards:not(.is-list) .cat-meta { column-gap: var(--cap-step); }

/* The code is six mono glyphs holding 38px of a 142px caption, and a tenth of
   an em of that is tracking. Tracking is what makes it read as a specimen
   stamp, so it keeps some — but at 0.1em it was buying letterform at the
   price's expense on exactly the widths where the line is tightest.

   That trade was made when the price was 14px at 600. It is now 12–12.5 at
   400, which gives the line back more than the tracking ever cost it, so the
   sheets' own `.1em` is affordable again: 02 and D2 both write the code at
   `letter-spacing:.1em`, and D1 and the plate spread at the same tracking one
   step up in size. Re-measured for wrapping at 390 before restoring it. */
.pw-cards:not(.is-list) .cat-sp { font-size: 9px; letter-spacing: 0.1em; }

@media (min-width: 761px) {
  .pw-cards.grid:not(.cat-grid):not(.is-list) .cat-sp { font-size: 9.5px; }
}
.pw-cards:not(.is-list) .plate-spread .cat-sp { font-size: 9.5px; }

/* Same trade on the tier: 0.05em of tracking across nine glyphs of
   "غير شائعة" is three more pixels the price could be using. The label still
   reads as a mono stamp at 0.02em. */
.pw-cards:not(.is-list) .cat-tier { letter-spacing: 0.02em; }

/* Availability used to be the caption's last row, and it was what kept the ＋
   clear of the price. With it gone for in-stock cards, the mark landed on top
   of the number on every tablet and desktop card. The row now ends short of
   the mark's column instead — which costs no height at all, and gives the
   wide layouts the same `price ＋` pairing the phone layout already had. */
.pw-cards:not(.is-list) .cat-meta {
  padding-inline-end: 48px;
  /* With the rarity gone the price is alone in this row, and space-between
     parks a lone item at the start — which would slide the number back beside
     the accession code. It belongs at the far end, opposite the code, with the
     add mark past it. */
  justify-content: flex-end;
}

/* The price is the one thing on this line that never gives way. Everything
   else may shrink or break around it.

   nowrap was tried here and rejected: it did keep the price inline on every
   card, but it bought that by clipping "شائعة" and "اقتناء مميز" on the sale
   cards, where the two-number price block leaves the tier about 2px. Losing
   the end of a word is worse than moving a number. */
.pw-cards:not(.is-list) .cat-price { flex-shrink: 0; }

/* The accession code is decoration — the quietest thing on the card, and the
   only text still allowed under the 11pt floor because nobody needs to read it.

   Phase 3D: it was painted in ink-soft while the rarity beside it sat in the
   muted neutral, so the 9px label carried more colour weight than the 11.2px
   one next to it — the hierarchy was upside down. It now takes the muted tone
   too, and with the rarity gone it is the whole eyebrow: an accession number
   on a specimen sheet, which is exactly as loud as that should be. */
.cat-sp {
  display: block;
  color: var(--archive-muted, #6a6760);
  font-family: 'IBM Plex Mono', monospace;
  font-size: 0.56rem;
  letter-spacing: 0.1em;
  opacity: 0.85;
}

/* Two lines, always — clamped so a long name cannot push its neighbour out of
   line, and reserved so a short one still occupies the same height. Before
   this the home grid truncated to one line with an ellipsis and the archive
   wrapped freely, so a row of cards ended up ragged by up to 54px. */
.cat-caption .name-ar {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: calc(2 * 1.25em);
  color: var(--archive-ink);
  font-family: var(--archive-display-ar);
  font-size: 1.2rem;
  /* D3. The sheet writes the card name explicitly — Screen 01:
     font-family:'El Messiri';font-weight:500;font-size:16px;line-height:1.25.
     The leading already matched; 600 was what the font request could serve. */
  font-weight: 500;
  line-height: 1.25;
}
html[dir='ltr'] .cat-caption .name-ar { font-family: var(--archive-display-en); font-size: 1.3rem; }
@media (hover: hover) {
  .cat-card:hover .name-ar { color: var(--archive-clay-deep); }
}

/* ---------- the name scale, per surface and per script ----------
   Like the price, the name is not one size in Claude Design. It is drawn five
   times and agrees with itself only within a surface:

     01 Home, Arabic phone ......... El Messiri 16 / 500 / lh 1.25
     02 Catalogue, Latin phone ..... Cormorant  17 / 500 / lh 1.2
     02 Catalogue, plate spread .... Cormorant  22 / 500 / lh 1.15
     D1 Desktop home ............... Cormorant  21 / 500 / lh 1.2
     D2 Desktop catalogue .......... Cormorant  18 / 500 / lh 1.2
     D3 Desktop product, related ... Cormorant  18 / 500

   Two readings worth stating.

   The phone pair inverts the desktop pair — home 16 against catalogue 17, but
   home 21 against catalogue 18 — and that is a script difference, not a
   contradiction. 01 is drawn in Arabic and 02 in Latin, and Cormorant sets far
   smaller on the body than El Messiri, so 17 Cormorant and 16 El Messiri are
   the same optical size. The existing code already carried that relationship
   (1.2rem Arabic against 1.3rem Latin); the sheets just put real numbers on it.

   The leading is script-specific for the same reason and the sheets say so
   outright: 1.25 for El Messiri, 1.2 for Cormorant. The 1.25 already here is
   correct for Arabic and stays.

   Arabic has no desktop sheet — D1, D2 and D3 are all drawn in English — so
   the desktop sizes are applied to both scripts, following the ruling already
   set on D1's hero: a size carries no script, only a weight can. */
.pw-cards:not(.is-list) .cat-caption .name-ar {
  font-size: 16px;
  min-height: calc(2 * 1.25em);
}
html[dir='ltr'] .pw-cards:not(.is-list) .cat-caption .name-ar {
  font-size: 17px;
  line-height: 1.2;
  min-height: calc(2 * 1.2em);
}

@media (min-width: 761px) {
  .pw-cards.grid:not(.cat-grid):not(.is-list) .cat-caption .name-ar,
  html[dir='ltr'] .pw-cards.grid:not(.cat-grid):not(.is-list) .cat-caption .name-ar { font-size: 21px; }

  .pw-cards.cat-grid:not(.is-list) .cat-caption .name-ar,
  html[dir='ltr'] .pw-cards.cat-grid:not(.is-list) .cat-caption .name-ar { font-size: 18px; }
}

/* The spread is the one card 02 draws larger, and on a tighter leading. */
.pw-cards:not(.is-list) .plate-spread .cat-caption .name-ar,
html[dir='ltr'] .pw-cards:not(.is-list) .plate-spread .cat-caption .name-ar {
  font-size: 22px;
  line-height: 1.15;
  min-height: calc(2 * 1.15em);
}


/* B4, level two: the botanical identity. It reads as the name's binomial —
   italic is what a herbarium label uses for a species, and it separates this
   line from the availability line that used to share its exact size and
   weight. Secondary to the Arabic name, but unmistakably identity, not status. */
.cat-caption .name-en {
  display: block;
  min-height: 1em;
  color: var(--archive-ink-soft);
  font-family: var(--archive-display-en);
  font-size: 0.76rem;
  font-style: italic;
  line-height: 1.35;
}

/* D1 is the only sheet that draws the binomial on a card, and it draws it
   exactly as this rule already does — Cormorant, italic, `#46544c` — at 14px.
   So the treatment was right and only the size was short.

   The line is KEPT on the catalogue and the related grids, where D2 and D3
   draw no binomial at all. That is a deliberate deviation: `name_en` is real
   botanical data and the binomial is what makes a specimen identifiable across
   languages. Following the sheet here would delete information rather than
   restyle it, and the archive is the one surface where the species name earns
   its place. Flagged for the owner — it is a content call, not a CSS one. */
@media (min-width: 761px) {
  .pw-cards.grid:not(.cat-grid):not(.is-list) .cat-caption .name-en { font-size: 14px; }
}

/* Tier on one side, price on the other. It wraps rather than clips: at 14px the
   worst case the catalogue can produce — the longest tier ("اقتناء مميز")
   beside a sale pair ("85 د.ك 75 د.ك") — no longer fits on one line inside a
   166px card on a 375pt phone. Wrapping drops the price to its own line only
   in that case; the rows stay level because grid rows stretch. */
.cat-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 2px 10px;
  margin-top: 7px;
}

/* 0.7rem = 11.2px, just clear of the 11pt floor. B3: an explicit 1.25 rather
   than the 1.7 these lines inherited from body copy — 1.7 gave a 16px line a
   27px box and inflated the caption for nothing. */
/* Phase 3D — rarity is the seal, and only the seal.
   It was being said twice: a stamped mark on the plate and the same fact
   spelled out in the caption underneath. Two visual languages for one piece of
   information is not hierarchy, it is repetition.

   Hidden rather than removed, and visually-hidden rather than display:none, so
   the words survive in the accessibility tree. That matters here: the seal is a
   <span> carrying its meaning in an aria-label, and aria-label on a plain span
   with no role is not reliably announced — this line is what actually
   guarantees a screen reader still hears "غير شائعة". */
.cat-tier {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* One price treatment for both grids. The home card used to set it at 700
   weight and the archive at 400 — the surface where people actually compare
   plants was showing the number more faintly than the one where they don't.
   14px at 600 carries on a specimen label without turning into a price sticker;
   the clay keeps it part of the archive rather than shouting over the name. */
/* ---------- the price primitive ----------
   Every price in every sheet — eleven card prices across seven screens, plus
   the cart, the vendor profile and the wishlist — is written the same way:
   IBM Plex Mono, `#7f432d`, `font-variant-numeric: tabular-nums`. Not one of
   them carries a `font-weight`.

   Two of those three we already had. `tabular-nums` we did not, and it is the
   one that does real work: without it the digits are proportional, so a column
   of prices in a grid does not line up on the decimal.

   On the weight. The sheets omit it, and this project has a standing rule that
   an omitted weight is not an explicit one (`ADMIN_MIGRATION_MATRIX.md` §1) —
   so 600 was never a violation. It is being taken to 400 on instruction, on
   the reading that a sheet which never once weights a price renders every one
   of them at 400, and the rendered sheet is the visual source of truth. Worth
   knowing that this reverses a deliberate earlier decision: 14/600 was chosen
   so the surface where people compare plants would not show the number more
   faintly than the one where they don't.

   Line-height stays at 1.25. The sheets set none, and 1.25 is inside what Mono
   renders at `normal` — replacing a real value with a computed one would be a
   change without a specification behind it. */
.cat-price {
  color: var(--archive-clay-deep);
  /* The figure ends in «د.ك». IBM Plex Mono has no Arabic glyphs, so the unit
     fell through to the system monospace face — measured on the deployed
     catalog: «د.ك» rendered 28.80px wide against 28.84px for generic
     monospace and 23.86px for IBM Plex Sans Arabic. --archive-mono exists for
     exactly this and carries the Arabic face behind the Latin one. */
  font-family: var(--archive-mono, 'IBM Plex Mono', 'IBM Plex Sans Arabic', monospace);
  font-size: 0.875rem;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  line-height: 1.25;
  white-space: nowrap;
}

/* ---------- the price scale, per surface ----------
   Claude Design does not draw the price at one size. It draws it at the size
   the surface calls for, and the surfaces disagree on purpose:

     01 Home, phone card ................ 12px
     02 Catalogue, phone 2-up card ...... 12px
     02 Catalogue, plate spread ......... 13.5px
     D1 Desktop home card ............... 13.5px
     D2 Desktop catalogue card .......... 12.5px
     D3 Desktop product, related ........ 12.5px

   So the home card is the editorial size and the catalogue card the index
   size, exactly as their names are. The related grids follow the catalogue,
   which is what D3 draws and also what they are — an index of neighbours.

   The 761–1040 band has no sheet at all. Rather than invent a third value for
   it, it takes the desktop number, so there is exactly one boundary and it
   sits where the sheets' own phone/desktop split sits.

   Scoped to `.pw-cards`, so the list view — which no sheet draws in any state
   — keeps the 14px it has rather than being given a number nothing specifies.
   It still picks up the primitive above, which is script- and surface-neutral. */
.pw-cards:not(.is-list) .cat-price { font-size: 12px; }

@media (min-width: 761px) {
  /* home is `.grid` without `.cat-grid`; the catalogue and both related grids
     carry `.cat-grid` and share D2/D3's number */
  .pw-cards.grid:not(.cat-grid):not(.is-list) .cat-price { font-size: 13.5px; }
  .pw-cards.cat-grid:not(.is-list) .cat-price { font-size: 12.5px; }
}

/* The spread is the one card 02 draws larger, at every width it exists. */
.pw-cards:not(.is-list) .plate-spread .cat-price { font-size: 13.5px; }

/* B4, the quietest level: status, not identity.
   It keeps 11.2px — that floor was fought for in A4 and dropping below it to
   look calmer would undo it. Quiet comes from colour and letterform instead:
   the muted neutral rather than the ink the Latin name uses, and none of the
   tracking that made it read as a label competing with the tier.
   Sold out is the only state that earns colour, because it is the only one
   that changes what you can do. */
.cat-avail {
  display: block;
  color: var(--archive-muted, #6a6760);
  font-family: var(--archive-mono, 'IBM Plex Mono', 'IBM Plex Sans Arabic', monospace);
  font-size: 0.7rem;
  font-weight: 400;
  line-height: 1.25;
  letter-spacing: 0.02em;
}
/* The label is always translated — «متوفر» / «نفدت» — and positive tracking
   opens the joins of a connected script. Latin keeps the sheet's 0.02em. */
html[lang="ar"] .cat-avail { letter-spacing: normal; }
.cat-avail.out { color: var(--archive-clay-deep); }

/* "متوفر" said, on every single card that had a ＋ on it, exactly what the ＋
   already said. Availability now speaks only when it has news: sold out. The
   element is left in the markup and only hidden, so nothing about the card
   builder or the data changes, and a screen reader still reaches it.

   Low stock cannot be expressed here yet — the builder emits `.out` for sold
   out and nothing else, so there is no hook in the DOM to hang it on. Adding
   one is a one-line change to pwBuildCard, which is frozen. */
.cat-avail:not(.out) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Two more things in this caption are read rather than admired, and both sat
   under the floor: how the plant is collected, and what it used to cost. The
   struck-through price is set here in rem rather than inherited as an em of
   .cat-price, so shrinking the price on a phone can never drag it back under. */
.cat-price .price-original { font-size: 0.7rem; }

/* How the plant is collected — Redesign 02 (المجموعة), the sheet that governs
   this card. It sets the line as plain body text under the price row: no Mono,
   no tracking and no dot marker, at 12.5px on the 1440 board and 11.5px on both
   834 and 390, in the muted neutral. The Phase 6b chip treatment stays on the
   home card and the cart line, which this sheet does not draw. */
.cat-caption .fulfill-chip {
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--archive-muted, #6a6760);
}
.cat-caption .fulfill-chip::before { content: none; }
/* The sheet draws the pickup case only and rules nothing about prior
   confirmation, so that state keeps the clay it already ships in. An omitted
   state is not a specified one. */
.cat-caption .fulfill-chip.approval { color: var(--gold-ink); }
@media (max-width: 1023px) {
  .cat-caption .fulfill-chip { font-size: 11.5px; }
}

/* The sheet keeps availability inside the price row, so its fulfilment line
   sits one distance below the price. This caption gives availability a row of
   its own, and that row is empty on every card that is not sold out, because
   the label is visually hidden there — leaving two row gaps where the sheet
   has one. Where there is no sold-out label whose reading order matters, the
   empty row is dropped and the line follows the price directly. */
.pw-cards:not(.is-list) .cat-caption:not(:has(.cat-avail.out)) {
  grid-template-areas:
    "name  name"
    "latin latin"
    "code  meta"
    "chip  chip";
}
/* Redesign 02 sets that distance at 7px on the 1440 board, 6 on 834 and 4 on
   390. The caption's own rhythm step is 8, so the difference is subtracted
   here rather than by rewriting a grid every other row depends on. */
.pw-cards:not(.is-list) .cat-caption > .fulfill-chip {
  margin-top: calc(7px - var(--cap-step) * 2);
}
@media (max-width: 1023px) {
  .pw-cards:not(.is-list) .cat-caption > .fulfill-chip { margin-top: calc(6px - var(--cap-step) * 2); }
}
@media (max-width: 767px) {
  .pw-cards:not(.is-list) .cat-caption > .fulfill-chip { margin-top: calc(4px - var(--cap-step) * 2); }
}

/* ---------- quick add ----------
   44x44 of real button, with the 36px archive mark drawn by ::before. The
   hit area used to be 36px of button plus a 4px pseudo-element bleed, which
   arrives at 44 but only as long as nothing ever restyles that pseudo. The
   target is now the element itself; the mark is decoration painted inside it,
   and it looks exactly as it did. */
.cat-add {
  position: absolute;
  inset-inline-end: -4px;
  bottom: 16px;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  color: var(--archive-ink);
  background: none;
  border: 0;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  /* isolate + z-index:-1 below: an absolutely positioned ::before paints in the
     positioned-descendant layer, i.e. ON TOP of the button's own text, which
     hid the ＋ behind the mark. Pushing it back one layer needs a stacking
     context on the button, or the -1 would drop it behind the card instead. */
  isolation: isolate;
  transition: color 200ms ease, scale 130ms cubic-bezier(.22, .9, .24, 1);
}

/* ---------- the acquisition mark, per surface ----------
   02 and D2 draw the ＋ as a 28px box at Mono 14; D1 draws it at 30 and Mono
   15. Ours was a 36px mark at 16.

   The BUTTON stays 44×44. This is the one place the sheets and the touch floor
   genuinely disagree — a 28px control is below every finger-target guideline —
   and the structure here was built for exactly that: the element is the hit
   area and `::before` paints the mark inside it. So the mark takes the sheet's
   size and the target keeps its 44, which is `preserve the visual appearance
   as closely as possible` without shipping a control a thumb cannot hit.
   `inset` is the arithmetic: 44 − 2×8 = 28, 44 − 2×7 = 30.

   Placement is the other half, and it was out by the width of the caption's
   padding. In every sheet the ＋ is a flex item INSIDE the caption, right-
   aligned to its content edge. Ours is absolutely positioned against the card,
   so at `inset-inline-end:-4px` the mark's edge landed on the card's border —
   outboard of the price's line end by the whole padding, 12px on a phone and
   18px on D1.

   The offset is therefore `caption padding − mark inset`, which puts the
   mark's outer edge exactly on the caption's content edge:
     phone      12 − 8 = 4      catalogue  15 − 8 = 7      home  18 − 7 = 11

   `.cat-meta`'s 48px end-clearance still covers it: the mark now reaches 30px
   into the caption's content instead of 12, and 48 > 30. Re-measured for
   overlap and wrapping at every width. */
.pw-cards:not(.is-list) .cat-add { font-size: 14px; inset-inline-end: 4px; }
.pw-cards:not(.is-list) .cat-add::before { inset: 8px; }

@media (min-width: 761px) {
  .pw-cards.cat-grid:not(.is-list) .cat-add { inset-inline-end: 7px; }

  .pw-cards.grid:not(.cat-grid):not(.is-list) .cat-add { font-size: 15px; inset-inline-end: 11px; }
  .pw-cards.grid:not(.cat-grid):not(.is-list) .cat-add::before { inset: 7px; }
}

.cat-add::before {
  content: '';
  position: absolute;
  inset: 4px;
  z-index: -1;
  background: var(--archive-paper);
  /* Back to the hairline. An earlier phase darkened this to α.38 so the one
     pressable thing on the card would not read at the weight of the things
     that are not — a real observation, but a legibility judgement, and the
     standing rule is that taste never justifies deviating from the sheets.
     `states` says "Hairline square", and D1 draws the card's quick-add as
     `border:1px solid rgba(20,33,27,.22)`. The action now reads through its
     hover, press and added states instead of through a heavier rest stroke. */
  border: 1px solid var(--archive-line);
  transition: background 200ms ease, border-color 200ms ease;
}

/* Press inverts the whole mark, not just its scale. On a touch screen there is
   no hover to preview the action, so the commit has to be unmistakable in the
   instant the finger lands — the same reason the ✓ that follows is a fill. */
.cat-add.added, .cat-add:active { color: var(--archive-paper); }
.cat-add.added::before,
.cat-add:active::before {
  background: var(--archive-ink);
  border-color: var(--archive-ink);
}
/* Split from the rule above, not wrapped with it. `.added` and
   `:active` are the ONLY confirmation a phone gets — the comment on
   this component says so itself. Guarding them alongside :hover
   would delete the commit feedback on every touch device. */
/* `states`, Quick add: hover is "Ink border", press is "Ink fill". Hover was
   filling too, which spent the press's own signal a step early and left the
   two states identical on a pointer device. The stroke darkens to ink and the
   ground stays paper; the fill now belongs to the press and the added state. */
@media (hover: hover) {
  .cat-add:hover::before {
    border-color: var(--archive-ink);
  }
}
.cat-add:active { scale: 0.94; }
.cat-add:focus-visible { outline: 2px solid var(--archive-clay); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .cat-card { animation: none; }
  .cat-add, .cat-add::before { transition: none; }
  .cat-add:active { scale: 1; opacity: 0.72; }
}

/* ---------- responsive ---------- */

@media (max-width: 1040px) {
  .cat-caption .name-ar { font-size: 1.1rem; }
}

@media (max-width: 760px) {
  .cat-caption .name-ar { font-size: 1.05rem; }
  html[dir='ltr'] .cat-caption .name-ar { font-size: 1.1rem; }
  /* Still clearly secondary against a 16.8px Arabic name, but no longer under
     the 11pt floor — it is the botanical identity, not decoration. */
  .cat-caption .name-en { font-size: 0.7rem; }
  /* The price does NOT shrink here. It used to drop to 0.8rem on a phone,
     which is the surface where people actually compare plants — the number
     holds 14px on every width. Checked for overflow on the narrowest card
     the grid produces (166px on a 375pt phone), including a sale row where
     the struck-through original sits beside it. */

  /* ---------- phones: the code and its tier become the eyebrow ----------
     An earlier pass put the code beside availability to free the price's line.
     It worked on width but paired the wrong two things: the accession code and
     the rarity are fixed properties of the specimen, availability is a state
     that changes when the last one sells. They should not share a line.

     So the code and the tier move together to the top, above the name, and the
     price takes a row of its own:

         SP-014 · شائعة
         مونستيرا
         Monstera
         17 د.ك                    ＋
         متوفر

     This also ends the width problem outright rather than negotiating with it.
     Nothing competes with the price for its line any more, so it cannot be
     pushed off one — the widest eyebrow (اقتناء مميز) needs 128px and the
     widest price block 99px, both inside a 142px caption on a 375pt phone.

     .cat-meta dissolves so the tier and the price can sit in different rows.
     display:contents is right here and was wrong before, because the caption
     is a grid now rather than a flex column — and it stays scoped away from the
     list view, which still needs .cat-meta to be a real box. */
  .pw-cards:not(.is-list) .cat-meta { display: contents; }

  .pw-cards:not(.is-list) .cat-caption {
    grid-template-areas:
      "code  tier"
      "name  name"
      "latin latin"
      "price price"
      "avail avail"
      "chip  chip";
  }
  /* The in-stock caption never got this template. The rule that drops the
     empty availability row for cards that are not sold out is written as
     `.cat-caption:not(:has(.cat-avail.out))`, and `:not()` takes the
     specificity of its argument — (0,5,0) against this rule's (0,3,0). Source
     order cannot help a weaker selector, so on every in-stock phone card the
     desktop areas kept winning, `price` named an area that did not exist, and
     the number was auto-placed into an implicit column at the inline end —
     which is exactly where the absolutely positioned ＋ sits. Measured live at
     320/360/375/390/430, Chromium and WebKit, RTL and LTR: 36px of the price
     under the mark on every in-stock card; 44px and clipping on a sale row.
     Only sold-out cards, which have no ＋ at all, were laid out as drawn.

     The phone template therefore needs the same in-stock twin as the desktop
     one, at the same specificity, later in the cascade. */
  .pw-cards:not(.is-list) .cat-caption:not(:has(.cat-avail.out)) {
    grid-template-areas:
      "code  tier"
      "name  name"
      "latin latin"
      "price price"
      "chip  chip";
  }

  /* Descendant, not child: display:contents changes how these are laid out,
     not whose children they are. */
  .pw-cards:not(.is-list) .cat-tier  { grid-area: tier; justify-self: start; }
  .pw-cards:not(.is-list) .cat-price { grid-area: price; justify-self: start; }

  /* The eyebrow is one unit, so its two halves sit tight; the name below it
     gets the full 8. */
  .pw-cards:not(.is-list) .cat-caption { row-gap: calc(var(--cap-step) * 2); }
  .pw-cards:not(.is-list) .cat-caption > .cat-sp { align-self: baseline; }

  /* A sale row is two figures — the struck original at 0.7rem and the price —
     and together they run to about 99px, which is more than the price row
     leaves clear of the mark's 44px column on a 375 card (138 − 44 = 94) and
     far more than on a 320 (113 − 44 = 69). `nowrap` is right for one figure
     and wrong for two: let the pair break BETWEEN the figures, never inside
     one, and keep the row short of the mark. Measured on the same fixture that
     showed 44px of overlap and a clipped unit before this rule. */
  .pw-cards:not(.is-list) .cat-price:has(.price-original) {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 8px;                  /* the original's own inline-end margin, moved here */
    max-width: calc(100% - 44px);
  }
  .pw-cards:not(.is-list) .cat-price:has(.price-original) .price-original { margin-inline-end: 0; }

  /* The ＋ shares the price's line rather than floating below the last row of
     text, which is where the eye expects the acquisition mark to be. */
  .cat-add { bottom: 12px; }
}
