/* ┌─────────────────────────────────────────────────────────────┐
   │  CACHE-BUST CONVENTION (read before editing this file)      │
   │  ─────────────────────────────────────────────────────────  │
   │  This file is referenced from every HTML file as            │
   │    <link rel="stylesheet" href="shared.css?v=N">            │
   │  The `?v=N` is a manual cache-busting version. The Apache   │
   │  rules in .htaccess cache CSS/JS for 1 year as immutable —  │
   │  so browsers will NEVER refetch until the URL changes.      │
   │                                                              │
   │  WHEN YOU EDIT THIS FILE, YOU MUST ALSO BUMP THE VERSION    │
   │  IN ALL HTML FILES. Run this in the project root:           │
   │    perl -i -pe 's/\?v=N/?v=N+1/g' *.html                     │
   │  (replace N with the current version — check any HTML).     │
   │  See AGENTS.md at project root for the full convention.     │
   └─────────────────────────────────────────────────────────────┘

   shared.css — chrome shared between home.html and subpages.
   Canonical source: home.html (more recent comments + tighter
   formatting). Drift absorbed during extraction:
     * tulpas's @media(max-width:700px) overlay-mobile rules were
       a subset of home's; home's fuller rule set kept.
     * tulpas's GDPR sub-rules used hardcoded font-size 11/12px,
       home's uses var(--fs-small) — home's version kept.
     * tulpas declares html{overflow-x:hidden;} body{...}; home
       declares html,body{...}. We adopt tulpas's looser version
       so subpage long scrolling works; home re-imposes its own
       hard-overflow inline next to its hero rules.
     * .mstack[data-overlay]/.mstack-close/.bg-preview/.preview-label
       are tulpas-only but harmless when included site-wide (they
       only activate when the matching markup attribute is present
       in chrome.js's "sub" mode).
     * Same applies to html.video-fs-landscape suppressor.
   ============================================================ */

*{box-sizing:border-box;margin:0;padding:0;font-family:'Safiro',sans-serif!important;text-transform:none!important;}
:root {
  --r:95; --g:90; --b:85;
  --acc: rgb(var(--r),var(--g),var(--b));
  --acc-muted: rgba(var(--r),var(--g),var(--b),0.12);
  --acc-border: rgba(var(--r),var(--g),var(--b),0.28);
  --acc-strong: rgba(var(--r),var(--g),var(--b),0.45);
  --bg-solid: #fafaf9;
  --bg-panel: rgba(250,250,249,0.80);
  --bg-panel-strong: rgba(252,252,251,0.90);
  --border: rgba(var(--r),var(--g),var(--b),0.12);
  --border-mid: rgba(var(--r),var(--g),var(--b),0.22);
  --text-dim: rgba(40,38,36,0.40);
  --text-mid: rgba(40,38,36,0.65);
  --text-bright: rgba(20,18,16,0.92);
  --gdpr-h: 0px; --bar-w:380px; --sliver:52px;
  --fs-hero: 50px;
  --fs-display: 48px;
  --fs-h1: 32px;
  --fs-h2: 18px;
  --fs-body: 16px;
  --fs-small: 12px;
}

/* Visually hidden but read by screen readers — used for <label>
   elements on placeholder-only form inputs and for the wordmark
   accessible-name <span>, so we don't change ANY visible
   typography while still meeting accessibility / SEO needs. */
.sr-only{
  position:absolute;
  width:1px;height:1px;
  padding:0;margin:-1px;
  overflow:hidden;
  clip:rect(0,0,0,0);
  white-space:nowrap;
  border:0;
}

:root{
  /* ── LIQUID-GLASS TOKENS ────────────────────────────────────────
     Single source of truth for the "Apple-style liquid glass" look
     used across pill buttons, close discs, sticky chrome surfaces,
     hover affordances, etc. Change a token here and every glass
     element in the site updates. */

  /* Saturated/brightened backdrop-filter shared by all variants. */
  --glass-filter: blur(24px) saturate(200%) brightness(1.05);

  /* LIGHT variant — frosted cream pill on a light surface. */
  --glass-light-bg:        rgba(255,255,255,0.30);
  --glass-light-bg-hover:  rgba(255,255,255,0.42);
  --glass-light-bg-active: rgba(255,255,255,0.55);
  --glass-light-border:    rgba(255,255,255,0.55);
  --glass-light-border-hover: rgba(255,255,255,0.75);
  --glass-light-shadow:
    0 8px 24px rgba(20,18,16,0.08),
    0 2px 4px rgba(20,18,16,0.04),
    inset 0 1.4px 0 rgba(255,255,255,0.85),
    inset 0 3px 4px rgba(255,255,255,0.35),
    inset 0 -1px 0 rgba(20,18,16,0.06),
    inset 0 0 0 0.5px rgba(255,255,255,0.40);
  --glass-light-shadow-active:
    0 2px 6px rgba(20,18,16,0.06),
    inset 0 1px 0 rgba(255,255,255,0.55),
    inset 0 -1px 0 rgba(20,18,16,0.10);

  /* PRIMARY variant — luminous "important" affirmative CTA.
     A bright chrome-cream glass with an OUTER glow so primary
     actions look lit-up and lifted against the page chrome. */
  --glass-primary-bg:        rgba(255,255,255,0.65);
  --glass-primary-bg-hover:  rgba(255,255,255,0.82);
  --glass-primary-bg-active: rgba(255,255,255,0.92);
  --glass-primary-border:    rgba(255,255,255,0.95);
  --glass-primary-shadow:
    0 0 0 4px rgba(255,255,255,0.18),
    0 0 24px rgba(255,255,255,0.30),
    0 10px 28px rgba(20,18,16,0.10),
    0 2px 6px rgba(20,18,16,0.05),
    inset 0 1.6px 0 rgba(255,255,255,0.95),
    inset 0 4px 6px rgba(255,255,255,0.45),
    inset 0 -1px 0 rgba(20,18,16,0.06),
    inset 0 0 0 0.5px rgba(255,255,255,0.55);

  /* ON-DARK variant — light tint pill over photographic backgrounds. */
  --glass-dark-bg:        rgba(240,238,235,0.16);
  --glass-dark-bg-hover:  rgba(240,238,235,0.28);
  --glass-dark-bg-active: rgba(240,238,235,0.42);
  --glass-dark-border:    rgba(240,238,235,0.35);
  --glass-dark-shadow:
    0 6px 18px rgba(0,0,0,0.30),
    0 1px 2px rgba(0,0,0,0.18),
    inset 0 1.2px 0 rgba(255,255,255,0.40),
    inset 0 2px 3px rgba(255,255,255,0.12),
    inset 0 -1px 0 rgba(0,0,0,0.18),
    inset 0 0 0 0.5px rgba(255,255,255,0.18);

  /* SURFACE variant — applied to large chrome panels (sticky header,
     gdpr banner, sidebar). Just the saturate-blur + a top edge
     specular; each panel keeps its own bg/border. */
  --glass-surface-shadow:
    inset 0 1.5px 0 rgba(255,255,255,0.75),
    inset 0 4px 6px rgba(255,255,255,0.18),
    0 1px 2px rgba(20,18,16,0.03);
}
@media(max-width:700px){
  :root{
    --fs-hero: 52px;
    --fs-display: 58px;
    --fs-h1: 38px;
    --fs-h2: 22px;
    --fs-body: 18px;
    --fs-small: 14px;
  }
}

/* All subpages share the same large hero title size for .series-title.
   Originally each subpage defined this inline; hoisted here so any new
   subpage automatically gets it without per-page CSS. Mobile keeps the
   38px from the @media block above. */
body[data-page="sub"]{ --fs-h1: 52px; }
@media(max-width:700px){ body[data-page="sub"]{ --fs-h1: 38px; } }

.sub-link {
  display:inline-block;
  width:auto;
  font-size:var(--fs-h2); letter-spacing:0.14em;
  color:var(--text-bright);
  text-decoration:none; background:none; border:none;
  cursor:pointer; padding:0;
  position:relative; transition:color 0.3s;
  white-space:nowrap;
}
.sub-link::after {
  content:''; position:absolute; bottom:-3px; left:0;
  width:100%; height:2px; background:var(--acc);
  transform-origin:left;
  animation:pulse-ul 3s ease-in-out infinite;
}
/* DRIFT NOTE: tulpas's pulse-ul keyframe used 0.2/0.7 opacity stops;
   home's uses 0.15/0.75. Home is canonical. */
@keyframes pulse-ul{0%{opacity:0.15;transform:scaleX(0.2);}55%{opacity:0.75;transform:scaleX(1);}100%{opacity:0.15;transform:scaleX(0.2);}}
.sub-link:hover{color:var(--text-bright);}
.sub-link:hover::after{animation:none;opacity:0.9;transform:scaleX(1);}

/* ─────────────────────────────────────────────────────────────────
   LIQUID-GLASS BUTTON VOCABULARY (shared)
   ─────────────────────────────────────────────────────────────────
   Apple-style "liquid glass" treatment: low-opacity white tint,
   saturated/brightened backdrop-blur (so colour passes through
   the pill rather than being dulled), strong specular top edge,
   subtle bottom inset and refractive inner ring, plus a soft
   outer shadow to lift the surface. Applied to every round / pill
   chrome element via the .liquid-glass utility class AND to the
   matching button selectors directly (so existing pages don't need
   markup changes to inherit the look). Per-button rules layered
   on top control size, padding, radius, and any colour overrides.
   ───────────────────────────────────────────────────────────────── */
/* ── LIGHT pill variant (default frosted glass on light surfaces) ── */
/* Cluster applies to close buttons AS IT ORIGINALLY DID — the
   filament-close override on the close-button block in the filament
   layer (below) only sets background, leaving box-shadow + backdrop-
   filter to inherit from this cluster so the close button keeps its
   ORIGINAL liquid-glass edge highlights + heavy backdrop-filter.
   Filament-decorated chrome buttons are intentionally still listed
   here; the filament layer's higher-specificity .filament selectors
   below override the relevant properties via cascade order.
   ──────────────────────────────────────────────────────────────  */
.liquid-glass,
.ov-close,
.mstack-close,
.mjump-close,
.mob-menu-btn,
.choice-strip-decline,
.choice-strip-customize,
.cookie-save-btn,
.lb-close{
  background:var(--glass-light-bg) !important;
  border:1px solid var(--glass-light-border) !important;
  backdrop-filter:var(--glass-filter) !important;
  -webkit-backdrop-filter:var(--glass-filter) !important;
  box-shadow:var(--glass-light-shadow) !important;
  color:var(--text-mid);
  -webkit-text-fill-color:var(--text-mid);
  -webkit-tap-highlight-color:transparent;
}
.liquid-glass:hover,
.ov-close:hover,
.mstack-close:hover,
.mjump-close:hover,
.mob-menu-btn:hover,
.choice-strip-decline:hover,
.choice-strip-customize:hover,
.cookie-save-btn:hover,
.lb-close:hover{
  background:var(--glass-light-bg-hover) !important;
  border-color:var(--glass-light-border-hover) !important;
  color:var(--text-bright);
  -webkit-text-fill-color:var(--text-bright);
}
.liquid-glass:active,
.ov-close:active,
.mstack-close:active,
.mjump-close:active,
.mob-menu-btn:active,
.choice-strip-decline:active,
.choice-strip-customize:active,
.cookie-save-btn:active,
.lb-close:active{
  background:var(--glass-light-bg-active) !important;
  box-shadow:var(--glass-light-shadow-active) !important;
}

/* ── PRIMARY (luminous chrome-cream affirmative CTAs) ── */
.choice-strip-accept,
.cookie-accept-all-btn{
  background:var(--glass-primary-bg) !important;
  border:1px solid var(--glass-primary-border) !important;
  backdrop-filter:var(--glass-filter) !important;
  -webkit-backdrop-filter:var(--glass-filter) !important;
  color:var(--text-bright) !important;
  -webkit-text-fill-color:var(--text-bright) !important;
  box-shadow:var(--glass-primary-shadow) !important;
  border-radius:999px !important;
  -webkit-tap-highlight-color:transparent;
}
.choice-strip-accept:hover,
.cookie-accept-all-btn:hover{
  background:var(--glass-primary-bg-hover) !important;
}
.choice-strip-accept:active,
.cookie-accept-all-btn:active{
  background:var(--glass-primary-bg-active) !important;
}

/* ── ON-DARK (light pill on photographic / dark backgrounds) ── */
.mcard-cta-arrow,
.mhint{
  background:var(--glass-dark-bg) !important;
  border:1px solid var(--glass-dark-border) !important;
  backdrop-filter:var(--glass-filter) !important;
  -webkit-backdrop-filter:var(--glass-filter) !important;
  color:#f0eeeb !important;
  -webkit-text-fill-color:#f0eeeb !important;
  box-shadow:var(--glass-dark-shadow) !important;
  -webkit-tap-highlight-color:transparent;
}

/* ── SURFACE (large chrome panels share the saturate-blur + a top
   edge specular; each panel keeps its own bg/border). */
body[data-page="sub"] .doc-header,
.choice-strip,
.ov-bottom,
.ov-panel{
  backdrop-filter:var(--glass-filter) !important;
  -webkit-backdrop-filter:var(--glass-filter) !important;
  box-shadow:var(--glass-surface-shadow) !important;
}

/* ── Desktop menu rows — pronounced light-glass on hover. ──
   The .left-bar already has the saturate-blur via .liquid-surface,
   so we just paint the higher-tint fill + the edge-highlights here. */
@media(min-width:701px){
  .snav-item{ transition:background 0.18s ease, box-shadow 0.18s ease, color 0.2s; }
  /* :not(.fil-glass) excludes filament-treated rows so filament.css
     can apply its dark-glass body without fighting !important. */
  .snav-item:not(.fil-glass):hover,
  .snav-item:not(.fil-glass).active{
    background:var(--glass-light-bg-active) !important;
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.85),
      inset 0 4px 6px rgba(255,255,255,0.22),
      inset 0 -1px 0 rgba(20,18,16,0.05),
      inset 0 0 0 0.5px rgba(255,255,255,0.50) !important;
  }
  .bar-nav-link{ transition:background 0.2s, padding 0.25s, margin 0.25s, box-shadow 0.2s; }
  .bar-nav-link:hover{
    background:rgba(255,255,255,0.45);
    border-radius:6px;
    box-shadow:
      inset 0 1.2px 0 rgba(255,255,255,0.75),
      inset 0 3px 4px rgba(255,255,255,0.18);
    padding-left:8px;padding-right:8px;
    margin-left:-8px;margin-right:-8px;
  }
}

/* ── Per-element overrides on top of the shared variants ──
   These are PURELY about size/shape/typography; the glass recipe
   itself comes from the variant rules above. */
.slide-prev,.slide-next{
  border-radius:999px !important;
  padding:10px 22px !important;
  cursor:pointer !important;
  font-family:'Safiro',sans-serif !important;
  letter-spacing:0.14em !important;
}
/* Overscroll preview round buttons (subpage.js #opv-card area).
   The CTA is a pill, the nav arrows are round discs. The pill +
   disc shapes both inherit the .is-on-dark glass treatment above
   from the selector list, so here we just lock the radius. */
#opv-cta-btn{
  border-radius:999px !important;
  padding:14px 30px !important;
}

/* Workshop / equilibrium page CTAs were previously force-rounded here.
   Reverted — let those pages keep their original button geometry. */
@media(max-width:700px){
  .mjump-cell:hover,
  .mjump-cell:active{
    box-shadow:
      inset 0 0 0 1px rgba(255,255,255,0.35),
      inset 0 1px 0 rgba(255,255,255,0.55);
  }
}

/* ── OVERLAYS ─────────────── */
.overlay{position:fixed;inset:0;z-index:10000;pointer-events:none;display:none;}
.overlay.open{pointer-events:all;display:block;}
/* (Earlier round bumped #ov-subscribe to z-index:10001 so it would
   sit above booking — but that put it in the same z-plane as
   .nav-spinner (also 10001, with position:fixed + transform
   creating a stacking context). On iOS Safari, sibling
   stacking-context elements at the same z-index can interfere
   with hit-testing even when one has pointer-events:none, which
   was the root cause of "subscribe buttons unresponsive" reports.
   Reverted to default 10000. Booking is now snapped shut
   synchronously before subscribe opens (see
   openSubscribeForWorkshops in shared.js), so the two never
   coexist and there's no stacking conflict to escape from. */

/* SLIDE-IN LITE MODE — set by JS for ~450ms while subscribe slides
   in. Drops both backdrop-filter passes (scrim + panel) for that
   window so the iPad GPU isn't simultaneously computing two full-
   viewport blurs while compositing the slide-in transform. After
   the class is removed, the frosted look returns. Background
   opacity bumped slightly so the panel still reads as "veiled"
   during the brief no-blur window — visual difference is barely
   perceptible because backdrop-filter samples are mostly hidden
   under the bg colour at high opacity. User explicitly approved
   this trade-off (slide-in lite, full visuals when settled). */
/* Only the panel's blur (the heavy blur(24px) pass on a 460×1080
   area) is dropped during slide-in. Scrim blur stays — that's
   what gives the page its continuous viewport blur. User reported
   the page visibly "losing its blur then restoring" when subscribe
   opened from booking; that was the scrim's blur being suppressed.
   The panel is mostly opaque anyway (bg-panel high alpha), so
   dropping the panel's backdrop-filter is barely visible. */
#ov-subscribe.opening-lite .ov-panel,
#ov-acquire.opening-lite .ov-panel{
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

/* WILL-CHANGE RELEASE when subscribe is settled. shared.css's base
   rules permanently pin .ov-panel (will-change:transform) and the
   .fil-bloom rotation pseudos (will-change:transform). With ~8
   pinned layers inside subscribe alone plus the panel itself, iOS
   Safari at iPad's 2x DPI keeps those buffers warm in GPU memory
   indefinitely, contributing to the persistent sluggish feel.
   Only the slide-in transition actually needs the pinned layers
   for smooth compositing; once subscribe is sitting still, the
   rotation animations don't need pinning — iOS can recycle layer
   buffers between frames. Release will-change on the settled
   state; the .opening-lite class window (450ms during slide-in)
   keeps the original pinning for smooth slide. */
#ov-subscribe:not(.opening-lite) .ov-panel{
  will-change: auto !important;
}
#ov-subscribe:not(.opening-lite) .fil-bloom,
#ov-subscribe:not(.opening-lite) .fil-bloom::before,
#ov-subscribe:not(.opening-lite) .fil-bloom::after{
  will-change: auto !important;
}
/* Same release for BOOKING. Its panel also carries the base
   .ov-panel{will-change:transform}; without this it was pinned across
   every open/close cycle and the slide degraded on the 2nd open onward
   (iOS kept the layer buffer warm and never recycled it). opening-lite
   (added on booking open in shared.js, 450ms) re-pins it just for the
   slide; settled/closed → released. */
#ov-booking:not(.opening-lite) .ov-panel{
  will-change: auto !important;
}
#ov-booking:not(.opening-lite) .fil-bloom,
#ov-booking:not(.opening-lite) .fil-bloom::before,
#ov-booking:not(.opening-lite) .fil-bloom::after{
  will-change: auto !important;
}
/* Same slide-in handling for ACQUIRE — its panel carries the heavy
   backdrop-filter blur(24px); over an image-heavy work page that made
   the slide drop frames / appear instant. opening-lite (added in
   shared.js for 450ms) drops the blur during the slide; settled state
   releases the pinned layers. */
#ov-acquire:not(.opening-lite) .ov-panel{
  will-change: auto !important;
}
#ov-acquire:not(.opening-lite) .fil-bloom,
#ov-acquire:not(.opening-lite) .fil-bloom::before,
#ov-acquire:not(.opening-lite) .fil-bloom::after{
  will-change: auto !important;
}
.ov-bd{position:absolute;inset:0;background:rgba(250,250,249,0.50);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);opacity:0;transition:opacity 0.35s;z-index:1;}
.overlay.open .ov-bd{opacity:1;}
.ov-panel{
  position:absolute;top:0;right:0;width:460px;height:100dvh;
  background:var(--bg-panel);
  backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);
  border-left:1px solid var(--border-mid);
  overflow-y:auto;
  /* Slide animation uses `transform:translateX` instead of animating
     `right` — `right` is a LAYOUT property, every frame triggered a
     full layout pass on a 1300+ node DOM (probe.js logged ~50% frame
     drops on mobile). `transform` is a COMPOSITE property: the GPU
     slides the layer with zero layout cost per frame. Identical
     visual result, ~3× cheaper to render. */
  transform:translateX(100%);
  transition:transform 0.38s cubic-bezier(0.25,0.46,0.45,0.94);
  display:flex;flex-direction:column;
  box-shadow:-4px 0 24px rgba(0,0,0,0.04);
  z-index:2;
  /* PERF: pre-allocate the GPU compositing layer so the slide-in
     transform doesn't pay first-paint allocation cost mid-animation.
     User reported overlays became laggy to open — backdrop-filter
     blur(24px) recomputes the sample area every frame as the panel
     slides in, and without will-change the browser may allocate the
     compositing layer late in the animation. */
  will-change:transform;
}
.overlay.open .ov-panel{transform:translateX(0);}
.ov-top{
  display:flex;justify-content:space-between;align-items:center;
  padding:20px 28px;border-bottom:1px solid var(--border);flex-shrink:0;
}
.ov-label{font-size:var(--fs-small);letter-spacing:0.22em;color:var(--text-dim);line-height:1;display:flex;align-items:center;}
.ov-close{
  width:36px;height:36px;background:none;
  border:1px solid var(--border-mid);border-radius:50%;cursor:pointer;
  position:relative;transition:all 0.2s;flex-shrink:0;
}
.ov-close:hover{border-color:var(--acc-strong);background:var(--acc-muted);}
.ov-close::before,.ov-close::after{
  content:'';position:absolute;top:50%;left:50%;
  width:14px;height:1px;background:var(--text-mid);transform-origin:center;
}
.ov-close::before{transform:translate(-50%,-50%) rotate(45deg);}
.ov-close::after{transform:translate(-50%,-50%) rotate(-45deg);}
.ov-close:hover::before,.ov-close:hover::after{background:var(--text-bright);}
.ov-body{padding:28px;flex:1;}

.portrait{width:100%;aspect-ratio:4/3;object-fit:cover;margin-bottom:16px;border:1px solid var(--border);}
.portrait-credit{font-size:var(--fs-small);letter-spacing:0.12em;color:var(--text-dim);margin-bottom:22px;}
.about-p{font-size:var(--fs-body);line-height:1.9;color:var(--text-mid);margin-bottom:14px;}
/* About overlay: longer-biography expander (read-more family) */
.about-long{display:none;}
#ov-about.bio-open .about-long{display:block;}
#ov-about.bio-open .about-short-tail{display:none;}
.about-more{
  display:inline-block;margin-top:2px;
  font-size:var(--fs-small);letter-spacing:0.22em;color:var(--text-dim);
  background:none;border:none;cursor:pointer;padding:0;font-family:inherit;transition:color 0.2s;
}
.about-more:hover{color:var(--text-bright);}
/* Quiet legal footnote inside the about overlay — keeps the privacy/cookie
   policy reachable on every viewport (the page footer is hidden on subpages
   and mobile), without crowding the about-links pill. */
.about-legal{margin-top:26px;font-size:var(--fs-small);letter-spacing:0.16em;}
.about-legal a{color:var(--text-dim);text-decoration:none;border-bottom:1px solid transparent;transition:color 0.2s,border-color 0.2s;}
.about-legal a:hover{color:var(--text-bright);border-bottom-color:var(--acc-border);}
/* Work-page series description: teaser + read-more (longer text) */
.series-desc-more{display:none;}
.header-right.desc-open .series-desc-more{display:block;margin-top:14px;}
.series-readmore{
  display:inline-block;margin-top:14px;
  font-size:var(--fs-small);letter-spacing:0.22em;color:var(--text-dim);
  background:none;border:none;cursor:pointer;padding:0;font-family:inherit;transition:color 0.2s;
}
.series-readmore:hover{color:var(--text-bright);}
/* Compact (scrolled) sticky header shrinks the description to 13px, so the
   read-more button at --fs-small (14px ≥701px) looked oversized next to it.
   Scale it down to sit just under the compact text. */
body[data-page="sub"] .page-header-sticky.compact .series-readmore{font-size:11px;margin-top:8px;letter-spacing:0.18em;}
/* About + privacy overlays: pin the action row to the bottom of the panel.
   `.ov-bottom` is a real sibling of `.ov-body` inside `.ov-panel`. The panel
   stops being its own scroll container — `.ov-body` scrolls internally
   instead. The bottom strip is a flex sibling that doesn't shrink, so it
   stays pinned at the panel bottom. Uses !important on the structural
   declarations because the base `.ov-panel{display:flex;...overflow-y:auto}`
   rule has equal specificity weight in the cascade and order-of-loading
   could otherwise win. */
#ov-about .ov-panel,
#ov-privacy .ov-panel{
  display:flex !important;
  flex-direction:column !important;
  overflow:hidden !important;
}
#ov-about .ov-body,
#ov-privacy .ov-body{
  flex:1 1 0 !important;
  min-height:0 !important;
  overflow-y:auto !important;
  -webkit-overflow-scrolling:touch;
}
#ov-about .ov-bottom,
#ov-privacy .ov-bottom{
  flex:0 0 auto !important;
}
.ov-bottom{
  background:var(--bg-panel-strong);
  backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
  border-top:1px solid var(--border);
  z-index:2;
}
/* Desktop tweaks for the about overlay: smaller body text. The button row
   keeps the shared `.about-links` font-size (var(--fs-body) = 16px) — we
   no longer down-size the buttons themselves. */
@media(min-width:701px){
  #ov-about .about-p{font-size:14px;line-height:1.85;}
  /* About-links on desktop — mirror the mobile pill layout so
     the filament-pill chassis (brackets + dark fill + bloom)
     wraps a tight row of links rather than the loose
     text-row-with-vertical-separators that the base
     .about-links rule produces. Same internal structure as the
     mobile #ov-about .about-links.ov-bottom at line 621:
     content-based link widths, hairline dividers between cells,
     compact 48px height, white text. The chassis brackets
     come from the global .about-links.filament-pill rule. */
  #ov-about .about-links.ov-bottom{
    margin:0 0 22px 0 !important;   /* breathing room from the bottom of the panel */
    padding:0 !important;
    display:flex;align-items:stretch;
    flex-wrap:nowrap;gap:0;
    border-top:none;
    height:44px;
    overflow:visible;
    /* width:auto + content-based children means the wrapper
       sizes to its links rather than spanning the ov-panel. */
    width:auto;
    align-self:center;
    /* Lighter dark fill on desktop than the mobile 0.15 — on
       desktop the pill sits on top of .ov-bottom's panel-strong
       backdrop (line 463), so two dark layers stacked read as
       too dark. Lighter alpha keeps the readable contrast
       without going slab-heavy. */
    background:rgba(20,18,16,0.06) !important;
  }
  #ov-about .about-links a{
    /* flex:0 0 auto FORCES content-based widths so each cell
       sizes to its text, not equal slices of the wrapper.
       Without !important the chassis filament-pill cascade
       (or the base .about-links flex defaults) was making
       cells equal-width regardless. */
    flex:0 0 auto !important;
    display:flex;align-items:center;justify-content:center;
    font-size:12px;letter-spacing:0.16em;text-transform:uppercase;
    color:var(--fil-fam-text);
    -webkit-text-fill-color:var(--fil-fam-text);
    text-decoration:none;
    /* Per-link horizontal padding controls breathing room
       between adjacent links AND the overall bar length
       (because the wrapper is width:auto sizing to content).
       Bumped 14px → 22px so the bar reads a bit longer and
       the labels have more whitespace between them. */
    padding:0 22px;
    white-space:nowrap;position:relative;
    border:none;border-right:none;border-bottom:none;
    transition:color 0.2s, background 0.2s;
  }
  /* Hairline divider on each link except the first (same pattern
     as the mobile a + a::before pseudo at line 667). */
  #ov-about .about-links a + a::before{
    content:'';position:absolute;left:0;top:28%;bottom:28%;
    width:1px;background:rgba(255,255,255,0.20);
  }
  #ov-about .about-links a:hover{
    color:#fff !important;
    -webkit-text-fill-color:#fff !important;
    border-bottom-color:transparent;
  }
  /* Bracket frame nudge (desktop): the glass bloom blurs ~14px past the host
     edge, so brackets anchored at the host edge (left:0/right:0) read as
     inset on this wide bar. Push them out so they sit at the glass edge and
     frame the bar. Desktop-only — mobile keeps the edge-anchored brackets. */
  #ov-about .about-links.filament-pill::before{left:-6px !important;}
  #ov-about .about-links.filament-pill::after{right:-6px !important;}
}
/* About-overlay link row: portfolio | cv | contact | privacy on one line.
   Same font size, even spacing, vertical separators between links.
   Mirrors the original genesis.html layout — canonical for all pages. */
.about-links{
  display:flex;gap:0;flex-wrap:wrap;justify-content:center;
  border-top:1px solid var(--border);padding-top:20px;margin-top:24px;
}
.about-links a{
  font-size:var(--fs-body);letter-spacing:0.1em;color:var(--text-mid);
  text-decoration:none;padding:6px 14px;
  border-right:1px solid var(--border-mid);
  border-bottom:1px solid transparent;transition:all 0.2s;
}
.about-links a:last-child{border-right:none;}
.about-links a:hover{color:var(--text-bright);border-bottom-color:var(--acc-border);}

.sub-big{font-size:var(--fs-display);font-weight:600;color:var(--text-bright);letter-spacing:-0.02em;line-height:0.92;margin-bottom:22px;}
.sub-intro{font-size:var(--fs-body);line-height:1.85;color:var(--text-bright);margin-bottom:26px;letter-spacing:0.04em;}
.priv-section{margin-bottom:22px;}
.priv-section h3{font-size:var(--fs-body);font-weight:600;letter-spacing:0.12em;color:var(--text-bright);margin-bottom:10px;}
.priv-section p,.priv-section li{font-size:var(--fs-body);line-height:1.85;color:var(--text-bright);margin-bottom:6px;opacity:0.85;}
/* Privacy overlay: same font scale as cookie overlay (14px desktop / 15px
   mobile) so the two policies feel coherent. The default .priv-section sizes
   above are used by other consumers (e.g. inline policy excerpts) so we
   override only inside #ov-privacy. */
#ov-privacy .sub-intro{font-size:14px;line-height:1.8;letter-spacing:0.04em;}
#ov-privacy .priv-section h3{font-size:13px;letter-spacing:0.12em;}
#ov-privacy .priv-section p,
#ov-privacy .priv-section li{font-size:14px;line-height:1.8;}
#ov-privacy .priv-eff{font-size:12px;}
/* Cookie-settings CTA pinned to bottom of the privacy overlay. Lives as a
   sibling of .ov-body inside .ov-panel — same .ov-bottom flex-shrink:0 base
   styling as the about-overlay action row. */
.priv-cookies-cta{
  padding:14px 28px;
  display:flex;justify-content:center;
}
.priv-cookies-cta button{
  font-size:13px;letter-spacing:0.1em;color:var(--text-mid);
  text-decoration:none;padding:10px 22px;
  background:transparent;border:1px solid var(--border-mid);
  border-radius:999px;cursor:pointer;transition:all 0.2s;
}
.priv-cookies-cta button:hover{color:var(--text-bright);border-color:var(--acc-strong);background:var(--acc-muted);}
@media(max-width:700px){
  .priv-cookies-cta{
    padding:18px 28px calc(18px + env(safe-area-inset-bottom,0px));
  }
  .priv-cookies-cta button{font-size:14px;padding:10px 22px;}
}
/* DRIFT NOTE: tulpas had only ".priv-section ul{padding-left:16px;}";
   home has ul,ol{padding-left:18px;}. Home is canonical. */
.priv-section ul,.priv-section ol{padding-left:18px;}
.priv-section a{color:var(--text-bright);border-bottom:1px solid var(--border-mid);opacity:0.85;}
.priv-eff{font-size:var(--fs-small);letter-spacing:0.12em;color:var(--text-dim);margin-bottom:24px;}

.cookie-toggle{margin:16px 0;display:flex;align-items:center;}
.cookie-toggle input[type="checkbox"]{display:none;}
.cookie-toggle input[type="checkbox"]:disabled + label{opacity:0.5;cursor:not-allowed;}
.cookie-toggle label{cursor:pointer;display:flex;align-items:center;gap:12px;font-size:var(--fs-body);color:var(--text-mid);transition:color 0.2s;-webkit-tap-highlight-color:transparent;}
.cookie-toggle label:hover{color:var(--text-bright);}
/* Cookie toggle — strong on/off contrast.
   OFF: dim recessed track, dark/dim thumb (looks "off / unlit").
   ON:  luminous bright track + bright cream thumb pulled to the
        right edge ("lit / engaged").
   The visual difference between states is intentionally large so
   the user can read on/off at a glance. */
.toggle-switch{
  width:44px;height:24px;
  background:rgba(20,18,16,0.18);
  border:1px solid rgba(20,18,16,0.20);
  border-radius:12px;
  position:relative;
  box-shadow:
    inset 0 1px 2px rgba(20,18,16,0.10),
    inset 0 -1px 0 rgba(255,255,255,0.20);
  transition:background 0.3s, border-color 0.3s, box-shadow 0.3s;
}
.toggle-switch::after{
  content:'';position:absolute;top:2px;left:2px;
  width:18px;height:18px;
  background:rgba(40,38,36,0.55);
  border-radius:50%;
  transition:transform 0.3s, background 0.3s, box-shadow 0.3s;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.20),
    0 1px 2px rgba(20,18,16,0.18);
}
.cookie-toggle input[type="checkbox"]:checked + label .toggle-switch{
  background:var(--glass-primary-bg);
  border-color:var(--glass-primary-border);
  box-shadow:
    0 0 0 3px rgba(255,255,255,0.22),
    0 0 18px rgba(255,255,255,0.30),
    inset 0 1.4px 0 rgba(255,255,255,0.95),
    inset 0 -1px 0 rgba(20,18,16,0.06);
}
.cookie-toggle input[type="checkbox"]:checked + label .toggle-switch::after{
  transform:translateX(20px);
  background:var(--bg-solid);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.95),
    0 1px 3px rgba(20,18,16,0.18);
}
/* margin-top:auto inside #ov-cookies's flex column .ov-body pushes
   this section to the bottom of the overlay regardless of how
   much content sits above it — so save-preferences and accept-all
   are always anchored at the visible bottom of the panel rather
   than floating in the middle when the content is short. */
.cookie-save-section{display:flex;gap:12px;margin-top:auto;padding-top:24px;border-top:1px solid var(--border);flex-shrink:0;}
.cookie-save-btn,.cookie-accept-all-btn{flex:1;padding:14px 20px;font-size:var(--fs-body);letter-spacing:0.08em;cursor:pointer;transition:all 0.3s;border:none;border-radius:999px;}
.cookie-save-btn{background:transparent;border:1px solid var(--border-mid);color:var(--text-bright);}
.cookie-save-btn:hover{background:var(--bg-panel);border-color:var(--text-mid);}
.cookie-accept-all-btn{background:var(--text-bright);border:1px solid var(--text-bright);color:var(--bg-solid);}
.cookie-accept-all-btn:hover{background:var(--text-mid);border-color:var(--text-mid);}
#ov-cookies .sub-intro{font-size:14px;line-height:1.8;letter-spacing:0.04em;}
#ov-cookies .priv-section h3{font-size:13px;letter-spacing:0.12em;}
#ov-cookies .priv-section p{font-size:14px;line-height:1.8;}
#ov-cookies .cookie-toggle label{font-size:14px;}
#ov-cookies .cookie-save-btn,#ov-cookies .cookie-accept-all-btn{font-size:14px;}

