/* FlowAds v2 — shared template stylesheet.
 * Phase D deliverable 4 (revised Phase D2, James's visual-gate feedback
 * 2026-07-02): ONE shared CSS approach, loaded/emitted once per page by
 * Phase E/F, all rules scoped under the .fa2- prefix. Mobile-first: base
 * rules are the single-column/small-screen layout, min-width media queries
 * progressively enhance. No JS required for any of it (C-1).
 *
 * ============================================================================
 * THE THREE-LAYER STYLING ARCHITECTURE (D2 orchestrator decision — James's
 * #1 cross-cutting demand: nothing visual hardcoded)
 * ============================================================================
 *   1. Base structure — this file's SELECTORS. Layout/mechanics only
 *      (flex/grid direction, aspect-ratio, image dimensions). No color, no
 *      font-family, no font-size, no radius, no spacing literal appears
 *      inside a selector body below the :root block — every one of those
 *      is a var(--fa2-*) reference. Enforced by grep test (see
 *      tests/templates-config.test.js): zero hex colors / px font-sizes
 *      outside :root.
 *   2. Theme variables — the :root block immediately below ("EDIT ME").
 *      Every color/font-family/font-size/radius/spacing knob for every
 *      template lives here as one flat, documented list.
 *   3. Per-site / per-template / per-brand override CSS from the DB (C-2) —
 *      NOT in this file. v2/templates/index.js's buildPageCss() takes raw
 *      CSS strings the render layer reads from site_config
 *      (template_css_override, template_css_override:<templateId>,
 *      brand_css_override:<brand_slug>) and emits them, sanitized, AFTER
 *      this stylesheet in the same <style> block — they win by ordinary
 *      cascade/source-order just by redeclaring a --fa2-* variable, no
 *      !important required. See buildPageCss()'s doc comment in index.js
 *      for the exact wiring contract.
 * ============================================================================
 */

:root {
  /* ==========================================================================
   * EDIT ME — FlowAds v2 template theme variables.
   * This is the ONE place every template's default look is defined. A
   * site-wide, per-template, or per-brand override (site_config, layer 3
   * above) re-declares any of these under a more specific/later selector —
   * nothing here needs to change to retheme a site.
   * ========================================================================== */

  /* ---- Typography -------------------------------------------------------- */
  --fa2-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --fa2-line-height: 1.45;
  --fa2-title-font-size: 0.98rem;       /* grid/stacked/vstack item titles */
  --fa2-title-font-size-lg: 1.28rem;    /* hero title */
  --fa2-subtitle-font-size: 0.88rem;
  --fa2-desc-font-size: 0.85rem;
  --fa2-header-font-size: 1.08rem;
  --fa2-header-weight: 700;
  --fa2-price-font-size: 1.05rem;
  --fa2-price-weight: 700;
  --fa2-price-was-font-size: 0.85rem;
  --fa2-badge-font-size: 0.7rem;
  --fa2-badge-weight: 700;
  --fa2-num-font-size: 0.8rem;
  --fa2-source-font-size: 0.72rem;
  --fa2-source-weight: 600;
  --fa2-disclosure-font-size: 0.72rem;
  --fa2-disclosure-link-font-size: 0.72rem;

  /* Truncation — item 4 (alignment). -webkit-line-clamp accepts a var()
   * integer directly; the max-height rule a few lines below it in each
   * selector is the plain-text fallback for engines without line-clamp
   * support (clips, no ellipsis, still keeps card heights consistent). */
  --fa2-title-lines: 2;
  --fa2-desc-lines: 2;

  /* ---- Color SCHEMES (coupled bg + foreground-on pairs) -------------------
   * Phase M, M1c — James's design principle (memory
   * feedback_color-schemes-as-coupled-pairs, 2026-07-03): a color choice is
   * never an independent bg/text pair that can drift apart and fail
   * contrast — it's a NAMED SCHEME, always overridden as a unit. Every
   * element below that needs a background + a legible foreground on it
   * (the CTA, the badge/accent, the rank-number circle, on-card text) points
   * at one of these pairs via var() rather than owning its own literal.
   * Because CSS custom properties resolve at USE time (not at declaration
   * time), a site override that redeclares just e.g.
   * `--fa2-scheme-amber-bg`/`-on` together automatically re-covers every
   * element wired to that scheme, and structurally CANNOT touch only one
   * half of a pair the way independent `--fa2-cta-bg`/`--fa2-cta-text`
   * variables previously could (the exact white-on-#ffd100 = 1.46:1 failure
   * class this restructure closes). tests/color-scheme-contrast.test.js
   * enforces every `--fa2-scheme-*-bg`/`-on` pair here at >= 4.5:1 WCAG AA —
   * a scheme that fails contrast fails the build.
   *
   * amber   = the "light scheme" (light bg, near-black on) — the default
   *           CTA. James: attractive amber/gold + near-black text
   *           (~11:1 here; his #ffd100 was illustrative of the HUE, paired
   *           dark to actually be legible).
   * crimson = "dark scheme" (saturated bg, white on) — badge/accent.
   * navy    = "dark scheme" (dark bg, white on) — the stacked rank-number
   *           circle.
   * surface = the neutral on-card text scheme (near-white bg, body/muted
   *           text) — source/brand labels and disclosure copy read against
   *           the plain card surface, not a colored chip, so they draw
   *           their foreground from THIS scheme rather than an independent
   *           literal of their own. */
  --fa2-scheme-amber-bg: #ffc107;
  --fa2-scheme-amber-on: #171310;
  --fa2-scheme-crimson-bg: #c22f3d;
  --fa2-scheme-crimson-on: #ffffff;
  --fa2-scheme-navy-bg: #1d3557;
  --fa2-scheme-navy-on: #ffffff;
  --fa2-scheme-surface-bg: #ffffff;
  --fa2-scheme-surface-on: #1f2937;
  --fa2-scheme-surface-on-muted: #6b7280;

  /* ---- Colors -------------------------------------------------------------
   * "Accent" is the one shared brand color (badges, banner border, stacked
   * rank circle) — separate from the CTA color, which is deliberately its
   * own high-contrast token (item 2) so a themed accent never has to also
   * double as a legible button color. Everything below is either a plain
   * surface/text role or an ALIAS of a scheme pair above — never a second,
   * independent copy of the same color. */
  --fa2-color-text: var(--fa2-scheme-surface-on);
  --fa2-color-text-secondary: #374151;
  --fa2-color-text-muted: var(--fa2-scheme-surface-on-muted);
  --fa2-color-border: #e5e7eb;
  --fa2-color-placeholder-bg: #e5e7eb;
  --fa2-color-price: #111827;
  --fa2-color-price-was: #9ca3af;
  --fa2-color-surface: var(--fa2-scheme-surface-bg);
  /* The card-hover tint. It existed ONLY as the name inside a var() fallback
   * (`var(--fa2-color-surface-alt, rgba(0,0,0,0.02))`) and was never declared,
   * so every card hover painted 2% black on white - invisible, which made a
   * click target that MEASURES as complete read as broken.
   *
   * Two declarations, the idiom the clamp rules already use: the literal is
   * what an engine without color-mix() keeps, and it is what the expression
   * below computes to on the default scheme, so the two cannot disagree on
   * anything shipping. The color-mix() is what makes a site override of the
   * surface PAIR re-cover the hover tint (M1c, coupled pairs) instead of
   * stranding a grey literal on a dark theme. */
  --fa2-color-surface-alt: #f2f3f5;
  --fa2-color-surface-alt: color-mix(in srgb, var(--fa2-scheme-surface-on) 6%, var(--fa2-scheme-surface-bg));

  --fa2-accent: var(--fa2-scheme-crimson-bg);
  --fa2-badge-bg: var(--fa2-accent);
  --fa2-badge-text: var(--fa2-scheme-crimson-on);
  --fa2-num-bg: var(--fa2-scheme-navy-bg);
  --fa2-num-text: var(--fa2-scheme-navy-on);
  --fa2-source-color: var(--fa2-scheme-surface-on-muted);
  --fa2-brand-color: var(--fa2-scheme-surface-on);
  --fa2-disclosure-color: var(--fa2-scheme-surface-on-muted);
  --fa2-disclosure-link-color: var(--fa2-scheme-surface-on);

  /* CTA — item 2 (Phase M redo, M1c). Amber scheme: near-black on amber/
   * gold, ~11:1 — comfortably above the 4.5:1 WCAG AA threshold, verified
   * programmatically for every scheme pair (not just this one) in
   * tests/color-scheme-contrast.test.js. */
  --fa2-cta-bg: var(--fa2-scheme-amber-bg);
  --fa2-cta-text: var(--fa2-scheme-amber-on);
  --fa2-cta-weight: 700;
  --fa2-cta-font-size: 0.95rem;
  --fa2-cta-radius: 6px;
  --fa2-cta-padding: 0.75rem 1.3rem;
  /* Small CTA variant (vertical-stack's per-row button, hero-card's
   * secondary link sizing) — its own theme knobs, not a hardcoded literal
   * on the selector, per the item-1 "nothing outside :root" rule. */
  --fa2-cta-font-size-sm: 0.82rem;
  --fa2-cta-padding-sm: 0.4rem 0.85rem;

  /* ---- Radius -------------------------------------------------------------- */
  --fa2-radius-sm: 6px;
  --fa2-radius: 8px;
  --fa2-radius-lg: 10px;
  --fa2-badge-radius: 4px;

  /* ---- Spacing scale (denser defaults — James's whitespace note) --------- */
  --fa2-space-2xs: 0.25rem;
  --fa2-space-xs: 0.35rem;
  --fa2-space-sm: 0.5rem;
  --fa2-space-md: 0.65rem;
  --fa2-space-lg: 0.9rem;
  --fa2-space-xl: 1.1rem;

  /* ---- Layout knobs (still theme-level, not structural) ------------------ */
  --fa2-meta-row-height: 1.55rem;       /* item 4: reserved price/source slot */

  /* potm — the featured image's column width, which is the ONLY thing the
   * hero_size param changes. A square product shot fits by HEIGHT under
   * object-fit:contain, so frame width beyond that height renders nothing:
   * these are square on purpose, and the unit stays landscape because of the
   * side-by-side split rather than because of the picture. */
  --fa2-potm-media-sm: 212px;
  --fa2-potm-media-md: 244px;
  --fa2-potm-media-lg: 276px;
  --fa2-potm-month-font-size: 0.64rem;
  --fa2-potm-peer-title-font-size: 0.78rem;
  --fa2-potm-hover-bg: #fbfaf8;         /* the whole-row hover wash */
  --fa2-banner-hero-media-width: 62%;   /* item 5: 1-item banner image width */
}

