/* ============================================================================
   Fexo CRM — pre-launch landing page
   ============================================================================
   Loaded AFTER site.css and only by index.html. Everything here is either new
   to this page or a deliberate override of a site.css rule for the single-page
   layout.

   THE ONE RULE still applies: no hex values. Every colour resolves through a
   token defined in site.css, so all four themes keep working — including the
   light "daylight" theme, which is where hardcoded dark values would show up as
   invisible text.
   ========================================================================= */

/* ══ HEADER ADDITIONS ════════════════════════════════════════════════════ */

/* The "In development" chip. Says the quiet part in the furniture of the page
   rather than only in the hero copy, so it is still visible once the visitor
   has scrolled past the fold. */
.build-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--edge);
  border-radius: var(--radius-full);
  background: var(--panel);
  color: var(--ink-dim);
  font-size: var(--text-xs);
  font-weight: var(--weight-med);
  letter-spacing: .02em;
  white-space: nowrap;
}

.build-chip__dot {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--warning);
  box-shadow: 0 0 0 3px var(--warning-subtle);
  flex-shrink: 0;
}

/* Below 560px the chip and "Sign in" both compete with the WhatsApp button for
   a very narrow bar. The chip goes first: the hero says the same thing three
   lines later, so nothing is actually lost. */
@media (max-width: 560px) {
  .build-chip { display: none; }
}

/* ══ WHATSAPP BUTTON ═════════════════════════════════════════════════════ */

/* site.css sets `.btn svg { fill: none; stroke: currentColor }` for its own
   line-art icons. The WhatsApp glyph is a SOLID path, so it renders as an
   invisible smear under that rule and must invert both properties. */
.btn .wa-ico {
  width: 16px;
  height: 16px;
  fill: currentColor;
  stroke: none;
  flex-shrink: 0;
}

/* site.css nudges `.btn-primary svg` on hover — right for an arrow, wrong for
   a logo, which should sit still. */
.btn-primary:hover .wa-ico { transform: none; }

/* ══ HERO ════════════════════════════════════════════════════════════════ */

/* The other pages use a two-column hero-grid. This page is centred and single
   column, so the shared .hero paddings are re-stated rather than inherited. */
.hero-launch {
  position: relative;
  padding-block: clamp(var(--sp-16), 13vw, 132px) clamp(var(--sp-12), 9vw, 96px);
  text-align: center;
  overflow: hidden;
}

.hero-launch .shell {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.eyebrow-pulse {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--accent-bright);
  flex-shrink: 0;
  animation: pulse-soft 2.4s var(--ease) infinite;
}

@keyframes pulse-soft {
  0%, 100% { opacity: 1;  box-shadow: 0 0 0 0 var(--accent-glow); }
  50%      { opacity: .7; box-shadow: 0 0 0 5px transparent; }
}

.launch-h1 {
  margin-top: var(--sp-5);
  max-width: 16ch;
  font-size: clamp(38px, 7.2vw, 76px);
  line-height: 1.02;
  letter-spacing: -.038em;
  font-weight: var(--weight-bold);
  text-wrap: balance;
}

/* The one italic on the page. It carries the whole promise of the headline, so
   it gets the accent gradient and nothing else on the page competes with it. */
.launch-h1 em {
  display: block;
  font-style: italic;
  font-weight: var(--weight-semi);
  background: linear-gradient(103deg, var(--accent-bright), var(--ink));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* Safari drops the painted text if the box has zero descender room. */
  padding-bottom: .08em;
}

.launch-sub {
  margin-top: var(--sp-6);
  max-width: 62ch;
  color: var(--ink-dim);
  font-size: clamp(var(--text-md), 1.9vw, var(--text-xl));
  line-height: 1.62;
  text-wrap: pretty;
}

.launch-cta {
  margin-top: var(--sp-8);
  justify-content: center;
}

.launch-note {
  margin-top: var(--sp-4);
  color: var(--ink-faint);
  font-size: var(--text-sm);
}

/* ══ PROGRESS RAIL ═══════════════════════════════════════════════════════ */

/* Three states, one claimed complete. The fill stops at the middle step
   deliberately — a bar that reads as nearly-finished would be a promise we
   have not earned, and the visitor can count the dots. */