@media(max-width:700px){
  /* Width override for mobile only — the `right:0` + `transform:translateX(100%)`
     base rule already slides the panel off by exactly its own width
     (= 100vw on mobile), so no `right:-100vw` override needed. */
  .ov-panel{width:100vw;}
  .ov-top{padding:24px 28px;}
  .ov-label{font-size:18px;}
  .ov-close{width:44px;height:44px;}
  .ov-close::before,.ov-close::after{width:16px;}
  /* About-links mobile: liquid-glass pill matching the .mdock vocabulary,
     but FLOATING ABSOLUTELY over the scrolling about text — no reserved
     row, no fixed background strip. The text scrolls behind the pill so
     the user catches the bottom paragraphs through the frosted glass.
     Position:absolute pulls the pill out of the .ov-panel flex flow;
     the .ov-body's padding-bottom keeps the last paragraph from being
     permanently hidden under it. */
  /* Selector uses `.about-links.ov-bottom` (matching BOTH classes
     the element has) AND keeps !important on height/margin/padding
     to beat `#ov-about .ov-bottom { height:0 !important }` further
     down in the same @media block. Both rules carry !important, so
     specificity decides — mine is 0,1,2,0 (extra .ov-bottom class)
     vs theirs 0,1,1,0, mine wins. Without !important, my rule loses
     to theirs regardless of specificity (CSS cascade: !important
     beats specificity), and the pill collapses to 0 height. */
  #ov-about .about-links.ov-bottom{
    position:absolute;
    left:16px;right:16px;
    bottom:calc(14px + env(safe-area-inset-bottom,0px));
    z-index:5;
    margin:0 !important;
    padding:0 !important;
    display:flex;align-items:stretch;
    flex-wrap:nowrap;gap:0;
    border-top:none;
    height:48px !important;
    border-radius:999px;
  }
  #ov-about .about-links a{
    /* flex:0 1 auto so each link sizes to its content rather
       than dividing the pill into 4 equal cells. User reported
       it looked uneven that 'portfolio' (long) and 'cv' (short)
       took the same space — content-based widths give a natural
       balanced row. */
    flex:0 1 auto;
    display:flex;align-items:center;justify-content:center;
    /* Bumped 11→12px so the labels read at the same scale as
       the dock subscribe / footer-link text. (User initially
       saw 13px and felt it was too big — settled on 12px to
       match the dock vocabulary.) */
    font-size:12px;letter-spacing:0.16em;text-transform:uppercase;
    /* White on the dark filament-pill backdrop — was
       var(--text-mid) (a dark-on-cream colour) which on the
       dark filament family fill reads muddy. */
    color:#fff;-webkit-text-fill-color:#fff;text-decoration:none;
    /* Each link gets its own horizontal padding so there's clear
       spacing between them — the divider pseudo at line 647
       provides the visual break between cells. */
    padding:0 10px;
    -webkit-tap-highlight-color:transparent;
    white-space:nowrap;position:relative;
    border:none;border-bottom:none;
    transition:color 0.2s, background 0.2s;
  }
  /* Hairline dividers between cells (drawn on each link except the first).
     Light translucent white instead of var(--border-mid) (a
     dark-on-cream accent that vanishes on the dark filament fill). */
  #ov-about .about-links a + a::before{
    content:'';position:absolute;left:0;top:28%;bottom:28%;
    width:1px;background:rgba(255,255,255,0.30);
  }
  #ov-about .about-links a:active{
    color:var(--text-bright);
    background:rgba(255,255,255,0.18);
  }
  /* Collapse the .ov-bottom flex slot so the pill floats absolutely over
     .ov-body instead of taking layout space. */
  #ov-about .ov-bottom{
    flex:0 0 0 !important;
    height:0 !important;
    padding:0 !important;margin:0 !important;
    background:transparent;
    backdrop-filter:none;-webkit-backdrop-filter:none;
    border-top:none;
    overflow:visible;
  }
  /* And give the body content enough trailing padding so the last
     paragraph isn't permanently hidden under the floating pill. The
     text DOES pass behind the pill while scrolling — that's the point —
     but at scroll-end the user can read the last line clearly. */
  #ov-about .ov-body{
    padding-bottom:calc(82px + env(safe-area-inset-bottom,0px)) !important;
  }

  /* Mobile privacy overlay: the cookie-settings CTA floats freely at the
     bottom over the scrolling policy text — same pattern as about-links.
     No backdrop, no divider, no panel. The button is just a translucent
     pill that the policy text scrolls behind. Container collapses to 0,
     button is positioned absolutely. */
  #ov-privacy .priv-cookies-cta.ov-bottom{
    position:absolute;
    left:0;right:0;
    bottom:calc(14px + env(safe-area-inset-bottom,0px));
    z-index:5;
    flex:0 0 0 !important;
    height:auto !important;
    padding:0 !important;margin:0 !important;
    background:transparent !important;
    backdrop-filter:none !important;-webkit-backdrop-filter:none !important;
    border-top:none !important;
    box-shadow:none !important;
    display:flex;justify-content:center;
    pointer-events:none; /* let the policy text underneath scroll-tap through */
    overflow:visible;
  }
  /* Host gets the SAME liquid-glass-primary base as the other
     membrane'd buttons (.choice-strip-accept, .cookie-accept-all-btn):
     bright cream-white pill with backdrop-filter + drop shadow.
     The membrane material's translucent dark surface + iri shimmer
     + top-edge highlight then sits on top of that bright base —
     the same layered visual the user already approved on the GDPR
     and cookies accept buttons. Without this base, the membrane's
     42%-opaque surface against the cream panel was reading as a
     faint smudge ("fully transparent feel"). */
  #ov-privacy .priv-cookies-cta.ov-bottom button{
    pointer-events:auto;
    font-size:13px;letter-spacing:0.12em;
    padding:10px 22px;
  }
  /* And padding-bottom on the privacy body so the last paragraph isn't
     permanently hidden under the floating button. */
  #ov-privacy .ov-body{
    padding-bottom:calc(82px + env(safe-area-inset-bottom,0px)) !important;
  }
  .about-links:not(.about-links-secondary):not(#ov-about .about-links) a{font-size:18px;letter-spacing:0.08em;padding:6px 14px;}
  .about-links-secondary a{font-size:12px;letter-spacing:0.06em;padding:4px 10px;}
  .about-p{font-size:var(--fs-body);line-height:1.85;}
  .portrait-credit{font-size:var(--fs-small);}
  .sub-big{font-size:var(--fs-display);line-height:0.95;}
  .sub-intro{font-size:var(--fs-body);line-height:1.75;}
  .priv-section h3{font-size:var(--fs-h2);}
  .priv-section p,.priv-section li{font-size:var(--fs-body);}
  .priv-eff{font-size:var(--fs-small);}
  #ov-cookies .sub-intro{font-size:15px!important;}
  #ov-cookies .priv-section h3{font-size:14px!important;}
  #ov-cookies .priv-section p{font-size:15px!important;}
  #ov-cookies .cookie-toggle label{font-size:15px!important;}
  #ov-cookies .cookie-save-btn,#ov-cookies .cookie-accept-all-btn{font-size:15px!important;}
  /* Privacy overlay mobile: same scale as cookie overlay (15px / 14px h3) */
  #ov-privacy .sub-intro{font-size:15px!important;}
  #ov-privacy .priv-section h3{font-size:14px!important;}
  #ov-privacy .priv-section p,
  #ov-privacy .priv-section li{font-size:15px!important;line-height:1.8!important;}
  #ov-privacy .priv-eff{font-size:13px!important;}
  .cookie-save-section{flex-direction:column;}
  .toggle-switch{width:52px;height:28px;}
  .toggle-switch::after{width:22px;height:22px;}
  .cookie-toggle input[type="checkbox"]:checked + label .toggle-switch::after{transform:translateX(24px);}
}

/* ── GDPR BANNER ────────────────────────────────────────────── */
.choice-strip {
  position:fixed; bottom:0; left:0; right:0;
  /* z-index 10000 (was 300) — needed to render ABOVE the
     `.page-transition` cover (z-index:9999) that's "active" during
     every fresh page load. With the old 300, the banner was being
     painted behind the page-transition's pixel-grid and looked
     invisible on desktop in private/fresh-load mode (where no
     consent is stored, so the banner actually tries to show). The
     pixel-grid then fades to opacity:0 so it doesn't visually
     conflict with banner content; it just needs to not be on top
     in the z-stack. Equal to .overlay (10000) — overlays come
     later in DOM order, so they still correctly cover the banner
     when an overlay is opened. */
  z-index:10000;
  background:var(--bg-panel-strong);
  backdrop-filter:blur(28px);-webkit-backdrop-filter:blur(28px);
  border-top:1px solid var(--border-mid);
  padding:16px 28px;
  /* `display:flex !important` defensive against the [hidden]
     attribute (UA stylesheet rule) and any other source of
     display:none. The banner was rendering at 0×0 in Safari
     private mode despite no traceable CSS rule setting
     display:none — the !important kills any such hiding. */
  display:flex !important;
  align-items:center; justify-content:space-between; gap:20px;
  transform:translateY(100%);
  transition:transform 0.45s cubic-bezier(0.25,0.46,0.45,0.94);
  box-shadow:0 -4px 24px rgba(0,0,0,0.03);
}
.choice-strip.show { transform:translateY(0); }
.choice-strip-text {
  font-size:var(--fs-small); letter-spacing:0.06em;
  color:var(--text-mid); line-height:1.7; flex:1;
}
.choice-strip-text a {
  color:var(--text-mid); border-bottom:1px solid var(--border-mid);
  text-decoration:none; transition:color 0.2s; cursor:pointer;
}
.choice-strip-text a:hover { color:var(--text-bright); }
.choice-strip-actions { display:flex; gap:10px; flex-shrink:0; }
/* Pill-shaped CTAs to align with the rest of the chrome (the round
   ov-close button, the rounded-pill view-toggle on the work pages, etc.)
   `border-radius:999px` gives a fully-rounded pill at any height. */
.choice-strip-decline {
  padding:9px 18px; font-size:var(--fs-small); letter-spacing:0.18em;
  background:none; border:1px solid var(--border-mid);
  color:var(--text-dim); cursor:pointer; transition:all 0.3s;
  border-radius:999px;
}
.choice-strip-decline:hover { border-color:var(--border-mid); color:var(--text-mid); }
.choice-strip-customize {
  padding:9px 20px; font-size:var(--fs-small); letter-spacing:0.18em;
  background:var(--bg-panel); border:1px solid var(--border-mid);
  color:var(--text-bright); cursor:pointer; transition:all 0.3s;
  border-radius:999px;
}
.choice-strip-customize:hover { background:var(--bg-solid); border-color:var(--text-mid); }
.choice-strip-accept {
  padding:9px 22px; font-size:var(--fs-small); letter-spacing:0.18em;
  background:var(--text-bright); border:1px solid var(--text-bright);
  color:var(--bg-solid); cursor:pointer; transition:all 0.3s;
  border-radius:999px;
}
.choice-strip-accept:hover { background:var(--text-mid); border-color:var(--text-mid); }
@media(max-width:700px){
  .choice-strip { flex-direction:column; align-items:flex-start; padding:14px 18px; }
  .choice-strip-actions { width:100%; flex-wrap:wrap; }
  .choice-strip-accept { flex:1 1 100%; order:1; text-align:center; }
  .choice-strip-customize { flex:1; order:2; text-align:center; }
  .choice-strip-decline { flex:1; order:3; text-align:center; }
}

/* ── HTML/BODY BASELINE ─────────────────────────────────────── */
/* CRITICAL: html background MUST be set so the browser shows the cream
   colour during the unpainted moment between page navigations (Safari
   shows its default white otherwise — that's the "white flash" when
   navigating from one page to another). The html bg is the very first
   thing painted by the browser, before any other CSS rule applies. */
html{overflow-x:hidden;background:var(--bg-solid);}
/* position:relative on body anchors the .page-footer (position:absolute
   bottom:0) to body's intrinsic bottom — short pages get footer at
   bottom of viewport (via min-height:100dvh), long pages get footer
   below all content (visible after scrolling). */
body{background:var(--bg-solid);color:var(--text-bright);min-height:100dvh;position:relative;}
/* v=94 — DISABLE SCROLL-ANCHORING.
   Chromium's overflow-anchor (default: auto) automatically adjusts
   scrollY when above-viewport layout changes, to keep the visually
   anchored element stable. That's normally helpful, but it fights
   the sticky-header morph: as .compact toggles, the sticky's height
   shrinks ~70px, which scroll-anchoring sees as an above-viewport
   layout change and "compensates" by dropping scrollY by ~70.
   That puts scrollY back below our compact threshold, triggering
   another toggle, which causes more layout shift, which triggers
   another scroll-anchor adjustment — a self-sustained oscillation
   that the user reported as rapid flicker during slow scroll across
   the morph threshold. v=93's hysteresis (40-80) wasn't wide enough
   to escape the ~70px scroll-anchor adjustment. Disabling
   scroll-anchoring breaks the feedback loop: scrollY now stays
   where the user put it during a layout shift, the threshold is
   crossed once cleanly, no oscillation. Side effect: when sticky
   morphs the gallery content visually shifts by ~70px under the
   pill — but the pill itself is at top:0 anyway, so the user
   doesn't see "their content jump" in the way scroll-anchoring
   was protecting against; they just see the gallery scroll-shift
   slightly as the sticky changes size, which reads as expected
   sticky behaviour. */
html, body{overflow-anchor:none;}

/* ── INSTANT CONTENT-AREA COVER (SUBPAGES ONLY) ─────────────────
   body::before is opaque on first paint; html.page-ready removes it.
   Scoped to body[data-page="sub"] because home doesn't add the
   page-ready class (its inline pageTransition skips it) — applying
   the cover to home would leave it permanently covering the hero
   content. Subpages inherit page-ready via shared.js's pageTransition. */
body[data-page="sub"]::before{
  content:"";position:fixed;top:0;right:0;bottom:0;left:0;z-index:9998;
  background:var(--bg-solid);
  pointer-events:none;
}
@media(min-width:701px){
  body[data-page="sub"]::before{left:var(--sliver);}
  html.bar-preopen body[data-page="sub"]::before{left:var(--bar-w);}
  .left-bar:hover ~ body[data-page="sub"]::before,
  .left-bar.open ~ body[data-page="sub"]::before{left:var(--bar-w);}
}
@media(max-width:700px){
  body[data-page="sub"]::before{left:0;}
}
html.page-ready body[data-page="sub"]::before{display:none;}

/* ── PAGE TRANSITION (pixel-dissolve overlay) ─────────────────
   Built dynamically by shared.js's pageTransition. CSS hoisted from
   per-page inline so it's available immediately at first paint. */
.page-transition{
  position:fixed;inset:0;z-index:9999;pointer-events:none;
  display:grid;grid-template-columns:repeat(30,1fr);grid-template-rows:repeat(20,1fr);
  /* Default clip excludes the visible sliver. When bar opens, expands
     to clip --bar-w. .full overrides to no clip (fullscreen cover). */
  clip-path:polygon(var(--sliver) 0, 100% 0, 100% 100%, var(--sliver) 100%);
}
.left-bar.open ~ .page-transition,
.left-bar:hover ~ .page-transition{clip-path:polygon(var(--bar-w) 0, 100% 0, 100% 100%, var(--bar-w) 100%);}
@media(min-width:701px){html.bar-preopen .page-transition{clip-path:polygon(var(--bar-w) 0, 100% 0, 100% 100%, var(--bar-w) 100%);}}
/* Bar collapsing during page-leave (previewNavigate / pageTransition):
   the .open class is still on the bar while it slides back to the
   sliver, so the .open ~ rule above keeps the clip-path at bar-w —
   meaning the area between sliver and bar-w (now exposed page
   content as the bar slides off) doesn't get pixelated to white.
   Use the SLIVER clip-path here (not full coverage) so the bar's
   final 52px sliver area always stays visible — that's the
   continuity element the user expects to persist across the
   page-to-page transition. Must come after the .open rule so
   equal-specificity source-order wins, and also overrides the
   .full helper (which previewNavigate adds simultaneously) via
   the explicit second selector. */
.left-bar.collapsing ~ .page-transition,
.left-bar.collapsing ~ .page-transition.full{
  clip-path:polygon(var(--sliver) 0, 100% 0, 100% 100%, var(--sliver) 100%) !important;
}
.page-transition.full{clip-path:none;}
.page-transition.active{pointer-events:all;}
/* Direction-specific transition durations:
     · Cover-IN (when .out is added): 0.4s ease — leisurely dissolve
       OVER the page on navigation, matching the spinner fade-in.
     · Reveal-OUT (when .out is removed): 0.35s ease — reveal of
       the new page on load. Was 0.25s but that was too quick to
       actually read; bumped up so the user can see the pixels
       dissolving away. Still faster than the 0.4s cover direction
       so the user gets to content slightly quicker than they wait
       on the way out. */
.pixel-block{
  background:var(--bg-solid);opacity:0;
  transition:opacity 0.35s ease;
}
.page-transition.out .pixel-block{
  opacity:1;
  transition:opacity 0.4s ease;
}
/* Instant on-load cover (no fade-IN) — see shared.js init()/playIn().
   Without this the cover ramps in over ~400ms while the cream body::before
   cover is already gone, exposing the page mid-load (the "page paints then
   pixelates" artifact). Removed right before the dissolve so the reveal
   still animates. */
.page-transition.tr-instant .pixel-block{ transition:none !important; }

/* ── LIST TRANSITION (section-switch pixel dissolve in left bar) ─ */
.list-transition{
  position:absolute;inset:0;z-index:10;pointer-events:none;
  display:grid;grid-template-columns:repeat(10,1fr);grid-template-rows:repeat(12,1fr);
}
.list-transition.active{pointer-events:all;}
.list-pixel{
  background:var(--bg-solid);
  opacity:0;
  transition:opacity 0.25s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.list-transition.active .list-pixel{opacity:1;}
@media(max-width:700px){
  .page-transition{
    grid-template-columns:repeat(18,1fr);
    grid-template-rows:repeat(24,1fr);
    clip-path:none;
  }
  .list-transition{grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(15,1fr);}
}

/* ── BACKGROUND STAGE — chrome.js renders previewWrap_machines and
   previewWrap_rebel with class="bg-stage". They need fixed-position
   sizing here so the absolutely-positioned <video> children have
   a proper container. Hoisted from per-page (previously home-only)
   so the menu hover-preview videos render correctly on every page. */
.bg-stage{
  position:fixed;top:0;left:var(--sliver);right:0;bottom:0;
  z-index:0;overflow:hidden;
  transition:left 0.42s cubic-bezier(0.25,0.46,0.45,0.94);
}
.bg-mirror{
  position:fixed;top:0;left:0;width:var(--sliver);bottom:0;
  z-index:0;overflow:hidden;
  transition:width 0.42s cubic-bezier(0.25,0.46,0.45,0.94);
}
@media(min-width:701px){
  /* !important needed to beat the inline default `left:var(--sliver)` set
     by chrome.js on previewWrap_machines/rebel — inline styles otherwise
     win this cascade. */
  html:has(.left-bar.open) .bg-stage,
  html:has(.left-bar:hover) .bg-stage,
  html.bar-preopen .bg-stage{left:var(--bar-w)!important;}
  html:has(.left-bar.open) .bg-mirror,
  html:has(.left-bar:hover) .bg-mirror,
  html.bar-preopen .bg-mirror{width:var(--bar-w);}
}
.bg-mirror-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity 0.8s ease;pointer-events:none;}
.bg-mirror-img.on{opacity:1;}
@media(max-width:700px){
  .bg-stage{display:none;}
  .bg-mirror{display:none;}
}

/* ── BACKGROUND PREVIEW STAGE (subpage hover) ─────────────── */
.bg-preview{
  position:fixed;
  top:0;left:var(--bar-w);right:0;
  height:100dvh;
  z-index:7;
  pointer-events:none;
  overflow:hidden;
}
.bg-preview-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity 0.8s ease;}
.bg-preview-img.on{opacity:1;}
.bg-preview-mirror{
  position:fixed;
  top:0;left:0;
  width:var(--bar-w);
  height:100dvh;
  z-index:4;
  pointer-events:none;
  overflow:hidden;
}
.bg-preview-mirror-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity 0.8s ease;}
.bg-preview-mirror-img.on{opacity:1;}
.preview-label{
  position:fixed;bottom:60px;left:calc(var(--sliver) + 60px);right:60px;
  display:flex;flex-direction:column;gap:6px;
  pointer-events:none;opacity:0;
  transition:opacity 0.4s ease, left 0.42s cubic-bezier(0.25,0.46,0.45,0.94);
  color:#f0eeeb;z-index:50;
}
.preview-label.on{opacity:1;}
body:has(.left-bar:hover) #previewLabel,
body:has(.left-bar.open) #previewLabel { left:calc(var(--bar-w) + 60px); }
@media(max-width:700px){.preview-label{display:none;}}
.preview-series{font-size:32px;font-weight:600;letter-spacing:-0.02em;line-height:1.15;white-space:normal;word-break:break-word;max-width:480px;}
.preview-desc{font-size:12px;letter-spacing:0.04em;line-height:1.6;max-width:600px;opacity:0.85;}
.preview-dots{display:flex;gap:8px;margin-top:10px;}
.preview-dot{width:6px;height:6px;border-radius:50%;background:currentColor;opacity:0.3;transition:all 0.3s;}
.preview-dot.on{opacity:1;width:24px;border-radius:3px;}
@media(max-width:700px){
  .bg-preview{display:none;}
  .bg-preview-mirror{display:none;}
  /* The desktop sidebar must not render at all on mobile — its default
     transform leaves a sliver poking out at the left edge. Hoisted from
     per-page inline CSS so every subpage inherits it automatically. */
  .left-bar{display:none!important;}
  .bar-sliver{display:none;}
  #barTopDesktop{display:none;}
  .bar-top{padding-top:26px;}
}

/* ── LEFT-BAR ──────────────────────── */
/* Backdrop-filter restored: the menu IS supposed to be a frosted-
   glass surface, not just an opaque cream panel. The earlier "halo"
   complaint turned out to be from .bg-preview-mirror (a separate
   bar-width fixed strip that was also in the surface-group rule);
   that's been pulled out separately. .left-bar's own blur is part
   of the design and stays. */
.left-bar{
  position:fixed;top:0;left:0;z-index:100;
  width:var(--bar-w);height:100dvh;
  background:var(--bg-panel);
  backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);
  border-right:1px solid var(--border-mid);
  transform:translateX(calc(var(--sliver) - var(--bar-w)));
  transition:transform 0.42s cubic-bezier(0.25,0.46,0.45,0.94);
  display:flex;overflow:hidden;
  box-shadow:4px 0 24px rgba(0,0,0,0.03);
}
/* .open = pinned/fixed open (also reflows page content, see .main rules).
   .peek = transient touch peek — slides the bar out as an OVERLAY only;
   it deliberately does NOT carry the layout-shift rules (.main padding,
   footer, background offset) that .open does, so the page underneath
   stays put. Both, plus desktop :hover, slide the bar fully out. */
.left-bar:hover,.left-bar.open,.left-bar.peek{transform:translateX(0);}
@media(min-width:701px){html.bar-preopen .left-bar{transform:translateX(0);}}
@media(min-width:701px){
  html.bar-preopen .left-bar .sliver-menu-text{opacity:0;}
  html.bar-preopen .left-bar .sliver-collapse-wrap{opacity:1;}
  html.bar-preopen .left-bar .sliver-collapse-text{color:var(--acc);}
  html.bar-preopen .left-bar .sliver-dot{background:var(--acc);}
}
@media(min-width:701px){html.no-bar-anim .left-bar{transition:none!important;}}
.left-bar.collapsing{transform:translateX(calc(var(--sliver) - var(--bar-w)))!important;}

.bar-sliver{
  width:var(--sliver);flex-shrink:0;
  display:flex;flex-direction:column;align-items:center;
  border-right:1px solid var(--border-mid);cursor:pointer;padding:20px 0;
}
.sliver-menu-word{flex:1;display:flex;align-items:center;justify-content:center;}
.sliver-menu-text{
  display:none; /* "menu" now lives on the right edge (.bar-menu-edge); left sliver is the collapse control */
  font-size:13px;font-weight:600;letter-spacing:0.25em;
  color:var(--text-bright);
  writing-mode:vertical-rl;text-orientation:mixed;
  transform:rotate(180deg);
  white-space:nowrap;
  transition:opacity 0.3s,color 0.3s;
}
/* Right-edge "menu" strip — the part that peeks through when the bar is collapsed. */
.bar-menu-edge{
  position:absolute;right:0;top:0;bottom:0;width:var(--sliver);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  background:var(--bg-panel);border-left:1px solid var(--border-mid);
  cursor:pointer;z-index:3;transition:opacity 0.3s;
}
.bar-menu-edge-sig-media{width:34px;height:auto;display:block;flex-shrink:0;}
.bar-menu-edge-text{
  writing-mode:vertical-rl;text-orientation:mixed;transform:rotate(180deg);
  font-size:13px;font-weight:600;letter-spacing:0.25em;color:var(--text-bright);white-space:nowrap;
}
.bar-menu-edge-burger{position:absolute;bottom:24px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;gap:5px;align-items:center;}
.bar-menu-edge-burger i{display:block;width:3px;height:3px;border-radius:50%;background:var(--text-bright);}
/* homepage: silver dots to echo the silver animated S */
body[data-page="home"] .bar-menu-edge-burger i{background:#8b8f94;}
/* Hide the menu strip whenever the bar is open or previewing. */
.left-bar.open .bar-menu-edge,
.left-bar.peek .bar-menu-edge,
.left-bar:hover .bar-menu-edge,
html.bar-preopen .left-bar .bar-menu-edge{opacity:0;pointer-events:none;}
.left-bar.open .sliver-menu-text,
.left-bar.peek .sliver-menu-text{opacity:0;}
/* Page-leave collapse: the bar is sliding back to the sliver but the
   .open / :hover class is still on it for the duration of the animation,
   which would keep the S + 3-dots strip hidden and the collapse
   affordance showing — so the sliver lands BLANK. Force the strip back
   on (and the collapse affordance off) the moment .collapsing lands so
   the sliver shows its persistent S + dots throughout the slide and the
   handoff to the next page is seamless. Comes after the hide rules so
   equal-specificity source-order wins; !important beats the :hover/open
   opacity:0 even while those linger mid-animation. */
.left-bar.collapsing .bar-menu-edge{opacity:1 !important;pointer-events:none;}
.left-bar.collapsing .sliver-collapse-wrap{opacity:0 !important;}
.sliver-collapse-wrap{
  display:flex;align-items:center;justify-content:center;
  padding-bottom:18px;opacity:0;transition:opacity 0.3s;
}
.left-bar:hover .sliver-collapse-wrap,
.left-bar.open .sliver-collapse-wrap,
.left-bar.peek .sliver-collapse-wrap{opacity:1;}
.sliver-collapse-text{
  font-size:11px;font-weight:600;letter-spacing:0.20em;color:var(--text-dim);
  writing-mode:vertical-rl;text-orientation:mixed;transform:rotate(180deg);
  white-space:nowrap;transition:color 0.3s;
}
.left-bar:hover .sliver-collapse-text,
.left-bar.open .sliver-collapse-text,
.left-bar.peek .sliver-collapse-text{color:var(--acc);}
.sliver-dots{display:flex;flex-direction:column;gap:5px;align-items:center;padding-bottom:20px;}
.sliver-dot{width:3px;height:3px;border-radius:50%;background:var(--text-dim);transition:background 0.4s;}
.left-bar:hover .sliver-dot,.left-bar.open .sliver-dot,.left-bar.peek .sliver-dot{background:var(--acc);}
/* TOUCH DEVICES — kill the hover-expand. Touch screens emulate :hover
   as a "sticky" state after a tap, so tapping the collapsed sliver
   would (a) hover-expand the bar and (b) flip the S/3-dots strip to
   pointer-events:none — and the very same tap then falls THROUGH to the
   menu row sitting behind the sliver, navigating instead of opening the
   menu. On coarse pointers we neutralise every hover-driven change to
   the collapsed bar: it stays at the sliver, and the strip stays the
   live tap target (which fires toggleBar). The bar only opens via that
   explicit toggle. Higher specificity + later source order than the
   bare :hover rules above, so these win without !important. */
@media(hover:none){
  .left-bar:not(.open):not(.peek):hover{transform:translateX(calc(var(--sliver) - var(--bar-w)));}
  .left-bar:not(.open):not(.peek):hover .bar-menu-edge{opacity:1;pointer-events:auto;}
  .left-bar:not(.open):not(.peek):hover .sliver-collapse-wrap{opacity:0;}
  .left-bar:not(.open):not(.peek):hover .sliver-collapse-text{color:var(--text-dim);}
  .left-bar:not(.open):not(.peek):hover .sliver-dot{background:var(--text-dim);}
  /* SMOOTHNESS — peek↔fixed reflows the page content via .main's
     padding-left (a layout-bound animation). On a weak tablet GPU,
     recomputing the bar's 24px backdrop-blur over that moving content
     every frame is what makes the reflow stutter (desktop absorbs it).
     For the ~0.42s the transition runs, .bar-lite drops the blur so the
     GPU only has to relayout, not re-blur. The panel is 80% opaque so
     the missing blur is barely perceptible. Touch-only — desktop keeps
     the blur and is already smooth. */
  .left-bar.bar-lite{backdrop-filter:none!important;-webkit-backdrop-filter:none!important;}
}

.bar-content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;}
.bar-top{padding:26px 26px 20px;border-bottom:1px solid var(--border);flex-shrink:0;position:relative;overflow:hidden;}
.bar-top-pixels{
  position:absolute;inset:0;z-index:200;pointer-events:none;
  display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(4,1fr);
  isolation:isolate;
}
.bar-top-pixel{background:var(--bg-solid);opacity:0;transition:opacity 0.4s ease;}
.bar-top-pixels.out .bar-top-pixel{opacity:1;}
@media(max-width:700px){.bar-top-pixels{display:none;}}

.big-name{
  display:flex;align-items:flex-end;
  font-size:var(--fs-hero);font-weight:600;color:var(--text-bright);
  line-height:0.88;letter-spacing:-0.03em;text-decoration:none;
  margin-bottom:10px;cursor:pointer;transition:opacity 0.3s;
  -webkit-tap-highlight-color:transparent;
}
/* :hover gated to real hover devices — on iOS, taps briefly
   trigger :hover which would darken the animated S / "eppe de roo"
   for a moment when the user taps it (user reported "darken when
   tapping"). Restrict to (hover: hover) so only mouse devices see
   the dim-on-hover affordance. */
@media (hover: hover){
  .big-name:hover{opacity:0.85;}
}
.tagline{font-size:var(--fs-small);letter-spacing:0.12em;color:var(--text-dim);line-height:1.7;}
@media(min-width:701px){.bar-top .tagline{display:none;}}

.sig-wrap{
  display:block;
  flex-shrink:0;
  height:calc(var(--fs-hero) * 2 * 1.05 + 4px);
  width:calc(var(--fs-hero) * 2 * 1.05 * 0.7);
  overflow:hidden;
  position:relative;
}
.sig-anim{
  display:block;
  height:calc(100% - 4px);
  width:auto;
  position:absolute;
  top:2px;
  left:50%;
  transform:translateX(-50%);
  opacity:0.9;
  transition:opacity 0.3s ease;
  pointer-events:none;
  -webkit-user-select:none;
  user-select:none;
  /* Firefox renders <video> elements with a default opaque black
     background — even when the video content has alpha, that
     black backdrop shows through the transparent regions. Force
     it transparent so the page background shows through the
     silver S's alpha cutout correctly. (Applies whether the
     element is an <img> on mobile or <video> on desktop — the
     property is harmless on <img>.) */
  background:transparent;
  background-color:transparent;
}
/* Belt-and-braces: also strip background from the video element
   directly via the tag selector, in case any browser applies a
   user-agent stylesheet default to <video> that overrides the
   class-targeted rule above. */
video.sig-anim{
  background:transparent !important;
  background-color:transparent !important;
}
@media (hover: hover){
  .big-name:hover .sig-anim{opacity:1;}
}

/* ── PAGE-LEAVE CENTER-SCREEN TURNING S ────────────────────────
   Applied by shared.js / subpage.js navigation handlers. A
   <video> element is injected at viewport center (or content-area
   center when the left bar is open) playing a Blender-rendered
   40-frame turn animation at 30fps (~1.33s). The video file IS
   the animation — no CSS rotation needed.
   Sits ABOVE the pixel-grid cover (z-index:9999) so the user
   sees: page pixellates to cream → S turns once in the center →
   new page paints.
   Desktop-only: on mobile the mstack menu already fills the
   visible page area, and a centered S over the cards reads as
   a glitch. JS gate in shared.js's _showNavSpinner enforces this. */
.nav-spinner{
  position:fixed !important;
  top:50% !important;
  left:50% !important;
  /* Three-state scale + fade pattern. Tuned to be UNMISTAKABLY
     visible:
       · base     — opacity:0, scale(0.4)   (mount: well-under-size
                                             + invisible, so the
                                             entrance grows ~2.5×)
       · .visible — opacity:1, scale(1)     (entrance: 0.5s
                                             ease-out-quart — fast,
                                             snappy, fully visible
                                             by ~500ms with the
                                             scale-pop reading as
                                             a clear "S appearing")
       · .leaving — opacity:0, scale(0.92)  (exit: 0.4s ease — slight
                                             shrink + fade, mirrors
                                             the index loader). */
  transform:translate(-50%, -50%) scale(0.2);
  z-index:10001 !important;   /* above .page-transition (9999) */
  pointer-events:none;
  width:280px;                /* desktop default — substantial
                                 focal point in the content area */
  /* Lock the box to a square BEFORE the video's metadata loads.
     With `height:auto` the parent collapsed until the video
     reported its intrinsic dimensions on `loadedmetadata`, at
     which point the height jumped from 0 → ~280px — and since
     the element is centered via `translate(-50%, -50%)`, that
     bigger height meant a bigger absolute Y translation, causing
     the S to suddenly "jerk up" when playback started. The
     source video is 1:1 (220×220), so aspect-ratio:1 reserves
     the right vertical space from the very first frame.
     Fallback to an explicit height for older browsers without
     aspect-ratio (Safari < 15). */
  height:280px;
  aspect-ratio:1 / 1;
  opacity:0;
  /* Entrance fade: 0.4s with ease-out-back overshoot — the
     transform curve goes slightly PAST 1.0 at peak (~1.08) before
     settling to 1.0, giving the S a satisfying "pop in" feel.
     Combined with the 5× scale growth (0.2 → 1), the entrance
     reads as a clear, deliberate arrival gesture rather than a
     subtle fade-up.
     Opacity uses a normal ease-out so the fade itself isn't
     bouncy — only the scale gets the overshoot. */
  transition:opacity 0.35s ease-out,
             transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
  /* Compositor hint for the video + transform layer. */
  will-change:transform, opacity;
}
.nav-spinner.visible{
  opacity:1;
  transform:translate(-50%, -50%) scale(1);
}
.nav-spinner.leaving{
  opacity:0;
  transform:translate(-50%, -50%) scale(0.92);
  /* Faster exit — the user has already seen the S; no need to
     drag the deflate out to 700ms. */
  transition:opacity 0.4s ease, transform 0.4s ease;
}
@media(max-width:700px){
  /* Mobile — bumped from 110→220 so the S reads as a clear
     focal element on phones too. Still smaller than desktop
     because the viewport is narrower, but proportionally
     larger relative to the screen. */
  .nav-spinner{ width:220px; height:220px; }
}
.nav-spinner video,
.nav-spinner img{
  display:block;
  width:100%;
  height:100%;             /* fill the locked-square parent so no
                              size jump happens when metadata loads */
  object-fit:contain;      /* if the asset isn't perfectly square,
                              letterbox/pillarbox rather than crop */
  visibility:visible !important;
  opacity:1 !important;
  /* Firefox <video> default background-color is opaque black —
     same fix as .sig-anim above so the alpha cutout of the
     turning S doesn't render against a black rectangle. */
  background:transparent !important;
  background-color:transparent !important;
}
@media(min-width:701px){
  /* Center within the content area (right of the left bar).
     Even when the bar is in its COLLAPSED sliver state it
     still occupies 52px on the left, so the spinner default
     of left:50% sits ~26px to the left of the actual content
     centre — visible on tablet portrait where the content
     column is narrower. The sliver-baseline rule fixes that
     for the default state; the .open / :hover / bar-preopen
     overrides then bump the offset up to bar-w/2 when the
     bar is unfolded. */
  .nav-spinner{
    left:calc(50% + var(--sliver) / 2) !important;
  }
  .left-bar.open ~ .nav-spinner,
  .left-bar:hover ~ .nav-spinner{
    left:calc(50% + var(--bar-w) / 2) !important;
  }
  html.bar-preopen .nav-spinner{
    left:calc(50% + var(--bar-w) / 2) !important;
  }
  /* Page-leave from an OPEN bar: previewNavigate adds .collapsing
     to slide the bar back to the sliver before the new page loads.
     The .open class is still on the bar during that animation, so
     the rule above would offset the spinner by bar-w/2 even though
     the bar is visually shrinking to ~52px. Snap the spinner to
     the sliver position the moment .collapsing lands — the bar
     animates back over ~0.42s and the spinner stays in the
     correct content-column centre throughout. Must come AFTER
     the .open rule so equal-specificity source-order wins. */
  .left-bar.collapsing ~ .nav-spinner{
    left:calc(50% + var(--sliver) / 2) !important;
  }
}
.name-text{
  display:block;
  margin-left:-20px;
  position:relative;
  background:linear-gradient(to bottom,
    rgba(35,33,30,0.95) 0%,
    rgba(85,82,78,0.85) 45%,
    rgba(85,82,78,0.85) 55%,
    rgba(35,33,30,0.95) 100%
  );
  -webkit-background-clip:text;
  background-clip:text;
  -webkit-text-fill-color:transparent;
}
.name-text-glow{
  position:absolute;
  inset:0;
  background:linear-gradient(to bottom,
    rgba(35,33,30,0.95) 0%,
    rgba(230,229,228,0.98) 45%,
    rgba(230,229,228,0.98) 55%,
    rgba(35,33,30,0.95) 100%
  );
  -webkit-background-clip:text;
  background-clip:text;
  -webkit-text-fill-color:transparent;
  opacity:0;
}
@media(min-width:701px){
  .name-text-glow.glow-ready{
    animation:sig-glow 9s linear forwards;
  }
  @keyframes sig-glow{
    0%   { opacity:0; }
    7%   { opacity:0.31; }
    14%  { opacity:0.53; }
    21%  { opacity:0.58; }
    29%  { opacity:0.66; }
    36%  { opacity:0.78; }
    43%  { opacity:0.91; }
    50%  { opacity:1; }
    57%  { opacity:0.96; }
    64%  { opacity:0.85; }
    71%  { opacity:0.74; }
    79%  { opacity:0.60; }
    86%  { opacity:0.54; }
    93%  { opacity:0.55; }
    100% { opacity:0; }
  }
}
.name-deroo{display:inline-block;padding-left:0.5em;}

.bar-nav{padding:14px 22px;border-bottom:1px solid var(--border);flex-shrink:0;display:flex;flex-direction:column;gap:1px;}
.bar-nav-link{font-size:16px;letter-spacing:0.07em;color:var(--text-mid);text-decoration:none;padding:7px 0;border-bottom:1px solid transparent;transition:color 0.2s ease, border-bottom-color 0.2s ease, background 0.2s ease;display:block;}
.bar-nav-link:hover,.bar-nav-link.cur{color:var(--text-bright);border-bottom-color:var(--acc-border);}.bar-nav-link.cur{font-weight:550;}

/* SECTION NAV (works / exhibitions / equilibrium) — desktop/tablet.
   Original plain treatment kept; the only refinement is shortening the
   hairline underline to hug the word instead of spanning the full
   column (align-self:flex-start makes the flex item size to content).
   No glass tile, no brackets — the filament bracket language stays
   reserved for the series rows. */
@media(min-width:701px){
  .bar-nav-link{
    align-self:flex-start;
  }
}

.bar-series{
  flex:1;overflow-y:auto;position:relative;
  /* PERF/UX: reserve the scrollbar gutter even when no scrollbar
     is showing. Without this, hovering a row caused .snav-explore
     to expand → total list height grew past the .bar-series visible
     height → scrollbar appeared → scrollbar took ~15 px of
     horizontal space inside the column → every row's content
     (including the right-anchored .snav-year) shifted left.
     Un-hover collapsed the row → scrollbar vanished → content
     slid back. That toggle read as a glitch. scrollbar-gutter:
     stable reserves the gutter unconditionally so layout never
     shifts on hover. */
  scrollbar-gutter:stable;
}
.bar-series::-webkit-scrollbar{width:2px;}
.bar-series::-webkit-scrollbar-thumb{background:var(--border-mid);}

.bar-series-hint{
  position:absolute;top:24px;left:0;right:0;
  text-align:center;font-size:22px;line-height:1;
  color:var(--text-dim);
  opacity:0;transition:opacity 0.6s ease;
  pointer-events:none;user-select:none;
}
.bar-series-hint.in{opacity:0.55;}
.bar-series-hint.out{opacity:0;}
@media(max-width:700px){.bar-series-hint{display:none;}}

