/* LUXVISION — Base reset, typography, and shared primitives */

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: auto; } /* Lenis owns smooth scroll; native is disabled */

body {
  margin: 0;
  background: var(--lv-bg);
  color: var(--lv-ink);
  font-family: var(--lv-font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img, video { display: block; max-width: 100%; }

h1, h2, h3, h4 {
  font-family: var(--lv-font-display);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.08;
  margin: 0;
}

p { margin: 0; }

a { color: inherit; text-decoration: none; }

.lv-wrap {
  max-width: var(--lv-max-width);
  margin: 0 auto;
  padding-left: var(--lv-space-gutter);
  padding-right: var(--lv-space-gutter);
}

.lv-section {
  position: relative;
  padding-top: var(--lv-space-section);
  padding-bottom: var(--lv-space-section);
}

/* Neutral by default - accent is reserved for a few deliberate moments
   (see .lv-eyebrow-accent) rather than colored on every section label. */
.lv-eyebrow {
  font-family: var(--lv-font-body);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--lv-ink-muted);
}

.lv-eyebrow-accent { color: var(--lv-accent); }

/* ── A SECTION HEADER, not an eyebrow ──────────────────────────────────────
   Joel, 15 September 2026: "I feel like 'Warranty' should be larger and in the
   cornflower colour. make this look like a section header."

   An eyebrow is a 0.78rem label that sits above a headline and gets out of the
   way. This is the opposite job: it names the chapter and is meant to be read
   as one. Roughly twice the size, same letterspacing so it still reads as a
   label rather than a second headline competing with the h2 under it.

   ⚠️ CORNFLOWER BY NAME, NOT var(--lv-accent). They are the same colour on the
   landing page and only there. Every product page re-points --lv-accent to its
   own line: PROLUX to lilac, ECOLUX to emerald. A section header that inherited
   the accent would come out purple or green the moment this class was reused,
   and nobody would notice until a screenshot.

   ⚠️ CONTRAST, MEASURED: #655af6 on #0b0b10 is 4.06:1. That clears WCAG AA for
   LARGE text (3:1) and does NOT clear it for body text (4.5:1). It is only
   sound here because this is large by definition. Do not reuse this colour at
   body size on this background — lilac is 6.48:1 and is the one that survives
   being made small. */
.lv-section-label {
  display: block;
  font-family: var(--lv-font-display);
  font-size: clamp(1.15rem, 2.3vw, 1.75rem);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1.1;
  color: var(--lv-cornflower);
}

.lv-h1 { font-size: clamp(2.6rem, 5.4vw, 4.6rem); }
.lv-h2 { font-size: clamp(2rem, 4vw, 3.2rem); }
.lv-h3 { font-size: clamp(1.3rem, 2vw, 1.7rem); }

.lv-body-lg {
  font-size: clamp(1.05rem, 1.4vw, 1.25rem);
  color: var(--lv-ink-muted);
  max-width: 62ch;
}

/* Buttons: pill shape (Shape Consistency Lock - interactive elements = pill) */
.lv-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.85rem 1.6rem;
  border-radius: var(--lv-radius-pill);
  font-family: var(--lv-font-body);
  font-weight: 700;
  font-size: 0.95rem;
  white-space: nowrap;
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform 0.35s var(--lv-ease), background 0.35s var(--lv-ease), border-color 0.35s var(--lv-ease);
}
.lv-btn:active { transform: scale(0.96); }

.lv-btn-primary {
  background: var(--lv-accent);
  color: #0b0b10;
}
.lv-btn-primary:hover { transform: translateY(-2px); }

.lv-btn-ghost {
  background: rgba(243, 242, 247, 0.05);
  border-color: var(--lv-border-strong);
  color: var(--lv-ink);
  backdrop-filter: blur(12px);
}
.lv-btn-ghost:hover { border-color: var(--lv-accent); }