.rail {
  margin-top: clamp(var(--sp-10), 7vw, var(--sp-16));
  width: 100%;
  max-width: 560px;
}

.rail-track {
  position: relative;
  height: 3px;
  border-radius: var(--radius-full);
  background: var(--edge-soft);
  overflow: hidden;
}

.rail-fill {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 50%;
  border-radius: var(--radius-full);
  background: linear-gradient(90deg, var(--accent), var(--accent-bright));
  transform-origin: left center;
  animation: rail-grow 1100ms var(--ease-out) both;
}

@keyframes rail-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.rail-steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  margin-top: var(--sp-4);
  list-style: none;
}

.rail-step {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  text-align: center;
}

.rail-dot {
  width: 9px;
  height: 9px;
  border-radius: var(--radius-full);
  background: var(--edge-strong);
  margin-bottom: var(--sp-1);
}

.rail-step.is-done .rail-dot { background: var(--success); }

.rail-step.is-now .rail-dot {
  background: var(--accent-bright);
  box-shadow: 0 0 0 4px var(--accent-subtle);
}

.rail-k {
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  color: var(--ink-dim);
}

.rail-step.is-done .rail-k,
.rail-step.is-now  .rail-k { color: var(--ink); }

.rail-v {
  font-size: var(--text-xs);
  color: var(--ink-mute);
  letter-spacing: .02em;
}

/* ══ SECTIONS ════════════════════════════════════════════════════════════ */

.section { padding-block: clamp(var(--sp-16), 10vw, 112px); }

.section-alt {
  background: var(--panel);
  border-block: 1px solid var(--edge-soft);
}

.section-head {
  max-width: 68ch;
  margin-bottom: clamp(var(--sp-10), 6vw, var(--sp-16));
}

.section-k {
  color: var(--accent-bright);
  font-size: var(--text-sm);
  font-weight: var(--weight-semi);
  letter-spacing: .085em;
  text-transform: uppercase;
  margin-bottom: var(--sp-3);
}

.section-head h2 {
  font-size: clamp(27px, 4vw, 42px);
  line-height: 1.14;
  letter-spacing: -.028em;
  font-weight: var(--weight-bold);
  text-wrap: balance;
}

.section-p {
  margin-top: var(--sp-4);
  color: var(--ink-dim);
  font-size: var(--text-md);
  line-height: 1.68;
  text-wrap: pretty;
}

/* ══ CARDS ═══════════════════════════════════════════════════════════════ */

.card { position: relative; }

.card-num {
  display: block;
  margin-bottom: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--accent-bright);
  opacity: .75;
  letter-spacing: .06em;
}

/* ── Feature cards ─────────────────────────────────────────────────────── */

.feat { display: flex; flex-direction: column; }

/* The status tag is the honesty mechanism of this page: "Working now" and
   "Planned" must never look alike at a glance, which is why they differ in
   colour AND in weight rather than colour alone. */
.feat-tag {
  align-self: flex-start;
  margin-bottom: var(--sp-4);
  padding: 3px var(--sp-3);
  border: 1px solid var(--edge);
  border-radius: var(--radius-full);
  background: var(--ground);
  color: var(--ink-mute);
  font-size: var(--text-xs);
  font-weight: var(--weight-med);
  letter-spacing: .03em;
  white-space: nowrap;
}

.feat.is-live .feat-tag {
  border-color: transparent;
  background: var(--success-subtle);
  color: var(--success);
  font-weight: var(--weight-semi);
}

.feat.is-build .feat-tag {
  border-color: transparent;
  background: var(--accent-subtle);
  color: var(--accent-bright);
  font-weight: var(--weight-semi);
}

/* A live feature gets a lit top edge. Decorative only — the tag carries the
   meaning, so this is not the sole signal. */
.feat.is-live::before {
  content: "";
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 2px;
  border-start-start-radius: inherit;
  border-start-end-radius: inherit;
  background: linear-gradient(90deg, transparent, var(--success), transparent);
  opacity: .55;
}

/* ── Pillars ───────────────────────────────────────────────────────────── */

