/* ------ ERLEN SITE STYLES ------ */
/*
 * Layout system + page-transition styles for Erlen Masson's personal site
 * (erlen.co.uk). Loads after design-system.css so the cascade resolves last
 * here. Layer: app — does not ship with the design system.
 *
 * Anything reusable across products belongs in the design system (fix it
 * upstream, not here). Keep this file scoped to this site.
 *
 * Transition timing uses design system motion primitives (--duration-*,
 * --ease-*) directly. Three speeds by intent:
 *   Hover / focus feedback:  --duration-2xs (100ms) + --ease-out
 *   Layout shifts:           --duration-xs  (200ms) + --ease-out
 *   Surface enter / exit:    --duration-s   (400ms) + --ease-out
 */


/* ------ FOCUS INDICATOR ------ */

/* Keyboard focus indicator — applies to all interactive elements that
   don't define their own focus-visible style. :where() keeps specificity
   at 0 so component-specific focus styles still win. */
:where(a, button, [tabindex]):focus-visible {
  outline: 2px solid var(--text-primary);
  outline-offset: 2px;
  /* border-radius: var(--radius-xs); */
}

/* Initial focus lands on the dialog container itself (erlen-nav.js openPanel)
   so no interactive element gets a focus ring when the nav opens. role="dialog"
   is only present while the panel is open, so this never affects the closed
   sidebar or the nav links inside it. */
#erlen-sidebar[role="dialog"]:focus,
#erlen-sidebar[role="dialog"]:focus-visible {
  outline: none;
}

/* Hash-jump targets (skip link, /#work, TOC links) are focused by script so
   the keyboard lands where the page scrolled — see initHashScroll in
   erlen.js. They are scroll containers, not controls, and a ring drawn round
   a whole <main> or section reads as a rendering fault. Stamped per move by
   the handler, so it never reaches a real interactive element. */
[data-focus-silent]:focus,
[data-focus-silent]:focus-visible {
  outline: none;
}



/* ------ TOKENS ------ */

:root {
  /* Site-level token overrides — design-system.css is vendored read-only,
     so brand deviations live here (this file loads after it and wins). */
  --background-primary: #edeae3;

  /* Site layout tokens */
  --background-sidebar: var(--background-primary);
  --sidebar-width: 400px;
  --sidebar-collapsed-width: 64px;
  --erlen-bar-height: 3.5rem;
  --mobile-bar-height: 56px;
  --mobile-drawer-width: 100vw;
  --post-spacing: var(--space-l);
  --erlen-gap: 1rem;
  --erlen-gap-2x: 1.5rem;
  --z-sidebar: 50;
  /* Mobile drawer + its backdrop sit above the in-page .page-header
     (hardcoded z=400, above the transition layer at z=300). Without this,
     opening the mobile nav leaves the chrome's eyebrow bar floating on top
     of the drawer. */
  --z-backdrop: 500;
  --z-drawer: 600;
  /* Mobile bar must sit ABOVE the open drawer so the logo + close (X) stay
     visible and the drawer content scrolls underneath it. */
  --z-mobile-bar: 700;
  --bd-video-inset: var(--erlen-gap);
  --scroll-offset: var(--erlen-gap);
  --sidebar-logo-width: 220px;
  --feed-gap: calc(var(--space-l));
  --erlen-page-overlay-color: rgba(0, 0, 0, 0.05);
}


/* Dark mode token overrides */
:root[data-theme="dark"] {
  --background-primary: var(--neutral-900);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --background-primary: var(--neutral-900);
  }
}

/* Canonical sprite-logo contract — every place the site renders a
   brand logo wraps the <svg> in `.svg-logo`. The wrapper carries
   inline aspect-ratio (from sprite metadata) and is the sizing surface;
   the SVG inside fills it via this single global rule. Together they
   mean: style the wrapper's width OR height in CSS and proportions
   stay correct without per-logo overrides. */
.svg-logo > svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Sentence-case badges site-wide. Repoints the engine's badge tokens rather
   than overriding the class (Layer Rule 3) — the engine also exposes
   .badge[data-case="none"] for one-off instances. */
.badge {
  --badge-case: none;
  --badge-tracking: normal;
}


/* ------ TYPOGRAPHY SCALE ------ */
/*
 * Site override of the five type-role sizes. This file loads after
 * design-system.css, so these win the cascade; the engine copy is vendored
 * and untouched.
 *
 * The engine converged on this approach in v2.x — its roles are stepped rem
 * too, for the same reason (no vw term in font-size, so type respects browser
 * zoom and user font-size preferences, WCAG 1.4.4). What still differs, and
 * why this block stays:
 *
 *   1. Different rungs. The engine steps its headline 56 → 48 → 28px and
 *      drops to 28px for everything ≤959px; this site wants 40 → 48 → 64px
 *      and no single flat mobile/tablet tier.
 *   2. A second layer the engine doesn't have — the reading column steps on
 *      CONTENT width, not viewport width.
 *
 * Two layers because surfaces differ:
 *
 *   LAYER 1  :root + @media        Universal default. Drives chrome (sidebar,
 *                                  mobile drawer, page-header), modals, toasts —
 *                                  anything OUTSIDE the page content. Steps on
 *                                  the VIEWPORT width.
 *
 *   LAYER 2  .page-wrapper +       The reading column. .page-wrapper is the
 *            @container main-content   consistent child of [data-barba-stage]
 *                                  (the main-content container), so it steps on
 *                                  the real CONTENT width — which already shrinks
 *                                  /grows when the sidebar collapses. It declares
 *                                  its own base so it never inherits a viewport
 *                                  size that's wrong for a narrow column.
 *
 * Rungs chosen to keep headline > title > subline at every tier. px in comments.
 */

/* LAYER 1 — universal default (chrome, drawer, modals). Viewport-stepped. */
:root {
  --headline-size: var(--font-6xl); /* 40px */
  --title-size:    var(--font-3xl); /* 28px */
  --subline-size:  var(--font-l);   /* 20px */
  --body-size:     var(--font-s);   /* 16px */
  --label-size:    var(--font-xs);  /* 14px — a11y floor, never steps */
}

@media (min-width: 769px) {
  :root {
    --headline-size: var(--font-7xl); /* 48px */
    --title-size:    var(--font-5xl); /* 36px */
    --subline-size:  var(--font-2xl); /* 24px */
  }
}

@media (min-width: 1080px) {
  :root {
    --headline-size: var(--font-9xl); /* 64px */
    --title-size:    var(--font-7xl); /* 48px */
    --subline-size:  var(--font-2xl); /* 24px */
    --body-size:     var(--font-m);   /* 18px */
  }
}

/* LAYER 2 — content column. Container-stepped; own base, does NOT inherit L1.
   .article.is-next-read rides along: the JS-built next-read card sits outside
   .page-wrapper but must type-match the real article lead it transitions into
   (see initNextRead in erlen-article.js) — without this it falls back to the
   viewport-stepped Layer 1 sizes and the headline visibly resizes on advance. */
.page-wrapper,
.article.is-next-read {
  --headline-size: var(--font-6xl); /* 40px */
  --title-size:    var(--font-3xl); /* 28px */
  --subline-size:  var(--font-l);   /* 20px */
  --body-size:     var(--font-s);   /* 16px */
  --label-size:    var(--font-xs);  /* 14px — a11y floor, never steps */
}

@container main-content (min-width: 700px) {
  .page-wrapper,
  .article.is-next-read {
    --headline-size: var(--font-7xl); /* 48px */
    --title-size:    var(--font-5xl); /* 36px */
    --subline-size:  var(--font-2xl); /* 24px */
  }
}

@container main-content (min-width: 1000px) {
  .page-wrapper,
  .article.is-next-read {
    --headline-size: var(--font-8xl); /* 56px */
    --title-size:    var(--font-7xl); /* 48px */
    --subline-size:  var(--font-2xl); /* 24px */
    --body-size:     var(--font-m);   /* 18px */
  }
}

/* A modal is a full-bleed surface — it uses the Layer 1 default (sized to the
   viewport, not the content column). Authored at <body>/top-layer it gets this
   for free. If a dialog is ever placed INSIDE .page-wrapper and should ignore
   the column scale, reset its role tokens on the dialog element, e.g.:
   dialog, [role="dialog"] { --headline-size: var(--font-9xl); } */


/* ------ BASE TYPOGRAPHY ------ */

.text-title {
  font-size: var(--title-size);
  font-weight: var(--title-weight);
  line-height: var(--title-leading);
  letter-spacing: var(--title-tracking);
}

.text-headline {
  font-size: var(--headline-size);
  font-weight: var(--headline-weight);
  line-height: var(--headline-leading);
  letter-spacing: var(--headline-tracking);
}

.text-label {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  line-height: var(--label-leading);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
}

.text-body {
  font-size: var(--body-size);
  font-weight: var(--body-weight);
  line-height: var(--body-leading);
  letter-spacing: var(--body-tracking);
}

.drop-cap::first-letter,
[data-drop-cap]::first-letter {
  font-family: var(--font-secondary);
}

/* ------ LABEL ROLE ------ */

.label {
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  line-height: var(--label-leading);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
}

.label.is-eyebrow {
  font-size: var(--label-size);
  font-weight: var(--font-weight-bold);
  letter-spacing: var(--letter-spacing-l);
}

.label.is-mono {
  font-family: var(--font-quaternary);
}

.label.is-serif {
  font-family: var(--font-tertiary);
}


/* ------ LAYOUT GRID ------ */

.layout {
  display: grid;
  grid-template-columns: var(--sidebar-width) 1fr;
  grid-template-areas: "sidebar main";
  min-height: 100vh;
  background: var(--background-primary);
  transition: grid-template-columns var(--duration-xl) var(--ease-out);
}

body.is-sidebar-collapsed .layout {
  grid-template-columns: var(--sidebar-collapsed-width) 1fr;
}


/* ------ INSET (universal gap primitive) ------ */
/*
 * Apply on any element to add a gap on chosen edges. Uses padding so the
 * element's own background fills the gap and the inner content is pushed
 * inward. Works on page wrappers, sections, cards — any block element.
 *
 *   <div data-inset="all">              ← all four sides
 *   <div data-inset="left right">       ← horizontal only
 *   <div data-inset="top right">        ← two specific edges
 *
 * No attribute = full bleed (no inner gap; content sits flush to edges).
 */

[data-inset~="top"]    { padding-top:    var(--erlen-gap); }
[data-inset~="right"]  { padding-right:  var(--erlen-gap); }
[data-inset~="bottom"] { padding-bottom: var(--erlen-gap); }
[data-inset~="left"]   { padding-left:   var(--erlen-gap); }
[data-inset="all"]     { padding: var(--erlen-gap); }


/* ------ SIDEBAR ------ */

.sidebar {
  grid-area: sidebar;
  position: sticky;
  top: 0;
  width: var(--sidebar-width);
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--background-sidebar);
  z-index: var(--z-sidebar);
  box-sizing: border-box;
  padding: 0 var(--erlen-gap);
  transition: width var(--duration-xl) var(--ease-out);
}

.sidebar::after {
  content: "";
  position: absolute;
  top: var(--erlen-gap);
  bottom: var(--erlen-gap);
  right: 0;
  width: var(--border-s);
  background: var(--border-faded);
  pointer-events: none;
}

.sidebar-inner {
  display: flex;
  flex-direction: column;
  flex: 1 1 0%;
  overflow-y: auto;
  overflow-x: hidden;
  gap: 0;
  scrollbar-width: none;
}

.sidebar-inner::-webkit-scrollbar {
  display: none;
}

/* -- Nav (vertical numbered list — same treatment at every size) -- */

/* Pages label above the links; breathing room below the nav before the Work
   list. Nav keeps DOM order everywhere: intro on top, nav underneath,
   scrolling with the content. */
.sidebar-header {
  display: flex;
  flex-direction: column;
  gap: 0;
  flex-shrink: 0;
  margin-bottom: var(--space-xl);
}

/* Default state for the two structural triggers. They are revealed
   contextually: .sidebar-burger in the desktop collapsed rail,
   .sidebar-close-row in the tablet overlay. DOM order is
   logo → burger → close-row → inner(intro → nav → work) → footer. The nav
   scrolls in DOM order (intro on top) at every size. */
.sidebar-burger,
.sidebar-close-row {
  display: none;
}

/* Burger: shown in the desktop collapsed rail (alongside the mini logo).
   Click expands the sidebar back to its 400px open state. */
body.is-sidebar-collapsed .sidebar-burger {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-s) 0;
}

/* Footer collapse toggle: hidden in the collapsed rail. The rail burger is
   the only opener — without this the footer button would sit in the rail
   doing nothing (it only ever collapses, never expands). */
body.is-sidebar-collapsed .footer-collapse-toggle {
  display: none;
}

/* Large, mixed-case, left-aligned rows with a muted page number on the right */
.nav {
  display: flex;
  flex-direction: column;
  gap: 0;
  width: 100%;
  counter-reset: nav-counter;
}

.nav-link {
  position: relative;
  display: inline-flex;
  align-items: end;
  justify-content: space-between;   /* label left, number right */
  font-family: var(--title-family);
  font-size: var(--font-3xl);
  font-weight: var(--font-weight-medium);
  color: var(--text-primary);
  text-decoration: none;
  transition: color var(--duration-2xs) var(--ease-out),
              background-color var(--duration-m) var(--ease-out);
  padding: var(--space-m) var(--erlen-gap);
  counter-increment: nav-counter;
}

/* Dividers as ::after/::before lines (not borders) so neighbours can hide
   the lines touching a highlighted row — same trick as .sidebar-slot-link. */
.nav-link::after,
.nav-link:first-child::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: var(--border-s);
  background-color: var(--border-faded);
  transition: background-color var(--duration-m) var(--ease-out);
}

.nav-link::after {
  bottom: 0;
}

.nav-link:first-child::before {
  top: 0;
}

.nav-link:hover,
.nav-link.is-active {
  color: var(--text-accent);
  background-color: color-mix(in srgb, var(--text-primary), var(--alpha-5));
  border-radius: var(--radius-s);
}

/* :has(+ ...) reaches the previous sibling — CSS has no prev-sibling combinator */
.nav-link:hover::after,
.nav-link:has(+ .nav-link:hover)::after,
.nav-link.is-active::after,
.nav-link:has(+ .nav-link.is-active)::after,
.nav-link:first-child:hover::before,
.nav-link:first-child.is-active::before {
  background-color: transparent;
}

.nav-link-num {
  font-size: var(--label-size);
  font-weight: var(--font-weight-regular);
  color: var(--text-faded);
}

.nav-link-num::before {
  content: counter(nav-counter, decimal-leading-zero);
}

/* -- Intro block (logo + intro line, centered) -- */

/* Wrapper around .intro-block — narrows the link's box for visual breathing
   room. Tune padding to taste. */
.intro-block-wrap {
  padding: var(--erlen-gap) 0;
}

.intro-block {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xl);
  padding: var(--space-3xl) var(--space-xl);
  text-decoration: none;
  color: var(--text-primary);
  background-color: transparent;
  transition: background-color var(--duration-m) var(--ease-out);
  border-radius: var(--radius-s);
}

a.intro-block:hover {
  background-color: var(--bg-faded-5);
  color: var(--text-primary);
}

.intro-block::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--border-s);
  /* background: var(--border-faded); */
  transition: background-color var(--duration-m) var(--ease-out);
}

.intro-block:hover::after,
.intro-block:has(+ .sidebar-about:hover)::after {
  background-color: transparent;
}

/* Animated @erlenmasson lottie wordmark. The source JSON is authored with
   white strokes; currentColor re-inks it to the intro-block's text color
   (black in light mode) and keeps it legible in dark mode. */
.intro-lottie {
  width: 100%;
  max-width: 12rem;
  aspect-ratio: 552 / 88;
}

.intro-lottie svg path {
  stroke: currentColor;
}

.svg-logo:has([data-logo="by-default-centered"]) {
  width: var(--sidebar-logo-width);
  aspect-ratio: 1050 / 505;
}

.svg-logo:has([data-logo="by-default-centered"]) svg {
  display: block;
  width: 100%;
  height: 100%;
}

.sidebar-intro {
  font-family: var(--font-primary);
  font-weight: inherit;
  color: var(--text-secondary);
  text-align: center;
  margin: 0;
  transition: opacity var(--duration-2xs) var(--ease-out), visibility var(--duration-2xs);
  display: none;
}

/* -- Sidebar about (intro paragraph + read-more) -- */

.sidebar-about {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: calc(var(--erlen-gap) * 2) var(--erlen-gap);
  text-decoration: none;
  color: var(--text-primary);
  /* border-radius: var(--radius-m); */
  background-color: transparent;
  transition: background-color var(--duration-m) var(--ease-out);
}

.sidebar-about:hover {
  background-color: var(--background-faded);
}

.sidebar-about::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--border-s);
  background: var(--border-faded);
  transition: background-color var(--duration-m) var(--ease-out);
}

.sidebar-about:hover::after {
  background-color: transparent;
}

.sidebar-about-copy {
  color: var(--text-secondary);
  margin: 0;
}

.sidebar-read-more {
  font-weight: var(--font-weight-medium);
  color: var(--text-accent);
  align-self: flex-start;
}

.sidebar-footer {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  min-height: var(--erlen-bar-height);
  border-top: var(--border-s) solid var(--border-faded);
  font-size: var(--text-size-s);
  color: var(--text-faded);
  background: var(--background-sidebar);
}

