/* ============================================================================
   Catalog module — product card grid (index) + product detail page.
   Builds on the app.css design system; only catalog-specific rules live here.
   ========================================================================= */

/* --- Product grid --------------------------------------------------------- */
.prod-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 16px;
  margin-bottom: 18px;
  align-items: stretch;
}
/* The whole card is the link, so it carries the hover and the focus ring —
   an inner focusable element would put the ring around the name alone. */
a.prod-card {
  display: flex; flex-direction: column;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 12px; box-shadow: 0 1px 2px rgba(16, 24, 20, .05);
  padding: 0; color: var(--ink); overflow: hidden;
  transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
a.prod-card:hover {
  text-decoration: none;
  border-color: var(--green-600);
  box-shadow: 0 8px 20px rgba(16, 24, 20, .10);
  transform: translateY(-2px);
}
a.prod-card:focus-visible { outline: 2px solid var(--green-600); outline-offset: 2px; }

/* Body is one padded column; the price block is pinned to the bottom with
   margin-top:auto so cards in a row line their prices up however long the
   names run. */
.prod-body { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px 14px; flex: 1 1 auto; }
.prod-meta {
  font-size: .72rem; color: var(--muted); letter-spacing: .02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Two lines, then ellipsis: a 90-character product name must not make its
   card twice the height of its neighbours. */
.prod-name {
  font-weight: 700; line-height: 1.35; font-size: .96rem;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.prod-flags { display: flex; flex-wrap: wrap; gap: 4px; }

.prod-price {
  margin-top: auto; padding-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--green-900);
}
.prod-figure { display: flex; align-items: baseline; flex-wrap: wrap; gap: 5px; }
.prod-figure .prod-range { font-size: 1.22rem; font-weight: 800; letter-spacing: -.01em; }
.prod-from, .prod-unit { font-weight: 400; color: var(--muted); font-size: .78rem; }
.prod-none { color: var(--muted); font-weight: 600; font-size: .85rem; }

/* --- Category navigation (storefront) ---------------------------------------
   Real anchors, not a dropdown: a crawler cannot operate a <select>, so a
   category page reached only through one is a page nothing links to. They
   scroll rather than wrap past two rows on a phone. */
.cat-nav {
  display: flex; gap: 8px; flex-wrap: wrap;
  margin: 0 0 14px;
}
a.cat-chip {
  display: inline-flex; align-items: baseline; gap: 6px;
  padding: 6px 12px; border-radius: 999px;
  background: var(--card); border: 1px solid var(--line);
  color: var(--ink); font-size: .86rem; font-weight: 600; white-space: nowrap;
}
a.cat-chip:hover { text-decoration: none; border-color: var(--green-600); }
a.cat-chip.is-on { background: var(--green-900); border-color: var(--green-900); color: #fff; }
a.cat-chip .cat-n { font-weight: 400; font-size: .76rem; opacity: .65; }

@media (max-width: 700px) {
  .cat-nav { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; }
}

/* --- Detail page banners ---------------------------------------------------- */
.order-banner { border-color: var(--green-600); background: #f0faf2; font-weight: 600; }
.disc-banner  { border-color: var(--amber); background: var(--amber-bg); }

/* --- Offers table ------------------------------------------------------------ */
tr.best-offer td { background: #f2faf4; }

/* --- Compliance panel ---------------------------------------------------------- */
.comp-state { padding: 8px 0 4px; border-top: 1px solid var(--line); }
.comp-state:first-of-type { border-top: none; }
.comp-state h3 { margin: 6px 0 8px; }
.comp-req { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; padding: 4px 0; }
.comp-req .comp-name { font-weight: 600; text-transform: capitalize; }
.comp-req .hint { flex-basis: 100%; margin: 0; }

/* A rule's validity window: two dates stacked in one cell rather than two
   columns of 140px date pickers pushing the row's buttons off the grid. */
.rule-window { white-space: nowrap; }
.rule-window input { display: block; }
.rule-window input + input { margin-top: 4px; }

/* --- "Request this document" ------------------------------------------------
   Sits at the right end of a document row: a short note box and the ask. On a
   phone the row has already wrapped, so it takes the full width rather than
   squeezing the note field to nothing. */
.doc-ask { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.doc-ask input { min-width: 210px; flex: 1 1 210px; }
@media (max-width: 700px) {
  .linked-list li .doc-ask { flex-basis: 100%; }
  .doc-ask input { flex-basis: 100%; }
}

/* --- Linked items / link forms ---------------------------------------------------- */
.linked-list { list-style: none; margin: 0 0 14px; padding: 0; }
.linked-list li {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  padding: 7px 0; border-bottom: 1px solid var(--line);
}
.linked-list li:last-child { border-bottom: none; }
.link-forms { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }

/* --- Mobile ---------------------------------------------------------------------- */
@media (max-width: 900px) {
  .prod-grid { grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: 12px; }
  .prod-body { padding: 10px 12px 12px; }
  .prod-figure .prod-range { font-size: 1.1rem; }
  .linked-list li .row-actions { margin-left: 0 !important; flex-basis: 100%; }
}


/* --- Card imagery ----------------------------------------------------------
   A fixed 4:3 well rather than an intrinsic height, so a grid of products
   whose photos were shot at different aspect ratios still lines up. Products
   with no photo keep the well (an empty placeholder) — a grid where only some
   cards have a picture reads as broken rather than as sparse. */
.prod-shot {
  position: relative; width: 100%; aspect-ratio: 4 / 3;
  background: #f6f7f6; border-bottom: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
/* font-size/colour apply to the ALT TEXT when an image fails to load — an
   unstyled broken image dumps full-size black text across the card. */
.prod-shot img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  font-size: .7rem; color: var(--muted); overflow: hidden;
}
.prod-shot .prod-shot-empty { color: var(--line); font-size: 1.8rem; }

/* Status over the photo: availability, a compliance warning, "linked".
   pointer-events:none so the badges never eat the card's click. */
.prod-tags {
  position: absolute; inset: 8px 8px auto 8px;
  display: flex; flex-wrap: wrap; gap: 4px; pointer-events: none;
}
.prod-tags .badge { box-shadow: 0 1px 3px rgba(16, 24, 20, .18); }
/* A slow zoom on hover — enough to feel live, not enough to crop anything the
   buyer was looking at. Off entirely for anyone who asked for less motion. */
.prod-shot img { transition: transform .35s ease; }
a.prod-card:hover .prod-shot img { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  a.prod-card, .prod-shot img { transition: none; }
  a.prod-card:hover { transform: none; }
  a.prod-card:hover .prod-shot img { transform: none; }
}

/* The storefront card carries its own padding (app.css), so pull the image
   well back out to the card edges — a photo inset by 16px on three sides and
   flush on none reads as a mistake. */
.shop-card { padding: 0 0 14px !important; overflow: hidden; }
.shop-card > *:not(.prod-shot) { margin-left: 16px; margin-right: 16px; }
.shop-card > .shop-card-cat { margin-top: 12px; }

/* Price + MOQ ranges. The range sits on one line and wraps as a unit so
   "$0.42 – $1.10" never breaks across lines mid-dash. */
.prod-range { white-space: nowrap; }
/* A card quoting a negotiated price says so: the same figure without the
   badge reads as list price, and the buyer has no way to tell the difference. */
.prod-yours { vertical-align: 1px; }
.prod-moq { font-weight: 400; color: var(--muted); font-size: .78rem; }

/* Option chips: what a buyer can choose before they open the product. */
.prod-opts { display: flex; flex-wrap: wrap; gap: 4px; }
.opt-chip {
  font-size: .7rem; line-height: 1; padding: 4px 7px; border-radius: 999px;
  background: var(--green-50, #f0faf2); color: var(--green-900);
  border: 1px solid var(--green-600); font-weight: 600;
}

/* --- Product gallery (detail page) -----------------------------------------
   Hero + thumbnail strip. The strip scrolls horizontally rather than wrapping,
   so a product with ten photos does not push the buy panel below the fold on a
   phone. */
/* Bounded, not full-bleed: a 4:3 well across a 1100px card is a metre of
   product photo and pushes the offers table off the screen. */
/* Centred in its card: bounded at 560px inside a ~1150px card, a left-hugging
   gallery leaves half the card empty beside it — most obvious on a product
   with a single photo, where there is no thumbnail strip to balance it. */
.gallery { display: flex; flex-direction: column; gap: 10px; max-width: 560px; margin-inline: auto; }
.gallery-hero {
  width: 100%; aspect-ratio: 4 / 3; max-height: 420px; background: #f6f7f6;
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.gallery-hero img { width: 100%; height: 100%; object-fit: contain; display: block; }
.gallery-strip {
  display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px;
  scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch;
}
.gallery-strip button {
  flex: 0 0 auto; width: 68px; height: 68px; padding: 0; cursor: pointer;
  background: #fff; border: 1px solid var(--line); border-radius: 8px;
  overflow: hidden; scroll-snap-align: start;
}
.gallery-strip button img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-strip button.is-active { border-color: var(--green-600); box-shadow: 0 0 0 2px rgba(24,128,56,.18); }
.gallery-strip button:focus-visible { outline: 2px solid var(--green-600); outline-offset: 2px; }

/* --- Option pickers --------------------------------------------------------- */
.opt-row { display: flex; flex-direction: column; gap: 3px; margin-bottom: 10px; }
.opt-row label { font-weight: 600; font-size: .85rem; }
.opt-row .hint { margin: 0; }
.opt-price-line {
  display: flex; justify-content: space-between; gap: 10px; flex-wrap: wrap;
  padding: 8px 10px; margin: 8px 0; border-radius: 8px;
  background: #f6f8f6; border: 1px solid var(--line); font-weight: 700;
}
.opt-price-line .hint { font-weight: 400; margin: 0; }

/* --- Master editor: image manager ------------------------------------------- */
.img-manager { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; }
.img-tile { border: 1px solid var(--line); border-radius: var(--radius); padding: 10px; background: #fff; }
.img-tile .shot {
  width: 100%; aspect-ratio: 4 / 3; background: #f6f7f6; border-radius: 6px;
  overflow: hidden; margin-bottom: 8px;
}
.img-tile .shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.img-tile.is-hero { border-color: var(--green-600); box-shadow: 0 0 0 2px rgba(24,128,56,.14); }

@media (max-width: 900px) {
  .gallery-strip button { width: 56px; height: 56px; }
  .img-manager { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
}

/* --- Bulk "add offers" grid --------------------------------------------------
   The defaults row is a compact strip above the factory table, not a full
   form-grid: it is a set of values to copy down, not a record being saved, and
   giving it the same visual weight as the grid below reads as two forms. */
.bulk-defaults {
  display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: flex-end;
  padding: 12px; margin-bottom: 12px;
  background: #f6f7f6; border: 1px solid var(--line); border-radius: var(--radius);
}
.bulk-defaults .field { margin: 0; flex: 0 1 130px; }
.bulk-defaults .field .hint { margin: 2px 0 0; }
.bulk-defaults .bulk-fill-action { flex: 0 0 auto; align-self: flex-end; }

@media (max-width: 700px) {
  .bulk-defaults .field { flex-basis: calc(50% - 7px); }
  .bulk-defaults .bulk-fill-action { flex-basis: 100%; }
}

/* --- Customer pricing: the "add several" disclosure ---------------------------
   <summary class="btn"> so the control reads as the button it is; the default
   marker would sit outside the button box and give it two affordances. */
.bulk-rules { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 14px; }
.bulk-rules > summary { display: inline-block; cursor: pointer; list-style: none; }
.bulk-rules > summary::-webkit-details-marker { display: none; }
.bulk-rules > summary::marker { content: ''; }
.bulk-rules[open] > summary { margin-bottom: 12px; }

/* The tier ladder. Two narrow number fields and a note that takes the rest;
   on a phone each field goes full width rather than squeezing to nothing. */
.tier-rows { display: flex; flex-direction: column; gap: 8px; }
.tier-row { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }
.tier-row > label { flex: 0 1 150px; margin-bottom: 0; }
.tier-row > label.tier-note { flex: 1 1 220px; }
/* The caption only earns its line on the first row — after that the columns
   are established and repeating it five times is noise. */
.tier-row:not(:first-child) > label { font-size: 0; }
.tier-row:not(:first-child) > label input { font-size: 1rem; }

/* A quantity that came from the customer price breaks is shown, not typed —
   entering it a second time is exactly what this is here to avoid. It keeps
   the column width of the input it replaces so the rows still line up. */
.tier-fixed { flex: 0 1 150px; display: flex; flex-direction: column; gap: 3px; }
.tier-fixed .tier-cap { font-weight: 600; font-size: .84rem; }
.tier-fixed .tier-val { padding: 7px 0; }
.tier-row:not(:first-child) > .tier-fixed .tier-cap { font-size: 0; }
/* What that quantity sells for: a column, not a note under the cost box —
   under it, the hint lifts the input out of line with the rest of the row. */
.tier-sell { flex: 0 0 76px; }
.tier-sell .tier-val { color: var(--muted); }

@media (max-width: 700px) {
  .tier-row > label, .tier-row > label.tier-note, .tier-row > .tier-fixed { flex-basis: 100%; }
  /* Full width means no column to infer the field from, so bring the captions
     back on every row. */
  .tier-row:not(:first-child) > label,
  .tier-row:not(:first-child) > .tier-fixed .tier-cap { font-size: .84rem; }
  /* One lonely line each for a caption and a number reads as a gap; keep the
     sell price on a single line. */
  .tier-sell { flex-direction: row; gap: 6px; align-items: baseline; }
  .tier-sell .tier-val { padding: 0 0 4px; }
  /* Stacked, the column is a caption per row — one saying nothing is noise.
     It stays on the desktop grid, where it is holding a column open. */
  .tier-sell.is-empty { display: none; }
}

/* --- Factory price breaks ------------------------------------------------------
   The ladder sub-row sits under its offer, so it must not read as another
   offer: no row hover, a quiet inset, and the editor tucked behind its own
   summary until someone wants it. */
.offer-breaks-row > td { background: #fbfcfb; border-bottom: 2px solid var(--line); padding: 10px 12px; }
table.data tr.offer-breaks-row:hover td { background: #fbfcfb; }
.offer-breaks > summary { display: inline-block; cursor: pointer; list-style: none; }
.offer-breaks > summary::-webkit-details-marker { display: none; }
.offer-breaks > summary::marker { content: ''; }
.offer-breaks[open] > summary { margin-bottom: 10px; }

/* The read-only ladder: one chip per rung, base rung muted so the breaks
   above it are what the eye lands on. */
.ladder-strip { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 10px; }
.ladder-rung {
  font-size: .76rem; line-height: 1.3; padding: 3px 8px; border-radius: 999px;
  background: var(--green-100); color: var(--green-900); border: 1px solid var(--green-600);
  white-space: nowrap;
}
.ladder-rung.is-base { background: #f1f3f2; color: var(--ink-soft); border-color: var(--line); }
/* The rung the quantity in the box is actually on. Muted siblings, so the
   buyer can see at a glance which one they are buying at and how far the next
   one is. */
.ladder-strip .ladder-rung { opacity: .55; }
.ladder-strip .ladder-rung.is-active {
  opacity: 1; font-weight: 700;
  box-shadow: 0 0 0 2px rgba(24, 128, 56, .18);
}
.ladder-rung .hint { margin: 0; font-size: .68rem; }
/* Inside the buyer's right-aligned price column the chips would otherwise
   stack against the wrong edge. */
td.num .ladder-strip { justify-content: flex-end; }

/* --- Compare picker ----------------------------------------------------------
   The tick sits above the card rather than inside it: a control nested in an
   <a> is unclickable in some browsers and unreachable by keyboard in others. */
.prod-slot { display: flex; flex-direction: column; gap: 6px; }
.prod-slot > .prod-card { flex: 1 1 auto; }
.prod-slot.is-picked > .prod-card {
  border-color: var(--green-600);
  box-shadow: 0 0 0 2px rgba(24, 128, 56, .18);
}
.cmp-pick {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: .78rem; color: var(--muted); cursor: pointer; align-self: flex-start;
}
.cmp-pick input:disabled + span { opacity: .45; }

/* A sticky bar rather than a button per card: the action only exists once two
   are ticked, and it must stay reachable however far the grid has scrolled. */
.cmp-bar {
  position: sticky; bottom: 12px; z-index: 5;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 0 12px; padding: 10px 14px;
  background: var(--card); border: 1px solid var(--green-600);
  border-radius: 999px; box-shadow: 0 6px 18px rgba(16, 24, 20, .14);
  width: fit-content;
}
.cmp-bar[hidden] { display: none; }

/* --- Compare table ------------------------------------------------------------ */
.compare-table th[scope="row"] { text-align: left; font-weight: 600; color: var(--muted); font-size: .85rem; }
.compare-table thead th { vertical-align: top; min-width: 170px; }
.cmp-shot {
  width: 100%; aspect-ratio: 4 / 3; max-height: 120px; background: #f6f7f6;
  border-radius: 8px; overflow: hidden; margin-bottom: 6px;
  display: flex; align-items: center; justify-content: center;
}
.cmp-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* --- Spec chips on a card ----------------------------------------------------
   A definition list, because that is what it is: a label and the values a
   buyer can have. Two columns so the labels align down the card and the eye
   can scan one axis across a row of cards; on a narrow card they stack. */
.prod-specs {
  display: grid; grid-template-columns: auto 1fr; gap: 2px 8px;
  margin: 0; font-size: .76rem; line-height: 1.35;
}
.prod-specs dt { color: var(--muted); white-space: nowrap; }
.prod-specs dd { margin: 0; font-weight: 600; color: var(--ink); }
@media (max-width: 480px) {
  .prod-specs { grid-template-columns: 1fr; gap: 0 0; }
  .prod-specs dt { font-size: .7rem; }
}
.prod-specs .spec-more { margin-left: 5px; font-weight: 400; color: var(--muted); }
