/* ================================================================
   membrane.css — unified "tulpa membrane" material system.
   Load order: AFTER shared.css. Self-contained; never imported by
   shared.css so it can be added/removed without touching the rest.

   The system at a glance
   ----------------------
   The material is a 4-layer pill (shaper > surface > iri-clip > iri)
   plus an optional .pill-text on top. Only ONE .pill-surface in the
   document may carry .active at any moment — the .active class is
   what enables the SVG-displacement filter and the iridescence
   animations. Every .pill-surface is tagged with data-floor="<name>"
   identifying which overlay/floor it belongs to. The membrane.js
   stack manager toggles .active on the surfaces of whichever floor
   is currently topmost.

   Floor naming convention (used everywhere in the site)
       main       — the page itself (sticky header, future home pills)
       menu       — the .mstack mobile menu overlay
       about      — #ov-about
       subscribe  — #ov-subscribe
       privacy    — #ov-privacy
       cookies    — #ov-cookies
       lightbox   — #lightbox

   "Skin" — visual tone of the surface (background/border/shadow).
       data-skin="dark"   (default) — the spec's frosted-dark glass.
       data-skin="light"  — picks up the site's existing light-glass
                            CSS variables so the membrane blends with
                            the rest of the chrome (sticky header,
                            mob-menu-btn, overlays, etc.).

   "Form" — base shape preset. Defaults to a 28px pill; small chips
   use data-form="small" (18px). Per-instance overrides remain free
   via inline border-radius on the .pill-shaper.
   ================================================================ */


/* ── 1. Shaper — morphs freely, NEVER overflow:hidden ─────────────
   Position is left to the host (the shaper inherits the size and
   placement of whatever it's wrapping — sticky header, button,
   custom widget). We only define the shape + transition. */
.pill-shaper{
  position:relative;
  border-radius:28px;
  transition:border-radius 0.7s cubic-bezier(0.34,1.56,0.64,1);
  cursor:pointer;
}
.pill-shaper[data-form="small"]{
  border-radius:18px;
}
/* Shapeshift bleed — opt-in via data-shape-on="hover". Without it
   the pill stays a regular pill (e.g. a sticky header pill that
   the user wouldn't expect to morph on hover). */
.pill-shaper[data-shape-on="hover"]:hover{
  border-radius:44px 20px 36px 24px / 24px 36px 20px 44px;
}
.pill-shaper[data-shape-on="hover"][data-form="small"]:hover{
  border-radius:24px 10px 18px 14px / 14px 18px 10px 24px;
}


/* ── 2. Surface — bg, border, shadow, filter when .active ────────
   SOURCE MATERIAL VERBATIM from material-on-tulpas-compare_5.html
   (which is the canonical spec — overrules the stress-test note
   that said "never overflow:hidden on .pill-surface". Compare-5
   uses overflow:hidden on .pill precisely because filter:url
   creates a rectangular compositing layer that Safari does NOT
   clip cleanly to the parent's rounded border-radius. Without
   overflow:hidden HERE, the user sees a rectangular halo around
   the rounded sticky pill — which is the bug they kept reporting.
   The minor loss of "displacement bleed past the edge" is the
   correct trade-off; visual containment wins.) */