/* Mobile-only footer action. Hidden on desktop + tablet (the collapse toggle
   owns the third slot there); revealed in the mobile drawer where collapse is
   meaningless. See the mobile media query for the swap. */
.footer-contact-link {
  display: none;
}

.sidebar-footer p {
  margin: 0;
  transition: opacity var(--duration-2xs) var(--ease-out), visibility var(--duration-2xs);
}

/* -- Nav button role class (token overrides on .button) -- */
.nav-btn {
  --button-bg: transparent;
  --button-border: transparent;
  --button-text-color: var(--text-primary);
  --button-radius: 0;
}

.button.nav-btn:hover {
  background-color: color-mix(in srgb, var(--text-primary), var(--alpha-5));
}


.dark-mode-toggle .dark-mode-icon-dark {
  display: none;
}

body.is-dark-mode .dark-mode-toggle .dark-mode-icon-light {
  display: none;
}

body.is-dark-mode .dark-mode-toggle .dark-mode-icon-dark {
  display: block;
}

.sound-toggle .sound-icon-off {
  display: none;
}

body.is-sound-off .sound-toggle .sound-icon-on {
  display: none;
}

body.is-sound-off .sound-toggle .sound-icon-off {
  display: block;
}


/* -- Sidebar section label -- */

.sidebar-label {
  color: var(--text-faded);
  padding: 0 var(--erlen-gap);
  margin: 0 0 var(--space-xs) 0;
  font-weight: var(--title-weight);
  font-size: var(--text-size-l);
  text-transform: none;
  letter-spacing: 0;
  color: inherit;
  text-decoration: none;
}

/* Some sidebar slot labels are pointer-only links to their section page.
   The underline is always present but transparent at rest, so it can fade in
   on hover (text-decoration-line itself can't be transitioned). Colour and
   underline both ease via the motion tokens. A quiet "happy accident", not a CTA. */
a.sidebar-label {
  text-decoration-line: underline;
  text-decoration-color: transparent;
  text-underline-offset: 0.1em;

}
a.sidebar-label:hover {
  color: var(--text-secondary);
  text-decoration-color: currentColor;
  text-underline-offset: 0.13em;
}

.nav-pages-label {  
    padding: 0 var(--erlen-gap);
    margin: 0 0 var(--space-m) 0;
    color: inherit;
}


.sidebar-about .sidebar-label {
  padding-left: 0;
}

/* -- Sidebar slot — case-study + article mini-cards rendered into the
   sidebar from the manifest. Card-style: padded link with a faded
   background, vertical gap between cards. -- */

/* Variant is set on the parent .sidebar-slot, never on the link or its children.
   To switch looks: change one class on the <nav>.
     <nav class="sidebar-slot slot-minimal">  → divider-line look (current default)
     <nav class="sidebar-slot slot-card">     → background-card look */

.sidebar-slot {
  display: flex;
  flex-direction: column;
}

/* Stacked sidebar sections (Case Studies + Playground) — section break above
   each slot after the first. */
.sidebar-slot + .sidebar-slot {
  margin-top: var(--erlen-gap);
}

/* -- Base — invariant across variants -- */

.sidebar-slot-link {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--erlen-gap);
  text-decoration: none;
  color: var(--text-primary);
  transition: background-color var(--duration-m) var(--ease-out);
}

.sidebar-slot-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--border-s);
  transition: background-color var(--duration-m) var(--ease-out);
}

.sidebar-slot-link.is-active .sidebar-slot-excerpt {
  color: color-mix(in srgb, var(--text-primary), var(--alpha-50));
}

.sidebar-slot-thumb {
  flex-shrink: 0;
  height: 66px;
  aspect-ratio: 1/1;
  object-fit: cover;
  border-radius: var(--radius-xs);
}

.sidebar-slot-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  min-width: 0;
}

.sidebar-slot-title {
  font-size: var(--text-size-m);
  line-height: var(--line-height-m);
  font-weight: 420;
}

.sidebar-slot-excerpt {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--text-size-s);
  line-height: var(--line-height-m);
  color: color-mix(in srgb, var(--text-primary), var(--alpha-50));
}

/* -- External (experience) links — open in a new tab. The ↗ mark mirrors the
   feed's .post-external so the new-tab cue is consistent site-wide. The link's
   ::after is already used for the divider line, so neutralise the global
   a[target="_blank"]::after glyph (design-system.css) here to keep the divider
   intact; the ↗ is rendered as a real element instead. -- */
.sidebar-slot-link[target="_blank"]::after {
  -webkit-mask-image: none;
  mask-image: none;
  width: auto;
  height: var(--border-s);
  margin-left: 0;
}

.sidebar-slot-external {
  position: absolute;
  top: var(--erlen-gap);
  right: var(--erlen-gap);
  display: flex;
  color: var(--text-faded);
  pointer-events: none;
}

.sidebar-slot-external .svg-icn {
  width: 1rem;
  height: 1rem;
}

/* -- Variant: slot-minimal — divider line, no card chrome -- */

.sidebar-slot.slot-minimal .sidebar-slot-link {
  padding: var(--erlen-gap);
}

.sidebar-slot.slot-minimal .sidebar-slot-link:hover,
.sidebar-slot.slot-minimal .sidebar-slot-link.is-active {
  background-color: var(--background-faded);
  border-radius: var(--radius-s);
}

.sidebar-slot.slot-minimal .sidebar-slot-link.is-active:hover {
  background-color: var(--background-darker);
}

.sidebar-slot.slot-minimal .sidebar-slot-link::after {
  background-color: var(--border-faded);
}

/* :has(+ ...) reaches the previous sibling — CSS has no prev-sibling combinator */
.sidebar-slot.slot-minimal .sidebar-slot-link:hover::after,
.sidebar-slot.slot-minimal .sidebar-slot-link:has(+ .sidebar-slot-link:hover)::after,
.sidebar-slot.slot-minimal .sidebar-slot-link.is-active::after,
.sidebar-slot.slot-minimal .sidebar-slot-link:has(+ .sidebar-slot-link.is-active)::after,
.sidebar-slot.slot-minimal .sidebar-slot-link:last-child::after {
  background-color: transparent;
}

/* -- Variant: slot-card — background cards, gap separator -- */

.sidebar-slot.slot-card {
  gap: var(--space-s);
}

.sidebar-slot.slot-card .sidebar-slot-link {
  padding: var(--erlen-gap);
  background-color: var(--background-faded);
  border-radius: var(--radius-s);
}

.sidebar-slot.slot-card .sidebar-slot-link:hover,
.sidebar-slot.slot-card .sidebar-slot-link.is-active,
.sidebar-slot.slot-card .sidebar-slot-link.is-active:hover {
  background-color: var(--background-darker);
}

.sidebar-slot.slot-card .sidebar-slot-link::after {
  background-color: transparent;
}

/* First-load reveal — initial hidden state, cleared by initSidebarReveal()
   in erlen-nav.js once the bd-intro curtain dismisses (or fonts.ready on
   repeat visits). visibility: hidden keeps invisible cards out of the tab
   order during the staggered delay window. Reduced-motion users see it
   instantly. */
.intro-block,
.sidebar-slot .sidebar-label,
.sidebar-slot-link {
  opacity: 0;
  visibility: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .intro-block,
  .sidebar-slot .sidebar-label,
  .sidebar-slot-link {
    opacity: 1;
    visibility: visible;
  }
}


/* ------ SIDEBAR — COLLAPSED (DESKTOP) ------ */

/* Brand lockup — avatar photo + text wordmark. Placeholder identity until a
   personal logo SVG exists; swap the markup in chrome.html when it does. */
.brand-avatar {
  display: block;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  object-fit: cover;
}

.brand-lockup {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--text-primary);
}

.brand-wordmark {
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

.mobile-bar-logo .brand-avatar-mobile {
  width: 36px;
  height: 36px;
}

.home-intro-avatar {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  object-fit: cover;
  margin-inline: auto;
}

body.is-sidebar-collapsed .sidebar {
  width: var(--sidebar-collapsed-width);
}

body.is-sidebar-collapsed .sidebar-inner {
  align-items: center;
}

body.is-sidebar-collapsed .sidebar-intro,
body.is-sidebar-collapsed .sidebar-footer p {
  opacity: 0;
  visibility: hidden;
}

body.is-sidebar-collapsed .sidebar-header,
body.is-sidebar-collapsed .intro-block-wrap,
body.is-sidebar-collapsed .intro-block,
body.is-sidebar-collapsed .sidebar-about,
body.is-sidebar-collapsed .sidebar-slot {
  display: none;
}

body.is-sidebar-collapsed .sidebar-footer {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);

}

/* -- Rail tooltips (design-system [data-tooltip], position=right) -- */

/* The rail triggers (mini logo, burger, dark-mode, sound) carry data-tooltip
   at every size, but bubbles only make sense where their labels are gone —
   the collapsed rail. Hidden inside the sidebar by default. */
.sidebar [data-tooltip]::before {
  display: none;
}

/* Enabled in the collapsed rail — desktop only. :not(.is-sidebar-tooltips-
   dismissed) is the WCAG 1.4.13 dismissal — Escape sets that class
   (erlen-nav.js), pointer or focus movement inside the sidebar removes it.
   The tablet AND mobile media queries repeat these selectors verbatim to
   switch them off (the collapsed class persists in localStorage across
   viewports, and the tablet rail must not depend on it). */
body.is-sidebar-collapsed:not(.is-sidebar-tooltips-dismissed) .sidebar [data-tooltip]:hover::before,
body.is-sidebar-collapsed:not(.is-sidebar-tooltips-dismissed) .sidebar [data-tooltip]:focus-visible::before {
  display: block;
}

/* Bubbles pop to the RIGHT of the 64px rail, over the main column. The
   sidebar doesn't clip them (no overflow set here; the triggers sit outside
   .sidebar-inner's overflow-x:hidden), but its stacking context (--z-sidebar,
   50) sits under the fixed .page-header (hardcoded z=400) that starts exactly
   at the rail's right edge — on L1/L2 pages the top bubbles would slide
   underneath it. 450 slots above the header but below --z-backdrop (500) /
   --z-drawer (600), so overlay + drawer keep painting above the rail. The
   mobile drawer re-asserts --z-drawer against this rule's specificity. */
/* Desktop-scoped like the tooltips themselves: at tablet/mobile the bump is
   pointless (no bubbles) and its transition override would break the drawer
   slide and the overlay's z timing when the collapsed class lingers. */
@media (min-width: 1080px) {
  body.is-sidebar-collapsed .sidebar {
    z-index: 450;
    /* The z bump waits for the width transition to settle (duration 0, delay
       --duration-xl): mid-collapse the fixed .page-header must keep sliding
       OVER the still-wide sidebar as it always did, not vanish beneath it.
       Expanding drops back to --z-sidebar instantly — the base .sidebar
       transition carries no z-index entry. */
    transition: width var(--duration-xl) var(--ease-out), z-index 0s var(--duration-xl);
  }
}


/* ------ MOBILE BAR (mobile only — hamburger trigger for sidebar drawer) ------ */
/* Hidden on desktop; the sidebar grid handles desktop layout. The narrow-viewport
   media query at the bottom of this file flips display + the grid template. */

.mobile-bar {
  display: none;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  height: var(--mobile-bar-height);
  padding: 0 var(--space-m);
  background: var(--background-sidebar);
  z-index: var(--z-mobile-bar);
  border-bottom: var(--border-s) solid var(--border-faded);
}

/* Mobile bar logo — wordmark on the left, tap target sized to the bar. */
.mobile-bar-logo {
  display: flex;
  align-items: center;
  height: 100%;
  padding: var(--space-xs) 0;
  color: var(--text-primary);
  text-decoration: none;
}

.svg-logo.mobile-bar-logo-mark {
  width: auto;
  height: 36px;
  aspect-ratio: 1050 / 505;
}

/* Hamburger is a .button.nav-btn[data-icon-only] (same as the footer icon
   buttons) — the button component drives bg/border/sizing. This rule only
   guarantees a comfortable 40px tap target on the mobile bar. */
.hamburger {
  width: 40px;
  height: 40px;
}

/* Hamburger icon morph: menu (≡) by default, close (✕) while the drawer is
   open. The mobile bar sits above the drawer so this stays tappable. */
.hamburger-icon-close {
  display: none;
}
body.is-drawer-open .hamburger-icon-open {
  display: none;
}
body.is-drawer-open .hamburger-icon-close {
  display: block;
}


/* ------ MAIN AREA + BARBA CONTAINERS ------ */

.main {
  grid-area: main;
  position: relative;
  min-height: calc(100vh - var(--mobile-bar-height));
  background-color: var(--background-primary);
  min-width: 0;
}

[data-barba="wrapper"] {
  position: relative;
  min-height: calc(100vh - var(--mobile-bar-height));
}

[data-barba="container"] {
  position: relative;
}

/* During a transition, both containers overlay each other so they can animate
   independently without affecting layout flow. */
body.is-animating [data-barba="wrapper"] {
  pointer-events: none;
}

/* ONLY the leaving container is taken out of flow during a transition. The
   entering container stays position:relative in normal flow at scroll 0 the
   whole time, so it never undergoes a position:fixed→static flip when
   is-animating is removed — that flip was a one-frame reflow of the entering
   page (the "advance" end-jump). Scoping to [data-erlen-role="leave"]
   structurally deletes that failure class for every scenario.

   position:fixed (not absolute) keeps the leaving container from being a
   containing block for its own sticky/fixed descendants — combined with the
   transform living only on [data-barba-stage], the leaving page + its sticky
   chrome move as one rigid unit. Its inline `top:-scrollY` (set in the
   before-hook atomic block, no !important here) overrides top:0 so it stays
   where the user was scrolled. fixed is viewport-relative, so inset past the
   sidebar by hand — left/right, not width:100% — mirroring the .page-overlay
   rule below. No bottom/height: it sizes to content so the stage can
   translate it. data-erlen-role="leave" is set on the leaving container in
   the before-hook atomic block so this rule (the pin) applies in the same
   paint-free task as is-animating + scrollTo (Bug B). */
body.is-animating [data-erlen-role="leave"] {
  position: fixed;
  top: 0;
  left: var(--sidebar-width);
  right: 0;
  /* No background here: the container is never transformed, so an opaque fill
     would sit static over home for the whole `exit` drop (and over the overlay
     on `enter`). The page background rides the moving stage via .page-wrapper
     below instead. */
}

/* Track sidebar collapse — mirror of the .page-overlay rule so the leaving
   container's left edge always aligns with the sidebar's current width. */
body.is-sidebar-collapsed.is-animating [data-erlen-role="leave"] {
  left: var(--sidebar-collapsed-width);
}

/* Mobile (≤768px): the sidebar is hidden behind a mobile-bar + drawer, so the
   leaving page pin sits flush left and starts under the mobile bar. (Tablet
   769–1079px offsets by the 64px rail instead — see the tablet block.) */
@media (max-width: 768px) {
  body.is-animating [data-erlen-role="leave"] {
    top: var(--mobile-bar-height);
    left: 0;
  }
}

/* The single per-page transform layer. Every page transition animates THIS
   element (never the container), so the container never becomes a containing
   block for its position:sticky/fixed descendants and the whole page + its
   sticky chrome moves as one rigid unit. Must stay exactly position:relative
   + display:block + width:100% with zero box-spacing — its box must coincide
   with the container content box or the scale pivot and the advance
   title-shift geometry shift. Do not add padding/margin/border/size here.

   container-type: inline-size hosts the @container main-content queries used
   throughout this file for content layouts (data-grid, content-feed, named
   layouts, component variants). The host MUST be a DESCENDANT of the pinned
   [data-barba="container"] — putting it on .main or [data-barba="wrapper"]
   would make the pin's position:fixed re-anchor to that element instead of
   the viewport, breaking the Bug-B atomic-pin. [data-barba-stage] already
   owns the transform layer for the same containing-block reason, so adding
   container-context here is architecturally consistent. */
[data-barba-stage] {
  position: relative;
  display: block;
  width: 100%;
  container-type: inline-size;
  container-name: main-content;
}


/*
 * Z-index per scenario. Barba 2.x does NOT add .barba-leave/.barba-enter
 * classes — we set data-erlen-role="leave|enter" ourselves in the
 * transition's leave() function. Without these explicit z-indexes, two
 * absolute siblings with equal z fall back to "later in DOM wins", which is
 * the wrong order for close (home gets appended after the leaving page
 * and would cover it).
 */

/* The scenario attribute on each container is the finalised SCENARIO name
   (enter / exit / swap / advance / fade). enter/swap rise and need the
   entering container above the .page-overlay (z-index 200); exit/advance
   keep their own stacking; fade crossfades (entering above so it can fade
   in over the leaving one). */

/* enter (home → page) — rise: home sits still beneath the overlay,
   the new page rises above it. */
body.is-animating [data-erlen-role="leave"][data-erlen-scenario="enter"]  { z-index: 1; }
body.is-animating [data-erlen-role="enter"][data-erlen-scenario="enter"]  { z-index: 300; }

/* exit (page → home): the leaving page falls AWAY in front of home */
body.is-animating [data-erlen-role="enter"][data-erlen-scenario="exit"] { z-index: 1; }
body.is-animating [data-erlen-role="leave"][data-erlen-scenario="exit"] { z-index: 2; }

/* swap (non-home ↔ non-home) — rise: same shape as enter. */
body.is-animating [data-erlen-role="leave"][data-erlen-scenario="swap"] { z-index: 1; }
body.is-animating [data-erlen-role="enter"][data-erlen-scenario="swap"] { z-index: 300; }