.fa2-block {
  box-sizing: border-box;
  font-family: var(--fa2-font-family);
  color: var(--fa2-color-text);
  line-height: var(--fa2-line-height);
  margin: var(--fa2-space-xl) 0;
}
.fa2-block *,
.fa2-block *::before,
.fa2-block *::after {
  box-sizing: inherit;
}
.fa2-block img {
  max-width: 100%;
  height: auto;
  display: block;
}
.fa2-block a {
  text-decoration: none;
  color: inherit;
}
/* CTA contrast fix (item 2 root cause): `.fa2-block a` above is an
 * element+class selector (specificity 0,1,1), which used to beat the
 * single-class `.fa2-btn` rule (0,1,0) and forced the button's text back to
 * the block's dark text color regardless of --fa2-cta-text — the button
 * LOOKED dark-on-dark even though the CSS "said" white. Excluding .fa2-btn
 * here (rather than bumping .fa2-btn's own specificity, which would ripple
 * into every override selector) is the minimal, cascade-correct fix. */
.fa2-block a:not(.fa2-btn) {
  color: inherit;
}

/* Shared atoms ------------------------------------------------------- */

.fa2-image--placeholder {
  background: var(--fa2-color-placeholder-bg);
  border-radius: var(--fa2-radius-sm);
  display: block;
}

/* Image pipeline (2026-07-11, docs/plans/briefs/image-pipeline.md §3.2).
 * A PROCESSED row renders its self-hosted variants as
 * <picture class="fa2-picture"><source webp><img class="fa2-*__image"></picture>
 * (templates/item-helpers.js's imageHtml). `display: contents` makes the
 * <picture> wrapper LAYOUT-TRANSPARENT: the <img> stays the flex/grid item its
 * parent's rules already target, so every existing per-template image rule
 * (.fa2-grid__image, .fa2-hero__image, ...) applies EXACTLY as it does to an
 * unprocessed row's bare <img> — no template CSS changes, no layout shift when
 * a row flips from unprocessed to processed.
 *
 * Every browser since 2018 supports display:contents; an older one falls back
 * to a picture element's default `display: inline`, which is also what it would
 * have done with no rule at all. UNPROCESSED rows emit no <picture> whatsoever,
 * so this selector never matches for them. */
.fa2-picture {
  display: contents;
}

.fa2-source {
  display: block;
  font-size: var(--fa2-source-font-size);
  font-weight: var(--fa2-source-weight);
  color: var(--fa2-source-color);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin: 0 0 var(--fa2-space-2xs);
}

/* brand label (Phase M, M1b) — "who it's about", distinct from .fa2-source
 * ("where the click lands"). Deliberately NOT uppercase/letter-spaced like
 * .fa2-source, so the two read as visually distinct roles rather than two
 * copies of the same chip. */
.fa2-brand {
  display: block;
  font-size: var(--fa2-source-font-size);
  font-weight: 500;
  font-style: italic;
  color: var(--fa2-brand-color);
  margin: 0 0 var(--fa2-space-2xs);
}

