/* ============================================================================
 * Listing detail — v4 shared shell
 * Ported from Sigrix_new/marketplace/Marketplace - Listing Image Prompt v4.html
 * and Sigrix_new/marketplace/Marketplace - Listing Skill v4.html, whose CSS is
 * byte-identical across the two files for everything in this sheet. The design
 * ships one shell and a per-type variant block; this is the shell.
 *
 * Every rule here is scoped under `body.listing-v4-page`, which each v4 detail
 * template carries alongside its own type class. A type variant sheet
 * (`sx-listing-image-prompt.css`, `sx-listing-skill.css`) loads after this one
 * and owns two things only: its accent tokens, and the markup no other type
 * renders.
 *
 * A variant's own token block is written `body.listing-v4-page.<type>-page`
 * — (0,2,1) against this sheet's (0,1,1) — so it wins on specificity rather
 * than on load order. CLAUDE.md's admin.css note is the reason: a same-
 * specificity override that happens to load later is a rule nobody can read
 * off either file.
 *
 * This sheet deliberately does NOT re-scope the whole --sx-* token set onto the
 * body the way persona/skill/agent/mcp v2 pages do. Those pages have to re-flip
 * four tokens the global dark rule skips (see CLAUDE.md), and every one of them
 * has shipped that gap at least once. Reading the shared tokens straight off
 * `.sx` — which base.html already puts on <body> and sx-home.css already flips
 * for dark — removes that whole class of bug. Only the page's own --ig-*
 * accents are declared here, and the dark block below re-flips every one of
 * them.
 * ==========================================================================*/

