/* Manifest Engine v0.1 — all section styles. Theme tokens arrive as CSS vars
   set by theme.js; hosts may also set them directly on .me-root. */

.me-root {
  --me-accent: #ffc20e;
  --me-surface: #261d26;

  --me-light-bg: #faf8f5;
  --me-light-ink: #221a22;
  --me-dark-bg: var(--me-surface);
  --me-dark-ink: #f5f1ea;

  /* Default sizes, one token per heading level plus paragraph text. A theme
     can override any of these (theme.js sets them as inline styles on this
     same element), so every section reads its size from the matching
     var() instead of its own hardcoded clamp() — meaning every h2 across
     every section is now the same size by default, where before each
     component had its own. Defaults below pick the most representative of
     each level's prior per-component values; h1 is unchanged (hero is the
     only h1), h4-h6 are new (previously no component used those tags). */
  --me-size-h1: clamp(2.5rem, 7vw, 5.5rem);
  --me-size-h2: clamp(1.8rem, 4vw, 3rem);
  --me-size-h3: clamp(1.4rem, 3vw, 2.2rem);
  --me-size-h4: 1.3rem;
  --me-size-h5: 1.1rem;
  --me-size-h6: 1rem;
  --me-size-p: 1.05rem;

  /* Typography is deliberately NOT set: the engine inherits the host page's
     body and heading faces so a section looks native to the site it sits in.
     Note this is the opposite of the colour handling above — there the
     section theme must win, so it declares explicitly; here the site must
     win, so the engine declares nothing. Never add `font-family: inherit`
     to the heading rule: that would force headings into the body face and
     override the site's heading font. */
  line-height: 1.6;
}

.me-root * { box-sizing: border-box; margin: 0; }

/* The per-section colour points (surface / ink / headingColor) arrive as
   inline vars on the section node, so each chain falls back to the theme
   token when the author left the field blank. */
.me-section[data-theme='light'] { background: var(--me-section-surface, var(--me-light-bg)); color: var(--me-section-ink, var(--me-light-ink)); --me-fx-ink: var(--me-section-ink, var(--me-light-ink)); }
.me-section[data-theme='dark'] { background: var(--me-section-surface, var(--me-dark-bg)); color: var(--me-section-ink, var(--me-dark-ink)); --me-fx-ink: var(--me-section-ink, var(--me-dark-ink)); }

/* Sizes and rhythm are the engine's; the face is the site's. Any heading in
   the manifest can render at any level (the editor's per-heading level
   chooser), so this covers h1-h6, not just the levels sections used to
   hardcode. */
.me-root h1, .me-root h2, .me-root h3, .me-root h4, .me-root h5, .me-root h6 {
  line-height: 1.1;
  font-weight: 700;
}

/* Font size is keyed purely on the rendered tag, not on which component or
   class it belongs to — a heading is the same size everywhere on the page
   regardless of which section or class renders it (see the theme system
   note above --me-size-h1). Component rules below must NOT set font-size
   themselves. */
.me-root h1 { font-size: var(--me-size-h1); }
.me-root h2 { font-size: var(--me-size-h2); }
.me-root h3 { font-size: var(--me-size-h3); }
.me-root h4 { font-size: var(--me-size-h4); }
.me-root h5 { font-size: var(--me-size-h5); }
.me-root h6 { font-size: var(--me-size-h6); }

/* Host themes and page builders (Breakdance, Elementor, block themes) almost
   always set `h1,h2,h3 { color }` globally. An inherited value loses to any
   rule matching the element directly, however weak — so text inside a section
   must claim its colour rather than inheriting it, or the section's theme is
   overridden. Split into a heading half and a body half so the per-section
   headingColor / ink vars reach each directly; with the vars unset, both
   compute exactly the old `color: inherit`. `:not(.me-fx-fill)` keeps
   headingFx headings transparent so the clipped gradient still shows. */
.me-root .me-section
  :is(h1, h2, h3, h4, h5, h6):not(.me-fx-fill) {
  color: var(--me-section-heading, inherit);
}
.me-root .me-section
  :is(p, li, figcaption, summary, strong, em, blockquote, dt, dd):not(.me-fx-fill):not(.me-crossfadeStack__caption) {
  color: var(--me-section-ink, inherit);
}

/* ---- anima entrance vocabulary ----
   fade  = opacity only            rise  = opacity + lift
   focus = opacity + lift + a blur that resolves, like a focus pull
   Easing is a quint ease-out: quick to leave, long slow settle, which is what
   makes an entrance read as considered rather than as a slideshow. Stagger
   index is assigned per sibling group by anima.js. */
[data-anima] {
  --me-anima-ease: cubic-bezier(0.22, 1, 0.36, 1);
  opacity: 0;
  transition:
    opacity 0.85s var(--me-anima-ease),
    transform 0.85s var(--me-anima-ease),
    filter 0.85s var(--me-anima-ease);
  transition-delay: calc(var(--me-stagger, 0) * 150ms);
}
/* sibling-index() is Baseline Newly Available (Chrome 138, Safari 26.2,
   Firefox 154 Aug-2026). Use it for stagger so anima.js doesn't need to
   assign --me-stagger per element. Browsers without it fall back to the
   var() above, which defaults to 0 — elements animate simultaneously
   instead of sequentially. */
@supports (transition-delay: calc(sibling-index() * 1ms)) {
  [data-anima] {
    transition-delay: calc(sibling-index() * 150ms);
  }
}
[data-anima='rise'],
[data-anima='slideUp'] { transform: translateY(var(--me-rise-y, 1.75rem)); }
[data-anima='focus'] {
  transform: translateY(1.25rem) scale(0.985);
  filter: blur(6px);
}
[data-anima].in { opacity: 1; transform: none; filter: none; }

/* Above the fold at mount: start painted so the element can be the Largest
   Contentful Paint immediately, and travel in from there. See anima.js —
   the fade is the only part of the entrance that costs anything.

   Dropping opacity from transition-property is load-bearing, not tidying.
   anima.js measures each element before tagging it, and that measurement
   flushes style — which makes opacity:0 a real computed value the browser
   will happily transition *away from* over 0.85s, reintroducing the exact
   delay this rule exists to remove. Excluded from the after-change style,
   no transition is generated and the opacity simply snaps. */
[data-anima][data-anima-paint] {
  opacity: 1;
  transition-property: transform, filter;
}

@media (prefers-reduced-motion: reduce) {
  [data-anima] { opacity: 1; transform: none; filter: none; transition: none; }
}

/* Scroll-driven entrance animations: browsers that support
   animation-timeline: view() get entrances scrubbed directly to
   scroll position, with no JS IntersectionObserver. The animation
   reverses when scrolling back up — the element follows the scroll
   rather than firing once and staying put.

   animation-range: entry 12% entry 100% matches the old JS
   rootMargin: 0px 0px -12% 0px — the animation starts once the
   element is 12% into the viewport and completes when it's fully
   entered.

   Each effect gets its own @keyframes so the browser only animates
   the properties that effect actually changes — a fade doesn't pay
   for a transform it doesn't use. */