.fa2-price-row {
  display: inline-flex;
  align-items: baseline;
  gap: var(--fa2-space-sm);
  margin: 0;
}
.fa2-price {
  font-weight: var(--fa2-price-weight);
  font-size: var(--fa2-price-font-size);
  color: var(--fa2-color-price);
}
.fa2-price--was {
  font-weight: 400;
  font-size: var(--fa2-price-was-font-size);
  color: var(--fa2-color-price-was);
  text-decoration: line-through;
}

/* item 4 + item 8: the reserved-height meta slot. A fixed floor height
 * (min-height) keeps a price-less card from collapsing shorter than a priced
 * neighbor in the same row. Holds the source label (item 8, shown on EVERY
 * unit) and, below it, the price row when a price exists — so when there is
 * no price the slot still carries the source label ("render source label
 * there when no price") and never goes empty. */
.fa2-grid__meta,
.fa2-banner__meta,
.fa2-stacked__meta {
  min-height: var(--fa2-meta-row-height);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--fa2-space-2xs);
}
.fa2-grid__meta,
.fa2-stacked__meta {
  align-items: flex-start;
}
.fa2-banner__meta {
  align-items: center;
}
/* source/brand labels inside a meta slot drop their own bottom margin —
 * the slot's gap handles the spacing between them and the price row. */
.fa2-grid__meta .fa2-source,
.fa2-banner__meta .fa2-source,
.fa2-stacked__meta .fa2-source,
.fa2-grid__meta .fa2-brand,
.fa2-banner__meta .fa2-brand,
.fa2-stacked__meta .fa2-brand {
  margin: 0;
}

/* ---- the action row (review finding 1, James's ruling 2026-09-13) --------
 * Price on the left edge, CTA on the right edge, ONE line.
 *
 * Five units used to put the button alone on a line of its own, centred with
 * nothing to align to. Measured on `hero` at the 816px article column: the
 * price row used 15% of its line and the CTA 27%, and 43% of the content
 * column was empty. `vertical-stack` already did it this way, which is why it
 * was the one unit deliberately NOT touched; `potm` ships the same shape.
 *
 * space-between with a SINGLE child - a price-less article card - puts the
 * button on the right edge, so a priced card and a price-less one keep the
 * same row height and the same button baseline. That is what closes the
 * article-versus-commerce step (finding 2) rather than any height hardcoded
 * anywhere: the price left the meta slot, so the slot is one line for every
 * card type, filled or not. */
.fa2-action {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fa2-space-md);
  flex-wrap: wrap;
}

/* ⚠ THE CTA NEEDS AN EDGE, NOT A SIBLING. `justify-content: space-between`
 * with a SINGLE child resolves to flex-start, so a price-less item (every
 * article in a mixed basket) put its button on the LEFT while a priced item
 * put its button on the right - in the same unit, side by side. Measured in a
 * mixed basket: priced gapRight 0, price-less gapRight 206.
 *
 * margin-left:auto is inert with two children (the button is already last) and
 * decisive with one, so the row's behaviour stops depending on how many
 * children it happens to have - which is the property the action row claims. */
.fa2-action .fa2-btn { margin-left: auto; }

/* basket-banner's row did not reach the card edges: 51px short on a priced row,
 * and collapsed to the button's own width on a price-less one, so the three
 * cards' buttons shared no vertical edge. */
.fa2-banner__card > .fa2-action,
.fa2-banner__hero-action { width: 100%; }

.fa2-btn {
  display: inline-block;
  padding: var(--fa2-cta-padding);
  border-radius: var(--fa2-cta-radius);
  font-weight: var(--fa2-cta-weight);
  font-size: var(--fa2-cta-font-size);
  color: var(--fa2-cta-text);
  background-color: var(--fa2-cta-bg);
  text-align: center;
  line-height: 1.2;
}
.fa2-btn:hover {
  filter: brightness(1.08);
}

.fa2-badge {
  display: inline-block;
  padding: var(--fa2-space-2xs) var(--fa2-space-sm);
  border-radius: var(--fa2-badge-radius);
  font-size: var(--fa2-badge-font-size);
  font-weight: var(--fa2-badge-weight);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--fa2-badge-text);
  background-color: var(--fa2-badge-bg);
}

.fa2-header {
  font-size: var(--fa2-header-font-size);
  font-weight: var(--fa2-header-weight);
  margin: 0 0 var(--fa2-space-md);
}

.fa2-disclosure {
  font-size: var(--fa2-disclosure-font-size);
  color: var(--fa2-disclosure-color);
  margin: var(--fa2-space-md) 0 0;
}
.fa2-disclosure__link {
  font-size: var(--fa2-disclosure-link-font-size);
  color: var(--fa2-disclosure-link-color);
  text-decoration: underline;
}

/* hero ----------------------------------------------------------------- */

.fa2-hero {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-lg);
  border: 1px solid var(--fa2-color-border);
  border-radius: var(--fa2-radius-lg);
  padding: var(--fa2-space-lg);
}
.fa2-hero__media {
  position: relative;
}
.fa2-hero__image {
  width: 100%;
  border-radius: var(--fa2-radius);
}
.fa2-hero__media .fa2-badge {
  position: absolute;
  top: var(--fa2-space-sm);
  left: var(--fa2-space-sm);
}
.fa2-hero__title {
  font-size: var(--fa2-title-font-size-lg);
  margin: 0 0 var(--fa2-space-xs);
}
.fa2-hero__subtitle {
  margin: 0 0 var(--fa2-space-sm);
  color: var(--fa2-color-text-muted);
  font-size: var(--fa2-subtitle-font-size);
}
.fa2-hero__desc {
  margin: 0 0 var(--fa2-space-sm);
  color: var(--fa2-color-text-secondary);
  font-size: var(--fa2-desc-font-size);
}
/* SUPERSEDED 2026-09-13, kept because the history explains the shape.
 * Phase M, M1c (compat audit §E.3) found the CTA sitting inline right after
 * the price row, left-aligned with dead whitespace to the right, and fixed it
 * by giving the button its own block-level line and centring that line with
 * `margin: auto`. That removed the dead whitespace by spending a whole row on
 * the button, and centring it left the price stranded alone on the row above.
 *
 * James, 2026-09-13: "the buttons aren't really aligned with anything - 'View
 * Deal' is in the middle of an otherwise empty line, '$139.98' is on the left
 * of again an empty line." The row-sharing fix below answers E.3's original
 * complaint the other way: the two share one line, each pinned to an edge, so
 * there is no dead whitespace AND no spent row. */
.fa2-hero__action {
  margin-top: var(--fa2-space-sm);
}

@media (min-width: 640px) {
  .fa2-hero--pos-left {
    flex-direction: row;
  }
  .fa2-hero--pos-right {
    flex-direction: row-reverse;
  }
  .fa2-hero--pos-left .fa2-hero__media,
  .fa2-hero--pos-right .fa2-hero__media {
    flex: 0 0 45%;
  }
  .fa2-hero--pos-left .fa2-hero__body,
  .fa2-hero--pos-right .fa2-hero__body {
    flex: 1;
  }
}