/* advance (next-read): leaving article on top sliding up, next revealed behind */
body.is-animating [data-erlen-role="leave"][data-erlen-scenario="advance"] { z-index: 2; }
body.is-animating [data-erlen-role="enter"][data-erlen-scenario="advance"] { z-index: 1; }

/* fade (defensive fallback) — crossfade: entering above so it fades in. */
body.is-animating [data-erlen-role="leave"][data-erlen-scenario="fade"] { z-index: 1; }
body.is-animating [data-erlen-role="enter"][data-erlen-scenario="fade"] { z-index: 300; }


/* ------ PAGE OVERLAY (rise transition) ------ */

/* A persistent dim layer used by the "rise" page transition. Covers the
   main content area + page-header, but leaves the sidebar (desktop) and
   mobile-bar at full brightness. Opacity is driven by WAAPI in
   erlen-barba.js — default state is invisible. */
.page-overlay {
  position: fixed;
  top: 0;
  left: var(--sidebar-width);
  right: 0;
  bottom: 0;
  background-color: var(--erlen-page-overlay-color);
  opacity: 0;
  pointer-events: none;
  z-index: 200;
}

/* Track sidebar collapse — mirror of the .page-header rule so the overlay's
   left edge always aligns with the sidebar's current width. */
body.is-sidebar-collapsed .page-overlay {
  left: var(--sidebar-collapsed-width);
}

/* Mobile (≤768px): the mobile bar appears, so the page-overlay starts under
   it and runs full-width. (Tablet offsets by the 64px rail — tablet block.) */
@media (max-width: 768px) {
  .page-overlay {
    top: var(--mobile-bar-height);
    left: 0;
  }
}

/* (The old parked-header rule lived here. Deleted in Phase 3: the page-header
   is now choreographed entirely by GSAP inside buildTransition's timeline —
   it sets its own offscreen start state inline at timeline build, so there is
   no CSS-parked default and no Bug-A race net to maintain. The header's
   permanent z-index:400 — so it always sits above the entering stage (peaks
   at 300) — moved onto the base .page-header rule below.) */


/* ------ SECTION HEADER (sticky pattern) ------ */

.section-header {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  min-height: var(--erlen-bar-height);
  background-color: var(--background-primary);
  border-bottom: var(--border-s) solid var(--border-faded);
  transition: opacity var(--duration-xs) var(--ease-out);
}

/* (The scenario-split opacity mask + its baseline fade-back-in helper lived
   here. Deleted in Phase 3. The mask faded the sticky chrome on close/push
   because the OLD model transformed the container (a containing-block trap).
   Since Phase 2 the transform is on the inner stage and the container is
   never transformed, so the sticky chrome rides the stage rigidly as one
   unit on every scenario — no mask needed (amendment J: on advance the
   toc/share/sticky-media now travel up WITH the article instead of fading).
   .section-header keeps its own opacity transition above for hover/state —
   that is unrelated to the deleted mask.) */

.section-header[data-sticky="false"] {
  position: static;
}

.section-header-start {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  min-width: 0;
  align-self: stretch;
}

.section-header-end {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  flex-shrink: 0;
  align-self: stretch;
}

.section-header-end > .button {
  align-self: stretch;
}

/* Mobile: the filter row can outgrow the screen — cap the header at viewport
   width and let the chips scroll horizontally under the finger instead of
   overflowing the page sideways. Scrollbar hidden: swipe-only. */
@media (max-width: 768px) {
  .section-header-end {
    flex-shrink: 1;
    min-width: 0;
    max-width: 100%;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .section-header-end::-webkit-scrollbar {
    display: none;
  }
}


/* ------ PAGE HEADER (top-of-page bar with close button) ------ */

/* Lives outside the Barba wrapper (sibling of <main>'s data-barba="wrapper").
   Position: fixed (not sticky) so it doesn't take flow space — toggling
   [hidden] doesn't reflow the document. The page-wrapper's padding-top
   (rule below) reserves the visual space for L1+ pages. The data-sticky
   attribute in markup is now a harmless no-op. */
.page-header {
  position: fixed;
  top: 0;
  left: var(--sidebar-width);
  right: 0;
  /* Always above the transition layers: the entering stage peaks at z-index
     300 during enter/swap. This was a scenario-scoped boost in the now-
     deleted parked-header rule; made unconditional since the header is
     persistent chrome and never needs to sit behind a transitioning page.
     (.page-overlay at 200 only has opacity mid-transition, so at rest the
     header being above it is invisible — safe.) */
  z-index: 400;
  background-color: var(--background-primary);
  padding: 0 var(--erlen-gap);
  transition: left var(--duration-s) var(--ease-out);
}

body.is-sidebar-collapsed .page-header {
  left: var(--sidebar-collapsed-width);
}

/* Reserve space for the fixed header on L1+ pages. Padding lives on the
   container's own page-wrapper so the entering page is correctly padded
   BEFORE animating in (driven by the container's own data-level, not by
   body[data-current-level] which updates late in afterEnter). */
/* The [data-barba-stage] wrapper sits between the container and .page-wrapper,
   so the reservation must traverse it (`> [data-barba-stage] > .page-wrapper`).
   The plain `> .page-wrapper` pair is kept as the stageOf()-degrade fallback
   (a container without a stage still gets padded). Without this the L1/L2
   page jams under the fixed .page-header and the next-read title snaps up. */
[data-barba="container"][data-level="1"] > [data-barba-stage] > .page-wrapper,
[data-barba="container"][data-level="2"] > [data-barba-stage] > .page-wrapper,
[data-barba="container"][data-level="1"] > .page-wrapper,
[data-barba="container"][data-level="2"] > .page-wrapper {
  padding-top: var(--erlen-bar-height);
}


/* Page background. It MUST live on .page-wrapper, not [data-barba="container"]:
   the container is never transformed (only the inner [data-barba-stage] moves),
   so a background on the container would occlude the .page-overlay on `enter`
   and the home page on `exit`. .page-wrapper is inside the stage, so its fill
   travels with the transform — the page reads as one opaque unit while still
   revealing the overlay/home beneath during transitions. */
.page-wrapper {
  background-color: var(--background-primary);
  min-height: 100vh;
}

/* Defensive — beats `.page-header { display: ... }` specificity when [hidden]
   is toggled on the persistent instance (e.g. on L0 home). */
.page-header[hidden] {
  display: none;
}

.page-header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  min-height: var(--erlen-bar-height);
  border-bottom: var(--border-s) solid var(--border-faded);
}

.page-header-start {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  min-width: 0;
  align-self: stretch;
}

.page-header-end {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  flex-shrink: 0;
  align-self: stretch;
}

.page-header-end > .button {
  align-self: stretch;
}




.eyebrow-header {
  margin: 0;
  font-size: var(--text-size-l);
}

/* Mobile (≤768px) chrome positioning — the mobile bar is shown, so the
   page-header sits full-width below it and sticky section-headers clear it.
   (Tablet offsets the page-header by the 64px rail — see the tablet block.) */
@media (max-width: 768px) {
  .section-header {
    top: var(--mobile-bar-height);
  }
  /* Mobile bar + page-header: the feed filter bar must pin below both. */
  .case-studies-section .section-header,
  .playground-section .section-header {
    top: calc(var(--mobile-bar-height) + var(--erlen-bar-height));
  }
  .page-header {
    left: 0;
    top: var(--mobile-bar-height);
  }
  body.is-sidebar-collapsed .page-header {
    left: 0;
  }
}

/* Filter-click target. scrollIntoView on the grid lands its top below the
   sticky filter bar with a small clearance. The sticky bar stays stuck at
   viewport top; the .feed-masterhead intro scrolls off-screen.
   Anchoring on .section-header directly would be wrong — it's position:
   sticky and its bounding rect collapses to 0 when stuck. */
.content-feed {
  scroll-margin-top: var(--erlen-bar-height);
}

/* Case studies: the filter bar sits below the masthead (not at the top of the
   section), so when it pins it must clear the opaque fixed .page-header
   (z-index 400) — top:0 would hide it behind the page-header. The masthead
   scrolls away above it. The filter-click scroll target must then clear both
   the page-header AND the pinned filter bar (each ≈ --erlen-bar-height). */
.case-studies-section .section-header,
.playground-section .section-header {
  top: var(--erlen-bar-height);
}
.case-studies-section .content-feed,
.playground-section .content-feed {
  scroll-margin-top: calc(var(--erlen-bar-height) * 2);
}

/* Home work section — L0, so there is no fixed page-header above it: the
   sticky filter bar pins flush to the viewport top on desktop. On mobile the
   sticky mobile bar occupies the top, so offset by its height (the
   --erlen-bar-height token tracks it). Same reasoning for the feed's
   scroll-margin and the /#work anchor target. */
.home-work {
  scroll-margin-top: var(--erlen-bar-height);
}
.home-work .section-header {
  top: 0;
}
.home-work .content-feed {
  scroll-margin-top: var(--erlen-bar-height);
}
@media (max-width: 768px) {
  .home-work .section-header {
    top: var(--erlen-bar-height);
  }
  .home-work .content-feed {
    scroll-margin-top: calc(var(--erlen-bar-height) * 2);
  }
}


/* ------ PAGE INTRO (shared) ------ */

.page-headline {
  font-size: var(--headline-size);
  font-weight: var(--headline-weight);
  color: var(--text-primary);
  margin: 0;
}

.page-title {
  font-size: var(--title-size);
  font-weight: var(--title-weight);
  color: var(--text-primary);
  font-stretch: var(--title-width);
}

.headline-display {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0;
  text-transform: uppercase;
  line-height: 0.85;;
}


.home-headline {
  margin: 0;
}

@container main-content (max-width: 768px) {
  .home-headline {
    font-size: 9vw;;
  }
}

.home-intro-content {
      padding-left: var(--space-s);
    padding-right: var(--space-s);
}

@container main-content (max-width: 768px) {
  .home-intro-content {
    /* mobile */
  }
}






/* Cycling word — last word of the home headline rotates through a list of
   alternates. Items stack absolutely; one is active in flow at a time. JS
   measures the longest item at init and locks min-width on the wrapper so
   swaps don't reflow the line. The animation style is selected by the
   data-cycle-style attribute and registered in erlen-home.js. */
.cycle-word {
  display: inline-block;
  position: relative;
  vertical-align: baseline;
  overflow: hidden;
  /* Buffer below the baseline so overflow:hidden doesn't clip descenders
     (g, p, y, etc.). Negative margin-bottom keeps external layout in place. */
  padding-bottom: 0.2em;
  margin-bottom: -0.2em;
  /* min-width is set inline by JS after measurement */
}

/* The cycle-word descender buffer shifts the inline word upward on a
   single-line heading where text precedes the cycle. The H1 puts the
   cycle-word on its own line (via <br>) so the buffer is invisible there;
   here it's inline after "Make your content" and visible as a baseline jog. */
#product-spotlight-heading .cycle-word {
  padding-bottom: 0;
  overflow: inherit;
}

.cycle-word-item {
  display: inline-block;
  position: absolute;
  inset: 0;
  visibility: hidden;
  white-space: nowrap;
}

.cycle-word-item.is-active {
  position: relative;
  visibility: visible;
}

/* Visually-hidden sibling that exposes the canonical word to screen readers.
   The animated .cycle-word wrapper is aria-hidden, so without this the
   heading would have no last-word at all in the accessibility tree. Standard
   "visually hidden" pattern; kept local until/unless a foundation utility
   lands in design-system.css. */
.cycle-word-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Visually-hidden polite live region announced on every Barba navigation
   (route-change announcement — SR users otherwise get no page-change cue
   because the title swap is silent and <main> has no accessible name).
   Persistent chrome (sibling of .page-overlay, outside the Barba wrapper)
   so it survives container swaps. Same standard "visually hidden" technique
   as .cycle-word-sr above. */
.route-status {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.page-subline {
  font-size: var(--subline-size);
  font-weight: var(--subline-weight);
  line-height: var(--line-height-l);
}

/* Inline image inside a headline — scales with the surrounding font-size,
   sits on the baseline, breathes a touch on each side. Multiple images per
   headline use the same class with no extra rules. */
.img-headline {
  display: inline-block;
  height: 0.7em;
  width: auto;
  vertical-align: baseline;
  margin: 0 0.15em;
  object-fit: cover;
  border-radius: var(--radius-s);
  display: none;
}

/* ------ HOME HERO (wraps a .bd-video player; site-specific padding + chrome) ------ */

.home-hero {
  --bd-video-inset: var(--erlen-gap);
}

.home-hero .bd-video-player {
  overflow: hidden;
  /* border-radius: var(--radius-m); */
  border: var(--border-s) solid var(--bg-faded-10);
  /* border-radius: var(--radius-s); */
  max-height: calc(100vh - var(--erlen-gap) * 2); /* cap hero to viewport; object-fit:cover crops past 16:9 */
}

/* ------ HOME STATS (three-column data points beneath the hero) ------ */

.home-stats-headline {
  color: var(--text-primary);
}

.home-stats-headline-faded {
  color: var(--text-faded);
}

.home-stats-item {
  border-left: var(--border-s) solid var(--border-faded);
  padding: 0 var(--space-2xl);
}

.home-stats-figure {
  font-size: var(--font-7xl);
  font-family: var(--font-tertiary);
  font-weight: var(--font-weight-semi-bold);
  line-height: 1;
  letter-spacing: var(--letter-spacing-xs);
  margin: 0;
}

.home-stats-label {
  font-size: var(--text-size-l);
  font-weight: var(--font-weight-medium);
  line-height: 1.4;
  color: var(--text-primary);
  margin: 0;
}

.home-stats-copy {
  color: var(--text-faded);
  margin: 0;
}

.home-stats-source {
  font-size: var(--text-size-xs);
  color: var(--text-faded);
  margin: var(--space-2xs) 0 0;
}

/* Stacked: rotate the divider from a left rule to a top rule when the items
   stack. .home-stats-item is shared by two grids that collapse at DIFFERENT
   content widths, so the rotation is scoped per grid to match each stack point
   exactly — a single global breakpoint left one grid stacked with the wrong
   divider in the band between the two thresholds.
     - home  (.home-stats → [data-grid][data-layout="thirds"]) stacks at 959.98px
     - about (.process-steps) stacks at 768px */
@container main-content (max-width: 959.98px) {
  .home-stats .home-stats-item {
    border-left: 0;
    border-top: var(--border-s) solid var(--border-faded);
    padding-left: 0;
    padding-top: var(--space-l);
  }
}

@container main-content (max-width: 768px) {
  .process-steps .home-stats-item {
    border-left: 0;
    border-top: var(--border-s) solid var(--border-faded);
    padding-left: 0;
    padding-top: var(--space-l);
  }
}

/* Decorative autoplay videos (article bodies, section backgrounds). No JS —
   pure HTML attributes (autoplay, muted, loop, playsinline). Distinct from
   the full .bd-video player. Emitted by the {{bg-video}} markdown shortcode
   in cms/generator/lib/render-markdown.js. */
.bg-video {
  display: block;
  width: 100%;
  height: auto;
}


/* ------ HOME SERVICES (lettered specialties list) ------ */
/* Letters A/B/C/D are rendered via CSS counter on ::before, so adding or
   reordering items keeps the markers in sync without manual edits. */

.services-list {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: services-counter;
}

.services-list-item {
  display: grid;
  grid-template-columns: 2rem 1fr;
  align-items: baseline;
  column-gap: var(--space-2xl);
  padding-block: var(--space-l);
  counter-increment: services-counter;
  font-family: var(--font-primary);
  font-size: var(--headline-size);
  font-weight: var(--font-weight-light);
  line-height: 1;
  letter-spacing: -0.01em;
}

.services-list-item::before {
  content: counter(services-counter, decimal-leading-zero);
  align-self: start;
  padding-top: 0.6em;
  font-family: var(--font-tertiary);
  font-weight: var(--font-weight-regular);
  letter-spacing: 0.05em;
  color: var(--text-faded);
}

@container main-content (max-width: 768px) {
  .services-list-item {
    grid-template-columns: 1.5rem 1fr;
    column-gap: var(--space-m);
    padding-block: var(--space-m);
  }
}


/* ------ CONTENT FEED (masonry grid) ------ */
/*
 * Row-span masonry. Grid auto-placement drops each card into the shortest
 * column, so DOM order (newest first) reads across the top row. erlen-feed.js
 * measures each card and sets its grid-row-end span — grid-auto-rows is 1px,
 * so span N = N px, with the vertical card gap baked into the span. Filtering
 * hides cards with [hidden] and the grid repacks in the same reflow — no JS
 * layout calls on filter. Replace with native CSS masonry (display: masonry)
 * once it's cross-browser.
 */

.feed-masterhead {
  padding: var(--space-8xl) var(--space-m);
  display: flex;
  flex-direction: column;
  text-align: center;
  gap: var(--space-l);

}

.feed-masterhead > * {
  margin: 0;
}

.feed-headline {
  font-family: var(--font-display);
  font-size: clamp(3rem, 15vw, 15rem);
  letter-spacing: 0;
  font-weight: 600;
  text-transform: uppercase;
  line-height: 1;
}

.feed-subline {
  font-size: clamp(1rem, 2vw, 2rem);
  color: var(--text-faded);
  font-style: italic;
}

.content-feed {
  padding-top: var(--feed-gap);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 1px;
  column-gap: var(--feed-gap);
  /* Cards shrink to content height instead of stretching over their spanned
     area (span = card + gap), so hover targets end at the card's edge. */
  align-items: start;
  width: 100%;
}

/* Pre-hydration: build-time skeletons (see cms/generator/lib/render-feed-skeletons.js)
   hold the feed's height in plain grid rows until erlen-feed.js replaces them
   and the JS masonry spans take over. The 1px auto-rows only work with spans,
   and masonry bakes the vertical gap into them — hence the explicit row-gap. */
.content-feed:has(.post-skeleton) {
  grid-auto-rows: auto;
  row-gap: var(--feed-gap);
}

/* -- Feed transition states -- */
/* Hide: quick exit. Show: slightly slower entrance. */
/* Transition style set via data-feed-transition on .content-feed */

/* fade (default) */
.post-item {
  opacity: 1;
  transform: scale(1);
  transition: opacity var(--duration-2xl) var(--ease-out),
              transform var(--duration-2xl) var(--ease-out);
}

.post-item.is-hiding {
  opacity: 0;
  transition-duration: var(--duration-2xs);
}

.post-item.is-showing {
  opacity: 0;
}

/* scale — shrink to 0, grow back to 1 */
[data-feed-transition="scale"] .post-item.is-hiding,
[data-feed-transition="scale-stagger"] .post-item.is-hiding {
  opacity: 0;
  transform: scale(0);
  transition-duration: var(--duration-2xs);
}

[data-feed-transition="scale"] .post-item.is-showing,
[data-feed-transition="scale-stagger"] .post-item.is-showing {
  opacity: 0;
  transform: scale(0);
}

@media (prefers-reduced-motion: reduce) {
  .post-item {
    transition: none;
  }
}

/* span:2 front-matter → card stretches across two feed columns (collapses
   back to one at the single-column breakpoint below) */
.post-item[data-span="2"] {
  grid-column: span 2;
}
@container main-content (max-width: 768px) {
  .post-item[data-span="2"] { grid-column: auto; }
}

@container main-content (max-width: 1280px) {
  .content-feed { grid-template-columns: repeat(3, 1fr); }
}

@container main-content (max-width: 960px) {
  .content-feed { grid-template-columns: repeat(2, 1fr); }
}

@container main-content (max-width: 768px) {
  .content-feed { grid-template-columns: 1fr; }
}


/* ------ STICKY STACK (reusable layout primitive) ------ */

/* Sticky .sticky-media beside a .scroll-stack column; combo with .section-content + data-grid. */
[data-grid].sticky-stack {
  align-items: start;        /* don't stretch columns — sticky needs travel room */
  grid-auto-flow: dense;     /* DOM-first .sticky-media claims its desktop column without gapping */
}

.sticky-media {
  position: sticky;
  top: calc(var(--erlen-bar-height) + var(--erlen-gap));
  height: calc(100vh - var(--erlen-bar-height) - var(--erlen-gap) * 2);
  align-self: start;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 5;
}

.scroll-stack {
  display: flex;
  flex-direction: column;
}

.erlen-bio {
    padding-left: var(--space-2xl);
}

/* Each direct child of .scroll-stack, any tag. */
.scroll-stack-item {
  display: flex;
  flex-direction: column;
  justify-content: center;     /* default — items vertically centred */
  gap: var(--space-m);
}

/* Margin reset for text at any depth; :where() keeps specificity at 0 so component rules still win. */
.scroll-stack-item :where(h1, h2, h3, h4, h5, h6, p, ul, ol) {
  margin: 0;
}

/* Vertical alignment variants — set data-align on the parent .scroll-stack. */
.scroll-stack[data-align="top"]    .scroll-stack-item { justify-content: flex-start;    }
.scroll-stack[data-align="bottom"] .scroll-stack-item { justify-content: flex-end;      }
.scroll-stack[data-align="space"]  .scroll-stack-item { justify-content: space-between; }

.about-beliefs .scroll-stack-item {
  border-bottom: var(--border-s) solid var(--border-faded);
  padding: var(--space-8xl) var(--space-l);
}


/* Single-column below 959.98px: release the sticky full-height media so it flows inline. */
@container main-content (max-width: 959.98px) {
  .sticky-media {
    position: static;
    height: auto;
    z-index: auto;
  }
}

@container main-content (max-width: 768px) {
  .scroll-stack {
    gap: var(--space-2xl);
  }
  .scroll-stack-item {
    min-height: auto;
    gap: var(--space-s);
  }
}

/* Bio header — display-size name above the bio copy. */
.founder-erlen {
  border-bottom: var(--border-s) solid var(--border-faded);
}

.founder-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
  padding-bottom: var(--space-3xl);
}