.bar-col-header{display:grid;grid-template-columns:32px 1fr auto;padding:8px 22px;border-bottom:1px solid var(--border);background:rgba(var(--r),var(--g),var(--b),0.06);flex-shrink:0;gap:8px;}
.bar-col-header span{font-size:var(--fs-small);letter-spacing:0.18em;color:var(--text-dim);}
.snav-item{display:grid;grid-template-columns:32px 1fr auto;gap:8px;align-items:center;padding:15px 22px;font-size:15px;font-weight:500;color:var(--text-mid);text-decoration:none;border-bottom:1px solid var(--border);transition:background 0.18s ease, box-shadow 0.18s ease, color 0.2s ease;cursor:pointer;}
.snav-item:hover{color:var(--text-bright);background:var(--acc-muted);}
.snav-item.active{color:var(--text-bright);}
.snav-num{font-size:10px;letter-spacing:0.12em;color:var(--text-dim);text-align:left;}
.snav-name{font-size:14px;font-weight:500;color:inherit;line-height:1.25;overflow-wrap:break-word;}
.snav-item:hover .snav-name,.snav-item.active .snav-name{color:var(--text-bright);}
.snav-item.active .snav-name{font-weight:600;}
.snav-year{font-size:10px;letter-spacing:0.12em;color:var(--text-dim);text-align:right;}
.snav-explore{
  grid-column:1 / -1;
  /* GROW BACK ON HOVER. User wanted the row to visibly change
     size, not stay locked. max-height 0 → 18 px transition
     restores the visible growth. The earlier "jump" complaint
     was about the rows shifting in a jarring way — softening
     it with: (a) a tight 0.15 s duration so the shift is fast
     enough to feel like a snap rather than a slide, and (b)
     contain:layout on the .snav-item host (set above) which
     scopes each row's reflow so the rest of the .bar-series
     list doesn't repaint when one row's height changes. */
  max-height:0;
  overflow:hidden;
  transition:max-height 0.15s ease, padding-top 0.15s ease;
  padding-top:0;
}
.snav-item:hover .snav-explore,
.snav-item.fil-glass.active .snav-explore{
  /* v=90 — bumped max-height 18 → 22 so the descenders of p/g/y
     in "explore series" render fully. Previous 18 was clipping
     the bottom of the letters (font-size 10px + padding-top 4px
     needed more vertical room for descenders below baseline). */
  max-height:22px;
  padding-top:4px;
}
.snav-explore-inner{
  display:inline-block;font-size:10px;letter-spacing:0.18em;
  color:var(--text-dim);position:relative;transition:color 0.2s;
}
.snav-explore-inner::after{
  content:'';position:absolute;bottom:-2px;left:0;
  width:100%;height:1px;background:var(--acc);
  transform-origin:left;
  /* v=97 — animation DECLARATION removed from base rule.
     Was: animation:pulse-ul 3s ease-in-out infinite;
     Problem: that ran the animation on EVERY snav-item's
     ::after pseudo, all 15 rows, even though .snav-explore is
     max-height:0 (collapsed) when the row isn't hovered or
     active. Chromium still ran the animation timeline and
     allocated a compositor layer per animating element — 15
     constant transform+opacity animations running in the
     background that the user never sees. Declaration moved
     into the :hover / .active scoped rules below, so only
     the currently-visible row's underline pulses. */
}
.snav-item:hover .snav-explore-inner{color:var(--text-mid);}
.snav-item:hover .snav-explore-inner::after,
.snav-item.active .snav-explore-inner::after{
  animation:pulse-ul 3s ease-in-out infinite;
}

/* Bar-info: subscribe link sits centred horizontally and only takes the
   width of its own text (so the pulse-underline tracks the text width
   instead of stretching across the bar). The .info-links row of
   about | portfolio | cv | contact also centres as a group, so the two
   rows feel like a single grouped footer. */
.bar-info{padding:18px 22px;border-top:1px solid var(--border);flex-shrink:0;display:flex;flex-direction:column;align-items:center;gap:14px;margin-bottom:var(--gdpr-h,0px);transition:margin-bottom 0.45s cubic-bezier(0.25,0.46,0.45,0.94);}
.bar-info .sub-link{
  display:inline-block;
  width:auto;
  max-width:max-content;
  font-size:16px;
  letter-spacing:0.07em;
  color:var(--text-mid);
  padding:6px 0;
}
.bar-info .sub-link:hover{color:var(--text-bright);}
.bar-info .sub-link::after{ bottom:-2px; }
.info-links{display:flex;gap:0;flex-wrap:nowrap;align-items:center;justify-content:center;}
.info-link{font-size:16px;letter-spacing:0.08em;color:var(--text-dim);text-decoration:none;background:none;border:none;cursor:pointer;padding:5px 10px;border-right:1px solid var(--border-mid);transition:color 0.25s;white-space:nowrap;}
.info-link:last-child{border-right:none;padding-right:0;margin-right:0;}
.info-link:hover{color:var(--text-bright);}
.mobile-only-link{display:none;}
/* portfolio + cv (.about-dl) show in the about-overlay link bar on every
   viewport. On mobile they're the only way to reach the PDFs (sidebar is
   hidden); on desktop they round the bar out to four items so it doesn't
   read as sparse — the sidebar copy is never on screen at the same time as
   the open overlay, so it doesn't feel like live duplication. */
.info-link:nth-last-child(2){border-right:none;}

/* Mobile menu trigger button — pill-shaped, plain "menu" text, matches
   the close-button vocabulary used on .ov-close / .mstack-close (round
   shape, subtle border, backdrop-blur background). Floats at top-LEFT
   so it doesn't collide with the .mstack-close X (which lands at top-
   right when the deck opens). Compact 36px-tall pill so it doesn't
   dominate the page header. */
/* The standalone .mob-top-bar was replaced by extending the page-header
   sticky to cover the top zone — this way the pill floats over the
   SAME blurred surface that the rest of the chrome uses, and the
   pill's own backdrop-filter actually has scrolling content to blur
   (the .mob-top-bar previously had nothing scrolling behind it). The
   .main margin-top override below resets the per-page 58px margin so
   the .page-header-sticky starts at viewport y=0 and covers the notch
   zone. .header-left's padding-top is bumped to clear the pill. */
@media(max-width:700px){
  body[data-page="sub"] .main{margin-top:0!important;}
}

/* Mobile menu trigger — pill that travels between two corners.
   At rest (top of page) it sits in the top-LEFT corner above the
   page-header. Once the user scrolls past a small threshold the
   pill animates down to the bottom-LEFT corner of the viewport.
   Reverse on scroll back. The `top` value is rewritten by JS
   (subpage.js #mobBtn-tracker IIFE) and the CSS `transition: top`
   handles the smoothing. */