/* vertical-stack --------------------------------------------------------- */

.fa2-vstack__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-sm);
}
.fa2-vstack__item {
  border: 1px solid var(--fa2-color-border);
  border-radius: var(--fa2-radius);
}
/* Phase M, M1c (compat audit §D.2 — confirmed v1 regression): a per-row CTA
 * button was added back. It's a SIBLING of the content link, not nested
 * inside it (nested <a> tags are invalid HTML and browsers reparent them
 * unpredictably) — `.fa2-vstack__row` is the flex row that holds both, so
 * "click anywhere on the row" still works via the content link while the
 * button gets its own explicit affordance. */
.fa2-vstack__row {
  display: flex;
  align-items: center;
  gap: var(--fa2-space-sm);
  padding: var(--fa2-space-md);
}
.fa2-vstack__link {
  display: flex;
  align-items: center;
  gap: var(--fa2-space-md);
  flex: 1;
  min-width: 0;
}
.fa2-vstack__cta {
  flex: 0 0 auto;
  padding: var(--fa2-cta-padding-sm);
  font-size: var(--fa2-cta-font-size-sm);
}
.fa2-vstack__image {
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  border-radius: var(--fa2-radius-sm);
  object-fit: cover;
}
.fa2-vstack__num {
  flex: 0 0 auto;
  width: 1.5rem;
  text-align: center;
  font-weight: 700;
  color: var(--fa2-color-text-muted);
}
.fa2-vstack__text {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-2xs);
  min-width: 0;
}
.fa2-vstack__title {
  font-weight: 600;
  font-size: var(--fa2-title-font-size);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fa2-vstack--compact .fa2-vstack__row {
  padding: var(--fa2-space-xs);
}
.fa2-vstack--compact .fa2-vstack__image {
  width: 48px;
  height: 48px;
}

@media (min-width: 640px) {
  .fa2-vstack__image {
    width: 96px;
    height: 96px;
  }
  .fa2-vstack__title {
    white-space: normal;
  }
}

/* basket-grid ------------------------------------------------------------ */

.fa2-grid__cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--fa2-space-lg);
  /* align-items:stretch is the grid default and is what makes every card in
   * a row equal-height (item 4) — left explicit so the intent survives a
   * future override that changes something else on this rule. */
  align-items: stretch;
}
.fa2-grid__card {
  border: 1px solid var(--fa2-color-border);
  border-radius: var(--fa2-radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.fa2-grid__image {
  width: 100%;
  aspect-ratio: 16 / 11;
  object-fit: cover;
}
.fa2-grid__body {
  padding: var(--fa2-space-md);
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-xs);
  flex: 1;
}
.fa2-grid__title {
  font-size: var(--fa2-title-font-size);
  font-weight: 700;
  margin: 0;
  line-height: var(--fa2-line-height);
  /* ⚠ TWO max-height DECLARATIONS ON PURPOSE — DO NOT COLLAPSE THEM.
   * Ruled by James 2026-09-14 after Prism reproduced the failure (flowads PR 153).
   *
   * The cap exists as the plain-text fallback for engines with no line-clamp
   * support. Derived from the TOKEN it can disagree with the line-height the
   * element actually uses — a host rule winning on specificity or !important
   * is enough — and then it clips partway through the last line and paints the
   * ellipsis BELOW the clip: hard-cut glyphs, no ellipsis, clamp present and
   * `display` still computing to flow-root. Prism's variant F, reproduced
   * deliberately: token 39.44px cap against a real 25.84px line-height.
   *
   * `1lh` IS the element's own computed line-height, so the second declaration
   * cannot disagree with it by construction. It is second because a browser
   * that does not understand `lh` drops it as invalid and keeps the first —
   * and that is exactly the engine class the fallback is FOR, so deriving must
   * not cost them the cap. Modern engines take the second and lose failure
   * mode F. Nothing here changes what a working clamp renders.
   */
  max-height: calc(var(--fa2-title-lines) * var(--fa2-line-height) * 1em);
  max-height: calc(var(--fa2-title-lines) * 1lh);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: var(--fa2-title-lines);
  -webkit-box-orient: vertical;
}
.fa2-grid__desc {
  font-size: var(--fa2-desc-font-size);
  color: var(--fa2-color-text-secondary);
  margin: 0;
  line-height: var(--fa2-line-height);
  max-height: calc(var(--fa2-desc-lines) * var(--fa2-line-height) * 1em);
  max-height: calc(var(--fa2-desc-lines) * 1lh);
  display: -webkit-box;
  -webkit-line-clamp: var(--fa2-desc-lines);
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.fa2-grid__card--minimal {
  border: none;
  border-bottom: 1px solid var(--fa2-color-border);
  border-radius: 0;
}
/* item 4: pin the CTA to the bottom of every card regardless of how much
 * title/description/meta content sits above it. .fa2-grid__body is a flex
 * column (above); margin-top:auto on the last flex child pushes everything
 * before it up and the button itself down to the shared baseline.
 * Phase M, M1c (compat audit §E.2): align-self was flex-start, smashing the
 * button against the card's left edge — changed to center. */
.fa2-grid__body > .fa2-action {
  margin-top: auto;
}

@media (min-width: 560px) {
  .fa2-grid__cards {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 900px) {
  .fa2-grid__cards {
    grid-template-columns: repeat(var(--fa2-cols, 3), 1fr);
  }
}

/* basket-banner ------------------------------------------------------------ */

.fa2-banner {
  border: 2px solid var(--fa2-accent);
  border-radius: var(--fa2-radius-lg);
  padding: var(--fa2-space-lg);
  position: relative;
}
.fa2-banner__badge {
  display: inline-block;
  background: var(--fa2-accent);
  color: var(--fa2-badge-text);
  font-weight: var(--fa2-badge-weight);
  font-size: var(--fa2-badge-font-size);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: var(--fa2-space-2xs) var(--fa2-space-md);
  border-radius: 999px;
  margin-bottom: var(--fa2-space-md);
}

/* Multi-item (2-3) layout — a row of equal-height cards. */
.fa2-banner__cards {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-lg);
  align-items: stretch;
}
.fa2-banner__card {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--fa2-space-sm);
}
.fa2-banner__image {
  width: 140px;
  height: 140px;
  object-fit: cover;
  border-radius: var(--fa2-radius);
}
.fa2-banner__title {
  font-size: var(--fa2-title-font-size);
  font-weight: 700;
  margin: 0;
  line-height: var(--fa2-line-height);
  max-height: calc(var(--fa2-title-lines) * var(--fa2-line-height) * 1em);
  max-height: calc(var(--fa2-title-lines) * 1lh);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: var(--fa2-title-lines);
  -webkit-box-orient: vertical;
}
.fa2-banner__card > .fa2-action {
  margin-top: auto;
}

@media (min-width: 640px) {
  .fa2-banner__cards {
    flex-direction: row;
  }
}

/* Single-item (item 5) layout — the redesign. Full-height image (cover),
 * roughly 60% width on desktop, text block filling the rest — replaces the
 * old "one centered card in a wide empty banner" look. */
.fa2-banner--single .fa2-banner__hero {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-lg);
}
.fa2-banner__hero-media {
  flex: 0 0 auto;
}
.fa2-banner__hero-image {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: var(--fa2-radius);
}
.fa2-banner__hero-body {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-xs);
}
.fa2-banner__hero-title {
  font-size: var(--fa2-title-font-size-lg);
  margin: 0;
}
.fa2-banner__hero-desc {
  font-size: var(--fa2-desc-font-size);
  color: var(--fa2-color-text-secondary);
  margin: 0;
  line-height: var(--fa2-line-height);
  max-height: calc(var(--fa2-desc-lines) * var(--fa2-line-height) * 1em);
  max-height: calc(var(--fa2-desc-lines) * 1lh);
  display: -webkit-box;
  -webkit-line-clamp: var(--fa2-desc-lines);
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Phase M, M1c (compat audit §E.2): align-self was flex-start; centered. */
.fa2-banner__hero-cta {
  margin-top: var(--fa2-space-xs);
  align-self: center;
}

@media (min-width: 640px) {
  .fa2-banner--single .fa2-banner__hero {
    flex-direction: row;
    align-items: stretch;
  }
  .fa2-banner__hero-media {
    flex: 0 0 var(--fa2-banner-hero-media-width);
    align-self: stretch;
  }
  .fa2-banner__hero-image {
    height: 100%;
    aspect-ratio: auto;
  }
  .fa2-banner__hero-body {
    flex: 1;
    justify-content: center;
  }
}

/* basket-stacked ------------------------------------------------------------ */

.fa2-stacked__list {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-lg);
}
.fa2-stacked__item {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-sm);
  border: 1px solid var(--fa2-color-border);
  border-radius: var(--fa2-radius);
  padding: var(--fa2-space-md);
  position: relative;
}
.fa2-stacked__num {
  position: absolute;
  top: var(--fa2-space-sm);
  left: var(--fa2-space-sm);
  width: 1.6rem;
  height: 1.6rem;
  border-radius: 50%;
  background: var(--fa2-num-bg);
  color: var(--fa2-num-text);
  font-weight: 700;
  font-size: var(--fa2-num-font-size);
  display: flex;
  align-items: center;
  justify-content: center;
}
.fa2-stacked__image {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--fa2-radius-sm);
}
.fa2-stacked__title {
  font-size: var(--fa2-title-font-size);
  font-weight: 700;
  margin: 0;
  line-height: var(--fa2-line-height);
  max-height: calc(var(--fa2-title-lines) * var(--fa2-line-height) * 1em);
  max-height: calc(var(--fa2-title-lines) * 1lh);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: var(--fa2-title-lines);
  -webkit-box-orient: vertical;
}
.fa2-stacked__desc {
  font-size: var(--fa2-desc-font-size);
  color: var(--fa2-color-text-secondary);
  margin: 0;
  line-height: var(--fa2-line-height);
  max-height: calc(var(--fa2-desc-lines) * var(--fa2-line-height) * 1em);
  max-height: calc(var(--fa2-desc-lines) * 1lh);
  display: -webkit-box;
  -webkit-line-clamp: var(--fa2-desc-lines);
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.fa2-stacked--compact .fa2-stacked__item {
  padding: var(--fa2-space-sm);
  gap: var(--fa2-space-xs);
}
.fa2-stacked__body {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-xs);
}
/* Phase M, M1c (compat audit §E.2) centred this button; 2026-09-13 it became
 * an action row that spans the body, so there is no intrinsic-width box left
 * to centre - the price and the button align to opposite edges instead. */