.pill-surface{
  position:absolute;inset:0;
  border-radius:inherit;
  /* overflow:hidden is universal again — keeps iri layers contained
     (their inset:-12% + blur(20px) leaks past the surface bounds
     without it). The earlier hypothesis that this combo broke desktop
     displacement turned out to be wrong; reverted. */
  overflow:hidden;
  background:rgba(20,15,10,0.42);
  border:1px solid rgba(255,255,255,0.18);
  /* SOURCE SPEC VERBATIM (material-on-tulpas-compare_5.html):
     just the outer drop + a single 1px white top highlight.
     Earlier rounds added three additional inset shadows
     (a stronger top, a secondary glow, a bottom darken, a
     perimeter outline) to pump the "light-catching edge" feel,
     but the user reverted: spec recipe reads cleaner without it. */
  box-shadow:
    0 12px 32px -10px rgba(0,0,0,0.45),
    inset 0 1px 0 rgba(255,255,255,0.30);
  /* DEFAULT: surface is visually OFF when not on the top floor.
     This isn't just visibility — opacity:0 lets the browser skip
     painting the iri layers' static conic-gradient + blur +
     mix-blend-mode entirely (those would otherwise consume GPU
     even with the SVG filter and CSS animations turned off).
     Combined with `.active` adding opacity back + the filter, the
     net result: ONLY the topmost floor's pill is rendered at all,
     every other membrane'd element has zero painting cost. */
  opacity:0;
  transition:opacity 0.32s ease;
  pointer-events:none;
}
.pill-surface.active{
  opacity:1;
  pointer-events:auto;
  filter:url(#waterRippleCtx);
  /* `isolation:isolate` creates a new stacking context for the children.
     Without this, Chromium/Edge flatten the inner `filter:blur(20px)` on
     .iri when the parent already has `filter:url()` applied — the
     iridescence renders as sharp conic-gradient sectors instead of a
     soft blurred wash. Safari handles the nested filter correctly even
     without isolation, but adding it here is harmless on Safari and
     fixes the Chromium rendering. (Reported only on the desktop sticky
     compact pill, but the fix applies universally.) */
  isolation:isolate;
}

/* ── LITE variant — radiant glass that EMITS light ────────────────
   A completely different material than the full dark frosted
   membrane. Where the full variant reads as a dark slab with a
   subtle iridescent wash, the lite variant reads as a panel
   internally lit — cream-warm radial fade across the surface,
   bright fresnel edges, a triple-ringed warm halo radiating outward,
   and the iri layers cranked + switched to `screen` blend so their
   colors ADD light to the surface rather than overlay-darken it.

   It's also much cheaper to render than the full variant: no
   per-frame SVG displacement (which is the dominant cost of the
   full material). The full material has the wobble; the lite
   material has the glow. Both keep the iri shimmer.

   How to opt in:
     · CSS — add `.lite` to the `.pill-surface`, OR
     · CSS — context-target via a parent selector
             (e.g. `.snav-item .pill-surface { … }`)

   Sticky compact stays FULL — it's the signature dark-frosted
   pill of the site. Mobile membrane'd elements stay full too:
   the floor stack already guarantees only ONE surface ever has
   .active at a time on mobile, so cost is capped — and the dark
   variant is the established mobile vocabulary.

   Lite is used on:
     · Desktop menu rows on hover (.snav-item) — many rows, hover
       moves continuously, lite scales without becoming a heat
       source AND reads as luminous accent rather than a dark slab.
     · Desktop [data-membrane] buttons — they share the screen with
       the sticky compact pill on desktop, so a different visual
       (radiant cream vs frosted dark) creates a clean hierarchy. */

/* Radiant-light material — the visual values are stored as custom
   properties so the same recipe can be referenced from both the
   explicit `.lite` class AND the desktop auto-lite contexts
   (.snav-item hover, [data-membrane]) without duplication. */
:root{
  --m-radiant-bg:
    radial-gradient(ellipse 130% 115% at 50% 28%,
      rgba(255,250,238,0.55) 0%,
      rgba(255,238,215,0.32) 55%,
      rgba(255,222,180,0.20) 100%);
  --m-radiant-border: rgba(255,240,215,0.70);
  --m-radiant-shadow:
    /* triple outer warm halo — RADIATING light into surroundings */
    0 0 14px rgba(255,232,195,0.55),
    0 0 32px rgba(255,215,160,0.32),
    0 0 64px rgba(255,200,140,0.16),
    /* fresnel inner highlights — bright top + perimeter */
    inset 0 2px 0 rgba(255,255,255,1.00),
    inset 0 5px 10px rgba(255,255,255,0.55),
    inset 0 0 0 0.5px rgba(255,255,255,0.78);
  --m-radiant-iri-filter: blur(20px) saturate(2.2) brightness(1.25);
  --m-radiant-iri-opacity: 1.0;
}

.pill-surface.lite.active{
  filter:none !important;
  background:var(--m-radiant-bg) !important;
  border-color:var(--m-radiant-border) !important;
  box-shadow:var(--m-radiant-shadow) !important;
}
.pill-surface.lite.active .iri{
  opacity:var(--m-radiant-iri-opacity);
  mix-blend-mode:screen; /* additive — colors ADD light */
  filter:var(--m-radiant-iri-filter);
}
.pill-surface.lite.active .iri.b{
  mix-blend-mode:screen;
  opacity:0.85;
}


/* ── 3. ripple-fx — non-clipping wrapper for the iri layers.
   Matches the source spec exactly: just a positioned container
   that holds the two iri elements. NO overflow:hidden here — the
   clipping happens at .pill-surface (single clipping layer for
   the whole pill, which is what the spec's .pill does too). The
   previous .iri-clip with its own overflow:hidden was a redundant
   third clipping layer that confused Safari's compositor when
   stacked under .pill-surface's filter:url. */
.ripple-fx{
  position:absolute;inset:0;
  z-index:1;
  pointer-events:none;
}


/* ── 4. iri / iri.b — iridescence layers ─────────────────────────
   SOURCE SPEC VERBATIM. animation:none ↔ iriRotate toggle (NOT
   the play-state preservation I was using before — the user
   wants the original behaviour). */
.iri{
  position:absolute;inset:-12%;
  pointer-events:none;
  background:conic-gradient(
    from 0deg at 50% 50%,
    rgba(180,210,225,0.50) 0%,  rgba(220,180,200,0.45) 14%,
    rgba(225,210,160,0.50) 28%, rgba(170,200,170,0.45) 42%,
    rgba(160,180,220,0.50) 56%, rgba(220,160,180,0.45) 70%,
    rgba(225,210,160,0.50) 84%, rgba(180,210,225,0.50) 100%
  );
  filter:blur(20px) saturate(1.2);
  mix-blend-mode:overlay;
  opacity:0.70;
  animation:none;
  /* GPU-layer hint — forces Chromium/Edge to render this element's
     filter:blur into its own composite layer, bypassing the parent
     .pill-surface's filter:url() compositing pass. Without this hint
     Chromium can drop the blur entirely on nested-filter ancestors,
     which the user observed as "sharp iridescence with no blur" on
     the desktop sticky compact pill. Safari renders correctly either
     way; will-change is just a cheap hint that costs nothing on
     Safari and fixes the Chromium bug. */
  will-change:filter, transform;
}
.iri.b{
  background:conic-gradient(
    from 180deg at 30% 70%,
    rgba(200,180,220,0.40), rgba(225,200,170,0.35),
    rgba(180,210,200,0.45), rgba(220,180,210,0.40),
    rgba(200,180,220,0.40)
  );
  mix-blend-mode:soft-light;
  animation:none;
}
.pill-surface.active .iri  { animation:iriRotate    36s linear infinite; }
.pill-surface.active .iri.b{ animation:iriRotateRev 52s linear infinite; }

@keyframes iriRotate{
  0%   { transform:rotate(0deg)   scale(1.00); }
  50%  { transform:rotate(180deg) scale(1.06); }
  100% { transform:rotate(360deg) scale(1.00); }
}
@keyframes iriRotateRev{
  0%   { transform:rotate(0deg);   }
  100% { transform:rotate(-360deg);}
}


/* ── 5. pill-text — content layer above the surface ──────────────
   Provides ONLY stacking + a polite pointer-events:none so clicks
   pass through to the host. Padding/typography belong to the host
   pill (sticky header, button, etc.). The membrane.js auto-wrap
   step uses this class to lift direct text nodes above the
   surface in declarative `data-membrane` retrofits. */
.pill-text{
  position:relative;
  z-index:5;
  pointer-events:none;
}


/* ── 6. Reduced motion — switch off the membrane animations.
   The displacement filter is the heaviest part; keep the surface
   visible (still looks like glass) but kill the moving parts. */
@media(prefers-reduced-motion: reduce){
  .pill-surface.active{ filter:none; }
  .pill-surface.active .iri,
  .pill-surface.active .iri.b{ animation:none; }
}

/* (Mobile-only overflow:hidden block deleted — turned out the
   overflow:hidden vs filter:url incompatibility was a wrong guess.
   The actual desktop displacement issue was unrelated: the GDPR
   banner was permanently keeping the 'gdpr' floor on top, which
   deactivated the sticky surface. Banner z-index fix in shared.css
   resolved that. overflow:hidden is universal again, see the
   .pill-surface base rule above.) */


/* ════════════════════════════════════════════════════════════════
   STICKY HEADER INTEGRATION
   ────────────────────────────────────────────────────────────────
   The subpage `.page-header-sticky` element doubles as the shaper
   for its compact-state pill. The pill-surface is injected into it
   at runtime by membrane.js (with data-floor="main", data-skin="light").

   Strategy:
     · Non-compact (full-bar) state — the .pill-surface fades to
       opacity 0 + pointer-events:none. The existing
       .page-header-sticky background/border/box-shadow keep their
       roles. Membrane is dormant.
     · Compact state — we strip the existing background/border/
       shadow off .page-header-sticky so the injected .pill-surface
       takes over rendering. We also drop overflow:hidden so the
       displacement bleed at the surface edge isn't clipped. The
       surface only carries .active when the floor stack puts main
       on top (membrane.js handles that wiring).
   ════════════════════════════════════════════════════════════════ */

/* The injected .pill-surface inside .page-header-sticky is fully
   handled by the global rule above (opacity:0 default, opacity:1
   only when .active). The sticky's surface only gains .active when
   topFloor==='main' AND sticky.classList.contains('compact'), so:
     · Top of scroll, no overlay  → not compact → no .active → opacity:0
     · Scrolled,    no overlay    → compact + main top → .active → opacity:1
     · Any overlay open           → main not top → no .active → opacity:0
                                    (even if sticky is still .compact —
                                     that's the harden-inactive fix). */
.page-header-sticky > .pill-surface{
  z-index:0;
}

/* DESKTOP-ONLY: extend the sticky's pill-surface BEYOND the visible
   sticky bounds on top/left/right. The SVG displacement filter
   wobbles pixels at all four edges of the surface; on the desktop
   compact strip the user only wants to see the wobble at the
   BOTTOM edge — not at the top (which sits at viewport y=0) and
   not at the sides (which sit against the left-bar / viewport
   edge). Pulling top/left/right to -28px pushes those wobbling
   edges off-screen / behind the left-bar, while bottom:0 keeps
   the bottom wobble exactly at the visible boundary.
   Mobile is unaffected: the rounded pill (border-radius:28px on
   sticky.compact) wraps cleanly on all sides and the wobble there
   reads as part of the membrane character. */
@media(min-width:701px){
  body[data-page="sub"] .page-header-sticky.compact > .pill-surface{
    /* Higher-specificity selector (0,0,3,1 vs the base .pill-surface
       at 0,0,1,0), so these individual top/left/right/bottom values
       override the base rule's `inset:0` shorthand cleanly. No need
       for an extra `inset:auto` reset (which we tried first and got
       wrong — `inset:auto` placed after the longhands collapsed the
       surface to 0×0 because the shorthand resets ALL four longhands). */
    top:-28px;
    left:-28px;
    right:-28px;
    bottom:0;
  }
}
/* Existing header content sits above the surface. */
.page-header-sticky > .page-header,
.page-header-sticky > .header-left,
.page-header-sticky > .header-right{
  position:relative;
  z-index:5;
}

/* shared.css now scopes the sticky's bg/blur/border-bottom to
   `:not(.compact)`, so the compact state is naturally transparent.
   No override needed here — the only thing this file does for the
   sticky compact state is fade the .pill-surface in. */
body[data-page="sub"] .page-header-sticky.compact > .pill-surface{
  opacity:1;
}

/* Mobile compact already gets `border-radius:28px` in shared.css —
   that's the .pill-shaper's role. Desktop compact in shared.css
   doesn't round the corners (full-width strip with smaller padding);
   we leave that alone. The membrane still renders inside, just as a
   rectangle with a faint sheen — which is the intended desktop
   compact look. */

/* Reduce backdrop-filter cost on mobile compact: the injected
   light-skin surface already has its own backdrop-filter, and the
   parent .page-header-sticky used to as well — that doubled the
   blur cost. We dropped the parent's blur in the compact override
   above, so only the surface still blurs. Net: same look, half the
   cost on the heaviest mobile breakpoint. */


/* ════════════════════════════════════════════════════════════════
   DECLARATIVE OPT-IN
   ────────────────────────────────────────────────────────────────
   For elements you don't want to retrofit by hand, slap
   data-membrane="<floor>" on any positioned wrapper and
   membrane.js will inject the surface inside it + tag it. This is
   the path forward for rolling membrane out to home pills, overlay
   buttons, etc. without rewriting their markup.
     <button class="sub-link" data-membrane="subscribe">…</button>
   ════════════════════════════════════════════════════════════════ */
[data-membrane]{
  position:relative;        /* surface needs a positioned parent */
}
[data-membrane] > .pill-surface{
  z-index:0;
}
[data-membrane] > *:not(.pill-surface){
  position:relative;
  z-index:5;
}
/* Cream-white text on every membrane'd element (the dark
   translucent surface needs light text). Same value the source
   spec uses for .pill-text (rgba(245,238,225,0.95)) plus the
   .pill-title text-shadow. !important so it beats per-button
   color rules in shared.css (e.g. .choice-strip-accept's !important
   --text-bright, .about-links a's --text-mid). */
[data-membrane],
[data-membrane] *{
  color: rgba(245,238,225,0.95) !important;
  -webkit-text-fill-color: rgba(245,238,225,0.95) !important;
}

/* When `#opv-cta-btn` (the overscroll-preview "go to" CTA injected
   by subpage.js) gets its membrane stamped at runtime, override
   the inline subpage.js bg/border so the dark frosted membrane
   surface paints uncontested. The 999px pill radius + padding
   come from shared.css (already !important). The host needs
   overflow:hidden so the iri layers inside the injected surface
   stay clipped to the pill shape. Specificity #id[attr]=0,1,1,0
   beats subpage.js's inline #id rule (0,1,0,0). */
#opv-cta-btn[data-membrane]{
  background:transparent !important;
  border:none !important;
  overflow:hidden;
}
#opv-cta-btn[data-membrane]:hover,
#opv-cta-btn[data-membrane]:active{
  background:transparent !important;
  border:none !important;
}
[data-membrane] .pill-text,
[data-membrane] > *:not(.pill-surface){
  text-shadow: 0 1px 0 rgba(0,0,0,0.20);
}