.mob-menu-btn{
  display:none;
  -webkit-appearance:none;
  appearance:none;
  position:fixed;
  top:calc(env(safe-area-inset-top, 0px) + 7px);
  left:14px;
  z-index:200;
  height:44px;
  padding:0 22px;
  /* "Liquid glass" — layered shadows simulate a piece of refractive
     glass:
       1. outer drop shadow — lifts the pill off the surface
       2. inset top highlight — strong specular catch on the top edge
       3. inset secondary highlight — softer light just below the rim
       4. inset bottom subtle line — implies thickness / underside
       5. inset full ring — refractive edge around the whole pill
     Combined with a saturated/brightened backdrop-filter the pill
     reads as a 3D shape with light passing through it. */
  background:rgba(255,255,255,0.30);
  border:1px solid rgba(255,255,255,0.55);
  border-radius:999px;
  cursor:pointer;
  font-family:'Safiro',sans-serif!important;
  font-size:16px;
  letter-spacing:0.14em;
  color:var(--text-mid);
  -webkit-text-fill-color:var(--text-mid);
  -webkit-tap-highlight-color:transparent;
  backdrop-filter:blur(24px) saturate(200%) brightness(1.05);
  -webkit-backdrop-filter:blur(24px) saturate(200%) brightness(1.05);
  box-shadow:
    0 8px 24px rgba(20,18,16,0.08),
    0 2px 4px rgba(20,18,16,0.04),
    inset 0 1.4px 0 rgba(255,255,255,0.85),
    inset 0 3px 4px rgba(255,255,255,0.35),
    inset 0 -1px 0 rgba(20,18,16,0.06),
    inset 0 0 0 0.5px rgba(255,255,255,0.40);
  transition:top 0.32s cubic-bezier(0.25,0.46,0.45,0.94), background 0.2s, border-color 0.2s, color 0.2s, box-shadow 0.2s;
  align-items:center;
  justify-content:center;
  white-space:nowrap;
  line-height:1;
}
.mob-menu-btn:hover{
  color:var(--text-bright);
  -webkit-text-fill-color:var(--text-bright);
  border-color:rgba(255,255,255,0.75);
  background:rgba(255,255,255,0.42);
}
.mob-menu-btn:active{
  background:rgba(255,255,255,0.55);
  box-shadow:
    0 2px 6px rgba(20,18,16,0.06),
    inset 0 1px 0 rgba(255,255,255,0.55),
    inset 0 -1px 0 rgba(20,18,16,0.10);
}
@media(max-width:700px){
  /* Floating menu pill — the bare sticky-header trigger wasn't an
     obvious affordance, so on mobile subpages the liquid-glass "menu"
     pill floats in the bottom-left, always reachable. It tucks away
     (fades down) once the user scrolls to the very bottom so it never
     covers the footer, and hides while the mstack menu overlay is open.
     (The sticky header still works as a trigger too — this is an
     additive, clearer affordance.) */
  /* Anchored by TOP (not bottom) so the slide between the bottom resting
     spot and the under-the-header spot animates as a single top transition.
     Default: bottom-left. When the "up next" strip slides in at the bottom,
     subpage.js sets an inline top just below the sticky header so the pill
     gets out of the strip's way (it slides UP rather than overlapping). */
  .mob-menu-btn{
    display:flex!important;
    /* Box at 16px to match the compact header pill (margin:0 16px). The
       bracket override below gives this pill the SAME gradient-bracket
       recipe as the header (bar pinned to the box edge), so both pills'
       visible left brackets land on the exact same 16px line. */
    left:16px;
    bottom:auto;
    /* Anchored by top; the reposition is done with transform:translateY
       (set by subpage.js) so it animates on the COMPOSITOR thread. The
       old approach animated `top`, which is layout-bound — it needs the
       main thread every frame, and the up-next strip's TV-static canvas
       redraw + backdrop-filter were hogging that thread at the exact
       moment the pill repositions, so it stuttered. Transform sidesteps
       the main thread entirely. */
    top:calc(100dvh - 44px - env(safe-area-inset-bottom, 0px) - 18px);
    z-index:200;
    transition:transform 0.42s cubic-bezier(0.25,0.46,0.45,0.94), opacity 0.3s ease, background 0.2s, border-color 0.2s, color 0.2s, box-shadow 0.2s;
    will-change:transform;
  }
  /* The .filament-pill chassis (transition: background, transform) is
     defined later at equal specificity and would otherwise wipe the
     transition above. Re-assert it (transform for the reposition slide
     + hover background) at higher specificity. */
  .mob-menu-btn.filament-pill{
    /* CRITICAL: .filament-pill{position:relative} is declared later than
       .mob-menu-btn{position:fixed} at equal specificity, so it wins the
       cascade and the button stays in normal FLOW — its 44px box parks at
       the top of <body> and pushes .main (and the whole page) down ~44px,
       which is the phantom top band. Re-assert fixed at higher specificity
       so the button is taken out of flow and floats as intended. */
    position:fixed !important;
    transition:transform 0.42s cubic-bezier(0.25,0.46,0.45,0.94), opacity 0.3s ease, background 0.15s ease, border-color 0.2s ease;
  }
  /* Give the menu pill the SAME three-stacked-gradient brackets the
     sticky-header pill uses (see the .page-header-sticky.filament-pill
     bracket rules), with the vertical bar pinned to the box edge
     (left:0 / right:0) and full host height (top/bottom:0). Combined with
     left:16px above, the menu pill's visible bracket lands on the exact
     same line as the header pill's — and looks identical, not the default
     SVG bracket that rendered the bar a few px inside the box. */
  .mob-menu-btn.filament-pill::before,
  .mob-menu-btn.filament-pill::after,
  .mob-book-bar-btn.filament-pill::before,
  .mob-book-bar-btn.filament-pill::after{
    top:0; bottom:0;
    background-image:
      linear-gradient(#ffffff,#ffffff),
      linear-gradient(#ffffff,#ffffff),
      linear-gradient(#ffffff,#ffffff);
    background-size:15px 4px, 15px 4px, 4px 100%;
    background-repeat:no-repeat,no-repeat,no-repeat;
  }
  .mob-menu-btn.filament-pill::before,
  .mob-book-bar-btn.filament-pill::before{ left:0; background-position:0 0, 0 100%, 0 0; }
  .mob-menu-btn.filament-pill::after,
  .mob-book-bar-btn.filament-pill::after{ right:0; background-position:100% 0, 100% 100%, 100% 0; }
  body.mstack-open .mob-menu-btn{opacity:0!important;pointer-events:none!important;}
}

/* ── PAGE FOOTER ──────────────────────────────────────────
   Home only — anchored to bottom of body via position:absolute (body
   has position:relative + min-height:100dvh from the baseline above).
   Hidden on subpages and on mobile (.mdock replaces on mobile). */
.page-footer{
  position:absolute;bottom:0;left:var(--sliver);right:0;
  display:grid;grid-template-columns:1fr 1fr 1fr;padding:10px 32px;
  border-top:1px solid var(--border);
  background:rgba(250,250,249,0.08);
  backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
  transition:left 0.42s cubic-bezier(0.25,0.46,0.45,0.94);
  z-index:4;
}
body[data-page="sub"] .page-footer{display:none!important;}
.page-footer span{font-size:var(--fs-small);letter-spacing:0.14em;color:var(--text-dim);}
.page-footer span:nth-child(2){text-align:center;}
.page-footer span:nth-child(3){text-align:right;}
/* Bar open / hovered / preopen → shifts footer right by --bar-w */
.left-bar:hover ~ .page-footer,
.left-bar.open ~ .page-footer{left:var(--bar-w);}
@media(min-width:701px){html.bar-preopen .page-footer{left:var(--bar-w);}}
@media(max-width:700px){
  .desktop-cookie-link{display:none;}
  /* On mobile the .mdock replaces .page-footer */
  .page-footer{display:none!important;}
}

/* ── LANDSCAPE LOCK ── */
.landscape-lock{display:none;position:fixed;inset:0;z-index:99999;background:var(--bg-solid);flex-direction:column;align-items:center;justify-content:center;gap:32px;padding:48px 32px;text-align:center;}
@media screen and (max-width:900px) and (orientation:landscape){.landscape-lock{display:flex!important;}}
html.video-fs-landscape .landscape-lock{display:none!important;}
.landscape-lock-line{width:40px;height:1px;background:var(--acc);animation:ll-line 2.8s ease-in-out infinite;}
.landscape-lock-label{font-size:10px;letter-spacing:0.28em;color:var(--text-dim);text-transform:uppercase!important;}
.landscape-lock-heading{font-size:var(--fs-h1,32px);font-weight:600;letter-spacing:-0.02em;line-height:0.92;color:var(--text-bright);}
.landscape-lock-sub{font-size:var(--fs-small,12px);letter-spacing:0.18em;color:var(--text-dim);line-height:1.8;}
@keyframes ll-line{0%,100%{transform:scaleX(0.15);opacity:0.2;}50%{transform:scaleX(1);opacity:0.8;}}

/* ════════════════════════════════════════════════════════════
   MOBILE CARD STACK + SLOT-MACHINE REEL (≤700px)
   Base markup is shared via chrome.js. Subpages opt into
   "overlay mode" by adding data-overlay="true" + .mstack-close.
   ════════════════════════════════════════════════════════════ */
.mstack{display:none;}
@media(max-width:700px){
  /* Mstack box. In page mode (homepage) it's the whole UI; in overlay
     mode (subpages) it's hidden until .show is added by the menu toggle. */
  .mstack{
    flex-direction:column;
    position:fixed;inset:0;z-index:30;
    background:var(--bg-solid);color:var(--text-bright);
    overflow:hidden;
  }
  /* Page mode (no data-overlay): auto-shows. */
  .mstack:not([data-overlay]){display:flex;}
  /* Overlay mode (subpages): hidden by default; .show toggles on.
     Z-index 200 puts it above the .mob-menu-btn (200) by virtue of
     source order.

     Permanent `display:flex` (instead of `display:none → display:flex`
     toggle) — keeps the mstack subtree in the layout tree at all
     times, so the first .show doesn't trigger a 120ms layout-and-
     first-paint hit (probe.js logged that on subpages). visibility
     + pointer-events do the actual hide/show; the existing
     mstack-morph-in keyframes still drive the visible reveal.
     Trade-off: marginally heavier first page-paint (the subtree
     gets laid out up-front), but every subsequent menu-open is
     paint-only — much smoother perceived UX. */
  /* ── EXPAND-FROM-TRIGGER ───────────────────────────────────────────
     The overlay grows OUT of the control that opened it:
       · floating menu button (bottom) → expands up/out from the pill
       · sticky header (top) → expands down/out from the header
     mstackShow() sets `transform-origin` to the trigger's on-screen
     centre (read from its rect), so the scale anchors there. Pure
     composite ops (scale + opacity) — the GPU scales an already-painted
     layer, no per-frame paint — which is why this stays smooth where the
     old clip-path morph spiked. visibility flips after the animation so
     the panel stays in layout for the whole collapse. */
  .mstack[data-overlay]{
    display:flex;
    visibility:hidden;
    pointer-events:none;
    opacity:0;
    transform:scale(0.55);
    transform-origin:50% 94%;            /* default (bottom button); JS overrides per trigger */
    z-index:200;
    transition: transform 0.44s cubic-bezier(0.22,0.61,0.36,1),
                opacity 0.30s ease,
                visibility 0s 0.44s;
    will-change:transform, opacity;
  }
  .mstack[data-overlay].show{
    visibility:visible;
    pointer-events:auto;
    opacity:1;
    transform:scale(1);
    transition: transform 0.44s cubic-bezier(0.22,0.61,0.36,1),
                opacity 0.26s ease,
                visibility 0s;
  }
  /* Drop .mdock's backdrop-filter during the mstack fade-out.
     The dock is INSIDE the mstack subtree; while the parent is
     fading out (NOT .show) the blur is wasted GPU work. The
     full 24px blur is preserved while .show is active because
     a dark element behind the dock relies on the strong blur
     to read correctly. (Earlier round reduced 24px → 8px to
     try smoothing mstack:show's first-frame spike, but the
     visual loss broke the dark element's appearance, and the
     reduction didn't actually fix the spike anyway.) */
  .mstack[data-overlay]:not(.show) .mdock{
    backdrop-filter:none;
    -webkit-backdrop-filter:none;
  }
  /* Drop .mdock's backdrop-filter while the quick-jump grid is open.
     The .mjumpGrid fully covers the .mdock subscribe pill at the
     bottom of the mstack — but the pill keeps painting its 24px
     backdrop-filter every frame, which probe.js measured as the
     dominant per-frame cost during mjump:open/close (31768px²,
     score 95k, identified as the heaviest element on screen).
     With the pill visually covered, that blur is purely wasted
     GPU work. Drop it for the duration of mjump-open and the
     mjump morph runs composite-only on top. */
  .mstack[data-overlay].mjump-open .mdock{
    backdrop-filter:none;
    -webkit-backdrop-filter:none;
  }
  /* Reveal animation — composite-only: transform:scale + opacity.
     Replaced the previous clip-path:inset() morph that animated
     from the sticky-pill footprint out to fullscreen. The clip-
     path approach was technically interesting but it does
     PROGRESSIVE REVEAL: each frame uncovers new content, and the
     first time a region with backdrop-filter (the .mdock) becomes
     visible, the compositor pays a first-paint hit (probe.js
     measured 126ms first-frame spike). transform:scale + opacity
     are both pure composite operations — the GPU just shifts +
     alphas an already-painted layer, no per-frame paint cost.
     Visual character: subtle "swell up" from 96% scale instead of
     "expand from pill", lower drama but consistently smooth. */
  @keyframes mstack-morph-in{
    0% {
      transform: scale(0.96) translateY(8px);
      opacity: 0;
    }
    100% {
      transform: none;
      opacity: 1;
    }
  }

  /* ── SLOT-MACHINE REEL ─────────────────────────────────────────────────── */
  .mreel-wrap{
    position:relative;flex-shrink:0;
    /* 76px = pane (44) + 16px peek area on top + 16px peek area on bottom.
       Was 88px, which gave 22px peek areas — labels were so far from the
       pane that only descenders/ascenders showed through, making the
       above peek look more visible than the below. Tightening symmetrically
       brings both prev/next labels visibly closer to the loupe edge. */
    height:76px;
    /* Default (homepage menu) — no close button to align with, so the
       wrap sits flush against the safe-area inset and the loupe pane
       sits at safe+38 vertically. */
    margin-top:env(safe-area-inset-top,0);
    overflow:hidden;
  }
  /* Subpage menu only — pushed down 6px so the wrap's vertical centre
     (margin-top + 38) lines up with the .mstack-close button's centre
     (top:safe+22, height:44, centre at safe+44). The homepage has no
     close button so this offset would just lower the slot machine for
     no reason. */
  .mstack[data-overlay] .mreel-wrap{
    margin-top:calc(env(safe-area-inset-top,0) + 6px);
  }
  /* Loupe-style window frame — a glass lens that the scrolling
     labels are magnified through. The radial gradient gives the
     pane a brighter "lens center" that fades into a darker rim,
     mimicking a real magnifying glass. `backdrop-filter:saturate`
     boosts the colour of the label that lands inside without
     blurring it (label stays crisp). The frame sits directly on
     top of the active label (z-index 2 above the .mreel-item
     z-index 1), and the magnification of the .cur item
     (transform:scale + bigger font) reads as if the lens is
     actually enlarging it. */
  .mreel-frame{
    position:absolute;left:50%;top:50%;
    transform:translate(-50%,-50%);
    /* Height + brightness match .mstack-close so the loupe pane and the
       overlay-mode close button read as the same component on the same
       horizontal axis. Same dim-warm tint, same neutral border, same
       6px blur — no radial gradient or brightness boost (which made the
       pane glow noticeably brighter than the surrounding chrome). */
    width:calc(100% - 32px);height:44px;
    pointer-events:none;
    border-radius:999px;
    background:rgba(40,38,36,0.10);
    border:1px solid var(--border-mid);
    backdrop-filter:blur(6px);
    -webkit-backdrop-filter:blur(6px);
    /* Behind the labels (z-index:1) so the active text shows ON TOP of
       the pane rather than disappearing behind it. The pane reads as the
       background of the active label rather than a lens covering it. */
    z-index:1;
  }
  /* Reel scroll area sits ABOVE the pane so labels are rendered on top. */
  .mreel{ z-index:2; }

  /* Overlay mode: the .mstack-close button sits at right:14px (44px wide),
     so the reel's frame would otherwise extend past the close button.
     Anchor the frame to left:16/right:64 (instead of the centred layout)
     so the pane stops cleanly before the button. */
  .mstack[data-overlay] .mreel-frame{
    left:16px;
    right:64px;
    width:auto;
    transform:translateY(-50%);
  }
  /* Also shift the scrollable reel so the active label centers WITHIN the
     offset frame instead of in the viewport. Without this, labels sit at
     viewport-center while the frame sits 24px to the left of viewport-
     center, making the active text look pushed to the right edge of the
     pane on subpages. Right inset of 48px = (close-button area 58px) − the
     frame's own right inset gap (10px), so label-center == frame-center. */
  .mstack[data-overlay] .mreel{
    right:48px;
  }
  /* Top + bottom fades — short (10px) so prev/next preview labels are
     clearly visible just outside the pane. Was 16-22px which masked
     most of the peek glyphs. The fade still smooths the wrap edge so
     the labels don't hard-clip. */
  .mreel-fade-top,.mreel-fade-bot{
    position:absolute;left:0;right:0;height:10px;
    z-index:3;pointer-events:none;
  }
  .mreel-fade-top{top:0;background:linear-gradient(to bottom,var(--bg-solid) 0%,rgba(250,250,249,0.0) 100%);}
  .mreel-fade-bot{bottom:0;background:linear-gradient(to top,var(--bg-solid) 0%,rgba(250,250,249,0.0) 100%);}

  /* Scrollable reel itself */
  .mreel{
    position:absolute;inset:0;
    overflow-y:auto;
    scroll-snap-type:y mandatory;
    -webkit-overflow-scrolling:touch;
    scrollbar-width:none;
  }
  .mreel::-webkit-scrollbar{display:none;}
  .mreel-item{
    /* Item shorter than the frame (32 vs 44) so the prev/next items
       sit closer to the active centre — their text glyphs land in the
       peek areas (just outside the pane on top + bottom). With items
       == frame, prev/next centres were 44px above/below the active
       centre, putting their glyphs entirely off-screen above/below
       the wrap. 32px brings them ~16px above/below the pane edge so
       previews are visible. */
    height:32px;
    display:flex;align-items:center;justify-content:center;
    scroll-snap-align:center;scroll-snap-stop:always;
    font-size:13px;letter-spacing:0.10em;
    color:var(--text-dim);
    transition:color 0.25s ease, transform 0.25s ease, font-size 0.25s ease, letter-spacing 0.25s ease, font-weight 0.25s ease;
    cursor:pointer;-webkit-tap-highlight-color:transparent;
    /* Suppress iOS long-press text-selection callout (Copy/Share popover)
       so our long-press → quick-jump grid handler runs cleanly. */
    user-select:none;-webkit-user-select:none;
    -webkit-touch-callout:none;
    flex-shrink:0;
  }
  /* Spacers so first/last items can be centered. Height = (mreel-wrap
     76px - mreel-item 32px) / 2 = 22px on each side. */
  .mreel-spacer{height:22px;flex-shrink:0;}
  /* Looking-glass enlargement: the label that lands inside the
     window-pane frame gets a transform-scale bump on top of its
     bigger font + tracking, so the text reads as if it's being
     magnified through a lens. The transition (declared on
     .mreel-item) animates the size + color jump as labels enter
     and leave the frame. */
  .mreel-item.cur{
    color:var(--text-bright);
    /* Active label sized to fit comfortably inside the 44px pane while
       leaving the prev/next labels visible just outside it. The +1px
       font + scale(1.10) + bold + bright color is enough to read as
       "selected"; bigger fonts overflowed the pane and pushed previews
       off the wrap entirely. */
    font-size:14px;letter-spacing:0.20em;
    font-weight:600;
    transform:scale(1.08);
    text-shadow:0 0 0.5px rgba(20,18,16,0.10);
  }
  /* Long-press feedback — label shrinks + dims while user holds, so they
     know the gesture is being detected. Transition matches LONG_PRESS_MS
     so it completes right as the grid opens. */
  .mreel-item.pressing{
    transform:scale(0.88);
    opacity:0.55;
    transition:transform 0.45s ease, opacity 0.45s ease;
  }
  /* Live position-in-section indicator next to active label, e.g. "(1/8)" */
  .mreel-count{
    font-size:0.65em;letter-spacing:0.04em;
    font-weight:400;
    opacity:0.55;margin-left:6px;
    font-variant-numeric:tabular-nums;
  }
  /* Slot-machine spin animation when section changes via tap */
  .mreel.spinning .mreel-item{
    transition:none;
    filter:blur(2px);opacity:0.7;
  }

  /* Live "1/8" position-in-section indicator, rendered INLINE next to the
     active reel label (replaces both the static (8) count and the right-edge
     mini-counter). Appears next to .mreel-item.cur only — see paintChrome().
     On intro the active label is 'works' but we hide this since the user
     isn't on a real work item yet. */

  /* Faint up/down chevrons next to the active reel label so the slot-machine
     reads as interactive. They sit just outside the active "frame" left/right. */
  .mreel-cue{
    /* Hidden by request — the slot reel pattern is self-explanatory and the
       arrow glyphs added visual noise on the left edge. */
    display:none;
    position:absolute;top:50%;
    transform:translateY(-50%);
    font-size:11px;line-height:1;
    color:var(--text-mid);
    opacity:0.55;pointer-events:none;
    z-index:4;
    transition:opacity 0.25s ease;
  }
  .mreel-cue.left{ left:14px; }
  .mreel-cue-stack{ display:flex;flex-direction:column;align-items:center;gap:2px; }

  /* Right-edge dot indicators: show position WITHIN the current section
     so the user has a map of how far through they are. Hidden on the
     intro card. */
  .mdots{
    position:absolute;right:6px;top:50%;
    transform:translateY(-50%);
    display:flex;flex-direction:column;align-items:center;gap:6px;
    z-index:7;pointer-events:none;
    transition:opacity 0.25s ease;
  }
  .mdots.hide{ opacity:0; }
  .mdot{
    width:5px;height:5px;border-radius:50%;
    background:rgba(250,250,249,0.30);
    transition:background 0.2s ease, transform 0.2s ease;
  }
  .mdot.on{
    background:rgba(250,250,249,0.95);
    transform:scale(1.25);
  }

  /* ── CARD STACK ────────────────────────────────────────────────────────── */
  /* No overflow:hidden + higher z-index than the reel so a swiping card
     visually passes OVER the slot-machine reel instead of being clipped by it. */
  .mcards{
    position:relative;flex:1;
    perspective:1200px;
    z-index:5;
  }
  .mreel-wrap{ z-index:1; }
  .mcard{
    position:absolute;left:16px;right:16px;
    /* Bottom value tuned so the user sees ACTIVE + 2 peeks fully above
       the dock pill, and the 3rd peek (pos 3) is ENTIRELY behind the
       pill — its 28px strip darkens through the frosted glass which
       the glow animation pulses against. Math:
         dock pill is (46 + 40 + 2-border + 14-bottom-margin) =
         102 + safe-bot from the bottom of the mstack.
         pos N peek bottoms sit at (X - 38, X - 70, X - 98) from bottom.
         For pos 2 fully above dock: X - 70 ≥ 102 + safe → X ≥ 172 + safe
         For pos 3 entirely behind dock: X - 70 ≤ 102 + safe → X ≤ 172 + safe
       Satisfied exactly at X = 172 + safe-bot — pos 2's bottom flush
       against the pill's top, pos 3's whole 28px peek strip behind the
       pill. */
    top:8px;bottom:calc(172px + env(safe-area-inset-bottom, 0px));
    border-radius:18px;
    overflow:hidden;
    /* Background matches page bg so the edges blend in during slide-in,
       lazy image load, or any moment the .mcard-media hasn't fully
       painted. .mcard-media itself stays dark — that's the art stage.
       No border — the artwork inside + drop shadow define the card edge. */
    background:var(--bg-solid);
    border:none;
    /* Lighter, warmer shadow — softer brown so it doesn't read heavy on the
     cream page bg. Two layers: a wide soft halo + a tight close shadow for
     edge definition. */
    box-shadow:0 18px 42px -20px rgba(70,55,42,0.18),
               0 3px 10px -4px rgba(70,55,42,0.10);
    will-change:transform,opacity;
    transition:transform 0.55s cubic-bezier(0.22,1,0.36,1),
               opacity 0.45s cubic-bezier(0.22,1,0.36,1);
    transform:translateY(120%);opacity:0;
    cursor:pointer;-webkit-tap-highlight-color:transparent;
  }
  /* Peek positions — each card behind the active one is offset & scaled down */
  .mcard[data-pos="0"]{transform:translateY(0) scale(1);opacity:1;z-index:5;}
  .mcard[data-pos="1"]{transform:translateY(38px) scale(0.94);opacity:0.92;z-index:4;}
  .mcard[data-pos="2"]{transform:translateY(70px) scale(0.88);opacity:0.70;z-index:3;}
  .mcard[data-pos="3"]{transform:translateY(98px) scale(0.82);opacity:0.45;z-index:2;}
  .mcard[data-pos="-1"]{transform:translateY(-110%) scale(0.96);opacity:0;z-index:1;}
  .mcard[data-pos="hidden"]{transform:translateY(120%) scale(0.8);opacity:0;z-index:0;pointer-events:none;}
  /* While dragging, disable transition for 1:1 finger tracking */
  .mcards.dragging .mcard{transition:none;}

  /* Card visual content (media area stays dark — it's the art stage) */
  .mcard-media{position:absolute;inset:0;background:#1a1a1a;}
  .mcard-media img,.mcard-media video{
    position:absolute;inset:0;width:100%;height:100%;
    object-fit:cover;display:block;
    /* Slightly favor upper portion of the frame on portrait crops so
       horizontally-composed works don't lose their center of interest.
       Per-card override possible via inline `--art-pos`. */
    object-position:var(--art-pos, center 38%);
  }
  /* The 'we-are-machines-made-for-dreaming' preview video has burned-in
     letterbox bars in the source file (the encoded frame includes a black
     border around the actual content), and object-fit:cover can't crop
     them off because they're part of the pixel data. Overscan the video
     element by ~10% so the bars push past the .mcard-media / .mjump-thumb
     edge — the rest of the frame still composes well at this scale because
     the underlying content is shot wide. Targeted by URL so other preview
     videos (which encode cleanly) keep their natural framing. */
  .mcard[data-url*="we-are-machines"] .mcard-media video,
  .mjump-cell[data-match-url*="we-are-machines"] .mjump-thumb video{
    transform:scale(1.10);
    transform-origin:center center;
  }
  .mcard-scrim{
    position:absolute;inset:0;
    background:linear-gradient(to bottom,
      rgba(0,0,0,0.42) 0%,
      rgba(0,0,0,0.05) 35%,
      rgba(0,0,0,0.15) 60%,
      rgba(0,0,0,0.85) 100%);
    pointer-events:none;
  }
  .mcard-label{
    position:absolute;left:22px;right:22px;top:22px;
    color:#f0eeeb;
    pointer-events:none;
  }
  /* Top meta row: card number on the LEFT, year on the RIGHT (flex
     space-between). Splitting them with the full card width between
     prevents '01 · 2024' from reading as a date like '01/2024'. */
  .mcard-num{
    display:flex;align-items:center;justify-content:space-between;
    font-size:12px;letter-spacing:0.22em;text-transform:uppercase;
    color:rgba(240,238,235,0.65);margin-bottom:8px;
  }
  .mcard-num .num{ display:inline-block; }
  .mcard-num .year{ display:inline-block; opacity:0.85; }
  .mcard-title{
    font-size:28px;font-weight:600;letter-spacing:-0.02em;line-height:1.05;
    color:#f0eeeb;margin-bottom:8px;text-transform:none;
  }
  .mcard-meta{
    font-size:13px;letter-spacing:0.10em;
    color:rgba(240,238,235,0.7);
  }
  .mcard-cta{
    position:absolute;left:22px;right:22px;bottom:22px;
    display:flex;align-items:center;justify-content:space-between;gap:14px;
    color:#f0eeeb;pointer-events:none;
  }
  .mcard-cta-text{
    font-size:13px;letter-spacing:0.20em;text-transform:uppercase;
    color:rgba(240,238,235,0.95);
  }
  /* Two-line CTA label on the intro card: primary "swipe to explore"
     and secondary "hold for grid" hint. Stacked in a column so the
     trailing arrow stays vertically centred against both lines via
     the parent .mcard-cta's align-items:center. */
  .mcard-cta-textwrap{
    display:flex;flex-direction:column;gap:2px;
  }
  .mcard-cta-hint{
    font-size:10px;letter-spacing:0.18em;text-transform:uppercase;
    color:rgba(240,238,235,0.55);
  }
  .mcard-cta-arrow{
    width:44px;height:44px;border-radius:50%;
    border:1px solid rgba(240,238,235,0.45);
    display:flex;align-items:center;justify-content:center;
    font-size:18px;color:#f0eeeb;
    background:rgba(240,238,235,0.10);
    backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  }
  /* The → glyph spans more horizontal pixels than ↑ at the same
     font-size, so the explore-card arrow reads as visually larger
     than the intro card's up-arrow. Shrink the right-arrow variant
     a notch so the two cards feel balanced. */
  .mcard-cta-arrow-right{ font-size:14px; }

  /* ── INTRO CARD (Plan Cult video + actual animated name/tagline) ───────── */
  /* Placeholder: the video is now deferred so the card can paint first.
     Without a poster, the .mcard-media area would be solid #1a1a1a — which
     would flash black before the video arrives. This subtle vertical gradient
     (matching the mood of the Plan Cult video) gives the card visual presence
     immediately, so deferring the video doesn't read as "broken" on slow
     networks or Low Power Mode. The video, once loaded, paints over this. */
  .mcard.intro .mcard-media{
    background:radial-gradient(ellipse at 50% 30%,
      #3b4452 0%,
      #232932 45%,
      #15181d 100%);
  }
  /* The intro card relies on a single .mcard-intro-blur element to mask the
     hardcoded subtitles AND tint the text-readable zone. The generic dark
     scrim is suppressed here so we don't stack multiple darkening / blurring
     layers over the Plan Cult video. */
  .mcard.intro .mcard-scrim{ display:none; }
  /* SINGLE bottom blur strip — does subtitle masking AND text-zone tinting
     with one element + one gradient mask. Was previously paired with the
     multi-stop .mcard-scrim which produced overlapping dark layers; now this
     is the only effect over the Plan Cult video.
     Real DOM element (.mcard-intro-blur) — iOS Safari is unreliable with
     backdrop-filter on ::before/::after inside transformed parents.
     translateZ(0) forces its own compositing layer. */
  .mcard.intro .mcard-intro-blur{
    position:absolute;left:0;right:0;bottom:0;
    height:42%;                             /* covers subtitle + name/CTA zone */
    pointer-events:none;
    z-index:2;
    /* 20px blur (>16px) smears the burned-in subtitles illegible. Subtle
       built-in dark tint replaces the deleted scrim so the name/tagline/CTA
       still have contrast against the video — no second element needed. */
    backdrop-filter:blur(20px) saturate(0.85);
    -webkit-backdrop-filter:blur(20px) saturate(0.85);
    background:linear-gradient(to bottom,
      rgba(0,0,0,0) 0%,
      rgba(0,0,0,0.22) 55%,
      rgba(0,0,0,0.55) 100%);
    /* One gradient mask: blur + tint fade in smoothly from the top of the
       strip and reach full strength before the subtitle row. */
    -webkit-mask-image:linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.85) 30%, black 55%);
    mask-image:linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.85) 30%, black 55%);
    -webkit-transform:translateZ(0);
    transform:translateZ(0);
  }
  /* Force the intro card to be its own stacking/compositing context so the
     backdrop-filter inside it is computed against the video below it. */
  .mcard.intro{ isolation:isolate; }
  /* The intro card hosts the existing #nameBtn (moved in via JS). The
     .tagline ("multidisciplinary visual artist & curator") is now hidden —
     see the .tagline rule below — so the host block only contains the
     animated S + 'eppe / de roo'. With the tagline gone there is room to
     drop the whole block lower on the card; bottom was 96px (which left the
     tagline ~24px above the .mcard-cta), now 72px so the bottom of the
     name lines up roughly where the tagline's bottom used to sit. The S
     and the 'eppe / de roo' text keep their own internal spacing/sizes
     (the .big-name + .sig-wrap rules below are unchanged), and left:22px
     keeps them aligned with the .mcard-cta and other card-edge content. */
  .mcard.intro .mcard-name-host{
    position:absolute;left:22px;right:22px;bottom:72px;
    pointer-events:auto;color:rgba(250,250,249,0.96);
    z-index:3;                              /* above the bottom blur strip */
    /* The hosted #nameBtn is an <a href="#"> — without these iOS Safari shows
       its native link-preview popup on long-press, which lifts/animates the
       visible S+name and competes with our jump-grid handler. */
    -webkit-touch-callout:none;
    -webkit-user-select:none;user-select:none;
  }
  .mcard.intro .mcard-name-host *{
    -webkit-touch-callout:none;
    -webkit-user-select:none;user-select:none;
  }
  .mcard.intro .mcard-name-host .big-name{
    /* Same size as old mobile menu: --fs-hero is 52px on ≤700px */
    font-size:52px;font-weight:600;line-height:0.88;letter-spacing:-0.03em;
    color:rgba(250,250,249,0.96);
    display:flex;align-items:flex-end;
    margin-bottom:4px;
    text-decoration:none;cursor:pointer;
  }
  /* Original old-menu ratios: ~113h × ~76w (taller than wide, aspect 0.7).
     The wrap is intentionally narrower than the webp's natural aspect — the
     base .sig-anim rule (line ~550) renders the image at full height, width
     auto, position absolute, and `overflow:hidden` here crops the sides. */
  .mcard.intro .mcard-name-host .sig-wrap{
    display:block;
    height:calc(52px * 2 * 1.05 + 4px);
    width:calc(52px * 2 * 1.05 * 0.7);
    flex-shrink:0;overflow:hidden;
    position:relative;                      /* needed: base .sig-anim is position:absolute */
  }
  /* Only override opacity — let the base .sig-anim rule (height:100%-4px;
     width:auto; position:absolute; centered) do the actual sizing. */
  .mcard.intro .mcard-name-host .sig-anim{
    opacity:0.93;
  }
  .mcard.intro .mcard-name-host .name-text{
    display:block;margin-left:-20px;        /* original old-menu offset */
    background:none!important;
    -webkit-text-fill-color:rgba(250,250,249,0.96)!important;
  }
  .mcard.intro .mcard-name-host .name-text-glow{display:none!important;}
  .mcard.intro .mcard-name-host .name-deroo{display:inline-block;padding-left:0.5em;}
  /* Tagline ("multidisciplinary visual artist & curator") removed from the
     mobile menu intro card on user request. Hidden via CSS rather than
     deleted from the DOM so the introHostStash markup + the existing JS
     that .appendChild()s it stay valid (move is a no-op visually). */
  .mcard.intro .mcard-name-host .tagline{
    display:none;
  }
  /* Make sure the CTA stays above the bottom blur strip */
  .mcard.intro .mcard-cta{ z-index:3; }
  /* Branded current-page card on a subpage: the .mcard-intro-blur strip is
     a backdrop-blur designed to mask the burned-in subtitles on the Plan
     Cult video. There are no subtitles on a static page thumbnail, and the
     blur was visibly distorting the bottom of the tulpas image — disable
     it for the branded variant. The .mcard-name-host has its own contrast
     against the image (text is rgba(250,250,249,0.96), the .sig-anim is a
     bright-white webp), so legibility is fine without the strip.
     EXCEPTION: when the card is .flipped to the Plan Cult video side, the
     subtitle-masking strip is needed again.
     TIMING: instant display:none on flip-back caused a "subtitles flash"
     because the Plan Cult video is still visible during the first ~350ms
     of the 700ms back-rotation (until the card crosses 90° edge-on and
     the front face takes over). The blur must stay opaque through that
     window. Use opacity transition with a 0.35s delay so the strip
     remains fully visible until the front face is showing, then fades
     out cleanly. visibility flips after the fade so the element doesn't
     intercept anything. */
  .mcard.intro[data-branded="1"] .mcard-intro-blur{
    opacity:1;
    visibility:visible;
  }
  .mcard.intro[data-branded="1"]:not(.flipped) .mcard-intro-blur{
    opacity:0;
    visibility:hidden;
    transition:opacity 0.15s 0.35s linear,
               visibility 0s 0.5s;
  }
  /* Flipping TO back side: blur should appear immediately so the
     subtitles are masked before the back face is revealed. */
  .mcard.intro[data-branded="1"].flipped .mcard-intro-blur{
    transition:opacity 0.1s linear, visibility 0s;
  }

  /* 3D MEDIA FLIP for the branded current-page card on subpages.
     The flipper wraps just the media layer; .mcard-scrim, .mcard-name-host,
     and .mcard-cta sit ABOVE the flipper and stay in place during the flip
     so the animated S, "eppe / de roo" tagline, "swipe to explore", and
     "hold for grid" hint are continuously visible across the rotation —
     looking like they're stable while the background turns. */
  .mcard.intro[data-branded="1"] .mcard-media-flipper{
    position:absolute;
    inset:0;
    perspective:1200px;
    transform-style:preserve-3d;
    transition:transform 0.7s cubic-bezier(0.42, 0, 0.34, 1);
  }
  .mcard.intro[data-branded="1"].flipped .mcard-media-flipper{
    transform:rotateY(180deg);
  }
  .mcard.intro[data-branded="1"] .mcard-media-face{
    position:absolute;
    inset:0;
    backface-visibility:hidden;
    -webkit-backface-visibility:hidden;
    overflow:hidden;
  }
  .mcard.intro[data-branded="1"] .mcard-media-back{
    transform:rotateY(180deg);
    /* Match the dark backdrop the homepage hero uses behind the Plan
       Cult video — both for the brief moment between flip-start and
       video first-frame, AND as a safety net if the video fails to load
       (network error, codec mismatch, etc.) so the card never reveals
       a blank/transparent face mid-flip. */
    background:radial-gradient(ellipse at 50% 30%, #3b4452 0%, #1a1a1a 65%);
  }
  .mcard.intro[data-branded="1"] .mcard-media-back video{
    position:absolute;
    inset:0;
    width:100%;
    height:100%;
    object-fit:cover;
  }

  /* ── DOCK at very bottom (two-row liquid-glass pill) ─────────────────────
     Subscribe gets the full row 1 (full text + glow pulse) so the long
     "subscribe to transmissions" string never has to fight about/contact
     for space. About + contact share row 2, split by a 1px hairline.
     Whole pill floats over the cards with breathing room around it. */
  /* A faint dark CARD-shaped rectangle behind the dock pill, sized like
     a peek card (slightly inset from the pill width, ~28px tall — about
     half the subscribe row). Positioned so its top edge aligns with the
     pill's top edge: through the pill's frosted glass it darkens just
     the SUBSCRIBE row of the pill, not the about/contact row below.
     Reads like the lowest card in the deck is tucked behind the upper
     part of the pill.
     (Tried removing this once the dock got its own dark filament fill —
     the look noticeably changed for the worse, so it's back. The rect
     adds depth that the dock's own bg alone doesn't fully replicate.) */
  .mstack::before{
    content:'';
    position:absolute;
    /* 16px inset from the pill's own edges, so it looks narrower than
       the pill (like a card behind a slightly-larger pill). */
    left:32px;right:32px;
    /* Bottom edge is 74px from .mstack bottom — pill bottom is at 14 +
       safe, pill top at 14 + safe + 88 = 102 + safe. Rect height 28,
       bottom at safe+74 → top at safe+102 = exactly pill top edge. */
    bottom:calc(env(safe-area-inset-bottom,0) + 74px);
    height:28px;
    background:rgba(30,24,18,0.55);
    border-radius:12px;
    z-index:9;         /* below .mdock (z:10) — pill is on top, dark below */
    pointer-events:none;
  }
  .mdock{
    position:absolute;
    left:16px;right:16px;
    bottom:calc(env(safe-area-inset-bottom,0) + 14px);
    padding:0;
    display:flex;flex-direction:column;align-items:stretch;
    z-index:10;pointer-events:auto;
    background:var(--glass-light-bg);
    border:1px solid var(--glass-light-border);
    box-shadow:var(--glass-light-shadow);
    backdrop-filter:var(--glass-filter);
    -webkit-backdrop-filter:var(--glass-filter);
    border-radius:22px;
    overflow:hidden;
  }
  /* While a side-panel overlay is open (.ov-panel — privacy /
     cookies / about / subscribe / booking), the .mdock is fully
     covered by the panel's full-width slide-in, so painting its
     24px backdrop-filter every frame is pure wasted GPU work.
     Suppressing it during opens keeps the slide-in composite-only.
     :has() is supported by iOS 17+ Safari and Chrome 105+; older
     browsers fall back to the always-on dock. */
  body:has(.overlay.open) .mdock{
    visibility:hidden;
  }
  /* Floating pill no longer needs the old flush-bottom gradient strip. */
  .mdock::before{display:none;}
  /* Subscribe — row 1, full width. Glow pulse animation draws attention. */
  .mdock .sub-link{
    flex:0 0 auto;
    height:46px;
    background:none;border:none;
    display:flex;align-items:center;justify-content:center;
    /* Subscribe reads as the headline action of the dock, so it
       sits one step above the 12px about/contact secondaries. */
    font-size:13px;letter-spacing:0.20em;text-transform:uppercase;
    font-weight:600;color:var(--text-bright);
    /* Asymmetric padding (left > right) shifts the flex-centred
       subscribe text a few px right so its visual centre lines up
       with the visual centre of the about/contact row below.
       "about" (5 chars) and "contact" (7 chars) each centre in
       their flex:1 cells — but the longer "contact" pulls the
       perceived weight rightward, making subscribe (geometrically
       centred at 50%) look slightly left of where it should sit. */
    padding:0 12px 0 20px;cursor:pointer;
    -webkit-tap-highlight-color:transparent;
    white-space:nowrap;position:relative;
    animation:dock-sub-glow 2.6s ease-in-out infinite;
    transition:background 0.2s;
  }
  /* Hairline below the subscribe row, inset slightly so it doesn't visually
     hit the rounded pill corners. */
  .mdock .sub-link::after{
    content:'';position:absolute;left:18px;right:18px;
    bottom:0;height:1px;background:var(--border-mid);
  }
  .mdock .sub-link:active{background:rgba(255,255,255,0.18);}
  /* Soft white-glow pulse — animates text-shadow only so it doesn't
     reflow or repaint the pill geometry. */
  @keyframes dock-sub-glow{
    0%,100%{
      text-shadow:0 0 0 rgba(255,255,255,0);
      color:var(--text-bright);
    }
    50%{
      text-shadow:0 0 14px rgba(255,255,255,0.65),
                  0 0 6px rgba(255,255,255,0.5);
      color:#fff;
    }
  }
  /* About + contact — row 2, two equal cells split by a centered hairline. */
  .mdock-row{
    flex:0 0 auto;
    display:flex;height:40px;align-items:stretch;
  }
  .mdock-row .footer-link{
    flex:1;
    background:none;border:none;
    display:flex;align-items:center;justify-content:center;
    /* Bumped 11→12px so about / contact read at the same
       scale as the subscribe link above and the about-overlay
       link pill (also 12px). */
    font-size:12px;letter-spacing:0.18em;text-transform:uppercase;
    color:var(--text-mid);text-decoration:none;
    padding:0 8px;cursor:pointer;
    -webkit-tap-highlight-color:transparent;
    white-space:nowrap;position:relative;
    transition:color 0.2s, background 0.2s;
  }
  .mdock-row .footer-link + .footer-link::before{
    content:'';position:absolute;left:0;top:25%;bottom:25%;
    width:1px;background:var(--border-mid);
  }
  .mdock-row .footer-link:active{
    color:var(--text-bright);
    background:rgba(255,255,255,0.18);
  }

  /* ── First-visit hint chip (animated swipe prompt) ───────────────────────
     Sits in the upper portion of the card area, just below the slot reel,
     so it reads as "this is how to interact with the cards" rather than
     floating near the dock. */
  .mhint{
    position:absolute;left:50%;
    top:calc(110px + env(safe-area-inset-top,0));
    bottom:auto;
    transform:translateX(-50%);
    display:flex;align-items:center;gap:8px;
    padding:9px 16px;border-radius:20px;
    background:rgba(40,38,36,0.78);
    border:1px solid rgba(40,38,36,0.18);
    color:rgba(250,250,249,0.92);
    font-size:11px;letter-spacing:0.20em;text-transform:uppercase;
    white-space:nowrap;pointer-events:none;
    z-index:11;
    backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
    opacity:0;transition:opacity 0.4s ease;
  }
  .mhint.show{opacity:1;}
  .mhint-arrow{font-size:13px;animation:mhint-bob 1.5s ease-in-out infinite;}
  @keyframes mhint-bob{0%,100%{transform:translateY(0);}50%{transform:translateY(-3px);}}
  /* Jump-grid variant of the hint chip — pinned to the bottom of the overlay
     (above the home indicator on iOS) so it floats over the grid without
     blocking the section header at the top. z-index high enough to sit above
     the cells but below the morphing ghost. */
  .mjump-hint{
    top:auto;
    bottom:calc(20px + env(safe-area-inset-bottom,0));
    z-index:42;
  }

  /* ── Quick-jump grid (long-press a reel label to open) ───────────────────
     Full-screen modal that overlays the entire .mstack. Shows thumbnail
     grid for the chosen section so the user can scan all items at once
     instead of swiping through. Tap a thumb → close + jump to that card. */
  .mjump-overlay{
    position:fixed;inset:0;z-index:40;
    /* Bumped from 0.96 → 0.99 to compensate for the dropped
       backdrop-filter — the underlying mstack content was already
       almost invisible behind 96% opaque white, so the blur was
       paying full GPU cost (273k px² × every fade frame = ~50%
       dropped frames per probe.js) for ~no visible benefit. The
       extra 3% opacity makes the overlay fully solid; mstack
       content underneath now doesn't paint while the grid is up. */
    background:rgba(250,250,249,0.99);
    display:flex;flex-direction:column;
    opacity:0;pointer-events:none;
    transition:opacity 0.25s ease;
  }
  .mjump-overlay.show{opacity:1;pointer-events:auto;}
  /* Sticky header: stays anchored above the scrollable grid with its OWN
     opaque background + bottom shadow, so cards scrolling underneath are
     fully obscured (no see-through) and the head reads as a clear separator
     above the thumbnails. z-index:2 keeps it above grid content during the
     morph animation. */
  .mjump-head{
    /* Floats absolutely over the top of the scrollable grid so cells
       can scroll UNDER the title pill + close button. Each pill carries
       its own backdrop-filter so cells passing behind get blurred,
       matching the iOS-style chrome elsewhere on the site. The
       .mjump-grid below adds top-padding equal to the head's height so
       cards never start clipped under the head. z-index bumped to 10
       so it clearly stacks above .mjump-num (z-index:2) and any other
       in-cell elements that might otherwise escape to the same level. */
    position:absolute;
    top:0;left:0;right:0;
    z-index:10;
    display:flex;align-items:center;
    gap:8px;
    padding:14px 14px 12px;
    padding-top:calc(14px + env(safe-area-inset-top,0));
    background:transparent;
  }
  .mjump-head::before{
    /* Invisible left spacer mirroring the close button's footprint so the
       title (flex:1, centered) ends up geometrically centered in the row.
       44px = .mjump-close width. */
    content:"";
    width:44px;height:44px;
    flex-shrink:0;
    visibility:hidden;
  }
  .mjump-head .mjump-title{
    /* iOS-style sticky-header pill: matches .mjump-close height (44px)
       and shares the liquid-glass surface treatment so the head reads
       as a balanced pair of pills. flex:0 1 auto + margin:auto means
       the pill auto-sizes to its text content and centers itself in
       the row between the left spacer and the close button. */
    flex:0 1 auto;
    margin:auto;
    display:inline-flex;
    align-items:center;
    height:44px;
    padding:0 22px;
    border-radius:999px;
    background:var(--glass-light-bg);
    border:1px solid var(--glass-light-border);
    box-shadow:var(--glass-light-shadow);
    backdrop-filter:var(--glass-filter);
    -webkit-backdrop-filter:var(--glass-filter);
    /* Tappable affordance — handler in cardstack.js closes the
       grid and jumps the cardstack to the section the pill is
       currently showing. */
    cursor:pointer;
    -webkit-tap-highlight-color:transparent;
    transition:opacity 0.18s ease, background 0.2s, border-color 0.2s;
  }
  .mjump-head .mjump-title:active{ opacity:0.6; }
  .mjump-head .mjump-close{
    /* No more position:absolute — flex aligns it for us. flex-shrink:0
       keeps the 44×44 size even on narrow screens. */
    flex-shrink:0;
  }
  .mjump-title{
    font-size:13px;letter-spacing:0.20em;text-transform:uppercase;
    color:var(--text-mid);font-weight:600;
    white-space:nowrap;
  }
  /* Mirror .ov-close exactly — same size, same border, same 1px-stroke X
     drawn via ::before/::after — so every close button on the site reads
     as the same component. The ✕ character in the markup gets hidden by
     font-size:0 / color:transparent (the aria-label on the <button>
     keeps it accessible). */
  .mjump-close{
    width:44px;height:44px;background:none;
    border:1px solid var(--border-mid);border-radius:50%;
    cursor:pointer;position:relative;flex-shrink:0;
    font-size:0;color:transparent;
    -webkit-tap-highlight-color:transparent;
    transition:all 0.2s;
  }
  .mjump-close::before,.mjump-close::after{
    content:'';position:absolute;top:50%;left:50%;
    width:16px;height:1px;background:var(--text-mid);transform-origin:center;
  }
  .mjump-close::before{transform:translate(-50%,-50%) rotate(45deg);}
  .mjump-close::after{transform:translate(-50%,-50%) rotate(-45deg);}
  .mjump-close:active{border-color:var(--text-mid);}
  .mjump-close:active::before,.mjump-close:active::after{background:var(--text-bright);}
  /* Outer container is now flex-column so section headers and per-section
     grids stack naturally; each section grid is a 2-col grid below its
     header. The whole thing scrolls as one continuous view. */
  .mjump-grid{
    flex:1;overflow-y:auto;
    /* Top padding clears the floating head (44px pill + 14px top padding
       + safe-area inset + 12px bottom padding ≈ 70px). scroll-padding-top
       matches so programmatic scrollTo lands the section header below
       the floating head instead of pinned underneath it. */
    padding:16px 16px 24px;
    padding-top:calc(env(safe-area-inset-top,0) + 80px);
    padding-bottom:calc(24px + env(safe-area-inset-bottom,0));
    scroll-padding-top:calc(env(safe-area-inset-top,0) + 80px);
    display:flex;flex-direction:column;gap:18px;
    -webkit-overflow-scrolling:touch;
  }
  /* Open-morph staging — when runMorphOpen begins (card → grid),
     it adds .open-morph-staging to .mjump-grid, then (after the
     ghost mounts) adds .staged-revealed to trigger the wave reveal.
     The wave is now CSS-driven via per-cell --mc-i / per-head --mh-i
     variables + transition-delay — this moves stagger timing from
     the main thread (where setTimeout calls bunched under load and
     produced clumped reveals) onto the compositor, so the wave is
     deterministic regardless of main-thread pressure.
       (1) transition always set on non-target cells while staging
           is active (so opacity/transform animate when reveal class
           is later added).
       (2) opacity:0 + transform applied only to non-revealed cells.
     This replaces ~12 cells × 3 inline style writes (= 36 style
     mutations in one frame, each forcing style invalidation on
     mobile Safari) with a single class swap. Stagger reveal happens
     by adding .revealed to each cell at the appropriate setTimeout,
     no longer needing inline style writes. */
  .mjump-grid.open-morph-staging .mjump-cell:not(.morph-target),
  .mjump-grid.open-morph-staging .mjump-section-head{
    transition:opacity 0.22s ease, transform 0.22s cubic-bezier(0.22,0.7,0.4,1);
  }
  .mjump-grid.open-morph-staging .mjump-cell:not(.morph-target){
    opacity:0;
    transform:translateY(8px) scale(0.97);
  }
  .mjump-grid.open-morph-staging .mjump-section-head{
    opacity:0;
  }
  /* Wave reveal — runMorphOpen adds .staged-revealed AFTER the ghost
     has mounted + painted. Per-cell delay = --mc-i × 32ms; per-head
     delay = --mh-i × 60ms. Browser handles the stagger on the
     compositor — no setTimeout bunching, no main-thread fragility. */
  .mjump-grid.open-morph-staging.staged-revealed .mjump-cell:not(.morph-target){
    opacity:1;
    transform:none;
    transition-delay:calc(var(--mc-i, 0) * 32ms);
  }
  .mjump-grid.open-morph-staging.staged-revealed .mjump-section-head{
    opacity:1;
    transition-delay:calc(var(--mh-i, 0) * 60ms);
  }
  .mjump-section-head{
    display:flex;align-items:baseline;gap:8px;
    margin-top:6px;
    padding:0 4px;
    border-bottom:1px solid var(--border);
    padding-bottom:8px;
    /* Tappable affordance — handler in cardstack.js closes the
       jump grid and jumps the cardstack to the first card of
       this section. */
    cursor:pointer;
    -webkit-tap-highlight-color:transparent;
    transition:opacity 0.18s ease;
  }
  .mjump-section-head:active{ opacity:0.55; }
  .mjump-section-head:first-child{ margin-top:0; }
  .mjump-section-name{
    font-size:14px;letter-spacing:0.18em;text-transform:uppercase;
    color:var(--text-bright);font-weight:600;
  }
  .mjump-section-count{
    font-size:11px;letter-spacing:0.10em;
    color:var(--text-dim);font-variant-numeric:tabular-nums;
  }
  .mjump-section-grid{
    display:grid;grid-template-columns:repeat(2,1fr);gap:14px;
  }
  .mjump-cell{
    /* Outer cell stays at full layout size. It carries a rounded
       translucent backdrop that becomes visible during .pressing —
       so the long-press "liquid glass" reveal follows the thumb's
       14px rounded shape instead of exposing the raw rectangular
       .mjump-overlay cream behind it. The inner press transform
       lives on .mjump-cell-inner below. */
    cursor:pointer;-webkit-tap-highlight-color:transparent;
    border-radius:14px;
    background:transparent;
    transition:background 0.5s ease;
  }
  .mjump-cell.pressing{
    /* The rounded "liquid glass" backdrop the user sees behind the
       shrunken thumb during long-press. Cream-tinted translucent so
       it reads as a glass card matching the rest of the menu's
       material vocabulary. The actual shrink + dim of the contents
       happens on .mjump-cell-inner so this backdrop stays at full
       cell size and shape. */
    background:rgba(255,255,255,0.55);
  }
  .mjump-cell-inner{
    display:flex;flex-direction:column;
    transform-origin:center center;
    transition:transform 0.15s ease, opacity 0.15s ease;
  }
  .mjump-cell:active .mjump-cell-inner{transform:scale(0.97);}
  /* Long-press feedback on the INNER wrapper — content shrinks + dims
     while the outer cell paints its rounded press backdrop. Same
     vocabulary as the reel labels: visible cue that the gesture is
     being detected. */
  .mjump-cell.pressing .mjump-cell-inner{
    transform:scale(0.92);
    opacity:0.7;
    transition:transform 0.5s ease, opacity 0.5s ease;
  }
  /* Drop the thumb's outer drop-shadow + 1px white border during press
     so it merges visually into the outer cream backdrop instead of
     reading as a second distinct rounded card stacked inside the
     first. The inset white highlights stay (they're inside the
     thumb's rounded clip and just give the image its glassy edge).
     Net effect: one rounded shape during press, not two. */
  .mjump-cell.pressing .mjump-thumb{
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.85),
      inset 0 3px 4px rgba(255,255,255,0.30),
      inset 0 -1px 0 rgba(255,255,255,0.10),
      inset 0 0 0 0.5px rgba(255,255,255,0.35);
    border-color:transparent;
    transition:box-shadow 0.5s ease, border-color 0.5s ease;
  }
  .mjump-thumb{
    width:100%;aspect-ratio:1/1;
    border-radius:14px;overflow:hidden;
    background:#1a1a1a;
    position:relative;
    /* Bright liquid-glass rim — light edge catches at the top of the
       thumb, soft white refractive ring all the way around. Bumped
       border opacity from 0.16 → 0.55 and the inset ring brightness
       so the rim reads as light, not dark. */
    border:1px solid rgba(255,255,255,0.55);
    box-shadow:
      0 8px 24px rgba(0,0,0,0.18),
      0 2px 4px rgba(0,0,0,0.08),
      inset 0 1.5px 0 rgba(255,255,255,0.85),
      inset 0 3px 4px rgba(255,255,255,0.30),
      inset 0 -1px 0 rgba(255,255,255,0.10),
      inset 0 0 0 0.5px rgba(255,255,255,0.35);
    transition:transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
  }
  .mjump-cell:hover .mjump-thumb,
  .mjump-cell:active .mjump-thumb{
    border-color:rgba(255,255,255,0.85);
    box-shadow:
      0 6px 16px rgba(0,0,0,0.16),
      inset 0 1.8px 0 rgba(255,255,255,0.95),
      inset 0 4px 6px rgba(255,255,255,0.40),
      inset 0 -1px 0 rgba(255,255,255,0.18),
      inset 0 0 0 0.5px rgba(255,255,255,0.55);
  }
  .mjump-thumb img,.mjump-thumb video{
    position:absolute;inset:0;width:100%;height:100%;
    object-fit:cover;display:block;
    object-position:var(--art-pos, center 38%);
  }
  .mjump-num{
    position:absolute;top:8px;left:10px;z-index:2;
    font-size:10px;letter-spacing:0.16em;color:rgba(240,238,235,0.95);
    /* Stronger text-shadow stack — single 1 px shadow was being
       overwhelmed when the image content directly behind the
       number was also light (e.g. the seppelin card has white
       spheres at the top-left where the "07" sits, making the
       number read as blurred low-contrast text). Multi-layer
       dark shadow gives the number a guaranteed contrast halo
       regardless of image content. */
    text-shadow:
      0 0 2px rgba(0,0,0,0.85),
      0 1px 4px rgba(0,0,0,0.65),
      0 0 8px rgba(0,0,0,0.45);
  }
  .mjump-text{
    padding:8px 4px 0;
    display:flex;flex-direction:column;gap:2px;
  }
  .mjump-tt{
    font-size:13px;line-height:1.25;letter-spacing:-0.01em;
    color:var(--text-bright);font-weight:600;
  }
  .mjump-yr{
    font-size:10px;letter-spacing:0.18em;color:var(--text-dim);
    text-transform:uppercase;
  }
  /* Overlay-mode close button. Only rendered when the mstack is in overlay
     mode (subpages). Mirrors .mjump-close visually so the close vocabulary is
     consistent across the menu and the jump grid. */
  .mstack-close{
    display:none;
    position:absolute;
    /* Vertically center with the slot-machine active line. The reel container
       (.mreel-wrap) is 88px tall, anchored at margin-top:safe-area-inset-top.
       Its visual center sits at safe + 44; a 44px-tall button centers there
       when its top = safe + 22 — close ↔ active reel frame share the same
       horizontal axis. */
    top:calc(env(safe-area-inset-top,0) + 22px);
    right:14px;
    z-index:60;
    width:44px;height:44px;background:rgba(40,38,36,0.10);
    border:1px solid var(--border-mid);border-radius:50%;
    cursor:pointer;
    font-size:0;color:transparent;
    -webkit-tap-highlight-color:transparent;
    backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
    transition:background 0.2s,border-color 0.2s;
  }
  .mstack-close::before,.mstack-close::after{
    content:'';position:absolute;top:50%;left:50%;
    width:16px;height:1px;background:var(--text-mid);transform-origin:center;
  }
  .mstack-close::before{transform:translate(-50%,-50%) rotate(45deg);}
  .mstack-close::after{transform:translate(-50%,-50%) rotate(-45deg);}
  .mstack-close:active{background:rgba(40,38,36,0.18);border-color:var(--text-mid);}
  .mstack-close:active::before,.mstack-close:active::after{background:var(--text-bright);}
  .mstack[data-overlay] .mstack-close{display:block;}


  /* When the jump grid is open, hide the overlay-mode close button on the
     menu so we don't end up with two stacked X's (the jump grid has its own
     close in its sticky header). Was using :has() — lands on Safari 15.4+
     so older devices showed both buttons (visibly misaligned: .mstack-close
     sits at safe+22px, .mjump-close is centered in the .mjump-head row at
     safe+~36px — they read as a stacked pair). Now driven by a JS-toggled
     class (.mjump-open) added in openJumpGrid / removed in closeJumpGrid:
     guaranteed to work in every browser.
     Was using `display:none` — but the close button has backdrop-filter:
     blur(6px), and toggling display causes iOS Safari to throw away its
     backdrop composite and rebuild it on re-show, which appears as a
     visible flash of the X (~1 frame of unstyled appearance) every time
     the user closes the jump grid back to the deck. Using opacity +
     visibility instead keeps the element in the render tree continuously,
     so the backdrop-filter stays composited and there's no re-paint
     hitch. pointer-events:none ensures the invisible button can't catch
     stray touches that the .mjump-close header button is meant to handle. */
  .mstack[data-overlay].mjump-open .mstack-close{
    opacity:0;
    visibility:hidden;
    pointer-events:none;
  }
}

@media(max-width:340px){
  /* Narrow screens (iPhone SE / mini): trim pill margins + shrink labels. */
  .mdock{ left:12px; right:12px; }
  .mdock .sub-link{ height:42px; font-size:11px; letter-spacing:0.16em; }
  .mdock-row{ height:36px; }
  .mdock-row .footer-link{ font-size:10px; letter-spacing:0.14em; padding:0 6px; }
  .mreel-item{ font-size:12px; letter-spacing:0.08em; }
  .mreel-item.cur{ font-size:13px; letter-spacing:0.14em; }
  .mreel-cue{ font-size:10px; }
  .mreel-cue.left{ left:10px; }
  .mcard{ left:12px; right:12px; bottom:calc(168px + env(safe-area-inset-bottom, 0px)); }
  .mcard-cta{ left:16px; right:16px; bottom:18px; }
  .mcard-cta-arrow{ width:38px; height:38px; font-size:16px; }
  .mcard-cta-arrow-right{ font-size:12px; }
  .mhint{ font-size:10px; padding:8px 12px; top:calc(100px + env(safe-area-inset-top,0)); }
}


/* ── SENDER.NET SUBSCRIPTION FORM (lives inside #ov-subscribe) ── */
.sdr-form-field{margin-bottom:10px;}
.sdr-form-field input{
  background:rgba(var(--r),var(--g),var(--b),0.08);
  color:var(--text-bright);
  border:1px solid var(--border-mid);
  border-radius:0;
  font-size:16px;
  padding:16px 18px;
  letter-spacing:0.06em;
  width:100%;
  box-sizing:border-box;
}
.sdr-form-field input::placeholder{color:var(--text-dim);}
.sdr-form-field input:focus{border-color:var(--acc-strong);outline:none;}
.sdr-privacy-note{
  font-size:13px;color:var(--text-bright);
  line-height:1.8;margin-bottom:20px;opacity:0.85;
}
.sdr-privacy-note a{
  color:var(--text-bright);
  border-bottom:1px solid var(--border-mid);text-decoration:none;
  opacity:0.85;
}
.sdr-what-label{
  display:block;font-size:13px;letter-spacing:0.08em;
  color:var(--text-bright);font-weight:400;
  margin:24px 0 10px 0;opacity:0.7;
}
.sdr-what-desc{
  font-size:14px;
  color:var(--text-bright);
  opacity:0.85;line-height:1.8;display:block;
  margin-bottom:20px;
}
.sdr-groups{margin-bottom:20px;margin-top:4px;}
.sdr-group-row{margin-bottom:14px;display:flex;align-items:center;}
.sdr-group-row label{display:flex;align-items:center;gap:12px;cursor:pointer;margin:0;}
.sdr-group-row input[type="checkbox"]{
  width:18px;height:18px;margin:0;
  flex-shrink:0;cursor:pointer;
  appearance:none;-webkit-appearance:none;
  background:rgba(var(--r),var(--g),var(--b),0.08);
  border:1px solid var(--border-mid);
  border-radius:0;
  position:relative;
  transition:all 0.2s;
}
.sdr-group-row input[type="checkbox"]:checked{
  background:var(--acc-muted);
  border-color:var(--acc-strong);
}
.sdr-group-row input[type="checkbox"]:checked::after{
  content:'';
  position:absolute;
  left:50%;top:50%;
  transform:translate(-50%,-50%);
  width:8px;height:8px;
  background:var(--acc);
}
.sdr-group-row .sdr-label{
  font-size:15px;color:var(--text-bright);
  font-weight:400;font-style:normal;
  letter-spacing:0.06em;margin:0;opacity:0.9;
  line-height:1.5;
}
.sdr-submit-btn{
  background:transparent;
  border:1px solid var(--border-mid);
  border-radius:0;
  color:var(--text-bright);
  font-size:16px;font-weight:500;
  letter-spacing:0.18em;
  padding:18px 20px;
  width:100%;
  transition:all 0.35s;
  margin-top:6px;
  opacity:0.9;
  cursor:pointer;
}
.sdr-submit-btn:hover{
  background:var(--acc-muted);
  border-color:var(--acc-strong);
  color:var(--text-bright);
  opacity:1;
}
.sdr-submit-btn:disabled{opacity:0.5;cursor:not-allowed;}
.sdr-success{display:none;}
.sdr-success h4{
  font-size:18px;font-weight:500;
  color:var(--text-bright);
  letter-spacing:0.06em;
  margin-bottom:8px;
}
.sdr-success p{
  font-size:14px;
  color:var(--text-mid);
}
@media(max-width:700px){
  /* iOS Safari auto-zooms <input> when font-size < 16px and never zooms back out
     after submit — keep at 16px on mobile to prevent the zoom entirely. */
  .sdr-form-field input{font-size:16px;padding:18px 20px;}
  .sdr-privacy-note{font-size:15px;line-height:1.7;}
  .sdr-what-label{font-size:15px;letter-spacing:0.08em;}
  .sdr-what-desc{font-size:15px;line-height:1.7;}
  .sdr-group-row{margin-bottom:16px;}
  .sdr-group-row input[type="checkbox"]{width:22px;height:22px;}
  .sdr-group-row input[type="checkbox"]:checked::after{width:10px;height:10px;}
  .sdr-group-row .sdr-label{font-size:15px;line-height:1.5;}
  .sdr-submit-btn{font-size:15px;font-weight:500;padding:20px 22px;opacity:1;}
}

/* ── ACQUISITION OVERLAY (#ov-acquire) ──────────────────────────
   Reuses the shared overlay chassis (.overlay/.ov-panel/.ov-body/
   .ov-top) plus the subscribe typography (.sub-big/.sub-intro).
   Only the personal note + the enquire CTA need their own rules. */
.acq-note{
  font-size:14px;
  line-height:1.8;
  color:var(--text-mid);
  letter-spacing:0.03em;
  margin-bottom:18px;
}
/* Per-work context line, populated by openAcquire(work). */
.acq-work{
  font-size:14px;
  font-style:italic;
  letter-spacing:0.03em;
  color:var(--text-bright);
  opacity:0.85;
  margin-bottom:16px;
}
/* Per-work CTA injected into the work-page header-left (under the title +
   medium) by shared.js. A quiet text link in the .series-readmore family,
   nudged a touch brighter than read-more so it reads as the action it is,
   rather than a heavy bordered box jammed beside the description. */
.work-acquire{
  display:block;
  width:100%;
  margin:64px 0 28px;
  text-align:center;
  background:none;border:none;padding:0;
  -webkit-appearance:none;appearance:none;
  -webkit-tap-highlight-color:transparent;
  font-size:var(--fs-small);
  letter-spacing:0.20em;
  text-transform:lowercase;
  color:var(--text-mid);
  cursor:pointer;
  transition:color 0.25s;
}
.work-acquire:hover{color:var(--text-bright);}
/* No dark fill on press/focus — it's a quiet text link, so the only
   feedback is the colour shift (the <button> default + mobile tap
   highlight were painting a grey box on click). */
.work-acquire:active,
.work-acquire:focus{background:none !important;outline:none;color:var(--text-bright);}
/* The enquire CTA is a liquid-glass filament chip (same family as the menu
   button / book chip — see the --fil-fam-* group and filament.js map).
   Filament supplies the surface, brackets and bloom; this rule only sets
   sizing/typography. No border/background here or it would fight the chip. */
.acq-enquire{
  display:inline-block;
  font-size:13px;font-weight:500;
  letter-spacing:0.16em;
  padding:11px 26px;
  text-decoration:none;
  cursor:pointer;
}
@media(max-width:700px){
  .acq-note{font-size:15px;}
  .acq-work{font-size:15px;}
  .acq-enquire{display:inline-block;width:auto;text-align:center;padding:12px 28px;font-size:13px;}
}
/* ─────────────────────────────────────────────────────────────────
   SUBPAGE .main CONTAINER — pushes content right of the bar-sliver
   so the page-header (left column = .header-left holding .series-title)
   isn't hidden under chrome.js's left-bar sliver. Originally inline in
   each subpage; lost from 5 of them during mega-extraction, which made
   the title appear "missing" on tulpas / ascetics / compositions /
   genesis / machines. Hoisted scoped to subpages so any new subpage
   inherits automatically.
   ───────────────────────────────────────────────────────────────── */
body[data-page="sub"] .main{
  padding-left:var(--sliver);
  transition:padding-left 0.42s cubic-bezier(0.25,0.46,0.45,0.94);
  position:relative;
  z-index:1;
}
/* OPTION-3 morph isolation (v=221). The sticky header is in normal
   flow, so when it animates smaller the entire works area below it
   (.main-body — the masonry grid + book, hundreds of nodes) gets
   repositioned every frame of the morph. `contain:layout` lets the
   browser treat .main-body's interior as self-contained: it slides as
   one block instead of re-flowing its children, which is what makes
   the animated size morph affordable. (Reverted in an earlier round
   because the membrane SVG filter was the dominant cost then and this
   alone changed nothing; now that the filter is deferred during the
   morph, this containment is the piece that lets the geometry animate
   without dragging the page. No visual effect.) */
body[data-page="sub"] .main-body{
  contain:layout;
}
body[data-page="sub"] .left-bar.open ~ .main{padding-left:var(--bar-w);}
@media(min-width:701px){
  html.bar-preopen body[data-page="sub"] .main{padding-left:var(--bar-w);}
}
@media(max-width:700px){
  html:has(body[data-page="sub"]),body[data-page="sub"]{height:100%;overflow:hidden;}
  body[data-page="sub"] .main{
    padding-left:0;
    padding-top:0;
    padding-bottom:calc(env(safe-area-inset-bottom, 20px) + 100px);
    height:100dvh;
    overflow-y:auto;
    overflow-x:hidden;
    /* No top margin — the header starts at viewport y=0 and only the
       safe-area inset (handled by header-left padding) holds content
       clear of the status bar. (The old 58px reserved a top menu zone
       that no longer exists; the !important override below also zeroed
       it, but keep the base consistent.) */
    margin-top:0;
    transition:none;
  }
}



/* ─────────────────────────────────────────────────────────────────
   SUBPAGE PAGE-HEADER (shared across all subpages)
   Hoisted from inline styles in 11 subpages.
   ───────────────────────────────────────────────────────────────── */
body[data-page="sub"] .header-right-mobile{display:none;}
body[data-page="sub"] .page-header-sticky{
  position:sticky;top:0;z-index:50;
  padding:0;
  will-change:transform;
  /* Suppress iOS Safari's default rectangular gray tap-highlight
     overlay — it draws a hard rectangle over the rounded compact
     pill and over the .header-left/.header-right bg blur, looking
     like a paint glitch. The pill itself is .compact-tappable for
     opening the mobile menu, which is why a touch action triggers
     the highlight in the first place. */
  -webkit-tap-highlight-color:transparent;
  /* Fade the sticky in/out alongside the overscroll-preview card.
     Without this the membrane on the compact pill deactivates the
     instant 'overscroll' takes top floor (recompute clears .active),
     which is BEFORE the card has finished sliding up to cover it —
     leaving a brief moment where the sticky is bare and the user
     sees the membrane vanish behind the rising card. Matching the
     fade duration to the overlay's transition (~0.35s) hands off
     the visual cleanly. */
  transition:opacity 0.35s ease;
}
body[data-page="sub"]:has(#opv-card.fullscreen) .page-header-sticky{
  opacity:0;
  pointer-events:none;
}
/* (Earlier round added a `:has(.mstack[data-overlay].show)
   .page-header-sticky { opacity:0 }` cross-fade so the sticky
   pill fade'd back in alongside the menu fade-out. The user
   reported it MADE the close transition janky — the simultaneous
   opacity-out on the mstack and opacity-in on the sticky pill
   were two re-composites running in parallel on the same screen
   area. Removed; the sticky just re-appears underneath the mstack
   as the mstack alpha falls, no extra animation needed.) */
/* Full-bar (non-compact) treatment — bg + blur + bottom seam.
   Scoped to :not(.compact) so the compact state is naturally
   transparent (the injected .pill-surface from membrane.js takes
   over the visual). No !important needed; no override needed in
   membrane.css. */
body[data-page="sub"] .page-header-sticky:not(.compact){
  background:var(--bg-panel);
  backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
  border-bottom:1px solid var(--border-mid);
}
body[data-page="sub"] .page-header{display:grid;grid-template-columns:1fr 1fr;}
/* v=88 — KILL THE INHERITED FILAMENT-PILL TEXT-SHADOW HALO.
   .filament-pill chassis (added by filament.js to .page-header-sticky)
   sets a 4-layer text-shadow (4/10/24/50 px) for the emissive
   "lit" look on close buttons and small pills. text-shadow is an
   INHERITED property so it cascades into every descendant text
   element of the sticky — .series-title, .series-meta, .series-desc.
   The result: the full-state sticky header title had a continuous
   bloom halo behind the text. Cosmetically unnecessary, AND it
   added significant per-frame cost during the morph (text re-glyph
   PLUS 4-layer shadow re-rasterization at each new font-size step).
   Reset to none on the sticky-header's text descendants. The
   .compact rule below has its own text-shadow:0 1px 0 ... which
   wins via specificity when .compact is applied, so compact-state
   text-shadow is preserved.) */
body[data-page="sub"] .page-header-sticky .series-title,
body[data-page="sub"] .page-header-sticky .series-meta,
body[data-page="sub"] .page-header-sticky .series-desc{
  text-shadow:none;
}

/* v=89 — Same fix on the sidebar snav-items. .snav-item.fil-glass
   ALSO gets .filament-pill via filament.js's apply() (the glass
   variant adds BOTH .filament-pill AND .fil-glass). The text-shadow
   halo inherits into the row text descendants — every hover or
   row-transition paints the 4-layer shadow on the text. Removing
   here makes sliding-mouse-across-the-list much cheaper.
   Targets the four text children that the snav-item layout uses:
   .snav-num, .snav-name, .snav-year, .snav-explore (+ inner). */
.snav-item .snav-num,
.snav-item .snav-name,
.snav-item .snav-year,
.snav-item .snav-explore,
.snav-item .snav-explore-inner{
  text-shadow:none;
}
body[data-page="sub"] .header-left{padding:28px 32px 22px;border-right:1px solid var(--border-mid);transition:padding 0.35s ease;}
body[data-page="sub"] .series-title{
  font-size:var(--fs-h1);font-weight:600;color:var(--text-bright);
  letter-spacing:-0.02em;line-height:0.88;margin-bottom:10px;
  background:none;border:none;cursor:default;padding:0;
  text-align:left;display:block;width:100%;
  /* OPTION-3 (v=221): animated geometry morph RESTORED. The shrink
     itself is cheap (a few text nodes) — what made it jank was that
     this sticky header is in normal flow, so shrinking it repositioned
     the entire .main-body below every frame. That reflow is now made
     near-free by `contain:layout paint` on .main-body (see the .main
     block) + `contain:layout` on the sticky tree, so the size can
     animate on its own subtree without dragging the page with it. */
  transition:font-size 0.35s ease, margin-bottom 0.35s ease, color 0.35s ease;
}
@media(max-width:700px){
  body[data-page="sub"] .series-title{cursor:pointer;}
  body[data-page="sub"] .series-title:hover{color:var(--text-bright);opacity:0.8;}
}
body[data-page="sub"] .series-meta{font-size:var(--fs-small);letter-spacing:0.12em;color:var(--text-dim);line-height:1.8;transition:font-size 0.35s ease, color 0.35s ease;}
body[data-page="sub"] .header-right{padding:28px 32px;transition:padding 0.35s ease;}
body[data-page="sub"] .series-desc{font-size:var(--fs-body);line-height:1.9;color:var(--text-mid);transition:font-size 0.35s ease, color 0.35s ease;}
/* COMPACT HEADER on scroll (desktop) */
@media(min-width:701px){
  body[data-page="sub"] .page-header-sticky.compact .header-left{padding:10px 32px 8px;}
  body[data-page="sub"] .page-header-sticky.compact .series-title{font-size:26px;line-height:1;margin-bottom:3px;}
  body[data-page="sub"] .page-header-sticky.compact .series-meta{font-size:10px;}
  body[data-page="sub"] .page-header-sticky.compact .header-right{padding:10px 32px;}
  body[data-page="sub"] .page-header-sticky.compact .series-desc{font-size:13px;line-height:1.55;
    display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
}
/* COMPACT TEXT COLOR — switch to cream-white over the dark membrane
   surface (rgba(20,15,10,0.42)) when the sticky becomes a pill.
   Same value the source spec uses for .pill-text plus the
   .pill-title text-shadow. The transition is on the base rules
   above (color 0.35s ease) so the dark→cream switch animates
   smoothly along with font/padding shrinks. Applies to BOTH desktop
   and mobile compact states (no @media wrap). */
body[data-page="sub"] .page-header-sticky.compact .series-title,
body[data-page="sub"] .page-header-sticky.compact .series-meta,
body[data-page="sub"] .page-header-sticky.compact .series-desc,
/* .header-book-btn — the equilibrium page's "book class level 1"
   CTA in the right column of the sticky header. Stays dark in
   compact state by default because its own color rule sets
   var(--text-bright). Including it here so it switches to white
   over the dark membrane surface, same as the title/meta/desc
   on its left side of the pill. */
body[data-page="sub"] .page-header-sticky.compact .header-book-btn{
  /* DESKTOP: pure white (user originally requested white when
     reporting the yellowish cream as off). */
  color:rgba(255,255,255,0.95) !important;
  -webkit-text-fill-color:rgba(255,255,255,0.95) !important;
  text-shadow:0 1px 0 rgba(0,0,0,0.20);
}
/* Animated underline pulse switches to white when the sticky
   header is compacted (matches the text-colour swap above).
   In the full-state the underline uses var(--acc) which reads
   correctly against the cream-page backdrop; in compact state
   the button sits over the dark membrane surface so the accent
   colour disappears — needs to become white. */
body[data-page="sub"] .page-header-sticky.compact .header-book-btn::after{
  background:rgba(255,255,255,0.95) !important;
}
/* MOBILE: keep the original cream colour on the sticky header.
   User wanted cream specifically on the mobile compact pill. */
@media(max-width:700px){
  body[data-page="sub"] .page-header-sticky.compact .series-title,
  body[data-page="sub"] .page-header-sticky.compact .series-meta,
  body[data-page="sub"] .page-header-sticky.compact .series-desc{
    color:rgba(245,238,225,0.95);
    -webkit-text-fill-color:rgba(245,238,225,0.95);
  }
}
/* COMPACT HEADER on scroll (mobile) — header stays at the TOP of
   the viewport at all times; on scroll past 60px it MORPHS from a
   full-width sticky bar into a floating pill in place. No bottom-
   fly any more — the user wanted the header to stay anchored to
   the top.

   The morph is animated via transitions on the few properties that
   can be interpolated (margin, border-radius, padding, background,
   box-shadow, border-color). Width "shrink" is achieved through
   margin transitions (margin:0 → margin:0 14px) which animate.
   We keep position:sticky throughout so the layout is identical
   in both states; .compact only changes the visual treatment. */
@media(max-width:700px){
  /* Mobile compact transition tuned for snappier feel — 0.28s with
     a "fast-out, soft-end" cubic curve so most of the visual movement
     happens in the first 60% of the timeline. The earlier 0.42s
     transition felt laggy because the layout-affecting properties
     (margin, top, padding, font-size, line-height) all run
     simultaneously and Safari has to re-lay-out the header subtree
     on every frame; a shorter window cuts the perceived jank
     dramatically without losing the morph effect.
     We also promote to a compositor layer via translateZ(0) so the
     pill's pixel data is cached between paints, leaving the layout
     engine free to handle just the geometry deltas. */
  body[data-page="sub"] .page-header-sticky{
    transition:
      margin 0.28s cubic-bezier(0.4,0,0.2,1),
      top 0.28s cubic-bezier(0.4,0,0.2,1),
      transform 0.28s cubic-bezier(0.4,0,0.2,1),
      border-radius 0.28s cubic-bezier(0.4,0,0.2,1),
      background 0.22s ease,
      box-shadow 0.22s ease,
      border-color 0.22s ease;
    will-change:margin, top, transform, border-radius;
    transform:translateZ(0);
  }
  /* .no-anim — applied briefly on menu OPEN to snap the sticky out
     of compact state without animating (the menu covers the sticky
     so the snap is invisible to the user). Disables transitions on
     the sticky AND its animated children for one frame, then is
     removed so the menu-CLOSE re-compact animates normally. */
  body[data-page="sub"] .page-header-sticky.no-anim,
  body[data-page="sub"] .page-header-sticky.no-anim .header-left,
  body[data-page="sub"] .page-header-sticky.no-anim .series-title,
  body[data-page="sub"] .page-header-sticky.no-anim .series-meta{
    transition:none !important;
  }
  /* Compact pill — pinned at the top of the viewport.
     IMPORTANT: with position:sticky the stuck position is anchored by
     `top:` (set to 0 on the base rule), NOT by margin-top. Pushing the
     pill down with margin-top alone has no effect once it's stuck —
     it only affects in-flow position. So we override `top:` to
     safe-area + 22px instead, which actually shifts the stuck
     position. Side margins still come from the margin shorthand. */
  /* Compact pill — pinned at the top of the viewport. The visual
     treatment (bg / border / shadow / clip / blur / filter) is ALL
     provided by the .pill-surface injected by membrane.js, which is
     the SINGLE clipping layer (matches the source spec where .pill
     alone has overflow:hidden + border-radius + filter:url; .pill-wrap
     parent is just positioning). NO overflow:hidden here — adding
     a second clipping layer at the parent broke Safari's compositor
     handling of filter:url and produced a rectangular halo around
     the rounded pill. border-radius stays so layout/spacing reads
     correctly; the surface inherits it. */
  body[data-page="sub"] .page-header-sticky.compact{
    /* Non-cover viewport already clears the status bar, so no
       env(safe-area-inset-top) needed. Top gap matches the 16px side
       margins so the floating pill is evenly inset on all sides. */
    top:16px !important;
    margin:0 16px 0 !important;
    border-radius:28px;
    z-index:50;
  }
  /* Bottom-docked variant — toggled by the drag handler in subpage.js.
     Position stays `sticky` (same as the top-dock state) so the only
     thing changing between the two states is transform — fully
     interpolatable, so the slide between top and bottom animates
     cleanly via the transition rule above. The translateY value is
     calculated so the pill's bottom edge ends up at viewport-height
     minus the bottom safe-area inset minus 14px breathing room.
       100dvh   = full viewport
       100%     = the pill's own height
       safe-top + safe-bot = system insets
       28px     = top inset (14) + bottom inset (14)
     The previous `position:fixed` swap made the slide instantaneous
     because `position` is not animatable — that was the glitch. */
  body[data-page="sub"] .page-header-sticky.compact.dock-bottom{
    transform:translateY(calc(
      100dvh
      - 100%
      - env(safe-area-inset-top, 0px)
      - env(safe-area-inset-bottom, 0px)
      - 28px
    ));
  }
  body[data-page="sub"] .page-header-sticky.compact.is-dragging{
    transition:none !important;
    cursor:grabbing;
  }
  body[data-page="sub"] .page-header-sticky.compact .header-left{
    padding:10px 20px 8px !important;
    border-bottom:none !important;
    background:transparent !important;
    transition:padding 0.28s cubic-bezier(0.4,0,0.2,1);
  }
  body[data-page="sub"] .page-header-sticky.compact .series-title{
    font-size:22px !important; line-height:1 !important; margin-bottom:2px !important;
  }
  /* Children animate in lockstep with the sticky parent — same 0.28s
     window. Font-size/line-height/padding/margin all touch layout, so
     keep the duration consistent or the morph desynchronizes. */
  body[data-page="sub"] .page-header-sticky .header-left,
  body[data-page="sub"] .page-header-sticky .series-title,
  body[data-page="sub"] .page-header-sticky .series-meta{
    transition:font-size 0.28s cubic-bezier(0.4,0,0.2,1),
               padding 0.28s cubic-bezier(0.4,0,0.2,1),
               margin 0.28s cubic-bezier(0.4,0,0.2,1),
               line-height 0.28s cubic-bezier(0.4,0,0.2,1);
  }
  body[data-page="sub"] .page-header-sticky.compact .series-meta{font-size:11px !important;}
  body[data-page="sub"] .page-header-sticky.compact .header-right-mobile{
    display:none !important;
  }
}

/* MOBILE responsive page-header */
@media(max-width:700px){
  /* Same backdrop blur on the sticky header as desktop. The previous
     transparent-+-solid-bg-on-children setup looked flat compared to
     the desktop's frosted-glass treatment. We blur on the sticky
     wrapper itself and let .header-left / .header-right-mobile remain
     translucent so content scrolling behind them gets the same blur
     pass as on desktop. The header-left's top padding is bumped to
     clear the floating menu pill (which lives at top:safe+7 height:44),
     so the pill sits inside the header chrome — and because the header
     itself is what's drawing the blur, the area immediately around AND
     under the pill is the same frosted surface (consistent with the
     blur the pill's own backdrop-filter applies). */
  body[data-page="sub"] .page-header-sticky{
    position:sticky;top:0;z-index:45;
    padding:0;
  }
  body[data-page="sub"] .page-header{display:block;}
  body[data-page="sub"] .header-left{
    background:transparent;
    backdrop-filter:none;-webkit-backdrop-filter:none;
    border-bottom:1px solid var(--border);
    border-right:none;
    /* These subpages are NOT viewport-fit=cover, so the content viewport
       already sits below the status-bar / notch — the title does NOT need
       an extra safe-area-inset-top pad. Adding env(safe-area-inset-top)
       here was producing a ~47px empty band above the header on devices
       where Safari reports a nonzero top inset even without cover. Fixed
       small pad instead; the viewport handles the status-bar clearance. */
    padding:10px 20px 16px;
  }
  body[data-page="sub"] .header-right-desktop{display:none;}
  body[data-page="sub"] .header-right-mobile{
    display:block;position:relative;
    padding:20px 20px 24px;
    background:transparent;
  }
  body[data-page="sub"] .series-desc{font-size:18px;line-height:1.75;}
  body[data-page="sub"] .series-meta{font-size:16px;line-height:1.9;}
  body[data-page="sub"] .series-title{font-size:38px;}
}

/* ─────────────────────────────────────────────────────────────────
   SUBPAGE VIEW-TOGGLE / DOC-HEADER (shared)
   Hoisted from inline CSS in 8 subpages.
   ───────────────────────────────────────────────────────────────── */
body[data-page="sub"] .doc-header{
  display:flex;align-items:center;justify-content:space-between;
  padding:14px 32px;border-bottom:1px solid var(--border-mid);
}
body[data-page="sub"] .doc-label{font-size:var(--fs-small);letter-spacing:0.20em;color:var(--text-dim);}
body[data-page="sub"] .view-toggle{display:flex;align-items:center;gap:10px;}
body[data-page="sub"] .view-toggle-label{font-size:10px;letter-spacing:0.18em;color:var(--text-dim);transition:color 0.25s;user-select:none;}
body[data-page="sub"] .view-toggle-label.active{color:var(--text-bright);}
body[data-page="sub"] .view-toggle-track{
  width:44px;height:22px;
  border:1px solid var(--border-mid);
  border-radius:11px;
  position:relative;cursor:pointer;flex-shrink:0;
  background:transparent;
  transition:border-color 0.25s, background 0.25s;
}
body[data-page="sub"] .view-toggle-track:hover{
  border-color:var(--text-mid);
}
body[data-page="sub"] .view-toggle-thumb{
  position:absolute;top:2px;left:2px;width:16px;height:16px;
  border-radius:50%;
  background:var(--text-mid);
  transition:transform 0.3s cubic-bezier(0.25,0.46,0.45,0.94);
}
body[data-page="sub"] .view-toggle-track.book .view-toggle-thumb{transform:translateX(22px);}

/* ─────────────────────────────────────────────────────────────────
   SUBPAGE MASONRY GRID (equal-area tiles, balanced white space)
   Each .mitem is sized so its width × height equals a target
   surface area K, while preserving the image's natural aspect
   ratio. setTileSpans (subpage.js) writes inline width/height
   per tile from naturalWidth/naturalHeight:
       w = sqrt(K · ratio),  h = sqrt(K / ratio)   where ratio = w/h
   A 1:1 painting becomes a sqrt(K) × sqrt(K) square; a 2:1
   landscape becomes a wider/shorter rectangle of the SAME area;
   a 1:2 portrait becomes taller/narrower with the same area.
   Result: every painting reads at roughly the same visible
   surface area regardless of its aspect ratio. Flex-wrap with
   centered rows distributes the resulting variable-width tiles
   evenly, and the gap supplies balanced white space between
   them without per-tile padding.
   ───────────────────────────────────────────────────────────────── */
body[data-page="sub"] .masonry{
  display:flex;
  flex-wrap:wrap;
  justify-content:center;
  align-items:center;
  gap:20px;
  padding:24px;
  opacity:1;transition:opacity 0.2s;
}
body[data-page="sub"] .masonry.hidden{
  opacity:0;pointer-events:none;
  height:0;overflow:hidden;
  padding:0;
}
body[data-page="sub"] .mitem{
  position:relative;cursor:pointer;overflow:hidden;
  opacity:0;transform:translateY(14px);
  transition:opacity 0.55s ease,transform 0.55s ease;
  /* width / height are set by setTileSpans inline-style so each
     tile reaches the target surface area at its natural ratio. */
  flex:0 0 auto;
  border:1px solid var(--border);
}
body[data-page="sub"] .mitem.visible{opacity:1;transform:translateY(0);}
body[data-page="sub"] .mimg{
  /* Tile aspect-ratio is matched to the image, so cover leaves no
     letterbox — the painting fills its tile exactly. The white
     space comes from the flex gap between tiles, not from
     inside-tile padding. */
  width:100%;height:100%;
  object-fit:cover;
  display:block;
  transition:transform 0.5s ease;
  border:none;
}
body[data-page="sub"] .mitem:hover .mimg{transform:scale(1.025);}
body[data-page="sub"] .moverlay{position:absolute;bottom:0;left:0;right:0;padding:22px 16px 14px;background:linear-gradient(to top,rgba(95,90,85,0.88) 0%,transparent 100%);opacity:0;transition:opacity 0.3s;pointer-events:none;}
body[data-page="sub"] .mitem:hover .moverlay{opacity:1;}
body[data-page="sub"] .moverlay-title{display:block;font-size:16px;font-weight:500;color:rgba(250,250,249,0.85);}
body[data-page="sub"] .moverlay-info{display:block;font-size:11px;letter-spacing:0.08em;color:rgba(250,250,249,0.65);margin-top:3px;}

/* ─────────────────────────────────────────────────────────────────
   SUBPAGE BOOK MODE (book-spread / book-text / book-image)
   .tulpas-book is the legacy class name retained for compatibility.
   ───────────────────────────────────────────────────────────────── */
body[data-page="sub"] .tulpas-book,
body[data-page="sub"] .book-mode{opacity:0;pointer-events:none;height:0;overflow:hidden;transition:opacity 0.2s;}
body[data-page="sub"] .tulpas-book.visible,
body[data-page="sub"] .book-mode.visible{opacity:1;pointer-events:all;height:auto;overflow:visible;}
body[data-page="sub"] .book-spread{display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--border-mid);}
body[data-page="sub"] .book-spread:last-child{border-bottom:none;}
body[data-page="sub"] .book-image{overflow:hidden;position:relative;}
body[data-page="sub"] .book-image img{display:block;width:100%;height:100%;object-fit:cover;min-height:420px;transition:transform 0.7s cubic-bezier(0.25,0.46,0.45,0.94);cursor:zoom-in;}
body[data-page="sub"] .book-image:hover img{transform:scale(1.03);}
body[data-page="sub"] .book-text{
  padding:64px 52px;display:flex;flex-direction:column;justify-content:center;
  border-left:1px solid var(--border-mid);
}
body[data-page="sub"] .book-spread:nth-child(even) .book-image{order:1;}
body[data-page="sub"] .book-spread:nth-child(even) .book-text{border-left:none;border-right:1px solid var(--border-mid);order:0;}
/* Explicit class-based ordering (entrotopia/apocalypse). Compound class
   selector beats :nth-child pseudo-class specificity, so these win when
   .text-left / .img-left is present and override the even/odd fallback. */
body[data-page="sub"] .book-spread.text-left .book-image{order:1;}
body[data-page="sub"] .book-spread.text-left .book-text{order:0;border-left:1px solid var(--border-mid);border-right:none;}
body[data-page="sub"] .book-spread.img-left .book-image{order:0;}
body[data-page="sub"] .book-spread.img-left .book-text{order:1;border-left:none;border-right:1px solid var(--border-mid);}

/* FULL-BLEED spread (used by exhibition pages — entrotopia, apocalypse).
   Single-column layout: the image fills the full width, and the .book-text
   floats over the bottom of the image as a darkening gradient overlay with
   white-tinted text. Hoisted from per-page so the cascade order is reliable
   regardless of which @import / inline order future pages use. */
body[data-page="sub"] .book-spread.full-bleed{
  grid-template-columns:1fr;
  position:relative;
  overflow:hidden;
}
body[data-page="sub"] .book-spread.full-bleed .book-image{order:0;}
body[data-page="sub"] .book-spread.full-bleed .book-image img{min-height:560px;}
body[data-page="sub"] .book-spread.full-bleed .book-text{
  position:absolute;bottom:0;left:0;right:0;
  padding:48px 52px;
  background:linear-gradient(transparent,rgba(20,18,16,0.68));
  border:none;
  display:block;
}
body[data-page="sub"] .book-spread.full-bleed .book-text-num{color:rgba(250,250,249,0.40);}
body[data-page="sub"] .book-spread.full-bleed .book-text-title{color:rgba(250,250,249,0.95);}
body[data-page="sub"] .book-spread.full-bleed .book-text-artist{color:rgba(250,250,249,0.55);}
body[data-page="sub"] .book-spread.full-bleed .book-text-desc{color:rgba(250,250,249,0.72);}
/* Mobile: keep the full-bleed overlay clean. The generic mobile rule
   `.book-text{padding:28px 20px;border-top:1px solid;...}` would otherwise
   draw a horizontal divider line through the middle of the image and crop
   the gradient padding too tightly. Bigger title size here as well — the
   exhibition main titles ("welcome to the age of chaos…", "sit back, relax
   & enjoy the apocalypse") need real presence over a full-page photo. */
@media(max-width:700px){
  body[data-page="sub"] .book-spread.full-bleed .book-text{
    padding:60px 24px 30px!important;
    border:none!important;
    background:linear-gradient(transparent 0%,rgba(20,18,16,0.55) 35%,rgba(20,18,16,0.85) 100%)!important;
  }
  body[data-page="sub"] .book-spread.full-bleed .book-text-num{
    margin-bottom:14px;
  }
  body[data-page="sub"] .book-spread.full-bleed .book-text-title{
    font-size:30px;line-height:1.05;margin-bottom:12px;
  }
  body[data-page="sub"] .book-spread.full-bleed .book-text-artist{
    font-size:11px;margin-bottom:14px;
  }
  body[data-page="sub"] .book-spread.full-bleed .book-text-desc{
    font-size:14px;line-height:1.65;
  }
  body[data-page="sub"] .book-spread.full-bleed .book-image img{
    min-height:78dvh;
  }
}
/* Desktop: bump the full-bleed title — the previous 22px got lost over a
   large photographic background. The non-full-bleed spreads keep the
   shared 22px treatment via .book-text-title above. */
@media(min-width:701px){
  body[data-page="sub"] .book-spread.full-bleed .book-text-title{
    font-size:42px;line-height:1.0;letter-spacing:-0.02em;margin-bottom:14px;
  }
}
body[data-page="sub"] .book-text-num{font-size:10px;letter-spacing:0.22em;color:var(--text-dim);margin-bottom:20px;display:block;}
body[data-page="sub"] .book-text-title{font-size:22px;font-weight:600;letter-spacing:-0.02em;color:var(--text-bright);line-height:1.1;margin-bottom:14px;}
body[data-page="sub"] .book-text-meta{font-size:12px;letter-spacing:0.12em;color:var(--text-dim);line-height:1.9;}
/* book-text-artist / book-text-desc — used by exhibition pages
   (entrotopia, apocalypse) for artist credit and description text inside
   book spreads. Styles match the originals. */
body[data-page="sub"] .book-text-artist{font-size:11px;letter-spacing:0.18em;color:var(--text-dim);margin-bottom:18px;display:block;}
body[data-page="sub"] .book-text-desc{font-size:var(--fs-body);line-height:1.9;color:var(--text-mid);}

/* ─────────────────────────────────────────────────────────────────
   SUBPAGE LIGHTBOX (tulpas convention — .lightbox / #lightbox)
   ───────────────────────────────────────────────────────────────── */
body[data-page="sub"] .lightbox{position:fixed;inset:0;z-index:10000;background:rgba(250,250,249,0.92);backdrop-filter:blur(48px);-webkit-backdrop-filter:blur(48px);opacity:0;pointer-events:none;transition:opacity 0.4s ease;display:grid;grid-template-rows:1fr auto;}
body[data-page="sub"] .lightbox.open{opacity:1;pointer-events:all;}
/* Host sizing only — chassis .filament-close handles the
   brackets / bloom / X glyph. No border-radius here so the
   chassis 3px wins for shape unification with other closes. */
body[data-page="sub"] .lb-close{position:absolute;top:24px;right:24px;z-index:10;width:36px;height:36px;cursor:pointer;display:flex;align-items:center;justify-content:center;}
body[data-page="sub"] .lb-stage{display:flex;align-items:center;justify-content:center;padding:64px 80px 24px;overflow:hidden;}
body[data-page="sub"] .lb-img{max-width:100%;max-height:100%;object-fit:contain;transition:opacity 0.28s ease;display:block;box-shadow:0 8px 40px rgba(0,0,0,0.08);border:1px solid var(--border);}
body[data-page="sub"] .lb-img.fade{opacity:0;}
body[data-page="sub"] .lb-info{padding:16px 32px 24px;border-top:1px solid var(--border);display:grid;grid-template-columns:auto 1fr auto;align-items:center;flex-shrink:0;gap:24px;}
body[data-page="sub"] .lb-prev,
body[data-page="sub"] .lb-next{background:none;border:none;cursor:pointer;font-size:16px;letter-spacing:0.20em;color:var(--text-mid);transition:color 0.2s;padding:4px 0;white-space:nowrap;opacity:0.55;
  -webkit-tap-highlight-color:transparent;
}
body[data-page="sub"] .lb-prev:hover,
body[data-page="sub"] .lb-next:hover{color:var(--text-bright);opacity:1;}
/* Active state — touch tap on iOS/Android otherwise paints a
   translucent gray rectangle over the whole button area (the
   default -webkit-tap-highlight-color). Replace that with a text-
   only darken so the press feels like part of the typography
   rather than a chrome state. */
body[data-page="sub"] .lb-prev:active,
body[data-page="sub"] .lb-next:active{
  color:var(--text-bright);opacity:1;
  background:transparent !important;
}
body[data-page="sub"] .lb-info-center{text-align:center;}
body[data-page="sub"] .lb-title{font-size:20px;font-weight:500;color:var(--text-bright);}
body[data-page="sub"] .lb-meta{font-size:12px;letter-spacing:0.1em;color:var(--text-dim);margin-top:5px;}
body[data-page="sub"] .lb-counter{font-size:11px;letter-spacing:0.18em;color:var(--text-dim);white-space:nowrap;margin-top:4px;}

/* MOBILE responsive: book / masonry / lightbox / view-toggle */
@media(max-width:700px){
  /* Mobile collapses the equal-area gallery back to a single full-
     width column so each work has room to read at thumb-size.
     setTileSpans writes desktop-only inline width/height — mobile
     overrides them with !important so the JS sizing can't leak. */
  body[data-page="sub"] .masonry{
    flex-direction:column;
    align-items:stretch;
    padding:0 0 calc(env(safe-area-inset-bottom, 60px) + 20px + var(--gdpr-h)) 0;
    gap:1px;
  }
  body[data-page="sub"] .mitem{
    grid-row:unset!important;
    width:100%!important;
    height:auto!important;
    aspect-ratio:auto;
    background:transparent;
    border:none;
  }
  body[data-page="sub"] .mimg{height:auto;object-fit:contain;}
  body[data-page="sub"] .doc-header{padding:12px 20px;}
  body[data-page="sub"] .book-spread{grid-template-columns:1fr;}
  body[data-page="sub"] .book-text{padding:28px 20px;border-left:none!important;border-right:none!important;border-top:1px solid var(--border-mid);}
  body[data-page="sub"] .book-spread:nth-child(even) .book-image{order:0;}
  body[data-page="sub"] .book-spread:nth-child(even) .book-text{order:1;}
  body[data-page="sub"] .book-image img{min-height:260px;}
  body[data-page="sub"] .tulpas-book,
  body[data-page="sub"] .book-mode{padding-bottom:calc(env(safe-area-inset-bottom, 60px) + 20px);}
  body[data-page="sub"] .lb-stage{padding:40px 24px 16px;}
  body[data-page="sub"] .lb-close{top:16px;right:16px;}
  body[data-page="sub"] .lb-info{padding:12px 16px 20px;gap:16px;}
  body[data-page="sub"] .lb-prev,
  body[data-page="sub"] .lb-next{font-size:14px;padding:6px 0;}
  body[data-page="sub"] .lb-title{font-size:17px;}
  body[data-page="sub"] .lb-meta{font-size:11px;}
  body[data-page="sub"] .lb-counter{margin-top:4px;}
}


/* ================================================================
   ════════════════════════════════════════════════════════════════
   MEMBRANE LAYER — merged in from membrane.css on 2026-05-13.
   ════════════════════════════════════════════════════════════════
   Originally a separate file (membrane.css), now appended here so
   the entire cascade lives in one document. Cascade position is
   preserved: anything below this banner used to live in membrane.css
   and loaded AFTER shared.css; that ordering still holds because
   it's now physically below the shared.css block. Nothing else has
   changed — these rules are byte-identical to the previous file.
   ════════════════════════════════════════════════════════════════
   ================================================================ */

/* ================================================================
   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.
   ================================================================ */


/* ── 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;
}
/* v=91 — PRE-WARM the pill-surface compositor at page load.
   The first sticky-header morph (cold cache) was noticeably
   laggier than subsequent ones because Chromium had to do
   first-time setup work synchronously on the first activation:
     · Allocate a backing texture for the (previously opacity:0)
       .pill-surface compositor layer
     · Compile the SVG filter shader chain (filter:url
       (#waterRippleCtx)) when first applied
     · Initialize the .iri rotation animations on the GPU
     · Populate the glyph atlas at the new compact-state font
       sizes (26px title, 10px meta, 13px desc)
   `will-change` hints tell Chromium to do this layer allocation
   at IDLE post-load, not on the first interaction. The cost is a
   small permanent GPU layer reservation — bounded, harmless. */
.page-header-sticky .pill-surface{
  /* will-change:filter REMOVED (v=219). Declaring `filter` here kept
     Chromium from promoting the surface to a compositor layer for the
     opacity fade even while the filter was momentarily `none` during
     the .pill-morphing morph window — the browser anticipates the
     filter returning and keeps the element on the CPU raster path,
     so the 0.32s opacity fade ran at ~30fps. Hinting only transform +
     opacity lets the fade composite cleanly; the SVG filter snaps back
     on (one paint) at the end of the morph when the pill is static. */
  will-change:transform, opacity;
}
.pill-surface.active{
  opacity:1;
  pointer-events:auto;
  filter:url(#waterRippleCtx);
  /* (Earlier rounds had `isolation:isolate` here as a Chromium nested-
     filter blur workaround — confirmed in testing that it didn't
     actually fix the bug on Chromium. Removed to drop the per-active-
     surface stacking-context cost. Chromium just renders sharp iri
     sectors without blur on the desktop sticky compact pill; Safari
     renders correctly natively.) */
}
/* v=86 — DROP THE MEMBRANE FILTER DURING THE STICKY MORPH WINDOW.
   The sticky-header's full↔compact transition resizes the host
   over 0.35s. Because .pill-surface is positioned absolutely
   inside the sticky, it resizes every frame too — and the SVG
   filter:url(#waterRippleCtx) chain (feTurbulence + feDisplacementMap)
   has to re-rasterize against the new surface dimensions every
   paint. On Chromium's Skia path, this is the dominant cost of
   the morph (every frame re-runs the entire turbulence chain).
   subpage.js adds .pill-morphing to the sticky synchronously
   when .compact toggles, schedules its removal at ~360ms
   (just after the layout transition completes), so the filter
   ONLY runs in the stable before/after states. Membrane
   shape-shifting is fully present after the morph — only the
   transition window itself paints without the filter. Visual:
   pill-surface fades in (.pill-surface base rule transitions
   opacity 0.32s) without displacement during the morph, then
   the displacement smoothly appears at the end. */
.page-header-sticky.pill-morphing .pill-surface.active{
  filter:none;
}
/* v=87 — ALSO pause the iri rotations during the morph window.
   Even with the SVG filter dropped above, the two .iri layers
   underneath were still rotating (36s + 52s linear infinite)
   inside a .pill-surface that's resizing per frame, with
   mix-blend-mode (overlay / soft-light) compositing them against
   the changing surface. That meant the compositor was repainting
   the iri layers every frame against a changing background even
   without the SVG filter on top — a secondary cost in the morph.
   Pausing the animations holds the iri layers at their current
   rotation state. The compositor can cache the iri raster as a
   bitmap. As soon as .pill-morphing is removed, the animations
   resume from the paused state — no jump, no jank. */
.page-header-sticky.pill-morphing .pill-surface.active .iri,
.page-header-sticky.pill-morphing .pill-surface.active .iri.b{
  animation-play-state:paused;
}
/* v=89 — Also HIDE the iri layers during the morph window.
   Pausing the animation stopped the rotation but the iri layers
   were still being COMPOSITED at each parent-pill-surface size.
   Because .iri has inset:-12% (extends 12% past its parent), as
   the parent resizes per frame the iri's dimensions also change
   per frame — forcing the compositor to re-rasterize the iri's
   conic-gradient + blur(20px) + mix-blend-mode chain at the new
   size every frame. That was the asymmetric cost of full→compact
   (compact has the .iri SourceGraphic, full doesn't).
   Setting opacity:0 with no transition means iri stops contributing
   entirely during the morph. The opacity transition on .pill-surface
   itself still fades the surface in/out — just without the iri color
   palette during the morph. As soon as .pill-morphing is removed,
   iri snaps back to its CSS-determined opacity (0.70 / 0.65 per the
   base rules) and rotations resume — the iri palette appears on
   the now-stable compact pill instantly. */
.page-header-sticky.pill-morphing .pill-surface.active .iri,
.page-header-sticky.pill-morphing .pill-surface.active .iri.b{
  opacity:0;
}

/* ── 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;
  /* (Earlier round had `will-change:filter, transform` here as a
     Chromium nested-filter blur workaround. Confirmed it didn't
     actually fix the bug, but did cost a permanent GPU layer per
     iri — and there are 12+ iri elements site-wide, so cumulative
     memory pressure was noticeable on mid-range mobile. Removed.) */
}
.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; }
}