.pillar h3 { margin-bottom: var(--sp-3); }

/* ══ CTA ═════════════════════════════════════════════════════════════════ */

.section-cta { padding-block: clamp(var(--sp-16), 11vw, 128px); }

.cta-box {
  position: relative;
  max-width: 720px;
  margin-inline: auto;
  padding: clamp(var(--sp-8), 5vw, var(--sp-16));
  border: 1px solid var(--edge);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(120% 130% at 50% 0%, var(--sky-1), transparent 68%),
    var(--panel);
  box-shadow: var(--shadow-lg);
  text-align: center;
  overflow: hidden;
}

.cta-box .section-k,
.cta-box .section-p { margin-inline: auto; }

.cta-box .section-p { max-width: 52ch; }

.cta-box h2 {
  margin-top: var(--sp-2);
  font-size: clamp(24px, 3.4vw, 36px);
  line-height: 1.16;
  letter-spacing: -.026em;
  font-weight: var(--weight-bold);
  text-wrap: balance;
}

.cta-box .btn { margin-top: var(--sp-8); }

.cta-fine {
  margin-top: var(--sp-4);
  color: var(--ink-faint);
  font-size: var(--text-sm);
}

/* ══ FOOTER ══════════════════════════════════════════════════════════════ */

/* This page has no footer link columns, so the bottom row IS the footer and
   needs the top border the .footer-grid normally provides. */
.footer-launch { border-top: 0; padding-top: 0; }

.footer-note { color: var(--ink-mute); }

/* ══ REVEAL ══════════════════════════════════════════════════════════════ */

/* ⚠️ Hidden state is scoped to .js-reveal, which the inline script adds to
   <html> only once it is actually running. Without that guard a blocked or
   failed script leaves every section permanently invisible — the page would
   look empty and nothing would report an error. Visible-by-default is the
   safe direction for content. */
.js-reveal .reveal {
  opacity: 0;
  transform: translateY(18px);
  transition:
    opacity   620ms var(--ease-out),
    transform 620ms var(--ease-out);
}

.js-reveal .reveal.is-in {
  opacity: 1;
  transform: none;
}

/* Stagger within a group. Applied as a transition-delay rather than an
   animation-delay so an element revealed on scroll is never mid-flight when it
   enters. */
.js-reveal .reveal[data-d="1"] { transition-delay:  70ms; }
.js-reveal .reveal[data-d="2"] { transition-delay: 140ms; }
.js-reveal .reveal[data-d="3"] { transition-delay: 210ms; }
.js-reveal .reveal[data-d="4"] { transition-delay: 280ms; }
.js-reveal .reveal[data-d="5"] { transition-delay: 350ms; }
.js-reveal .reveal[data-d="6"] { transition-delay: 420ms; }

/* ══ PRINT ═══════════════════════════════════════════════════════════════ */

/* site.css's print block hides `.sky` and flattens `.card`, but it predates
   this page and knows nothing about the elements below. Without this, a printed
   copy carries a filled CTA slab, a progress bar that means nothing on paper,
   and — worst — every .reveal frozen at opacity 0, because print takes no
   scroll events so nothing ever gets .is-in. That last one prints a blank page. */
@media print {
  .js-reveal .reveal {
    opacity: 1 !important;
    transform: none !important;
  }

  .rail,
  .build-chip { display: none !important; }

  .cta-box {
    background: none !important;
    box-shadow: none !important;
    border-color: var(--edge-strong) !important;
  }

  /* The gradient-clipped headline paints as transparent text on paper. */
  .launch-h1 em {
    background: none;
    -webkit-text-fill-color: currentColor;
    color: var(--ink);
  }
}

/* ══ MOTION PREFERENCE ═══════════════════════════════════════════════════ */

/* The script already bails out before adding .js-reveal when reduced motion is
   requested. This is the belt-and-braces half: it also stops the two ambient
   animations, which run from CSS alone and would otherwise continue. */