@supports (animation-timeline: view()) {
  [data-anima] {
    opacity: 1;
    transition: none;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 25% contain 20%;
    /* entry 25%: hold back until the element is a quarter into the
       viewport rather than firing the moment it peeks in (the old
       12% was the JS rootMargin equivalent — too early for
       scroll-driven). contain 20%: extend the animation past full
       entry so the reader has time to see it play out, rather than
       it completing the instant the element is fully in view.

       No animation-delay here, deliberately: under a scroll timeline,
       animation-delay is converted to a scroll-offset using an
       implementation-defined "effective velocity," which produces a
       negligible, browser-dependent offset — not real stagger. Grid
       siblings (cardRow) also share a vertical scroll position, so
       there's no scroll-position-based way to sequence them anyway.
       Stacked elements (mediaList items) stagger naturally, each
       entering at its own scroll position. */
  }
  [data-anima='fade'] {
    animation-name: me-anima-fade;
  }
  [data-anima='rise'],
  [data-anima='slideUp'] {
    animation-name: me-anima-rise;
  }
  [data-anima='focus'] {
    animation-name: me-anima-focus;
  }

  /* cardRow stagger: siblings in the same grid row share a vertical
     scroll position, so animation-delay can't sequence them (see the
     note on the base rule above). animation-range can, though — it's
     already position-based, so shifting each column's entry/contain
     window by a fixed offset staggers the cascade without needing a
     time delay at all. nth-child(4n+N) keys stagger to column position
     rather than raw DOM order, so multi-row grids restart the cascade
     each row instead of drifting later with every row (the row's own
     lower position on the page already staggers row-to-row). Capped at
     4 columns to match the manifest schema's columns maximum. */
  .me-cardRow__grid > [data-anima]:nth-child(4n+2) {
    animation-range: entry 40% contain 35%;
  }
  .me-cardRow__grid > [data-anima]:nth-child(4n+3) {
    animation-range: entry 55% contain 50%;
  }
  .me-cardRow__grid > [data-anima]:nth-child(4n+4) {
    animation-range: entry 70% contain 65%;
  }

  @keyframes me-anima-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  @keyframes me-anima-rise {
    0%   { opacity: 0; transform: translateY(var(--me-rise-y, 1.75rem)); }
    /* Fade in across the first 30% so the element is fully visible
       while the remaining 70% of the animation range plays out the
       translate — otherwise the translate is invisible behind a
       still-fading element. */
    30%  { opacity: 1; transform: translateY(var(--me-rise-y, 1.75rem)); }
    100% { opacity: 1; transform: translateY(0); }
  }
  @keyframes me-anima-focus {
    from { opacity: 0; transform: translateY(1.25rem) scale(0.985); filter: blur(6px); }
    to   { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
  }

  /* Above-fold LCP: elements on screen at mount are already past the
     entry range and would resolve to the end state — but paint them
     immediately to avoid any frame-one flash while the timeline
     resolves. */
  [data-anima-paint] {
    animation: none;
    opacity: 1;
    transform: none;
    filter: none;
  }

  /* mediaList separator rules: pseudo-elements depend on .in on the
     parent <li>, so they need their own scroll-driven animation when
     the IntersectionObserver is skipped. No animation-delay needed —
     each <li> enters at its own scroll position, so the items (and
     their separators) already stagger naturally. */
  .me-mediaList__item::after {
    transition: none;
    animation: me-mediaList-rule both;
    animation-timeline: view();
    animation-range: entry 25% contain 20%;
  }
  @keyframes me-mediaList-rule {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }

  @media (prefers-reduced-motion: reduce) {
    [data-anima] {
      animation: none;
      opacity: 1;
      transform: none;
      filter: none;
    }
    .me-mediaList__item::after {
      animation: none;
      transform: scaleX(1);
    }
  }
}

/* ---- headingFx: scroll-driven colour fill ---- */
/* A two-stop gradient clipped to the glyphs; --me-fill slides it so the
   text fills left-to-right. currentColor can't be used inside the gradient
   (background-clip requires transparent text), so sections expose their ink
   as --me-fx-ink. */
.me-fx-fill {
  --me-fill: 0;
  --me-fx-to: var(--me-fx-ink);
  background-image: linear-gradient(
    90deg,
    var(--me-fx-to) 46%,
    color-mix(in srgb, var(--me-fx-ink) 22%, transparent) 54%
  );
  background-size: 200% 100%;
  background-repeat: no-repeat;
  background-position-x: calc(100% - var(--me-fill) * 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}
.me-fx-fill--accent { --me-fx-to: var(--me-accent); }

/* ---- hero ---- */
.me-hero { position: relative; }
.me-hero__sticky {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  display: grid; place-items: center;
}
/* top/left rather than inset:0 so the drift custom property (--me-hero-drift,
   set by hero.js from the manifest's `parallax` field) can grow the height
   without fighting a bottom offset — grown by exactly the drift distance so
   translating the media upward through the pin never reveals its bottom
   edge. Falls back to 0px (i.e. plain height:100%) when parallax is unset. */
.me-hero__media {
  position: absolute; top: 0; left: 0; width: 100%;
  height: calc(100% + var(--me-hero-drift, 0px));
  object-fit: cover; will-change: transform;
}
/* The video that replaces the poster after load. Two nearly identical
   pictures, so the crossfade is short; under reduced motion it is a cut. */
.me-hero__media--upgrade { opacity: 0; transition: opacity 0.6s ease; }
.me-hero__media--upgrade.is-ready { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .me-hero__media--upgrade { transition: none; }
}
/* A real element, not ::after on the media — pseudo-elements generate no box
   on replaced elements, so the original scrim never rendered at all. */
.me-hero__scrim {
  position: absolute; inset: 0;
  background: rgba(0 0 0 / var(--me-scrim, 0.35));
}
.me-hero__content {
  position: relative; z-index: 1; text-align: center;
  padding: 0 1.5rem; max-width: 60rem; will-change: transform, opacity;
}
.me-hero[data-theme] .me-hero__content { color: var(--me-section-ink, #fff); text-shadow: 0 2px 24px rgba(0 0 0 / 0.45); }
.me-hero__heading { letter-spacing: -0.02em; }
.me-hero__subtext { font-size: var(--me-size-p); margin-top: 1.25rem; opacity: 0.9; }
.me-hero__cue {
  position: absolute; bottom: 2rem; left: 50%; z-index: 1;
  width: 1.5rem; height: 2.4rem; transform: translateX(-50%);
  border: 2px solid var(--me-cue-color, rgba(255 255 255 / 0.8)); border-radius: 0.75rem;
}
.me-hero__cue::before {
  content: ''; position: absolute; top: 0.4rem; left: 50%;
  width: 0.25rem; height: 0.55rem; border-radius: 0.2rem;
  background: var(--me-cue-color, var(--me-accent)); transform: translateX(-50%);
  animation: me-cue 1.6s ease-in-out infinite;
}
@keyframes me-cue {
  0%, 100% { transform: translate(-50%, 0); opacity: 1; }
  70% { transform: translate(-50%, 0.7rem); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .me-hero__cue::before { animation: none; }
}

/* Pin-progress animation: content fade/lift, media parallax drift + scale,
   and cue fade, all driven by scroll position through the pin via a named
   view-timeline instead of a scroll listener. `--me-hero-pin`'s subject is
   the tall (pinLength * 100vh) .me-hero root, not the sticky child, which is
   what sidesteps the usual view-timeline / position:sticky ambiguity: the
   root is a plain, non-sticky flow box, so its geometry is unambiguous.
   `animation-range: contain` is exactly the span during which the root
   fully covers the scrollport — i.e. exactly the stuck duration.

   Chrome/Edge/Safari only, deliberately — see hero.js's top comment (and its
   note that Firefox's current status is unresolved between disagreeing
   sources — re-check before relying on either claim). The @supports gate
   matters, not just as tidiness: without it, an unsupported browser would
   still run `animation:` on the default time-based timeline with the
   shorthand's default 0s duration, which snaps straight to the end keyframe
   and stays there — an invisible heading, not a quiet static hero. Gating
   the whole block keeps unsupported browsers on the plain, un-animated
   rules above. */
@supports (animation-timeline: view()) {
  .me-hero { view-timeline-name: --me-hero-pin; view-timeline-axis: block; }
  .me-hero__media {
    animation: me-hero-media linear both;
    animation-timeline: --me-hero-pin;
    animation-range: contain 0% contain 100%;
  }
  .me-hero__content {
    animation: me-hero-content linear both;
    animation-timeline: --me-hero-pin;
    animation-range: contain 0% contain 100%;
  }
  .me-hero__cue {
    animation: me-hero-cue linear both;
    animation-timeline: --me-hero-pin;
    animation-range: contain 0% contain 25%;
  }
  @keyframes me-hero-media {
    from { transform: translateY(0) scale(1); }
    to   { transform: translateY(calc(var(--me-hero-drift, 0px) * -1)) scale(1.08); }
  }
  @keyframes me-hero-content {
    0%   { opacity: 1; transform: translateY(0); }
    50%  { opacity: 1; transform: translateY(-20px); }
    100% { opacity: 0; transform: translateY(-40px); }
  }
  @keyframes me-hero-cue {
    from { opacity: 1; }
    to   { opacity: 0; }
  }
  @media (prefers-reduced-motion: reduce) {
    .me-hero__media, .me-hero__content, .me-hero__cue { animation: none; }
  }
}

/* ---- splitMedia ---- */
/* Heading across the top, then media and a narrow column of copy beside it.
   Proportions are taken from the reference: at 1280px the media ran 651px
   against 325px of text, so roughly 2:1 with a generous gutter. */
.me-splitMedia { padding: clamp(3.5rem, 9vw, 7rem) clamp(1.5rem, 5vw, 4.5rem); }
.me-splitMedia__inner {
  max-width: 78rem; margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  column-gap: clamp(2rem, 5vw, 4.5rem);
  row-gap: clamp(1.75rem, 4vw, 3rem);
}
/* Size and rhythm only — never a family or a text-transform. The reference
   sets both, but those belong to the host site, not to the engine. */
.me-splitMedia__heading {
  grid-column: 1 / -1;
  line-height: 0.95; letter-spacing: -0.02em; text-wrap: balance;
}
.me-splitMedia__figure { grid-column: 1; margin: 0; }
.me-splitMedia__media {
  width: 100%; aspect-ratio: var(--me-split-ratio, 16 / 9);
  object-fit: cover; display: block; border-radius: 1rem;
}
.me-splitMedia__caption { margin-top: 0.75rem; font-size: 0.9rem; opacity: 0.7; }
.me-splitMedia__aside { grid-column: 2; display: flex; flex-direction: column; }
/* Bottom by default: the reference sits its copy on the media's baseline,
   which is what stops the short column reading as if it had floated up. */
.me-splitMedia[data-align='bottom'] .me-splitMedia__aside { justify-content: flex-end; }
.me-splitMedia[data-align='center'] .me-splitMedia__aside { justify-content: center; }
.me-splitMedia[data-align='top'] .me-splitMedia__aside { justify-content: flex-start; }
.me-splitMedia__body {
  font-size: var(--me-size-p); line-height: 1.3;
  margin-bottom: 1rem;
}
.me-splitMedia__aside .me-cta { align-self: flex-start; margin-top: 1rem; }

.me-splitMedia--flip .me-splitMedia__inner {
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
}
.me-splitMedia--flip .me-splitMedia__figure { grid-column: 2; }
.me-splitMedia--flip .me-splitMedia__aside { grid-column: 1; }

/* One column below the split. DOM order is heading, media, copy, so the media
   leads on mobile in both orientations without reordering anything. */
@media (max-width: 768px) {
  .me-splitMedia__inner,
  .me-splitMedia--flip .me-splitMedia__inner { grid-template-columns: minmax(0, 1fr); }
  .me-splitMedia__figure, .me-splitMedia__aside,
  .me-splitMedia--flip .me-splitMedia__figure,
  .me-splitMedia--flip .me-splitMedia__aside { grid-column: 1; }
  .me-splitMedia[data-align] .me-splitMedia__aside { justify-content: flex-start; }
}

/* ---- compareSlider ---- */
/* Drag-to-reveal comparison. Two images/video stack absolutely inside the
   frame; the "after" layer is clipped by --me-compare-pos, a percentage
   custom property written on `input` from a native range input laid
   full-bleed over the frame. That range is the only thing that moves the
   boundary — dragging, click-to-jump, and arrow-key stepping all come from
   the browser's own range behaviour, not hand-rolled pointer tracking. The
   visible handle is a purely cosmetic echo of the same var(); it never
   drives state itself. */
.me-compareSlider { padding: clamp(3.5rem, 9vw, 7rem) clamp(1.5rem, 5vw, 4.5rem); }
.me-compareSlider__inner { max-width: 64rem; margin: 0 auto; }
.me-compareSlider__heading {
  margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
  line-height: 0.95; letter-spacing: -0.02em; text-wrap: balance;
}
.me-compareSlider__figure { margin: 0; }
.me-compareSlider__frame {
  position: relative; overflow: hidden; border-radius: 1rem;
  aspect-ratio: var(--me-compare-ratio, 16 / 9);
  --me-compare-pos: 50%;
}
.me-compareSlider__img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; pointer-events: none;
}
/* Clips the after layer's right edge back to the handle, revealing it only
   on the left — the before layer underneath needs no clip of its own. */
.me-compareSlider__img--after { clip-path: inset(0 calc(100% - var(--me-compare-pos)) 0 0); }
.me-compareSlider__label {
  position: absolute; bottom: 1rem; z-index: 1;
  padding: 0.4rem 0.65rem; border-radius: 0.5rem;
  background: var(--me-label-fill, rgba(0, 0, 0, 0.55)); color: #fff;
  font-size: 0.8rem; font-weight: 600; letter-spacing: 0.02em;
  pointer-events: none;
}
.me-compareSlider__label--before { right: 1rem; }
.me-compareSlider__label--after { left: 1rem; }
.me-compareSlider__handle {
  position: absolute; top: 0; bottom: 0;
  left: var(--me-compare-pos, 50%);
  width: 3px; margin-left: -1.5px;
  background: var(--me-handle-color, #fff); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
  pointer-events: none; z-index: 1;
}
.me-compareSlider__grip {
  position: absolute; top: 50%; left: 50%;
  width: 2.75rem; height: 2.75rem;
  transform: translate(-50%, -50%);
  border-radius: 999px; background: var(--me-handle-color, #fff);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.me-compareSlider__grip::before, .me-compareSlider__grip::after {
  content: ''; position: absolute; top: 50%; width: 0; height: 0;
  border-top: 6px solid transparent; border-bottom: 6px solid transparent;
  transform: translateY(-50%);
}
.me-compareSlider__grip::before { left: 0.7rem; border-right: 7px solid var(--me-surface); }
.me-compareSlider__grip::after { right: 0.7rem; border-left: 7px solid var(--me-surface); }
/* Invisible but interactive: full-bleed so a drag anywhere in the frame
   moves the boundary, not just a grab on the handle itself. */
.me-compareSlider__range {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: ew-resize; z-index: 2;
  -webkit-appearance: none; appearance: none; background: transparent;
  touch-action: pan-y;
}
.me-compareSlider__range::-webkit-slider-runnable-track { -webkit-appearance: none; background: transparent; }
/* Invisible and small, not full-width: a thumb sized to match its track
   leaves the browser's click-to-value math zero usable travel distance to
   divide by, and some engine builds (seen: Chrome 151) resolve that
   degenerate case to 0 on every click instead of the clicked fraction. The
   full-bleed range element above already covers the whole frame, so a
   normal small thumb still lets a click anywhere jump to that position —
   this never relied on the thumb itself being large. */
.me-compareSlider__range::-webkit-slider-thumb { -webkit-appearance: none; width: 2.75rem; height: 2.75rem; background: transparent; cursor: ew-resize; }
.me-compareSlider__range::-moz-range-track { background: transparent; border: none; }
.me-compareSlider__range::-moz-range-thumb { width: 2.75rem; height: 2.75rem; background: transparent; border: none; cursor: ew-resize; }
/* The operative control is invisible, so arrow-key operation works with zero
   visual indication of where focus is. Light the styled handle up instead. */
.me-compareSlider__frame:has(.me-compareSlider__range:focus-visible) .me-compareSlider__handle {
  box-shadow: 0 0 0 3px var(--me-focus-color, var(--me-accent, #ffc20e)), 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.me-compareSlider__caption { margin-top: 0.75rem; font-size: 0.9rem; opacity: 0.7; }

@media (max-width: 768px) {
  .me-compareSlider__label { font-size: 0.7rem; padding: 0.3rem 0.5rem; }
  .me-compareSlider__grip { width: 2.25rem; height: 2.25rem; }
}

/* ---- mediaList ---- */
/* Copy column and a tall media panel, bottom-aligned. The reference runs
   504px of text against a 507x612 panel at 1280 — near enough equal columns
   with a wide gutter, and a portrait crop rather than a landscape one. */
.me-mediaList { padding: clamp(3.5rem, 9vw, 7rem) clamp(1.5rem, 5vw, 4.5rem); }
.me-mediaList__inner {
  max-width: 78rem; margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(2rem, 8vw, 8.5rem);
  row-gap: clamp(2rem, 5vw, 3.5rem);
}
.me-mediaList__col { grid-column: 1; display: flex; flex-direction: column; }
.me-mediaList__figure { grid-column: 2; margin: 0; }
.me-mediaList--flip .me-mediaList__col { grid-column: 2; }
.me-mediaList--flip .me-mediaList__figure { grid-column: 1; }

.me-mediaList[data-align='bottom'] .me-mediaList__col { justify-content: flex-end; }
.me-mediaList[data-align='center'] .me-mediaList__col { justify-content: center; }
.me-mediaList[data-align='top'] .me-mediaList__col { justify-content: flex-start; }

/* The pill is two palette points now: the fill defaults to the accent, the
   text to the page surface. */
.me-mediaList__eyebrow {
  align-self: flex-start;
  padding: 0.4rem 0.65rem; border-radius: 0.5rem;
  background: var(--me-eyebrow-fill, var(--me-accent)); color: var(--me-eyebrow-text, var(--me-surface));
  font-size: 0.82rem; line-height: 1.2;
}
.me-mediaList__heading {
  margin-top: 1.5rem;
  line-height: 1.05;
  letter-spacing: -0.015em; text-wrap: balance;
}
.me-mediaList__list { list-style: none; margin: 2.25rem 0 0; padding: 0; }
.me-mediaList__item {
  position: relative;
  font-size: clamp(1.05rem, 1.6vw, 1.35rem); line-height: 1.2;
  padding-bottom: 1.25rem; margin-bottom: 1.25rem;
}
/* The rule is a pseudo-element, so the list stays a list. It wipes in from the
   leading edge when the item does — pseudo-elements run their own transition,
   independent of the entrance on the <li> itself. */
.me-mediaList__item::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: currentColor; opacity: 0.25;
  transform: scaleX(0); transform-origin: left center;
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: calc(var(--me-stagger, 0) * 150ms);
}
@supports (transition-delay: calc(sibling-index() * 1ms)) {
  .me-mediaList__item::after {
    transition-delay: calc(sibling-index() * 150ms);
  }
}
.me-mediaList__item.in::after { transform: scaleX(1); }
.me-mediaList__media {
  width: 100%; aspect-ratio: var(--me-list-ratio, 5 / 6);
  object-fit: cover; display: block; border-radius: 1rem;
}
.me-mediaList__caption { margin-top: 0.75rem; font-size: 0.9rem; opacity: 0.7; }

@media (prefers-reduced-motion: reduce) {
  .me-mediaList__item::after { transform: none; transition: none; }
}
/* One column below the split; the list leads, the panel follows. */
@media (max-width: 768px) {
  .me-mediaList__inner { grid-template-columns: minmax(0, 1fr); }
  .me-mediaList__col, .me-mediaList__figure,
  .me-mediaList--flip .me-mediaList__col,
  .me-mediaList--flip .me-mediaList__figure { grid-column: 1; }
  .me-mediaList[data-align] .me-mediaList__col { justify-content: flex-start; }
  .me-mediaList__media { aspect-ratio: var(--me-list-ratio-mobile, 4 / 3); }
}

/* ---- prose ---- */
.me-prose { padding: clamp(4rem, 10vw, 8rem) 1.5rem; }
.me-prose__inner { max-width: 42rem; margin: 0 auto; }
.me-prose--center .me-prose__inner { text-align: center; }
.me-prose__heading { margin-bottom: 1.5rem; }
.me-prose__body { margin-bottom: 1.1rem; font-size: var(--me-size-p); }
.me-prose__figure { margin: 2.5rem 0 0; }
.me-prose__image { width: 100%; border-radius: 0.5rem; }

.me-cta {
  display: inline-block; margin-top: 2rem; padding: 0.9rem 2.2rem;
  background: var(--me-accent); color: var(--me-surface);
  font-weight: 700; text-decoration: none; border-radius: 999px;
  transition: transform 0.25s ease;
}
.me-cta:hover { transform: translateY(-2px); }

/* ---- contact ---- */
/* Heading left, details right, collapsing below the 768px split. The name
   line is the stack's lead-in — a muted tracked label, not mediaList's
   accent pill. Email and telephone are the prominent links; the address
   keeps the author's line breaks (pre-line). No transitions of its own —
   entrances are anima's. */
.me-contact { padding: clamp(4rem, 10vw, 8rem) 1.5rem; }
.me-contact__inner {
  max-width: 64rem; margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(2.5rem, 7vw, 6rem);
  align-items: start;
}
.me-contact__heading { text-wrap: balance; }
.me-contact__details {
  display: flex; flex-direction: column; gap: 1.1rem;
}
.me-contact__name {
  font-size: 0.8rem; letter-spacing: 0.12em; text-transform: uppercase;
  opacity: 0.65;
}
.me-contact__email, .me-contact__telephone {
  color: inherit; text-decoration: none; text-underline-offset: 0.25em;
  font-size: var(--me-size-h3); font-weight: 600; line-height: 1.15;
  width: fit-content; overflow-wrap: anywhere;
}
.me-contact__email:hover, .me-contact__telephone:hover {
  color: var(--me-link-hover, var(--me-accent)); text-decoration: underline;
}
.me-contact__address {
  font-size: var(--me-size-p); white-space: pre-line; opacity: 0.9;
}
/* Inside the details flex the shared .me-cta (inline-block, margin-top 2rem)
   would stretch full-width and double the stack gap. */
.me-contact__details .me-cta { align-self: flex-start; margin-top: 1.25rem; }

/* One column below the split; the heading leads, the details follow. */
@media (max-width: 768px) {
  .me-contact__inner { grid-template-columns: minmax(0, 1fr); row-gap: 2.5rem; }
}

/* ---- footer ---- */
/* The page footer as a section: a hairline-topped, two-row bar. Row one
   holds the site name (heading) left and the closing links right in a
   wrapping horizontal row; row two carries the credits line, the author's
   own line breaks (pre-line). The hairline is currentColor at 25% ink so
   it reads on both themes; a hairlineColor palette name replaces
   currentColor in that mix. The heading size comes from its tag (h3 by
   default) — no font-size here. */
.me-footer { padding: clamp(3rem, 8vw, 5rem) 1.5rem;
  border-top: 1px solid color-mix(in srgb, var(--me-hairline-color, currentColor) 25%, transparent); }
.me-footer__inner { max-width: 64rem; margin: 0 auto; }
.me-footer__row {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between;
  column-gap: clamp(2rem, 6vw, 4rem); row-gap: 1.25rem;
}
.me-footer__heading { text-wrap: balance; }
.me-footer__links {
  list-style: none; padding: 0; display: flex; flex-wrap: wrap;
  gap: 0.4rem 2rem;
}
.me-footer__linkAnchor {
  color: inherit; text-decoration: none; text-underline-offset: 0.25em;
}
.me-footer__linkAnchor:hover {
  color: var(--me-link-hover, var(--me-accent)); text-decoration: underline;
}
.me-footer__credits {
  margin-top: 1.75rem; font-size: var(--me-size-p);
  opacity: 0.75; white-space: pre-line;
}
/* One column below the split: row one stacks, name over links. */
@media (max-width: 768px) {
  .me-footer__row { flex-direction: column; align-items: flex-start; }
}

/* ---- cardRow ---- */
.me-cardRow { padding: clamp(4rem, 10vw, 8rem) 1.5rem; }
.me-cardRow__inner { max-width: 72rem; margin: 0 auto; }
.me-cardRow__intro {
  max-width: 42rem; margin-bottom: 3rem;
  font-size: var(--me-size-p); font-weight: 600;
}
.me-cardRow__grid {
  list-style: none; padding: 0; display: grid; gap: 1.5rem;
  grid-template-columns: repeat(var(--me-cols, 3), 1fr);
  /* The long card rise the anima vocabulary consumes (translateY). Set here,
     not in JS, so a host theme can override the distance like any other
     token. */
  --me-rise-y: 150px;
}
.me-card {
  padding: 2rem; border-radius: 0.75rem;
  background: var(--me-card-fill, rgba(128 128 128 / 0.08));
  border: 1px solid var(--me-card-border, rgba(128 128 128 / 0.15));
}
[data-style="outline"] .me-card { background: none; }
.me-card__link { color: inherit; text-decoration: none; display: block; }
.me-card__media { width: 100%; border-radius: 0.5rem; margin-bottom: 1.25rem; }
.me-card__heading { margin-bottom: 0.6rem; }
.me-card__body { opacity: 0.85; font-size: var(--me-size-p); }
@media (max-width: 900px) { .me-cardRow__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .me-cardRow__grid { grid-template-columns: 1fr; } }

/* ---- stickySwap ---- */
.me-stickySwap { display: grid; grid-template-columns: 1fr 1fr; }
.me-stickySwap--left .me-stickySwap__media { order: -1; }
.me-stickySwap__media { position: relative; }
.me-stickySwap__frame,
.me-stickySwap__video {
  position: sticky; top: 0; height: 100vh; width: 100%;
  overflow: hidden;
}
.me-stickySwap__video { object-fit: cover; display: block; }
.me-stickySwap__img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0; transition: opacity 0.6s ease;
}
.me-stickySwap__img.active { opacity: 1; }
.me-stickySwap__steps { padding: 20vh 0; }
.me-stickySwap__step {
  min-height: 80vh; display: flex; align-items: center;
  padding: 0 clamp(1.5rem, 6vw, 5rem);
}
.me-stickySwap__card { max-width: 26rem; opacity: 0.35; transition: opacity 0.4s ease; }
.me-stickySwap__step.active .me-stickySwap__card { opacity: 1; }
.me-stickySwap__heading { margin-bottom: 1rem; }
.me-stickySwap__body { margin-bottom: 0.9rem; font-size: var(--me-size-p); }

/* bg variant: media fills the section, steps overlay as cards */
.me-stickySwap--bg { grid-template-columns: 1fr; }
.me-stickySwap--bg .me-stickySwap__media { position: absolute; inset: 0; }
.me-stickySwap--bg { position: relative; }
.me-stickySwap--bg .me-stickySwap__card {
  background: var(--me-swap-card-fill, rgba(0 0 0 / 0.6)); color: #fff;
  padding: 2rem; border-radius: 0.75rem; backdrop-filter: blur(8px);
}

/* mobile: media pins to the top half, steps scroll beneath */
@media (max-width: 768px) {
  .me-stickySwap { grid-template-columns: 1fr; }
  .me-stickySwap--left .me-stickySwap__media { order: 0; }
  .me-stickySwap__frame, .me-stickySwap__video { height: 45vh; top: 0; z-index: 2; }
  .me-stickySwap__media { position: sticky; top: 0; height: 45vh; z-index: 2; }
  .me-stickySwap__steps { padding: 5vh 0; }
  .me-stickySwap__step { min-height: 60vh; }
}

/* reduced motion: no pin, no crossfade — a plain column of step cards, each
   carrying its own image, with the section video's poster as a static figure.
   Class-driven (not a media query) so it keys off the same decision the JS
   made at mount, like expandMedia's --static. */
.me-stickySwap--static { display: block; }
/* height:auto beats the mobile block's 45vh sticky column — nothing pins
   anymore, so the column is just the poster's own box. */
.me-stickySwap--static .me-stickySwap__media { position: static; height: auto; }
.me-stickySwap--static .me-stickySwap__poster {
  position: static; height: auto; width: 100%;
  aspect-ratio: 16 / 9; object-fit: cover;
  border-radius: 0.75rem;
}
.me-stickySwap--static .me-stickySwap__steps { padding: 4vh 0; }
.me-stickySwap--static .me-stickySwap__step { min-height: 0; }
.me-stickySwap--static .me-stickySwap__card { opacity: 1; max-width: 36rem; }
.me-stickySwap--static .me-stickySwap__img--inCard {
  /* position: static undoes the base rule's absolute inset:0 — the frame
     path needs that (pinned, crossfaded via .active), the inCard image is
     an ordinary in-flow block in its card. Without it the reveal fix only
     made them visible while every card's image covered the whole section. */
  position: static;
  display: block; width: 100%; height: auto;
  aspect-ratio: 16 / 9; object-fit: cover;
  border-radius: 0.75rem; margin-bottom: 1.5rem;
}

/* ---- testimonial ---- */
/* A pinned quote card over a full-bleed parallax background. Modelled on
   stickySwap's --bg variant (media fills the section, a card overlays it)
   but the card itself is what's pinned, not just the media, and it carries
   the tour section's popover look (near-opaque dark blur) rather than
   stickySwap's lighter rgba(0 0 0 / 0.6) card. */
.me-testimonial { position: relative; }
.me-testimonial__sticky {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  display: flex; flex-direction: column;
  padding: clamp(2rem, 6vw, 4rem) clamp(1.5rem, 6vw, 5rem);
  color: var(--me-section-ink, #fff);
}
.me-testimonial__bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
/* 120% tall and centred so there is travel room on both edges; the drift
   below is a percentage of this element's own height, so it needs no pixel
   measurement — see testimonial.js. */
.me-testimonial__bgMedia {
  position: absolute; left: 0; top: -10%; width: 100%; height: 120%;
  object-fit: cover; display: block;
  transform: translateY(calc(var(--me-testimonial-parallax, 0) * var(--me-testimonial-drift, 0) * 1%));
}
.me-testimonial__scrim { position: absolute; inset: 0; background: rgba(0 0 0 / 0.45); }
.me-testimonial__heading {
  position: relative; z-index: 1; margin-bottom: clamp(1.5rem, 4vw, 3rem);
  line-height: 0.95; letter-spacing: -0.02em; text-wrap: balance;
  text-shadow: 0 2px 24px rgba(0 0 0 / 0.45);
}
.me-testimonial__deck {
  position: relative; z-index: 1; flex: 1; min-height: 0;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
/* Three states, keyed off (step index vs. active index) in testimonial.js.
   "before" and "after" both sit off-screen; which edge they sit on is what
   makes the swap read as directional travel rather than a generic fade. */
.me-testimonial__box {
  position: absolute; margin: 0; width: min(100%, 40rem);
  padding: clamp(1.75rem, 4vw, 2.75rem);
  background: var(--me-testimonial-fill, rgba(12 10 12 / 0.88)); backdrop-filter: blur(14px);
  border-radius: 1rem; color: var(--me-section-ink, #fff);
  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s ease;
}
.me-testimonial__box[data-state='current'] { transform: translateY(0); opacity: 1; }
.me-testimonial__box[data-state='before'] { transform: translateY(-100%); opacity: 0; pointer-events: none; }
.me-testimonial__box[data-state='after'] { transform: translateY(100%); opacity: 0; pointer-events: none; }
.me-testimonial__quote {
  font-size: var(--me-size-p); line-height: 1.5; margin: 0 0 1rem;
  quotes: '\201C' '\201D';
}
.me-testimonial__quote::before { content: open-quote; }
.me-testimonial__quote::after { content: close-quote; }
.me-testimonial__author {
  font-size: 0.8rem; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.75;
}
.me-testimonial__step { min-height: 90vh; }

@media (max-width: 768px) {
  .me-testimonial__step { min-height: 70vh; }
  .me-testimonial__box { padding: 1.5rem; }
}

@media (prefers-reduced-motion: reduce) {
  .me-testimonial__box { transition: none; }
}

/* static: reduced motion, or a single testimonial with nothing to advance
   to. No pin, no steps (testimonial.js doesn't create them); every quote
   that exists renders in normal flow, all at once. */
.me-testimonial--static .me-testimonial__sticky {
  position: static; height: auto;
  padding: clamp(3rem, 8vw, 6rem) clamp(1.5rem, 6vw, 5rem);
}
.me-testimonial--static .me-testimonial__bgMedia { top: 0; height: 100%; transform: none; }
.me-testimonial--static .me-testimonial__deck {
  position: static; flex-direction: column; gap: 1.5rem; overflow: visible;
}
.me-testimonial--static .me-testimonial__box {
  position: static; width: 100%; max-width: 40rem; margin: 0 auto;
  transform: none !important; opacity: 1 !important; pointer-events: auto;
}

/* ---- statBar ---- */
.me-statBar { padding: clamp(4rem, 10vw, 8rem) 1.5rem; }
.me-statBar__heading { margin-bottom: 3rem; }
.me-statBar__grid {
  display: grid; gap: 2.5rem 3rem;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  max-width: 72rem; margin: 0 auto;
}
.me-statBar__stat {
  display: flex; flex-direction: column; gap: 0.5rem;
  --me-rise-y: 40px; /* small travel for the entrance lift */
}
.me-statBar__value {
  font-size: clamp(2.5rem, 6vw, 4.5rem);
  font-weight: 700; line-height: 1.1;
  font-variant-numeric: tabular-nums; /* digits don't shift width while counting */
}
.me-statBar__label { opacity: 0.85; font-size: var(--me-size-p); }
.me-statBar__fill {
  height: 3px; border-radius: 999px;
  background: var(--me-stat-fill, var(--me-accent));
  transform-origin: left center; /* fills left-to-right */
  transform: scaleX(0);
}
@media (max-width: 600px) {
  .me-statBar__grid { grid-template-columns: 1fr 1fr; gap: 2rem 1.25rem; }
  .me-statBar__stat:last-child:nth-child(odd) { grid-column: 1 / -1; } /* odd count: last stat spans */
}

/* static: reduced motion or duration <= 0 — statBar.js writes the final
   values and full bars inline and registers nothing. */
.me-statBar--static .me-statBar__fill { transform: scaleX(1); }

/* ---- crossfadeStack ---- */
/* Scroll scrubs --me-scrub (0..1) over N viewport-heights of pin; each
   layer's --at (spaced 1/(N-1)) and --at-fade (N-1), written by JS at
   build time, resolve a continuous linear crossfade: 1 at its own stop,
   0.5 at both midpoints, 0 at adjacent stops — constant total brightness,
   no ghost overlap. Captions live inside their layer and ride the same
   opacity curve for free. */
.me-crossfadeStack { position: relative; --me-scrub: 0; }
.me-crossfadeStack__sticky {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
}
.me-crossfadeStack__layer {
  position: absolute; inset: 0;
  opacity: max(0, calc(1 - abs(var(--me-scrub) - var(--at)) * var(--at-fade, 1)));
}
.me-crossfadeStack__image {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.me-crossfadeStack__caption {
  position: absolute; left: clamp(1rem, 4vw, 2.5rem);
  right: clamp(1rem, 4vw, 2.5rem); bottom: clamp(1rem, 4vw, 2.5rem);
  margin: 0; max-width: 34rem;
  color: var(--me-caption-color, #fff); text-shadow: 0 1px 3px rgb(0 0 0 / 0.7);
  font-size: var(--me-size-p); opacity: 0.9;
}

/* static: reduced motion — the last image, unpinned, in normal flow.
   --me-scrub: 1 resolves last = 1, others = 0. crossfadeStack.js registers
   nothing under reduced motion, so this class is the whole static path. */
.me-crossfadeStack--static { height: auto !important; --me-scrub: 1; }
.me-crossfadeStack--static .me-crossfadeStack__sticky {
  position: static; height: 100vh; overflow: hidden;
}

@media (max-width: 768px) { /* MOBILE_MAX — src/util/capability.js */
  .me-crossfadeStack__caption { bottom: 1rem; left: 1rem; right: 1rem; }
}

/* ---- proseHighlight ---- */
/* A pinned prose block whose words scrub from a muted ink to the section's
   full ink over a fixed 300vh pin. Each word span carries --at/--at-fade
   (crossfadeStack math) and resolves the same abs() curve into --p, a
   colour-mix ratio — never opacity, so a word not currently "reached" is
   dim, not gone. Whitespace between spans is bare text, so wrapping is
   native. */
.me-proseHighlight { position: relative; --me-scrub: 0; }
.me-proseHighlight__sticky {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  display: flex; flex-direction: column; justify-content: center;
  padding: clamp(4rem, 10vw, 8rem) 1.5rem;
}
.me-proseHighlight__heading { margin-bottom: 1.5rem; }
.me-proseHighlight__text { max-width: 52rem; }
.me-proseHighlight__paragraph {
  margin-bottom: 1.1rem;
  font-size: var(--me-size-h3);
  line-height: 1.4;
}
/* No opacity here — a word this reveal has not "reached" yet still has to be
   readable, just dim, or the effect reads as most of the paragraph missing
   rather than as a highlight sweeping through it. Colour alone carries the
   reveal: --me-prose-dim (resting) to --me-prose-highlight (peak), both
   overridable per-section (see proseHighlight.js) for authors whose section
   background makes the computed defaults too low-contrast. */
.me-proseHighlight__word {
  --p: max(0, calc(1 - abs(var(--me-scrub) - var(--at)) * var(--at-fade, 1)));
  color: color-mix(in srgb, var(--me-prose-dim), var(--me-prose-highlight, currentColor) calc(var(--p) * 100%));
}
.me-proseHighlight[data-theme='light'] {
  --me-prose-dim: color-mix(in srgb, var(--me-section-ink, var(--me-light-ink)) 45%, var(--me-section-surface, var(--me-light-bg)));
}
.me-proseHighlight[data-theme='dark'] {
  --me-prose-dim: color-mix(in srgb, var(--me-section-ink, var(--me-dark-ink)) 45%, var(--me-section-surface, var(--me-dark-bg)));
}
/* static: reduced motion — the full text, unpinned, in normal flow.
   --p: 1 overrides the scrub formula per word (the scrub formula with
   --me-scrub: 1 would leave only the last word lit). proseHighlight.js
   registers nothing under reduced motion, so this class is the whole
   static path. */
.me-proseHighlight--static { height: auto !important; }
.me-proseHighlight--static .me-proseHighlight__sticky {
  position: static; height: auto; overflow: visible;
}
.me-proseHighlight--static .me-proseHighlight__word { --p: 1; }

/* ---- timeline ---- */
/* A vertical timeline in normal page flow: a connector line draws
   top-to-bottom as the section enters the viewport, and each milestone dot
   lights in a window around the draw head. JS writes --me-scrub (0..1) from
   the section's entry — 0 while its top is below the viewport, 1 when its
   bottom reaches the viewport's bottom edge — and each milestone card
   carries --at/--at-fade (crossfadeStack math, spaced 1/(N-1)) that resolve
   the same abs() curve into --p on its dot. The line is a length channel,
   not an opacity one: scaleY with origin top draws it proportionally
   (statBar's fill, rotated). Text entrances ride data-anima as usual. */
.me-timeline { position: relative; --me-scrub: 0; padding: clamp(4rem, 10vw, 8rem) 1.5rem; }
.me-timeline__heading { margin-bottom: 3rem; }
.me-timeline__track { position: relative; max-width: 46rem; margin: 0 auto; }
.me-timeline__line {
  position: absolute; top: 0.3rem; bottom: 0.3rem; left: calc(0.9rem - 1.5px);
  width: 3px; border-radius: 999px;
  background: var(--me-line-color, var(--me-accent)); opacity: 0.4;
  transform-origin: top center; /* the draw head is the top */
  transform: scaleY(var(--me-scrub));
}
.me-timeline__milestone {
  position: relative; padding: 0.15rem 0 3rem 2.5rem; /* content clears the gutter */
}
.me-timeline__milestone:last-child { padding-bottom: 0; }
.me-timeline__dot {
  position: absolute; top: 0.3rem; left: calc(0.9rem - 7px);
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid var(--me-dot-color, var(--me-accent)); background: var(--me-dot-color, var(--me-accent));
  --p: max(0, calc(1 - abs(var(--me-scrub) - var(--at)) * var(--at-fade, 1)));
  opacity: var(--p);
}
.me-timeline__date {
  display: block; margin-bottom: 0.35rem;
  font-size: 0.875em; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; opacity: 0.75;
}
.me-timeline__itemHeading { margin-bottom: 0.5rem; } /* size comes from .me-root h3 — no font-size here */
.me-timeline__body { margin-bottom: 0.5rem; opacity: 0.85; font-size: var(--me-size-p); }
.me-timeline__media { margin-top: 0.9rem; }
.me-timeline__image { width: 100%; height: auto; display: block; border-radius: 0.5rem; }

/* Lazy stills fade in once they can paint instead of popping mid-scroll:
   assets.image() tags every deferred img me-media-fade and adds .is-ready on
   load+decode (or error — alt text must never sit invisible). Same shape as
   hero's poster->video upgrade. The class lives on the img, never on a
   wrapper: anima's scroll-driven animation owns opacity on [data-anima]
   containers, so a figure-level fade would be dead CSS in view-timeline
   browsers. Reduced motion: a cut. */
.me-media-fade {
  opacity: 0;
  transition: opacity 0.6s ease;
}
.me-media-fade.is-ready { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .me-media-fade { transition: none; }
}

/* static: reduced motion — the full line and every milestone visible.
   --me-scrub: 1 resolves the line to full draw; --p: 1 overrides the
   per-dot window formula (which with --me-scrub: 1 would leave only the
   tail dots lit). timeline.js registers nothing under reduced motion, so
   this class is the whole static path. */
.me-timeline--static { --me-scrub: 1; }
.me-timeline--static .me-timeline__dot { --p: 1; }

@media (max-width: 768px) { /* MOBILE_MAX — src/util/capability.js */
  .me-timeline__milestone { padding-bottom: 2.25rem; }
}

/* ---- timed captions (shared across video sections) ---- */
/* A subtitle bar pinned to the bottom of a section's media frame: one line
   at a time, swapped as the clip's timecode crosses each caption's window
   (see src/util/captions.js). Hard swaps — subtitle-standard, a crossfade
   would blur two lines together mid-swap. The bar is a polite live region,
   so each swap announces to screen readers. */
.me-captions {
  position: absolute; left: 0; right: 0;
  bottom: clamp(0.75rem, 3vw, 1.5rem);
  display: flex; justify-content: center;
  pointer-events: none; /* never block a drag (compareSlider handle, cardTrack grab) */
  z-index: 3;
}
.me-captions__line {
  max-width: min(88%, 40rem);
  padding: 0.35rem 0.9rem;
  background: rgba(0 0 0 / 0.55);
  color: #fff;
  border-radius: 0.4rem;
  font-size: 0.95rem;
  line-height: 1.4;
  text-align: center;
}
.me-captions__line:empty { display: none; }

/* static: reduced motion / save-data — the poster with the caption lines as
   ordinary stacked text (scrubVideo-static precedent). */
.me-captions--static {
  position: static;
  display: block;
  padding: 1rem 0 0;
}
.me-captions--static .me-captions__line {
  display: block; max-width: none; margin-bottom: 0.5rem;
  padding: 0; background: none; color: inherit;
  border-radius: 0; text-align: left;
}
.me-captions--static .me-captions__line:last-child { margin-bottom: 0; }

/* ---- expandMedia ---- */
/* All geometry is driven by --me-expand (0 = inset, 1 = full-bleed),
   written by the section's scroll handler. */
.me-expandMedia { position: relative; --me-expand: 0; }
.me-expandMedia__sticky {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
}
.me-expandMedia__heading {
  margin-bottom: 2rem; text-align: center; padding: 0 1.5rem;
  opacity: calc(1 - var(--me-expand) * 1.8);
}
/* Optional section background, behind the expanding frame. Sits inside the
   sticky viewport so it holds still for the whole pin rather than scrolling
   past under the frame. */
.me-expandMedia__bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.me-expandMedia__bgMedia { width: 100%; height: 100%; object-fit: cover; display: block; }
.me-expandMedia__bgScrim {
  position: absolute; inset: 0;
  background: rgba(0 0 0 / var(--me-bg-scrim, 0.45));
}
/* Everything in the sticky has to out-stack the background layer. */
.me-expandMedia__heading, .me-expandMedia__figure { position: relative; z-index: 1; }

.me-expandMedia__figure { margin: 0; flex: none; }
.me-expandMedia__frame {
  width: calc(62vw + 38vw * var(--me-expand));
  height: calc(52vh + 48vh * var(--me-expand));
  border-radius: calc(0.75rem * (1 - var(--me-expand)));
  overflow: hidden;
}
.me-expandMedia__media {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.me-expandMedia__caption {
  margin-top: 1rem; text-align: center; font-size: 0.95rem;
  opacity: calc(0.75 - var(--me-expand) * 1.5);
}
@media (max-width: 768px) {
  .me-expandMedia__frame {
    width: calc(86vw + 14vw * var(--me-expand));
    height: calc(45vh + 55vh * var(--me-expand));
  }
}

/* ---- rail ---- */
/* Translation is pure CSS: 100% here is the track's own width, so
   (100% - 100vw) is exactly the horizontal overflow. JS only writes
   --me-rail and the section's pin height. */
.me-rail { position: relative; --me-rail: 0; --me-rail-bg: 0; }
.me-rail__sticky {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  display: flex; align-items: center;
}

/* Parallax background. JS sets its width to 100vw + (overflow x parallax), so
   the same (100% - 100vw) translation trick the track uses moves it exactly
   its own travel — one ratio from JS, geometry in CSS. */
.me-rail__bg {
  position: absolute; top: 0; left: 0; height: 100%;
  transform: translateX(calc((100% - 100vw) * -1 * var(--me-rail-bg)));
  will-change: transform;
}
.me-rail__bgMedia { width: 100%; height: 100%; object-fit: cover; display: block; }
.me-rail__scrim { position: absolute; inset: 0; background: rgba(0 0 0 / 0.45); }
.me-rail__track {
  position: relative; z-index: 1;
  flex: none; width: max-content;
  display: flex; align-items: stretch; gap: clamp(1.5rem, 3vw, 2.5rem);
  padding: 0 8vw;
  transform: translateX(calc((100% - 100vw) * -1 * var(--me-rail)));
  will-change: transform;
}
.me-rail__panel {
  flex: none; display: flex; flex-direction: column;
  padding: 2rem; border-radius: 0.75rem;
  background: var(--me-panel-fill, rgba(128 128 128 / 0.08));
  border: 1px solid var(--me-card-border, rgba(128 128 128 / 0.15));
}
[data-style="outline"] .me-rail__panel { background: none; }
.me-rail--narrow .me-rail__panel { width: 22rem; }
.me-rail--wide .me-rail__panel { width: 34rem; }
.me-rail--full .me-rail__panel { width: 84vw; }
.me-rail__media {
  width: 100%; aspect-ratio: 16 / 10; object-fit: cover;
  border-radius: 0.5rem; margin-bottom: 1.5rem;
}
.me-rail__heading { margin-bottom: 0.6rem; }
.me-rail__body { opacity: 0.85; font-size: var(--me-size-p); }
.me-rail__progress {
  position: absolute; bottom: 2.5rem; left: 50%;
  transform: translateX(-50%);
  width: min(30vw, 16rem); height: 3px; border-radius: 2px;
  background: rgba(128 128 128 / 0.3); overflow: hidden;
}
.me-rail__progress i {
  display: block; height: 100%; background: var(--me-progress-fill, var(--me-accent));
  transform: scaleX(var(--me-rail)); transform-origin: left;
}

/* mobile: native scroll-snap strip, no pin, no transform, no parallax layer */
@media (max-width: 768px) {
  .me-rail { height: auto !important; }
  .me-rail__bg { display: none; }
  .me-rail__sticky {
    position: static; height: auto; overflow: visible;
    padding: clamp(3rem, 8vw, 5rem) 0;
  }
  .me-rail__track {
    transform: none; width: auto; max-width: 100vw;
    overflow-x: auto; scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    padding: 0 6vw 1.5rem;
  }
  .me-rail__panel { scroll-snap-align: center; }
  .me-rail--narrow .me-rail__panel,
  .me-rail--wide .me-rail__panel,
  .me-rail--full .me-rail__panel { width: 78vw; }
  .me-rail__progress { display: none; }
}

/* reduced motion: vertical stack, no pin, no rail */
.me-rail--static { height: auto !important; }
.me-rail--static .me-rail__sticky {
  position: static; height: auto; overflow: visible;
  padding: clamp(3rem, 8vw, 5rem) 1.5rem;
}
.me-rail--static .me-rail__track {
  transform: none; width: auto; max-width: 60rem; margin: 0 auto;
  flex-direction: column; overflow: visible; padding: 0;
}
.me-rail--static .me-rail__panel { width: auto !important; }
.me-rail--static .me-rail__progress { display: none; }

/* ---- scrubVideo ---- */
/* JS writes --me-scrub (0..1) and seeks the video; each overlay fades in
   around its --at progress via CSS abs(). */
.me-scrubVideo { position: relative; --me-scrub: 0; }
.me-scrubVideo__sticky {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
}
.me-scrubVideo__video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.me-scrubVideo__scrim {
  position: absolute; inset: 0;
  background: rgba(0 0 0 / var(--me-scrim, 0));
  /* The stepped --me-scrim value flips at band edges like the overlays;
     the transition makes the darkening fade with the card it serves. */
  transition: background-color 0.4s ease;
}
.me-scrubVideo__overlay {
  position: absolute; transform: translate(-50%, -50%);
  max-width: 24rem; padding: 1.5rem 1.75rem; border-radius: 0.75rem;
  background: var(--me-overlay-fill, rgba(0 0 0 / 0.55)); color: var(--me-section-ink, #fff);
  backdrop-filter: blur(8px);
  pointer-events: none;
  /* The scroll only FLIPS the target — a step across the overlay's band
     (|scrub - at| < 1/at-fade): fully visible inside, hidden outside. The
     transition runs the fade on its own clock, so a fast flick through the
     band still gets a complete fade-in, hold, fade-out, and a mid-fade
     direction change reverses smoothly. The ×100000 factor turns any
     in-band value into 1 (the old ramp would read 0.5 a quarter of the
     way in — the test suite pins the difference). */
  opacity: clamp(0, 1, calc((1 - abs(var(--me-scrub) - var(--at)) * var(--at-fade, 8)) * 100000));
  transition: opacity 0.4s ease;
}
.me-scrubVideo__overlayHeading { margin-bottom: 0.5rem; }
.me-scrubVideo__overlayBody { font-size: var(--me-size-p); opacity: 0.9; }

/* anchor:'frame' — JS sizes this layer to the video's rendered content box
   so an overlay's [x%, y%] tracks the picture instead of the viewport.
   Hidden until the first geometry pass, so overlays never flash at inset:0
   before loadedmetadata. Section-anchored overlays never see this layer. */
.me-scrubVideo__frame { position: absolute; opacity: 0; }
.me-scrubVideo--laidOut .me-scrubVideo__frame { opacity: 1; }

/* manual mode (reducedMotion:'manual') — no pin, no scroll range. The frame
   is letterboxed to the clip's own ratio, so overlay percentages land on the
   picture exactly as the editor captured them. Buttons step --me-scrub
   between overlay stops; the fade curve above resolves the rest. */
.me-scrubVideo--manual { padding: clamp(3rem, 8vw, 5rem) 1.5rem; }
.me-scrubVideo__manualFrame {
  position: relative; margin: 0 auto; max-width: 60rem;
  aspect-ratio: var(--me-ar, 16 / 9);
  border-radius: 0.75rem; overflow: hidden; background: #000;
}
.me-scrubVideo__manualFrame .me-scrubVideo__video { object-fit: contain; }
/* The fade here runs only on an explicit button press, never from scroll,
   so a short transition stays within the reduced-motion contract. */
.me-scrubVideo--manual .me-scrubVideo__overlay { transition: opacity 0.2s ease; }
.me-scrubVideo__transport {
  display: flex; align-items: center; justify-content: center; gap: 1rem;
  margin: 1.25rem auto 0; max-width: 60rem;
}
.me-scrubVideo__transportBtn {
  font: inherit; color: inherit; cursor: pointer;
  padding: 0.5rem 1.1rem; border-radius: 0.5rem;
  border: 1px solid currentColor; background: none;
}
.me-scrubVideo__transportBtn[disabled] { opacity: 0.4; cursor: default; }
.me-scrubVideo__transportCount {
  font-size: var(--me-size-p); opacity: 0.75;
  font-variant-numeric: tabular-nums;
}

/* static fallback (mobile / reduced motion / missing asset):
   poster + overlay copy as ordinary stacked content */
.me-scrubVideo--static { padding: clamp(3rem, 8vw, 5rem) 1.5rem; }
.me-scrubVideo__fallback { margin: 0 auto; max-width: 60rem; }
.me-scrubVideo__poster { width: 100%; border-radius: 0.75rem; }
.me-scrubVideo__fallbackBlock { padding: 1.75rem 0 0.25rem; max-width: 34rem; }

/* ---- stepVideo ---- */
.me-stepVideo { position: relative; }
.me-stepVideo__sticky {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
}
.me-stepVideo__video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.me-stepVideo__scrim {
  position: absolute; inset: 0;
  background: rgba(0 0 0 / var(--me-scrim, 0));
}
.me-stepVideo__screens { position: absolute; inset: 0; }
.me-stepVideo__screen {
  position: absolute; inset: 0; opacity: 0; pointer-events: none;
  transition: opacity 0.5s ease;
}
.me-stepVideo__screen.active { opacity: 1; pointer-events: auto; }

.me-stepVideo__card {
  position: absolute; left: clamp(1.5rem, 6vw, 5rem); top: 50%;
  transform: translateY(-50%);
  max-width: 26rem; padding: 1.75rem 2rem; border-radius: 0.75rem;
  background: var(--me-step-card-fill, rgba(0 0 0 / 0.55)); color: var(--me-section-ink, #fff);
  backdrop-filter: blur(8px);
}
/* The first screen's heading renders as h2, later screens as h3 (see
   stepVideo.js) — tag-qualified so each picks up the matching theme size. */
.me-stepVideo__heading { margin-bottom: 0.75rem; }
.me-stepVideo__body { font-size: var(--me-size-p); opacity: 0.92; margin-bottom: 0.6rem; }
.me-stepVideo__summary {
  position: absolute; bottom: 2.5rem; left: clamp(1.5rem, 6vw, 5rem);
  max-width: 24rem; padding: 0.75rem 1.25rem; border-radius: 999px;
  background: rgba(0 0 0 / 0.55); color: var(--me-section-ink, #fff); font-size: 0.92rem;
  backdrop-filter: blur(8px);
}

/* Sized by JS to the video's rendered content box (see stepVideo.js), so node
   percentages map to points in the frame rather than points in the viewport.
   Falls back to the section box before metadata loads. */
.me-stepVideo__nodeLayer { position: absolute; inset: 0; pointer-events: none; }

/* nodes hidden until laid out, arrived, and active — see the compound
   selector below for the visibility gate */
.me-stepVideo__node {
  position: absolute; transform: translate(-50%, -50%);
  opacity: 0; transition: opacity 0.4s ease 0.15s; pointer-events: none;
}
/* Nodes are hidden until node-layer geometry is computed (--laidOut), the video
   transition has completed (.arrived), AND this screen is active. The --laidOut
   guard prevents a layout flash where nodes appear at the default inset:0
   position before loadedmetadata fires. */
.me-stepVideo--laidOut.me-stepVideo.arrived .me-stepVideo__screen.active .me-stepVideo__node {
  opacity: 1; pointer-events: auto;
}
.me-stepVideo__nodeBtn {
  display: flex; align-items: center; gap: 0.6rem;
  background: none; border: none; cursor: pointer;
  color: var(--me-section-ink, #fff); font: inherit; padding: 0.25rem;
}
.me-stepVideo__nodeDot {
  flex: none; width: 1rem; height: 1rem; border-radius: 50%;
  background: var(--me-node-dot, var(--me-accent));
  box-shadow: 0 0 0 0 rgba(255 194 14 / 0.55);
  animation: me-node-pulse 2s ease-out infinite;
}
/* The ring derives from the dot colour itself via color-mix — a custom dot
   tints the pulse too. Unset, this is the same accent premultiplied at 55%
   in srgb, pixel-identical to the old rgba(255 194 14 / 0.55). */
@keyframes me-node-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--me-node-dot, var(--me-accent)) 55%, transparent); }
  70% { box-shadow: 0 0 0 0.8rem color-mix(in srgb, var(--me-node-dot, var(--me-accent)) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--me-node-dot, var(--me-accent)) 0%, transparent); }
}
.me-stepVideo__nodeTitle {
  font-weight: 700; text-shadow: 0 1px 12px rgba(0 0 0 / 0.8);
  white-space: nowrap;
  /* A span (outside the claim list), so it inherits the nodeBtn colour;
     the section heading var pins it when the author set one. */
  color: var(--me-section-heading, inherit);
}
.me-stepVideo__nodePanel {
  display: none; position: absolute; top: 100%; left: 0;
  margin-top: 0.6rem; width: min(20rem, 60vw);
  padding: 1.25rem 1.5rem; border-radius: 0.75rem;
  background: var(--me-node-panel, rgba(0 0 0 / 0.7)); color: var(--me-section-ink, #fff);
  backdrop-filter: blur(10px); font-size: var(--me-size-p);
}
.me-stepVideo__node.open .me-stepVideo__nodePanel { display: block; }
.me-stepVideo__nodeBoard { width: 100%; border-radius: 0.4rem; margin-bottom: 0.75rem; }

.me-stepVideo__markers {
  position: absolute; right: 1.5rem; top: 50%;
  transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 1rem;
}
.me-stepVideo__marker {
  position: relative;
  width: 0.65rem; height: 0.65rem; border-radius: 50%;
  border: 2px solid var(--me-marker-color, rgba(255 255 255 / 0.7)); background: transparent;
  cursor: pointer; padding: 0; transition: background 0.3s ease;
}
/* Hit area only — the visible dot stays a track-scaled 0.65rem. ::before
   gives the button a real WCAG 2.2 SC 2.5.8 (AA) pointer target (24x24px
   minimum) without changing what's painted; centered on the dot so the
   enlarged target doesn't shift the marker's apparent position. */
.me-stepVideo__marker::before {
  content: '';
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 1.5rem; height: 1.5rem;
}
.me-stepVideo__marker.active { background: var(--me-marker-color, var(--me-accent)); border-color: var(--me-marker-color, var(--me-accent)); }

/* static fallback: poster + stacked cards */
.me-stepVideo--static { padding: clamp(3rem, 8vw, 5rem) 1.5rem; }
.me-stepVideo__fallback { margin: 0 auto; max-width: 60rem; }
.me-stepVideo__poster { width: 100%; border-radius: 0.75rem; }
.me-stepVideo--static .me-stepVideo__fallbackCard { padding: 1.75rem 0 0.25rem; max-width: 36rem; }
.me-stepVideo__fallbackList { padding-left: 1.2rem; margin-top: 0.5rem; }
.me-stepVideo__fallbackList li { margin-bottom: 0.4rem; }

@media (prefers-reduced-motion: reduce) {
  .me-stepVideo__nodeDot { animation: none; }
  .me-stepVideo__screen { transition: none; }
}

/* overlay mode: media holds full-bleed and sticky; blocks scroll over it */
.me-expandMedia--overlay { height: auto; }
.me-expandMedia__pin { position: absolute; inset: 0; }
.me-expandMedia__blocks {
  position: relative; z-index: 2;
  padding: 0 1.5rem 25vh;
}
.me-expandMedia__block {
  min-height: 85vh;
  display: flex; align-items: center; justify-content: center;
}
.me-expandMedia__blockCard {
  max-width: 34rem; padding: 2rem 2.25rem; border-radius: 0.75rem;
  background: var(--me-block-fill, rgba(0 0 0 / 0.55)); color: #fff;
  backdrop-filter: blur(8px);
}
/* cardLevel can put this at h2, h3, or h4 depending on nesting (see
   expandMedia.js) — tag-qualified for the matching theme size. */
.me-expandMedia__blockHeading { margin-bottom: 0.75rem; }
.me-expandMedia__blockBody { margin-bottom: 0.6rem; opacity: 0.92; font-size: var(--me-size-p); }


/* blockStyle: stack — cards rise and come to rest on one another.
   The stacking is `position: sticky` at an offset that grows with the card's
   index, so nothing positions these from JS and the effect cannot drift.
   Scaling it to more cards is arithmetic, not another rule.

   The one thing JS contributes is --me-c (0..1): how far this card has been
   covered by the next one. It drives the shrink and the shading, which is
   what stops a finished deck from reading as a single card. */
.me-expandMedia__blocks[data-style='stack'] {
  --me-stack-top: 13vh;    /* where the first card comes to rest */
  --me-stack-step: 2.4rem; /* sliver of each card left visible below the next */
  --me-stack-travel: 46vh; /* scroll distance between one card and the next */
  --me-stack-dwell: 45vh;  /* how long the finished deck holds before leaving */
  display: flex; flex-direction: column;
  gap: var(--me-stack-travel);
  width: min(100%, 46rem); margin-inline: auto;
  /* The deck sits over full-bleed media, so its own copy is light regardless
     of the section theme — the deck copy IS a heading, so it follows the
     section heading colour and falls back to white. Set on the container
     rather than on the intro: the claim rule above forces the colour of
     every text element inside a section at (0,3,1), which outranks a single
     class on the element itself — so the colour has to arrive by
     inheritance to take effect at all. The cards override it again with
     their own ink. */
  color: var(--me-section-heading, #fff);
}
/* The dwell has to be a real sibling with height. Two things that look like
   they would work do not: padding on the container sits outside the content
   box that sticky is clamped to, and margin on the last card is *subtracted*
   from its sticky range, because the constraint rectangle is the containing
   block inset by the element's own margins. Either way the last card reached
   its rest position and was released in the same frame — it never joined the
   deck it had just completed. */
.me-expandMedia__stackTail { flex: none; height: var(--me-stack-dwell); }
/* Renders as h2 when the section has no own heading, h3 otherwise (see
   expandMedia.js's introLevel) — tag-qualified for the matching theme size. */
.me-expandMedia__blocksIntro {
  max-width: 24ch; margin: 0 auto clamp(3rem, 10vh, 8rem);
  text-align: center;
  font-weight: 700;
  line-height: 1.15; text-wrap: balance;
  text-shadow: 0 0.15rem 1.5rem rgba(0 0 0 / 0.65);
}

.me-expandMedia__card {
  --me-c: 0;
  position: sticky;
  top: calc(var(--me-stack-top) + var(--me-i) * var(--me-stack-step));
  display: flex; flex-direction: column;
  min-height: 20rem; padding: 2.6rem 2rem 2rem;
  border-radius: 1.5rem; overflow: hidden;
  /* Always a light card, whatever the section theme: it sits over media, and
     the whole point of the deck is that it reads as paper on top of picture.
     blocks[].fill recolours it through the same var the panel slab uses. */
  background: var(--me-block-fill, var(--me-light-bg)); color: var(--me-light-ink);
  box-shadow: 0 1.25rem 2.5rem rgba(0 0 0 / 0.3);
  transform: scale(calc(1 - 0.045 * var(--me-c)));
  transform-origin: top center;
}
/* Shading as a pseudo-element rather than a filter: filter would rasterise the
   whole card (video thumbnail included) on every scroll frame. */
.me-expandMedia__card::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: rgba(10 8 10 / calc(0.24 * var(--me-c)));
}
.me-expandMedia__cardStrip {
  position: absolute; top: 0; left: 0; right: 0; height: 0.5rem;
  background: var(--me-card-accent, var(--me-accent));
}
/* Base size comes from the global tag rule above, whatever level this card
   heading renders at. The `large` variant keeps its ~1.45x bump over that
   base size for visual emphasis within a stack — tag-qualified because the
   multiplier has to know which theme token it's scaling. */
.me-expandMedia__cardHeading { max-width: 18ch; }
.me-expandMedia__card--large .me-expandMedia__cardHeading { max-width: 12ch; }
.me-expandMedia__card--large h2.me-expandMedia__cardHeading { font-size: calc(var(--me-size-h2) * 1.45); }
.me-expandMedia__card--large h3.me-expandMedia__cardHeading { font-size: calc(var(--me-size-h3) * 1.45); }
.me-expandMedia__card--large h4.me-expandMedia__cardHeading { font-size: calc(var(--me-size-h4) * 1.45); }
.me-expandMedia__cardFoot {
  margin-top: auto; padding-top: 2rem;
  display: flex; align-items: flex-end; gap: 1.5rem;
}
.me-expandMedia__cardBody {
  flex: 1; font-size: var(--me-size-p); line-height: 1.5; opacity: 0.75;
}
.me-expandMedia__cardMedia {
  flex: none; width: 10.5rem; aspect-ratio: 5 / 4; margin: 0;
  border-radius: 0.6rem; overflow: hidden; background: rgba(0 0 0 / 0.35);
}
.me-expandMedia__cardMediaEl { width: 100%; height: 100%; object-fit: cover; display: block; }

@media (max-width: 768px) {
  .me-expandMedia__blocks[data-style='stack'] {
    --me-stack-top: 9vh; --me-stack-step: 1.5rem; --me-stack-travel: 34vh;
    width: 100%; padding-inline: 0;
  }
  .me-expandMedia__card { padding: 2rem 1.4rem 1.4rem; min-height: 16rem; }
  .me-expandMedia__cardFoot { flex-direction: column; align-items: stretch; gap: 1.1rem; }
  .me-expandMedia__cardMedia { width: 100%; aspect-ratio: 16 / 9; }
}

/* reduced motion: a plain column of cards, no sticking, no shading */
.me-expandMedia--static .me-expandMedia__blocks[data-style='stack'] {
  gap: 1.5rem; padding-bottom: 0;
}
.me-expandMedia--static .me-expandMedia__card {
  position: static; transform: none;
}
.me-expandMedia--static .me-expandMedia__card::after { display: none; }
/* Nothing to hold against when nothing sticks — otherwise it is 45vh of
   dead space under the last card. */
.me-expandMedia--static .me-expandMedia__stackTail { display: none; }
/* No media behind it in the static fallback, so the deck goes back to reading
   off the section theme. Released on the container for the same cascade
   reason the colour was set there. */
.me-expandMedia--static .me-expandMedia__blocks[data-style='stack'] { color: inherit; }
.me-expandMedia--static .me-expandMedia__blocksIntro { text-shadow: none; }

/* reduced-motion / static: plain figure in the flow, no pin */
.me-expandMedia--static { height: auto; }
.me-expandMedia--static .me-expandMedia__sticky {
  position: static; height: auto; overflow: visible;
  padding: clamp(4rem, 10vw, 8rem) 1.5rem;
}
.me-expandMedia--static .me-expandMedia__heading,
.me-expandMedia--static .me-expandMedia__caption { opacity: 1; }
.me-expandMedia--static .me-expandMedia__frame {
  width: min(100%, 60rem); height: auto;
  aspect-ratio: 16 / 9; border-radius: 0.75rem;
}
/* Nothing is pinned, so a full-bleed background would just be a tall band of
   picture behind the flow. Keep it, but let the section box bound it. */
.me-expandMedia--static .me-expandMedia__bg { position: absolute; }
.me-expandMedia--static .me-expandMedia__blocks { padding: 0 1.5rem; max-width: 60rem; margin: 0 auto; }
.me-expandMedia--static .me-expandMedia__block { min-height: 0; display: block; }
.me-expandMedia--static .me-expandMedia__blockCard {
  max-width: 36rem; padding: 1.75rem 0 0.25rem;
  background: none; color: inherit; backdrop-filter: none;
}

/* Pin-progress animation for --me-expand, driven by scroll position through
   the pin via a named view-timeline instead of a scroll listener (see
   expandMedia.js's top comment). Two shapes, because expandMedia has two
   structurally different pins:

   - Simple pin: the tall (pinLength * 100vh) .me-expandMedia root IS the pin
     -- same recipe as the hero rewrite's --me-hero-pin, a plain, non-sticky
     flow box, so `animation-range: contain` is exactly the stuck duration.
   - Overlay/blocks pin: the frame that reads --me-expand lives inside
     .me-expandMedia__pin, a sibling of .me-expandMedia__spacer, whose height
     *is* the desired progress range ((pinLength - 1) * 100vh). `timeline-scope`
     bridges the spacer's named timeline to the pin's subtree. The spacer's
     own "cover" transit runs from the spacer entering below the scrollport to
     fully exiting above it -- a span of (spacerHeight + viewportHeight) -- so
     the point where progress should read 0% (the spacer's top reaching the
     viewport top) lands at `cover calc(100% / pinLength)`, not `cover 0%`,
     because the transit includes the spacer's entry from below. This isn't
     obvious from the spec text alone -- it was verified numerically against
     the JS formula at pinLength 2 and 3 before being written here.

   `--me-pin-length` and `--me-expand-dir` are static, manifest-derived custom
   properties written once at mount (expandMedia.js) -- no DOM measurement.

   Unlike the hero's Firefox gap, Chrome/Edge/Safari/Firefox are all current
   on this feature set as of Aug 2026 -- the @supports gate here is a
   correctness guard for older versions of all four, not a browser-exclusion
   choice. Without it, an unsupported browser would run `animation:` on the
   default time-based timeline and freeze on its end keyframe. */
@property --me-expand {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}
@supports (animation-timeline: view()) {
  @keyframes me-expand-progress {
    from { --me-expand: 0; }
    to   { --me-expand: 1; }
  }
  .me-expandMedia:not(.me-expandMedia--overlay):not(.me-expandMedia--static) {
    view-timeline-name: --me-expand-pin;
    animation: me-expand-progress linear both;
    animation-timeline: --me-expand-pin;
    animation-range: contain;
    animation-direction: var(--me-expand-dir, normal);
  }
  .me-expandMedia--overlay {
    timeline-scope: --me-expand-pin-overlay;
  }
  .me-expandMedia--overlay > .me-expandMedia__spacer {
    view-timeline-name: --me-expand-pin-overlay;
  }
  .me-expandMedia--overlay > .me-expandMedia__pin {
    animation: me-expand-progress linear both;
    animation-timeline: --me-expand-pin-overlay;
    animation-range: cover calc(100% / var(--me-pin-length, 2)) cover 100%;
    animation-direction: var(--me-expand-dir, normal);
  }
  @media (prefers-reduced-motion: reduce) {
    .me-expandMedia, .me-expandMedia__pin { animation: none; }
  }
}

/* ---- tour ---- */
.me-tour { position: relative; }
.me-tour__stage {
  position: relative; width: 100%; height: 100vh; height: 100dvh;
  overflow: hidden; background: #000; touch-action: pan-y;
  user-select: none; -webkit-user-select: none;
  /* Anchor for .me-tour__poi: once that panel is a popover, the browser
     promotes it to the top layer, where plain `position: absolute` would
     resolve against the viewport instead of this (possibly letterboxed,
     non-fullscreen) stage box.
     Baseline: Chrome 125+ (May 2024), Safari 18.0+, Firefox 134+.
     In unsupported browsers the popover renders at viewport centre —
     still usable, just not pinned to the stage.  No JS fallback needed. */
  anchor-name: --me-tour-stage;
}
/* An authored aspect letterboxes instead of filling the viewport — the right
   choice for a master plan, where cropping can push a pin off screen. */
.me-tour[data-aspect='16:9'] .me-tour__stage { height: auto; aspect-ratio: 16 / 9; max-height: 100dvh; }
.me-tour[data-aspect='9:16'] .me-tour__stage { height: auto; aspect-ratio: 9 / 16; max-height: 100dvh; margin-inline: auto; width: auto; }
.me-tour:focus-visible { outline: 2px solid var(--me-accent); outline-offset: -2px; }
.me-tour.is-dragging .me-tour__stage { cursor: grabbing; }

/* Deck: stacked layers, one visible. Opacity is driven by .is-front and the
   duration is set per swap in JS — 0ms for frame-matched cuts. */
.me-tour__deck { position: absolute; inset: 0; }
.me-tour__layer {
  position: absolute; inset: 0; opacity: 0;
  transition: opacity 420ms linear;
  will-change: opacity;
}
.me-tour__layer.is-front { opacity: 1; z-index: 2; }
/* Geometry is set in px by the tour once the clip's real size is known (see
   frame.js) — not `inset: 0`, which would fight those inline left/top values.
   These are only the pre-metadata defaults. */
.me-tour__layerVideo, .me-tour__layerImg {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  display: block; object-fit: cover;
}
.me-tour[data-fit='contain'] :is(.me-tour__layerVideo, .me-tour__layerImg) { object-fit: contain; }
/* An empty pooled layer must not paint the browser's broken-image glyph. */
.me-tour__layerImg:not([src]) { display: none; }

/* Hotspot plane: sized by JS to the rendered frame (see hotspots.js). */
.me-tour__hotspots { position: absolute; inset: 0; z-index: 4; pointer-events: none; }
/* Zero-sized: the wrap IS the anchor point. The button hangs off it, shifted
   by JS so the dot — not the button's centre — lands on the coordinate, and
   flipped so the label always runs toward the middle of the stage. */
.me-tour__hotspot {
  position: absolute; width: 0; height: 0;
  opacity: 0; transition: opacity 0.45s ease 0.1s; pointer-events: none;
}
.me-tour[data-state='resting'] .me-tour__hotspot { opacity: 1; pointer-events: auto; }
.me-tour__hotspotBtn {
  position: absolute; top: 0; left: 0; white-space: nowrap;
  display: flex; align-items: center; gap: 0.55rem;
  background: none; border: none; cursor: pointer; color: #fff;
  font: inherit; padding: 0.3rem; text-shadow: 0 1px 6px rgba(0 0 0 / 0.6);
  transform: translate(-0.75rem, -50%); /* replaced by the measured offset */
}
.me-tour__hotspot.is-flipped .me-tour__hotspotBtn { flex-direction: row-reverse; }
.me-tour__hotspotDot {
  flex: none; width: 0.9rem; height: 0.9rem; border-radius: 50%;
  /* The shared me-node-pulse keyframes tint the ring from this var too, so a
     per-hotspot dotColor recolours the dot and its pulse together. */
  background: var(--me-node-dot, var(--me-accent));
  box-shadow: 0 0 0 0 rgba(255 194 14 / 0.55);
  animation: me-node-pulse 2s ease-out infinite;
}
.me-tour__hotspotIcon { flex: none; width: 2.25rem; height: 2.25rem; object-fit: contain; }
.me-tour__hotspotLabel { font-size: 0.85rem; letter-spacing: 0.06em; text-transform: uppercase; }
.me-tour__hotspot--pin .me-tour__hotspotBtn {
  padding: 0.4rem 0.9rem 0.4rem 0.5rem; border-radius: 999px;
  background: rgba(0 0 0 / 0.45); backdrop-filter: blur(6px);
}
.me-tour__hotspot--pin.is-flipped .me-tour__hotspotBtn { padding: 0.4rem 0.5rem 0.4rem 0.9rem; }
.me-tour__hotspot--pin .me-tour__hotspotBtn:hover { background: rgba(0 0 0 / 0.7); }
.me-tour__hotspot.is-open .me-tour__hotspotDot { background: #fff; }

/* Chrome */
.me-tour__ui { position: absolute; inset: 0; z-index: 6; pointer-events: none; }
.me-tour__ui button { pointer-events: auto; }

.me-tour__loader {
  position: absolute; inset: 0; display: grid; place-items: center;
  opacity: 0; transition: opacity 0.3s ease; background: rgba(0 0 0 / 0.25);
}
.me-tour[data-state='loading'] .me-tour__loader { opacity: 1; transition-delay: 0.4s; }
.me-tour__spinner {
  width: 2rem; height: 2rem; border-radius: 50%;
  border: 2px solid rgba(255 255 255 / 0.25); border-top-color: #fff;
  animation: me-tour-spin 0.9s linear infinite;
}
@keyframes me-tour-spin { to { transform: rotate(360deg); } }

.me-tour__info {
  position: absolute; left: clamp(1.25rem, 4vw, 3rem); bottom: clamp(1.25rem, 4vw, 3rem);
  max-width: min(28rem, 60vw); color: var(--me-section-ink, #fff); pointer-events: none;
  text-shadow: 0 1px 12px rgba(0 0 0 / 0.55);
  transition: opacity 0.3s ease;
}
/* The stage's own info title renders as h2; a hotspot's node-label card
   reuses this class as h3 (see tour.js) — tag-qualified accordingly. */
.me-tour__title {
  margin-bottom: 0.5rem;
  /* Reused by the static fallback as h3 (no info card above it) — inherit
     keeps fallback titles on the section ink when headingColor is unset. */
  color: var(--me-section-heading, inherit);
}
.me-tour__body p { font-size: var(--me-size-p); opacity: 0.9; margin-bottom: 0.35rem; }
.me-tour__metrics { display: flex; gap: 1.75rem; margin-top: 1rem; flex-wrap: wrap; }
.me-tour__metrics dt { font-size: 1.5rem; font-weight: 700; color: var(--me-accent); }
.me-tour__metrics dd { font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; opacity: 0.8; }

.me-tour__nav {
  position: absolute; right: clamp(1.25rem, 4vw, 3rem); bottom: clamp(1.25rem, 4vw, 3rem);
  display: flex; align-items: center; gap: 0.6rem;
  transition: opacity 0.3s ease;
}
.me-tour__btn {
  font: inherit; font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--me-nav-color, #fff); background: rgba(0 0 0 / 0.45); border: 1px solid rgba(255 255 255 / 0.25);
  border-radius: 999px; padding: 0.6rem 1.1rem; cursor: pointer;
  backdrop-filter: blur(6px); transition: background 0.2s ease, opacity 0.2s ease;
}
.me-tour__btn:hover { background: rgba(0 0 0 / 0.72); }
.me-tour__btn[hidden] { display: none; }
.me-tour__ringPos { color: var(--me-nav-color, #fff); font-size: 0.8rem; opacity: 0.75; font-variant-numeric: tabular-nums; }
.me-tour__ringPos[hidden] { display: none; }
/* Controls are meaningless mid-move; dim rather than remove so the bar does
   not reflow under the pointer. */
.me-tour:not([data-state='resting']) .me-tour__nav { opacity: 0.35; pointer-events: none; }

.me-tour__poi {
  /* Anchored to .me-tour__stage (see anchor-name above) rather than DOM
     position, since a popover renders in the top layer regardless of where
     it sits in the document. */
  position: fixed; position-anchor: --me-tour-stage; margin: 0;
  top: anchor(top); bottom: anchor(bottom); right: anchor(right); left: auto;
  width: min(26rem, 88vw); height: auto; max-width: none; max-height: none;
  padding: clamp(1.5rem, 4vw, 2.5rem); border: none;
  background: var(--me-poi-fill, rgba(12 10 12 / 0.88)); backdrop-filter: blur(14px);
  color: var(--me-poi-color, #fff); overflow-y: auto;
  display: flex; flex-direction: column; gap: 1rem;
  transform: translateX(100%);
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1),
    display 0.45s allow-discrete, overlay 0.45s allow-discrete;
}
.me-tour__poi:popover-open { transform: none; }
@starting-style {
  .me-tour__poi:popover-open { transform: translateX(100%); }
}
.me-tour__poiClose {
  align-self: flex-end; font: inherit; font-size: 0.75rem; letter-spacing: 0.08em;
  text-transform: uppercase; background: none; border: 1px solid rgba(255 255 255 / 0.3);
  border-radius: 999px; padding: 0.4rem 0.9rem; color: var(--me-poi-color, #fff); cursor: pointer;
}
.me-tour__poiMedia:empty { display: none; }
.me-tour__poiMedia :is(img, video) { width: 100%; border-radius: 0.6rem; display: block; }
.me-tour__poiBody p { font-size: var(--me-size-p); opacity: 0.9; margin-bottom: 0.5rem; }

@media (max-width: 768px) {
  .me-tour__info { max-width: 70vw; bottom: 5.5rem; }
  .me-tour__nav { left: clamp(1.25rem, 4vw, 3rem); justify-content: center; }
  .me-tour__hotspotLabel { font-size: 0.72rem; }
}

/* reduced motion / no playable clip: the whole tour as a document */
.me-tour--static .me-tour__fallback {
  padding: clamp(3rem, 8vw, 6rem) 1.5rem; max-width: 60rem; margin: 0 auto;
  display: flex; flex-direction: column; gap: 2.5rem;
}
.me-tour--static .me-tour__fallbackPoster { width: 100%; border-radius: 0.75rem; display: block; }
.me-tour--static .me-tour__metrics dt { color: inherit; }
.me-tour--static .me-tour__fallbackList { padding-left: 1.25rem; margin-top: 0.75rem; }
.me-tour--static .me-tour__fallbackList li { margin-bottom: 0.4rem; }

/* ---- gate ----------------------------------------------------------------
   A cover screen with one button. `meta.gate` puts it over the whole page;
   the tour mounts the same markup inside its stage (--tour). Also the gesture
   that unlocks autoplay, and the window in which the opening media buffers. */
.me-gate {
  position: absolute; inset: 0; display: grid; place-items: center;
  background: #0c0a0c; pointer-events: auto;
  opacity: 1; transition: opacity 0.55s ease;
}
.me-gate.is-gone { opacity: 0; pointer-events: none; }

/* Above the tour's UI layer (z-index 6). */
.me-gate--tour { z-index: 8; }

/* Over the page, not over a section: fixed, so scrolling behind it — which the
   lock below prevents anyway — could never slide it away. The z-index has to
   clear a host site's fixed nav; raise it on .me-gate--page if a theme goes
   higher still. `dialog.me-gate--page` (tag-qualified) ties the UA
   stylesheet's `dialog:modal` on specificity and wins on source order: that
   rule centres a fit-content box with a capped max-width/height, all reset
   here so showModal() still covers the viewport exactly like the plain div
   did before. */
dialog.me-gate--page {
  position: fixed; z-index: 9999; overscroll-behavior: contain;
  margin: 0; padding: 0; border: none;
  width: 100%; height: 100%; max-width: none; max-height: none;
}
.me-gate--page::backdrop { background: transparent; }
html.me-gate-lock, html.me-gate-lock body { overflow: hidden; }

.me-gate__bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0.4; }
.me-gate__card { position: relative; text-align: center; max-width: 32rem; padding: 2rem; }

/* The card is not inside a .me-section, so the rule that makes section text
   claim its colour does not reach it — and a host theme's global
   `h1,h2,h3 { color }` beats an inherited value however weak it is. Both the
   heading and the body state their colour here for that reason. */
.me-gate__heading { color: #fff; margin-bottom: 0.75rem; }
.me-gate__body { color: #fff; opacity: 0.85; margin-bottom: 0.5rem; font-size: var(--me-size-p); }
.me-gate__btn {
  margin-top: 1.5rem; font: inherit; font-size: 0.85rem; letter-spacing: 0.12em;
  text-transform: uppercase; cursor: pointer; color: #14100f;
  background: var(--me-accent); border: none; border-radius: 999px; padding: 0.9rem 2.2rem;
}
.me-gate__btn:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .me-gate { transition: none; }
}

/* ---- cardTrack ----------------------------------------------------------
   A heading over a horizontal strip of cards. The strip is a real scroll
   container: touch drag, trackpad, momentum, the scrollbar and scroll-into-
   view for a focused card are all the browser's, and only the mouse
   grab-to-drag is ours. Proportions follow the reference — a 425px card with
   a 1.49:1 picture at 1280px wide. */
.me-cardTrack {
  padding: clamp(3.5rem, 9vw, 6rem) 0 clamp(4rem, 10vw, 7rem); overflow: hidden;
  --me-track-gutter: clamp(1.5rem, 5vw, 3rem);
  /* The strip is full-bleed but its first card has to line up with the heading,
     which is centred in a 78rem column. Past that width the gutter alone stops
     matching — at 1920 it would leave the card 336px adrift — so the inset
     carries the centring margin too. Below 78rem the max() falls back to the
     plain gutter. */
  --me-track-inset: max(
    var(--me-track-gutter),
    calc((100% - 78rem) / 2 + var(--me-track-gutter))
  );
}
.me-cardTrack__inner {
  max-width: 78rem; margin: 0 auto;
  padding: 0 var(--me-track-gutter) clamp(2rem, 4vw, 3rem);
}
.me-cardTrack__heading {
  letter-spacing: -0.01em; text-wrap: balance;
}
.me-cardTrack__intro { max-width: 42rem; margin-top: 1rem; opacity: 0.85; font-size: var(--me-size-p); }

.me-cardTrack__strip { position: relative; }
.me-cardTrack__track {
  list-style: none; margin: 0; display: flex; gap: clamp(0.75rem, 1.5vw, 1.25rem);
  overflow-x: auto; overflow-y: hidden;
  /* The strip runs to the edges of the page, so the first card lines up with
     the heading and the last one can still be dragged clear of the edge. */
  padding: 0.5rem var(--me-track-gutter) 1.5rem var(--me-track-inset);
  scroll-padding-inline-start: var(--me-track-inset);
  /* A horizontal drag at the edge of the strip is a back-swipe on a trackpad
     otherwise, which navigates away mid-browse. */
  overscroll-behavior-x: contain;
  cursor: grab;
  /* The bar is the one part of a native scroller that looks wrong here — a
     grey slab under a row of cards. Hiding it costs nothing the strip needs:
     it still scrolls by every means, and the cut-off card at the edge is the
     affordance that there is more. */
  scrollbar-width: none;
}
.me-cardTrack__track::-webkit-scrollbar { width: 0; height: 0; display: none; }
.me-cardTrack__track::after {
  /* A trailing gutter as an inert box: padding-inline-end on a flex scroll
     container is dropped at the end of the scroll range in some engines, so
     the last card ends up flush against the edge. */
  content: ''; flex: 0 0 clamp(1rem, 20vw, 16rem);
}
.me-cardTrack__track--snap { scroll-snap-type: x proximity; }
.me-cardTrack__track--snap .me-cardTrack__card { scroll-snap-align: start; }
.me-cardTrack__track.is-dragging { cursor: grabbing; scroll-snap-type: none; }
/* Selecting the copy while dragging turns the strip blue from card to card. */
.me-cardTrack__track.is-dragging * { user-select: none; }

.me-cardTrack__card { flex: 0 0 auto; width: var(--me-card-w, 24rem); max-width: 82vw; }
.me-cardTrack--wide .me-cardTrack__card { --me-card-w: 30rem; }
.me-cardTrack__link { display: block; color: inherit; text-decoration: none; }
.me-cardTrack__figure {
  position: relative; overflow: hidden; border-radius: 0.85rem;
  aspect-ratio: var(--me-track-ratio, 3 / 2); background: var(--me-figure-fill, rgba(128 128 128 / 0.12));
}
/* The picture is the drag handle as much as anything else, and a native image
   drag mid-throw leaves the strip stuck to the pointer. */
.me-cardTrack__media {
  width: 100%; height: 100%; object-fit: cover; display: block;
  -webkit-user-drag: none; user-select: none;
  /* opacity rides with transform so the me-media-fade reveal doesn't
     clobber the hover zoom (transition is one property — the last
     declaration in file order wins the whole list). */
  transition: opacity 0.6s ease, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.me-cardTrack__badge {
  position: absolute; right: 0.75rem; bottom: 0.75rem;
  padding: 0.3rem 0.7rem; border-radius: 999px;
  background: var(--me-badge-fill, rgba(0 0 0 / 0.6)); color: #fff;
  font-size: 0.72rem; letter-spacing: 0.04em;
}
.me-cardTrack__tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 1rem; }
.me-cardTrack__tag {
  padding: 0.3rem 0.6rem; border-radius: 0.45rem;
  background: var(--me-tag, var(--me-accent)); color: var(--me-surface);
  font-size: 0.7rem; letter-spacing: 0.02em; line-height: 1.2;
}
.me-cardTrack__title { margin-top: 0.75rem; }
.me-cardTrack__body { margin-top: 0.5rem; opacity: 0.85; font-size: var(--me-size-p); }

.me-cardTrack__link:hover .me-cardTrack__media { transform: scale(1.03); }
.me-cardTrack__track:focus-visible { outline: 2px solid var(--me-accent); outline-offset: 4px; }

/* Prev/next. Centred on the picture band, whose height JS measures once the
   strip has layout — the card is taller than its picture by however much copy
   it carries, so centring on the card would put these in the body text. */
.me-cardTrack__arrow {
  position: absolute; z-index: 2;
  top: calc(0.5rem + var(--me-figure-h, 12rem) / 2); transform: translateY(-50%);
  width: 2.75rem; height: 2.75rem; padding: 0; border-radius: 999px; cursor: pointer;
  display: grid; place-items: center;
  color: var(--me-arrow-color, #fff); background: rgba(0 0 0 / 0.45);
  border: 1px solid rgba(255 255 255 / 0.25);
  backdrop-filter: blur(6px);
  transition: background 0.2s ease, opacity 0.2s ease;
}
.me-cardTrack__arrow--prev { left: clamp(0.5rem, 2vw, 1.25rem); }
.me-cardTrack__arrow--next { right: clamp(0.5rem, 2vw, 1.25rem); }
.me-cardTrack__arrow:hover { background: rgba(0 0 0 / 0.72); }
.me-cardTrack__arrow[hidden] { display: none; }
/* Dimmed rather than removed at the ends: a control that vanishes under the
   pointer moves the other one and makes the pair feel unreliable. */
.me-cardTrack__arrow:disabled { opacity: 0.3; cursor: default; }
.me-cardTrack__arrow:disabled:hover { background: rgba(0 0 0 / 0.45); }
.me-cardTrack__chevron {
  width: 0.6rem; height: 0.6rem;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
}
.me-cardTrack__arrow--prev .me-cardTrack__chevron { transform: rotate(135deg); margin-left: 0.2rem; }
.me-cardTrack__arrow--next .me-cardTrack__chevron { transform: rotate(-45deg); margin-right: 0.2rem; }

/* A finger already drags the strip; the buttons would only cover a card. */
@media (hover: none) {
  .me-cardTrack__arrow { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .me-cardTrack__media { transition: none; }
  .me-cardTrack__link:hover .me-cardTrack__media { transform: none; }
}

/* ---- page chrome: scroll-progress bar + section dots (meta.nav) ---- */
/* Fixed to the viewport, not to a section. Like the gate, the z-index has
   to clear a host site's fixed nav; raise it if a theme goes higher still.
   Stays below the gate (dialog, top layer, 9999) so the gate covers the
   chrome while it is up. As with the gate, a transformed host ancestor
   would break position: fixed — same limitation, accepted. */
.me-pageNav { position: fixed; inset: 0; z-index: 9990; pointer-events: none; }
.me-pageNav__bar { position: absolute; top: 0; left: 0; right: 0; height: 3px; }
.me-pageNav__bar i {
  display: block; height: 100%;
  background: var(--me-accent); /* token inherited from .me-root */
  transform: scaleX(var(--me-progress, 0));
  transform-origin: left center;
}
.me-pageNav__dots {
  position: absolute; top: 50%; right: 1rem; transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 0.75rem;
  margin: 0; padding: 0; list-style: none;
  pointer-events: auto;
}
/* The whole button is the 24px hit target; the visible dot is smaller and
   centred inside it (same call as stepVideo's markers). */
.me-pageNav__dot {
  width: 1.5rem; height: 1.5rem; padding: 0;
  border: 0; border-radius: 50%;
  background: transparent; cursor: pointer; position: relative;
}
.me-pageNav__dot::before {
  content: ''; position: absolute; inset: 0; margin: auto;
  width: 0.65rem; height: 0.65rem; border-radius: 50%;
  background: rgba(0 0 0 / 0.25); /* before html[data-me-nav] first lands */
  transition: background-color 200ms ease;
}
html[data-me-nav='light'] .me-pageNav__dot::before { background: rgba(0 0 0 / 0.4); }
html[data-me-nav='dark'] .me-pageNav__dot::before { background: rgba(255 255 255 / 0.4); }
.me-pageNav__dot.active::before { background: var(--me-accent); }
.me-pageNav__dot:focus-visible { outline: 2px solid var(--me-accent); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  .me-pageNav__dot::before { transition: none; }
}