.fa2-stacked__body > .fa2-action {
  margin-top: auto;
}

@media (min-width: 640px) {
  .fa2-stacked__item {
    flex-direction: row;
    align-items: stretch;
  }
  /* Review finding 3, measured: image heights 150px vs 189px in one fixture,
   * a 39px spread, because the row above sets `align-items: stretch` and the
   * image grows to fill whichever card is taller. `aspect-ratio: 4 / 3` cannot
   * win against stretch, and `flex: 0 0 auto` - already here, and proposed as
   * half the fix - governs the MAIN axis (width) only, which in a row is not
   * the axis doing the stretching. `align-self: flex-start` is the whole fix:
   * it opts this one child out of the stretch so the aspect ratio decides the
   * height again. potm already uses exactly this pattern. */
  .fa2-stacked__image {
    width: 200px;
    flex: 0 0 auto;
    align-self: flex-start;
  }
  .fa2-stacked__body {
    flex: 1;
  }
}

/* potm — Product of the Month ----------------------------------------------
 * Mobile-first like everything here: the base rules are the stacked phone
 * layout and the min-width query turns it into the side-by-side unit. */

.fa2-potm__card {
  border: 1px solid var(--fa2-color-border);
  border-radius: var(--fa2-radius);
  overflow: hidden;
  background: var(--fa2-color-surface);
}
.fa2-potm__hero {
  display: flex;
  flex-direction: column;
  position: relative;                  /* the stretched link's containing block */
}
.fa2-potm__media { position: relative; flex: 0 0 auto; width: 100%; }
.fa2-potm__image { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block; }
/* A square or near-square shot (item-helpers.js imageFitClass reads the row's
 * real image_width/image_height) gets a SQUARE frame and is contained, so a
 * product keeps its legs. A landscape shot — a resort, a cruise, a tour — has
 * no class and fills the 16:10 frame the way it was always going to. */
.fa2-potm__image.fa2-image--contain { aspect-ratio: 1 / 1; }
.fa2-potm__badge { position: absolute; top: var(--fa2-space-sm); left: var(--fa2-space-sm); z-index: 2; }

.fa2-potm__body {
  flex: 1;
  min-width: 0;
  padding: var(--fa2-space-lg);
  display: flex;
  flex-direction: column;
}
.fa2-potm__title {
  font-size: var(--fa2-title-font-size-lg);
  font-weight: 700;
  margin: 0 0 var(--fa2-space-xs);
  line-height: var(--fa2-line-height);
}
.fa2-potm__why {
  font-size: var(--fa2-desc-font-size);
  color: var(--fa2-color-text-secondary);
  margin: 0 0 var(--fa2-space-md);
  border-left: 3px solid var(--fa2-accent);
  padding-left: var(--fa2-space-sm);
}
/* Price LEFT, button RIGHT, one row, pinned to the foot of the column so the
 * CTA lines up with the bottom of the image. James, 2026-09-13: the button was
 * centred on an otherwise empty line by auto margins and the price sat alone
 * on another, which cost a whole row and gave the button nothing to align to. */
.fa2-potm__action {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--fa2-space-md);
  flex-wrap: wrap;
  margin-top: auto;
}
/* THE WHOLE HERO ROW IS THE CLICK TARGET (James: the button is a magnet, not
 * the sole hotspot). A stretched link rather than a wrapping anchor: one <a>,
 * no nesting, no script, so C-1 still holds. */