/* (Earlier round had a desktop auto-lite rule for [data-membrane]
   surfaces that dropped filter:url. Reverted at user request — the
   privacy banner accept / cookies overlay accept buttons on desktop
   keep the full dark-frosted displacement material exactly like
   their mobile counterparts. The lite/radiant material is reserved
   for the desktop menu hover state only.) */


/* ════════════════════════════════════════════════════════════════
   OVERLAY SCRIM PERF
   ────────────────────────────────────────────────────────────────
   Probe.js measurements showed the overlay backdrops are by far
   the dominant GPU cost on mobile — `.ov-bd` is full-viewport
   (~329k px²) running backdrop-filter:blur(12px). When two
   overlays stack, you're paying for the blur twice. iOS Safari
   buckles at p95 frame time 120-180ms while scrolling behind it.

   Two targeted optimisations, no shared.css edits required:

   (1) Drop or reduce backdrop-filter on overlay scrims.
       Mobile: gone entirely. Solid tint compensates visually.
       Desktop: blur reduced 12px → 8px (~half the GPU cost,
       still reads as frosted).
       Also drop blur on .mjump-overlay (already 96% opaque,
       so the blur was almost invisible anyway).

   (2) Stacked-scrim suppression — when 2+ .overlay panels are
       open at once, only the TOPMOST one renders its backdrop.
       Underlying overlays get .bg-suppressed (set by membrane.js)
       which display:none's their .ov-bd and kills backdrop-filter
       on their .ov-panel. The user can't see them anyway because
       they're behind the topmost panel, so this is invisible.
   ════════════════════════════════════════════════════════════════ */