@media (prefers-reduced-motion: reduce) {
  .js-reveal .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .eyebrow-pulse,
  .rail-fill { animation: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   See it — real product screenshots (2026-08-19)
   ══════════════════════════════════════════════════════════════════════════
   Every claim above this section is words. This is the section that answers
   "what am I actually buying?", so the images are given room: one wide shot
   that carries the product's whole shape, then a grid of the pieces. */

.section-shots { background: var(--panel); }

@media (prefers-reduced-motion: reduce) {
  .pshot, .pshot:hover { transform: none; transition: none; }
}


/* ── Hero product shot ─────────────────────────────────────────────────
   The first proof on the page. Tilted very slightly and lifted off the
   background so it reads as a screen rather than a flat image pasted in,
   but not so much that the text inside it stops being legible — a
   screenshot nobody can read proves nothing. */
.hero-shot {
  margin: 48px auto 0;
  max-width: 1080px;
  position: relative;
  border-radius: 14px;
  /* Clips the image to the rounded frame; without it the square corners
     of the JPG sit proud of the border. */
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.11);
  background: #0b1120;
  box-shadow:
    0 2px 8px rgba(0, 0, 0, 0.32),
    0 24px 60px -18px rgba(0, 0, 0, 0.72),
    0 0 0 1px rgba(255, 255, 255, 0.03) inset;
}
.hero-shot img {
  display: block;      /* kills the inline-image baseline gap */
  width: 100%;
  height: auto;
}
/* ⚠️ NO overlay gradient here. A translucent wash across the top of the
   frame reads as a grey shade dulling the screenshot — the product shot is
   the proof on this page and nothing may sit on top of it. The border and
   shadow already separate the frame from the background. */
@media (max-width: 720px) {
  .hero-shot { margin-top: 32px; border-radius: 10px; }
}


/* ══ PRODUCT CAROUSEL ══════════════════════════════════════════════════════
   ⚠️ Class prefix is `cslide`/`car-`, NOT `.shot`. site.css already owns
   `.shot` for an abstract decorative tile whose ::before/::after paint an
   accent floor-line and window rectangle — reusing that name painted those
   blocks straight over the real screenshots. That was the "shade on the
   picture". Never name anything here `.shot`.

   ⚠️ NOTHING is layered on top of a screenshot. No gradient wash, no scrim,
   no tint. These images ARE the argument the page is making; anything that
   dulls them defeats the section. */

.carousel {
  margin-top: 34px;
}

.carousel-stage {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
}

.carousel-track {
  flex: 1;
  min-width: 0;              /* lets the flex child actually shrink */
  border-radius: 18px;
  border: 1px solid var(--edge);
  background: var(--panel);
  overflow: hidden;
  box-shadow: 0 24px 64px -20px rgba(0, 0, 0, 0.66);
}

/* ── A slide ───────────────────────────────────────────────────────────── */

/* ⚠️ `[hidden]` MUST win over this rule. The HTML `hidden` attribute is only
   a UA stylesheet `display: none`, so ANY later `display` declaration — this
   `grid` included — silently overrides it and every slide renders stacked
   down the page. Declared first, and re-asserted below with !important. */
.cslide[hidden] { display: none !important; }

.cslide {
  margin: 0;
  display: grid;
  /* Image leads, explanation sits beside it. At this ratio the screenshot is
     large enough to actually read, which is the entire point. */
  grid-template-columns: minmax(0, 1.62fr) minmax(0, 1fr);
  align-items: stretch;
}

/* The entrance. Applied on the ACTIVE slide only, so switching slides
   replays it rather than animating everything at once on load. */
.cslide.is-active {
  animation: cslide-in 620ms cubic-bezier(.16, 1, .3, 1) both;
}
@keyframes cslide-in {
  from { opacity: 0; transform: translateY(10px) scale(.994); }
  to   { opacity: 1; transform: none; }
}

.cslide-media {
  position: relative;
  overflow: hidden;
  background: #0b1120;
  /* Pins the frame to a consistent shape so the carousel never jumps height
     between slides — a stage that resizes on every advance reads as broken. */
  aspect-ratio: 16 / 10;
}
.cslide-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left top;   /* keeps the sidebar and header in frame */
}

/* A slow drift across the screenshot while its slide is showing — the "live"
   motion, kept subtle because a moving screenshot you cannot read is worse
   than a still one you can. */