.founder-header h1 {
  font-family: var(--font-display);
  font-size: var(--font-10xl);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0;
  line-height: 0.8;
  text-transform: uppercase;
}

/* ------ ABOUT PAGE ------ */

/* -- Media frame (feature/hero mask — image, video, or BD-Video player) -- */

.media-frame {
  width: 100%;
  overflow: hidden;
}

.media-frame > img,
.media-frame > picture,
.media-frame > picture > img,
.media-frame > video,
.media-frame > .bd-video,
.media-frame > .bg-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Parallax — oversize the image so bd-animations.js has vertical slack to translate;
   negative margin-top centers it. Tune per-instance with --bd-parallax-overflow.
   Covers both a bare <img> child and a responsive <picture> > <img>. */
.media-frame[data-bd-parallax] > img,
.media-frame[data-bd-parallax] > picture > img {
  height: calc(100% + var(--bd-parallax-overflow, 30%));
  margin-top: calc(var(--bd-parallax-overflow, 30%) / -2);
}

/* Fill — drop the aspect-ratio and fill the parent's height (e.g. inside .sticky-media).
   Gated on container width so it reverts to data-ratio when the content area is narrow. */
@container main-content (min-width: 960px) {
  .media-frame.is-fill {
    height: 100%;
    aspect-ratio: auto;
  }
}

/* -- Origin / Founding story -- */

.origin-prose {
  columns: 2;
  column-gap: var(--space-2xl);
  color: var(--text-primary);
}

.origin-prose p { margin: 0; }
.origin-prose p:not(:last-child) { margin-bottom: var(--space-l); }

.origin-prose em {
  font-style: italic;
  color: var(--text-primary);
}

@container main-content (max-width: 768px) {
  .origin-prose { columns: 1; }
}

/* -- How we work / process -- */

.about-process-divider {
  margin: var(--space-5xl) 0;
}

/* ------ ARTICLE HEADER ------ */

[data-article] {
  position: relative;
}

.article-lead {
  padding: var(--section-l) 0;
  background-color: var(--background-primary);
}

.article-header > .block {
  align-items: center;
  text-align: center;
  gap: var(--space-xl);

}

/* -- Article meta (badge, read time, date) -- */

.article-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-l);
}

.article-meta-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-s);
  color: var(--text-faded);
}

.article-meta-item .svg-icn {
    width: 1rem;
    height: 1rem;
}

.article-headline {
  margin: 0;
  font-size: var(--headline-size);
  font-weight: var(--headline-weight);
  line-height: var(--headline-leading);
  letter-spacing: var(--headline-tracking);
  text-wrap: balance;
}



/* -- Article author (avatar + name link) -- */

.article-author {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  color: var(--text-primary);
  text-decoration: none;
}

.article-author-by {
  font-style: italic;
  color: var(--text-faded);
}

.article-author-avatar {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-pill);
  object-fit: cover;
}



/* -- Article content wrapper (3-column layout) -- */

.article-content-wrapper {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: var(--space-2xl);
  width: 100%;
}

.article-sidebar {
  flex: 1 1 0%;
  align-self: stretch;
}

.toc-block,
.share-block {
  position: sticky;
  top: calc(var(--mobile-bar-height) + var(--space-8xl));
  display: flex;
  flex-direction: column;
  gap: var(--space-l);
  max-width: 220px;
  margin: 0 auto;
  opacity: 0;
  transition: opacity var(--duration-l) var(--ease-out);
}

.toc-block.is-visible,
.share-block.is-visible {
  opacity: 1;
}

.article-body {
  flex: 0 1 auto;
  width: 60%;
  max-width: 600px;
}

/* Article figures ({{figure}}/{{img}} in the markdown body). .cs-media ships with
   no margin of its own because inside a .block the engine zeroes child
   margin-block and gap owns the rhythm. .article-body is not a .block — it's
   prose flow — so figures get their spacing here. margin-block only, matching the
   engine's own reset: margin-inline must stay free for data-align="center".
   Collapses with the preceding <p>'s margin-bottom rather than summing, so this
   is the gap on both sides. --space-xl matches the h1-h6 margin-top rhythm. */
.article-body .cs-media {
  margin-block: var(--space-xl);
}

/* TOC-link / hash jumps land below the fixed .page-header instead of behind it.
   Mobile adds --mobile-bar-height because the page-header is pushed down by
   the mobile bar at <=768px. */
.article-body :is(h1, h2, h3, h4, h5, h6) {
  scroll-margin-top: calc(var(--erlen-bar-height) + var(--erlen-gap));
}

@container main-content (max-width: 768px) {
  .article-body :is(h1, h2, h3, h4, h5, h6) {
    scroll-margin-top: calc(var(--mobile-bar-height) + var(--erlen-bar-height) + var(--space-m));
  }
}


/* -- Table of contents -- */

.toc-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

.toc-link {
  display: block;
  color: var(--text-secondary);
}

.toc-link:hover {
  color: var(--text-primary);
}

.toc-link.is-active {
  color: var(--text-primary);
  font-weight: var(--font-weight-medium);
}

.toc-list li {
  margin: 0;
}

.article-sidebar .toc-link {
  text-decoration: none;
  color: var(--text-faded);
}

.article-sidebar .toc-link.is-active {
  color: var(--text-primary);
}

/* -- "In this article" (inline TOC, always visible) -- */

.in-this-article {
  padding-left: var(--space-xl);
  margin-bottom: var(--space-xl);
  border-left: var(--border-s) solid var(--border-faded);
  display: flex;
  flex-direction: column;
  gap: var(--space-l);
}

/* -- Share block -- */

.share-links {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-s);
}

.share-links .button {
  --button-padding-y: var(--space-s);
  --button-padding-x: var(--space-s);
  --button-text-size: var(--font-s);
  --icon-size: 1em;
}

/* -- Responsive: collapse sidebars on tablet -- */

@container main-content (max-width: 960px) {
  .article-sidebar {
    display: none;
  }

  .article-body {
    width: 100%;
    max-width: none;
  }
}


/* ------ LEGAL PAGES ------ */

/* Long-form legal / policy layout (terms, privacy, cookies, accessibility).
   Deliberately its OWN class set — not the article's — so the two doc layouts
   can evolve independently. The shared TOC engine (erlen-article.js) keys on
   the [data-toc-*] attributes, NOT these classes; the generated .toc-list /
   .toc-link and the .is-active / .is-visible states are shared with articles
   by design. */

.legal-lead {
  padding: var(--section-l) 0;
  background-color: var(--background-primary);
}

.legal-header > .block {
  align-items: center;
  text-align: center;
  gap: var(--space-xl);
}

.legal-headline {
  margin: 0;
  font-size: var(--headline-size);
  font-weight: var(--headline-weight);
  line-height: var(--headline-leading);
  letter-spacing: var(--headline-tracking);
  text-wrap: balance;
}

.legal-content {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: var(--space-2xl);
  width: 100%;
}

.legal-rail {
  flex: 1 1 0%;
  align-self: stretch;
}

.legal-body {
  flex: 0 1 auto;
  width: 60%;
  max-width: 600px;
}

/* TOC / hash jumps clear the fixed .page-header (mobile adds the mobile bar). */
.legal-body :is(h1, h2, h3, h4, h5, h6) {
  scroll-margin-top: calc(var(--erlen-bar-height) + var(--erlen-gap));
}

@container main-content (max-width: 768px) {
  .legal-body :is(h1, h2, h3, h4, h5, h6) {
    scroll-margin-top: calc(var(--mobile-bar-height) + var(--erlen-bar-height) + var(--space-m));
  }
}

/* Sticky sidebar TOC — hidden until the inline sentinel scrolls out of view
   (JS toggles .is-visible on [data-toc-reveal]). */
.legal-toc {
  position: sticky;
  top: calc(var(--mobile-bar-height) + var(--space-8xl));
  display: flex;
  flex-direction: column;
  gap: var(--space-l);
  max-width: 220px;
  margin: 0 auto;
  opacity: 0;
  transition: opacity var(--duration-l) var(--ease-out);
}

.legal-toc.is-visible {
  opacity: 1;
}

.legal-toc .toc-link {
  text-decoration: none;
  color: var(--text-faded);
}

.legal-toc .toc-link.is-active {
  color: var(--text-primary);
}

/* Inline TOC at the top of the body — always visible. */
.legal-toc-inline {
  padding-left: var(--space-xl);
  margin-bottom: var(--space-xl);
  border-left: var(--border-s) solid var(--border-faded);
  display: flex;
  flex-direction: column;
  gap: var(--space-l);
}

.legal-toc .label,
.legal-toc-inline .label {
  color: var(--text-faded);
}

/* Collapse both rails on tablet; body goes full width. */
@container main-content (max-width: 960px) {
  .legal-rail {
    display: none;
  }

  .legal-body {
    width: 100%;
    max-width: none;
  }
}


/* ------ NEXT READ ------ */

/* Next-read block — sits between the current article and the next-read
   card as a standalone sibling. Only created by JS. */
.next-read {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-m);
  padding: var(--space-6xl) 0;
  background-color: var(--background-primary);
}

.next-read::before,
.next-read::after {
  content: "";
  flex: 1;
  height: var(--border-s);
  background-color: var(--divider-color);
}

.next-read-label {
  color: var(--text-faded);
}

/* Sibling gap between the current article and the next-read preview card */
.article + .article {
  margin-top: var(--erlen-gap);
}

/* Next-read wrapper — shares .article base (bg + radius) with the current
   article card. Relative positioning + cursor live on the wrapper so the
   full-coverage link overlay works across the whole card. */
.article.is-next-read {
  position: relative;
  cursor: pointer;
}

/* Full-coverage clickable overlay */
.article.is-next-read .next-read-link {
  position: absolute;
  inset: 0;
  z-index: 1;
}



/* ------ POSTS ------ */


/* -- Base post (shared across all types) -- */

.post {
  display: flex;
  flex-direction: column;
  gap: var(--post-spacing);
  padding: var(--post-spacing);
  background-color: var(--bg-faded-3);
  text-decoration: none;
  color: var(--text-primary);
  overflow: hidden;
  border-radius: var(--radius-s);
  transition: background-color var(--duration-l) var(--ease-out);
}

.post[data-feed-layout="cover"] {
  padding: 0;
}


.post:hover {
  background-color: var(--bg-faded-7);
}

/* Thumbnail container — acts as a mask. The ratio lives on the container,
   the image inside fills it via object-fit: cover. This means the image
   can be any aspect ratio and the container crops it. */
.post-thumbnail {
  position: relative; /* scopes the .case-study-card-logo scroll-reveal overlay */
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  /* border-radius: var(--radius-s); */
}

.img-thumb,
.vdo-thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--duration-2xl) var(--ease-out);
}

.post:hover .img-thumb,
.post:hover .vdo-thumb {
  transform: scale(1.05);
}

/* Video thumbnail — hover-to-play, bound by thumb-hover.js. Playback + zoom
   both fire on hover. Touch + narrow viewports see the poster only. */
.vdo-thumb {
  background: var(--background-faded);
}

.post-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-s);
  z-index: 10;
}

.post-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
}

/* Hover-reveal: body hidden until the card is hovered or keyboard-focused.
   Only on hover-capable devices — touch users keep the body visible. */
@media (hover: hover) {
  .post-body {
    opacity: 0;
    transition: opacity var(--duration-s) var(--ease-out);
  }
  .post:hover .post-body,
  .post:focus-visible .post-body,
  .splide__slide .post-body {
    opacity: 1;
  }
  @media (prefers-reduced-motion: reduce) {
    .post-body {
      transition: none;
    }
  }
}

/* Contact success recommendations opt out of hover-reveal — the cards appear
   after submit with no pointer on them, so the titles must read immediately. */
[data-contact-recommendations] .post-body {
  opacity: 1;
}

/* Post labels */

.post-label,
.post-client-label {
  color: var(--text-faded);
}


.post[data-feed-layout="cover"] .post-label {
  color: var(--white-alpha-70);
}

.post[data-feed-layout="cover"] .post-client-label {
  color: var(--white);
}


.post-title {
  margin: 0;
  font-weight: var(--title-weight);
  color: var(--text-primary);
  font-size: var(--text-size-xl);
  text-wrap: balance;
}

.splide .post-title {
  font-size: var(--text-size-l);
}

.post-excerpt {
  margin: 0;
  color: var(--text-faded);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  display: none;
}

.post:hover .post-excerpt {
  color: var(--text-primary);
}

.post-meta {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  margin-top: var(--space-xs);
}

.post-meta-item {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--text-faded);
}

.bottom-meta {
  display: none;
}

.post-meta > *:not(:last-child)::after {
  content: "\00b7";
  margin-left: var(--space-s);
}
.post-read-status {
  display: none;
  color: var(--text-faded);
  flex-shrink: 0;
  background: none;
  padding: 0;
  gap: var(--space-xs);
}

.post-item.is-read .post-read-status {
  display: flex;
}

.post-read-status .svg-icn {
  width: 1.1rem;
  height: 1.1rem;
}