.fa2-potm__cta { position: static; }
.fa2-potm__cta::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.fa2-potm__hero:hover { background: var(--fa2-potm-hover-bg); }

/* ═══════════════════════════════════════════════════════════════════════════
 * THE WHOLE AD IS THE CLICK TARGET - the same stretched link, generalised
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * James, 2026-09-14: "it is essential that the click area is the entire area
 * of the ad - not just the 'learn more' / 'shop now' button."
 *
 * MEASURED BEFORE THIS EXISTED (elementFromPoint at three points inside every
 * card, each scrolled into view first so the probe was valid): the centre of a
 * card was DEAD in hero, basket-grid, basket-banner and basket-stacked.
 * vertical-stack and potm were the only two that already worked - which is why
 * the pattern above is the model rather than an invention.
 *
 * The ratio is the argument: a 123px button inside a 191px-tall stacked card is
 * a small fraction of the unit a reader can actually act on.
 *
 * ── WHY A STRETCHED LINK AND NOT A WRAPPING ANCHOR ─────────────────────────
 * Wrapping the card in <a> would nest anchors wherever a card already carries
 * one (hero-card has four), which is invalid HTML and behaves unpredictably.
 * The ::after keeps ONE anchor per target, needs no script, and leaves the
 * markup alone - so this is a pure CSS change and C-1 still holds.
 *
 * ── THE ESCAPE HATCH, WHICH IS THE PART THAT IS EASY TO GET WRONG ──────────
 * An overlay at inset:0 covers EVERYTHING in the container, including other
 * links. Anything that must stay separately clickable has to be lifted above
 * it - that is the .fa2-escape-hatch group at the end of this section, and the
 * disclosure corner-link is the live case: it sits inside the hero card, and
 * without the lift a reader could not reach the advertiser disclosure. That
 * would be a compliance problem, not a cosmetic one.
 */
.fa2-hero,
.fa2-grid__card,
.fa2-banner__card,
.fa2-stacked__item {
  position: relative;
}

/* ⚠ hero-card IS SCOPED TO ITS BODY, NOT THE WHOLE CARD, AND THAT IS LOAD
 * BEARING. Measured structure: the block's children are the badge, the image
 * link, the body, the <details> alternatives drawer and the disclosure - the
 * drawer is a SIBLING of the body, not inside it. Stretching over the whole
 * card would therefore cover the drawer's <summary> and make it impossible to
 * open, turning a working feature into a dead one to gain area that the image
 * link already provides. Scoped to the body, the overlay covers the title and
 * the action strip - the parts that were dead - while the image link, the
 * drawer and the disclosure keep their own hit boxes. */
.fa2-herocard__body {
  position: relative;
}

.fa2-hero__cta,
.fa2-grid__action .fa2-btn,
.fa2-banner__action .fa2-btn,
.fa2-banner__hero-cta,
.fa2-stacked__action .fa2-btn,
.fa2-herocard__cta {
  position: static;
}

.fa2-hero__cta::after,
.fa2-grid__action .fa2-btn::after,
.fa2-banner__action .fa2-btn::after,
.fa2-banner__hero-cta::after,
.fa2-stacked__action .fa2-btn::after,
.fa2-herocard__cta::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* The card reads as one object on hover, so the enlarged target is visible
 * rather than merely present. Keyboard focus lands on the real anchor, which
 * is unmoved - focus-within carries the same cue to it.
 *
 * ⚠ THIS RULE SHIPPED DOING NOTHING. `--fa2-color-surface-alt` was never
 * declared anywhere, so the var() fell through to its inline fallback of
 * rgba(0,0,0,0.02) - 2% black on white. James, 2026-09-14: "it appears to be a
 * patchwork with some items able to click and some not." The hit box measured
 * complete (36 of 36 points per card); it was the AFFORDANCE that was missing,
 * and an invisible hover is indistinguishable from a dead card.
 *
 * The token is now real (see the :root theme block), so the fallback is gone -
 * a fallback that is the only thing ever used is a default in hiding. The inset
 * outline is what makes the tint legible on a light card at a glance. */
.fa2-grid__card:hover,
.fa2-stacked__item:hover,
.fa2-banner__card:hover,
.fa2-grid__card:focus-within,
.fa2-stacked__item:focus-within,
.fa2-banner__card:focus-within {
  background: var(--fa2-color-surface-alt);
  box-shadow: inset 0 0 0 1px var(--fa2-color-border);
}

/* ⚠ ABOVE THE OVERLAY. Every interactive thing that is NOT the card's own
 * destination lives here, or the stretched link swallows it. */
.fa2-disclosure,
.fa2-disclosure__link,
.fa2-herocard__alts,
.fa2-herocard__alts-toggle,
.fa2-herocard__alt,
.fa2-herocard__secondary,
.fa2-potm__peer {
  position: relative;
  z-index: 2;
}

.fa2-potm__peers {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--fa2-space-sm);
  border-top: 1px solid var(--fa2-color-border);
  padding: var(--fa2-space-md) var(--fa2-space-lg);
}
/* Each prior month is ONE rectangle, and the hit box is deliberately larger
 * than its ink: interior padding with an equal negative margin, so the target
 * grows without the layout moving. */
.fa2-potm__peer {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-2xs);
  min-width: 0;
  padding: var(--fa2-space-xs);
  margin: calc(var(--fa2-space-xs) * -1);
  border-radius: var(--fa2-radius-sm);
}
.fa2-potm__peer:hover { background: var(--fa2-potm-hover-bg); }
.fa2-potm__peer-image {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--fa2-radius-sm);
  display: block;
}
.fa2-potm__peer-image.fa2-image--contain { object-fit: contain; }
.fa2-potm__month {
  font-size: var(--fa2-potm-month-font-size);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fa2-num-bg);
  margin-top: var(--fa2-space-2xs);
}
.fa2-potm__peer-title {
  font-size: var(--fa2-potm-peer-title-font-size);
  font-weight: 600;
  line-height: var(--fa2-line-height);
  display: -webkit-box;
  -webkit-line-clamp: var(--fa2-title-lines);
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* ⚠ THE PAIR, ADDED 2026-09-14 - potm shipped with NO cap at all, which
   * Prism flagged as unowned. Immune to failure mode F by construction, but
   * also missing the plain-text fallback the other six clamped rules carry:
   * an engine without line-clamp support rendered this title at full length.
   *
   * James's ruling was DERIVE rather than DROP, on the grounds that the
   * fallback is real behaviour for a real engine class. That reasoning applies
   * to adding one as much as to keeping one, so potm gets the same shape: the
   * token expression first for engines that do not understand `lh`, and
   * `calc(N * 1lh)` second, which cannot disagree with the element's own
   * line-height and so cannot reproduce F on anything modern. */
  max-height: calc(var(--fa2-title-lines) * var(--fa2-line-height) * 1em);
  max-height: calc(var(--fa2-title-lines) * 1lh);
}
.fa2-potm__peer-price {
  font-size: var(--fa2-potm-peer-title-font-size);
  font-weight: var(--fa2-price-weight);
  color: var(--fa2-color-price);
}