.cslide.is-active .cslide-media img {
  animation: cslide-drift 9s ease-in-out both;
}
@keyframes cslide-drift {
  from { transform: scale(1.015) translate3d(0, 0, 0); }
  to   { transform: scale(1.055) translate3d(-1.2%, -1.4%, 0); }
}

.cslide-body {
  padding: 34px 34px 30px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
}
.cslide-k {
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-bright, #4f8cff);
  font-weight: 700;
}
.cslide-body h3 {
  margin: 0;
  font-size: clamp(19px, 2.1vw, 25px);
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.cslide-body p {
  margin: 0;
  font-size: 15px;
  line-height: 1.68;
  color: var(--ink-dim);
}

/* Stagger the text in behind the slide itself. */
.cslide.is-active .cslide-k    { animation: cs-up 560ms 90ms  var(--ease-out, ease) both; }
.cslide.is-active .cslide-body h3 { animation: cs-up 560ms 160ms var(--ease-out, ease) both; }
.cslide.is-active .cslide-body p  { animation: cs-up 560ms 230ms var(--ease-out, ease) both; }
@keyframes cs-up {
  from { opacity: 0; transform: translateY(9px); }
  to   { opacity: 1; transform: none; }
}

/* ── Arrows ────────────────────────────────────────────────────────────── */

.car-nav {
  flex: none;
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--edge);
  background: var(--panel-raised, var(--panel));
  color: var(--ink-dim);
  cursor: pointer;
  transition: background 180ms var(--ease, ease), color 180ms var(--ease, ease),
              border-color 180ms var(--ease, ease), transform 180ms var(--ease, ease);
}
.car-nav svg { width: 21px; height: 21px; }
.car-nav:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  transform: scale(1.06);
}
.car-nav:focus-visible {
  outline: 2px solid var(--accent-bright, #4f8cff);
  outline-offset: 3px;
}

/* ── Dots + play/pause ─────────────────────────────────────────────────── */

.carousel-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 18px;
  padding: 0 60px;             /* clears the arrow columns */
}