/* Experience external-link affordance — sits in the header end-slot where the
   read badge would be on internal cards, signalling the card opens out in a new
   tab. Icon colour stays consistent with .post-label across layouts. */
.post-external {
  display: flex;
  flex-shrink: 0;
  color: var(--text-faded);
}

.post-external .svg-icn {
  width: 1.1rem;
  height: 1.1rem;
}

.post[data-feed-layout="cover"] .post-external {
  color: var(--white-alpha-70);
}

/* Experience posts open in a new tab and so inherit the global
   a[target="_blank"]::after external-link glyph (design-system.css). The card
   already signals "opens externally" via .post-external (↗) in its header, so
   suppress the duplicate inline glyph on the card link itself. */
.post[target="_blank"]::after {
  content: none;
}

.post-hero {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  margin-bottom: var(--space-l);
}


/* -- Cover layout (full-bleed image with text overlay) -- */
/* Card height is driven by aspect-ratio on the post itself (default 4:5).
   Override with data-ratio="W:H" on the .post element. The :not([data-ratio])
   rule scopes the default so the global [data-ratio] block can win. */

.post[data-feed-layout="cover"] {
  position: relative;
  padding: 0;
}

.post[data-feed-layout="cover"]:not([data-ratio]) {
  aspect-ratio: 4 / 5;
}

.post[data-feed-layout="cover"] .post-header {
  padding: var(--erlen-gap) var(--erlen-gap) var(--space-5xl) var(--erlen-gap);
    background: linear-gradient(to bottom, var(--black-alpha-30) 0%, transparent 100%);

}
.post[data-feed-layout="cover"] .post-thumbnail {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  z-index: 0;
}

.post[data-feed-layout="cover"] .post-body {
  position: relative;
  z-index: 1;
  justify-content: flex-end;
  height: 100%;
  padding: 0 var(--post-spacing) var(--space-2xl) var(--post-spacing);
  background: linear-gradient(to top, var(--black-alpha-70) 0%, transparent 70%);
}

.post[data-feed-layout="cover"] .post-title {
  color: var(--white);
  /* font-size: var(--font-xl); */
}

.post[data-feed-layout="cover"] .post-excerpt {
  color: var(--white-alpha-80);
}

.post[data-feed-layout="cover"] .post-meta {
  color: var(--white-alpha-60);
}

.post[data-feed-layout="cover"] .post-read-status {
  color: var(--white);
}




/* -- Content type accent colours -- */



/* ------ CASE STUDY ------ */

/* -- Header -- */
.case-study-header-content {
    border-bottom: var(--border-s) solid var(--border-faded);
}


.case-study-header-content {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
}

.case-study-header-actions {
  justify-self: end;
  align-self: end;
}

.case-study-synopsis {
  color: var(--text-secondary);
  text-wrap: pretty;;
}

.case-study-services {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  display: none;
}

.case-study-services .tag:nth-child(n + 6) {
  display: none;
}

/* -- Toggle button -- */

.button.case-study-toggle {
  gap: var(--space-xs);
  white-space: nowrap;
  --button-border: var(--button-text-color);
}

.case-study-toggle-icon-close {
  display: none;
}

.case-study-wrapper.is-info-open .case-study-toggle-icon-add,
body.is-case-study-open .case-study-toggle-wrap .case-study-toggle-icon-add {
  display: none;
}

.case-study-wrapper.is-info-open .case-study-toggle-icon-close,
body.is-case-study-open .case-study-toggle-wrap .case-study-toggle-icon-close {
  display: block;
}

/* -- Fixed toggle wrap (persistent, outside Barba container) -- */

.case-study-toggle-wrap {
  display: none;
  position: fixed;
  bottom: var(--erlen-gap-2x);
  right: var(--erlen-gap-2x);
  z-index: 50;
  transition: opacity var(--duration-xs) var(--ease-out),
              transform var(--duration-xs) var(--ease-out);
}

.case-study-toggle-wrap.is-visible {
  display: flex;
}

.case-study-toggle-wrap.is-hidden {
  opacity: 0;
  pointer-events: none;
  transform: translateY(var(--space-s));
}

/* -- Two-panel body -- */

.case-study-body {
  display: flex;
  align-items: stretch;
  scroll-margin-top: var(--erlen-bar-height);
}

.case-study-visuals {
  flex: 1 1 0%;
  display: flex;
  flex-direction: column;
  gap: var(--erlen-gap);
  min-width: 0;
  padding: var(--erlen-gap);
  transition-property: width;
  transition-duration: 800ms;
  transition-timing-function: cubic-bezier(0.86, 0, 0.07, 1);
}

.case-study-visuals > p {
  margin: 0;
}

.case-study-visuals img,
.case-study-visuals video,
.case-study-visuals .bd-video {
  width: 100%;
  display: block;
}

/* -- Visual grid (side-by-side media) -- */

.case-study-grid {
  display: grid;
  gap: var(--erlen-gap);
}

.case-study-grid[data-cols="2"] { grid-template-columns: repeat(2, 1fr); }
.case-study-grid[data-cols="3"] { grid-template-columns: repeat(3, 1fr); }

.case-study-grid img,
.case-study-grid video,
.case-study-grid .bd-video,
.case-study-grid .bg-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* -- Media wrapper (placeholder + scroll-reveal container) -- */

.cs-media {
  position: relative;
  overflow: hidden;
  background-color: var(--background-faded);
  border-radius: var(--radius-s);
}

.cs-media::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 24px;
  height: 24px;
  margin: -12px 0 0 -12px;
  border: 2px solid var(--border-primary);
  border-top-color: transparent;
  border-radius: 50%;
  animation: cs-spin 0.8s linear infinite;
  opacity: 0.4;
  pointer-events: none;
}

@keyframes cs-spin {
  to { transform: rotate(360deg); }
}

/* Loaded state (set by erlen-media.js once the media has painted). Without it
   the spinner animates forever underneath the media and the placeholder
   background shows through transparent images, letterboxed ratios and iframes
   before first paint. */
.cs-media.is-loaded {
  background-color: transparent;
}

.cs-media.is-loaded::after {
  display: none;
}

.cs-media img,
.cs-media video,
.cs-media .bd-video {
  position: relative;
  z-index: 1;
  width: 100%;
  display: block;
}

/* Third-party embed ({{vimeo}}, {{embed}}). Unlike img/video it has no intrinsic
   ratio to grow into, so it fills the frame the wrapper's data-ratio already sizes.
   border-radius is inherited rather than left to the wrapper's overflow:hidden —
   Safari won't clip a compositing layer (which an iframe always is) to a rounded
   parent, so the embed would sit square-cornered among rounded media. */
.cs-media .vimeo-embed,
.cs-media .embed-frame {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  border: 0;
  border-radius: inherit;
  display: block;
}

.case-study-grid .cs-media {
  height: 100%;
}

.case-study-grid .cs-media img,
.case-study-grid .cs-media video,
.case-study-grid .cs-media .bg-video {
  height: 100%;
  object-fit: cover;
}

@media (prefers-reduced-motion: reduce) {
  .cs-media::after { animation: none; }
}

.case-study-content {
  width: 0;
  height: 0;
  max-width: 50%;
  opacity: 0;
  overflow: clip;
  display: flex;
  flex-direction: column;
  transition-property: width, opacity;
  transition-duration: 800ms, 200ms;
  transition-timing-function: cubic-bezier(0.86, 0, 0.07, 1), ease-out;
  transition-delay: 0ms, 0ms;
}

.case-study-wrapper.is-info-open .case-study-content {
  width: 50%;
  height: auto;
  opacity: 1;
  transition-delay: 0ms, 200ms;
}

.case-study-content-inner {
  position: sticky;
  top: 0;
  padding: var(--erlen-gap) var(--erlen-gap) var(--space-8xl) var(--erlen-gap);
  display: flex;
  flex-direction: column;
  gap: var(--space-xl);
  max-width: 36rem;
  margin: 0 auto;
}

/* -- Info panel typography -- */

.case-study-content-inner h2 {
  margin: 0;
}

.case-study-content-inner h3 {
  margin: 0;
}

.case-study-content-inner h4 {
  margin: 0;
}

.case-study-content-inner p {
  color: var(--text-secondary);
  line-height: var(--line-height-l);
}

.case-study-content-inner ul {
  padding-left: var(--space-l);
  color: var(--text-secondary);
  line-height: var(--line-height-l);
}

/* -- Case study mobile -- */

@container main-content (max-width: 768px) {
  .case-study-header [data-grid] > .block {
    grid-column: 1 / -1;
  }

  .case-study-header-actions {
    justify-self: stretch;
  }

  .case-study-header-actions .case-study-toggle {
    width: 100%;
  }

  .case-study-body {
    flex-direction: column;
    scroll-margin-top: calc(var(--mobile-bar-height) + var(--erlen-bar-height));
  }

  .case-study-content {
    order: -1;
    width: 100%;
    height: auto;
    max-width: 100%;
    max-height: 0;
    transition-property: max-height, opacity;
    transition-duration: 800ms, 200ms;
    transition-timing-function: cubic-bezier(0.86, 0, 0.07, 1), ease-out;
  }

  .case-study-wrapper.is-info-open .case-study-content {
    width: 100%;
    max-height: 200vh;
    opacity: 1;
  }

  .case-study-content-inner {
    position: static;
    max-height: none;
  }

  .case-study-grid[data-cols="2"],
  .case-study-grid[data-cols="3"] {
    grid-template-columns: 1fr;
  }
}


/* -- Related work slider -- */



.case-study-slider:empty { display: none; }

.case-study-slider {
  overflow: hidden;
  width: 100%;
  padding: var(--space-2xl) 0;
 }

.case-study-slider-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2xl) var(--space-xl);
    border-top: var(--border-s) solid var(--border-faded);

}

.case-study-slider-title {
  font-size: var(--title-size);
  font-weight: var(--title-weight);
  line-height: var(--title-leading);
  letter-spacing: var(--title-tracking);
  margin: 0;
}

.case-study-splide {
  width: 100%;
}
.case-study-track {
  width: 100%;
}

.case-study-slide .post {
  height: 100%;
}

.case-study-arrow.splide__arrow--prev svg {
    transform: none !important;
}


/* ------ BACKDROP (mobile drawer scrim) ------ */

.sidebar-backdrop {
  position: fixed;
  inset: 0;
  background: var(--background-modal, var(--black-alpha-50));
  z-index: var(--z-backdrop);
  opacity: 0;
  pointer-events: none;
  /* Matches the page-transition motion (--duration-m + --ease-in-out) so the
     drawer scrim fades with the same smoothness as a page swap. */
  transition: opacity var(--duration-m) var(--ease-in-out);
}

body.is-drawer-open .sidebar-backdrop,
body.is-overlay-open .sidebar-backdrop {
  opacity: 1;
  pointer-events: auto;
}


/* ============================================================
   TABLET (769–1079px) — 64px rail + 400px overlay
   ============================================================ */

/* Tablet keeps the sidebar inline as a 64px rail. The rail burger opens it as
   a 400px overlay (NOT full width) with a backdrop. The overlay reuses the
   base desktop flex column — nav pinned, inner scrolls, footer pinned — so the
   close button + footer stay fixed while the work list scrolls. */
@media (min-width: 769px) and (max-width: 1079px) {
  .layout,
  body.is-sidebar-collapsed .layout {
    grid-template-columns: var(--sidebar-collapsed-width) 1fr;
  }

  .sidebar {
    width: var(--sidebar-collapsed-width);
  }

  /* Rail shows: burger + (empty inner spacer) + footer */
  .sidebar-burger {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-s) 0;
  }

  /* Hide full-width content in the rail (inner stays as the flex spacer that
     pushes the footer to the bottom) */
  .sidebar-header,
  .intro-block-wrap,
  .sidebar-slot {
    display: none;
  }

  .sidebar-footer {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
  }
  .footer-collapse-toggle {
    display: none;
  }

  /* Chrome aligns with the 64px rail (no mobile bar at this size). */
  .page-header,
  .page-overlay,
  body.is-animating [data-erlen-role="leave"] {
    left: var(--sidebar-collapsed-width);
  }

  /* -- Overlay (burger tapped) -- */
  body.is-overlay-open .sidebar {
    width: var(--sidebar-width);
    position: fixed;
    inset: 0 auto 0 0;
    z-index: var(--z-drawer);
    /* inherits the base flex column: .sidebar-inner flex:1 1 0
       overflow-y:auto (intro + nav + work scroll), footer
       flex-shrink:0 (pinned bottom). */
  }
  body.is-overlay-open .sidebar-burger {
    display: none;
  }
  body.is-overlay-open .sidebar-header {
    display: flex;
  }
  body.is-overlay-open .intro-block-wrap {
    display: block;
  }
  body.is-overlay-open .sidebar-slot {
    display: block;
  }
  body.is-overlay-open .sidebar-footer {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
  /* Close button pinned top-right — position:absolute against the fixed
     overlay stays put while the inner scrolls under it. */
  body.is-overlay-open .sidebar-close-row {
    display: block;
    position: absolute;
    top: var(--space-s);
    right: var(--space-s);
    z-index: 2;
  }

  /* Footer collapse toggle never applies on tablet (no inline collapse). */
  body.is-overlay-open .footer-collapse-toggle {
    display: none;
  }

  /* Rail tooltips never on tablet — the 64px rail here is permanent, so
     tooltips would hinge on whether the desktop collapse class happens to
     linger in localStorage (nondeterministic for identical UI), they'd leak
     into the expanded overlay's footer buttons, and on touch a tap would
     strand a sticky :hover bubble. Selectors mirror the desktop enabling
     rules verbatim (same specificity, later in source) so these always win
     here. */
  body.is-sidebar-collapsed:not(.is-sidebar-tooltips-dismissed) .sidebar [data-tooltip]:hover::before,
  body.is-sidebar-collapsed:not(.is-sidebar-tooltips-dismissed) .sidebar [data-tooltip]:focus-visible::before {
    display: none;
  }
}


/* ============================================================
   MOBILE (≤768px) — hidden sidebar + mobile bar + drawer
   ============================================================ */

@media (max-width: 768px) {
  .layout,
  body.is-sidebar-collapsed .layout {
    grid-template-columns: 1fr;
    grid-template-rows: var(--mobile-bar-height) 1fr;
    grid-template-areas:
      "mobilebar"
      "main";
  }

  /* Mobile bar: sticky at the top, z-index ABOVE the drawer so the logo +
     close (X) stay visible and the drawer content scrolls underneath it. */
  .mobile-bar {
    display: flex;
    grid-area: mobilebar;
    position: sticky;
    top: 0;
  }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: var(--mobile-drawer-width);
    height: 100dvh;
    z-index: var(--z-drawer);
    transform: translateX(-100%);
    /* Matches the page-transition motion (--duration-m + --ease-in-out) so the
       drawer slides open/closed with the same smoothness as a page swap. One
       declaration governs both directions, keeping open/close symmetric. */
    transition: transform var(--duration-m) var(--ease-in-out);
    overflow-y: auto;
    overflow-x: hidden;
    /* Offset the scroll content below the fixed mobile bar; as the user
       scrolls, nav + intro + work slide UP under the opaque bar. */
    padding-top: var(--mobile-bar-height);
  }

  /* Whole column scrolls as one — only the footer pins. */
  .sidebar-inner {
    flex: 0 0 auto;
    overflow: visible;
  }

  /* Footer pinned to the bottom while the rest scrolls behind it. Its opaque
     bg occludes content scrolling underneath. */
  .sidebar-footer {
    position: sticky;
    bottom: 0;
  }

  .sidebar::after {
    display: none;
  }

  body.is-drawer-open .sidebar {
    transform: translateX(0);
  }

  /* Rail-only triggers never show on mobile (the hamburger morph closes).
     The collapse-to-rail toggle joins them — the full-width drawer can't
     collapse, so the footer's third slot becomes Contact instead. */
  .sidebar-burger,
  .sidebar-close-row,
  .footer-collapse-toggle,
  body.is-sidebar-collapsed .sidebar-burger {
    display: none;
  }

  /* Contact takes the collapse toggle's place in the drawer footer
     (matches the other nav-btn buttons' inline-flex). */
  .footer-contact-link {
    display: inline-flex;
  }

  /* Drawer always shows full content even if .is-sidebar-collapsed lingers. */
  body.is-sidebar-collapsed .sidebar {
    width: var(--mobile-drawer-width);
  }
  body.is-sidebar-collapsed .sidebar-inner {
    align-items: stretch;
  }
  body.is-sidebar-collapsed .sidebar-intro,
  body.is-sidebar-collapsed .sidebar-footer p,
  body.is-sidebar-collapsed .sidebar-header,
  body.is-sidebar-collapsed .intro-block-wrap,
  body.is-sidebar-collapsed .intro-block,
  body.is-sidebar-collapsed .sidebar-about,
  body.is-sidebar-collapsed .sidebar-slot {
    display: revert;
    opacity: 1;
    visibility: visible;
  }

  /* Rail tooltips never on mobile — the drawer shows full labels, and the
     collapsed class can linger from desktop (localStorage). Selectors mirror
     the desktop enabling rules verbatim (same specificity, later in source)
     so these always win here. */
  body.is-sidebar-collapsed:not(.is-sidebar-tooltips-dismissed) .sidebar [data-tooltip]:hover::before,
  body.is-sidebar-collapsed:not(.is-sidebar-tooltips-dismissed) .sidebar [data-tooltip]:focus-visible::before {
    display: none;
  }

  /* The footer collapse toggle has no role on mobile. */
  [data-sidebar-collapse] {
    display: none;
  }

  /* Lock body scroll while the drawer is open */
  body.is-drawer-open {
    overflow: hidden;
  }
}