/* Mobile — drop backdrop-filter on overlay scrims entirely.
   Bump background opacity slightly so the screen still reads as
   "veiled" rather than "tinted glass". */
@media (max-width: 700px){
  .overlay .ov-bd{
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    background: rgba(250,250,249,0.78) !important;
  }
  /* .mjump-overlay's bg is already rgba(250,250,249,0.96) — drop
     the blur, the visual is unchanged. */
  .mjump-overlay{
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
  }
  /* .ov-panel is also full-width on mobile (width:100vw in
     shared.css). Round-1 left it with blur(12px); probe.js
     subsequently identified it as the new dominant cost (~819k
     score, FPS 15-30 while scrolling inside an overlay). Same
     treatment as .ov-bd: drop backdrop-filter entirely, bump
     background opacity from 0.80 → 0.94 to compensate. The eye
     barely notices the missing blur on a near-opaque surface;
     the GPU does. Expected: FPS 15 → ~55 inside overlays. */
  .ov-panel{
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    background: rgba(250,250,249,0.94) !important;
  }
}

/* Desktop — keep the frosted look but at half the cost. 12 → 8 px.
   The eye barely registers the difference at the panel's smaller
   visible-area, but the GPU does. */
@media (min-width: 701px){
  .overlay .ov-bd{
    backdrop-filter: blur(8px) !important;
    -webkit-backdrop-filter: blur(8px) !important;
  }
}