body.listing-v4-page {
  /* Release the shell's content wrapper. base.html wraps its content block in
     `.container`, which is `max-width: var(--container-max)` = 1200px. The
     prototype has no such wrapper — its `.ig-main` is full-bleed and
     `.ig-container` (1280px) is what centres the content — so on this page the
     wrapper did two wrong things at once: it squeezed every section 40px
     narrower per side than the design draws them, and it clipped the hero's
     radial glow to that column, painting it as a tinted rectangle with hard
     vertical edges against the page either side.

     `--container-max` is the design system's own knob and `.container` is its
     only reader, so releasing it here is one inherited token rather than a
     specificity fight with a shared selector. Safe on this page specifically
     because every top-level block under `main.ig-page` supplies its own
     `.ig-container` — the hero, the single wrapper `.ig-main` puts around all
     five numbered sections, and the sticky bar — so nothing goes full-bleed
     except the backgrounds that are meant to. The nav and footer are outside
     this wrapper in base.html and are unaffected. */
  --container-max: none;

  /* Graphic prompts read as the teal/persona accent in the prototype, and this
     is the shell's default for any v4 type that declares no accent of its own.

     It used to be `var(--sx-accent)`, which looks like the tidy answer — one
     shared token, theme-flipped for free, no fifth hardcoded palette. It was
     the wrong one, because `--sx-accent` is a **fill**: `#14B8A6` measures
     2.49:1 on white and 2.19:1 on its own 14% tint, and the shell writes
     `--ig-accent` as *text* in seven places (`.ig-kind`, `.ig-sig .ico`,
     `.ig-editable`, `.ig-varnote code`, `.ig-open-note code`, and the graphic
     variant's `.ig-pre .v` / `.ig-card-cap .k`). `.ig-editable` is the worst
     of them — accent ink on an accent-soft background, so it read at the 2.19
     figure. That is CLAUDE.md's "a fill is not an ink" rule one level up from
     a literal: a token can be sound in every use it was written for and unsound
     the moment a new consumer paints it as text.

     So the pair is declared here instead. Light is the same hue (173°) taken
     down to the lightest value that clears AA on every light surface a chip can
     land on — 5.96:1 on `--sx-surface`, 5.72 on `--sx-bg`, 5.62 on
     `--sx-surface-2`, and 4.87 / 4.69 / 4.60 on its own tint over each. Dark is
     byte-identical to what `--sx-accent` already resolved to, so that theme does
     not move at all. Measured, not guessed — see the contrast test.

     `--ig-accent-soft` / `-line` derive from `var(--ig-accent)` rather than from
     `--sx-accent` so the tint tracks the ink; per the `--pp-accent` note in
     CLAUDE.md, that `var()` re-resolves against whichever value wins on this
     same element, so the dark block below needs no copy of them. */
  --ig-accent: #0c7065;
  --ig-accent-soft: color-mix(in srgb, var(--ig-accent) 14%, transparent);
  --ig-accent-line: color-mix(in srgb, var(--ig-accent) 34%, transparent);
  --ig-accent-ink: #06413a;
  --ig-brand: var(--sx-brand-3, #3d4dff);
  --ig-brand-soft: color-mix(in srgb, var(--sx-brand-3, #3d4dff) 12%, transparent);
  --ig-brand-line: color-mix(in srgb, var(--sx-brand-3, #3d4dff) 40%, transparent);
  --ig-ok: var(--sx-ok, #1f9d55);
  --ig-ok-soft: color-mix(in srgb, var(--sx-ok, #1f9d55) 14%, transparent);
  --ig-ok-line: color-mix(in srgb, var(--sx-ok, #1f9d55) 38%, transparent);
  --ig-warn: var(--sx-warn, #c77800);
  --ig-warn-soft: color-mix(in srgb, var(--sx-warn, #c77800) 12%, transparent);
  /* The ink counterparts. `--ig-ok` / `--ig-warn` above are **fills**, and the
     shell paints them as *text* in five places — the grade chip, the warn
     verdict, the assurance ticks, the "you own this" label and the review-done
     glyph — all of which measured 2.58:1 to 3.29:1 in light mode: present,
     correctly styled, unreadable. Dark was fine throughout, which is the whole
     reason it looked sound for as long as it did.

     Exactly the split `--sx-brand-2` / `--sx-brand-text` already draws in
     base.html, and the same trap #5213 fixed one token over for `--ig-accent`.
     The rule these encode: **a fill is never a `color:`** — the contract test
     enforces that rather than trusting the next reader to notice.

     Solved against every background each is painted on, not just white: the
     plain surfaces, `--ig-ok-soft`'s 14%, `.rv-done .ico`'s 22%, and the 12/13%
     tint the badge chips mix from the ink itself. The 22% case is what makes
     this darker than the badge-only pair it replaces. Measured, not guessed. */
  --ig-ok-text: #16703c;
  --ig-warn-text: #925800;
  --ig-shot-badge-bg: rgba(8, 10, 16, 0.62);
  --ig-shot-badge-line: rgba(255, 255, 255, 0.18);
  --ig-shot-badge-ink: #ffffff;
  --ig-hero-glow-a: color-mix(in srgb, var(--sx-brand-3, #3d4dff) 10%, transparent);
  --ig-hero-glow-b: color-mix(in srgb, var(--sx-accent, #14b8a6) 8%, transparent);
  --ig-lock-wash: linear-gradient(
    160deg,
    color-mix(in srgb, var(--sx-brand-3, #3d4dff) 12%, var(--sx-surface-2, #f7f8fb)) 0%,
    var(--sx-surface-2, #f7f8fb) 58%
  );
  /* Lightbox chrome. The scrim is dark in *both* themes on purpose — it is
     the dark room a picture is looked at in, not a surface that follows the
     page — so the ink over it is white in both and neither is a theme bug.
     They are declared per theme all the same, because the contract test
     (tests/test_image_prompt_listing_v4_page.py) reads a literal in the light
     block as a token that must be restated, and an exemption for "this one is
     meant to be the same" is a hole the next literal walks through. */
  --ig-lbx-scrim: rgba(9, 11, 18, 0.82);
  --ig-lbx-ink: #ffffff;
  --ig-lbx-chip-bg: rgba(255, 255, 255, 0.12);
  --ig-lbx-chip-line: rgba(255, 255, 255, 0.22);
  --ig-mono: var(--sx-font-mono, 'JetBrains Mono', ui-monospace, monospace);
  --ig-display: var(--sx-font-display, 'Space Grotesk', 'Geist', ui-sans-serif, system-ui, sans-serif);
}

/* Dark re-flip. Every --ig-* token declared above appears here; the ones built
   with color-mix() re-resolve for free because --sx-accent / --sx-brand-3 are
   themselves flipped on `[data-theme="dark"] .sx`, so only the tokens carrying
   a literal need restating. `--ig-accent-ink` is one of those: it is ink *on*
   the accent, so it has to darken as the accent brightens. */
[data-theme="dark"] body.listing-v4-page {
  /* Exactly what `var(--sx-accent)` resolved to here before the light value was
     derived, so dark does not move. It passes comfortably as ink already —
     11.69:1 on the surface, 8.52:1 on its own tint — which is the whole reason
     the old shared-token spelling looked fine for as long as it did: only one
     of the two themes was ever wrong. */
  --ig-accent: #7ce3d4;
  --ig-accent-ink: #04211d;
  /* What `--sx-ok` / `--sx-warn` already resolve to here, restated so the pair
     is declared once per theme rather than half-derived — the light values are
     the ones that had to move (6.20:1 and 6.95:1 worst here, so dark never
     needed the walk-down). */
  --ig-ok-text: #6ee7a1;
  --ig-warn-text: #f5b454;
  --ig-shot-badge-bg: rgba(4, 6, 11, 0.7);
  --ig-shot-badge-line: rgba(255, 255, 255, 0.14);
  --ig-shot-badge-ink: #ffffff;
  --ig-hero-glow-a: color-mix(in srgb, var(--sx-brand-3, #6b78ff) 14%, transparent);
  --ig-hero-glow-b: color-mix(in srgb, var(--sx-accent, #7ce3d4) 9%, transparent);
  --ig-lock-wash: linear-gradient(
    160deg,
    color-mix(in srgb, var(--sx-brand-3, #6b78ff) 16%, var(--sx-surface-2, #1a1f28)) 0%,
    var(--sx-surface-2, #1a1f28) 58%
  );
  /* A touch deeper than light's, since the page behind it is already dark and
     a scrim that does not separate from it stops reading as an overlay. */
  --ig-lbx-scrim: rgba(2, 4, 9, 0.88);
  --ig-lbx-ink: #ffffff;
  --ig-lbx-chip-bg: rgba(255, 255, 255, 0.1);
  --ig-lbx-chip-line: rgba(255, 255, 255, 0.18);
}

/* base.css gives every `main` 34px of top padding. Here that padding sits
   *above* the hero, so the glow — whose densest point is its own top edge —
   began at full strength 34px below the nav and drew a hard horizontal seam
   across the page. The prototype's `.ig-main` sets `padding-bottom` only, so
   its hero starts flush under the nav and the gradient's top edge lands on the
   header's own border where there is nothing to see. `.ig-hero` carries its
   own 26px of top padding, so the content spacing is unchanged. */
body.listing-v4-page main.ig-page {
  padding-top: 0;
}

body.listing-v4-page .ig-container {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0 32px;
}
@media (max-width: 760px) {
  body.listing-v4-page .ig-container {
    padding: 0 20px;
  }
}

body.listing-v4-page .ig-mono {
  font-family: var(--ig-mono);
  font-feature-settings: 'zero', 'ss02';
  letter-spacing: -0.01em;
}

/* ── owner bar ──────────────────────────────────────────────────────────── */
.ig-ownerbar {
  position: relative;
  z-index: 5;
  border-bottom: 1px solid var(--sx-line);
  background: color-mix(in srgb, var(--sx-brand-3, #3d4dff) 9%, var(--sx-bg-2, #f4f5f8));
}
.ig-ownerbar--preview {
  background: color-mix(in srgb, var(--ig-warn) 12%, var(--sx-bg-2, #f4f5f8));
}
.ig-ownerbar-in {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 50px;
  padding-top: 9px;
  padding-bottom: 9px;
  flex-wrap: wrap;
}
.ig-ownerbar-msg {
  flex: 1 1 auto;
  min-width: 0;
}
.ig-ownerbar .ig-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ig-brand);
  flex: none;
}
.ig-ownerbar--preview .ig-dot {
  background: var(--ig-warn);
}
.ig-ownerbar .t {
  font-size: 13.5px;
  font-weight: 600;
}
.ig-ownerbar .s {
  font-size: 12.5px;
  color: var(--sx-text-2);
}
.ig-ownerbar .ig-acts {
  display: flex;
  gap: 8px;
  margin-left: auto;
  flex: none;
}

/* ── shared small controls ──────────────────────────────────────────────── */
/* css/responsive.css applies a site-wide `min-height: 44px` tap-target floor to
   every <button>. These controls are deliberately shorter than that, so each
   opts out explicitly — without it a 32px button renders 32x44 beside a
   visually identical 32px <a>. tests/test_image_prompt_listing_v4_page.py pins
   the opt-outs, and fails loudly if the global floor is ever removed. */
.ig-btn,
button.ig-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  min-height: 0;
  padding: 0 16px;
  border-radius: var(--sx-r-md, 10px);
  border: 1px solid var(--sx-line);
  background: var(--sx-surface);
  color: var(--sx-text);
  font-family: inherit;
  font-weight: 500;
  font-size: 14px;
  letter-spacing: -0.005em;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, transform 0.05s;
}
.ig-btn:hover {
  background: var(--sx-surface-2);
}
.ig-btn:active {
  transform: translateY(1px);
}
.ig-btn--primary,
button.ig-btn--primary {
  background: linear-gradient(135deg, var(--sx-brand-3, #6b78ff) 0%, var(--sx-brand-2, #1a1f6b) 100%);
  color: var(--sx-brand-ink, #fff);
  border-color: transparent;
  font-weight: 600;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.18) inset,
    0 6px 18px color-mix(in srgb, var(--sx-brand-2, #1a1f6b) 32%, transparent);
}
.ig-btn--primary:hover {
  background: linear-gradient(135deg, var(--sx-brand, #3d4dff) 0%, var(--sx-brand-2, #1a1f6b) 100%);
}
.ig-btn--sm,
button.ig-btn--sm {
  height: 32px;
  min-height: 0;
  padding: 0 12px;
  font-size: 13px;
  border-radius: 8px;
}
.ig-btn--lg,
button.ig-btn--lg {
  height: 48px;
  min-height: 0;
  padding: 0 22px;
  font-size: 15px;
  border-radius: 12px;
  font-weight: 600;
}
.ig-icon-btn {
  width: 32px;
  height: 32px;
  min-height: 0;
  border-radius: 8px;
  border: 1px solid var(--sx-line);
  background: var(--sx-surface);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--sx-text-2);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.ig-icon-btn:hover {
  background: var(--sx-surface-2);
  border-color: var(--sx-line-2);
  color: var(--sx-text);
}
.ig-eyebrow-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--ig-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sx-text-3);
}

/* ── hero ───────────────────────────────────────────────────────────────── */
.ig-hero {
  position: relative;
  padding: 26px 0 8px;
  overflow: hidden;
}
.ig-hero-bg {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(900px 420px at 18% 0%, var(--ig-hero-glow-a) 0%, transparent 70%),
    radial-gradient(700px 360px at 92% 12%, var(--ig-hero-glow-b) 0%, transparent 72%);
}
.ig-hero > .ig-container {
  position: relative;
}
.ig-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(380px, 460px);
  gap: 48px;
  align-items: start;
  margin-top: 20px;
}
/* "No samples" state (handoff §4): the hero drops to one column and the
   quality card moves into the right rail, rather than leaving a half-empty
   proof frame where the mosaic would have been. */
.ig-grid.is-proofless {
  grid-template-columns: minmax(0, 1fr);
  max-width: 760px;
}

.ig-mosaic-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 12px;
  flex-wrap: wrap;
}
.ig-varnote {
  font-size: 12.5px;
  color: var(--sx-text-2);
}
.ig-varnote code {
  font-family: var(--ig-mono);
  font-size: 12px;
  color: var(--ig-accent);
}
.ig-jump {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ig-brand);
}
.ig-jump:hover {
  text-decoration: underline;
}

/* ── hero identity ──────────────────────────────────────────────────────── */
.ig-eyebrow {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}
.ig-kind {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--ig-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ig-accent);
}
.ig-kind .ig-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ig-accent);
}
.ig-model {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 24px;
  padding: 0 10px;
  border-radius: 999px;
  border: 1px solid var(--ig-brand-line);
  background: var(--ig-brand-soft);
  color: var(--ig-brand);
  font-family: var(--ig-mono);
  font-size: 11.5px;
  font-weight: 500;
}
/* The automated-test verdict chip (#3117). A pass keeps the brand chip above;
   a warn has to *read* differently, not merely carry a different attribute —
   a badge that says "with warnings" only in its title text is a distinction
   nobody sees. `min-height: 0` because the chip is 24px and the site-wide
   44px tap-target floor in responsive.css matches every element, not only
   buttons (CLAUDE.md). */
.ig-verdict[data-testing-verdict="warn"] {
  border-color: color-mix(in srgb, var(--ig-warn) 38%, transparent);
  background: var(--ig-warn-soft);
  color: var(--ig-warn-text);
}
/* A seller's self-reported verdict, not a platform measurement (#5109) —
   the same neutral treatment `.ig-new` already uses for the token-weight /
   licence chips beside this one, so an unverified claim never reads as the
   brand-chip "we tested this" trust signal above. All three tokens are
   global and already theme-flipped, so no dark re-flip needed here. */
.ig-verdict[data-testing-verdict="self-reported"] {
  border-color: var(--sx-line-2);
  background: var(--sx-surface);
  color: var(--sx-text-2);
}
.ig-new {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 10px;
  border-radius: 999px;
  border: 1px solid var(--sx-line-2);
  background: var(--sx-surface);
  color: var(--sx-text-2);
  font-family: var(--ig-mono);
  font-size: 11px;
  letter-spacing: 0.06em;
}
.ig-title {
  font-family: var(--ig-display);
  font-weight: 700;
  font-size: clamp(30px, 3.2vw, 44px);
  line-height: 1.04;
  letter-spacing: -0.032em;
  margin: 0 0 16px;
  text-wrap: balance;
}
.ig-desc {
  margin: 0;
  font-size: 15.5px;
  line-height: 1.62;
  color: var(--sx-text-2);
  max-width: 50ch;
  text-wrap: pretty;
}
.ig-facts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px 28px;
  margin: 26px 0;
  padding: 22px 0;
  border-top: 1px solid var(--sx-line);
  border-bottom: 1px solid var(--sx-line);
}
.ig-fact .l {
  display: block;
  font-family: var(--ig-mono);
  font-size: 10px;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--sx-text-3);
  margin-bottom: 7px;
}
.ig-fact .v {
  font-size: 14.5px;
  font-weight: 500;
  color: var(--sx-text);
}
.ig-fact .v .sub {
  color: var(--sx-text-2);
  font-weight: 400;
}
.ig-fact-seller {
  display: flex;
  align-items: center;
  gap: 9px;
}
/* The seller spans the grid rather than sharing a column, which is what keeps
   the handle, the date and the badge chips on one line each. Without it the
   row lands in a ~216px column at 1440px: the handle wraps under its own
   monogram and whichever fact shares the row (Voice, on the persona page) is
   truncated by the view model to fit a width it never needed to fit. */
.ig-fact--wide {
  grid-column: 1 / -1;
}
/* Only the wide row wraps, and the scope is load-bearing rather than tidy.
   `.ig-fact-seller` is the monogram and the handle side by side; let a *narrow*
   row wrap and the handle drops onto a second flex line **below** its own
   monogram (measured: 44px tall becomes 77px), which is what the two v4 pages
   that pass no badges would get. The wide row is the one with a chip row to put
   somewhere, and it has the width to keep the handle beside the monogram while
   the chips take the line underneath. */
.ig-fact--wide .ig-fact-seller {
  flex-wrap: wrap;
}
.ig-av {
  width: 24px;
  height: 24px;
  border-radius: 7px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, var(--sx-brand-3, #3d4dff) 0%, var(--sx-brand-2, #1a1f6b) 100%);
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  flex: none;
}
.ig-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 26px;
}
.ig-tag {
  display: inline-flex;
  align-items: center;
  height: 26px;
  padding: 0 11px;
  border-radius: 999px;
  border: 1px solid var(--sx-line);
  background: var(--sx-surface);
  color: var(--sx-text-2);
  font-family: var(--ig-mono);
  font-size: 11.5px;
}
.ig-tag:hover {
  border-color: var(--sx-line-2);
  color: var(--sx-text);
}

/* ── hero commerce ──────────────────────────────────────────────────────── */
.ig-buy {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 18px 20px;
  border-radius: 16px;
  border: 1px solid var(--sx-line-2);
  background: var(--sx-surface);
  box-shadow: var(--sx-shadow-2);
}
.ig-price {
  font-family: var(--ig-display);
  font-size: 32px;
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1;
  white-space: nowrap;
}
.ig-price .cur {
  font-size: 19px;
  vertical-align: 0.28em;
  margin-right: 1px;
  color: var(--sx-text-2);
}
.ig-buy-cta {
  flex: 1;
  min-width: 0;
}
.ig-buy-cta .ig-btn {
  width: 100%;
}
.ig-assure {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin: 12px 2px 0;
  padding: 0;
  list-style: none;
}
.ig-assure li {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  color: var(--sx-text-2);
  list-style: none;
}
.ig-assure svg {
  color: var(--ig-ok-text);
  flex: none;
}

/* ── listing quality ────────────────────────────────────────────────────── */
.ig-signals {
  margin-top: 26px;
  border: 1px solid var(--sx-line);
  border-radius: 16px;
  background: var(--sx-surface);
  overflow: hidden;
}
.ig-signals-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 13px 18px;
  border-bottom: 1px solid var(--sx-line);
}
.ig-signals-head .t {
  font-family: var(--ig-mono);
  font-size: 10.5px;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--sx-text-2);
}
.ig-grade {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 22px;
  padding: 0 9px;
  border-radius: 999px;
  background: var(--ig-ok-soft);
  border: 1px solid var(--ig-ok-line);
  color: var(--ig-ok-text);
  font-family: var(--ig-mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
}
.ig-sigs {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.ig-sig {
  display: flex;
  gap: 11px;
  align-items: flex-start;
  padding: 12px 18px;
  border-top: 1px solid var(--sx-line);
}
.ig-sigs .ig-sig:nth-child(-n + 2) {
  border-top: 0;
}
.ig-sigs .ig-sig:nth-child(even) {
  border-left: 1px solid var(--sx-line);
}
.ig-sig .ico {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-top: 1px;
  background: var(--ig-accent-soft);
  color: var(--ig-accent);
}
/* An unmet check is muted with an info glyph, never a warning colour — it
   states what this listing has not shown yet, not that something is wrong. */
.ig-sig.is-off .ico {
  background: var(--sx-surface-2);
  color: var(--sx-text-3);
}
.ig-sig .t {
  font-size: 13.5px;
  font-weight: 500;
}
.ig-sig .s {
  display: block;
  font-size: 12.5px;
  color: var(--sx-text-2);
  margin-top: 2px;
}
.ig-sig.is-off .t {
  color: var(--sx-text-2);
}

/* ── sections ───────────────────────────────────────────────────────────── */
.ig-main {
  padding-bottom: 40px;
}
.ig-sec {
  margin-top: 96px;
  scroll-margin-top: 80px;
}
.ig-sec-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 28px;
  padding-bottom: 18px;
  margin-bottom: 26px;
  border-bottom: 1px solid var(--sx-line);
}
.ig-sec-head h2 {
  font-family: var(--ig-display);
  font-size: 27px;
  font-weight: 700;
  letter-spacing: -0.025em;
  margin: 8px 0 0;
  line-height: 1.15;
}
.ig-sec-head .sub {
  margin: 8px 0 0;
  font-size: 14px;
  color: var(--sx-text-2);
  max-width: 56ch;
}
.ig-sec-aux {
  font-family: var(--ig-mono);
  font-size: 11.5px;
  color: var(--sx-text-2);
  white-space: nowrap;
  padding-bottom: 4px;
}

/* ── 01 · the artifact ──────────────────────────────────────────────────── */
.ig-locked {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
  border: 1px solid var(--sx-line);
  border-radius: 20px;
  overflow: hidden;
  background: var(--sx-surface);
}
.ig-locked-l {
  padding: 34px 36px;
}
.ig-locked-l h3 {
  font-family: var(--ig-display);
  font-size: 20px;
  letter-spacing: -0.02em;
  margin: 0 0 8px;
}
.ig-locked-l > p {
  margin: 0 0 22px;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--sx-text-2);
  max-width: 46ch;
}
.ig-incl {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ig-incl li {
  display: grid;
  grid-template-columns: 20px 1fr;
  gap: 12px;
  align-items: start;
  padding: 11px 0;
  list-style: none;
}
.ig-incl li + li {
  border-top: 1px solid var(--sx-line);
}
.ig-incl .ico {
  width: 20px;
  height: 20px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--ig-brand-soft);
  color: var(--ig-brand);
  margin-top: 1px;
}
.ig-incl .t {
  display: block;
  font-size: 14px;
  font-weight: 500;
}
.ig-incl .s {
  display: block;
  font-size: 12.5px;
  color: var(--sx-text-2);
  margin-top: 2px;
  overflow-wrap: anywhere;
}
.ig-locked-r {
  position: relative;
  padding: 34px;
  border-left: 1px solid var(--sx-line);
  background: var(--ig-lock-wash);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 16px;
}
.ig-lock-ico {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--sx-surface);
  border: 1px solid var(--sx-line-2);
  color: var(--ig-brand);
}
.ig-locked-r .t {
  font-family: var(--ig-display);
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.ig-locked-r .s {
  font-size: 13.5px;
  color: var(--sx-text-2);
  line-height: 1.55;
  max-width: 34ch;
}
.ig-locked-price {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
.ig-locked-price .p {
  font-family: var(--ig-display);
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.03em;
}
.ig-locked-price .n {
  font-family: var(--ig-mono);
  font-size: 11.5px;
  color: var(--sx-text-2);
}

/* The artifact, open — what a viewer with access reads instead of the
   unlock panel above. Shell rather than variant: every v4 type has a
   deliverable to hand over, and only what goes *inside* the <pre> differs
   (a graphic prompt highlights its variables, a skill prints its SKILL.md
   body). Those per-type rules stay in the variant sheets. */
.ig-open {
  border: 1px solid var(--sx-line);
  border-radius: 20px;
  background: var(--sx-surface);
  overflow: hidden;
}
.ig-open-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 22px;
  border-bottom: 1px solid var(--sx-line);
  flex-wrap: wrap;
}
.ig-owned {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--ig-ok-text);
}
.ig-pre {
  margin: 0;
  padding: 24px 26px;
  font-family: var(--ig-mono);
  font-size: 13px;
  line-height: 1.85;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--sx-text-2);
  overflow-x: auto;
}
.ig-open-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 0 26px 24px;
}
.ig-open-note {
  flex: 1 1 100%;
  margin: 2px 0 0;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--sx-text-2);
}
.ig-open-note code {
  font-family: var(--ig-mono);
  font-size: 12px;
  color: var(--ig-accent);
}

/* ── 03 · details ───────────────────────────────────────────────────────── */
.ig-det {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: 64px;
  align-items: start;
}
.ig-spec {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0 24px;
}
.ig-spec dt {
  font-family: var(--ig-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--sx-text-3);
  padding: 11px 0;
  border-top: 1px solid var(--sx-line);
}
.ig-spec dd {
  margin: 0;
  padding: 11px 0;
  border-top: 1px solid var(--sx-line);
  font-size: 13.5px;
  text-align: right;
  color: var(--sx-text);
  min-width: 0;
  overflow-wrap: anywhere;
}
.ig-spec dt:first-of-type,
.ig-spec dd:first-of-type {
  border-top: 0;
}
.ig-spec .ig-mono {
  font-size: 12.5px;
  color: var(--sx-text-2);
  /* The dd allows anywhere-breaks so a long value cannot overflow the column,
     but a parameter string must not break *inside* a token — `--v 8.2` split
     across two lines reads as a typo in the flags, which is the one thing this
     row exists to state precisely. Wrap at the spaces between flags instead. */
  overflow-wrap: normal;
  word-break: normal;
}
.ig-spec dd .note {
  display: block;
  margin-top: 5px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--sx-text-2);
}
.ig-editable {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 8px;
  height: 18px;
  padding: 0 7px;
  border-radius: 999px;
  border: 1px solid var(--ig-accent-line);
  background: var(--ig-accent-soft);
  color: var(--ig-accent);
  font-family: var(--ig-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
}
/* ── Shop badges ──────────────────────────────────────────────────────────
   Rendered by `v4_shop_badges` in components/listing_v4.html, which both v4
   pages can call — so the rules live in the shell rather than in the variant
   of whichever page opted in first. A class rendered by a shared macro but
   styled in one variant sheet is the failure this shell exists to avoid: 200,
   correct markup, no styling.

   `--trust` and `--role` are deliberately different colours. A role chip says
   who published the listing; a trust chip says what the platform checked. Both
   are read as *text on their own tint*, so both take the badge ink pair above
   rather than the `--ig-ok` / `--ig-warn` fills — see the note there. */
.ig-shopb-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 21px;
  /* The floor in responsive.css matches `button` but not `span`, so this row
     needs no opt-out today — stated because a future revision that makes a
     chip a <button> would silently become 44px tall beside its neighbours. */
  padding: 0 8px;
  border-radius: 999px;
  border: 1px solid var(--sx-line-2);
  background: var(--sx-surface-2);
  color: var(--sx-text-2);
  font-family: var(--ig-mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.03em;
  white-space: nowrap;
}
.ig-shopb-chip svg {
  flex: none;
}
/* Tint and border derive from the ink, so a theme that restates only the ink
   re-resolves both for free (the `--pp-accent` rule in CLAUDE.md). */
.ig-shopb-chip--trust {
  border-color: color-mix(in srgb, var(--ig-ok-text) 40%, transparent);
  background: color-mix(in srgb, var(--ig-ok-text) 12%, transparent);
  color: var(--ig-ok-text);
}
.ig-shopb-chip--role {
  border-color: color-mix(in srgb, var(--ig-warn-text) 44%, transparent);
  background: color-mix(in srgb, var(--ig-warn-text) 13%, transparent);
  color: var(--ig-warn-text);
}
/* Two arrangements of one chip set, and the difference is where they sit. The
   compact row is the hero's — chips beside the seller's handle, each carrying
   its reason as a `title` — and the list is the seller card's, which prints
   the reason as text because it has the width for it. */
.ig-shopb {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}
.ig-shopb-list {
  display: grid;
  gap: 9px;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--sx-line);
}
.ig-shopb-item {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.ig-shopb-item .n {
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--sx-text-2);
  text-wrap: pretty;
}
/* ── Tokens & cost ────────────────────────────────────────────────────────
   The stacked bar, its legend and the three cost cards, shared by both v4
   pages. The prototypes spell this twice — `.ip-*` in the graphic prompt file
   and `.sk-*` in the skill one — with the cost cards and the note byte-
   identical between them and the legend differing only in its row rhythm. One
   copy lives here for the same reason the rest of the shell does: a token
   section rendered on a page whose variant sheet lacked the rule would return
   200 with every element present and no styling at all.

   What genuinely differs per type is the legend row — the graphic prompt
   carries a description line under each segment name, the skill carries a mono
   filename — so the variants tune `.ig-tok-leg` rather than restating the box.

   No control in here is focusable, so the 44px tap-target floor in
   responsive.css never applies and nothing needs to opt out of it. */
.ig-tok-bar {
  display: flex;
  gap: 2px;
  height: 12px;
  border-radius: 999px;
  overflow: hidden;
  margin: 2px 0 20px;
}
.ig-tok-seg {
  display: block;
  min-width: 6px;
}
/* Five fills, cycled by the template (`loop.index0 % 5 + 1`). Every one is an
   existing shell token or a mix of two, so this block declares no literal and
   needs no dark counterpart. They are read as *fills* only — never as ink —
   which is what makes `--ig-accent` safe here where the contrast note in
   CLAUDE.md warns against it as text.

   Three were enough while the section had at most three rows (a skill divides
   into two spans, a graphic prompt into three). A persona divides into one row
   per authored block — up to eight — so a three-colour cycle put the same
   swatch on rows 1 and 4 of the same legend. Four and five avoid `--ig-warn`
   deliberately: it is an amber, and a type whose accent is also an amber would
   then draw two segments a reader has to compare by lightness alone. */
.ig-tok-seg--1 {
  background: var(--ig-accent);
}
.ig-tok-seg--2 {
  background: var(--ig-brand);
}
.ig-tok-seg--3 {
  background: var(--ig-ok);
}
.ig-tok-seg--4 {
  background: color-mix(in srgb, var(--ig-brand) 55%, var(--ig-ok));
}
.ig-tok-seg--5 {
  background: var(--sx-text-3);
}
.ig-tok-legend {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px 36px;
}
.ig-tok-leg {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 13.5px;
  min-width: 0;
}
.ig-tok-leg i {
  width: 9px;
  height: 9px;
  border-radius: 3px;
  flex: none;
  margin-top: 5px;
}
.ig-tok-leg .t {
  color: var(--sx-text);
  min-width: 0;
}
.ig-tok-leg .d {
  display: block;
  margin-top: 3px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--sx-text-2);
}
.ig-tok-leg .v {
  margin-left: auto;
  flex: none;
  font-family: var(--ig-mono);
  font-size: 12px;
  color: var(--sx-text-3);
  white-space: nowrap;
}
.ig-tok-cost {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  margin-top: 26px;
}
.ig-tok-cost .c {
  padding: 16px 18px;
  border: 1px solid var(--sx-line);
  border-radius: 14px;
  background: var(--sx-surface);
}
.ig-tok-cost .k {
  display: block;
  font-family: var(--ig-mono);
  font-size: 10px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--sx-text-3);
}
.ig-tok-cost .n {
  display: block;
  margin-top: 7px;
  font-family: var(--ig-display);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.ig-tok-cost .s {
  display: block;
  margin-top: 3px;
  font-size: 12px;
  color: var(--sx-text-2);
}
.ig-tok-note {
  margin: 24px 0 0;
  padding-top: 18px;
  border-top: 1px dashed var(--sx-line-2);
  font-size: 13px;
  line-height: 1.6;
  color: var(--sx-text-2);
  max-width: 78ch;
  text-wrap: pretty;
}

.ig-changelog {
  margin-top: 22px;
  border: 1px solid var(--sx-line);
  border-radius: 14px;
  background: var(--sx-surface);
  padding: 0 16px;
}
.ig-changelog > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 0;
  cursor: pointer;
  list-style: none;
  font-size: 14px;
  font-weight: 600;
}
.ig-changelog > summary::-webkit-details-marker {
  display: none;
}
.ig-changelog dl {
  margin: 0;
  padding: 0 0 16px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px 16px;
  font-size: 13px;
}
.ig-changelog dt {
  font-family: var(--ig-mono);
  font-size: 11.5px;
  color: var(--sx-text-3);
}
.ig-changelog dd {
  margin: 0;
  color: var(--sx-text-2);
}

.ig-seller {
  margin-top: 28px;
  border: 1px solid var(--sx-line);
  border-radius: 16px;
  padding: 20px;
  background: var(--sx-surface);
}
.ig-seller-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.ig-seller-av {
  width: 40px;
  height: 40px;
  border-radius: 11px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, var(--sx-brand-3, #3d4dff) 0%, var(--sx-brand-2, #1a1f6b) 100%);
  color: #fff;
  font-weight: 700;
  font-size: 16px;
  flex: none;
}
.ig-seller-id {
  flex: 1 1 140px;
  min-width: 0;
}
.ig-seller-name {
  font-weight: 600;
  font-size: 14.5px;
  display: flex;
  align-items: center;
  gap: 6px;
}
.ig-seller-sub {
  font-family: var(--ig-mono);
  font-size: 11.5px;
  color: var(--sx-text-2);
  margin-top: 1px;
}
.ig-seller p {
  margin: 14px 0 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--sx-text-2);
}
.ig-seller-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 14px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ig-brand);
}
.ig-seller-link:hover {
  text-decoration: underline;
}
.ig-seller-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

.ig-faq {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--sx-line);
}
.ig-faq li {
  border-bottom: 1px solid var(--sx-line);
  list-style: none;
}
.ig-faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 18px 2px;
  cursor: pointer;
  list-style: none;
  font-size: 15px;
  font-weight: 500;
}
.ig-faq summary::-webkit-details-marker {
  display: none;
}
.ig-faq summary:hover {
  color: var(--ig-brand);
}
.ig-faq-ico {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1px solid var(--sx-line);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--sx-text-2);
  flex: none;
  transition: transform 0.18s;
}
.ig-faq details[open] .ig-faq-ico {
  transform: rotate(180deg);
}
.ig-faq p {
  margin: 0 0 20px;
  padding-right: 44px;
  font-size: 14px;
  line-height: 1.65;
  color: var(--sx-text-2);
  max-width: 64ch;
  white-space: pre-line;
}

/* ── related ────────────────────────────────────────────────────────────
   The row's card lived here — a cover-led `.ig-rel-card` with its own price
   pill and its own byline rule. It renders the shared listing card in the
   shared `.sx-listing-grid` now (docs/design_sync_log.md, 2026-08-29), so
   there is nothing left for this sheet to style: the numbered section head
   above is still the page's, the cards below it are css/sx-home.css's. */

/* ── sticky buy bar ─────────────────────────────────────────────────────── */
.ig-stick {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--sx-bg-2, #f4f5f8) 92%, transparent);
  backdrop-filter: blur(16px) saturate(130%);
  -webkit-backdrop-filter: blur(16px) saturate(130%);
  border-top: 1px solid var(--sx-line);
  transform: translateY(120%);
  transition: transform 0.26s cubic-bezier(0.2, 0.8, 0.3, 1);
}
.ig-stick.is-on {
  transform: none;
}
/* The bar ships `hidden` so a reader with no JavaScript never gets one stuck
   across the foot of the page; the page script unhides it and the transform
   above does the reveal.

   base.css now carries a global `[hidden] { display: none !important }`
   (#5017), so this rule is belt-and-braces rather than load-bearing — but the
   bar is `position: fixed` with its own transform, which is the shape that
   goes wrong most visibly if that global rule is ever narrowed, and stating it
   here costs one declaration. */
.ig-stick[hidden] {
  display: none;
}
.ig-stick-in {
  display: flex;
  align-items: center;
  gap: 20px;
  height: 64px;
}
.ig-stick-t {
  font-weight: 600;
  font-size: 14.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ig-stick-m {
  font-family: var(--ig-mono);
  font-size: 11.5px;
  color: var(--sx-text-3);
  white-space: nowrap;
}
.ig-stick-r {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-left: auto;
}
.ig-stick-p {
  font-family: var(--ig-display);
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

/* ── reviews (section 04) ───────────────────────────────────────────────
   The v4 review module, ported from the design repo's portable
   `<style id="sx-reviews-css">` block in
   `Sigrix_new/marketplace/Marketplace - Listing Image Prompt v4.html`.

   Two columns. The right one is the write form and the cards — roughly what
   the shared `_reviews_panel.html` already rendered. The left one is the
   reason the module exists: an average, a star row, a tally and a 5→1
   distribution. The shared panel renders a count and a list and nothing else,
   so a listing's rating *spread* was not visible on any surface in the app.

   Prototype tokens map onto the page's own. This sheet reads `--sx-*` straight
   off `.sx` rather than re-scoping a token set (see the file header), so the
   module inherits dark mode for free: `--text-mute` → `--sx-text-2`,
   `--text-soft`/`--text-faint` → `--sx-text-3` and the derivation below,
   `--line-strong` → `--sx-line-2`, `--bg-1` → `--sx-bg-2`, `--good` →
   `--ig-ok`. The two new tokens are both `color-mix()` over a shared token
   rather than literals, so the dark block has nothing to re-flip for them —
   which is what `test_the_dark_block_re_flips_every_page_local_token_that_carries_a_literal`
   checks.

   Prefix note: `rv-` is this page's alone. The sheet loads on no other page,
   and nothing else in `src/static/css/` defines an `rv-*` selector — the
   `.pp-locked` collision in CLAUDE.md is what that check exists to avoid. */
.rv {
  --ig-star: var(--sx-warn, #c77800);
  /* The prototype's fourth text tier (--text-faint, alpha .32 dark / .38 light)
     is below AA at the sizes this module uses it — measured in Chromium at
     3.27:1 light and 3.82:1 dark on the "/ 5", the bar counts, the side note
     and the review date, all of which are 11-12.5px. Those read --sx-text-3
     instead (5.35:1 / 5.84:1), and the faint tone survives only where it is
     drawing the *unfilled star* rather than words: an empty star and a star
     button at rest are non-text UI, which WCAG 1.4.11 puts at 3:1, and both
     clear it. Naming the token for that job is what stops it drifting back
     onto text — the failure is silent, since the rule applies and the element
     renders either way. */
  --ig-star-off: color-mix(in srgb, var(--sx-text-3, #6b7382) 76%, transparent);
  display: grid;
  grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
  gap: 64px;
  align-items: start;
}

/* ── summary rail ─────────────────────────────────────────────────────── */
.rv-main {
  min-width: 0;
}
.rv-sum {
  display: grid;
  gap: 18px;
}
.rv-avg {
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.rv-avg .n {
  font-family: var(--ig-display);
  font-size: 46px;
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 0.9;
}
.rv-avg .of {
  font-family: var(--ig-mono);
  font-size: 12px;
  color: var(--sx-text-3);
}
.rv-stars {
  display: inline-flex;
  gap: 3px;
  color: var(--ig-star);
  line-height: 1;
  font-size: 14px;
  letter-spacing: 2px;
}
.rv-stars .off {
  color: var(--ig-star-off);
}
.rv-avg-stars {
  margin-top: 10px;
}
.rv-tally {
  font-size: 12.5px;
  color: var(--sx-text-3);
}
.rv-bars {
  display: grid;
  gap: 7px;
}
.rv-bar {
  display: grid;
  grid-template-columns: 10px 1fr 22px;
  gap: 10px;
  align-items: center;
  font-family: var(--ig-mono);
  font-size: 11px;
  color: var(--sx-text-3);
}
.rv-bar .track {
  height: 5px;
  border-radius: 999px;
  background: var(--sx-surface-2);
  overflow: hidden;
}
.rv-bar .fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--ig-star);
}
.rv-bar .c {
  text-align: right;
  color: var(--sx-text-3);
}
/* No ratings yet: the rail keeps its shape so the server and product_detail.js
   are updating the same DOM rather than building two different ones, and the
   bars — which would all read zero — are the one part that says nothing. */
.rv-sum[data-empty='true'] .rv-bars,
.rv-sum[data-empty='true'] .rv-avg .of,
.rv-sum[data-empty='true'] .rv-stars {
  display: none;
}
.rv-sum[data-empty='true'] .rv-avg .n {
  color: var(--sx-text-3);
}
.rv-side-note {
  font-size: 12px;
  line-height: 1.55;
  color: var(--sx-text-3);
  padding-top: 16px;
  margin: 0;
  border-top: 1px solid var(--sx-line);
}

/* ── write form ───────────────────────────────────────────────────────── */
.rv-form {
  border: 1px solid var(--sx-line);
  border-radius: var(--sx-r-lg, 14px);
  background: var(--sx-bg-2);
  padding: 22px;
  box-shadow: var(--sx-shadow-2, 0 8px 28px rgba(0, 0, 0, 0.16));
}
.rv-form h3,
.rv-form h4 {
  font-family: var(--ig-display);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 4px;
}
.rv-form .lead {
  margin: 0 0 20px;
  font-size: 13.5px;
  color: var(--sx-text-3);
  max-width: 52ch;
}
.rv-lbl {
  display: block;
  font-family: var(--ig-mono);
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sx-text-3);
  margin-bottom: 9px;
}
/* The label is 10.5px uppercase with wide tracking; "(optional)" is an aside
   inside it and reads as noise in the same treatment. */
.rv-lbl-note {
  text-transform: none;
  letter-spacing: 0;
}
.rv-help {
  display: block;
  font-size: 12.5px;
  color: var(--sx-text-3);
  margin: -4px 0 9px;
}
.rv-field {
  display: block;
  margin-bottom: 18px;
}
.rv-pick {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 18px;
  flex-wrap: wrap;
}
.rv-pick .set {
  display: flex;
  gap: 2px;
}
/* min-height opt-out: css/responsive.css floors every <button> at 44px
   site-wide, so a 32px star button renders 32x44 without this (CLAUDE.md).
   Selected by class rather than as `.rv-pick button` so the floor sweep in
   tests/test_image_prompt_listing_v4_page.py can see it — that check reads
   class names off the markup, and a bare element selector is invisible to it. */
.rv-star {
  width: 32px;
  height: 32px;
  min-height: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--ig-star-off);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
  transition: color 0.12s, background 0.12s;
}
.rv-star:hover:not([disabled]) {
  background: var(--sx-surface-2);
}
/* `is-selected` / `is-hovered` are what product_detail.js toggles, and it sets
   them on every star up to the value — `aria-checked` marks only the exact one,
   so keying the fill off that would light a single star instead of a row. */
.rv-star.is-selected,
.rv-star.is-hovered {
  color: var(--ig-star);
}
.rv-star[disabled] {
  cursor: not-allowed;
}
.rv-pick .word {
  font-size: 12.5px;
  color: var(--sx-text-2);
}
.rv-in {
  width: 100%;
  background: var(--sx-bg);
  border: 1px solid var(--sx-line);
  border-radius: var(--sx-r-md, 10px);
  color: var(--sx-text);
  font: inherit;
  font-size: 14px;
  padding: 11px 13px;
  transition: border-color 0.12s, box-shadow 0.12s;
}
.rv-in::placeholder {
  color: var(--sx-text-3);
}
.rv-in:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--sx-brand-3, #6b78ff) 60%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--sx-brand, #3d4dff) 16%, transparent);
}
textarea.rv-in {
  min-height: 112px;
  resize: vertical;
  line-height: 1.6;
}
.rv-form-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 16px;
  flex-wrap: wrap;
}
.rv-hint {
  font-size: 12px;
  color: var(--sx-text-3);
  margin: 0;
}
.rv-hint--block {
  margin-bottom: 16px;
}
.rv-hint .num {
  font-family: var(--ig-mono);
  color: var(--sx-text-3);
}
.rv-acts-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.rv-status:empty {
  display: none;
}
.rv-status {
  margin: 14px 0 0;
  font-size: 13px;
  color: var(--sx-text-2);
}
.rv-optin {
  display: grid;
  gap: 8px;
  margin-bottom: 18px;
}
.rv-optin-toggle {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  font-size: 13px;
  color: var(--sx-text-2);
  line-height: 1.5;
}
.rv-optin-toggle input {
  min-height: 0;
  width: 15px;
  height: 15px;
  margin-top: 2px;
  flex: none;
}
.rv-optin .rv-hint {
  font-size: 11.5px;
}

/* ── posted / blocked states ──────────────────────────────────────────── */
.rv-done {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  border: 1px solid var(--ig-ok-line);
  background: var(--ig-ok-soft);
  border-radius: var(--sx-r-lg, 14px);
  padding: 16px 18px;
}
.rv-done .ico {
  flex: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--ig-ok) 22%, transparent);
  color: var(--ig-ok-text);
  font-size: 11px;
  margin-top: 1px;
}
.rv-done .t {
  font-weight: 600;
  font-size: 13.5px;
}
.rv-done .s {
  display: block;
  margin-top: 3px;
  font-size: 12.5px;
  color: var(--sx-text-3);
}
.rv-gate {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  border: 1px dashed var(--sx-line-2);
  border-radius: var(--sx-r-lg, 14px);
  padding: 18px 20px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--sx-text-2);
}
.rv-gate .ico {
  flex: none;
  color: var(--sx-text-3);
  margin-top: 1px;
}

/* ── the cards ────────────────────────────────────────────────────────── */
.rv-list {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}
/* The prototype puts a monogram in its own 34px column beside the header.
   `author_chip` already renders the reviewer's real avatar next to their name
   and carries the data-* the author-card popover reads, so the avatar rides in
   the header row instead — same adjacency, and it keeps a shipped feature the
   monogram would have replaced with a placeholder. */
.rv-item {
  padding: 24px 0;
  border-top: 1px solid var(--sx-line);
}
.rv-hd {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.rv-hd .ac-chip {
  padding: 0;
  border: 0;
  background: transparent;
}
.rv-when {
  font-family: var(--ig-mono);
  font-size: 11px;
  color: var(--sx-text-3);
}
.rv-title {
  margin: 9px 0 0;
  font-weight: 600;
  font-size: 14px;
  color: var(--sx-text);
}
.rv-body {
  margin: 9px 0 0;
  font-size: 14.5px;
  line-height: 1.62;
  color: var(--sx-text-2);
  max-width: 66ch;
  text-wrap: pretty;
}
.rv-body > :first-child {
  margin-top: 0;
}
.rv-body > :last-child {
  margin-bottom: 0;
}
.rv-item-acts {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 13px;
}
/* min-height opt-out, same floor as .rv-star above. */
button.rv-mini {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 27px;
  min-height: 0;
  padding: 0 11px;
  border-radius: 8px;
  border: 1px solid var(--sx-line);
  background: transparent;
  color: var(--sx-text-3);
  font: inherit;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.rv-mini:hover {
  background: var(--sx-surface-2);
  color: var(--sx-text);
}
.rv-none {
  border: 1px dashed var(--sx-line-2);
  border-radius: var(--sx-r-lg, 14px);
  padding: 30px 24px;
  margin-top: 28px;
  text-align: center;
  list-style: none;
}
.rv-none .t {
  display: block;
  font-family: var(--ig-display);
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.rv-none .s {
  display: block;
  margin: 6px auto 0;
  font-size: 13.5px;
  color: var(--sx-text-3);
  max-width: 42ch;
}


/* ── the chat card (hero proof, and section 02) ──────────────────────────
   Shared, because two variants render a recorded exchange and neither owns it.
   A persona's argument is how it talks — the hero shows its stored greeting,
   section 02 a stored scenario. A text prompt's is what one run returns — the
   hero shows the reply alone, section 02 the same exchange with the input that
   produced it. Same card, two readings.

   It lived in `sx-listing-persona.css` while personas were the only page
   rendering it, and moved here when the text prompt page landed rather than
   being copied: a selector declared in two variant sheets resolves by whichever
   sheet the browser parsed last, so a page loading only one gets whichever half
   its variant happens to hold. `test_no_selector_is_declared_in_two_variant_sheets`
   holds that line.

   Every colour is a token, so each page's own `--ig-accent` drives its own
   card and nothing here needs to know which type it is drawing. Both are
   quotations of seller-authored text, never the deliverable; the prompt itself
   is section 01's business. */
.pv-chat {
  border: 1px solid var(--sx-line);
  border-radius: 16px;
  background: var(--sx-surface);
  overflow: hidden;
}
.pv-chat-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 16px;
  border-bottom: 1px solid var(--sx-line);
  font-family: var(--ig-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sx-text-3);
}
.pv-msg {
  padding: 17px 19px;
}
.pv-msg + .pv-msg {
  border-top: 1px solid var(--sx-line);
}
.pv-msg--user {
  background: var(--sx-bg-2);
}
.pv-who {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-bottom: 10px;
  font-family: var(--ig-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--sx-text-3);
}
.pv-msg--ai .pv-who {
  color: var(--ig-accent);
}
.pv-who .av {
  width: 19px;
  height: 19px;
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--ig-accent-soft);
  color: var(--ig-accent);
  font-size: 9.5px;
  font-weight: 700;
  flex: none;
}
/* `white-space: pre-wrap` where the prototype has one <p> per paragraph: a
   stored greeting or scenario answer is a single seller-authored string, and
   the line breaks they typed are the shape of the reply. */
.pv-msg p {
  margin: 0;
  font-size: 14px;
  line-height: 1.65;
  color: var(--sx-text-2);
  white-space: pre-wrap;
  text-wrap: pretty;
}
.pv-msg--ai p {
  color: var(--sx-text);
}

/* ── the guarantees row (section 02) ─────────────────────────────────────
   The shell's `.ig-incl` list with a surface under it. What fills it is the
   seller's own Rules block, quoted line for line — see
   `persona_listing_view.persona_guards` and `prompt_listing_view.prompt_guards`
   for why it is quoted rather than summarised. Shared for the same reason the
   chat card above is.

   The caption carries the block's top margin so the list can sit tight under
   it. It exists because the alternative was a sub-line under every row saying
   where the rule came from — the same sentence three times, which is the
   padding that makes a list stop being read. */
.pv-guards-cap {
  margin: 32px 0 12px;
}
.ig-guards {
  margin-top: 0;
  border: 1px solid var(--sx-line);
  border-radius: 14px;
  padding: 4px 18px;
  background: color-mix(in srgb, var(--sx-surface) 55%, transparent);
}
.ig-guards .ico {
  background: var(--ig-accent-soft);
  color: var(--ig-accent);
}


/* ── item manifest ───────────────────────────────────────────────────────
   The list under section 01's CTA, so a buyer counts what they get in the same
   glance as the price. Two types render it and mean different things by a row:
   a skill's is its SKILL.md, a persona's is one authored block. What they
   share is every declaration here, the label included — the *one* property
   that differs is its typeface, since a filename is set in mono and a block
   name is prose, and the skill variant restates that one line at (0,3,1) so it
   wins on specificity rather than on which sheet loaded last. */
.ig-files {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--sx-line);
  border-radius: 12px;
  overflow: hidden;
}
.ig-file {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: var(--sx-bg-2);
  color: var(--sx-text-2);
}
.ig-file + .ig-file {
  border-top: 1px solid var(--sx-line);
}
.ig-file .n {
  font-size: 12.5px;
  color: var(--sx-text);
}
.ig-file .z {
  margin-left: auto;
  font-family: var(--ig-mono);
  font-size: 11.5px;
  color: var(--sx-text-3);
}

/* ── owner stats ─────────────────────────────────────────────────────────
   Replaces the buy column for the seller's own view. Three numbers the
   platform genuinely holds — both prototypes ask for "installs / views",
   which are not tracked per listing, so likes / versions / reviews take their
   place rather than rendering three zeros. Byte-identical in the skill and
   persona prototypes, which is why it is here rather than copied into each. */
.ig-owner-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  padding: 18px 20px;
  border: 1px solid var(--sx-line-2);
  border-radius: 16px;
  background: var(--sx-surface);
  box-shadow: var(--sx-shadow-md, 0 8px 24px rgba(15, 18, 26, 0.06));
}
.ig-owner-stats .v {
  display: block;
  font-family: var(--ig-display);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.ig-owner-stats .l {
  display: block;
  margin-top: 2px;
  font-family: var(--ig-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--sx-text-3);
}

/* ── Versions ─────────────────────────────────────────────────────────────
   Releases are the one thing a buyer re-reads after installing, so both
   prototypes promote them out of the details disclosure into a numbered
   section of their own. The skill file spells them `.sk-ver*` and the persona
   file `.pv-ver*`; the declarations between those two names are identical, so
   this is one component named twice rather than two components. (It did start
   in `sx-listing-skill.css`, with a comment saying only that page had them.
   The second page is what moved it.)

   `subgrid` is what keeps the version, its chip, its weight and its date in
   four aligned columns while each row's note spans the rest — a per-row grid
   would let every row pick its own column widths. */
.ig-vers {
  display: grid;
  grid-template-columns: auto auto minmax(0, auto) 1fr auto;
  margin: 0;
  list-style: none;
  border: 1px solid var(--sx-line);
  border-radius: 16px;
  background: var(--sx-surface);
  padding: 6px 22px;
}
.ig-ver {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
  gap: 0 12px;
  padding: 17px 0;
  list-style: none;
}
.ig-ver + .ig-ver {
  border-top: 1px solid var(--sx-line);
}
.ig-ver .dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--sx-line-2);
}
.ig-ver.is-cur .dot {
  background: var(--ig-accent);
  box-shadow: 0 0 0 4px var(--ig-accent-soft);
}
.ig-ver .v {
  font-family: var(--ig-mono);
  font-size: 13.5px;
}
.ig-ver .z,
.ig-ver .d {
  font-family: var(--ig-mono);
  font-size: 11.5px;
  color: var(--sx-text-3);
  white-space: nowrap;
}
.ig-ver .d {
  text-align: right;
}
.ig-ver .s {
  grid-column: 4 / -1;
  margin: 6px 0 0;
  font-size: 13px;
  line-height: 1.6;
  color: var(--sx-text-2);
  text-wrap: pretty;
}
.ig-ver-cur {
  display: inline-flex;
  align-items: center;
  height: 19px;
  padding: 0 8px;
  border-radius: 999px;
  border: 1px solid var(--ig-accent-line);
  background: var(--ig-accent-soft);
  color: var(--ig-accent);
  font-family: var(--ig-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

@media (max-width: 900px) {
  .rv {
    grid-template-columns: 1fr;
    gap: 34px;
  }
  .rv-form {
    padding: 18px;
  }
}

@media (max-width: 1080px) {
  .ig-grid {
    grid-template-columns: 1fr;
    gap: 36px;
  }
  .ig-det {
    grid-template-columns: 1fr;
    gap: 40px;
  }
  .ig-locked {
    grid-template-columns: 1fr;
  }
  .ig-locked-r {
    border-left: 0;
    border-top: 1px solid var(--sx-line);
  }
}
@media (max-width: 760px) {
  .ig-owner-stats {
    grid-template-columns: 1fr;
    gap: 10px;
  }
  /* subgrid gives up at this width — five columns in 360px puts the date under
     the version number. One flowing column per row keeps every field readable
     and the note still last. */
  .ig-vers {
    display: block;
    padding: 6px 18px;
  }
  .ig-ver {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
  .ig-ver .d {
    margin-left: auto;
  }
  .ig-ver .s {
    flex: 1 1 100%;
    margin-top: 2px;
  }
  /* Both drop to one column rather than shrinking: the legend row ends with a
     nowrap `N · NN%` value pushed right by `margin-left: auto`, and the cost
     card's number is a 20px display face — neither survives a third of 360px. */
  .ig-tok-legend {
    grid-template-columns: 1fr;
  }
  .ig-tok-cost {
    grid-template-columns: 1fr;
  }
  .ig-facts {
    grid-template-columns: 1fr;
  }
  .ig-sigs {
    grid-template-columns: 1fr;
  }
  .ig-sigs .ig-sig:nth-child(2) {
    border-top: 1px solid var(--sx-line);
  }
  .ig-sigs .ig-sig:nth-child(even) {
    border-left: 0;
  }
  .ig-sec {
    margin-top: 64px;
  }
  .ig-sec-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
  .ig-locked-l {
    padding: 26px 22px;
  }
  .ig-buy {
    flex-wrap: wrap;
  }
  /* Handoff §8.4: on mobile the bar keeps only the CTA. The title and meta are
     already on screen above it, and a three-part bar at 360px leaves the button
     too narrow to hit. */
  .ig-stick-m,
  .ig-stick-t {
    display: none;
  }
  .ig-stick-r {
    margin-left: 0;
    width: 100%;
  }
  .ig-stick-r .ig-btn {
    flex: 1;
  }
}

/* ── image zoom ─────────────────────────────────────────────────────────── */
/* The trigger. Strips only the button chrome that gets in the way — padding,
   inherited font, the pointer — and deliberately declares no border or
   background, so each page's own trigger class (`.ig-card-img`,
   `.ig-shot-fill`) keeps drawing the frame it already drew. Those live in the
   variant sheet, which loads after this one, so they win the tie either way;
   not restating them here is what keeps this rule from being a second place
   that decides how a sample looks. */
button.ig-zoom {
  display: block;
  width: 100%;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: zoom-in;
  -webkit-appearance: none;
  appearance: none;
  /* css/responsive.css floors every <button> at 44px site-wide, and these are
     sized by the picture inside them, not by a height of their own. Harmless
     today (a sample is far taller than 44px at every breakpoint) and stated so
     a narrower trigger later cannot inherit a floor nobody looked for. */
  min-height: 0;
}
.ig-zoom:focus-visible {
  outline: 2px solid var(--ig-accent);
  outline-offset: 3px;
}

/* ── lightbox ───────────────────────────────────────────────────────────── */
.ig-lbx {
  position: fixed;
  inset: 0;
  /* Above the sticky buy bar (40) and the site nav (5). The page's own ladder
     is the whole ladder here, because listing_v4.js reparents this onto <body>
     at boot — an overlay left inside the page shell competes from inside
     whatever stacking context an ancestor happens to open, where no z-index of
     its own can win (CLAUDE.md). */
  z-index: 1400;
  display: flex;
  /* `stretch`, not `center`: the panel below sizes the picture with a
     percentage max-height, and a percentage against an `auto` parent height is
     *ignored* — so a centred (content-sized) panel let a 1280px-tall render
     paint straight over the caption under it, at every viewport, silently. The
     panel takes the full frame and centres the picture inside it instead. */
  align-items: stretch;
  justify-content: center;
  padding: clamp(12px, 3vw, 32px);
}
/* [hidden] is an ordinary (0,1,0) author rule, so the display above beats it
   and the overlay would sit across the page forever. */
.ig-lbx[hidden] {
  display: none;
}
.ig-lbx-scrim {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  background: var(--ig-lbx-scrim);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
}
.ig-lbx-panel {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: min(1120px, 100%);
  /* Definite, which is the whole point — see the comment on `.ig-lbx` above.
     The bar and the caption take what they need and the stage takes the rest,
     so the picture is bounded by real pixels rather than by a percentage that
     resolves to nothing. */
  height: 100%;
}
.ig-lbx-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.ig-lbx-count {
  font-family: var(--ig-mono);
  font-size: 11.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ig-lbx-ink);
}
button.ig-lbx-x,
button.ig-lbx-nav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 44px exactly: the site-wide tap floor is the right size for these, so they
     take it rather than opting out of it. */
  width: 44px;
  height: 44px;
  flex: none;
  padding: 0;
  border-radius: 999px;
  border: 1px solid var(--ig-lbx-chip-line);
  background: var(--ig-lbx-chip-bg);
  color: var(--ig-lbx-ink);
  cursor: pointer;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  transition:
    background 0.16s ease,
    transform 0.16s ease;
}
.ig-lbx-x:hover,
.ig-lbx-nav:hover {
  background: color-mix(in srgb, var(--ig-lbx-ink) 26%, transparent);
}
.ig-lbx-x:focus-visible,
.ig-lbx-nav:focus-visible {
  outline: 2px solid var(--ig-lbx-ink);
  outline-offset: 2px;
}
.ig-lbx-nav[hidden] {
  display: none;
}
.ig-lbx-stage {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  /* The picture is what gives when the viewport is short: the stage takes the
     room the bar and the caption leave, and the image fits inside it. Sizing
     the image against the viewport instead (`72vh` and friends) adds up with
     everything above and below it and overflows on exactly the short windows
     the cap was meant to protect. `min-height: 0` is what lets a flex item
     shrink below its content at all. */
  flex: 1 1 auto;
  min-height: 0;
}
.ig-lbx-img {
  display: block;
  min-width: 0;
  flex: 0 1 auto;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 14px;
  border: 1px solid var(--ig-lbx-chip-line);
  background: var(--ig-lbx-scrim);
}
.ig-lbx-cap {
  flex: none;
  max-height: 34%;
  overflow: auto;
  padding: 14px 16px;
  border-radius: 14px;
  border: 1px solid var(--sx-line);
  /* The page's own surface, not the scrim: the caption carries the same
     server-rendered markup the card does, so it has to read against the same
     tokens in both themes rather than against a palette invented here. */
  background: var(--sx-surface);
  color: var(--sx-text-2);
  font-size: 12.5px;
  line-height: 1.5;
}
.ig-lbx-cap:empty {
  display: none;
}
/* The page behind an open lightbox must not scroll under it. A class rather
   than an inline style write, so the rule is readable in one place. */
html.ig-lbx-open,
html.ig-lbx-open body {
  overflow: hidden;
}

@media (max-width: 640px) {
  /* The arrows would eat the picture's width at this size. Keyboard and swipe
     are gone with them, so the count reads "3 / 9" and the stage is the
     picture — tapping the scrim closes, which is the phone gesture anyway. */
  .ig-lbx-nav {
    position: absolute;
    bottom: -56px;
  }
  .ig-lbx-nav--prev {
    left: 0;
  }
  .ig-lbx-nav--next {
    right: 0;
  }
  .ig-lbx-cap {
    margin-top: 56px;
  }
}