/* ------ PAGE CLOSE BUTTON (X) ------ */

/* Site close-btn — extends the .button.transparent variant with a high-contrast icon color */
.close-btn {
  --button-color: var(--text-primary);
  --icon-size: 1.3rem;
}

.close-btn-label {
  font-family: var(--font-tertiary);
  font-size: var(--text-size-s);
  font-weight: var(--font-weight-bold);
  text-transform: uppercase;
  letter-spacing: var(--letter-spacing-l);
  color: var(--text-faded);
}

/* Disable case study layout transitions during page transitions.
   These transitions are for user interactions (info panel toggle),
   not for Barba page animations. Without this, the 800ms layout
   transition fires during the page transition and creates stutter. */
body.is-animating .case-study-visuals,
body.is-animating .case-study-content {
  transition: none;
}

@media (max-width: 768px) {
  .close-btn-label {
    display: none;
  }
  /* Label hides → restore square padding so the button matches other icon-only buttons */
  .close-btn {
    --button-padding-x: var(--button-padding-y);
  }
}


/* ------ CONTACT FORM ------ */

.contact-block {
  max-width: 790px;
  width: 100%;
  margin-left: auto;
  margin-right: auto;
  padding: var(--space-3xl) 0;
}

.contact-headline {
  font-weight: var(--headline-weight);
  font-size: var(--headline-size);
  line-height: var(--headline-leading);
}

/* -- Form layout -- */

.contact-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xl);
  margin-top: var(--space-2xl);
  position: relative;
}

.contact-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

.contact-label {
  font-family: var(--font-primary);
  font-size: var(--body-size);
  color: var(--text-primary);
  font-weight: var(--font-weight-regular);
  white-space: nowrap;
  margin-bottom: 0;
}

/* -- Inline label + input on one line (desktop) -- */

.contact-inline {
  display: flex;
  align-items: center;
  gap: var(--space-m);
}

.contact-inline input,
.contact-inline .contact-email-wrap {
  flex: 1;
  min-width: 0;
}

@container main-content (max-width: 768px) {
  .contact-inline {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-s);
  }
}

/* -- Bottom-border-only inputs (name + email) -- */

.contact-inline input,
.contact-email-wrap input {
  border-top: 0;
  border-left: 0;
  border-right: 0;
  border-radius: 0;
  background-color: transparent;
  border-color: var(--border-primary);
  border-bottom-width: var(--border-s);
}

/* Mouse/programmatic focus: thicken the underline + keep the subtle fill. */
.contact-inline input:focus,
.contact-email-wrap input:focus {
  background-color: var(--input-background);
  border-bottom-width: var(--border-m);
  border-bottom-color: var(--text-primary);
  outline: none;
}

/* Keyboard focus: the site-standard visible ring. */
.contact-inline input:focus-visible,
.contact-email-wrap input:focus-visible {
  outline: var(--border-m) solid var(--text-primary);
  outline-offset: 2px;
}

/* Keep the design-system red error ring on a keyboard-focused errored field. */
.contact-inline input.is-error:focus-visible,
.contact-email-wrap input.is-error:focus-visible {
  outline-color: var(--status-danger);
}

/* -- Chip groups (button-based selectable chips) -- */

.contact-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-m);
}

.contact-chips .button {
  flex: 1 1 auto;
  padding-left: 0;
  padding-right: 0;
}

.contact-chips .button.is-selected {
  --button-bg: var(--text-primary);
  --button-text-color: var(--background-primary);
  --button-border: var(--text-primary);
}

.contact-chips .button.is-selected:hover {
  background-color: var(--text-primary) !important;
  color: var(--background-primary) !important;
  border-color: var(--text-primary) !important;
}

.contact-chips .button .svg-icn {
  display: none;
}

.contact-chips .button.is-selected .svg-icn {
  display: inline-flex;
}

@container main-content (max-width: 768px) {
  /* Services: single column */
  .contact-chips[role="group"] {
    flex-direction: column;
  }

  /* Budget: two columns */
  .contact-chips[role="radiogroup"] .button {
    flex: 1 1 calc(50% - var(--space-m) / 2);
  }
}

/* -- Textarea -- */

.contact-form textarea {
  resize: vertical;
  min-height: var(--space-13xl);
}

/* -- Email validation indicator -- */

.contact-email-wrap {
  position: relative;
}

.contact-email-wrap input {
  padding-right: var(--space-2xl);
}

.contact-email-tick {
  position: absolute;
  right: var(--space-m);
  top: 50%;
  transform: translateY(-50%);
  color: var(--status-success);
  font-size: var(--text-size-l);
  pointer-events: none;
}

/* Error states handled by design system (.is-error, .is-error:focus) */

/* -- Submit area -- */

.contact-actions {
  display: flex;
}

.contact-actions .button {
  width: 100%;
}

.contact-error-callout {
  margin: 0;
}

/* -- Page wrapper -- */

.contact-wrapper {
  overflow: hidden;
}

/* -- Form state toggle -- */

.contact-form-state .contact-success {
  display: none;
}

.contact-form-state.is-submitted .contact-form {
  display: none;
}

.contact-form-state.is-submitted .contact-success {
  display: flex;
  flex-direction: column;
  gap: var(--space-l);
  margin-top: var(--space-2xl);
  padding-top: var(--space-2xl);
}

.contact-success-box {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  padding: var(--space-2xl);
  border: var(--border-s) solid var(--border-faded);
}

.contact-success-title {
  margin: 0;
}

.contact-success-body {
  margin: 0;
  color: var(--text-secondary);
}

/* -- Post-submit recommendations: 2-column editorial grid -- */
/* Shared .post[data-feed-layout="editorial"] cards; eyebrow label spans both columns. */

.contact-recommendations {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--feed-gap, var(--space-l));
  margin-top: var(--space-l);
}

.contact-recommendations-label {
  grid-column: 1 / -1;
  margin: 0;
}

@container main-content (max-width: 768px) {
  .contact-recommendations {
    grid-template-columns: 1fr;
  }
}


/* ------ SKIP LINK ------ */

.skip-link {
  position: absolute;
  top: var(--space-s);
  left: var(--space-s);
  /* Must clear every fixed/sticky chrome layer or the link takes focus while
     painted behind one — it was z 480, under the mobile bar's 700, so at
     <=768px the first Tab appeared to do nothing. Top of the stack is correct
     here: a skip link is only ever on screen while focused, and it has to be
     the visible thing at that moment. */
  z-index: calc(var(--z-mobile-bar) + 1);
  padding: var(--space-s) var(--space-m);
  background: var(--text-primary);
  color: var(--background-primary);
  font-family: var(--font-tertiary);
  text-decoration: none;
  /* border-radius: var(--radius-s); */
  transform: translateY(-150%);
  transition: transform var(--duration-m) var(--ease-in-out);
}

.skip-link:focus-visible {
  transform: translateY(0);
  outline: none;
}


/* ------ FOOTER ------ */
/* Dark surface scoped to the footer via [data-theme="dark"]. The design system
   flips --background-primary, --text-primary, --text-faded, --border-color
   inside this element only, so it remains dark regardless of the global theme. */

.footer-wrapper {
  background-color: var(--neutral-950);
  color: var(--text-primary);
  padding: 0 var(--space-2xl);
  border-radius: var(--radius-s);
}

/* -- Top grid: links / brand / contact -- */

.footer-grid {
  padding-top: var(--space-3xl);
  padding-bottom: var(--space-2xl);
  align-items: end;
}

.footer-col {
  display: flex;
  flex-direction: column;
  gap: var(--space-m);
}

.footer-col-end {
  text-align: right;
}

.footer-label {
  color: var(--text-faded);
}

.footer-nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

.footer-link {
  color: var(--text-primary);
  text-decoration: none;
  transition: color var(--duration-xs) var(--ease-out);
}

.footer-link:hover,
.footer-link:focus-visible {
  color: var(--text-secondary);
}

/* -- Brand block (centered logo + tagline) -- */

.footer-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-l);
  text-align: center;
  padding: var(--space-5xl) 0;
}

.footer-logo {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  color: var(--text-primary);
}

a.footer-logo:hover {
  color: var(--text-secondary);

}

.footer-logo .intro-lottie {
  max-width: 300px;
}

/* -- Bottom row: copyright + legal -- */

.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-m) var(--space-l);
  padding: var(--space-2xl) 0;
  border-top: var(--border-s) solid var(--border-faded);
  font-size: var(--text-size-s);
  color: var(--text-faded);
}

.footer-copyright {
  margin: 0;
}

.footer-legal {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-l);
}

.footer-legal-link {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: color var(--duration-xs) var(--ease-out);
}

.footer-legal-link:hover,
.footer-legal-link:focus-visible {
  color: var(--text-primary);
}

/* -- Responsive: stack columns on narrow viewports -- */
/* The design system's mobile reset ([data-grid] > *) has lower specificity than
   the data-col-start rules, so we re-stack the footer blocks explicitly here. */

@container main-content (max-width: 768px) {
  .footer-grid > .block {
    grid-column: 1 / -1;
  }

  .footer-col-end {
    text-align: left;
  }

  .footer-bottom {
    justify-content: flex-start;
  }
}


/* ------ ERROR PAGE (404) ------ */

.error-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(100vh - var(--mobile-bar-height) - (var(--erlen-gap) * 2));
  padding: var(--space-3xl) var(--erlen-gap);
  background-color: var(--black-alpha-3);
  border-radius: var(--radius-m);
}

.error-page__inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-l);
  max-width: 44rem;
  width: 100%;
}

.error-page__code {
  font-family: var(--font-tertiary);
  font-size: var(--font-10xl);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  color: var(--text-faded);
  margin: 0;
  animation: error-code-fade var(--motion-page-fade-duration) var(--motion-page-fade-easing) both;
}

.error-page-heading {
  font-size: var(--headline-size);
  line-height: 1.05;
  color: var(--text-primary);
  margin: 0;
  max-width: 20ch;
}

.error-page__lede {
  font-size: var(--text-size-l);
  line-height: 1.4;
  color: var(--text-secondary);
  margin: 0;
  max-width: 60ch;
}

.error-page__actions {
  margin-top: var(--space-s);
}

.error-page__suggestions {
  width: 100%;
  margin-top: var(--space-xl);
  padding-top: var(--space-l);
  border-top: var(--border-s) solid var(--border-secondary);
}

.error-page-suggestions-label {
  color: var(--text-secondary);
  margin: 0 0 var(--space-m) 0;
}

.error-page__suggestions ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s) var(--space-l);
}

.error-page__suggestions a {
  font-size: var(--text-size-l);
  color: var(--text-primary);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: color var(--duration-m) var(--ease-in-out);
}

.error-page__suggestions a:hover {
  color: var(--text-secondary);
}

@keyframes error-code-fade {
  from { opacity: 0; transform: translateY(0.25rem); }
  to   { opacity: 1; transform: translateY(0); }
}

@container main-content (max-width: 768px) {
  .error-page__code { font-size: var(--font-8xl); }
}


/* ------ ABOUT — CAPABILITIES ACCORDION ------ */

.capability-description p {
  margin: 0 0 var(--space-m);
}

.capability-description p:last-child { margin-bottom: 0; }

/* Format accordion header sizing — the Services page capability sections. */
.capability-formats [data-depth="1"] > .accordion-header {
  padding: var(--space-l);
}

.capability-formats [data-depth="1"] .accordion-header {
  font-size: var(--text-size-l);
}

/* -- Capability formats (inner accordion of formats per capability) -- */

.capability-formats .accordion-item:last-child {
  border-bottom: none;
}

.capability-formats .accordion-item:first-child {
  border-top: var(--border-s) solid var(--border-faded);
}

.capability-formats .accordion {
  border-top: none;
}

/* -- Services page — portfolio carousel -- */

/* Each slide is a <figure>: 4:5 media frame + a caption naming the project.
   Splide (fixedWidth) owns the slide width; the frame fills it via [data-ratio]. */
.services-slide {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
  margin: 0;
}

/* Caption keeps .label's small size/weight but drops the uppercase treatment
   (and its uppercase-tuned tracking) — project names read as plain sentence case. */
.services-slide-caption {
  text-transform: none;
  font-style: normal;
  letter-spacing: normal;
}

/* -- How we work (process steps) -- */

.process-steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-xl);
}

@container main-content (max-width: 768px) {
  .process-steps {
    grid-template-columns: 1fr;
  }
}

.process-step-title {
  margin: 0 0 var(--space-s);
font-size: var(--text-size-xl);

}

.process-step-desc {
  line-height: var(--line-height-xl);
  color: var(--text-secondary);
  margin: 0;
}



/* 12-col grid — opt-in via presence of data-grid (works on any element).
   container-type establishes a containment context so --col-unit can
   resolve against the grid's own width (100cqi), letting children
   compose padding/margin equal to one grid column. */
[data-grid] {
  display: grid !important;
  container-type: inline-size;
  grid-template-columns: repeat(12, 1fr);
  column-gap: var(--erlen-gap);
  row-gap: var(--space-xl);
  --col-unit: calc((100cqi - 11 * var(--erlen-gap)) / 12);
}

[data-grid] > * {
  grid-column: 1 / -1;
}

[data-grid] > [data-col-span="1"]  { grid-column-end: span 1; }
[data-grid] > [data-col-span="2"]  { grid-column-end: span 2; }
[data-grid] > [data-col-span="3"]  { grid-column-end: span 3; }
[data-grid] > [data-col-span="4"]  { grid-column-end: span 4; }
[data-grid] > [data-col-span="5"]  { grid-column-end: span 5; }
[data-grid] > [data-col-span="6"]  { grid-column-end: span 6; }
[data-grid] > [data-col-span="7"]  { grid-column-end: span 7; }
[data-grid] > [data-col-span="8"]  { grid-column-end: span 8; }
[data-grid] > [data-col-span="9"]  { grid-column-end: span 9; }
[data-grid] > [data-col-span="10"] { grid-column-end: span 10; }
[data-grid] > [data-col-span="11"] { grid-column-end: span 11; }
[data-grid] > [data-col-span="12"] { grid-column-end: span 12; }

[data-grid] > [data-col-start="1"]  { grid-column-start: 1; }
[data-grid] > [data-col-start="2"]  { grid-column-start: 2; }
[data-grid] > [data-col-start="3"]  { grid-column-start: 3; }
[data-grid] > [data-col-start="4"]  { grid-column-start: 4; }
[data-grid] > [data-col-start="5"]  { grid-column-start: 5; }
[data-grid] > [data-col-start="6"]  { grid-column-start: 6; }
[data-grid] > [data-col-start="7"]  { grid-column-start: 7; }
[data-grid] > [data-col-start="8"]  { grid-column-start: 8; }
[data-grid] > [data-col-start="9"]  { grid-column-start: 9; }
[data-grid] > [data-col-start="10"] { grid-column-start: 10; }
[data-grid] > [data-col-start="11"] { grid-column-start: 11; }
[data-grid] > [data-col-start="12"] { grid-column-start: 12; }

/* Named layout patterns — :nth-child column placement on a data-grid.
   Apply data-layout="split|feature|feature-alt|headline|thirds|masthead|spotlight"
   to the [data-grid] parent. Children stay content-agnostic; the parent
   owns column positions.

   Gated on @container main-content (min-width:960px) — the SAME measurement
   source as the collapse rule below — so the named placements never fight
   the mobile collapse. They have higher specificity than the base
   [data-grid] > * rule, so without this gate they'd hold position even at
   1-column grids. Measuring the content area (not the viewport) is what
   makes them flip correctly when the sidebar narrows the content. */
@container main-content (min-width: 960px) {
  /* split: 6 · 6 — two equal halves */
  [data-grid][data-layout="split"]       > :nth-child(1) { grid-column: 1 / span 6; }
  [data-grid][data-layout="split"]       > :nth-child(2) { grid-column: 7 / span 6; }

  /* feature: 4 · 8 — minor left, major right */
  [data-grid][data-layout="feature"]     > :nth-child(1) { grid-column: 1 / span 4; }
  [data-grid][data-layout="feature"]     > :nth-child(2) { grid-column: 5 / span 8; }

  /* feature-alt / headline: 8 · 4 — major left, minor right.
     headline is a semantic alias used on page/section headers with a
     compact action area. */
  [data-grid][data-layout="feature-alt"] > :nth-child(1),
  [data-grid][data-layout="headline"]    > :nth-child(1) { grid-column: 1 / span 8; }
  [data-grid][data-layout="feature-alt"] > :nth-child(2),
  [data-grid][data-layout="headline"]    > :nth-child(2) { grid-column: 9 / span 4; }

  /* thirds: 4 · 4 · 4 — three equal columns */
  [data-grid][data-layout="thirds"]      > :nth-child(1) { grid-column: 1 / span 4; }
  [data-grid][data-layout="thirds"]      > :nth-child(2) { grid-column: 5 / span 4; }
  [data-grid][data-layout="thirds"]      > :nth-child(3) { grid-column: 9 / span 4; }

  /* masthead / spotlight: 3 · 6 · 3 — center-dominant with symmetric
     flankers. spotlight is the alias for product-feature layouts. */
  [data-grid][data-layout="masthead"]    > :nth-child(1),
  [data-grid][data-layout="spotlight"]   > :nth-child(1) { grid-column: 1 / span 3; }
  [data-grid][data-layout="masthead"]    > :nth-child(2),
  [data-grid][data-layout="spotlight"]   > :nth-child(2) { grid-column: 4 / span 6; }
  [data-grid][data-layout="masthead"]    > :nth-child(3),
  [data-grid][data-layout="spotlight"]   > :nth-child(3) { grid-column: 10 / span 3; }
}