.car-dots {
  display: flex;
  gap: 9px;
  flex-wrap: wrap;
}
.car-dot {
  width: 30px;
  height: 5px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--edge-strong, #3a4a72);
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: background 200ms var(--ease, ease), width 260ms var(--ease-out, ease);
}
.car-dot:hover { background: var(--ink-mute, #5b6883); }
.car-dot:focus-visible {
  outline: 2px solid var(--accent-bright, #4f8cff);
  outline-offset: 3px;
}
.car-dot.is-on { width: 56px; background: var(--edge-strong, #3a4a72); }

/* The fill IS the timer — the viewer can see how long the slide has left,
   so the carousel never feels like it moved without warning. */
.car-dot.is-on::after {
  content: "";
  position: absolute;
  inset: 0;
  transform-origin: left center;
  background: linear-gradient(90deg, var(--accent), var(--accent-bright, #4f8cff));
  animation: car-fill var(--car-dur, 6s) linear both;
}
.car-dot.is-on.is-paused::after { animation-play-state: paused; }
@keyframes car-fill {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.car-play {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--ink-faint);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: color 180ms var(--ease, ease), border-color 180ms var(--ease, ease);
}
.car-play:hover { color: var(--ink); border-color: var(--edge-strong, #3a4a72); }
.car-play:focus-visible {
  outline: 2px solid var(--accent-bright, #4f8cff);
  outline-offset: 3px;
}
/* Two bars = pause; swapped to a triangle when the carousel is stopped. */
.car-play-ico {
  width: 9px; height: 10px;
  border-left: 3px solid currentColor;
  border-right: 3px solid currentColor;
}
.car-play[aria-pressed="false"] .car-play-ico {
  width: 0; height: 0;
  border: 5px solid transparent;
  border-left: 8px solid currentColor;
  border-right: 0;
}

/* ── Narrow ────────────────────────────────────────────────────────────── */

@media (max-width: 900px) {
  .cslide { grid-template-columns: minmax(0, 1fr); }
  .cslide-body { padding: 22px 22px 24px; }
  /* Arrows overlay the image on small screens — side columns would leave the
     screenshot too narrow to read, which is the whole point of the section. */
  .carousel-stage { gap: 0; }
  .car-nav {
    position: absolute;
    top: 32%;
    z-index: 2;
    background: rgba(11, 17, 32, 0.72);
    backdrop-filter: blur(6px);
  }
  .car-prev { left: 10px; }
  .car-next { right: 10px; }
  .carousel-foot { padding: 0; }
}

/* ⚠️ Reduced motion: no drift, no entrance, and the JS also stops autoplay.
   A carousel that keeps advancing by itself is exactly what this preference
   is asking us not to do. */
@media (prefers-reduced-motion: reduce) {
  .cslide.is-active,
  .cslide.is-active .cslide-media img,
  .cslide.is-active .cslide-k,
  .cslide.is-active .cslide-body h3,
  .cslide.is-active .cslide-body p,
  .car-dot.is-on::after {
    animation: none !important;
  }
  .car-nav:hover { transform: none; }
}


/* ── Click-to-zoom ─────────────────────────────────────────────────────── */

/* A transparent button covering the screenshot. Transparent so nothing sits
   on top of the image — the whole reason the old overlay was removed — but
   still a real <button>, so it is focusable and announced. */
.cslide-zoom {
  position: absolute;
  inset: 0;
  z-index: 2;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: zoom-in;
}
.cslide-zoom:focus-visible {
  outline: 2px solid var(--accent-bright, #4f8cff);
  outline-offset: -4px;
}

/* A small hint, so it is discoverable that the shot opens. Sits in the
   corner over the frame, NOT across the image. */
.cslide-zoom::after {
  content: "⤢";
  position: absolute;
  right: 12px;
  bottom: 12px;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  font-size: 15px;
  color: #fff;
  background: rgba(11, 17, 32, 0.74);
  border: 1px solid rgba(255, 255, 255, 0.16);
  backdrop-filter: blur(6px);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 200ms var(--ease, ease), transform 200ms var(--ease, ease);
}
.cslide-media:hover .cslide-zoom::after,
.cslide-zoom:focus-visible::after {
  opacity: 1;
  transform: none;
}

/* ── Lightbox ──────────────────────────────────────────────────────────── */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  padding: 4vh 4vw;
  background: rgba(4, 8, 18, 0.9);
  backdrop-filter: blur(10px);
  animation: lb-in 220ms var(--ease-out, ease) both;
}
/* ⚠️ `[hidden]` must beat `display: grid` — the same trap that made every
   carousel slide render stacked. */
.lightbox[hidden] { display: none !important; }

@keyframes lb-in { from { opacity: 0; } to { opacity: 1; } }

.lb-figure {
  margin: 0;
  max-width: 1500px;
  width: 100%;
  animation: lb-pop 300ms var(--ease-out, ease) both;
}
@keyframes lb-pop {
  from { opacity: 0; transform: scale(.97) translateY(8px); }
  to   { opacity: 1; transform: none; }
}

.lb-media {
  border-radius: 14px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.13);
  background: #0b1120;
  box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.8);
}
.lb-media img {
  display: block;
  width: 100%;
  height: auto;
  /* Never taller than the viewport: a "full size" view the reader has to
     scroll to see is not a preview. */
  max-height: 82vh;
  object-fit: contain;
}

.lb-cap {
  margin: 14px auto 0;
  max-width: 760px;
  text-align: center;
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-dim, #a8b4cb);
}

.lb-close {
  position: fixed;
  top: 18px;
  right: 18px;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(11, 17, 32, 0.72);
  color: #fff;
  cursor: pointer;
  transition: background 180ms var(--ease, ease), transform 180ms var(--ease, ease);
}
.lb-close svg { width: 22px; height: 22px; }
.lb-close:hover { background: var(--accent); transform: scale(1.06); }
.lb-close:focus-visible {
  outline: 2px solid var(--accent-bright, #4f8cff);
  outline-offset: 3px;
}

/* Locks the page behind the dialog. Without it the body scrolls under the
   overlay while the reader thinks they are panning the image. */
body.lb-open { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .lightbox, .lb-figure { animation: none !important; }
  .lb-close:hover { transform: none; }
}