@media (min-width: 620px) {
  /* The row stretches so the copy column matches the image, and the media opts
   * OUT with align-self — the one declaration that stops a flex image growing
   * to fill a taller row. */
  .fa2-potm__hero { flex-direction: row; align-items: stretch; }
  .fa2-potm__media { align-self: flex-start; }
  .fa2-potm--s .fa2-potm__media { width: var(--fa2-potm-media-sm); }
  .fa2-potm--m .fa2-potm__media { width: var(--fa2-potm-media-md); }
  .fa2-potm--l .fa2-potm__media { width: var(--fa2-potm-media-lg); }
}

/* sponsor chrome (Phase M, M1b) --------------------------------------------
 * A wrapper OPTION composing with any template's output, never a template
 * of its own (Piece 7) — see lib/sponsorChrome.js. Appended as a footer
 * line below whatever the inner template rendered, mirroring v1's exact
 * basket sponsor-footer placement (compat audit §B: sponsor footer, THEN
 * disclosure). */
.fa2-sponsor-wrap {
  display: block;
}
.fa2-sponsor-line {
  margin: var(--fa2-space-sm) 0 0;
  font-size: var(--fa2-disclosure-font-size);
  color: var(--fa2-scheme-surface-on-muted);
  text-align: center;
}
.fa2-sponsor-line__link {
  color: var(--fa2-scheme-surface-on);
  font-weight: 600;
  text-decoration: underline;
}

/* hero-card (Phase M, M1a) --------------------------------------------------
 * v1's most-used legacy template (47 live uses) — a STACKED card (badge ->
 * image -> body -> alternatives -> disclosure), distinct from `hero`'s
 * side-by-side image/body layout, matching v1's actual generateHeroCardHTML
 * shape (app.js) rather than reusing hero's. The alternatives drawer is a
 * native <details>/<summary> — no script tags anywhere (C-1), the browser's
 * built-in disclosure behavior replaces v1's JS toggle handler. */
.fa2-herocard {
  border: 1px solid var(--fa2-color-border);
  border-radius: var(--fa2-radius-lg);
  overflow: hidden;
}
.fa2-herocard__badge {
  display: block;
  border-radius: 0;
  text-align: center;
  padding: var(--fa2-space-sm) var(--fa2-space-md);
}
.fa2-herocard__image-link {
  display: block;
}
.fa2-herocard__image {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}
.fa2-herocard__body {
  padding: var(--fa2-space-lg);
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-xs);
}
.fa2-herocard__title-link {
  display: block;
}
.fa2-herocard__title {
  font-size: var(--fa2-title-font-size-lg);
  margin: 0;
}
.fa2-herocard__quote {
  margin: 0;
  padding: var(--fa2-space-sm) var(--fa2-space-md);
  border-left: 3px solid var(--fa2-accent);
  font-style: italic;
  color: var(--fa2-color-text-secondary);
  font-size: var(--fa2-desc-font-size);
}
/* The primary button and the secondary link travel together as ONE right-hand
 * group inside .fa2-herocard__action, so `justify-content` here is about that
 * pair, not about the card. Centred became flex-end when the group stopped
 * owning the whole line (2026-09-13). */