/* Mobile collapse: below 960 every [data-grid] becomes a single column.
   data-col-span / data-col-start / named layouts all stack as source
   order. --col-unit resets to 0 so any padding/margin derived from
   it goes away on mobile.

   max-width is 959.98 (not 960) so this never matches at the SAME content
   width as the named-layout min-width:960 rule above. At exactly 960px of
   content (e.g. a 1360px viewport with the 400px sidebar open) both rules
   would otherwise fire — collapsing the grid to 1fr while still positioning
   children at grid-column spans — and the layout breaks. The sub-pixel gap
   hands 960px cleanly to the named layouts.

   data-stack-order optionally promotes children to a position earlier
   than their source order — visual-only via CSS `order`. RULE: only
   apply to children that contain NO interactive elements (links,
   buttons, inputs, details, [tabindex], or a heading starting a new
   section). DOM and tab order are unchanged. See README.md. */
@container main-content (max-width: 959.98px) {
  [data-grid] {
    grid-template-columns: 1fr;
    --col-unit: 0px;
  }
  [data-grid] > [data-stack-order="1"] { order: -3; }
  [data-grid] > [data-stack-order="2"] { order: -2; }
  [data-grid] > [data-stack-order="3"] { order: -1; }
}


/* ------ REDUCED MOTION ------ */

@media (prefers-reduced-motion: reduce) {
  .layout,
  .sidebar,
  .sidebar-intro,
  .sidebar-footer p,
  .sidebar-backdrop,
  .nav-link,
  .nav-link::before,
  .nav-link::after,
  [data-sidebar-collapse],
  .post,
  .close-btn,
  .contact-chips .button,
  .skip-link,
  .error-page__code,
  .sidebar-slot-link,
  .sidebar-slot-link::before {
    transition: none;
    animation: none;
  }
}


/* ------ STYLEGUIDE ------ */

/* -- Styleguide table — combo class on .table .table-full -- */

.table-styleguide th,
.table-styleguide td {
  padding-top: var(--space-l);
  padding-bottom: var(--space-l);
  padding-left: 0;
}

.table-styleguide th {
  padding-top: var(--space-s);
  padding-bottom: var(--space-s);
}

.table-styleguide th:first-child,
.table-styleguide td:first-child {
  width: 180px;
  padding-right: var(--space-l);
  vertical-align: top;
}

.table-styleguide td:first-child code {
  font-size: var(--label-size);
  color: var(--text-secondary);
}

.table-styleguide td:last-child > *:first-child {
  margin-top: 0;
}

.table-styleguide td:last-child > *:last-child {
  margin-bottom: 0;
}

/* -- Token copy — inline copy button for code chips in styleguide tables -- */

.token-copy {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  position: relative;
}

.token-copy code {
  transition: color var(--duration-2xs) var(--ease-out), background var(--duration-2xs) var(--ease-out);
}

.token-copy:hover code {
  color: var(--text-primary);
  background: color-mix(in srgb, var(--text-primary), var(--alpha-15));
}

.token-copy.is-copied code {
  color: var(--status-success);
}

.token-copy::after {
  content: "";
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  flex-shrink: 0;
  background-color: currentColor;
  mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 14C8 15.1046 8.89543 16 10 16H18C19.1046 16 20 15.1046 20 14V6C20 4.89543 19.1046 4 18 4H10C8.89543 4 8 4.89543 8 6V14ZM6 18V2H22V18H6ZM2 22V6H4V20H18V22H2Z'/%3E%3C/svg%3E");
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 14C8 15.1046 8.89543 16 10 16H18C19.1046 16 20 15.1046 20 14V6C20 4.89543 19.1046 4 18 4H10C8.89543 4 8 4.89543 8 6V14ZM6 18V2H22V18H6ZM2 22V6H4V20H18V22H2Z'/%3E%3C/svg%3E");
  mask-size: contain;
  -webkit-mask-size: contain;
  mask-repeat: no-repeat;
  -webkit-mask-repeat: no-repeat;
  opacity: 0.4;
  transition: opacity var(--duration-2xs) var(--ease-out);
  pointer-events: none;
}

.token-copy:hover::after {
  opacity: 0.8;
}

.token-copy.is-copied::after {
  opacity: 1;
  color: var(--status-success);
  mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.54998 18L3.84998 12.3L5.27498 10.875L8.13576 13.7358C8.91681 14.5168 10.1831 14.5168 10.9642 13.7358L18.725 5.97501L20.15 7.40001L9.54998 18Z'/%3E%3C/svg%3E");
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9.54998 18L3.84998 12.3L5.27498 10.875L8.13576 13.7358C8.91681 14.5168 10.1831 14.5168 10.9642 13.7358L18.725 5.97501L20.15 7.40001L9.54998 18Z'/%3E%3C/svg%3E");
}

/* -- Icon grid — full catalog of sprite icons; click a tile to copy its name -- */

.sg-icon-grid {
  display: grid;
  /* auto-fill keeps it responsive; the max() floor caps the row at 6 columns
     on wide containers, dropping to fewer (min 116px tiles) when narrow */
  grid-template-columns: repeat(auto-fill, minmax(max(116px, (100% - 5 * var(--space-s)) / 6), 1fr));
  gap: var(--space-s);
  margin-top: var(--space-l);
}

.sg-icon-tile {
  flex-direction: column;
  justify-content: center;
  gap: var(--space-s);
  width: 100%;
  padding: var(--space-l) var(--space-s);
  border: var(--border-s) solid var(--border-faded);
  border-radius: var(--radius-m);
  text-align: center;
  transition: border-color var(--duration-2xs) var(--ease-out),
    background var(--duration-2xs) var(--ease-out),
    color var(--duration-2xs) var(--ease-out);
}

.sg-icon-tile .svg-icn {
  width: 28px;
  height: 28px;
  color: var(--text-primary);
}

.sg-icon-name {
  font-family: var(--font-quaternary);
  font-size: var(--font-2xs);
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

/* The whole tile is the copy affordance — suppress the inline glyph from .token-copy */
.sg-icon-tile::after {
  content: none;
}

.sg-icon-tile:hover {
  border-color: var(--border-secondary);
  background: var(--background-faded);
}

.sg-icon-tile.is-copied {
  border-color: var(--status-success);
}

.sg-icon-tile.is-copied .svg-icn,
.sg-icon-tile.is-copied .sg-icon-name {
  color: var(--status-success);
}

/* -- Color row — swatch with hex/CSS copy buttons -- */

.color-row {
  display: flex;
  align-items: center;
  padding: var(--space-xl);
  font-size: var(--text-size-l);
  font-weight: var(--font-weight-medium);
  cursor: default;
  position: relative;
  height: 80px;
}

.color-row-name {
  pointer-events: none;
}

.color-row.is-text-dark .color-row-name,
.color-row.is-text-dark .color-copy-btn {
  color: var(--black);
}

.color-row.is-text-light .color-row-name,
.color-row.is-text-light .color-copy-btn {
  color: var(--white);
}

.color-row-actions {
  display: none;
  gap: var(--space-s);
  margin-left: auto;
}

.color-row:hover .color-row-actions,
.color-row:focus-within .color-row-actions {
  display: flex;
}

.color-copy-btn {
  color: inherit;
  background: none;
  border: none;
  font-size: var(--text-size-s);
  font-weight: var(--font-weight-semi-bold);
  cursor: pointer;
  padding: var(--space-2xs) var(--space-s);
}

/* State swap: default visible, copied hidden — toggles on .is-copied */
.color-copy-btn .copy-btn-default { display: inline-flex; align-items: center; gap: var(--space-s); }
.color-copy-btn .copy-btn-copied  { display: none; }
.color-copy-btn.is-copied .copy-btn-default { display: none; }
.color-copy-btn.is-copied .copy-btn-copied  { display: inline-flex; align-items: center; gap: var(--space-s); }

.color-copy-btn:hover {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 2.5px;
}

.sg-eyebrow {
  display: block;
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-tracking);
  text-transform: uppercase;
  color: var(--text-faded);
  margin-bottom: var(--space-xs);
}

.sg-section {
  margin-top: var(--space-3xl);
}

.sg-section:first-child {
  margin-top: 0;
}

.sg-note {
  color: var(--text-secondary);
  margin-top: 0;
  margin-bottom: var(--space-l);
}

.sg-toc {
  margin-bottom: var(--space-2xl);
  padding: var(--space-l);
  background: var(--bg-faded-3);
  border-radius: var(--radius-m);
}

.sg-toc ul {
  list-style: none;
  padding: 0;
  margin: var(--space-s) 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-l);
}

.sg-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-m);
  align-items: center;
}

.sg-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: var(--space-l);
}

.sg-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-s);
}

.sg-swatch {
  width: 100%;
  height: 48px;
  border-radius: var(--radius-s);
  border: var(--border-s) solid var(--border-faded);
}

.sg-spacer {
  height: 24px;
  background: var(--text-faded);
}

.sg-token-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: var(--space-2xs);
  font-size: var(--text-size-s);
}

.sg-token-row code {
  color: var(--text-secondary);
}

.sg-tooltip-demo {
  border-bottom: var(--border-s) dashed var(--text-faded);
  cursor: help;
}

.sg-radius-demo {
  width: 80px;
  height: 80px;
  background: var(--background-faded);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-size-s);
}

.sg-thumb-placeholder {
  background: var(--background-faded);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-faded);
  font-family: var(--font-tertiary);
}

/* Styleguide-only — the placeholder has no intrinsic height on its own, so
   give the cursor solo demo thumbs a hoverable box to test the solo cursor. */
.sg-cursor-thumb {
  height: 140px;
  border-radius: var(--radius-m);
}

/* Constrain demo-only components (e.g. post cards in styleguide) */
.sg-demo {
  max-width: 320px;
}

@container main-content (max-width: 768px) {
  .table-styleguide th:first-child,
  .table-styleguide td:first-child {
    width: 120px;
  }
}

/* ------ SPLIT GRID ------ */

.split-grid {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: var(--space-xl);
  align-items: start;
}

@container main-content (max-width: 768px) {
  .split-grid {
    grid-template-columns: 1fr;
    gap: var(--space-l);
  }
}

/* ------ LOGO TICKER ------ */

.logo-ticker-section {
  overflow: hidden;
  margin-top: var(--space-2xl);
}

/* Slides are injected by erlen-logos.js only after the Splide vendor script
   loads, so the slider is empty at first paint — reserve the tallest slide's
   height to prevent the jump. Values = the equal-area math in erlen-logos.js
   for the tallest logo (lift-labs): sqrt(TARGET_AREA / (134/92)) × 1.3 rem.
   Update alongside LOGOS / TARGET_AREA(_MOBILE); breakpoint mirrors the
   window.innerWidth <= 600 check in buildLogoSlides. */
.logo-slider {
  min-height: 4.04rem;
}
@media (max-width: 600px) {
  .logo-slider {
    min-height: 3.24rem;
  }
}

.logo-ticker-slide {
  display: flex;
  align-items: center;
}

.logo-ticker-slide svg {
  display: block;
}

/* ------ TICKER (Products → Reporting section) ------ */
/* Auto-scrolling stats ticker. Slides hand-authored in products.html;
   Splide instance mounted by initReportTicker (products.js). */

/* Outer container — clips ticker overflow at the section edges. */

.reporting-headline {
  text-wrap: pretty;
}

.ticker-section {
  overflow: hidden;
  margin-top: var(--space-2xl);
}

/* Slide layout — left-aligned: icon at the top with a wide gap below it,
   then the value, then the label. */
.ticker-slide {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-s);
  background-color: var(--background-faded);
  border-radius: var(--radius-s);
  padding: var(--space-l) var(--space-xl);
}

.ticker-wrapper {
  margin: 0 calc(-1 * var(--erlen-gap));
}

/* Big stat — the slide's headline. */
.ticker-value {
  font-size: var(--font-4xl);
  /* font-family: var(--font-tertiary); */
  font-weight: var(--font-weight-medium);
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  white-space: nowrap;
  text-align: left;
  margin: 0;
}

/* Caption beneath the value. */
.ticker-label {
  font-size: var(--text-size-s);
  line-height: var(--line-height-m);
  color: var(--text-secondary);
  white-space: nowrap;
  text-align: left;
  margin: 0;
}

/* Larger feature icon, set off from the value by a wide bottom margin.
   .svg-icn base handles centering + aspect-ratio. */
.ticker-icon {
  --icon-size: 1.75rem;
  margin-bottom: var(--space-l);
  color: var(--text-faded);
}

/* Mobile scale-down — keeps slides legible at narrow widths. */
@container main-content (max-width: 768px) {
  .ticker-value {
    font-size: var(--font-2xl);
  }

  .ticker-icon {
    width: 1rem;
    height: 1rem;
  }
}

/* ------ HOME: CASE STUDIES ------ */

.case-study-cards {
  display: flex;
  flex-direction: column;
  gap: var(--space-12xl);
}

/* Card is a link; reset link styles so inner type rules win. */
.case-study-card {
  display: grid;
  text-decoration: none;
  color: var(--text-primary);
  overflow: hidden;
}

a.case-study-card:hover {
  color: var(--text-primary);
}

/* Info column: title at top, stat at bottom — stretches to match
   the right-side media-frame's height (set by 16:9 ratio). */
.case-study-card-info {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-l);
}

/* Heading group: eyebrow (client name) sits tight above the title.
   Wrapping these two in their own flex column means the parent
   .case-study-card-info can keep `justify-content: space-between` and
   still push the stat to the bottom of the column. */
.case-study-card-heading {
  display: flex;
  flex-direction: column;
  gap: var(--space-l);
}

.case-study-card-client {
  margin: 0;
  color: var(--text-faded);
}

.case-study-card-title {
  margin: 0;
}

/* Stat: <p> wraps the whole thing — its rules style the caption text
   directly. The value <span> is a block-level inline-flex callout
   (icon + number) above the caption. */
.case-study-card-stat {
  margin: 0;
  font-size: var(--text-size-s);
  color: var(--text-secondary);
}

.case-study-card-stat-value {
  display: flex;
  align-items: center;
  gap: var(--space-s);
  margin-bottom: var(--space-xs);
  font-family: var(--font-tertiary);
  font-weight: var(--font-weight-medium);
  font-size: var(--font-7xl);
  color: var(--text-primary);
}

.case-study-card-stat-value .svg-icn {
  width: 1em;
  height: 1em;
}

/* Media block is the positioning context for the logo overlay
   (and any future scroll-revealed overlay layered on top). */
.case-study-card-media {
  position: relative;
  overflow: hidden;
}

/* Brand logo overlay — fills .case-study-card-media. Decorative
   (aria-hidden in markup); revealed only while body.is-scrolling.
   Phase 1 ships text content; Phase 2 swaps for an <img>. */
.case-study-card-logo {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  color: var(--white);
  background-color: var(--black-alpha-40);
  transition: opacity var(--duration-m) var(--ease-out);

  /* Phase 1 text styling — sized to read as a stand-in logo.
     The scrim above guarantees contrast over any video poster. */
  font-size: var(--font-3xl);
  font-weight: var(--font-weight-medium);
  text-align: center;
}

/* Logo mark — wrapper sizing for the sprite SVG. The .case-study-card-logo
   parent is the full-size scrim; this child sizes the artwork itself so the
   logo reads as a centred mark, not a stretched fill.
   `aspect-ratio`, `--logo-ratio` and `--logo-scale` are set inline by
   erlen-logos.js (renderLogoSpriteMarkup) from the sprite viewBox.
   Equal-area sizing: width ∝ √ratio keeps every brand at the same visual
   area relative to the card, so wide wordmarks (Hypebeast) no longer
   shrink and compact marks (Adidas) no longer dominate. The first width
   is a fallback for browsers without CSS sqrt(); the max caps are loose
   guardrails, not the sizing mechanism.
   Per-brand overrides hook in via `[data-logo="<name>"]`. */