.lv-tag-real {
  display: inline-block;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.2rem 0.55rem;
  border-radius: var(--lv-radius-sm);
  background: rgba(243, 242, 247, 0.08);
  color: var(--lv-ink-faint);
  border: 1px dashed var(--lv-border-strong);
}

/* Reusable frame for scroll-scrubbed images: clips the scale/parallax
   animation to the original layout footprint (see initScrubImages). */
.lv-img-frame { position: relative; overflow: hidden; border-radius: var(--lv-radius-lg); }
.lv-img-frame .lv-scrub-img { width: 100%; height: 100%; object-fit: cover; display: block; will-change: transform; }

/* Word-mask kinetic type reveal - see initSplitReveals */
.lv-word-mask { display: inline-block; overflow: hidden; vertical-align: top; padding-bottom: 0.08em; }
.lv-word-inner { display: inline-block; will-change: transform; }

::selection { background: var(--lv-accent-soft); color: var(--lv-ink); }

/* Film grain: fixed, pointer-events:none, never on a scrolling container
   (would tank mobile FPS) - adds cinematic texture to the dark base for
   almost no cost since it never repaints. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 999;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 250 250'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── footer ─────────────────────────────────────────────────────────────
   Here and not in landing.css because the same markup is on every page and
   only the landing page loads landing.css. */
.lv-footer { background: var(--lv-bg); border-top: 1px solid var(--lv-border); padding: 3.5rem 0; z-index: 10; position: relative; }
.lv-footer-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: 2rem; }
@media (max-width: 760px) { .lv-footer-grid { grid-template-columns: 1fr 1fr; } }

/* Footer links. Added 14 Sept 2026 with the real social accounts — the markup
   used a class that had no rule behind it, so the links would have rendered as
   default browser blue on a near-black footer. */
.lv-footer-link {
  color: var(--lv-ink-muted);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color .2s ease, border-color .2s ease;
}
.lv-footer-link:hover,
.lv-footer-link:focus-visible { color: var(--lv-ink); border-bottom-color: currentColor; }

/* ── SOCIAL BADGES ─────────────────────────────────────────────────────────
   14 September 2026. Joel: "Lets use the LinkedIn and YouTube logos for the
   social media links ... apply the same badge look and style so the site looks
   consistent." Then, seeing the first pass: "the monochrome is better but they
   need to have the same shading and glass effect as our Badges."

   ── WHY THE FIRST PASS LOOKED FLATTER THAN THE HERO BADGES ────────────────

   Same CSS recipe, different situation, and three real causes:

   1. **`backdrop-filter` had nothing to work on.** A hero badge sits over
      photography, so blurring and refracting its backdrop produces visible
      glass. The footer is flat near-black: refract nothing, get nothing. So the
      tile can no longer lean on its backdrop and has to carry its OWN light —
      a top-edge highlight, a diagonal body gradient and a dark lower right, all
      painted rather than sampled.

   2. **The mark was too small.** Hero glyphs fill 75% of the tile. 47% left the
      mark swimming, and a lot of what reads as "badge" is how far the glyph
      sits into the corners.

   3. **`transition: --p` did nothing at all.** A custom property is not
      animatable unless it has been registered with `@property`, so the hover
      snapped instead of easing. It was written, it looked reasonable, and the
      browser ignored it silently. Registered below.

   ⚠️ Kept as its own class rather than reusing `.lv-badge`: that one carries a
   flip, a card back, hover growth and a scroll target, none of which a footer
   link should inherit.

   ⛔ THE GLYPH ITSELF GETS NO TREATMENT. All of the above is the tile. See
   img/social/00_READ ME.
   ───────────────────────────────────────────────────────────────────────── */

/* Makes `--p` a real animatable number. Without this the transition is dropped
   without warning, which is exactly what happened. */
@property --p {
  syntax: "<number>";
  inherits: true;
  initial-value: 0.5;
}

.lv-social-row { display: flex; gap: 0.65rem; margin-top: 0.7rem; }