.fa2-herocard__ctas {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--fa2-space-sm);
}
.fa2-herocard__action {
  margin-top: var(--fa2-space-xs);
}
.fa2-herocard__secondary {
  font-size: var(--fa2-cta-font-size-sm);
  font-weight: 600;
  color: var(--fa2-scheme-surface-on);
  text-decoration: underline;
}
.fa2-herocard__alts {
  border-top: 1px solid var(--fa2-color-border);
  padding: var(--fa2-space-md) var(--fa2-space-lg);
}
.fa2-herocard__alts-toggle {
  cursor: pointer;
  font-weight: 600;
  font-size: var(--fa2-subtitle-font-size);
  color: var(--fa2-scheme-surface-on);
}
.fa2-herocard__alts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--fa2-space-sm);
  margin-top: var(--fa2-space-sm);
}
.fa2-herocard__alt {
  display: flex;
  flex-direction: column;
  gap: var(--fa2-space-2xs);
  border: 1px solid var(--fa2-color-border);
  border-radius: var(--fa2-radius-sm);
  padding: var(--fa2-space-xs);
}
.fa2-herocard__alt-image {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--fa2-radius-sm);
}
.fa2-herocard__alt-title {
  font-size: var(--fa2-desc-font-size);
  font-weight: 600;
  line-height: var(--fa2-line-height);
  max-height: calc(2 * var(--fa2-line-height) * 1em);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.fa2-herocard__alt-price {
  font-size: var(--fa2-price-was-font-size);
  font-weight: 700;
  color: var(--fa2-color-price);
}

/* ---------------------------------------------------------------------------
 * Atlas 2026-07-12 — two live-verified fixes from the MWB canary.
 * Appended at the end deliberately: both rules must win on source order over
 * the component rules above (identical specificity), without !important.
 * ------------------------------------------------------------------------- */

/* 1. Square/portrait source images must not be cropped into a landscape slot.
 *    Amazon product shots are 1500x1500; the hero-card slot is 16:10 with
 *    object-fit: cover, which lopped the top and bottom off the product.
 *    imageFitClass() (item-helpers.js) stamps this class from the row's stored
 *    image_width/image_height, so genuinely-landscape art (Viator 720x480) is
 *    untouched and keeps filling the slot edge to edge. */
.fa2-image--contain {
  object-fit: contain;
  background: var(--fa2-scheme-surface-bg);
}

/* 2. The disclosure ("Sponsored") is a <p>, so a host site's own paragraph rule
 *    (e.g. `.article p { margin-bottom: 1.5rem }`, specificity 0,2,0) outranks
 *    `.fa2-disclosure` (0,1,0) and adds a phantom gap under every ad block.
 *    Seen live on MenWhoBlog. Match that specificity from inside our own block
 *    so the host template cannot reintroduce the space. */
.fa2-block .fa2-disclosure {
  margin: var(--fa2-space-md) 0 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * SURVIVING THE HOST PAGE — the block styles itself, it does not ask nicely
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * James, 2026-09-14, on a live MT page: "various sized text ... the template is
 * wonky". MEASURED on that page against the same template rendered straight
 * from the ads server, which was correct:
 *
 *   31 <p> on the page, exactly ONE still carried a class
 *   34 self-closing " />" tags on the page, ZERO in the served markup
 *
 * A Joomla content plugin parses the article body into a DOM and re-serializes
 * it AFTER FlowAds has injected the block, and rebuilding a paragraph drops its
 * attributes. So `fa2-stacked__desc` / `fa2-grid__desc` never reached the
 * browser, every description fell back to the host's `p` rule at 16px, and the
 * 2-line clamp went with it - which is why one card ran three lines and the
 * rest ran two. The ad copy itself was never the problem.
 *
 * ── THE RULE THIS SECTION ENCODES ──────────────────────────────────────────
 *
 * A unit rendered into someone else's page cannot depend on its own class
 * attributes surviving, and it cannot depend on INHERITANCE at all: a host rule
 * on a bare element (`h3 { color: #4d4d4d }`) beats an inherited value no
 * matter how specific the ancestor that set it. Both failures are silent and
 * both look like a template bug.
 *
 * So, twice over:
 *   1. every description is selected STRUCTURALLY as well as by class, so
 *      attribute stripping cannot unstyle it;
 *   2. every text element declares the properties a host might set on the bare
 *      element - size, weight, colour - rather than inheriting them, at
 *      `.fa2-block`-scoped specificity so a host content-area rule loses.
 *
 * This is a safety net, not a licence: the render modules still emit the
 * classes, and whatever is rewriting those paragraphs is still a bug worth
 * fixing on its own side (it is also injecting interlinks into ad copy).
 */

/* 1. DESCRIPTIONS — by class AND by position. The only <p> inside a card body
 *    is the description; the `Sponsored` disclosure is a direct child of
 *    .fa2-block and so is untouched by these. */
.fa2-grid__desc,
.fa2-block .fa2-grid__body > p,
.fa2-stacked__desc,
.fa2-block .fa2-stacked__body > p,
.fa2-banner__hero-desc,
.fa2-block .fa2-banner__hero-body > p {
  font-size: var(--fa2-desc-font-size);
  font-weight: 400;
  color: var(--fa2-color-text-secondary);
  margin: 0;
  line-height: var(--fa2-line-height);
  max-height: calc(var(--fa2-desc-lines) * var(--fa2-line-height) * 1em);
  max-height: calc(var(--fa2-desc-lines) * 1lh);
  display: -webkit-box;
  -webkit-line-clamp: var(--fa2-desc-lines);
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* THE DISCLOSURE, WHEN ITS CLASS DOES NOT SURVIVE. James, 2026-09-14: "the size
 * / representation needs to be consistent across the different ad units."
 *
 * It is one rule in one place (.fa2-disclosure, above) so it SHOULD already be
 * consistent - and it is inconsistent for the same reason everything else on
 * this page was: it renders as <p class="fa2-disclosure"> and the host rewrites
 * paragraphs. Where the class survives it is 0.72rem and muted; where it does
 * not it inherits the host's body copy, so two units side by side disagree.
 *
 * Templates put it in one of two places, which is why one selector cannot cover
 * it: grid/stacked/potm/banner/vstack make it a DIRECT child of the block, hero
 * nests it inside .fa2-hero__body (where the description rule above already
 * catches it at a near-identical size, which is why hero needs nothing here).
 *
 * ⚠ .fa2-block > p IS SAFE ONLY BECAUSE THE DISCLOSURE IS THE ONLY DIRECT <p>
 * CHILD ANY TEMPLATE EMITS - headers are <h2>, card bodies are nested. A
 * template that later adds a top-level paragraph would pick this up; if that
 * happens, give it a class and exclude it here rather than loosening the rule. */
.fa2-block > p {
  font-size: var(--fa2-disclosure-font-size);
  color: var(--fa2-disclosure-color);
}

/* ⚠ potm's `__why` IS THE ONLY PROSE THAT TEMPLATE HAS. potm.js renders the
 * hero's image, title, price and button, and no item description at all - the
 * single line of copy it can show is `params.quote`, in a <p class="fa2-potm__why">.
 * Missed in the first pass of this section, and it is the one that matters
 * most: on a live MT page `fa2-potm__why` was the ONLY fa2-potm__* class absent
 * from the markup, because the host rewrites paragraphs and that is the only
 * potm element that IS one. The text arrived; the accent rule and the quiet
 * colour did not, so the curated line read as an ordinary body paragraph. */
.fa2-potm__why,
.fa2-block .fa2-potm__body > p {
  font-size: var(--fa2-desc-font-size);
  font-weight: 400;
  color: var(--fa2-color-text-secondary);
  margin: 0 0 var(--fa2-space-md);
  line-height: var(--fa2-line-height);
  border-left: 3px solid var(--fa2-accent);
  padding-left: var(--fa2-space-sm);
}

/* Hero's description is not clamped (it is the one unit with room for it), so
 * it keeps its own shape and only gains the structural selector. */
.fa2-hero__desc,
.fa2-block .fa2-hero__body > p {
  font-size: var(--fa2-desc-font-size);
  font-weight: 400;
  color: var(--fa2-color-text-secondary);
  margin: 0 0 var(--fa2-space-sm);
  line-height: var(--fa2-line-height);
}

/* 2. TITLES — colour and weight declared, not inherited.
 *    Measured on MT: the card title rendered 15.68px/700/#4d4d4d against a
 *    description at 16px/400/#1f2937, so the headline was SMALLER AND LIGHTER
 *    than the body copy beneath it. The size was ours and correct; the colour
 *    came from the host's `h3` rule, which inheritance cannot defend against. */
.fa2-block .fa2-grid__title,
.fa2-block .fa2-stacked__title,
.fa2-block .fa2-vstack__title,
.fa2-block .fa2-banner__title,
.fa2-block .fa2-potm__title,
.fa2-block .fa2-herocard__title {
  color: var(--fa2-color-text);
  font-weight: 700;
  font-family: inherit;
}
.fa2-block .fa2-grid__title,
.fa2-block .fa2-stacked__title,
.fa2-block .fa2-vstack__title {
  font-size: var(--fa2-title-font-size);
}
.fa2-block .fa2-hero__title,
.fa2-block .fa2-banner__hero-title {
  color: var(--fa2-color-text);
  font-weight: 700;
  font-family: inherit;
}

/* 3. THE CARD LOOKS CLICKABLE.
 *    The hit box was never the problem: 36 of 36 sample points inside every
 *    card on that MT page resolved to the card's own link, zero dead. The
 *    affordance was. The dead hover rule and the token that fixes it live with
 *    the click-target section above; what is left here is the shape the tint
 *    needs to read as one object, and the cursor. */
.fa2-grid__card,
.fa2-stacked__item,
.fa2-banner__card {
  border-radius: var(--fa2-radius-sm);
  transition: background-color 120ms ease, box-shadow 120ms ease;
}
/* The cursor is the other half of the signal. The stretched link's ::after is
 * part of an <a>, so it already carries pointer - but the CARD is what the
 * reader is over, and a host rule on the elements underneath can set their own
 * cursor. Stating it on the card settles it for the whole rectangle. */
.fa2-grid__card,
.fa2-stacked__item,
.fa2-banner__card,
.fa2-grid__card *,
.fa2-stacked__item *,
.fa2-banner__card * {
  cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
  .fa2-grid__card,
  .fa2-stacked__item,
  .fa2-banner__card {
    transition: none;
  }
}