/* Stacked-scrim suppression. The underlying overlay's .ov-bd is
   completely redundant when the topmost overlay's scrim covers
   the same viewport area, so we hide it. The .ov-panel keeps its
   layout (so the close-of-topmost transition reveals it cleanly)
   but loses backdrop-filter cost while it's behind. */
.overlay.bg-suppressed > .ov-bd{
  display: none !important;
}
.overlay.bg-suppressed > .ov-panel{
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}


/* (.left-bar, .bg-preview-mirror, .ov-bottom backdrop-filter: those
   are now removed at the source — see shared.css's "SURFACE" group.) */


/* ════════════════════════════════════════════════════════════════
   MENU ITEMS — left intentionally untouched by this stylesheet.
   ────────────────────────────────────────────────────────────────
   Earlier rounds had .snav-item:hover and .snav-item.active running
   the dark-frosted membrane material with SVG displacement. The
   user reverted both: hover and active both use shared.css's
   original liquid-glass treatment (bright-cream bg + four
   light-catching inset shadows). No membrane on menu rows.

   No CSS rules here, and membrane.js's tagSpecificButtons() does
   NOT inject a .pill-surface into .snav-item rows anymore — there's
   nothing for the membrane material to render against, so saving
   the markup work and the GPU layers.
   ════════════════════════════════════════════════════════════════ */