.case-study-card-logo-mark {
  --logo-area-scale: 14%;
  width: 21%;
  width: calc(sqrt(var(--logo-ratio, 1)) * var(--logo-scale, 1) * var(--logo-area-scale));
  max-width: 45%;
  max-height: 35%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Spanned cards are twice as wide, so the %-based equal-area sizing would
   double the mark — halve the multiplier to keep it at single-column scale.
   Below the span-collapse breakpoint the card is one column again, so the
   base multiplier returns. Only the custom property is overridden here: a
   `width` declaration in these rules would out-specific the base calc()
   and break the equal-area sizing. */
.post-item[data-span="2"] .case-study-card-logo-mark {
  --logo-area-scale: 7%;
}

@container main-content (max-width: 768px) {
  .post-item[data-span="2"] .case-study-card-logo-mark {
    --logo-area-scale: 14%;
  }
}

/* Legacy <img> path — kept for any future bitmap-logo case. */
.case-study-card-logo img {
  max-width: 30%;
  max-height: 40%;
  width: auto;
  height: auto;
}

body.is-scrolling .case-study-card-logo {
  opacity: 1;
}

/* Reduced motion: keep the overlay permanently hidden rather than flashing
   in/out on every scroll. It's purely decorative reinforcement, so hiding
   it entirely is preferable to an instant opacity flip for vestibular
   sensitivity. */
@media (prefers-reduced-motion: reduce) {
  .case-study-card-logo {
    transition: none;
  }
  body.is-scrolling .case-study-card-logo {
    opacity: 0;
  }
}

/* ------ TESTIMONIALS ------ */

.home-cta .button-group {
  justify-content: center;
}

.cta-headline {
  font-size: var(--headline-size);
  font-weight: var(--title-weight);
  line-height: var(--title-leading);
  letter-spacing: var(--title-tracking);
  text-wrap: balance;
}

.cta-paragraph {
  text-wrap: balance;
}



.splide__arrow--prev svg {
    transform: scaleX(1) !important;
}

.testimonial-pause {
  display: none !important;
}

.testimonial-wrapper {
  overflow: hidden;
}

.splide__arrows .button {
  border-radius: var(--radius-pill);
}

/* Splide's vendor CSS sets overflow: hidden on the track and list, which
   clips anything positioned above the card edge (e.g. a half-overlapping
   avatar). Override here so the mark rebuild is free to overflow. */
.testimonial-slider .splide__track,
.testimonial-slider .splide__list {
  overflow: visible;
}

.testimonial-slide {
  display: flex;
  align-items: flex-start;
  overflow: visible;
  gap: var(--space-xl);
}

.testimonial-card {
  margin-top: 25px;
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-l);
  padding: var(--space-2xl);
  background-color: var(--background-faded);
  /* Top-left stays sharp (anchors to the avatar). Other three corners
     round to soften the card. */
  border-radius: 0 var(--radius-l) var(--radius-l) var(--radius-l);
}

.testimonial-quote {
  font-size: var(--text-size-l);
  line-height: 1.3;
  color: var(--text-primary);
  margin: 0;
}

.testimonial-mark {
    flex: 0 0 auto;
    color: var(--background-primary);
    background: var(--text-primary);
    border-radius: 50%;
    overflow: hidden;
    height: 60px;
    width: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 10px;
}

.testimonial-mark .svg-logo {
  max-width: 45px;
  max-height: 45px;
}

.js .testimonial-quote {
  opacity: 0;
}

.testimonial-attribution {
  color: var(--text-faded);
}

/* Centered cluster: progress + pause + arrows. Below the slider track. */
.testimonial-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-l);
  margin-top: var(--space-2xl);
}

.testimonial-progress {
  width: 8rem;
  height: 1px;
  background-color: var(--divider-color);
  overflow: hidden;
}

.testimonial-progress .splide__progress__bar {
  background-color: var(--text-primary);
  height: 100%;
  width: 0%;
}

.testimonial-controls .splide__arrows {
  display: flex;
  gap: var(--space-s);
}

/* Override Splide's default absolute arrow positioning — we render them
   inside the centered controls cluster, not at the track edges. */
.testimonial-controls .splide__arrow--prev,
.testimonial-controls .splide__arrow--next,
.testimonial-controls .testimonial-pause {
  position: static;
  transform: none;
  opacity: 1;
}

/* Pause/play icon swap — only one icon visible at a time, driven by
   the `.is-paused` class on the button. */
.testimonial-pause .testimonial-pause-icon-play {
  display: none;
}

.testimonial-pause.is-paused .testimonial-pause-icon-pause {
  display: none;
}

.testimonial-pause.is-paused .testimonial-pause-icon-play {
  display: flex;
}

@container main-content (max-width: 768px) {
  .testimonial-card {
    /* Slide width is now controlled by Splide's fixedWidth breakpoints;
       the card stays at its desktop percentage of slide width. */
    padding: var(--space-xl);
  }


  .testimonial-progress {
    width: 5rem;
  }
}

/* ------ GSAP FOUC PREVENTION ------ */
/* Hide animated elements until JS reveals them.
   Scoped to .js so content remains visible if JavaScript is disabled. */

.js [data-bd-animate],
.js [data-text-animate] {
  opacity: 0;
}

/* ------ PRINT SAFETY ------ */
/* Ensure GSAP-animated elements are visible when printing */

@media print {
  [data-bd-animate],
  [data-text-animate],
  [data-bd-leave] {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
  }
}

/* ------ ASPECT RATIO — global utility attribute ------ */
/* Apply data-ratio="W:H" to any element to set its aspect ratio.
   Component defaults set their own aspect-ratio; data-ratio overrides it.
   Lives at the end of the file so source-order beats component defaults
   at the same (0,1,0) specificity. */

[data-ratio="1:1"]  { aspect-ratio: 1 / 1; }
[data-ratio="2:1"]  { aspect-ratio: 2 / 1; }
[data-ratio="3:1"]  { aspect-ratio: 3 / 1; }
[data-ratio="4:3"]  { aspect-ratio: 4 / 3; }
[data-ratio="3:2"]  { aspect-ratio: 3 / 2; }
[data-ratio="3:4"]  { aspect-ratio: 3 / 4; }
[data-ratio="16:9"] { aspect-ratio: 16 / 9; }
[data-ratio="21:9"] { aspect-ratio: 21 / 9; }
[data-ratio="4:5"]  { aspect-ratio: 4 / 5; }
[data-ratio="5:4"]  { aspect-ratio: 5 / 4; }
[data-ratio="9:16"] { aspect-ratio: 9 / 16; }
/* Full-bleed modern phone screen (iPhone X+ / most Android). For screenshots that
   have to be shown whole — 9:16 would crop the status bar and home indicator. */
[data-ratio="9:19.5"] { aspect-ratio: 9 / 19.5; }


/* ------ LINE LENGTH — global utility attribute ------ */
/* Apply data-line-length="<value>" to any text element to constrain its
   measure (max-width) to a known-good readability target. Values are named
   typographically — headline (display titles) → wide (max comfortable prose).
   Bringhurst: 45–75ch is the satisfactory range for body text; 66ch ideal. */

[data-line-length="headline"] { max-width: var(--line-length-headline); }
[data-line-length="small"]    { max-width: var(--line-length-small); }
[data-line-length="xsmall"]    { max-width: var(--line-length-xsmall); }
[data-line-length="body"]     { max-width: var(--line-length-body); }
[data-line-length="medium"]   { max-width: var(--line-length-medium); }
[data-line-length="wide"]     { max-width: var(--line-length-wide); }


/* ============================================================
   HOME — PRODUCT SPOTLIGHT
   Static product list. Each <li> is a whole-row link laid out by the
   [data-grid] data-layout="spotlight" pattern (media · info · CTA),
   collapsing to a single column at ≤959.98px. Entrance reveal is the
   shared bd-animations scrub-group (data-bd-scrub-group on the <ol>,
   data-bd-animate="slide-up" on each <li>); videos autoplay + loop.
   ============================================================ */

.product-spotlight .product-spotlight-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Row metrics live on the <li>; the [data-grid] sits one level inward on
   .product-spotlight-link so the whole row is one tappable target. The
   min-height gives the rows a consistent vertical rhythm. */
.product-spotlight .product-spotlight-item {
  min-height: 20rem;
  border-bottom: var(--border-s) solid var(--border-faded);
  margin: 0;
}


.product-spotlight .product-spotlight-item:last-child {
  border: none;
}

/* Whole-row link wrapper. Anchor reset + grid host. Three children opt
   into the [data-grid] 12-col layout via data-col-span / data-col-start
   in the markup: media spans cols 1–3, info spans cols 5–9 (col 4 is
   an explicit gutter between media and info), CTA spans cols 10–12. */
.product-spotlight .product-spotlight-link {
  display: grid;
  align-items: center;
  color: inherit;
  text-decoration: none;
  padding: var(--space-3xl) 0;

}

.product-spotlight .product-spotlight-media {
  aspect-ratio: 5 / 3;
  width: 100%;
  /* Cursor parallax (desktop only) — initProductParallax in erlen-home.js
     writes per-tile --cursor-x/y while the pointer is over the row; the lerp
     in JS is the smoothing, so no CSS transition here. Fallback 0px keeps the
     transform an identity no-op on touch / reduced-motion (JS never sets the
     vars there). */
  transform: translate(var(--cursor-x, 0px), var(--cursor-y, 0px));
}

.product-spotlight .product-spotlight-media img,
.product-spotlight .product-spotlight-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* border-radius: var(--radius-s); */
  display: block;
}

/* .product-spotlight-info is the middle grid cell (cols 4–9 via the
   data-layout="spotlight" named pattern). padding-inline-start inset
   by one --col-unit gives the content the visual cols-5–9 width while
   the cell itself remains 6 cols wide. on mobile (≤959) --col-unit
   resets to 0px so the inset disappears with the collapse. */
.product-spotlight .product-spotlight-info {
  display: flex;
  flex-direction: column;
  gap: var(--space-l);
  padding-inline-start: var(--col-unit);
}

/* Text colors. */
.product-spotlight .product-spotlight-title,
.product-spotlight .product-spotlight-subline,
.product-spotlight .product-spotlight-cta {
  color: var(--text-primary);
}

.product-spotlight .product-spotlight-description {
  color: var(--text-faded);
}

/* Per-element typography (color set in the grouped rule above). */
.product-spotlight .product-spotlight-title {
  margin: 0;
  font-size: var(--title-size);
  line-height: 1;
  letter-spacing: -0.02em;
}

.product-spotlight .product-spotlight-subline {
  margin: 0;
}

.product-spotlight .product-spotlight-description {
  margin: 0;
  text-wrap: balance;
}

/* Subtle trending icon + label CTA, stacked icon-above-label and
   horizontally centered. column-reverse keeps the icon visually first
   without reordering the markup (label first reads naturally and the
   icon is aria-hidden). */
.product-spotlight .product-spotlight-cta {
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: var(--space-s);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

.product-spotlight .product-spotlight-cta .svg-icn {
  width: 1.5rem;
  height: 1.5rem;
}

/* Stacked — at the grid-collapse breakpoint the CTA reads as a full-width
   faded button: icon + label on one line, subtle fill. Mirrors the tokens
   of .button[data-variant="faded"]; --text-alpha-10 is built on
   --text-primary, so the fill adapts to dark mode with no per-theme rule. */
@container main-content (max-width: 959.98px) {
  .product-spotlight .product-spotlight-cta {
    flex-direction: row;
    justify-content: center;
    gap: var(--space-s);
    padding: var(--space-l) var(--space-xl);
    background-color: var(--text-alpha-10);
    border-radius: var(--radius-s);
  }

  .product-spotlight .product-spotlight-cta .svg-icn {
    width: 1.25rem;
    height: 1.25rem;
  }
}

/* Mobile — [data-grid] auto-collapses to a single column at ≤959px, so
   media / info / CTA stack naturally as a vertical flow. */
@container main-content (max-width: 768px) {
  .product-spotlight .product-spotlight-item {
    row-gap: var(--space-m);
    min-height: auto;
  }

  .product-spotlight .product-spotlight-media {
    aspect-ratio: 16 / 9;
  }
}

/* ============================================================
   EXPERIMENTAL — product spotlight hover effects (desktop only)
   Self-contained. Delete this whole block to revert to a plain
   static list (media always visible, no sibling dim). Pointer/
   hover-gated so touch is unaffected. Both effects target
   .product-spotlight-link / -media, never the <li> (which carries
   data-bd-animate — bd-animations sets its opacity inline and would
   win over a class rule).
   ============================================================ */
@media (hover: hover) and (pointer: fine) {
  .product-spotlight .product-spotlight-link {
    transition: opacity var(--duration-l) var(--ease-out);
  }

  /* Hovering one row dims its siblings so the row under the cursor stands out. */
  .product-spotlight .product-spotlight-list:hover .product-spotlight-item:not(:hover) .product-spotlight-link {
    opacity: 0.4;
  }

  /* Media hidden until its row is hovered, then fades to full. */
  .product-spotlight .product-spotlight-media {
    opacity: 0.4;
    transition: opacity var(--duration-s) var(--ease-out);
  }

  .product-spotlight .product-spotlight-link:hover .product-spotlight-media {
    opacity: 1;
  }
}


/* ------ BOOK A CALL ------ */

/* Full-viewport Google Appointment Schedule iframe.
   .page-wrapper already adds padding-top: var(--erlen-bar-height) on L1 pages,
   so the iframe height is the remaining viewport space below the fixed page header. */
.booking-iframe {
  display: block;
  width: 100%;
  height: calc(100dvh - var(--erlen-bar-height));
  border: 0;
}

@container main-content (max-width: 768px) {
  .booking-iframe {
    height: calc(100dvh - var(--mobile-bar-height) - var(--erlen-bar-height));
  }
}


/* ------ INTERACTIVE COVER ------ */

/* An article hero that runs a piece of interactive work instead of an image.
   The generator renders the poster (the article's `hero:` image, still the LCP
   element) plus an empty mount; interactive-cover.js lazy-loads the named cover
   and fades the poster out once it reports a live mount. Every failure path
   leaves the poster showing, so these rules must look right with no JS at all.

   Ratio is the format's own: 16:9 in the content column, 4:5 once it narrows —
   the compound selector outranks the global [data-ratio] table below, so a cover
   frame carries no data-ratio at all. */

.media-frame.interactive-cover {
  position: relative;
  aspect-ratio: 16 / 9;
}

@container main-content (max-width: 768px) {
  .media-frame.interactive-cover {
    aspect-ratio: 4 / 5;
  }
}

/* Both layers fill the frame and are explicitly stacked, so the markup order of
   mount and poster can never decide which is on top. The POSTER is the top
   layer: it must be what shows before the piece mounts, on every failure path,
   and with JS off (the mount underneath paints its opaque piece background from
   first paint). Fading it out is then a true crossfade revealing the live field
   below. pointer-events: none is load-bearing — the poster stays stacked above
   the mount even at opacity 0, and the field's pointermove lands through it. */
.interactive-cover-poster,
.interactive-cover-mount {
  position: absolute;
  inset: 0;
}

.interactive-cover-poster {
  z-index: 2;
  pointer-events: none;
  transition: opacity var(--duration-s) var(--ease-out);
}

/* The poster is a wrapper div (renderPicture emits either <picture> or a bare
   <img>, so the wrapper is the one stable fade layer); its media fills it. The
   .media-frame > img rules above don't reach in here — the img is two levels down. */
.interactive-cover-poster > img,
.interactive-cover-poster > picture,
.interactive-cover-poster > picture > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Opacity ONLY — never display/visibility. The faded poster stays in the
   accessibility tree, and its img alt (cover-alt) is the piece's one accessible
   description: the live field is aria-hidden, so switching this to display:none
   or visibility:hidden would silently delete the cover for screen readers. */
.interactive-cover.is-live .interactive-cover-poster {
  opacity: 0;
}

.interactive-cover-mount {
  z-index: 1;
  overflow: hidden;
  /* The finger drags the magnet, but the reader must always be able to scroll
     past the cover: pan-y hands every vertical gesture to the browser and leaves
     the rest to us. The piece drops its held-touch state on the resulting
     pointercancel. Never `none` here — that traps the page. */
  touch-action: pan-y;
}

/* The site's own cursor overlay steps aside inside the frame (data-cursor-native
   on the mount), and the OS pointer is hidden so the cover's artwork is the only
   cursor. Gated on .is-live: until the piece has actually mounted (still loading,
   failed, JS off) there is no replacement artwork, and hiding the pointer over
   the poster would strand anyone who tracks it — the OS arrow stays until the
   Claude mark exists. Must OUTRANK `.bd-cursor-ready [data-cursor-native]` in
   bd-cursor.css (cursor:auto, specificity 0,2,0 — and that file loads AFTER this
   one, so equal specificity would lose on source order): these sit at 0,4,0.
   Hover-gated so touch devices, where cursor is moot, are untouched. */
@media (hover: hover) {
  .interactive-cover.is-live .interactive-cover-mount[data-cover],
  .interactive-cover.is-live .interactive-cover-mount[data-cover] * {
    cursor: none;
  }
}

/* The field fills the mount; tools and the cursor art are absolutely positioned
   inside it and moved only by JS transforms (translate + rotate), so they need no
   layout flow. */
.cover-field {
  position: absolute;
  inset: 0;
}

.cover-tool,
.cover-cursor {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  will-change: transform;
}

.cover-cursor {
  z-index: 10;
}

.cover-tool svg,
.cover-cursor svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* -- Cover: cursor magnet -- */

/* A bespoke piece palette, deliberately NOT themed: the hand-drawn line was
   designed on this warm yellow, and the cover reads as a framed object on the
   page rather than part of the page's own surface. Held across light and dark. */
.interactive-cover-mount[data-cover="cursor-magnet"] {
  --cover-bg: #ffea83;
  --cover-ink: #1a1a1a;
  background: var(--cover-bg);
}

/* Tools render in the fixed piece ink through currentColor, so the strokes read
   on the yellow whatever the page theme. The Claude mark keeps its own colours. */
.interactive-cover-mount[data-cover="cursor-magnet"] .cover-tool {
  color: var(--cover-ink);
}

/* Forced colors (Windows High Contrast): keep the artwork's own palette. The
   backplate would otherwise force to Canvas while the Claude mark's fixed fills
   persist — losing the yellow ground and, on a black canvas, the mark's dark
   parts. Standard practice for self-contained artwork; the piece is aria-hidden
   decoration whose meaning is carried by the poster's alt text. */
@media (forced-colors: active) {
  .interactive-cover-mount[data-cover="cursor-magnet"] {
    forced-color-adjust: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The piece already holds the field at home and only rotates; drop the poster
     crossfade too so nothing animates on arrival. */
  .interactive-cover-poster {
    transition: none;
  }

  .cover-tool,
  .cover-cursor {
    will-change: auto;
  }
}