/* ════════════════════════════════════════════════════════════════
   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;
}
/* Pure-white text on every membrane'd element (the dark
   translucent surface needs light text). User reported the
   cream (rgba 245,238,225) read as yellowish on the sticky
   header on both mobile and desktop — changed to pure white
   rgba(255,255,255,0.95) so the text reads as cool-white on
   the membrane material. !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(255,255,255,0.95) !important;
  -webkit-text-fill-color: rgba(255,255,255,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;
}
/* Filament suppression — same idea as scrim suppression above
   but for the filament chassis. When a floor (overlay OR #mstack)
   is in the floor stack but not topmost, the topmost panel
   covers it, so the filament-touched buttons inside are
   invisible. Their bracket drop-shadow halos, .fil-bloom
   blur(14px), and the two rotating conic-gradient pseudos
   (filIriA / filIriB) keep running unless suppressed.
   Strategy: kill `filter` and `animation` on every filament
   pseudo + bloom inside the underneath floor. The selector list
   targets `.bg-suppressed` generally (not `.overlay.bg-suppressed`)
   so #mstack with the class also matches — covers the
   "overlay opens on top of the mobile menu" case. */
.bg-suppressed .filament::before,
.bg-suppressed .filament::after,
.bg-suppressed .filament-pill::before,
.bg-suppressed .filament-pill::after,
.bg-suppressed .filament-close::before,
.bg-suppressed .filament-close::after,
.bg-suppressed .filament-text-wrap::before,
.bg-suppressed .filament-text-wrap::after,
.bg-suppressed .filament-checkbox-box::before,
.bg-suppressed .filament-checkbox-box::after,
.bg-suppressed .fil-bloom,
.bg-suppressed .fil-bloom::before,
.bg-suppressed .fil-bloom::after{
  filter:none !important;
  animation: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.
   ════════════════════════════════════════════════════════════════ */


/* ================================================================
   ════════════════════════════════════════════════════════════════
   FILAMENT LAYER — merged in from filament.css on 2026-05-13.
   ════════════════════════════════════════════════════════════════
   Originally a separate file (filament.css), now appended here so
   the entire cascade lives in one document. Cascade position is
   preserved: anything below this banner used to live in filament.css
   and loaded AFTER shared.css + membrane.css; that ordering still
   holds because it's now physically below those two blocks. Nothing
   else has changed — these rules are byte-identical to the previous
   file.
   ════════════════════════════════════════════════════════════════
   ================================================================ */

/* ================================================================
   filament.css — cold-light bracket material, site-wide.
   ----------------------------------------------------------------
   Extracted from filament-preview-v3.html. Load AFTER shared.css
   and AFTER membrane.css so:
     - filament tokens override anything similarly named upstream
     - filament chassis can sit ON TOP of a membrane host (the
       membrane's absolutely-positioned .pill-surface fills the
       host's bounds while the filament brackets stick out at
       top/-3px, bottom/-3px, left/-6px, right/-6px and the
       .fil-bloom span sits inside as a non-clipped span)

   Stacking with membrane
   ----------------------
   A button with both data-membrane AND .filament looks like:
     <button class="filament" data-membrane="cookies">
       <div class="pill-surface active" data-floor="cookies">…</div>
       <span class="fil-bloom"></span>
       <span class="fil-label">accept all</span>
     </button>
   The membrane.js wiring runs first and inserts the pill-surface;
   filament.js runs next, sees the host doesn't yet have a fil-bloom,
   and inserts one. The bracket pseudo-elements come from this CSS.

   The site-wide waterRipple SVG filter (used by membrane) is shared.
   No new SVG defs needed here — filament's bloom is pure CSS.
   ================================================================ */

/* ── Filament tokens (cold light palette) ─────────────────────── */
:root{
  --fil-edge:         rgba(255,255,255,1);
  --fil-edge-soft:    rgba(255,255,255,0.75);
  --fil-glow-near:    rgba(255,255,255,0.90);
  --fil-glow-mid:     rgba(255,255,255,0.40);
  --fil-glow-far:     rgba(255,255,255,0.14);

  /* ════════════════════════════════════════════════════════════════
     FILAMENT FAMILY RECIPE TOKENS
     ────────────────────────────────────────────────────────────────
     Single source of truth for "what a non-close filament button
     looks like" — used by the unified family rule at the END of
     this filament block. Edit a value here and every non-close
     filament button (.mjump-title, .cookie-*, .choice-strip-*,
     .priv-cookies-cta button, .sdr-submit-btn, .about-links
     mobile, .sdr-form-field) updates together.

     EXPLICITLY DOES NOT AFFECT:
       - .filament-close (close discs — chassis recipe + .x emission)
       - .page-header-sticky (subpage compact-pill — own treatment)
       - .fil-glass variants (.snav-item, .mreel-frame — glass nav)
       - .filament-checkbox-box (different visual category)
     Those hosts never reference these tokens.
     ════════════════════════════════════════════════════════════════ */

  /* HOST surface — the glass tile behind the brackets + text. */
  --fil-fam-bg:           rgba(255,255,255,0.08);
  --fil-fam-bg-hover:     rgba(255,255,255,0.14);
  --fil-fam-bg-active:    rgba(255,255,255,0.22);
  --fil-fam-border:       1px solid rgba(255,255,255,0.55);
  --fil-fam-border-hover: 1px solid rgba(255,255,255,0.75);
  --fil-fam-radius:       3px;
  --fil-fam-shadow:
    0 8px 24px rgba(20,18,16,0.08),
    0 2px 4px rgba(20,18,16,0.04),
    inset 0 1.4px 0 rgba(255,255,255,0.85),
    inset 0 3px 4px rgba(255,255,255,0.35),
    inset 0 -1px 0 rgba(20,18,16,0.06),
    inset 0 0 0 0.5px rgba(255,255,255,0.40);
  --fil-fam-backdrop:     blur(24px) saturate(200%) brightness(1.05);

  /* CONTENT — text/link color + the (off) drop-shadow halo. */
  --fil-fam-text:         rgba(255,255,255,0.95);
  --fil-fam-label-filter: none;

  /* BLOOM — the darkening backdrop layer behind brackets/text. */
  --fil-fam-bloom-bg:       rgba(0,0,0,0.36);
  --fil-fam-bloom-bg-hover: rgba(0,0,0,0.30);

  /* IRIDESCENCE — the conic-gradient ::before/::after layers on
     the bloom. mix-blend-mode:screen is what lets the colourful
     pastels emerge instead of being multiplied to grey by the
     chassis-default overlay/soft-light blend.
     Toned down (May 2026 post-refactor) — earlier 0.95/0.90 + sat
     1.8/1.5 was the level needed when only .mjump-title carried
     the boost. Now that every host in the family uses the same
     values, that intensity reads as overpowering across the site.
     Halving opacity + dropping saturation back closer to 1×
     restores a subtle iridescent shimmer that still reads as
     "lit cold-light material" without dominating. */
  --fil-fam-iri-blend:    screen;
  --fil-fam-iri-opacity-a: 0.50;
  --fil-fam-iri-opacity-b: 0.45;
  --fil-fam-iri-filter-a: blur(14px) saturate(1.2);
  --fil-fam-iri-filter-b: blur(18px) saturate(1.0);

  --fil-body-rest:
    linear-gradient(90deg,
      rgba(255,255,255,0.04) 0%,
      rgba(255,255,255,0.01) 50%,
      rgba(255,255,255,0.04) 100%);
  --fil-body-hover:
    linear-gradient(90deg,
      rgba(255,255,255,0.06) 0%,
      rgba(255,255,255,0.02) 50%,
      rgba(255,255,255,0.06) 100%);
  --fil-body-checked:
    radial-gradient(ellipse 120% 120% at 50% 50%,
      rgba(255,255,255,0.10) 0%,
      rgba(255,255,255,0.02) 100%);
}

/* Plain [ ] brackets — white light bevel, encoded as inline SVG. */
:root{
  --fil-bracket-l: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 40'><defs><linearGradient id='v' x1='0' y1='0' x2='1' y2='0'><stop offset='0' stop-color='rgba(255,255,255,0.50)'/><stop offset='0.5' stop-color='rgba(255,255,255,1)'/><stop offset='1' stop-color='rgba(255,255,255,0.72)'/></linearGradient><linearGradient id='ht' x1='0' y1='0' x2='0' y2='1'><stop offset='0' stop-color='rgba(255,255,255,0.50)'/><stop offset='0.5' stop-color='rgba(255,255,255,1)'/><stop offset='1' stop-color='rgba(255,255,255,0.72)'/></linearGradient><linearGradient id='hb' x1='0' y1='1' x2='0' y2='0'><stop offset='0' stop-color='rgba(255,255,255,0.50)'/><stop offset='0.5' stop-color='rgba(255,255,255,1)'/><stop offset='1' stop-color='rgba(255,255,255,0.72)'/></linearGradient></defs><rect x='2' y='2' width='4' height='36' fill='url(%23v)'/><rect x='2' y='2' width='13' height='4' fill='url(%23ht)'/><rect x='2' y='34' width='13' height='4' fill='url(%23hb)'/></svg>");
  --fil-bracket-r: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 40'><defs><linearGradient id='v' x1='1' y1='0' x2='0' y2='0'><stop offset='0' stop-color='rgba(255,255,255,0.50)'/><stop offset='0.5' stop-color='rgba(255,255,255,1)'/><stop offset='1' stop-color='rgba(255,255,255,0.72)'/></linearGradient><linearGradient id='ht' x1='0' y1='0' x2='0' y2='1'><stop offset='0' stop-color='rgba(255,255,255,0.50)'/><stop offset='0.5' stop-color='rgba(255,255,255,1)'/><stop offset='1' stop-color='rgba(255,255,255,0.72)'/></linearGradient><linearGradient id='hb' x1='0' y1='1' x2='0' y2='0'><stop offset='0' stop-color='rgba(255,255,255,0.50)'/><stop offset='0.5' stop-color='rgba(255,255,255,1)'/><stop offset='1' stop-color='rgba(255,255,255,0.72)'/></linearGradient></defs><rect x='12' y='2' width='4' height='36' fill='url(%23v)'/><rect x='3' y='2' width='13' height='4' fill='url(%23ht)'/><rect x='3' y='34' width='13' height='4' fill='url(%23hb)'/></svg>");

  /* FLAT bracket variant — for .fil-glass. Single white <path> (no
     internal gradients) so the bracket reads as ONE piece of glass,
     not three bevelled rectangles. User feedback was that the
     gradient bracket looked "3D" and "overlapping"; this version is
     intentionally flat. The path traces a closed [ shape:
       top hook + vertical bar + bottom hook → one continuous fill.
     fill-opacity:0.95 keeps it nearly-opaque so it's visible on any
     bg; the .fil-glass filter below adds subtle white halo glow. */
  --fil-bracket-l-flat: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 40'><path d='M2 2 L15 2 L15 6 L6 6 L6 34 L15 34 L15 38 L2 38 Z' fill='%23ffffff' fill-opacity='0.95'/></svg>");
  --fil-bracket-r-flat: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 40'><path d='M16 2 L3 2 L3 6 L12 6 L12 34 L3 34 L3 38 L16 38 Z' fill='%23ffffff' fill-opacity='0.95'/></svg>");
}

/* ── fil-label sits BEHIND the bloom; bloom is a blurred dark span ─ */
/* Text sits ABOVE bloom + brackets — crisp and always on top.
   filter:drop-shadow renders the same kind of halo as the close-
   button X strokes. BUT: the X is a 16×4 px SOLID rectangle, so
   the halo emits evenly from a wide continuous surface. Text is
   thin (~2-3 px stroke) glyphs with interior negative space, and
   when the outer drop-shadow radius (22 px previously) is bigger
   than the glyph height (~16 px), neighbouring character halos
   bleed into one another and the letterforms smear into an
   unreadable lit shape. Tightening the stack to a max ~8 px outer
   radius keeps the lit-character reading without smearing — text
   reads as glowing strokes rather than as a smudged outline, the
   way the close button's X reads as crisp white bars with halo
   rather than as a smudge. The wider INTERIOR fill that the close
   button gets from the X's halo overlapping across the disc is
   delivered separately below by the radial .fil-bloom override
   (which paints a bright centre + dark edges across .filament and
   .filament-pill hosts where there's no X to do that job). */
.filament .fil-label,
.filament-pill .fil-label,
.filament-close .fil-label{
  position:relative;
  z-index:5;
  pointer-events:none;
  filter:
    drop-shadow(0 0 1px rgba(255,255,255,1))
    drop-shadow(0 0 3px rgba(255,255,255,0.95))
    drop-shadow(0 0 7px rgba(255,255,255,0.55));
}

/* .fil-bloom — the DARKENING layer underneath the brackets/text.
   The close-button-style "darkening halo" is here: a dark
   translucent area inside the host, blurred outward, providing the
   contrast that makes white brackets/text emit visibly. Bumped
   from 0.18 to 0.30 alpha so the darkening reads clearly. The
   bright outer halo is separately added via box-shadow on the
   host (so the body has darker interior + brighter outer ring,
   matching the close button). */
.fil-bloom{
  position:absolute;
  inset:0 -2px;
  border-radius:3px;
  background:rgba(0,0,0,0.30);
  overflow:hidden;
  filter:blur(14px);
  pointer-events:none;
  z-index:0;
  transition:opacity 0.15s ease;
  will-change:opacity;
}
.fil-bloom::before{
  content:'';
  position:absolute;
  inset:-20%;
  background:conic-gradient(
    from 0deg at 50% 50%,
    rgba(140,180,255,0.55) 0%,
    rgba(200,160,255,0.45) 14%,
    rgba(160,230,220,0.50) 28%,
    rgba(180,160,255,0.45) 42%,
    rgba(120,200,255,0.55) 56%,
    rgba(200,140,240,0.45) 70%,
    rgba(160,220,255,0.50) 84%,
    rgba(140,180,255,0.55) 100%);
  filter:blur(18px) saturate(1.4);
  mix-blend-mode:overlay;
  opacity:0.65;
  /* User wants visible iridescence even at rest (matches the
     close-button halo character). Slow 22s rotation reads as
     "always-on iridescence" without being distracting. */
  animation:filIriA 22s linear infinite;
  will-change:transform;
}
.fil-bloom::after{
  content:'';
  position:absolute;
  inset:-20%;
  background:conic-gradient(
    from 180deg at 30% 70%,
    rgba(160,140,255,0.40),
    rgba(120,210,240,0.35),
    rgba(180,160,255,0.45),
    rgba(140,200,255,0.40),
    rgba(160,140,255,0.40));
  filter:blur(22px) saturate(1.2);
  mix-blend-mode:soft-light;
  opacity:0.70;
  animation:filIriB 32s linear infinite;
  will-change:transform;
}
@keyframes filIriA{
  0%   { transform:rotate(0deg)   scale(1);    }
  50%  { transform:rotate(180deg) scale(1.06); }
  100% { transform:rotate(360deg) scale(1);    }
}
@keyframes filIriB{
  0%   { transform:rotate(0deg);   }
  100% { transform:rotate(-360deg);}
}
.filament:hover .fil-bloom::before,
.filament:hover .fil-bloom::after,
.filament-pill:hover .fil-bloom::before,
.filament-pill:hover .fil-bloom::after,
.filament-checkbox-box:hover .fil-bloom::before,
.filament-checkbox-box:hover .fil-bloom::after,
.filament-close:hover .fil-bloom::before,
.filament-close:hover .fil-bloom::after,
.filament-text-wrap:focus-within .fil-bloom::before,
.filament-text-wrap:focus-within .fil-bloom::after,
.filament-checkbox input:checked ~ .filament-checkbox-box .fil-bloom::before,
.filament-checkbox input:checked ~ .filament-checkbox-box .fil-bloom::after{
  animation-play-state:running;
}
.filament:hover .fil-bloom,
.filament-pill:hover .fil-bloom,
.filament-close:hover .fil-bloom{
  background:rgba(0,0,0,0.24);
}
.filament-checkbox-box .fil-bloom{
  inset:0 -2px;
}

/* WHOLE-FAMILY FLAT-DARK BLOOM — matches .mjump-title.filament-pill
   exactly. The works pill has its own per-host override at line
   5014 that sets rgba(0,0,0,0.36) flat dark, with higher
   specificity than this rule, so the works pill never actually
   saw the radial gradient I'd been tuning here. I was matching
   the wrong target — the works pill we kept calling "the
   reference" is using a FLAT DARK bloom (close-button recipe),
   not a radial bright-centre bloom.
   Aligning every non-close filament host to the same 0.36 alpha
   flat-dark backdrop the works pill actually uses. Iridescent
   conic-gradient pseudos on top still render via mix-blend
   overlay/soft-light, brackets at -6/-6 still extend past the
   host with their drop-shadow halo, and text on top reads
   against the now-darkened interior the same way the works pill's
   "works (8)" reads. .filament-close hosts are excluded — they
   already have their own chassis path (rgba(0,0,0,0.30) plus the
   .x stroke emission). */
.filament > .fil-bloom,
.filament-pill > .fil-bloom{
  background:rgba(0,0,0,0.36) !important;
}
.filament:hover > .fil-bloom,
.filament-pill:hover > .fil-bloom{
  background:rgba(0,0,0,0.30) !important;
}

/* ── Common chassis ───────────────────────────────────────────
   PAINT-ONLY, NEVER RESIZE.
   The chassis intentionally does NOT set padding, width, height,
   font-size, letter-spacing, or display. Every existing button has
   its own sizing in shared.css and we want filament to wrap that
   sizing, not replace it. The chassis only sets:
     - position:relative IF host is currently static — needed so the
       bracket pseudo-elements anchor to the host. We use a special
       fallback: hosts that already have position:fixed/absolute/
       sticky are explicitly opted out below.
     - background tint, color, transition for the "lit" feel.
   ─────────────────────────────────────────────────────────────── */
.filament,
.filament-pill,
.filament-text-wrap,
.filament-checkbox-box,
.filament-close{
  position:relative;
  background:var(--fil-body-rest);
  color:rgba(255,255,255,0.92);
  font-family:inherit;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:background 0.15s ease, transform 0.08s ease;
  /* TOUCH RELIABILITY: touch-action:manipulation eliminates the
     iOS 300ms double-tap-zoom wait — was previously only on
     .filament-close, but the user reported subscribe-overlay
     buttons (sdr-submit-btn.filament, the two filament-checkbox
     boxes) were unresponsive on tap. Now every filament chassis
     variant gets the same fast-tap behaviour. Text input is
     unaffected because it doesn't use these variants directly
     (input itself doesn't carry a filament class). */
  touch-action:manipulation;
}
/* Chassis-injected children are pointer-events:none. iOS Safari
   sometimes hit-tests on a filtered child instead of the host,
   swallows the tap, and the click handler never fires. Forcing
   pointer-events:none on the specific chassis pieces eliminates
   that. Brackets are ::before/::after pseudos and handled
   separately. NOTE: this is scoped to .fil-bloom / .fil-label /
   .glyph by class — NOT `> *` — because some filament-pill hosts
   (.page-header-sticky in particular) wrap real user-interactive
   children like a book button. A blanket `> *` rule disabled
   those taps and broke "book class level 1" in the sticky header. */
.fil-bloom,
.filament .fil-label,
.filament-pill .fil-label,
.filament-close .fil-label,
.filament-checkbox-box .glyph{
  pointer-events:none;
}

/* Hosts that bring their own non-static positioning need it back —
   the chassis selector above used to clobber position:fixed (which
   broke .mob-menu-btn). These overrides restore each known case.

   NOTE: .mjump-close is NOT in this list. It uses position:relative
   in shared.css and lives as a flex item in .mjump-head — making it
   position:absolute pops it out of flow, putting it top-left of its
   containing block (the user reported "close button is left instead
   of right") and breaking the centering of .mjump-title. The chassis
   default (position:relative) is exactly what mjump-close wants. */
.mstack-close.filament-close{ position:absolute; }

/* Bracket pseudo-elements */
.filament::before,
.filament::after,
.filament-pill::before,
.filament-pill::after,
.filament-text-wrap::before,
.filament-text-wrap::after,
.filament-checkbox-box::before,
.filament-checkbox-box::after,
.filament-close::before,
.filament-close::after{
  content:'';
  position:absolute;
  top:-3px; bottom:-3px;
  width:22px;
  pointer-events:none;
  background-repeat:no-repeat;
  background-size:100% 100%;
  z-index:2;
  /* Full 5-layer halo — this is what gives the close-button
     brackets their visible glow. The earlier 3-layer perf-reduction
     killed the outer halo that the user kept pointing at as the
     "iridescent halo effect from closing buttons". Restored to
     match filament-preview-v3 verbatim. */
  filter:
    drop-shadow(0 0 1px  rgba(255,255,255,1))
    drop-shadow(0 0 4px  rgba(255,255,255,1))
    drop-shadow(0 0 10px rgba(255,255,255,1))
    drop-shadow(0 0 22px rgba(255,255,255,0.95))
    drop-shadow(0 0 45px rgba(255,255,255,0.80));
  opacity:1;
  transition:transform 0.08s ease, filter 0.10s ease, opacity 0.12s ease;
  /* PERF: will-change removed at rest — it pins a GPU layer per
     button even when nothing's animating. Re-added on :hover below. */
}
/* PERF: on-hover will-change removed. Was allocating a fresh
   compositor layer for the bracket pseudos every time a filament
   button entered :hover state, and releasing it on :hover-out.
   User diagnostic on iPad+mouse: subscribe panel responsive when
   mouse is outside the panel area (scrim), sluggish the moment
   mouse enters the panel — because the panel contains filament
   buttons (.ov-close, .sdr-submit-btn) and mouse-moving across
   them caused iOS Safari to thrash allocating/releasing layers
   for each hover-in/out. Removing will-change here means the
   :hover filter change still happens, iOS just composites it
   ad-hoc instead of pre-allocating. Brackets remain pinned via
   their static will-change-free state; the filter transition
   (0.10s ease) is brief enough that the lack of pre-allocation
   is imperceptible. */
.filament::before,
.filament-pill::before,
.filament-text-wrap::before,
.filament-checkbox-box::before,
.filament-close::before{
  left:-6px;
  background-image:var(--fil-bracket-l);
}
.filament::after,
.filament-pill::after,
.filament-text-wrap::after,
.filament-checkbox-box::after,
.filament-close::after{
  right:-6px;
  background-image:var(--fil-bracket-r);
}

/* Hover — breathe animation */
.filament:hover,
.filament-pill:hover{
  background:var(--fil-body-hover);
}
/* PERF: filamentBreathe animation DISABLED. Was animating a
   5-layer drop-shadow filter at 60 fps on every filament
   button's bracket pseudos on :hover. Stacked with the existing
   static drop-shadow halo + the iridescent bloom pseudos + the
   text-shadow halo, the GPU was running ~12-15 compositing
   passes per frame per hovered button. User reported clicking
   any filament button became slow — this was the dominant
   cost. Brackets stay at their static drop-shadow halo (which
   is already substantial); the breathing micro-animation isn't
   worth the perf hit. */
/*
@media (hover: hover) {
  .filament:hover::before,
  .filament:hover::after,
  .filament-pill:hover::before,
  .filament-pill:hover::after,
  .filament-checkbox-box:hover::before,
  .filament-checkbox-box:hover::after,
  .filament-close:hover::before,
  .filament-close:hover::after{
    animation:filamentBreathe 1.4s ease-in-out infinite;
  }
}
*/
@keyframes filamentBreathe{
  0%,100%{
    filter:
      drop-shadow(0 0 1px  rgba(255,255,255,1))
      drop-shadow(0 0 4px  rgba(255,255,255,1))
      drop-shadow(0 0 10px rgba(255,255,255,1))
      drop-shadow(0 0 22px rgba(255,255,255,0.95))
      drop-shadow(0 0 45px rgba(255,255,255,0.80));
    opacity:1;
  }
  50%{
    filter:
      drop-shadow(0 0 1px  rgba(255,255,255,1))
      drop-shadow(0 0 5px  rgba(255,255,255,1))
      drop-shadow(0 0 14px rgba(255,255,255,1))
      drop-shadow(0 0 30px rgba(255,255,255,1))
      drop-shadow(0 0 60px rgba(255,255,255,0.90));
    opacity:1;
  }
}

/* Active — brackets clamp inward AND flare bright */
.filament:active::before,
.filament-pill:active::before,
.filament-close:active::before{
  transform:translateX(3px);
  filter:
    drop-shadow(0 0 2px  rgba(255,255,255,1))
    drop-shadow(0 0 6px  rgba(255,255,255,1))
    drop-shadow(0 0 16px rgba(255,255,255,1))
    drop-shadow(0 0 36px rgba(255,255,255,1))
    drop-shadow(0 0 70px rgba(255,255,255,0.90));
  opacity:1;
}
.filament:active::after,
.filament-pill:active::after,
.filament-close:active::after{
  transform:translateX(-3px);
  filter:
    drop-shadow(0 0 2px  rgba(255,255,255,1))
    drop-shadow(0 0 6px  rgba(255,255,255,1))
    drop-shadow(0 0 16px rgba(255,255,255,1))
    drop-shadow(0 0 36px rgba(255,255,255,1))
    drop-shadow(0 0 70px rgba(255,255,255,0.90));
  opacity:1;
}

.filament:active,
.filament-pill:active,
.filament-close:active{
  transform:scale(0.992);
}

/* A11Y: keyboard-only focus ring */
.filament:focus-visible,
.filament-pill:focus-visible,
.filament-close:focus-visible{
  outline:1px solid var(--fil-edge-soft);
  outline-offset:6px;
  border-radius:3px;
}

/* ── 1. Normal button — typography only, NO sizing ──
   Padding/width/font-size/letter-spacing left to the host. We just
   paint the cold-light text emission so any size button reads as
   "lit". The radius:3px is what the brackets visually demand — the
   chassis's emanation reads as architectural even if the host's
   underlying radius would be a 999px pill. */
.filament{
  border-radius:3px;
  color: rgba(255,255,255,0.55);
  text-shadow:
    0 0 4px  rgba(255,255,255,1),
    0 0 10px rgba(255,255,255,1),
    0 0 24px rgba(255,255,255,0.95),
    0 0 50px rgba(255,255,255,0.80);
}

/* ── 2. Close button — radius only, NO sizing ──
   Width / height / display / padding inherited from the host
   (.ov-close is 36x36, .mstack-close 44x44, .lb-close has its own).
   We need radius:3px so the chassis reads architectural and the
   .fil-bloom's matching radius:3px isn't visually misaligned. */
.filament-close{
  border-radius:3px;
}
/* The .x glyph carries the actual ✕ strokes. In the preview HTML
   the host was display:inline-flex which made .x (a span) layout
   as a flex item with respected width/height. Here we retrofit
   onto buttons with default inline-block display, so .x has to
   anchor itself absolutely against the host. The host's
   position:relative is already set by the chassis. */
/* Close glyph — render as a real lowercase 'x' character in the
   Safiro font (filament.js inserts the text). Earlier attempt
   used the '×' multiplication sign which is a different unicode
   codepoint; user wanted the Safiro 'x' letterform specifically.
   Centering uses flex (not just transform: translate(-50%, -50%))
   because text characters don't sit at the exact center of their
   em box — flex centering aligns the rendered glyph centrally
   inside a known box, which then sits centred via translate. */
/* TOUCH RELIABILITY: ensure ALL children of the close button are
   pointer-events:none so taps/clicks always reach the button
   itself. The chassis already sets pointer-events:none on the
   bracket pseudos, .fil-bloom, and the .x; this catch-all
   guarantees that future-added children (or any I missed) also
   pass touches through. touch-action:manipulation removes the
   300 ms tap delay on touch devices.
   PRIOR REGRESSION: had pointer-events:auto !important on the
   host as a "belt" — but on desktop that !important could
   interfere with CSS transitions that briefly disable pointer
   events during overlay open/close animations. Removed the
   !important; default button behaviour (pointer-events:auto)
   is correct on its own. */
.filament-close{
  touch-action:manipulation;
}
.filament-close > *{
  pointer-events:none;
}
.filament-close .x{
  position:absolute;
  top:50%;
  left:50%;
  /* Shrunk from 32×32 to 22×22 — at 26 px font size + 700 weight,
     the rendered glyph is ~16 px wide × ~16 px tall, so a 22 px
     box centres it comfortably without occupying 73% of the 44×44
     button surface. iOS Safari sometimes mis-hit-tests touch
     events on filtered children even with pointer-events:none,
     and the previous oversized 32 px .x box was likely the cause
     of "harder to tap" on the close buttons. Smaller box, less
     surface area for the hit-test confusion. */
  width:22px;
  height:22px;
  /* Extra belt: -webkit-touch-callout:none prevents iOS from
     intercepting the touch to show its long-press menu over the
     glyph. */
  -webkit-touch-callout:none;
  -webkit-user-select:none;
  user-select:none;
  /* Translate compensation for Safiro's lowercase 'x' metrics.
     Empirical sweep:
       -5 px (overshot, glyph in upper third)
        0 px (too low, glyph sat in lower half — baseline offset)
       -4 px (a bit too high)
       -3 px (this version — between -4 and the baseline-low) */
  transform:translate(-50%,calc(-50% - 3px));
  display:flex;
  align-items:center;
  justify-content:center;
  pointer-events:none;
  z-index:3;       /* above the .fil-bloom (z:0) but next to brackets (z:2) */
  /* Explicit font-family — defensive in case anything else in the
     cascade sets a different family on the close button's host. */
  font-family:'Safiro',sans-serif !important;
  /* Size + weight tuned to match the previous CSS-rectangle X
     (which was 16×4 px solid rectangles rotated 45° at the
     centre of a 44 px disc — visually ~18 px across with ~4 px
     stroke thickness). 26 px font-size renders the lowercase
     'x' at roughly that visible scale (x-height ~13 px), and
     700 weight gives a substantial stroke without the over-
     bold dominance of 900. Earlier 18+600 was too small,
     earlier 34+900 was too big — 26+700 splits the difference. */
  font-size:26px;
  line-height:1;
  font-weight:700;
  /* 0.92 alpha matches the previous CSS-rectangle X stroke
     colour exactly — slightly under full white so the blur
     below softens the edges into the halo cleanly instead of
     leaving a hard-edged white shape inside a soft glow. */
  color:rgba(255,255,255,0.92);
  -webkit-text-fill-color:rgba(255,255,255,0.92);
  /* HALO via text-shadow instead of filter:drop-shadow. The user
     reported close buttons registering visual tap (:active state)
     on mobile but the click event not firing. The cause is
     iOS Safari's known hit-test bug with `filter` on positioned
     children — even with pointer-events:none the filter creates
     a compositor layer that intercepts the touch's synthetic-
     click conversion. text-shadow doesn't create a compositor
     layer and doesn't affect hit-testing, so the tap → click
     conversion works reliably. Multiple text-shadows stack to
     produce the same luminous halo character the drop-shadow
     stack gave. Lost the 0.6 px filter:blur softening — minor
     trade for reliable taps. */
  text-shadow:
    0 0 6px rgba(255,255,255,1),
    0 0 16px rgba(255,255,255,0.80);
  transition:text-shadow 0.15s ease;
}
/* Belt-and-suspenders: if filament.js is still serving from cache
   with the old ensureX that creates an EMPTY .x span (no text
   inside), this CSS ::before paints the x glyph anyway. The new
   filament.js puts text content in the span directly; this rule
   only renders the pseudo when the span is empty (using :empty)
   so we don't double-paint after the JS reload. */
.filament-close .x:empty::before{
  content:'x';
}
.filament-close:hover .x{
  text-shadow:
    0 0 8px rgba(255,255,255,1),
    0 0 22px rgba(255,255,255,0.90);
}

/* ── 3. Checkbox ── */
.filament-checkbox{
  display:inline-flex; align-items:center; gap:14px;
  cursor:pointer;
  font-size:11px; letter-spacing:0.22em; text-transform:uppercase;
  color:rgba(255,255,255,0.85);
  text-shadow:0 0 8px rgba(255,255,255,1), 0 0 20px rgba(255,255,255,0.60);
  -webkit-tap-highlight-color:transparent;
}
.filament-checkbox input[type="checkbox"]{
  position:absolute; opacity:0; pointer-events:none;
}
.filament-checkbox-box{
  width:48px; height:28px; border-radius:3px;
  display:inline-flex; align-items:center; justify-content:center;
  flex-shrink:0;
}
.filament-checkbox-box .glyph{
  width:8px; height:8px; border-radius:1px;
  background:var(--fil-edge);
  opacity:0; transform:scale(0.4);
  filter:
    drop-shadow(0 0 3px rgba(255,255,255,0.95))
    drop-shadow(0 0 8px  rgba(255,255,255,0.55))
    drop-shadow(0 0 18px rgba(255,255,255,0.25));
  transition:opacity 0.28s ease, transform 0.28s cubic-bezier(0.34,1.56,0.64,1);
}
.filament-checkbox input:checked ~ .filament-checkbox-box .glyph{
  opacity:1; transform:scale(1);
}
.filament-checkbox input:checked ~ .filament-checkbox-box{
  background:var(--fil-body-checked);
}
.filament-checkbox input:checked ~ .filament-checkbox-box::before,
.filament-checkbox input:checked ~ .filament-checkbox-box::after{
  filter:
    drop-shadow(0 0 2px rgba(255,255,255,1))
    drop-shadow(0 0 8px  rgba(255,255,255,0.90))
    drop-shadow(0 0 18px rgba(255,255,255,0.60))
    drop-shadow(0 0 36px rgba(255,255,255,0.28));
}
.filament-checkbox input:focus-visible ~ .filament-checkbox-box{
  outline:1px solid var(--fil-edge-soft); outline-offset:4px;
}

/* ── 4. Textfield ── */
.filament-text-wrap{
  display:inline-block;
  width:360px;
  border-radius:3px;
  cursor:text;
}
.filament-text-wrap:focus-within{
  background:var(--fil-body-hover);
}
.filament-text-wrap:focus-within::before,
.filament-text-wrap:focus-within::after{
  animation:filamentBreathe 1.4s ease-in-out infinite;
}
.filament-text{
  display:block;
  width:100%;
  padding:10px 32px;
  background:transparent;
  border:none;
  border-radius:3px;
  position:relative;
  z-index:1;
  color:rgba(255,255,255,0.92);
  font-size:13px; letter-spacing:0.06em;
  font-family:inherit;
  outline:none;
  caret-color:var(--fil-edge);
}
.filament-text::placeholder{
  color:rgba(255,255,255,0.55);
  letter-spacing:0.18em;
  text-transform:uppercase;
  font-size:11px;
}

/* ── 5. Sticky pill — radius + text-glow only, NO sizing ──
   Like the rest, we let the host's own typography and width win.
   Just paint the lit emission. */
.filament-pill{
  border-radius:3px;
  color: rgba(255,255,255,0.55);
  text-shadow:
    0 0 4px  rgba(255,255,255,1),
    0 0 10px rgba(255,255,255,1),
    0 0 24px rgba(255,255,255,0.95),
    0 0 50px rgba(255,255,255,0.80);
}

/* ──────────────────────────────────────────────────────────────
   SITE INTEGRATION OVERRIDES
   ──────────────────────────────────────────────────────────────
   The shared.css/membrane.css selectors set their own padding,
   width, and background. When a button also has a filament class,
   we want the filament chassis to win for visual treatment (bg,
   color, brackets, bloom) but keep the site's sizing where it
   already feels right (the cookie strip buttons sit in a flex row
   with fixed widths, the .series-title needs its own typography
   hierarchy, etc.). Tweaks below.
   ────────────────────────────────────────────────────────────── */

/* The chassis no longer sets padding/font-size/letter-spacing/width
   so the choice-strip and cookie-panel buttons keep their native
   sizing from shared.css. No per-button overrides needed.

   .series-title likewise keeps its own typography. */

/* The close buttons (.ov-close / .mstack-close / .mjump-close)
   already use ::before/::after to draw their X strokes — rotated
   45/-45deg, 1px tall, centered via top:50%/left:50%. When we
   apply .filament-close, the SAME pseudo-elements need to become
   the filament BRACKETS (top:-3px to bottom:-3px, 22px wide, full
   height, NO rotation). Specificity between `.ov-close::before`
   and `.filament-close::before` is equal, so filament wins on
   properties it sets — but the rotate/height/left/background-color
   from shared.css would otherwise leak through.

   Below we explicitly reset those leaked properties. The visible X
   for these close buttons now comes from the injected <span class="x">
   child (handled by filament.js + the .filament-close .x rules
   above). */
.ov-close.filament-close::before,
.ov-close.filament-close::after,
.mstack-close.filament-close::before,
.mstack-close.filament-close::after,
.mjump-close.filament-close::before,
.mjump-close.filament-close::after{
  transform:none;
  height:auto;
  left:auto;
  right:auto;
  top:-3px;
  bottom:-3px;
  width:22px;
  background-color:transparent; /* prevent shared.css's var(--text-mid) bar */
}
.ov-close.filament-close::before,
.mstack-close.filament-close::before,
.mjump-close.filament-close::before{
  left:-6px;
  background-image:var(--fil-bracket-l);
}
.ov-close.filament-close::after,
.mstack-close.filament-close::after,
.mjump-close.filament-close::after{
  right:-6px;
  background-image:var(--fil-bracket-r);
}
/* Hover/active state — the close buttons' native pseudo rules in
   shared.css use the `background` SHORTHAND (e.g.
   `.ov-close:hover::before { background: var(--text-bright); }`).
   Background shorthand resets `background-image:none`, which would
   wipe the filament bracket SVG mid-hover and mid-press. The result
   would be: brackets visible at rest, then vanish on hover, then
   reappear on mouse-out — so the breathe animation has nothing to
   animate visually.
   These rules re-apply the bracket image with full specificity
   matching shared.css's hover/active rules (0,2,1). */
.ov-close.filament-close:hover::before,
.mstack-close.filament-close:hover::before,
.mjump-close.filament-close:hover::before,
.ov-close.filament-close:active::before,
.mstack-close.filament-close:active::before,
.mjump-close.filament-close:active::before{
  background:none;
  background-color:transparent;
  background-image:var(--fil-bracket-l);
  background-repeat:no-repeat;
  background-size:100% 100%;
}
.ov-close.filament-close:hover::after,
.mstack-close.filament-close:hover::after,
.mjump-close.filament-close:hover::after,
.ov-close.filament-close:active::after,
.mstack-close.filament-close:active::after,
.mjump-close.filament-close:active::after{
  background:none;
  background-color:transparent;
  background-image:var(--fil-bracket-r);
  background-repeat:no-repeat;
  background-size:100% 100%;
}

/* Active-state CLAMP — the bracket pseudos need to translateX
   inward on press. The chassis rule (.filament-close:active::before
   { transform:translateX(3px) }) has specificity 0,2,1, which TIES
   with my earlier reset block .ov-close.filament-close::before
   { transform:none }. Source order tiebreaker put the reset later,
   so it was winning the cascade in :active state — meaning the
   clamp wasn't visible.
   Solution: explicit active overrides at higher specificity (0,3,2)
   that re-assert the clamp translation. The breathe filter
   animation that runs on :hover keeps working because no rule here
   touches `filter`. */
.ov-close.filament-close:active::before,
.mstack-close.filament-close:active::before,
.mjump-close.filament-close:active::before,
.lb-close.filament-close:active::before{
  transform:translateX(3px) !important;
}
.ov-close.filament-close:active::after,
.mstack-close.filament-close:active::after,
.mjump-close.filament-close:active::after,
.lb-close.filament-close:active::after{
  transform:translateX(-3px) !important;
}
/* Hover — add the same breathe animation (already declared on
   .filament-close:hover, but might lose to source-order between
   my reset and the chassis active block). Re-declare here. */
.ov-close.filament-close:hover::before,
.ov-close.filament-close:hover::after,
.mstack-close.filament-close:hover::before,
.mstack-close.filament-close:hover::after,
.mjump-close.filament-close:hover::before,
.mjump-close.filament-close:hover::after,
.lb-close.filament-close:hover::before,
.lb-close.filament-close:hover::after{
  animation:filamentBreathe 1.4s ease-in-out infinite;
}
/* Strip the native border/background on close discs so the filament
   chassis is the only visual layer. Add the same outer halo
   box-shadow as the chrome buttons so the close discs share the
   exact same "round halo underneath" reading. */
/* Close buttons — NO body overrides from filament. The chassis
   brackets (::before/::after with 5-layer drop-shadow halo) and
   the .fil-bloom span sit on top of shared.css's original cluster
   styling: var(--glass-light-bg), var(--glass-light-border),
   var(--glass-filter), var(--glass-light-shadow). The close
   button looks EXACTLY as it did before any filament additions
   beyond brackets/bloom. */

/* ────── Filament + membrane stack ───────────────────────────
   When a button carries both data-membrane AND a filament class,
   filament defines the SILHOUETTE — architectural 3px corners and
   bracket pseudos — while membrane paints the interior surface.

   To make the silhouettes coherent, we force the host to filament's
   3px radius (membrane's .pill-surface inherits the radius, so the
   iridescent surface follows the architectural shape).

   The .fil-bloom (z:0) sits under the membrane surface (z:1) and is
   therefore visually masked by it. That's fine for the combo: the
   brackets + bracket drop-shadow halo are what carry the "cold light"
   reading; the iridescent membrane carries the body. The bloom only
   becomes the dominant interior visual on PURE-filament buttons.

   Text inside membrane'd buttons is already wrapped in .pill-text by
   membrane.js (z-index:5). filament.js detects this and skips its
   own .fil-label wrap to avoid double-wrapping. */
[data-membrane].filament,
[data-membrane].filament-pill,
[data-membrane].filament-close{
  border-radius:3px;
}
[data-membrane].filament > .pill-surface,
[data-membrane].filament-pill > .pill-surface,
[data-membrane].filament-close > .pill-surface{
  z-index:1;
  border-radius:3px;
}
[data-membrane].filament .pill-text,
[data-membrane].filament-pill .pill-text,
[data-membrane].filament-close .pill-text{
  position:relative;
  z-index:5;
}

/* ══════════════════════════════════════════════════════════════
   GLASS VARIANT — .fil-glass modifier
   ══════════════════════════════════════════════════════════════
   Now intentionally minimal: same emissive white SVG bracket +
   white halo as the chassis (close-button look) — the modifier's
   ONLY job is to suppress the .fil-bloom iridescent backdrop on
   hosts where the host's own backdrop-filter already supplies the
   glass body (e.g. .mreel-frame's faint-dark glass, .snav-item's
   light glass on hover/active).

   Earlier iterations tried "calm dark drop-shadow" and "flat
   gradient bracket" variations — the user kept asking for the
   close-button look everywhere, so we converge.
   ══════════════════════════════════════════════════════════════ */
/* No bloom on glass — the chassis adds .fil-bloom if ensureBloom
   was called, but glass variants skip ensureBloom (see filament.js
   apply()). Belt-and-braces: any stray bloom is hidden. */
.fil-glass > .fil-bloom{
  display:none !important;
}

/* ── .mreel-frame — close-button-look loupe pane in the mobile
   menu slot machine.
   ─────────────────────────────────────────────────────────────
   The chassis selector .filament-pill sets background:
   var(--fil-body-rest) which would replace shared.css's faint-dark
   body (rgba(40,38,36,0.10)) — leaving the pane with no visible
   glass body, only brackets. Re-assert the close-button-family
   body explicitly here. */
/* Loupe pane — slightly darker body (per "even darken it a bit")
   WITHOUT the dark border the user complained about. The user
   wants the darker body so the white text passing through reads
   more intensely, but they explicitly said no dark edge — so
   border:none. */
.mreel-frame.fil-glass{
  border-radius:3px !important;
  /* Slight bump from 0.20 → 0.32 so the loupe pane reads more
     clearly as a dark window over the lighter reel content
     behind it. */
  background:rgba(40,38,36,0.32) !important;
  border:none !important;
  backdrop-filter:blur(6px) !important;
  -webkit-backdrop-filter:blur(6px) !important;
  box-shadow:none !important;
}
/* On subpages (mstack overlay mode), the loupe pane is anchored
   asymmetrically so its right edge ends cleanly before the close
   button. Close button sits at right:14 + width:44 = takes 58 px
   from the right edge. Anchoring the pane at right:72 leaves a
   14 px gap between the pane's right edge and the close button's
   left edge — clear visual separation without making the pane
   unnecessarily narrow.
   max-width belt-and-braces: explicitly cap the pane's width to
   viewport-width minus margins so it cannot overflow even if the
   right anchor rule loses the cascade for any reason. */
.mstack[data-overlay] .mreel-frame.fil-glass{
  right:72px !important;
  max-width:calc(100vw - 88px) !important;
  box-sizing:border-box !important;
}

/* mreel-item.cur — the active slot-machine label INSIDE the loupe
   pane. White text, no drop-shadow halo per user request (the
   reel labels were blooming too heavily — the same family pattern
   we used elsewhere, flat white text reads cleaner). */
@media(max-width:700px){
  .mreel-item.cur{
    color:rgba(255,255,255,0.95) !important;
    -webkit-text-fill-color:rgba(255,255,255,0.95) !important;
    filter:none !important;
    text-shadow:none !important;
  }
}

/* ── snav-item (desktop sidebar nav rows) as filament-glass-pill
   ───────────────────────────────────────────────────────────────
   Three things this block has to fix:

   1. The chassis selector `.filament, .filament-pill, ... { color:
      rgba(255,255,255,0.92) }` forces the text to nearly-white,
      which is invisible on the light sidebar bg. Override default
      to var(--text-mid) and on hover/active to cream-white (which
      reads against the dark glass body, like the close-button X
      strokes against the close button's glass body).

   2. Brackets at left:-6px / right:-6px were extending past the
      narrow .bar-series column and triggering horizontal scroll.
      Move them to left:0 / right:0 (anchored at the host's edge,
      inside the host's 22px padding). The chassis pseudo width
      is 22px so they fit cleanly in that padding without
      overlapping the .snav-num / .snav-name / .snav-year content
      that starts at the inside of the padding.

   3. Brackets only visible on :hover / .active. The chassis
      paints them always; this gates with opacity.
   ─────────────────────────────────────────────────────────────── */

/* Default state — text back to shared.css's --text-mid (the
   chassis would otherwise paint it white).
   Snappy transitions: shared.css sets `transition: all 0.2s` on
   .snav-item which animates EVERYTHING (including the expensive
   backdrop-filter) when moving between rows — that's the sluggish
   feel the user reported. Restrict the transition to bg + color
   (cheap) and snappier (0.12s). The backdrop-filter swap on
   :hover stays GPU-instant. */
.snav-item.fil-glass{
  color:var(--text-mid) !important;
  -webkit-text-fill-color:var(--text-mid) !important;
  transition:background 0.12s ease, color 0.12s ease !important;
  /* PERF: kill the .filament-pill chassis text-shadow at the host
     level so it never inherits to children. Previously the host
     inherited the 4/10/24/50 px chassis text-shadow stack, and
     on row-to-row hover handoff there was a frame where my
     :hover-state text-shadow:none override stopped matching on
     the row being un-hovered, so the chassis text-shadow flashed
     visibly for that frame — the "millisecond text bloom" user
     reported. Setting it none on the host itself means children
     inherit nothing, and there's no hover→non-hover gap. */
  text-shadow:none !important;
  /* PERF: contain layout changes inside this row so .snav-explore's
     height transition doesn't trigger reflow up the .bar-series
     parent tree. Each row owns its own reflow scope. This is what
     makes the smooth grow animation cheap enough to leave on. */
  contain:layout;
}
/* Brackets default = invisible AND vertically collapsed; on hover
   and .active they fade in AND scale to full height — the
   "growing in size" effect the user asked for. Brackets stay at
   left:0 / right:0 (inside the row's padding) because the parent
   .bar-series has overflow-y:auto which forces overflow-x:auto
   too, and left:-6 / right:-6 brackets caused a horizontal scroll
   bar. */
.snav-item.fil-glass::before,
.snav-item.fil-glass::after{
  /* Brackets anchored INSIDE the host (top:0 / bottom:0 instead
     of the chassis default -3 / -3). The first row in the list
     sits at the top of .bar-series, which has overflow-y:auto —
     a -3 px top extension on that row got clipped by the scroll
     container's overflow boundary, so the top hook of the bracket
     was missing on the topmost row (user's screenshot). Anchoring
     flush at top:0 keeps the bracket fully visible inside the
     row's own bounds and the bracket still spans the full row
     height (which is what carries the "growth on expand"). */
  top:0 !important;
  bottom:0 !important;
  height:auto !important;
  opacity:0;
  /* PERF: opacity-only transition (snap everything else).
     Overrides the chassis's transition:transform 0.08s ease,
     filter 0.10s ease, opacity 0.12s ease which was driving
     additional layout/paint cost per frame during row-to-row
     hover handoff. */
  transition:opacity 0.18s ease !important;
  /* PERF: override the chassis's will-change:filter,transform
     on :hover. That hint creates a new GPU compositing layer
     per row at hover-time and tears it down on unhover — during
     quick row-to-row moves this thrashes layers across all the
     rows you pass through. With the animation/filter changes
     disabled here, no layer hint is needed. */
  will-change:auto !important;
  left:auto !important;
  right:auto !important;
  /* (v=102 — translateZ on snav-item bracket pseudos REMOVED entirely.
     Tested in v=97 (always-on), v=100 (always-on again), and v=101
     (state-conditional on :hover/.active). All three variants slowed
     down the sticky-header morph regardless of how few promoted layers
     existed. Hypothesis: even the rule-evaluation cost of an
     additional :hover/.active selector match on snav-items, combined
     with whatever compositor-tree side-effects translateZ has on
     Chromium, was bleeding into the sticky morph's frame budget. The
     snav-item hover smoothness benefit of translateZ wasn't worth the
     sticky regression. Reverted entirely. Trade-off acknowledged: the
     snav-item hover-handoff has the 5-layer drop-shadow per-frame
     recomputation cost. Alternative optimizations to explore if
     needed: reduce snav-item drop-shadow layer count, use will-change
     ONLY on hover, or replace the drop-shadow halo with a baked-in
     SVG glow on the bracket asset. None tried yet.) */
}
.snav-item.fil-glass::before{
  left:0 !important;
  /* CSS-gradient bracket (same pattern as .page-header-sticky):
     fixed-size hooks at top + bottom corners, vertical bar that
     stretches between them. As the row grows when .snav-explore
     expands, the bar grows but the hooks stay small — that's
     the "dynamic height change" the user wanted. The previous
     SVG bracket stretched as one image, so hook size grew with
     the row and the growth wasn't visually distinct. */
  background-image:
    linear-gradient(#ffffff,#ffffff),
    linear-gradient(#ffffff,#ffffff),
    linear-gradient(#ffffff,#ffffff) !important;
  background-position:
    0 0,           /* top hook at top-left */
    0 100%,        /* bottom hook at bottom-left */
    0 0 !important; /* vertical bar at left edge */
  background-size:
    12px 3px,
    12px 3px,
    3px 100% !important;
  background-repeat:no-repeat,no-repeat,no-repeat !important;
}
.snav-item.fil-glass::after{
  right:0 !important;
  background-image:
    linear-gradient(#ffffff,#ffffff),
    linear-gradient(#ffffff,#ffffff),
    linear-gradient(#ffffff,#ffffff) !important;
  background-position:
    100% 0,        /* top hook at top-right */
    100% 100%,     /* bottom hook at bottom-right */
    100% 0 !important; /* vertical bar at right edge */
  background-size:
    12px 3px,
    12px 3px,
    3px 100% !important;
  background-repeat:no-repeat,no-repeat,no-repeat !important;
}
/* Bracket glow — full 5-layer halo matching the chassis close-
   button stack. With contain:layout scoping the row's reflow,
   the GPU cost stays paid-per-row and we can afford the heavy
   outer 45 px shadow that gives the brackets their fully lit-up
   character. */
.snav-item.fil-glass::before,
.snav-item.fil-glass::after{
  filter:
    drop-shadow(0 0 1px rgba(255,255,255,1))
    drop-shadow(0 0 4px rgba(255,255,255,1))
    drop-shadow(0 0 10px rgba(255,255,255,1))
    drop-shadow(0 0 22px rgba(255,255,255,0.95))
    drop-shadow(0 0 45px rgba(255,255,255,0.80)) !important;
}
/* PERF: kill the filamentBreathe animation on snav-item brackets.
   The chassis applies it on every .filament-pill:hover::before/
   ::after — a 5-layer drop-shadow filter animated at 60 fps for
   1.4 s infinite. During row-to-row hover handoff TWO rows were
   running this simultaneously, which was the dominant lag
   source. The brackets here just fade in to a static state and
   stay there. */
.snav-item.fil-glass:hover::before,
.snav-item.fil-glass:hover::after,
.snav-item.fil-glass.active::before,
.snav-item.fil-glass.active::after{
  opacity:1;
  animation:none !important;
}
/* Repaint trigger — applied by shared.js's _repaintActiveSnavBrackets
   for two frames after openOv()/closeOv() to force the bracket
   pseudo's cached `background-size: ... 100%` gradient to
   re-rasterise against the host's current height. Chromium/Safari
   occasionally skip this invalidation when the overlay's compositor
   layer is added or removed, which is the source of the
   intermittent off-axis bracket frames after overlay state changes.
   The tiny translateZ promotes the pseudo onto a fresh layer
   momentarily; the class is removed two frames later so we don't
   leave a permanent promoted layer behind. */
.snav-item.fil-glass.snav-bracket-repaint::before,
.snav-item.fil-glass.snav-bracket-repaint::after{
  transform:translateZ(0);
}
/* Hover / active body — DARK glass so white text reads on it.
   PERF FIX: dropped the per-row backdrop-filter:var(--glass-
   filter) (blur 24 saturate 200 brightness 1.05). User reported
   lag moving between rows — and they were right: backdrop-filter
   is one of the most expensive CSS operations the GPU can do,
   and during row-to-row hover transition you'd have TWO
   simultaneous backdrop-filter computations (old row fading,
   new row fading) plus the .snav-explore max-height transition
   on both rows plus the bracket pseudo drop-shadow halos. The
   parent .left-bar (.liquid-surface) already has its own
   backdrop-filter, so the per-row blur was largely redundant
   work. Without it, the row reads as a flat-ish dark glass tile
   (rgba 20,18,16,0.42 fill + the bright inset edge highlights
   from the box-shadow stack), which is essentially how it
   already read — same visual, no GPU thrash. */
.snav-item.fil-glass:hover,
.snav-item.fil-glass.active{
  /* User reported rows still too dark vs before. The pre-fix
     backdrop-filter included saturate(200%) brightness(1.05) on
     top of the dark fill — those two filters lifted the apparent
     brightness ~20% even though the fill was 0.42 alpha. Without
     backdrop-filter, the raw fill reads darker. Drop alpha to 0.28
     to match the previous perceived brightness while keeping the
     row recognisable as a dark glass tile. */
  background:rgba(20,18,16,0.28) !important;
  /* outline (not border) — avoids the 1px box-sizing shift that
     would slide the right-anchored .snav-year on hover. */
  outline:1px solid rgba(255,255,255,0.30) !important;
  outline-offset:-1px !important;
  box-shadow:
    0 8px 24px rgba(20,18,16,0.18),
    0 2px 4px rgba(20,18,16,0.08),
    inset 0 1.4px 0 rgba(255,255,255,0.45),
    inset 0 3px 4px rgba(255,255,255,0.10),
    inset 0 -1px 0 rgba(20,18,16,0.10),
    inset 0 0 0 0.5px rgba(255,255,255,0.20) !important;
}
/* Hover / active text — white with drop-shadow halo (same as X
   strokes). Apply filter to each text-bearing child so the glow
   renders identically to the close-button content. */
.snav-item.fil-glass:hover,
.snav-item.fil-glass.active,
.snav-item.fil-glass:hover .snav-name,
.snav-item.fil-glass.active .snav-name,
.snav-item.fil-glass:hover .snav-num,
.snav-item.fil-glass.active .snav-num,
.snav-item.fil-glass:hover .snav-year,
.snav-item.fil-glass.active .snav-year,
.snav-item.fil-glass:hover .snav-explore-inner,
.snav-item.fil-glass.active .snav-explore-inner{
  /* User requested "superwhite when selected" — pure white at
     alpha 1.0, not 0.95. */
  color:rgba(255,255,255,1) !important;
  -webkit-text-fill-color:rgba(255,255,255,1) !important;
}
.snav-item.fil-glass:hover .snav-name,
.snav-item.fil-glass.active .snav-name,
.snav-item.fil-glass:hover .snav-num,
.snav-item.fil-glass.active .snav-num,
.snav-item.fil-glass:hover .snav-year,
.snav-item.fil-glass.active .snav-year,
.snav-item.fil-glass:hover .snav-explore-inner,
.snav-item.fil-glass.active .snav-explore-inner{
  /* Two glow sources to kill:
     1. filter:drop-shadow that used to sit on these spans directly
        (4/10/22 px halo) — set to filter:none.
     2. text-shadow INHERITED from .filament-pill's chassis rule
        (4/10/24/50 px white stack at line 4164). text-shadow
        inherits, so the title/number/year/sublabel inherited the
        glow even after I killed the filter. Explicit text-shadow:
        none here finally shuts it off.
     The brackets + dark glass tile carry the lit reading; text
     reads as flat pure white. */
  filter:none !important;
  text-shadow:none !important;
}
/* (.snav-explore expansion for .active state moved into the
   combined :hover + .active rule at line ~1204 so there's a
   single source of truth. Deleted duplicate that lived here.) */

/* ── about-links on mobile as filament-glass-pill ─────────────
   The .about-links container is membrane'd on mobile (member.js's
   tagSpecificButtons applies data-floor="about"). User wants glass
   brackets instead. Hide the membrane pill-surface on mobile, show
   the brackets only at mobile widths.
   On desktop the container is a flat text-link row — we hide the
   brackets entirely so it stays as-is. */
/* About-links — bracketed pill design on ALL viewports. Previously
   gated to mobile only with a flat-text-row on desktop; user
   requested the same pill design everywhere with one pair of
   brackets framing all 4 links. Bracket pseudo opacity:1 and
   bloom display:block at the global scope. Visual recipe (bg /
   border / etc.) lives in the always-on family block at end of
   file. */
.about-links.filament-pill::before,
.about-links.filament-pill::after{
  opacity:1;
}
.about-links.filament-pill > .pill-surface{
  display:none !important;
}

/* ────── Sticky header pill — filament gated on .compact ──────
   The .page-header-sticky carries the filament-pill class always
   (because filament.js applies it once on init, and class toggles
   don't refire the MutationObserver's childList-only callback).
   CSS hides the bracket pseudos and the bloom whenever the host
   is NOT in .compact mode — the full-mode header doesn't get an
   outsized iridescent blob behind it.

   ALSO the bracket is REDESIGNED here, because the SVG-based
   bracket from filament-preview-v3 was designed for 36-44px tall
   buttons; on a multi-line compact pill (100-180px tall), the SVG
   stretches and the corner hooks balloon to 20-30px tall, which
   looks distorted. Below we replace the bracket image with three
   CSS gradients — top hook (fixed 4px tall), bottom hook (fixed
   4px tall), vertical bar (100% height). That keeps the bracket
   marks small at the corners regardless of pill height, with a
   clean continuous vertical bar in between. The white drop-shadow
   stack from the chassis still wraps around the gradient shape.

   Exceptions to chassis defaults
   - DON'T apply border-radius:3px to the sticky; shared.css gives
     the compact pill a 28px radius that we want to keep.
   - DON'T force background:var(--fil-body-rest) on the sticky;
     the membrane surface owns the interior fill.
   ─────────────────────────────────────────────────────────────── */
.page-header-sticky.filament-pill{
  border-radius:inherit;    /* preserve shared.css's pill geometry */
  background:transparent;
}
/* MEMBRANE owns the iridescence on the sticky header. The earlier
   `display:none` on .pill-surface killed the shape-shifting and
   the moving iridescence the user actually wanted. Membrane is back;
   the duplication is solved by hiding FILAMENT's own .fil-bloom
   instead (see below) — keeping the membrane iridescence the user
   originally specced.

   Override the surface's border-radius from the inherited 28px pill
   to 3px so the iridescent material reads as the BRACKETED rectangle
   the user asked for ("membrane should fill the brackets instead of
   being a pill shape"). */
.page-header-sticky.filament-pill > .pill-surface{
  border-radius:3px !important;
}

/* STACKING FIX: membrane.js's wireStickyHeader injects the
   .pill-surface as a position:absolute layer but does NOT
   add the data-membrane attribute to .page-header-sticky
   (only applyMembraneTo does, and wireStickyHeader uses its
   own path). So the [data-membrane] > *:not(.pill-surface)
   rule that gives sibling children z-index:5 never matches
   the sticky header's children, and the positioned pill-
   surface paints OVER the static-positioned text content
   (.header-left, .series-title, etc.) — user reported the
   text was "underneath the membrane layers." Explicit
   stacking here mirrors what the data-membrane rule would
   have done: pill-surface at z:1, all other direct children
   at z:5. */
.page-header-sticky > .pill-surface{
  z-index:1;
}
.page-header-sticky > *:not(.pill-surface){
  position:relative;
  z-index:5;
}
/* Hide filament's own bloom — the membrane surface above is the
   single iridescence source now. */
.page-header-sticky.filament-pill > .fil-bloom{
  display:none !important;
}
/* Brackets — hidden by default, ONLY visible when the sticky
   header is in .compact mode. Earlier the brackets were always
   on; user reported they were rendering in full mode too where
   they shouldn't be. Compact-gating below.
   ALSO anchored to top:0 / bottom:0 INSIDE the host (chassis
   default is top:-3 / bottom:-3 which extended past the host
   bounds). On the sticky header, the host sits at the very top
   of the viewport, so a -3 px top extension renders behind the
   browser chrome / outside the visible page area. top:0 keeps
   the top hook visible at the host's actual top edge.
   Same reasoning for left:0 / right:0 (chassis default -6 / -6
   would extend behind the sidebar on the left and past the page
   edge on the right). */
.page-header-sticky.filament-pill::before,
.page-header-sticky.filament-pill::after{
  opacity:0;
  pointer-events:none;
  top:0 !important;
  bottom:0 !important;
  left:auto !important;
  right:auto !important;
  /* STACKING: chassis sets bracket pseudos at z-index:2. The
     recent .page-header-sticky > *:not(.pill-surface) fix sets
     all direct children to z-index:5 (so text reads above the
     pill-surface). But that puts the children ABOVE the bracket
     pseudos too — content in .header-right that extends to the
     right edge then paints over the right bracket. Bumping
     brackets to z:10 keeps them on top of both pill-surface
     (z:1) AND the text children (z:5). */
  z-index:10 !important;
  /* Full 5-layer halo — same stack the chassis uses everywhere
     else. User wanted stronger luminescent glow; with the
     opacity snap (no fade transition) and will-change:auto
     overrides above keeping the GPU layer count down, we can
     afford the heavier 22 / 40 px outer shadows that give the
     halo its full lit character. */
  filter:
    drop-shadow(0 0 1px rgba(255,255,255,1))
    drop-shadow(0 0 4px rgba(255,255,255,1))
    drop-shadow(0 0 10px rgba(255,255,255,1))
    drop-shadow(0 0 22px rgba(255,255,255,0.95))
    drop-shadow(0 0 45px rgba(255,255,255,0.80)) !important;
  /* v=85 — ASYMMETRIC OPACITY TRANSITIONS.
     The two directions of the sticky morph have different costs:
       · Full→compact: layout shrinks; brackets shrink with it;
         their drop-shadow re-rasterizes per frame as size changes.
         Solution: keep brackets at opacity:0 for the first ~0.12s
         of the morph (covering the heaviest layout reshape) then
         fade in over the next 0.15s. By the time they're visible
         most of the size change is done — drop-shadow rasterizes
         a small number of times instead of ~21.
       · Compact→full: layout grows; brackets would grow with it;
         drop-shadow re-rasterizes per frame again.
         Solution: brackets snap to opacity:0 INSTANTLY when
         .compact is removed (no transition on base rule). The
         drop-shadow contributes zero frames to the reverse morph.
     The browser picks the transition definition from the
     destination state, so putting the slow transition on .compact
     handles full→compact and putting no transition on the base
     rule handles compact→full. v=84 had a single transition that
     applied to both directions; the reverse direction was the
     remaining lag the user reported. */
  transition:none;
  will-change:auto !important;
}
.page-header-sticky.filament-pill.compact::before,
.page-header-sticky.filament-pill.compact::after{
  opacity:1;
  transition:opacity 0.15s ease 0.12s;
}
.page-header-sticky.filament-pill::before{
  left:0 !important;
}
.page-header-sticky.filament-pill::after{
  right:0 !important;
}

/* Redraw the brackets using three stacked CSS gradients so the
   vertical bar grows with the pill but the hooks stay fixed.
   - Layer 1: top hook (15px × 4px, anchored top-left for ::before,
     top-right for ::after).
   - Layer 2: bottom hook (15px × 4px, anchored bottom-left /
     bottom-right).
   - Layer 3: vertical bar (4px × 100%, anchored left/right edge). */
/* (.page-header-sticky.filament-pill::before / ::after bracket
   gradients consolidated with .mdock + .sdr-* in the shared
   "three-stacked-gradient brackets" rule at end of file.) */

/* SECONDARY BUTTONS — no iridescence on customize/decline. Hide
   the iridescent conic-gradient pseudos so they read as the
   "secondary" treatment user requested. */
.choice-strip-customize.filament > .fil-bloom::before,
.choice-strip-customize.filament > .fil-bloom::after,
.choice-strip-decline.filament > .fil-bloom::before,
.choice-strip-decline.filament > .fil-bloom::after{
  display:none !important;
}
/* SECONDARIES: customize / decline / save-preferences.
   User wants these to read as clearly less prominent than the
   accept-all primary. Tuning all four contrast dials together:
   1. Brackets at 40% opacity — faded outlines, no halo.
   2. Bloom darkness 0.15 — much lighter than the 0.50 primary.
   3. Bg darkness 0.05 dark — barely-tinted glass.
   4. Text at 0.65 alpha white — dim, readable against the slight
      dark backdrop but clearly de-emphasised vs the primary's
      pure-white text.
   Net effect: secondaries read as faded glass chips while the
   accept-all primary punches through with full luminescence,
   bright fill, saturated iridescence, and bold white text. */
.choice-strip-customize.filament::before,
.choice-strip-customize.filament::after,
.choice-strip-decline.filament::before,
.choice-strip-decline.filament::after,
.cookie-save-btn.filament::before,
.cookie-save-btn.filament::after{
  filter:drop-shadow(0 0 1px rgba(255,255,255,0.4)) !important;
  opacity:0.55 !important;
}
.choice-strip-customize.filament > .fil-bloom,
.choice-strip-decline.filament > .fil-bloom,
.cookie-save-btn.filament > .fil-bloom{
  background:rgba(0,0,0,0.15) !important;
}
/* Dim the text on secondaries so it reads as the muted choice
   relative to the primary's bright white. */
.choice-strip-customize.filament,
.choice-strip-customize.filament .fil-label,
.choice-strip-customize.filament .pill-text,
.choice-strip-decline.filament,
.choice-strip-decline.filament .fil-label,
.choice-strip-decline.filament .pill-text,
.cookie-save-btn.filament,
.cookie-save-btn.filament .fil-label,
.cookie-save-btn.filament .pill-text{
  color:rgba(255,255,255,0.65) !important;
  -webkit-text-fill-color:rgba(255,255,255,0.65) !important;
}
/* Also kill iridescence on save-preferences (customize/decline
   have their .fil-bloom pseudos display:none-d in the rule
   above; save-preferences uses the family screen-blend boost so
   we need to suppress it specifically). */
.cookie-save-btn.filament > .fil-bloom::before,
.cookie-save-btn.filament > .fil-bloom::after{
  display:none !important;
}

/* ACCEPT-ALL — PROMINENT primary button. Now that customize and
   decline are dimmed to glass-style, accept-all needs to read
   as clearly the primary CTA. Four layers of emphasis:
   1. Darker, more saturated host fill for contrast against the
      cream banner backdrop and against the dimmed secondaries.
   2. 7-layer bracket drop-shadow halo extending to 120 px with
      full-alpha inner stops — brackets read as intense emitters
      of light, the kind of glow that draws the eye unmistakably
      to the CTA.
   3. Stronger iridescence: opacity 0.90 / 0.85 (vs family token
      defaults of 0.50 / 0.45) and saturate 2.0 / 1.6 so the
      conic-gradient pastels read at full colour, not muted.
   4. Bolder text + thicker letter-spacing for typographic weight.
   The screen-blend iridescence mode already applies via the
   close-disc + cookie-button selector lists (line 5305+); these
   overrides amplify it on this specific host. */
.choice-strip-accept.filament,
.cookie-accept-all-btn.filament{
  background:rgba(20,18,16,0.55) !important;
  font-weight:600 !important;
  letter-spacing:0.20em !important;
  /* OUTER GLOW via box-shadow — capped at 30 px max blur (was
     120 px which was a major perf hit). Two cheaper layers give
     the prominence without thrashing the compositor. */
  box-shadow:
    0 0 12px rgba(255,255,255,0.55),
    0 0 30px rgba(255,255,255,0.25),
    /* family inset highlights preserved */
    inset 0 1.4px 0 rgba(255,255,255,0.85),
    inset 0 3px 4px rgba(255,255,255,0.35),
    inset 0 -1px 0 rgba(20,18,16,0.06),
    inset 0 0 0 0.5px rgba(255,255,255,0.40) !important;
}
.choice-strip-accept.filament::before,
.choice-strip-accept.filament::after,
.cookie-accept-all-btn.filament::before,
.cookie-accept-all-btn.filament::after{
  /* Bracket halo at chassis 5-layer recipe — was 7 layers up to
     120 px which paired with the filamentBreathe animation
     meant the GPU was animating ~12 filter stops per frame. */
  filter:
    drop-shadow(0 0 1px rgba(255,255,255,1))
    drop-shadow(0 0 4px rgba(255,255,255,1))
    drop-shadow(0 0 10px rgba(255,255,255,1))
    drop-shadow(0 0 22px rgba(255,255,255,0.95))
    drop-shadow(0 0 45px rgba(255,255,255,0.80)) !important;
  /* PERF: kill the chassis filamentBreathe animation that runs
     on :hover for filament buttons. The breathe animates a
     5-layer drop-shadow filter at 60 fps continuously — on a
     CTA that's already heavily filtered, the cost compounds. */
  animation:none !important;
}
.choice-strip-accept.filament:hover::before,
.choice-strip-accept.filament:hover::after,
.cookie-accept-all-btn.filament:hover::before,
.cookie-accept-all-btn.filament:hover::after{
  animation:none !important;
}
.choice-strip-accept.filament > .fil-bloom,
.cookie-accept-all-btn.filament > .fil-bloom{
  background:rgba(0,0,0,0.50) !important;
}
.choice-strip-accept.filament > .fil-bloom::before,
.cookie-accept-all-btn.filament > .fil-bloom::before{
  opacity:0.75 !important;
  filter:blur(14px) saturate(1.5) !important;
}
.choice-strip-accept.filament > .fil-bloom::after,
.cookie-accept-all-btn.filament > .fil-bloom::after{
  opacity:0.70 !important;
  filter:blur(18px) saturate(1.3) !important;
}
/* Mobile-only: nudge the accept-all bg a touch darker. Desktop
   stays at 0.55 alpha (per user feedback the desktop balance is
   right); mobile gets 0.68 so the CTA reads with a bit more
   weight against the privacy strip's panel-strong backdrop. */
@media(max-width:700px){
  .choice-strip-accept.filament,
  .cookie-accept-all-btn.filament{
    background:rgba(20,18,16,0.68) !important;
  }
}

/* Snav-item hover/active reveals the bloom (.fil-glass hides it
   by default). Same chassis recipe — iridescent layers visible. */
.snav-item.fil-glass:hover > .fil-bloom,
.snav-item.fil-glass.active > .fil-bloom{
  display:block !important;
}

/* SUBSCRIBE FORM — text input + checkboxes ────────────────────
   The email input keeps its native sizing from .sdr-form-field
   shared.css rules; .filament-text below makes it transparent so
   the wrapper's brackets + dark bloom show through. The native
   border/background on the input are neutralised here.
   The checkbox box gets filament chassis treatment — the native
   <input type="checkbox"> is hidden by the chassis selector
   (.filament-checkbox input[type="checkbox"] { opacity:0 }), and
   the injected .filament-checkbox-box span carries the visual.
   ────────────────────────────────────────────────────────────── */
.sdr-form-field.filament-text-wrap{
  width:100%;
  display:block;
  cursor:text;
}
.sdr-form-field input.filament-text{
  background:transparent !important;
  border:none !important;
  border-radius:3px !important;
  color:rgba(255,255,255,0.95) !important;
  -webkit-text-fill-color:rgba(255,255,255,0.95) !important;
  padding:14px 18px !important;
  position:relative;
  z-index:5;
  filter:
    drop-shadow(0 0 4px rgba(255,255,255,1))
    drop-shadow(0 0 10px rgba(255,255,255,0.85))
    drop-shadow(0 0 22px rgba(255,255,255,0.55));
}
.sdr-form-field input.filament-text::placeholder{
  color:rgba(255,255,255,0.75) !important;
  -webkit-text-fill-color:rgba(255,255,255,0.75) !important;
}

/* Checkbox host: the <label> picks up filament-checkbox class.
   Its child .filament-checkbox-box is the actual chassis. Keep the
   label's flex layout (label text sits to the right of the box). */
.sdr-groups label.filament-checkbox{
  display:flex;
  align-items:center;
  gap:12px;
  cursor:pointer;
}
/* The original .sdr-group-row input[type="checkbox"] in shared.css
   styles the input visibly (appearance:none, custom box). filament
   hides it; neutralise the sizing so it doesn't take layout space. */
.sdr-groups label.filament-checkbox input[type="checkbox"]{
  width:0 !important;
  height:0 !important;
  margin:0 !important;
  appearance:none !important;
  -webkit-appearance:none !important;
}

/* .lb-close-specific pseudo reset. Uses !important because
   shared.css has `body[data-page="sub"] .lb-close::before` rules
   (specificity 0,3,2) which beat our normal `.lb-close.filament-close::before`
   (0,3,1). The body-attribute selector wraps the whole subpage
   ruleset for the lightbox so it scopes its X-strokes to subpages
   only — but on subpages those rules win unless we elevate.

   Scoped with :not(.fil-glass) so the GLASS variant (when the lb-close
   is wired as filament-glass-close) can apply its own translucent
   gradient brackets without losing to these !important assertions. */
.lb-close.filament-close::before,
.lb-close.filament-close::after{
  transform:none !important;
  height:auto !important;
  left:auto;
  right:auto;
  top:-3px !important;
  bottom:-3px !important;
  width:22px !important;
  background:none !important;
  background-color:transparent !important;
  background-repeat:no-repeat !important;
  background-size:100% 100% !important;
}
.lb-close.filament-close::before{
  left:-6px !important;
  background-image:var(--fil-bracket-l) !important;
}
.lb-close.filament-close::after{
  right:-6px !important;
  background-image:var(--fil-bracket-r) !important;
}
/* Hover + active — re-assert the bracket image on top of shared.css's
   `:hover::before { background:var(--text-bright) }` shorthand reset.
   Also scoped to :not(.fil-glass) for the same reason. */
.lb-close.filament-close:hover::before,
.lb-close.filament-close:active::before{
  background:none !important;
  background-color:transparent !important;
  background-image:var(--fil-bracket-l) !important;
  background-repeat:no-repeat !important;
  background-size:100% 100% !important;
}
.lb-close.filament-close:hover::after,
.lb-close.filament-close:active::after{
  background:none !important;
  background-color:transparent !important;
  background-image:var(--fil-bracket-r) !important;
  background-repeat:no-repeat !important;
  background-size:100% 100% !important;
}

/* (Old .lb-close.fil-glass overrides removed — lb-close no longer
   uses the glass variant. It's plain filament-close now, matching
   mstack-close.) */

/* ────── Bracket position INSIDE the host for overflow-clipped
/* FILAMENT FAMILY — unified recipe driven by --fil-fam-* tokens
   at :root. One selector list, one set of declarations. Edit a
   token at :root and every host below updates at once.
   .page-header-sticky and .filament-close stay out (their own
   chassis); .fil-glass variants stay out too. */
.mjump-title.filament-pill,
.cookie-accept-all-btn.filament,
.cookie-save-btn.filament,
.choice-strip-accept.filament,
.choice-strip-decline.filament,
.choice-strip-customize.filament,
.priv-cookies-cta .filament,
#ov-privacy .priv-cookies-cta button.filament,
#ov-privacy .priv-cookies-cta.ov-bottom button.filament,
.sdr-submit-btn.filament,
.sdr-form-field.filament-text-wrap,
.about-links.filament-pill,
.mdock.filament-pill,
.mhint.filament-pill,
#opv-strip.filament-pill,
#opv-cta-btn.filament-pill,
.mob-menu-btn.filament-pill,
.acq-enquire.filament-pill,         /* acquisition overlay "enquire" CTA — same glass chip */
.mob-book-bar-btn.filament-pill{   /* equilibrium "book class level 1" — identical glass chip to the menu button */
  background:var(--fil-fam-bg) !important;
  border:var(--fil-fam-border) !important;
  border-radius:var(--fil-fam-radius) !important;
  backdrop-filter:var(--fil-fam-backdrop) !important;
  -webkit-backdrop-filter:var(--fil-fam-backdrop) !important;
  box-shadow:var(--fil-fam-shadow) !important;
  color:var(--fil-fam-text) !important;
  -webkit-text-fill-color:var(--fil-fam-text) !important;
  overflow:visible !important;
}

/* ── Overscroll surfaces: kill chassis bloom-on-text, force fully
   white fill ────────────────────────────────────────────────────
   The base .filament-pill rule (line ~4625) sets a heavy multi-
   layer white-glow text-shadow ("lit emission") that's perfect on
   large pill text but at 10px on the strip / CTA, the glow dominates
   the glyphs and reads as fuzzy / not-quite-white. Combined with the
   --fil-fam-text 0.95-alpha fill, the text never lands as crisp
   white. Forced opaque white + text-shadow:none on the strip, the
   CTA, and ALL their descendants so glyphs paint hard-edged. */
#opv-strip,
#opv-strip *,
#opv-cta-btn,
#opv-cta-btn * {
  text-shadow:none !important;
  color:#fff !important;
  -webkit-text-fill-color:#fff !important;
}
/* Lift text children above the .fil-bloom layer. .fil-bloom is
   positioned absolute with z-index:0, which paints AFTER static
   inline content in the stacking order (CSS 2.1 § Appendix E,
   step 5 > step 4). So plain inline spans (#opv-strip-arrow /
   -label / -name and the CTA's `→` span) end up underneath the
   bloom and read as washed-out / ghosted. The chassis solves this
   for auto-wrapped .fil-label nodes (z-index:5 on them) but our
   strip carries pre-existing element children that filament.js
   leaves alone, so we lift them ourselves. Excludes .fil-bloom and
   .pill-surface so those background layers stay at the bottom. */
#opv-strip > :not(.fil-bloom):not(.pill-surface),
#opv-cta-btn > :not(.fil-bloom):not(.pill-surface){
  position:relative;
  z-index:5;
}
/* Kill the .fil-label drop-shadow halo on the CTA + strip text.
   The chassis applies a triple white drop-shadow filter to every
   .fil-label (line ~4097) to create the "lit emission" around the
   glyphs — perfect on big pill text but it paints as a fuzzy halo
   around the small 11–13px text here. User wants crisp white text,
   no halo. */
#opv-strip .fil-label,
#opv-cta-btn .fil-label,
.mob-book-bar-btn .fil-label,
.acq-enquire .fil-label,
.mob-menu-btn .fil-label{
  filter:none !important;
}

/* #opv-cta-btn brackets are now in the shared
   THREE-STACKED-GRADIENT recipe further down (search for
   "tall host" recipe). That keeps the top/bottom hooks fixed
   at 4px while the vertical bar stretches with the host —
   exactly what's needed for the wrapped two-line CTA.

   HORIZONTAL POSITION OVERRIDE: chassis default sits the
   brackets at left:-6px / right:-6px which reads tight on a
   square host (.sdr-submit-btn) but appears to float outside
   the curve on a pill (#opv-cta-btn has border-radius:999px).
   Move them in so the vertical bar lands close to where the
   pill's rounded edge is actually visible — matches the
   visual attachment of the brackets on .sdr-submit-btn. */
#opv-cta-btn.filament-pill::before{
  left:-2px !important;
}
#opv-cta-btn.filament-pill::after{
  right:-2px !important;
}
.mjump-title.filament-pill:hover,
.cookie-accept-all-btn.filament:hover,
.cookie-save-btn.filament:hover,
.choice-strip-accept.filament:hover,
.choice-strip-decline.filament:hover,
.choice-strip-customize.filament:hover,
.priv-cookies-cta .filament:hover,
.sdr-submit-btn.filament:hover,
.sdr-form-field.filament-text-wrap:focus-within,
#opv-strip.filament-pill:hover,
#opv-cta-btn.filament-pill:hover,
.mob-book-bar-btn.filament-pill:hover,
.acq-enquire.filament-pill:hover,
.mob-menu-btn.filament-pill:hover{
  background:var(--fil-fam-bg-hover) !important;
  border-color:var(--fil-fam-border-hover) !important;
}
.mjump-title.filament-pill:active,
.cookie-accept-all-btn.filament:active,
.cookie-save-btn.filament:active,
.choice-strip-accept.filament:active,
.choice-strip-decline.filament:active,
.choice-strip-customize.filament:active,
.priv-cookies-cta .filament:active,
.sdr-submit-btn.filament:active{
  background:var(--fil-fam-bg-active) !important;
}

/* BLOOM — flat dark backdrop. Iridescent pseudos render on top.
   POSITION RESET: the rule at line 3401 ([data-membrane] >
   *:not(.pill-surface)) sets position:relative + z-index:5 on
   every direct child of a membrane'd host — INCLUDING the
   .fil-bloom span filament.js injects. That collapses the bloom
   from an absolutely-positioned interior-filling layer (chassis
   default) to a zero-height inline-relative span between the
   pill-surface and the pill-text. Result: no visible bloom, no
   iridescence, hollow-looking buttons — the actual reason the
   membrane'd buttons looked "off". Re-assert the chassis position
   here. */
/* .cookie-save-btn DELIBERATELY OMITTED — it's a secondary
   button (glass-style, like customize/decline) and has its own
   lighter bloom override at line ~5044. Including it here would
   make the family-default 0.36-alpha dark bloom win source-order
   over my lighter override. */
/* Unified bloom positioning across the family. Background fill
   varies per host so it's set elsewhere — only the layout
   (display / position / inset / z-index) is shared here. */
.mjump-title.filament-pill > .fil-bloom,
.cookie-accept-all-btn.filament > .fil-bloom,
.cookie-save-btn.filament > .fil-bloom,
.choice-strip-accept.filament > .fil-bloom,
.priv-cookies-cta .filament > .fil-bloom,
#ov-privacy .priv-cookies-cta.ov-bottom button.filament > .fil-bloom,
.sdr-submit-btn.filament > .fil-bloom,
.sdr-form-field.filament-text-wrap > .fil-bloom,
.about-links.filament-pill > .fil-bloom,
.mdock.filament-pill > .fil-bloom,
.mhint.filament-pill > .fil-bloom{
  display:block !important;
  position:absolute !important;
  inset:0 -2px !important;
  z-index:0 !important;
}
/* Family-default dark bloom fill — cookie-save-btn opts out
   (keeps its own lighter secondary bloom defined further down). */
.mjump-title.filament-pill > .fil-bloom,
.cookie-accept-all-btn.filament > .fil-bloom,
.choice-strip-accept.filament > .fil-bloom,
.priv-cookies-cta .filament > .fil-bloom,
#ov-privacy .priv-cookies-cta.ov-bottom button.filament > .fil-bloom,
.sdr-submit-btn.filament > .fil-bloom,
.sdr-form-field.filament-text-wrap > .fil-bloom{
  background:var(--fil-fam-bloom-bg) !important;
}
/* .cookie-save-btn omitted — secondary button keeps its lighter
   bloom on hover too (no dark-fill brighten step). */
.mjump-title.filament-pill:hover > .fil-bloom,
.cookie-accept-all-btn.filament:hover > .fil-bloom,
.choice-strip-accept.filament:hover > .fil-bloom,
.priv-cookies-cta .filament:hover > .fil-bloom,
.sdr-submit-btn.filament:hover > .fil-bloom{
  background:var(--fil-fam-bloom-bg-hover) !important;
}

/* TEXT — flat crisp white, no drop-shadow halo. Applies to both
   .fil-label (non-membrane) and .pill-text (membrane'd) and to
   inline <a> children in pills like .about-links. */
.mjump-title.filament-pill .fil-label,
.cookie-accept-all-btn.filament .fil-label,
.cookie-save-btn.filament .fil-label,
.choice-strip-accept.filament .fil-label,
.choice-strip-decline.filament .fil-label,
.choice-strip-customize.filament .fil-label,
.priv-cookies-cta .filament .fil-label,
.sdr-submit-btn.filament .fil-label,
.mjump-title.filament-pill .pill-text,
.cookie-accept-all-btn.filament .pill-text,
.cookie-save-btn.filament .pill-text,
.choice-strip-accept.filament .pill-text,
.priv-cookies-cta .filament .pill-text,
.sdr-submit-btn.filament .pill-text,
.priv-cookies-cta .filament a{
  filter:var(--fil-fam-label-filter) !important;
  color:var(--fil-fam-text) !important;
  -webkit-text-fill-color:var(--fil-fam-text) !important;
  text-shadow:none !important;
}

/* IRIDESCENCE — additive screen blend so conic pastels emerge.
   Customize/decline are deliberately omitted: their bloom pseudos
   are set to display:none in a separate rule above so they stay
   iridescence-free. */
.mjump-title.filament-pill > .fil-bloom::before,
.cookie-accept-all-btn.filament > .fil-bloom::before,
.cookie-save-btn.filament > .fil-bloom::before,
.choice-strip-accept.filament > .fil-bloom::before,
.priv-cookies-cta .filament > .fil-bloom::before,
#ov-privacy .priv-cookies-cta.ov-bottom button.filament > .fil-bloom::before,
.sdr-submit-btn.filament > .fil-bloom::before,
.sdr-form-field.filament-text-wrap > .fil-bloom::before,
/* Extended to all close discs so .lb-close gets the same
   iridescent halo the other close buttons have. User reported
   the lightbox close lacked it — same chassis bloom but the
   default overlay blend mode mutes pastels to grey. Screen
   blend lets the colours emerge. */
.ov-close.filament-close > .fil-bloom::before,
.mstack-close.filament-close > .fil-bloom::before,
.mjump-close.filament-close > .fil-bloom::before,
.lb-close.filament-close > .fil-bloom::before{
  opacity:var(--fil-fam-iri-opacity-a) !important;
  filter:var(--fil-fam-iri-filter-a) !important;
  mix-blend-mode:var(--fil-fam-iri-blend) !important;
}
/* lb-close lives in the lightbox where the backdrop is
   rgba(250,250,249,0.92) — near-white. The chassis bloom at
   0.30 black reads slightly faint against that lightness.
   Bump JUST enough so the disc matches the depth the other
   close discs get from sitting on their darker backdrops —
   0.38 is a small step over chassis, not a slab. Mild outer
   shadow added for a touch of lift on the cream lightbox
   without going aggressive. */
.lb-close.filament-close > .fil-bloom{
  background:rgba(0,0,0,0.38) !important;
}
.lb-close.filament-close{
  box-shadow:
    0 2px 6px rgba(20,18,16,0.10),
    var(--glass-light-shadow) !important;
}
/* Mobile: the disc reads lighter than desktop at the same
   alpha (smaller absolute size on a small viewport, plus the
   lightbox uses a heavier blur effect that pushes the bloom
   visually lighter). Push back to the heavier values that
   felt right in the previous pass. */
@media(max-width:700px){
  .lb-close.filament-close > .fil-bloom{
    background:rgba(0,0,0,0.55) !important;
  }
  .lb-close.filament-close{
    box-shadow:
      0 4px 12px rgba(20,18,16,0.18),
      0 12px 28px rgba(20,18,16,0.10),
      var(--glass-light-shadow) !important;
  }
}
.mjump-title.filament-pill > .fil-bloom::after,
.cookie-accept-all-btn.filament > .fil-bloom::after,
.cookie-save-btn.filament > .fil-bloom::after,
.choice-strip-accept.filament > .fil-bloom::after,
.priv-cookies-cta .filament > .fil-bloom::after,
#ov-privacy .priv-cookies-cta.ov-bottom button.filament > .fil-bloom::after,
.sdr-submit-btn.filament > .fil-bloom::after,
.sdr-form-field.filament-text-wrap > .fil-bloom::after,
.ov-close.filament-close > .fil-bloom::after,
.mstack-close.filament-close > .fil-bloom::after,
.mjump-close.filament-close > .fil-bloom::after,
.lb-close.filament-close > .fil-bloom::after{
  opacity:var(--fil-fam-iri-opacity-b) !important;
  filter:var(--fil-fam-iri-filter-b) !important;
  mix-blend-mode:var(--fil-fam-iri-blend) !important;
}

/* MEMBRANE'S .pill-surface hidden — filament owns the visual on
   every host in this family. */
.choice-strip-accept.filament > .pill-surface,
.cookie-accept-all-btn.filament > .pill-surface,
.priv-cookies-cta .filament > .pill-surface{
  display:none !important;
}

/* ════════════════════════════════════════════════════════════════
   ABOUT-LINKS — UNIFIED FAMILY RECIPE (all viewports)
   User requested the same bracketed pill design on desktop AND
   mobile — one pair of brackets around all 4 links. Removed the
   @media gating that previously kept this mobile-only.
   DESKTOP TUNING: 0.12 alpha dark fill — lighter than the earlier
   0.22 / 0.42 attempts but still dark enough to give white text
   contrast against the cream overlay backdrop. The .filament-
   pill chassis hover rule brightens the bg with var(--fil-body-
   hover) (a light rgba white) which made text invisible on
   desktop — overridden here with a darker hover and !important
   so it stays readable. */
.about-links.filament-pill{
  /* Only the host-specific bg is set here — border / radius /
     backdrop-filter / box-shadow / color / overflow all come
     from the unified family rule. 0.15 alpha dark gives white
     link text enough contrast against the cream overlay; the
     desktop @media block at line 484 lightens this further
     since on desktop the pill sits over a panel-strong backdrop. */
  background:rgba(20,18,16,0.15) !important;
}
/* Bracket position: chassis default is left:-6 / right:-6 which
   on a wide about-links pill that nearly fills the overlay width
   pushes the brackets past the overlay's visible bounds. Anchor
   inside the host (left:0 / right:0) so brackets sit within the
   pill rather than overhanging. */
.about-links.filament-pill::before{
  left:0 !important;
}
.about-links.filament-pill::after{
  right:0 !important;
}
/* Hover: darker (not lighter) so contrast strengthens — chassis
   family default brightens the bg on hover, which would make the
   white text disappear into the lit-up tile against cream. */
.about-links.filament-pill:hover{
  background:rgba(20,18,16,0.40) !important;
  border-color:var(--fil-fam-border-hover) !important;
}
.about-links.filament-pill:hover a,
.about-links.filament-pill:hover .fil-label{
  color:rgba(255,255,255,1) !important;
  -webkit-text-fill-color:rgba(255,255,255,1) !important;
}
.about-links.filament-pill:active{
  background:var(--fil-fam-bg-active) !important;
}
.about-links.filament-pill > .fil-bloom{
  background:var(--fil-fam-bloom-bg) !important;
}
.about-links.filament-pill:hover > .fil-bloom{
  background:var(--fil-fam-bloom-bg-hover) !important;
}
.about-links.filament-pill .fil-label,
.about-links.filament-pill .pill-text,
.about-links.filament-pill a{
  filter:var(--fil-fam-label-filter) !important;
  color:var(--fil-fam-text) !important;
  -webkit-text-fill-color:var(--fil-fam-text) !important;
  text-shadow:none !important;
}
.about-links.filament-pill > .fil-bloom::before{
  opacity:var(--fil-fam-iri-opacity-a) !important;
  filter:var(--fil-fam-iri-filter-a) !important;
  mix-blend-mode:var(--fil-fam-iri-blend) !important;
}
.about-links.filament-pill > .fil-bloom::after{
  opacity:var(--fil-fam-iri-opacity-b) !important;
  filter:var(--fil-fam-iri-filter-b) !important;
  mix-blend-mode:var(--fil-fam-iri-blend) !important;
}
.about-links.filament-pill > .pill-surface{
  display:none !important;
}

/* ════════════════════════════════════════════════════════════════
   MOBILE FOOTER DOCK — filament-pill family member.
   ----------------------------------------------------------------
   .mdock is wired in filament.js MAP as 'filament-pill', so the
   chassis brackets + bloom span come for free. The overrides
   below patch around four collisions that prior wiring rounds
   exposed:

     (1) Chassis `.filament-pill { position:relative; }` collides
         with `.mdock { position:absolute }`. Same specificity,
         chassis is later in source order → chassis wins → dock
         loses anchor → cards above resize.
         FIX: position:absolute !important.

     (2) Chassis `.filament-pill { border-radius:3px; }` is what
         we want anyway (sharper than the legacy 22px) so we
         re-state via --fil-fam-radius for clarity.

     (3) `.mdock { overflow:hidden }` clips the bracket pseudos
         at left:-6 / right:-6.
         FIX: overflow:visible !important.

     (4) `.mdock::before { display:none }` (legacy gradient-strip
         opt-out at line 2028) suppresses the chassis bracket
         pseudo.
         FIX: display:block !important on .mdock.filament-pill::before.

   Bonus fix: the dock's children use --text-bright /
   --text-mid, which are dark-on-cream colors that vanish on the
   dark filament-family backdrop. Override to white. */
@media(max-width:700px){
  .mdock.filament-pill{
    /* Family treatment (border / radius / backdrop / shadow /
       color) inherited from the unified rule. Host-specific
       overrides only here:
       (1) re-assert position:absolute (the dock is bottom-pinned;
           the chassis would otherwise set it to relative).
       (2) bg at 0.20 instead of family default — the dock is
           much bigger than a typical pill so a heavier fill
           reads as a brick + crushes the iridescence contrast.
       Note: overflow:visible inherited from family rule. */
    position:absolute !important;
    background:rgba(20,18,16,0.20) !important;
  }
  /* (4) override the legacy display:none on ::before */
  .mdock.filament-pill::before{
    display:block !important;
  }
  /* Brackets visible at rest. */
  .mdock.filament-pill::before,
  .mdock.filament-pill::after{
    opacity:1 !important;
  }
  /* Replace the chassis SVG bracket image with three stacked
     CSS gradients (fixed 4px top hook + fixed 4px bottom hook +
     vertical bar at 100% height). The chassis SVG is designed
     for ~40px buttons and stretches its hooks vertically on the
     ~86px dock; the three-gradient technique keeps the hooks at
     a fixed 4px tall while the vertical bar grows with the host.
     Same approach as the sticky-header brackets at line 5016. */
  /* (.mdock.filament-pill bracket gradients consolidated with
     .page-header-sticky + .sdr-* in the shared "three-stacked-
     gradient brackets" rule at end of file.) */
  /* Light bloom so the iridescence has something to read
     against without doubling the dark fill of the dock body. */
  .mdock.filament-pill > .fil-bloom{
    background:rgba(0,0,0,0.12) !important;
  }
  /* Iridescent conic-gradient layers — tone down saturation
     and opacity. Previous pass pushed saturate to 1.5 and
     opacity to 0.85 which read as "lots of colour"; user
     wants softer / less colourful. Family defaults are
     0.50 / 0.45 opacity, blur 14/18, saturate 1.2/1.0 — use
     those for the softer feel. */
  .mdock.filament-pill > .fil-bloom::before{
    opacity:var(--fil-fam-iri-opacity-a) !important;
    filter:var(--fil-fam-iri-filter-a) !important;
    mix-blend-mode:var(--fil-fam-iri-blend) !important;
  }
  .mdock.filament-pill > .fil-bloom::after{
    opacity:var(--fil-fam-iri-opacity-b) !important;
    filter:var(--fil-fam-iri-filter-b) !important;
    mix-blend-mode:var(--fil-fam-iri-blend) !important;
  }
  /* Text colors — override the dock's dark-on-cream defaults so
     subscribe + about/contact read against the dark backdrop.
     position:relative + z-index:2 lifts them above the bloom. */
  .mdock.filament-pill .sub-link,
  .mdock.filament-pill .footer-link{
    color:#fff !important;
    -webkit-text-fill-color:#fff !important;
    position:relative;
    z-index:2;
  }
  /* The chassis .filament-pill rule (line ~4348) has a 50px-blur
     text-shadow halo that inherits down to every text descendant.
     With wide-blur shadows applied per-glyph, longer words
     accumulate more overlapping halo — "contact" (7 chars) reads
     visibly brighter than "about" (5 chars). Strip the inherited
     halo on the static about/contact links so the fill is even.
     .sub-link's text-shadow is owned by the dock-sub-glow keyframe
     so the animation takes care of its halo separately. */
  .mdock.filament-pill .footer-link{
    text-shadow:none !important;
  }
  /* Subscribe glow pulse — leave the @keyframes dock-sub-glow
     animation running so the text breathes the way it always
     has. The keyframe animates color:var(--text-bright) (dark)
     at 0%/100% and #fff at 50%, but `color:#fff !important` on
     the rule above wins against animation declarations in the
     cascade (per CSS Cascade Level 4: !important author declarations
     rank above CSS animations) so the text stays white throughout
     while the text-shadow keyframe still pulses the glow. */
  /* Hairline DIVIDERS need to be visible on the dark filament
     dock backdrop. The defaults use var(--border-mid) (a
     dark-on-cream accent) which disappears on the dark fill.
     Switch all three to translucent white:
       - .sub-link::after — the animated pulse underline below
         subscribe (the "moving" one — keeps its pulse-ul
         keyframe from the base .sub-link::after rule).
       - .mdock-row .footer-link + .footer-link::before — the
         vertical divider between about and contact. */
  .mdock.filament-pill .sub-link::after{
    background:rgba(255,255,255,0.55) !important;
    /* The base .sub-link::after rule sets width:100% (line 154).
       The mobile .mdock override adds left:18 / right:18 to inset
       the line from the rounded pill corners — but with both
       left/right AND width set on a position:absolute box, the
       spec says width wins, so the box stays container-wide and
       extends 18px past the right edge. The pulse-ul keyframe
       then scales this oversize box from transform-origin:left
       and at full scaleX(1) the underline runs out past the
       right bracket. Reset width:auto so left + right actually
       size the box (container - 18 - 18 = container - 36). */
    width:auto !important;
  }
  .mdock.filament-pill .mdock-row .footer-link + .footer-link::before{
    background:rgba(255,255,255,0.30) !important;
  }

  /* ───────────────────────────────────────────────────────────
     MOBILE GESTURE-HINT CHIPS — .mhint as filament-pill.
     Same override pattern as .mdock above: chassis would
     clobber position:absolute, so re-assert; clear the legacy
     20px pill radius; let brackets escape; dark family fill;
     white text (the .mhint default rgba(250,250,249,0.92) is
     already near-white but make it explicit). The fade-in
     .show transition lives on opacity which chassis doesn't
     touch, so it still works. */
  .mhint.filament-pill{
    /* Family treatment (border / radius / backdrop / shadow /
       color / overflow) inherited from the unified rule.
       Host-specific: position:absolute (chassis would set to
       relative) and a heavier bg so the chip reads loudly
       against whatever scrolling content is behind it. */
    position:absolute !important;
    background:rgba(20,18,16,0.55) !important;
  }
  .mhint.filament-pill::before,
  .mhint.filament-pill::after{
    opacity:1 !important;
  }
  .mhint.filament-pill > .fil-bloom{
    background:rgba(0,0,0,0.30) !important;
  }
  .mhint.filament-pill > .fil-bloom::before{
    opacity:var(--fil-fam-iri-opacity-a) !important;
    filter:var(--fil-fam-iri-filter-a) !important;
    mix-blend-mode:var(--fil-fam-iri-blend) !important;
  }
  .mhint.filament-pill > .fil-bloom::after{
    opacity:var(--fil-fam-iri-opacity-b) !important;
    filter:var(--fil-fam-iri-filter-b) !important;
    mix-blend-mode:var(--fil-fam-iri-blend) !important;
  }
  /* Lift the arrow + text spans above the bloom. */
  .mhint.filament-pill > .mhint-arrow,
  .mhint.filament-pill > span{
    position:relative;
    z-index:2;
    color:#fff !important;
    -webkit-text-fill-color:#fff !important;
  }

  /* ───────────────────────────────────────────────────────────
     CARD STACK CARDS — subtler corner radius.
     Original was 18px (visibly rounded). Down to 8px so the
     corners are softened but still read as squared-off. No
     bracket framing — bracket experiment was reverted; only
     the radius is touched. overflow:hidden stays so the
     image inside the card is still clipped to the corner. */
  .mcard{
    border-radius:8px !important;
  }

  /* ───────────────────────────────────────────────────────────
     CARD CTA ARROW — minimal no-halo brackets.
     Squared-frame treatment was visually too heavy on the small
     44×44 disc. Replaced with light CSS-gradient brackets and
     NO drop-shadow halo (no luminescence) — just two thin white
     brackets defining the host edges. The original
     `backdrop-filter: blur(8px)` and 16% glass fill were what
     made the button read as opaque-frosty; both removed so the
     photo behind is fully visible between the brackets.
     The Safiro `›`/`↑` glyph sits slightly above the geometric
     centre because of the font's baseline metrics — small
     padding-top nudge corrects the visual centring without
     disturbing the flex centring logic. */
  .mcard-cta-arrow{
    border-radius:var(--fil-fam-radius) !important;
    border:none !important;
    /* Lit interior — flat translucent white fill so the lighting
       reads as a SQUARE of light matching the bracket geometry,
       not a circular bubble inside square brackets. ~12% alpha
       is enough to feel illuminated without becoming opaque. */
    background:rgba(255,255,255,0.12) !important;
    box-shadow:none !important;
    backdrop-filter:none !important;
    -webkit-backdrop-filter:none !important;
    position:relative;
    overflow:visible;
    /* Safiro arrow centring iterations:
       v1: padding-top:3px → chevron above centre (too high).
       v2: line-height:1 + padding-top:8px → below centre.
       v3: line-height:1 + padding-top:3px → still below centre.
       v4: padding-top:5px (no line-height) → still below centre
           AND the intro card's `↑` glyph also pushed below.
       v5: padding-top:0 → chevron lands correctly but the
           intro card's `↑` reads a touch too high.
       v6 (here): the two glyphs need different padding because
       their metrics differ. Chevron stays at 0; intro card's
       `↑` gets a tiny 2px nudge via .mcard.intro selector. */
    padding-top:0 !important;
  }
  /* Intro card uses `↑` (full-height arrow) which sits a bit
     higher in the line-box than the `›` chevron other cards
     use. Tiny push-down so the arrow lands at visual centre
     while the chevron stays where it is. */
  .mcard.intro .mcard-cta-arrow{
    padding-top:2px !important;
  }
  .mcard-cta-arrow::before,
  .mcard-cta-arrow::after{
    content:'';
    position:absolute;
    top:0; bottom:0;
    width:10px;
    pointer-events:none;
    z-index:2;
    background-repeat:no-repeat,no-repeat,no-repeat;
    background-image:
      linear-gradient(rgba(240,238,235,0.75),rgba(240,238,235,0.75)),
      linear-gradient(rgba(240,238,235,0.75),rgba(240,238,235,0.75)),
      linear-gradient(rgba(240,238,235,0.75),rgba(240,238,235,0.75));
    background-size:
      8px 2px,
      8px 2px,
      2px 100%;
    /* NO filter / drop-shadow — flat, no luminescence. */
  }
  .mcard-cta-arrow::before{
    left:-2px;
    background-position:
      0 0,           /* top hook at top-left */
      0 100%,        /* bottom hook at bottom-left */
      0 0;           /* vertical bar at left edge */
  }
  .mcard-cta-arrow::after{
    right:-2px;
    background-position:
      100% 0,        /* top hook at top-right */
      100% 100%,     /* bottom hook at bottom-right */
      100% 0;        /* vertical bar at right edge */
  }
}

/* ═══════════════════════════════════════════════════════════════
   THREE-STACKED-GRADIENT BRACKETS — shared "tall host" recipe.
   ----------------------------------------------------------------
   The chassis bracket SVG (`--fil-bracket-l/r`, viewBox 0 0 18 40)
   is sized for ~40px buttons. Hosts taller than that (sticky
   header pill, footer dock, subscribe submit / email input
   ~58-86px) stretch the SVG hooks disproportionately. The
   three-stacked-gradient technique keeps the top and bottom
   hooks at a FIXED 4px and grows the vertical bar with host
   height. Same visual recipe across all four hosts so they
   live in one rule rather than four parallel copies. */
.page-header-sticky.filament-pill::before,
.page-header-sticky.filament-pill::after,
.mdock.filament-pill::before,
.mdock.filament-pill::after,
.sdr-submit-btn.filament::before,
.sdr-submit-btn.filament::after,
.sdr-form-field.filament-text-wrap::before,
.sdr-form-field.filament-text-wrap::after,
#opv-cta-btn.filament-pill::before,
#opv-cta-btn.filament-pill::after{
  background-image:
    linear-gradient(#ffffff,#ffffff),
    linear-gradient(#ffffff,#ffffff),
    linear-gradient(#ffffff,#ffffff) !important;
  background-size:
    15px 4px,
    15px 4px,
    4px 100% !important;
  background-repeat:no-repeat,no-repeat,no-repeat !important;
}
.page-header-sticky.filament-pill::before,
.mdock.filament-pill::before,
.sdr-submit-btn.filament::before,
.sdr-form-field.filament-text-wrap::before,
#opv-cta-btn.filament-pill::before{
  background-position:
    0 0,           /* top hook at top-left */
    0 100%,        /* bottom hook at bottom-left */
    0 0 !important;/* vertical bar at left edge */
}
.page-header-sticky.filament-pill::after,
.mdock.filament-pill::after,
.sdr-submit-btn.filament::after,
.sdr-form-field.filament-text-wrap::after,
#opv-cta-btn.filament-pill::after{
  background-position:
    100% 0,        /* top hook at top-right */
    100% 100%,     /* bottom hook at bottom-right */
    100% 0 !important;/* vertical bar at right edge */
}


/* ════════════════════════════════════════════════════════════════
   CHROMIUM-ONLY PERFORMANCE OPTIMIZATIONS
   ────────────────────────────────────────────────────────────────
   Scoped to `html.is-chromium` — class added by shared.js's
   Chromium-detection IIFE on Chrome / Edge / Brave / Opera /
   Vivaldi / Android Chrome. EXCLUDED: iOS Chrome (CriOS uses
   Webkit), Safari, Firefox, legacy EdgeHTML.

   These rules are additive overrides only — never modify
   `will-change` (which caused Chromium's compositor to churn
   layers and produce visible flashing). Restricted to:
     · Reducing blur radius on heavy always-visible surfaces
     · Adding `contain:paint` to elements that already have
       `contain:layout` (extension, not replacement)
     · Adding `content-visibility:auto` to off-screen content
     · Shortening sticky-header transition durations
   ──────────────────────────────────────────────────────────────── */

/* ── 1. Reduce backdrop-filter blur radius on always-visible
        surfaces. Cost scales linearly with radius on Skia. ────
   `.left-bar` is the #1 perceived offender: visible on every
   desktop page, 380px × full viewport, re-blurring whenever
   content behind changes (bgVideo on home, preview videos on
   hover). 24px → 12px halves the per-frame shader work without
   visible difference on the tinted, saturated frosted-glass
   surface (Safari keeps 24px via Metal where it's free). */
html.is-chromium .left-bar{
  backdrop-filter:blur(12px);
  -webkit-backdrop-filter:blur(12px);
}

/* `.lightbox` backdrop — 48px is overkill on Chromium when
   open over playing content. 20px keeps the heavy frosted look. */
html.is-chromium body[data-page="sub"] .lightbox{
  backdrop-filter:blur(20px);
  -webkit-backdrop-filter:blur(20px);
}

/* ── 2. `contain:paint` on isolated regions ──
   Extends existing `contain:layout` where present. Tells the
   compositor "pixel changes inside this element don't escape
   its bounds" — reduces per-hover/per-frame dirty-rect size.
   AVOIDED on `.page-header-sticky` (breaks position:sticky)
   and `.fil-bloom` (the previous round had this combined with
   a will-change override that triggered layer churn; this
   round we omit will-change entirely so paint-containment
   alone is safe). */
html.is-chromium .snav-item.fil-glass{
  /* contain:paint REMOVED (v=233) — it clipped the bracket pseudo-
     elements' drop-shadow glow halo to the row's own box, so on
     Chromium the brackets lost their outer "light gradient". Keep
     contain:layout (the reflow-scoping benefit) but let paint escape
     so the full 45px glow radiates again. */
  contain:layout;
}
/* (Removed v=71 — caused flashing on Edge when card hover-state
   changed crossed the contain:paint boundary. Chrome was fine
   but Edge's compositor flashed the layer. Same reasoning
   below for content-visibility on .mitem.)
html.is-chromium .mcard{
  contain:paint;
} */

/* ── 3. content-visibility:auto on off-screen gallery items ──
   REMOVED v=71 → v=72. Edge flashed .mitem elements as they
   crossed the rendering boundary during scroll. content-
   visibility:auto is correct in spec but Edge's specific
   compositor heuristics produce visible flicker on each
   render-skip toggle. Chrome was fine with it; Edge was not.
   Rather than UA-detect specifically Edge, we drop the rule
   entirely — the perf win on Chrome was modest and not worth
   breaking Edge.
html.is-chromium body[data-page="sub"] .mitem{
  content-visibility:auto;
  contain-intrinsic-size:auto 320px;
} */

/* ── 4. Shorten the sticky-header compact transition on
        Chromium (0.35s → 0.22s) ──
   OPTION-3 (v=221): geometry animation restored (see base rules).
   The per-frame reflow that made this expensive is now contained to
   the header's own subtree by `contain:layout paint` on .main-body +
   `contain:layout` on the sticky, so animating font/padding here is
   cheap. Kept at the shorter 0.22s on Chromium so it lands quickly. */
html.is-chromium body[data-page="sub"] .header-left,
html.is-chromium body[data-page="sub"] .header-right{
  transition:padding 0.22s ease;
}
html.is-chromium body[data-page="sub"] .series-title{
  transition:font-size 0.22s ease,
             margin-bottom 0.22s ease, color 0.22s ease;
}
html.is-chromium body[data-page="sub"] .series-meta{
  transition:font-size 0.22s ease, color 0.22s ease;
}
html.is-chromium body[data-page="sub"] .series-desc{
  transition:font-size 0.22s ease,
             color 0.22s ease;
}

/* ════════════════════════════════════════════════════════════════
   CHROMIUM v=72 — ROUND 2 (TARGETED RENDER-COST REDUCTIONS)
   ────────────────────────────────────────────────────────────────
   Round 1 (v=71) covered backdrop-filter radius cuts, containment,
   content-visibility, and shortened sticky transitions. User
   reported "feels smoother but still not Safari-smooth."

   Round 2 targets the OTHER big Chromium cost centers that survive
   into v=71:
     - .fil-bloom rotation animations (22s + 32s slow rotations
       running on every .filament/.snav-item/etc element. Each
       runs through Skia's blur+conic-gradient+mix-blend pipeline
       every frame. Safari composites these via Metal essentially
       for free. Chromium pays for each one continuously.)
     - .fil-bloom static blur radii (14/18/22px on Skia)
     - mix-blend-mode (forces isolation buffer, expensive on
       Chromium's compositor)
     - filter:url(#waterRippleCtx) on .pill-surface.active (static
       SVG filter rasterized via Skia CPU path)

   NONE of these rules touch will-change — round-1 v=68 attempted
   that and caused layer-churn flashing. We instead disable the
   animation cleanly; the will-change:transform that's already on
   the pseudo-elements becomes a no-op for static elements (layer
   gets promoted once, then stays static, no per-frame work).
   ──────────────────────────────────────────────────────────────── */

/* ── 5. Stop the .fil-bloom rotations on Chromium ──
   Two slow rotations (22s, 32s) animate transform on the
   ::before/::after pseudos of every .fil-bloom. Each rotation
   forces Chromium to re-blur the conic-gradient + re-apply
   mix-blend-mode + re-composite the underlying surface, every
   frame, even when offscreen if will-change pinned the layer.
   Holding them static keeps the iridescent look (it's barely
   perceptible motion at 22s/32s anyway) at near-zero cost. */
html.is-chromium .fil-bloom::before,
html.is-chromium .fil-bloom::after{
  animation:none;
}

/* ── 6. Cut .fil-bloom blur radii on Chromium ──
   Skia rasterizes CSS blur on the compositor thread; cost
   scales with radius squared. Halving radii drops the per-
   element blur work to ~25%. Safari composites blur via Metal
   shaders where the cost is fixed regardless of radius. */
html.is-chromium .fil-bloom{
  filter:blur(8px);
}
html.is-chromium .fil-bloom::before{
  filter:blur(10px) saturate(1.4);
}
html.is-chromium .fil-bloom::after{
  filter:blur(12px) saturate(1.2);
}

/* ── 7. Drop mix-blend-mode on .fil-bloom pseudos on Chromium ──
   mix-blend-mode requires Chromium to allocate a temporary
   isolation buffer for each blended layer and re-composite
   the underlying surface every paint. Removing it gives a
   simpler (slightly more saturated) iridescent look that's
   nearly indistinguishable through the brackets/text on top
   but saves a substantial compositor step on every .filament,
   .filament-pill, .snav-item.fil-glass, .mreel-frame.fil-glass
   element on screen. */
html.is-chromium .fil-bloom::before,
html.is-chromium .fil-bloom::after{
  mix-blend-mode:normal;
  opacity:0.45;       /* compensate visually for no-blend */
}

/* ── 8. (REMOVED — user request, Option B) ──
   Earlier rounds dropped filter:url(#waterRippleCtx) on
   .pill-surface.active for Chromium to recover frame budget;
   user explicitly wants the membrane shape-shifting visible
   on Chromium as part of the site's visual identity. Filter
   restored to Chromium. Trade-off: Chromium runs the SVG
   turbulence+displacement chain through Skia, more frame
   budget consumed. Iri rotations also remain on (Option B). */

/* ── 9. (v=99 TEST — reverted) transform:translateZ(0) on the
   sticky header was added in v=72 to force its own compositor
   layer. User wants to test whether removing it changes morph
   feel — possibly Chromium's automatic layer promotion handles
   the sticky better without our hint, or possibly our hint is
   pinning a permanent layer that competes with adjacent layers
   for compositor budget. Reverted here for testing; re-add if
   the morph feels worse without it.
html.is-chromium body[data-page="sub"] .page-header-sticky{
  transform:translateZ(0);
}
*/