.lv-social {
  --p: 0.55;                      /* resting presence. 1 on hover */
  --tile: 46px;
  position: relative;
  display: grid;
  place-items: center;
  width: var(--tile);
  height: var(--tile);
  border-radius: 14px;
  text-decoration: none;
  isolation: isolate;
  transition: --p 0.34s cubic-bezier(0.3, 1.4, 0.55, 1),
              transform 0.34s cubic-bezier(0.3, 1.4, 0.55, 1);

  /* The body. Two gradients: the diagonal sheet of glass from the hero badge,
     and a soft top-left key light so the tile is lit from somewhere rather than
     evenly bright. */
  background:
    radial-gradient(120% 100% at 22% 8%,
      rgba(255,255,255,calc(0.20 * var(--p))) 0%,
      rgba(255,255,255,0) 58%),
    linear-gradient(148deg,
      rgba(255,255,255,calc(0.15 * var(--p))) 0%,
      rgba(255,255,255,calc(0.05 * var(--p))) 38%,
      rgba(255,255,255,calc(0.02 * var(--p))) 62%,
      rgba(255,255,255,calc(0.11 * var(--p))) 100%);

  /* The four edge lights, brightest on top, exactly as badge-strip.css does it,
     plus a cast shadow so the tile sits ON the footer rather than in it. */
  box-shadow:
    inset 0 1px 0 0 rgba(255,255,255,calc(0.40 * var(--p))),
    inset 1px 0 0 0 rgba(255,255,255,calc(0.17 * var(--p))),
    inset -1px 0 0 0 rgba(255,255,255,calc(0.11 * var(--p))),
    inset 0 -1px 0 0 rgba(255,255,255,calc(0.09 * var(--p))),
    inset 0 0 22px 6px rgba(255,255,255,calc(0.06 * var(--p))),
    inset 0 -14px 18px -12px rgba(0,0,0,calc(0.55 * var(--p))),
    0 12px 26px -10px rgba(0,0,0,calc(0.65 * var(--p)));
}
.lv-social:hover, .lv-social:focus-visible { --p: 1; transform: translateY(-2px); }
.lv-social:focus-visible { outline: 2px solid var(--lv-accent, #6fc6ef); outline-offset: 3px; }

/* Live refraction, same as the hero badges. It contributes little over a flat
   footer, which is why the body above does the work, but it costs nothing and
   comes alive anywhere the footer gains a background. */
.lv-social::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; z-index: 1;
  backdrop-filter: blur(calc(3px * var(--p)))
                   saturate(calc(100% + 50% * var(--p)))
                   brightness(calc(100% + 8% * var(--p)))
                   url(#lv-refract);
  -webkit-backdrop-filter: blur(calc(3px * var(--p)))
                           saturate(calc(100% + 50% * var(--p)));
  opacity: var(--p);
}
/* The diagonal sheen. */
.lv-social::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; z-index: 2; mix-blend-mode: screen;
  background: linear-gradient(122deg, transparent 26%, #ffffff2e 44%,
                              #ffffff08 55%, transparent 70%);
  opacity: var(--p);
}

.lv-social img, .lv-social svg {
  position: relative; z-index: 3;
  width: 62%; height: 62%;
  object-fit: contain;
  /* ⛔ The ONLY thing applied to the mark: how present it is. Both brands permit
     a solid one-colour icon on a dark background and prohibit recolouring,
     outlining, distorting or adding effects to it. The hero badges put a
     drop-shadow on their glyphs; these do not get one, because those glyphs are
     ours and these two are not. The glass around it does the seating instead. */
  opacity: calc(0.55 + 0.45 * var(--p));
  transition: opacity 0.34s ease;
}

/* Placeholder state, for a tile whose mark has not been dropped in yet. */
.lv-social.is-placeholder { border: 1px dashed rgba(255,255,255,0.22); }
.lv-social.is-placeholder span {
  position: relative; z-index: 3;
  font: 600 0.62rem/1 ui-monospace, monospace;
  letter-spacing: 0.06em;
  color: var(--lv-ink-muted, #8497a6);
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE WARRANTY DIALOG. Built by shared/warranty-modal.js.

   ⚠️ IT LIVES IN base.css, NOT landing.css. Every product page ends with a
   warranty claim, and the moment one of them gets a "full terms" button this
   has to already work there. Putting it in the landing stylesheet would make
   that a discovery rather than a decision.
   ═══════════════════════════════════════════════════════════════════════════ */
.lv-wm { position: fixed; inset: 0; z-index: 900; display: grid; place-items: center; }
.lv-wm[hidden] { display: none; }
/* The page behind must not scroll. A dialog you can scroll the page behind is
   a dialog people lose. */
html.lv-wm-open, html.lv-wm-open body { overflow: hidden; }

.lv-wm-scrim {
  position: absolute; inset: 0;
  background: rgba(4, 4, 8, 0.82);
  backdrop-filter: blur(3px);
}
.lv-wm-panel {
  position: relative;
  width: min(820px, 92vw);
  /* ⚠️ dvh, not vh. On iOS the address bar collapses and a vh-sized panel grows
     past the viewport mid-scroll, taking the pinned footer with it. */
  max-height: min(86dvh, 900px);
  display: grid;
  grid-template-rows: auto 1fr auto;   /* head · scrolling body · pinned foot */
  /* ⚠️ TRANSLUCENT, so the glass layer below has something to refract. An opaque
     panel with a backdrop-filter on top of it renders as a flat tint: the filter
     samples what is BEHIND the element, and an opaque fill means nothing behind
     it is ever visible. Glass is a hole, not a coating. */
  background: color-mix(in srgb, var(--lv-bg-raised) 72%, transparent);
  border: 1px solid var(--lv-border-strong);
  border-radius: 20px;
  overflow: hidden;
  /* ⚠️ NO `isolation: isolate` HERE, THOUGH IT LOOKS LIKE IT BELONGS. It would
     contain the sheen's screen blend, which is why it was written first. But
     `isolation: isolate` also creates a BACKDROP ROOT, and a backdrop root is
     the boundary a `backdrop-filter` is allowed to sample within. The glass
     child would then be filtering the panel's own background instead of the
     page behind the dialog: the entire effect silently becomes a flat tint,
     with no error and nothing in the CSS looking wrong.
     The sheen is contained by .lv-wm's own stacking context (z-index 900),
     which is a stacking context WITHOUT being a backdrop root. */
  box-shadow: 0 60px 120px -40px rgba(0,0,0,0.9),
              0 0 0 1px rgba(255,255,255,0.04);
}

/* ── THE GLASS EDGES ────────────────────────────────────────────────────────
   Joel, 15 September 2026: "design a cool pop out window that has the glass
   effect edges and lets the user scroll the info."

   ⚠️ EDGES, NOT THE WHOLE PANEL, AND THAT IS THE WHOLE DESIGN. Frosting the
   full sheet puts five thousand words of legal text on a surface that is
   sampling whatever happens to be behind it, and the contrast of every line
   then depends on where the visitor stopped scrolling. The refraction is
   masked to a band around the rim, and the reading column stays solid.

   Same recipe as the product badges in badge-strip.css, so the two read as one
   material. The `url(#lv-refract)` displacement filter is defined inline in
   each page's <svg>; browsers without it fall back to blur and saturate,
   which is the same effect minus the ripple. */
/* ⚠️ z-index 0 AND z-index 1 on the content, both explicit. The glass layers are
   `position: absolute` and the header, body and footer are static grid items, so
   by default the POSITIONED elements paint on top: the panel renders as a sheet
   of frosted glass with the terms hidden behind it. Nothing about that failure
   looks like a stacking bug when you see it. */
.lv-wm-glass {
  z-index: 0;
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  backdrop-filter: blur(16px) saturate(150%) brightness(112%) url(#lv-refract);
  -webkit-backdrop-filter: blur(16px) saturate(150%) brightness(112%);
  /* The mask IS the "edges" part: two identical layers, one covering the
     content box and one the whole element, subtracted from each other. What
     survives is a ring the thickness of the padding, so the refraction exists
     only where the panel meets the page and the reading column stays clear.

     ⚠️ `exclude` MUST NOT GO IN THE SHORTHAND. `mask-composite` is a longhand
     the `mask` shorthand resets, so it belongs on its own line after it — and
     `-webkit-mask` does not accept the `exclude` keyword at all, which makes
     the whole declaration invalid and drops the mask. An unmasked glass layer
     covers the entire panel: five thousand words of terms behind frosted
     glass, in WebKit only, which is the browser nobody checks last. */
  padding: 26px;                 /* = the thickness of the glass band */
  -webkit-mask: linear-gradient(#000 0 0) content-box,
                linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;   /* the old WebKit spelling of `exclude` */
  mask: linear-gradient(#000 0 0) content-box,
        linear-gradient(#000 0 0);
  mask-composite: exclude;
}
/* A lit rim: bright top-left, dark bottom-right, the way a real bevel catches
   a room. One inset shadow does both. */
.lv-wm-panel::after {
  content: "";
  z-index: 2;   /* the rim sits over everything, including the foot */
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: inset 1px 1px 0 rgba(255,255,255,0.14),
              inset -1px -1px 0 rgba(0,0,0,0.5),
              inset 0 0 40px rgba(101,90,246,0.07);
}
.lv-wm-sheen {
  z-index: 0;
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  mix-blend-mode: screen;
  background: linear-gradient(122deg, transparent 24%, #ffffff1f 42%,
                              #ffffff06 54%, transparent 68%);
}
/* ⚠️ Respect the setting. backdrop-filter on a panel this size is a live
   recomposite of everything behind it, which is exactly the class of effect
   that triggers discomfort for people who ask for less motion and drains a
   laptop battery. The panel simply goes solid. */
@media (prefers-reduced-motion: reduce) {
  .lv-wm-glass { backdrop-filter: none; -webkit-backdrop-filter: none; }
  .lv-wm-panel { background: var(--lv-bg-raised); }
}

.lv-wm-head, .lv-wm-body, .lv-wm-foot { position: relative; z-index: 1; }

.lv-wm-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 1.1rem clamp(1.1rem, 3vw, 2rem);
  border-bottom: 1px solid var(--lv-border);
}
.lv-wm-head h2 { font-size: clamp(1.05rem, 2.2vw, 1.4rem); margin: 0; }
.lv-wm-x {
  flex: none; width: 38px; height: 38px; border-radius: 50%;
  background: transparent; border: 1px solid var(--lv-border-strong);
  color: var(--lv-ink); font-size: 1.4rem; line-height: 1; cursor: pointer;
}
.lv-wm-x:hover { background: var(--lv-bg-raised-2); }

.lv-wm-body {
  overflow-y: auto; overscroll-behavior: contain;
  padding: clamp(1.1rem, 3vw, 2rem);
  font-size: 0.92rem; line-height: 1.62; color: var(--lv-ink-muted);
}
.lv-wm-body h3 {
  font-family: var(--lv-font-display); font-size: 1rem; color: var(--lv-ink);
  margin: 1.6rem 0 0.5rem;
}
.lv-wm-sec:first-child h3 { margin-top: 0; }
.lv-wm-body p, .lv-wm-body li { margin: 0 0 0.7rem; }
.lv-wm-body ul, .lv-wm-body ol { margin: 0 0 0.9rem; padding-left: 1.3rem; }
.lv-wm-body a { color: var(--lv-lilac); }
/* ⚠️ A RULE ABOVE, NOT BELOW, NOW THAT IT SITS AT THE FOOT. The border-bottom
   was separating it from the terms when it led; kept as-is at the end it would
   underline the last line in the panel and read as a cut-off page. */
.lv-wm-meta {
  font-size: 0.76rem; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--lv-ink-faint); border-bottom: 1px solid var(--lv-border);
  padding-bottom: 0.9rem;
}
.lv-wm-meta.is-footnote {
  border-bottom: 0;
  border-top: 1px solid var(--lv-border);
  padding-bottom: 0;
  padding-top: 1.1rem;
  margin-top: 1.8rem;
  font-size: 0.68rem;
  line-height: 1.6;
}
.lv-wm-table { width: 100%; border-collapse: collapse; margin: 0.4rem 0 1rem; }
.lv-wm-table th, .lv-wm-table td {
  text-align: left; padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--lv-border);
}
.lv-wm-table th { color: var(--lv-ink); font-size: 0.8rem; letter-spacing: 0.08em;
  text-transform: uppercase; }
.lv-wm-table td:last-child { color: var(--lv-ink); font-weight: 600; }

/* The impact clause is the one section a reader is looking for. */
.lv-wm-sec.is-key {
  border-left: 2px solid var(--lv-lilac);
  padding-left: 1rem; margin: 1.4rem 0;
}
.lv-wm-sec.is-key h3 { color: var(--lv-lilac); }

.lv-wm-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: 1rem clamp(1.1rem, 3vw, 2rem);
  border-top: 1px solid var(--lv-border);
  background: var(--lv-bg);
}
.lv-wm-note { font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--lv-ink-faint); }
.lv-wm-loading { color: var(--lv-ink-faint); }

/* ── THE SAMPLE CARD VIEW ───────────────────────────────────────────────────
   The same dialog, showing an image instead of the Agreement. One panel rather
   than two, because a second dialog would be a second focus trap, a second
   Escape handler and a second thing to keep in step with this one. */
.lv-wm-card { margin: 0; }
.lv-wm-card img {
  width: 100%;
  height: auto;
  border-radius: 10px;
  border: 1px solid var(--lv-border-strong);
  /* The certificate is white on its left half and sits on a near-black panel.
     Without a shadow the white edge reads as a hole cut in the dialog. */
  box-shadow: 0 24px 60px -24px rgba(0,0,0,0.85);
  /* ⚠️ Cosmetic, not protection. It stops an accidental drag-to-desktop; it does
     not stop a right-click, devtools, or a screenshot, and nothing on a web page
     does. The real answer is the SAMPLE stamp printed into the artefact. */
  -webkit-user-drag: none;
  user-select: none;
}
.lv-wm-card figcaption {
  margin-top: 1rem;
  font-size: 0.86rem;
  line-height: 1.6;
  color: var(--lv-ink-muted);
}
/* ── THE CATALOGUE, AS A STACK OF PAGES ────────────────────────────────────
   Twelve rendered spreads, scrolled in the same panel as everything else.
   ⚠️ width/height ARE ON THE <img> and must stay there. Without them each page
   has zero height until its bytes arrive, so the scroll container collapses,
   then grows twelve times as the lazy images land, and the reader's scroll
   position jumps under them on every one. */
.lv-wm-pages { display: flex; flex-direction: column; gap: clamp(0.75rem, 2vw, 1.25rem); }
.lv-wm-pages img {
  width: 100%;
  height: auto;
  border-radius: 6px;
  border: 1px solid var(--lv-border);
  background: var(--lv-bg);
  box-shadow: 0 18px 44px -22px rgba(0,0,0,0.8);
}
.lv-wm[data-view="catalogue"] .lv-wm-panel { width: min(900px, 94vw); }
/* The pages carry their own margin; the panel's reading padding is too generous
   for full-bleed artwork and wastes a tenth of the width on every page. */
.lv-wm[data-view="catalogue"] .lv-wm-body { padding: clamp(0.75rem, 2vw, 1.25rem); }

/* The card is landscape and wants the width; the terms want a reading measure. */
.lv-wm[data-view="card"] .lv-wm-panel { width: min(1040px, 94vw); }

@media (max-width: 620px) {
  .lv-wm-panel { width: 96vw; max-height: 92dvh; border-radius: 14px; }
  .lv-wm-foot { flex-direction: column; align-items: stretch; }
  .lv-wm-foot .lv-btn { text-align: center; }
}
