/* GENERATED FILE — DO NOT EDIT.
   Built by _dev/build-css.sh from ds/tokens/*.css + css/site.css.
   Edit those and re-run the script. Anything written here is lost. */

/* ---- ds/tokens/fonts.css ---- */
/* TT Hoves Pro — brand typeface. Only Regular (400) and Medium (500) were supplied.
   Heavier weights (DemiBold/Bold) are NOT available: set display type in Medium at large
   sizes rather than faux-bolding.

   SECOND LOCAL CHANGE TO THIS FILE (see README "Re-syncing the design system"):
   the src urls point at WOFF2 subsets rather than the raw .ttf desktop cuts. The
   .ttf files were 240 KB each and carried 938 characters — Cyrillic, Greek, small
   caps — for a page that uses 78. The subsets are ~17 KB each and are built from
   the same licensed files by _dev/build-fonts.sh. Vertical metrics and advance
   widths are byte-identical to the originals, so nothing reflows.

   WOFF2 has been supported by every shipping browser since 2016. There is no
   .ttf fallback on purpose: adding one costs a wasted request on the handful of
   browsers that would need it and buys nothing on the ones that do not. */
@font-face{font-family:"TT Hoves Pro";src:url("../assets/fonts/tt-hoves-pro-regular-latin.woff2") format("woff2");font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:"TT Hoves Pro";src:url("../assets/fonts/tt-hoves-pro-medium-latin.woff2") format("woff2");font-weight:500;font-style:normal;font-display:swap}

/* ---- ds/tokens/colors.css ---- */
/* EBR colour system — drawn from the logo files: purple #6B00F6, spark yellow #FBDE37, ink #1C1C1C. */
:root{
  /* Brand */
  --ebr-purple:#6B00F6;
  --ebr-purple-press:#5400C4;
  --ebr-purple-soft:#8F44F9;
  --ebr-purple-wash:#F1E8FE;
  --ebr-yellow:#FBDE37;
  --ebr-yellow-press:#E4C50F;
  --ebr-yellow-wash:#FEF8DC;

  /* Ink / neutral ramp */
  --ink-1000:#000000;
  --ink-900:#1C1C1C;
  --ink-700:#3D3D3D;
  --ink-500:#6E6E6E;
  --ink-400:#949494;
  --ink-300:#C4C4C4;
  --ink-200:#E2E2E0;
  --ink-100:#EFEEEB;
  --paper:#F7F6F3;
  --white:#FFFFFF;

  /* Semantic surfaces */
  --surface-page:var(--paper);
  --surface-card:var(--white);
  --surface-inverse:var(--ink-900);
  --surface-brand:var(--ebr-purple);
  --surface-accent:var(--ebr-yellow);
  --surface-muted:var(--ink-100);

  /* Semantic text */
  --text-strong:var(--ink-900);
  --text-body:var(--ink-700);
  --text-muted:var(--ink-500);
  --text-on-dark:var(--white);
  --text-on-dark-muted:rgba(255,255,255,.62);
  --text-brand:var(--ebr-purple);
  --text-link:var(--ebr-purple);
  --text-link-hover:var(--ink-900);

  /* Lines */
  --line-hairline:var(--ink-200);
  --line-strong:var(--ink-900);
  --line-on-dark:rgba(255,255,255,.18);

  /* Status — derived, used sparingly in product UI */
  --status-positive:#0F8A5F;
  --status-warning:var(--ebr-yellow-press);
  --status-critical:#D02B20;
  --focus-ring:var(--ebr-purple);
}

/* ---- ds/tokens/typography.css ---- */
/* Two type sizes, no middle: display for statements, small for everything else. */
:root{
  --font-display:"TT Hoves Pro",-apple-system,"Helvetica Neue",Arial,sans-serif;
  --font-body:"TT Hoves Pro",-apple-system,"Helvetica Neue",Arial,sans-serif;
  --font-mono:ui-monospace,"SFMono-Regular",Menlo,"Courier New",monospace;

  --weight-regular:400;
  --weight-medium:500;

  /* Display — statements. Clamp for fluid pages. */
  --size-display-xl:clamp(56px,8.4vw,132px);
  --size-display-l:clamp(40px,5.4vw,84px);
  --size-display-m:clamp(30px,3.4vw,52px);
  --size-title:28px;
  /* Small — everything else */
  --size-body-l:19px;
  --size-body:16px;
  --size-small:14px;
  --size-label:11px;

  --leading-display:0.94;
  --leading-title:1.12;
  --leading-body:1.5;
  --leading-tight:1.25;

  --tracking-display:-0.035em;
  --tracking-title:-0.02em;
  --tracking-body:-0.005em;
  --tracking-label:0.16em;

  /* Semantic roles */
  --type-display:var(--weight-medium) var(--size-display-xl)/var(--leading-display) var(--font-display);
  --type-heading:var(--weight-medium) var(--size-display-m)/var(--leading-title) var(--font-display);
  --type-body:var(--weight-regular) var(--size-body)/var(--leading-body) var(--font-body);
  --type-lead:var(--weight-regular) var(--size-body-l)/1.45 var(--font-body);
  --type-small:var(--weight-regular) var(--size-small)/1.45 var(--font-body);
  --type-label:var(--weight-medium) var(--size-label)/1 var(--font-mono);
}

/* ---- ds/tokens/spacing.css ---- */
/* 4px base. Section rhythm is deliberately large — whitespace is the brand. */
:root{
  --space-1:4px;
  --space-2:8px;
  --space-3:12px;
  --space-4:16px;
  --space-5:24px;
  --space-6:32px;
  --space-7:48px;
  --space-8:64px;
  --space-9:96px;
  --space-10:140px;

  --gutter:24px;
  --page-margin:clamp(20px,5vw,72px);
  --section-y:clamp(72px,10vw,160px); /* @kind spacing */
  --max-width:1440px;
  --measure:62ch;
  --grid-columns:12; /* @kind other */
}

/* ---- ds/tokens/radius-borders.css ---- */
/* Geometry follows the logo: squared letterforms, tight radii. Pills only on buttons/tags. */
:root{
  --radius-none:0;
  --radius-xs:2px;
  --radius-sm:4px;
  --radius-md:8px;
  --radius-lg:14px;
  --radius-media:18px;
  --radius-pill:999px;

  --border-hairline:1px solid var(--line-hairline);
  --border-strong:1px solid var(--line-strong);
  --border-on-dark:1px solid var(--line-on-dark);
  --rule-width:1px;
}

/* ---- ds/tokens/elevation.css ---- */
/* Near-flat system. Depth comes from hairlines and colour fields, not shadow.
   Shadows exist only for things that float above the page (menus, slide-over panels). */
:root{
  --shadow-none:none;
  --shadow-raised:0 1px 2px rgba(28,28,28,.06);
  --shadow-overlay:0 24px 60px -20px rgba(28,28,28,.28);
  --shadow-panel:-24px 0 80px -30px rgba(28,28,28,.35);
  --scrim-dark:rgba(28,28,28,.55); /* @kind color */
  --blur-glass:blur(14px) saturate(1.1); /* @kind other */
  --glass-fill:rgba(247,246,243,.72); /* @kind color */
}

/* ---- ds/tokens/motion.css ---- */
/* Motion replaces decoration. Long, eased, no bounce, no spring. */
:root{
  --ease-out:cubic-bezier(.16,1,.3,1); /* @kind other */
  --ease-in-out:cubic-bezier(.65,0,.35,1); /* @kind other */
  --ease-linear:linear; /* @kind other */

  --dur-instant:120ms; /* @kind other */
  --dur-fast:220ms; /* @kind other */
  --dur-base:420ms; /* @kind other */
  --dur-slow:720ms; /* @kind other */
  --dur-reveal:1100ms; /* @kind other */

  --transition-hover:color var(--dur-fast) var(--ease-out),background-color var(--dur-fast) var(--ease-out),border-color var(--dur-fast) var(--ease-out),opacity var(--dur-fast) var(--ease-out); /* @kind other */
  --transition-transform:transform var(--dur-base) var(--ease-out); /* @kind other */
  --stagger-step:80ms; /* @kind other */
}

/* ---- ds/tokens/base.css ---- */
/* Minimal global base — consumers get sane defaults without a reset framework. */
*,*::before,*::after{box-sizing:border-box}
body{margin:0;background:var(--surface-page);color:var(--text-body);font:var(--type-body);letter-spacing:var(--tracking-body);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
h1,h2,h3,h4{margin:0;color:var(--text-strong);font-family:var(--font-display);font-weight:var(--weight-medium);letter-spacing:var(--tracking-display);line-height:var(--leading-display)}
h1{font-size:var(--size-display-xl)}
h2{font-size:var(--size-display-l)}
h3{font-size:var(--size-display-m);letter-spacing:var(--tracking-title);line-height:var(--leading-title)}
h4{font-size:var(--size-title);letter-spacing:var(--tracking-title);line-height:var(--leading-title)}
p{margin:0 0 1em;text-wrap:pretty}
a{color:var(--text-link);text-decoration:none;text-underline-offset:4px;transition:var(--transition-hover)}
a:hover{color:var(--text-link-hover);text-decoration:underline}
::selection{background:var(--ebr-yellow);color:var(--ink-900)}
:focus-visible{outline:2px solid var(--focus-ring);outline-offset:3px}
.ebr-label{font:var(--type-label);letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-muted)}
@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;transition-duration:.01ms!important}}

/* ---- css/site.css ---- */
/* ==========================================================================
   EBR Agency — homepage
   Built on the EBR design system tokens (../ds/styles.css).

   House rules this file obeys, from the design system readme:
   - Two type sizes, no middle: display, and small. Plus the 11px caps label.
   - Flat colour fields only. No gradients, no shadows on cards or buttons.
   - Depth comes from hairlines and colour fields.
   - One easing curve, --ease-out, for everything.
   - Hover states shift colour, never shadow. Press is scale(.975).
   - Purple does one thing per screen. Yellow only on dark or purple.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Page shell
   -------------------------------------------------------------------------- */

/* One deliberate token override.
   `tokens/typography.css` resolves --type-label through --font-mono, but the
   design system readme is explicit — "TT Hoves Pro … No second family", with the
   11px caps label "treated as a third, functional size" of that same family —
   and the homepage wireframe sets every label in TT Hoves too. Two sources
   against one, so the label runs in the brand face. Delete this block to get
   the mono labels back. */
:root { --type-label: var(--weight-medium) var(--size-label)/1 var(--font-body); }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* The sticky header is 72px and anchor targets would otherwise land underneath
   it. Nothing to do with any library — a pre-existing gap, kept because it is
   one line and removing it puts the bug back. */
:target { scroll-margin-top: 92px; }

/* §28 Darker body copy (26 Oct 2026). Paragraph text only; labels, numbers,
   headings and text on dark grounds keep their tokens. Was --text-muted
   #6E6E6E and --text-body #3D3D3D; headings stay #1C1C1C. */
:root { --text-copy: #4A4A4A; --text-copy-strong: #2B2B2B; }
/* §28c Body copy one pixel larger (26 Oct 2026): 16→17, 14→15, 19→20, on
   paragraph text only. Headings, labels, buttons and form fields keep theirs. */
:root {
  --type-copy: var(--weight-regular) calc(var(--size-body) + 1px)/var(--leading-body) var(--font-body);
  --type-copy-small: var(--weight-regular) calc(var(--size-small) + 1px)/1.45 var(--font-body);
  --type-copy-lead: var(--weight-regular) calc(var(--size-body-l) + 1px)/1.45 var(--font-body);
}

body {
  font: var(--type-copy);
  color: var(--text-copy-strong);
  background: var(--surface-page);
  overflow-x: hidden;
}

.e-shell {
  max-width: var(--max-width);
  margin: 0 auto;
  padding-inline: var(--page-margin);
}

/* Icons: monochrome, currentColor, never in a coloured circle except the
   accordion toggle and the work-card arrow. */
svg { display: block; flex-shrink: 0; }

.e-skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--ink-900);
  color: var(--white);
  padding: 12px 20px;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.e-skip:focus { left: 0; color: var(--white); text-decoration: none; }

/* Full-bleed ground, contained content. Each section owns its ground colour. */
.e-section { position: relative; padding-block: var(--section-y); }
.e-section--tight { padding-block: clamp(48px, 6vw, 88px); }

.e-ground-paper { background: var(--paper); }
.e-ground-white { background: var(--white); }
.e-ground-ink   { background: var(--ink-900); color: var(--text-on-dark); }
/* ⚠ THE MUTED TEXT COLOUR IS REDEFINED ON THIS GROUND, and it has to be.

   `--text-on-dark-muted` is 62% white. Over `--ink-900` that composites to
   12.9:1; over `--ebr-purple` it is 3.29:1 — under the floor, because the
   purple is nowhere near as dark as it looks beside paper. 82% clears it at
   5.0:1 and still reads as secondary.

   Redefining the TOKEN rather than patching each rule is the point: every
   existing `.e-ground-brand .e-lead`, `.e-stat__label`, `[data-todo]` and
   anything added later picks the accessible value up without being touched.
   It was first fixed locally on the work banner's boxes and immediately
   missed by the next purple band, which is how it earned promoting. */
.e-ground-brand {
  background: var(--ebr-purple);
  color: var(--text-on-dark);
  --text-on-dark-muted: rgba(255,255,255,.82);
}

/* The hairline that separates grounds of the same value. */
.e-ground-white + .e-ground-white,
.e-ground-paper + .e-ground-paper { border-top: var(--border-hairline); }

.e-section__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-6);
  margin-bottom: var(--space-8);
}

/* Display statements ------------------------------------------------------ */

.e-display {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-display);
  color: var(--text-strong);
  text-wrap: pretty;
}
.e-display--xl { font-size: var(--size-display-xl); }
.e-display--l  { font-size: var(--size-display-l); }
.e-display--m  { font-size: var(--size-display-m); line-height: 1.04; }
.e-ground-ink .e-display,
.e-ground-brand .e-display { color: var(--white); }

.e-lead {
  margin: 0;
  font: var(--type-copy-lead);
  color: var(--text-copy);
  max-width: 46ch;
}
.e-ground-ink .e-lead,
.e-ground-brand .e-lead { color: var(--text-on-dark-muted); }

.e-measure { max-width: var(--measure); }

/* --------------------------------------------------------------------------
   2. SectionLabel — tiny caps label. Opens every section.
   -------------------------------------------------------------------------- */

.e-label {
  display: flex;
  align-items: center;
  gap: 12px;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.e-label__index { color: var(--ebr-purple); }
.e-ground-ink .e-label,
.e-ground-brand .e-label { color: var(--text-on-dark-muted); }
.e-ground-ink .e-label__index,
.e-ground-brand .e-label__index { color: var(--ebr-yellow); }

/* --------------------------------------------------------------------------
   3. Button
   -------------------------------------------------------------------------- */

.e-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-family: var(--font-body);
  font-weight: var(--weight-medium);
  font-size: 11px;
  padding: 13px 22px;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: var(--transition-hover), transform var(--dur-instant) var(--ease-out);
}
.e-btn:hover { text-decoration: none; }
.e-btn:active { transform: scale(.975); }

.e-btn--sm { font-size: 10px; padding: 9px 16px; }
.e-btn--lg { font-size: 12px; padding: 17px 30px; }

.e-btn--primary { background: var(--ebr-purple); color: var(--white); border-color: var(--ebr-purple); }
.e-btn--primary:hover { background: var(--ebr-purple-press); border-color: var(--ebr-purple-press); color: var(--white); }

.e-btn--accent { background: var(--ebr-yellow); color: var(--ink-900); border-color: var(--ebr-yellow); }
.e-btn--accent:hover { background: var(--ebr-yellow-press); border-color: var(--ebr-yellow-press); color: var(--ink-900); }

.e-btn--dark { background: var(--ink-900); color: var(--white); border-color: var(--ink-900); }
.e-btn--dark:hover { background: var(--ebr-purple); border-color: var(--ebr-purple); color: var(--white); }

.e-btn--outline { background: transparent; color: var(--ink-900); border-color: var(--ink-900); }
.e-btn--outline:hover { background: var(--ink-900); color: var(--white); }

.e-btn--ghost { background: transparent; color: var(--ink-900); border-color: transparent; padding: 13px 6px; }
.e-btn--ghost:hover { color: var(--ebr-purple); }

/* On a dark or purple ground the outline/ghost buttons invert. */
.e-btn--on-dark.e-btn--outline { color: var(--white); border-color: rgba(255,255,255,.35); }
.e-btn--on-dark.e-btn--ghost   { color: var(--white); border-color: transparent; }
.e-btn--on-dark.e-btn--outline:hover,
.e-btn--on-dark.e-btn--ghost:hover { background: var(--white); color: var(--ink-900); border-color: var(--white); }

.e-btn--block { width: 100%; }

/* --------------------------------------------------------------------------
   4. ArrowLink — the arrow nudges 2px up-right on hover.
   -------------------------------------------------------------------------- */

.e-arrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  font-size: var(--size-body);
  font-family: var(--font-body);
  color: var(--text-strong);
  border-bottom: 1px solid var(--line-hairline);
  padding-bottom: 4px;
  transition: var(--transition-hover);
}
.e-arrow:hover { color: var(--ebr-purple); border-bottom-color: var(--ebr-purple); text-decoration: none; }
.e-arrow svg { transition: var(--transition-transform); }
.e-arrow:hover svg { transform: translate(2px, -2px); }

.e-arrow--on-dark { color: var(--white); border-bottom-color: var(--line-on-dark); }
.e-arrow--on-dark:hover { color: var(--white); border-bottom-color: var(--ebr-yellow); }

/* --------------------------------------------------------------------------
   5. Tag
   -------------------------------------------------------------------------- */

.e-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--line-hairline);
  border-radius: var(--radius-pill);
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  background: transparent;
  transition: var(--transition-hover);
}
.e-tag--brand { background: var(--ebr-purple-wash); color: var(--ebr-purple); border-color: transparent; }
.e-tag--accent { background: var(--ebr-yellow); color: var(--ink-900); border-color: var(--ebr-yellow); }

/* --------------------------------------------------------------------------
   6. Header — the only fixed element on the page.
   -------------------------------------------------------------------------- */

.e-header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--glass-fill);
  -webkit-backdrop-filter: var(--blur-glass);
  backdrop-filter: var(--blur-glass);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
}
.e-header.is-scrolled { border-bottom-color: var(--line-hairline); }

/* Homepage: from the third section down (site.js §2 sets `is-white` off
   `data-header-white`) the bar keeps the same frosted glass as every other
   page. It was solid white until 20 Oct 2026; Adrian asked for the glass back
   so the homepage matches the rest of the site. The class still marks the
   point where the light-on-dark header hands over, so it stays. */
.e-header.is-white {
  background: var(--glass-fill);
  -webkit-backdrop-filter: var(--blur-glass);
          backdrop-filter: var(--blur-glass);
}

.e-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  padding: 16px var(--page-margin);
  max-width: var(--max-width);
  margin: 0 auto;
}
.e-header__logo { display: flex; }
.e-header__logo img { height: 26px; width: auto; display: block; }

.e-nav { display: flex; align-items: center; gap: var(--space-6); }
.e-nav__link {
  position: relative;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-decoration: none;
  padding-bottom: 3px;
  color: var(--text-muted);
  border-bottom: 1px solid transparent;
  transition: var(--transition-hover);
}
.e-nav__link:hover { color: var(--ink-900); text-decoration: none; }
.e-nav__link.is-active { color: var(--ink-900); border-bottom-color: var(--ebr-purple); }

.e-header__actions { display: flex; align-items: center; gap: 14px; }

/* Scroll progress — a 2px purple rule under the header. */
.e-progress {
  position: absolute;
  left: 0; bottom: -1px;
  height: 2px;
  width: 100%;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: var(--ebr-purple);
  pointer-events: none;
}

/* Hamburger, mobile only. */
.e-burger {
  display: none;
  flex-direction: column;
  gap: 4px;
  padding: 6px;
  background: none;
  border: none;
  cursor: pointer;
}
.e-burger span {
  display: block;
  width: 20px; height: 1.5px;
  background: var(--ink-900);
  transition: transform var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}
.e-burger.is-open span:nth-child(1) { transform: translateY(2.75px) rotate(45deg); }
.e-burger.is-open span:nth-child(2) { transform: translateY(-2.75px) rotate(-45deg); }

.e-mobilenav {
  position: fixed;
  inset: 0;
  z-index: 39;
  background: var(--paper);
  padding: 96px var(--page-margin) var(--space-8);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.e-mobilenav.is-open { opacity: 1; pointer-events: auto; }
.e-mobilenav a {
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: 34px;
  letter-spacing: var(--tracking-display);
  color: var(--ink-900);
  text-decoration: none;
  padding-bottom: var(--space-4);
  border-bottom: var(--border-hairline);
}
.e-mobilenav a:hover { color: var(--ebr-purple); text-decoration: none; }

/* --------------------------------------------------------------------------
   7. Hero
   -------------------------------------------------------------------------- */

.e-hero { padding-block: clamp(72px, 9vw, 140px) clamp(64px, 8vw, 110px); }

/* Centred composition. Every child still owns its own measure — centring the
   block is not the same as letting the lines run the full width. */
.e-hero__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-6);
  text-align: center;
}
.e-hero__inner > * { margin-inline: auto; }
.e-hero .e-label { justify-content: center; }
.e-hero__title { max-width: 20ch; }
.e-hero__lead { max-width: 42ch; }
.e-hero__actions { display: flex; gap: 14px; align-items: center; justify-content: center; flex-wrap: wrap; padding-top: var(--space-3); }

/* --- Intro curtain --------------------------------------------------------

   After fiasco.design. Their first-load curtain is `c-Preloader`, not the
   `c-PageTransition` panel that handles internal navigation — two different
   components with different motion, and the preloader is the one worth
   copying, because it is the one with the curve.

   Read out of their bundle rather than guessed at:

     - fixed, full viewport, flex-centred on the mark, overflow hidden,
       z-index above everything, brand colour ground;
     - the panel never moves. It is wiped away by an SVG `clipPath` in
       objectBoundingBox units whose bottom edge sweeps up the screen;
     - the mark drifts up a little, then leaves with the panel.

   Structure and motion are theirs. The ground is EBR violet, and the words
   are the ones the footer closes on, so the page opens and ends on the same
   phrase. Sentence case, because the design system has no title case in it.
   -------------------------------------------------------------------------- */

.e-intro {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: none;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--ebr-purple);

  /* The whole exit lives in here. Script morphs the path; the panel itself
     keeps a transform of none from first frame to last. */
  -webkit-clip-path: url(#e-intro-clip);
          clip-path: url(#e-intro-clip);
}

/* Only ever shown when script is running to take it away again. */
.js .e-intro { display: flex; }

/* Parked, not removed. The curtain used to be taken out of the document when
   it finished, which was right when it only ever ran once — §11 of the script
   now drives the same element in the other direction on the way out of a page,
   and it cannot drive an element that is no longer there. Parked is
   `display:none`: no space, no clicks, nothing to see. */
.js .e-intro.is-parked { display: none; }

/* Out of flow, so it cannot disturb the centring of the mark. Kept at zero
   size rather than display:none — a hidden subtree stops some browsers
   resolving the clip reference. */
.e-intro__clip {
  position: absolute;
  width: 0;
  height: 0;
}

/* Fallback for anything that will not resolve an SVG clip path on an HTML
   box: drop the clip and slide the panel off the top instead. Never reached
   in a current browser, and there purely so the page cannot be sealed shut
   behind a curtain that has no way to leave. */
.e-intro.is-fallback {
  -webkit-clip-path: none;
          clip-path: none;
}
.e-intro.is-fallback.is-done {
  transform: translateY(-105%);
  transition: transform 800ms cubic-bezier(.83, 0, .17, 1);
}

/* Script drives this: down 0 at rest, then up the screen in two moves. */
.e-intro__mark {
  margin: 0;
  text-align: center;
  will-change: transform;
}

.e-intro__line {
  display: block;
  /* Same clip-box correction as the hero: at this leading the descenders fall
     outside the line box and a bare overflow:hidden shears them off. */
  padding-bottom: .2em;
  margin-bottom: -.2em;
}

.e-intro__word {
  display: block;

  /* Set as the strapline is set in the logo files: uppercase, Medium, widely
     tracked. The brand guide calls this the Label role and says it "mirrors
     the logo strapline exactly" — so the curtain carries the lockup's own
     wording in the lockup's own treatment, rather than a display headline. */
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: .12em;

  /* One line, always. Sized off the viewport rather than the display scale,
     because tracked uppercase runs far wider than the same string set tight. */
  font-size: clamp(20px, 5.2vw, 64px);
  white-space: nowrap;

  line-height: 1.02;
  color: var(--white);
}
/* No entrance flourish, deliberately. Fiasco's mark is simply there on the
   first painted frame and holds still until the curtain starts to go; the
   only motion it ever has is the drift and the exit, both driven in script. */

/* On violet the accent is yellow — the same rule the logo follows.
   The left margin is negative because tracked type leaves a letter-space
   hanging off the final D, which would otherwise push the spark adrift. */
.e-intro .e-spark {
  width: .62em;
  height: .62em;
  transform: none;
  margin-left: -.02em;
}

/* No scrolling underneath while the curtain is up. */
html.is-intro, html.is-intro body { overflow: hidden; }

/* Reduced motion gets the page, immediately. */
@media (prefers-reduced-motion: reduce) {
  .js .e-intro { display: none; }
  html.is-intro, html.is-intro body { overflow: auto; }
}

/* --- Custom cursor --------------------------------------------------------

   A dot that tracks exactly and a ring that lags behind it. Both are painted
   white and the wrapper blends with `difference`, so the cursor inverts
   against whatever is under it — it reads dark on paper and light on the ink
   and violet bands without a single colour rule per section.

   Only ever built for a fine pointer with hover, and never under reduced
   motion. Touch devices and anyone who asks for less movement keep the system
   cursor, untouched.
   -------------------------------------------------------------------------- */

.e-cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 200;
  pointer-events: none;
  mix-blend-mode: difference;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.e-cursor.is-live { opacity: 1; }

.e-cursor__dot,
.e-cursor__ring {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: var(--radius-pill);
  transform: translate3d(var(--x, -100px), var(--y, -100px), 0) translate(-50%, -50%);
}

.e-cursor__dot {
  width: 6px;
  height: 6px;
  background: #fff;
  transition: width var(--dur-fast) var(--ease-out),
              height var(--dur-fast) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}

.e-cursor__ring {
  width: 34px;
  height: 34px;
  border: 1px solid rgba(255, 255, 255, .7);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: width var(--dur-base) var(--ease-out),
              height var(--dur-base) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}

.e-cursor__label {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: #fff;
  opacity: 0;
  white-space: nowrap;
  transition: opacity var(--dur-fast) var(--ease-out);
}

/* Anything clickable: the ring closes in on the dot. */
.e-cursor.is-link .e-cursor__ring { width: 22px; height: 22px; border-color: #fff; }
.e-cursor.is-link .e-cursor__dot { width: 3px; height: 3px; }

/* Media that opens something: the ring becomes a disc carrying a word.
   The disc stays white and the label goes pure black, because black is the
   identity under a difference blend — it leaves the backdrop untouched. So the
   word knocks out of the disc in whatever colour the section behind it is. */
.e-cursor.is-media .e-cursor__ring {
  width: 88px;
  height: 88px;
  background: #fff;
  border-color: transparent;
}
.e-cursor.is-media .e-cursor__label { opacity: 1; color: #000; }
.e-cursor.is-media .e-cursor__dot { opacity: 0; }

/* Over a text field the system cursor is more useful than ours. */
.e-cursor.is-text { opacity: 0; }

/* Hide the native cursor only where ours has actually taken over. */
html.has-cursor,
html.has-cursor a,
html.has-cursor button,
html.has-cursor .e-work,
html.has-cursor .e-reel__stage { cursor: none; }
html.has-cursor input,
html.has-cursor textarea,
html.has-cursor select { cursor: auto; }

/* --- Cursor-reactive headline ---------------------------------------------

   After unseen.co, which renders its type to a WebGL2 texture and warps it in
   a fragment shader. This gets the same behaviour out of an SVG displacement
   filter on live DOM text — so the headline stays real, selectable, indexable
   text rather than a picture of text.

   The split uses violet and spark yellow rather than the usual red/cyan, so it
   reads as ink misregistration in EBR's own two colours.

   The filter is only attached while a pointer is near, because running text
   through a filter rasterises it and costs a little crispness. Nobody on a
   touch screen, and nobody who never reaches for the headline, ever pays that.
   -------------------------------------------------------------------------- */

.e-hero__title {
  --warp-split: 0px;
  text-shadow:
    calc(var(--warp-split) * -1) 0 0 rgba(107, 0, 246, .5),
    var(--warp-split) 0 0 rgba(251, 222, 55, .5);
}
.e-hero__title.is-warping { filter: url(#e-warp); }

@media (prefers-reduced-motion: reduce) {
  .e-hero__title { text-shadow: none; }
  .e-hero__title.is-warping { filter: none; }
}

/* Testimonial video card sitting under the fold edge. */
.e-vidcard {
  text-align: left;
  display: flex;
  gap: 20px;
  align-items: center;
  padding-top: var(--space-6);
  text-decoration: none;
  color: inherit;
  width: fit-content;
}
.e-vidcard:hover { text-decoration: none; color: inherit; }
.e-vidcard__thumb {
  position: relative;
  flex: 0 0 200px;
  height: 124px;
  border-radius: var(--radius-media);
  background: var(--ink-900);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.e-vidcard__play {
  width: 44px; height: 44px;
  border-radius: var(--radius-pill);
  background: rgba(255,255,255,.16);
  display: flex; align-items: center; justify-content: center;
  color: var(--white);
  transition: background-color var(--dur-fast) var(--ease-out);
}
.e-vidcard:hover .e-vidcard__play { background: var(--ebr-purple); }
.e-vidcard__stamp {
  position: absolute;
  bottom: 10px; left: 14px;
  font: var(--weight-medium) 10px/1 var(--font-body);
  letter-spacing: .12em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text-on-dark-muted);
}
.e-vidcard__copy { display: flex; flex-direction: column; gap: 8px; }
.e-vidcard__quote { font: var(--type-lead); color: var(--text-copy); max-width: 30ch; }
.e-vidcard__meta { font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--ink-400); }

/* --------------------------------------------------------------------------
   8. Showreel — full-bleed ink band.
   -------------------------------------------------------------------------- */

/* One height, two users: the dark stage and the player that replaces it. They
   must match exactly or the band jumps on click, so it is a variable rather
   than the same clamp() written twice. */
.e-reel {
  /* A full screen, and nothing more clever than that — the section stays in
     normal flow, scrolls with the page, and keeps the bridge joining it to the
     banner above. Nothing here is sticky or fixed.

     `100svh`, not `100vh`, for the reason already recorded at §31: `vh` on a
     phone measures the viewport with the address bar retracted, so the band
     runs taller than the screen and pushes its own foot copy out of sight.
     That is the overflow, not a symptom of it. */
  --reel-h: 100svh;
  position: relative;
  background: var(--ink-900);
  color: var(--text-on-dark);
}

/* THE JOIN between the banner and the showreel.
 *
 * The banner ends on the Moon's surface, which is bright, and the showreel now
 * opens on the nebula still, whose top edge is rgb(5,1,11) — measured, not
 * guessed. Butted together that is a hard horizontal line across the page.
 *
 * So this straddles the boundary rather than sitting on one side of it: it
 * reaches up into the banner's last stretch and down into the showreel's
 * first, and is fully opaque exactly where the two meet. Both the banner's
 * final pixel and the showreel's first are therefore painted the same colour,
 * and the seam has nowhere to show. A gradient on one side alone cannot do
 * that — it just moves the step somewhere else.
 *
 * z-index 1 is deliberate and sits between two existing layers: above the
 * banner's film (0) and the showreel's poster, below the banner's own text and
 * buttons (2) and the showreel's controls (2). Dimming either of those would
 * trade one problem for another. */
.e-reel__bridge {
  --bridge-h: clamp(140px, 22vh, 260px);
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--bridge-h) * -1);
  height: calc(var(--bridge-h) * 2);
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom,
    rgba(5, 1, 11, 0) 0%,
    rgb(5, 1, 11) 50%,
    rgba(5, 1, 11, 0) 100%);
  transition: opacity var(--dur-base) var(--ease-out);
}

/* Once the film is running its own frames reach the top of the band, and the
   lower half of this would sit over them as a dark wash. The join no longer
   needs hiding at that point — the reader is watching, not scrolling past. */
.e-reel.is-playing .e-reel__bridge { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .e-reel__bridge { transition: none; }
}
/* The crop window. The video is sized to COVER this box and anything past the
   edges is cut, which is why the overflow lives here and not on the stage. */
.e-reel__frame {
  position: relative;
  height: var(--reel-h);
  overflow: hidden;
  background: var(--ink-900);
}

.e-reel__stage {
  position: absolute;
  inset: 0;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  cursor: pointer;
  border: none;
  width: 100%;
  /* Transparent, not ink: the poster sits behind it inside the frame. */
  background: transparent;
  color: inherit;
  padding: 0;
}

/* `[hidden]` is a UA rule at (0,1,0) and `.e-reel__stage` matches it, so the
   author `display: flex` above wins and the attribute does nothing. Script
   sets `hidden` to retire the poster once the film starts; without this line
   the poster stays on top of the playing video and the section looks dead. */
.e-reel__stage[hidden] { display: none; }
/* The only "texture" allowed: a flat hairline field, not a gradient wash. */
.e-reel__stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(to right, rgba(255,255,255,.05) 1px, transparent 1px);
  background-size: 96px 100%;
  opacity: .9;
}
.e-reel__play {
  position: relative;
  width: 84px; height: 84px;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(255,255,255,.35);
  display: flex; align-items: center; justify-content: center;
  color: var(--white);
  transition: var(--transition-hover);
}
.e-reel__stage:hover .e-reel__play { background: var(--ebr-purple); border-color: var(--ebr-purple); }
.e-reel__stamp {
  position: relative;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
}
/* The poster. Dark by choice — nebula-field, the same still the services
   banner uses — because the play ring and the stamp are white, and the
   hairline field they used to sit on is gone once a picture is underneath. */
.e-reel__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

/* A scrim over the poster, not over the whole frame — it must disappear with
   the poster when the video starts, or the film plays behind a permanent veil.
   The nebula's centre is bright magenta exactly where the ring sits, so
   without this the white-on-white is unreadable at the one point that matters. */
.e-reel__stage::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(14,10,22,.45), rgba(14,10,22,.68));
  z-index: 0;
}
.e-reel__play,
.e-reel__stamp { z-index: 1; }

/* THE VIDEO, COVERING THE BAND.

   `aspect-ratio` with both minimums and no fixed width or height is what makes
   this cover rather than stretch: the box grows until it is at least as large
   as the frame on BOTH axes while holding 16:9, and the frame crops the rest.
   Setting width:100% instead would stretch the iframe and hand the letterboxing
   back to Vimeo, which is the thing this is meant to stop. */
.e-reel__player {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  aspect-ratio: 16 / 9;
  min-width: 100%;
  min-height: 100%;
  width: auto;
  height: auto;
  border: 0;
  background: var(--ink-900);
  z-index: 0;
}

/* A FULL SCREEN IS TALLER THAN WIDESCREEN, AND THE FILM HAS TO GIVE.
 *
 * Covering works while the band is wide. Once it is a whole portrait screen it
 * stops working: measured at 100svh, a 16:9 film covering a 390x844 phone shows
 * 26% of its own width — a vertical strip through the middle, with anything
 * framed or captioned sliced off both edges. On an iPad held upright it is 42%.
 *
 * So past roughly square, the film is fitted rather than cropped: whole frame,
 * centred, the band's ink above and below it. The section still fills the
 * screen, which is what was asked for — it is the film that stops being
 * stretched to meet it.
 *
 * THE POSTER IS DELIBERATELY NOT INCLUDED. The nebula is abstract texture, so
 * a vertical slice of it still reads as a nebula and fills the screen properly.
 * Fitting that too would put a letterboxed still on a dark ground before
 * anyone has even pressed play, which is a worse first impression than a crop
 * nobody can detect.
 *
 * 1/1 is the switch, not 16/9: at 16/9 an ordinary desktop window would flip
 * to fitted while still showing about three quarters of the frame, which is a
 * crop worth keeping. Below square the loss is over half and it is not. */
@media (max-aspect-ratio: 1 / 1) {
  .e-reel__player {
    min-width: 0;
    min-height: 0;
    width: 100%;
    height: auto;
    max-width: 100%;
    max-height: 100%;
  }
}

/* Our own controls, because Vimeo's live at the bottom of a frame whose bottom
   is cropped off. Vimeo's chrome is turned off inline (`controls=0`) rather
   than left to be clipped — a half-visible scrub bar reads as a broken embed.
   The full set is one button away, in the lightbox. */
.e-reel__controls {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  gap: 10px;
  padding: var(--space-4);
  justify-content: flex-end;
  /* Legibility over whatever frame the film happens to be on. */
  background: linear-gradient(to top, rgba(14,10,22,.72), transparent);
}
.e-reel__controls[hidden] { display: none; }

.e-reel__ctl {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 14px;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(255,255,255,.3);
  background: rgba(14,10,22,.55);
  backdrop-filter: blur(6px);
  color: var(--white);
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  cursor: pointer;
  transition: var(--transition-hover);
}
.e-reel__ctl:hover { background: var(--ebr-purple); border-color: var(--ebr-purple); }
.e-reel__ctl:focus-visible { outline: 2px solid var(--white); outline-offset: 2px; }

@media (max-width: 560px) {
  .e-reel__ctl-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .e-reel__ctl { padding: 10px; }
}

/* The foot's "Watch the full reel" is a button now, because it opens the
   overlay rather than going anywhere. It has to keep looking like the arrow
   links beside it, so the button chrome is stripped back off. */
.e-arrow--btn {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  cursor: pointer;
}
.e-reel__player:focus-visible { outline: 2px solid var(--ebr-purple); outline-offset: -2px; }

/* --------------------------------------------------------------------------
   The showreel lightbox.

   Sits at 70/71, above the contact scrim and panel (60/61) and below the intro
   curtain (999) — the same scale documented at §26b. It carries its own scrim
   rather than reusing `#scrim`, which the contact panel owns and toggles.

   Here the video is NOT cropped: the band crops because it is a band, but a
   reader who asked for full size wants the whole frame and Vimeo's real
   controls. So this one is sized to 16:9 and fitted inside the viewport.
   -------------------------------------------------------------------------- */

.e-lightbox {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  padding: var(--space-5);
}
.e-lightbox[hidden] { display: none; }

.e-lightbox__scrim {
  position: absolute;
  inset: 0;
  background: rgba(10, 8, 16, .92);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.e-lightbox.is-open .e-lightbox__scrim { opacity: 1; }

/* The wrapper has to be exactly as wide as the video, not as wide as it is
   allowed to be — the close button hangs off its top-right corner, and on a
   short viewport the 16:9 frame is narrower than the 1200px cap, which left
   the button floating in open space well clear of the picture. So the height
   allowance is converted back into a width here and capped against it. */
.e-lightbox__inner {
  position: relative;
  z-index: 71;
  width: min(1200px, 100%);
  max-width: calc((100vh - var(--space-7) * 2) * 16 / 9);
  opacity: 0;
  transform: scale(.98);
  transition: opacity var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
.e-lightbox.is-open .e-lightbox__inner { opacity: 1; transform: none; }

/* 16:9, and never taller than the viewport allows once the close button and
   the padding have taken their share. */
.e-lightbox__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  width: 100%;
  background: #000;
  border-radius: var(--radius-media);
  overflow: hidden;
}
.e-lightbox__frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.e-lightbox__close {
  position: absolute;
  top: calc(var(--space-5) * -1 - 8px);
  right: 0;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(255,255,255,.3);
  background: rgba(255,255,255,.08);
  color: var(--white);
  cursor: pointer;
  transition: var(--transition-hover);
}
.e-lightbox__close:hover { background: var(--ebr-purple); border-color: var(--ebr-purple); }
.e-lightbox__close:focus-visible { outline: 2px solid var(--white); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .e-lightbox__scrim,
  .e-lightbox__inner { transition: none; }
}

/* ON the picture, not under it.
 *
 * This used to be a block in normal flow below the frame, which gave the
 * section height the frame did not fill — a flat `--ink-900` band under the
 * nebula with the line sitting in it. Two pictures separated by a strip of
 * grey, which is the thing the bridge above was added to stop happening at the
 * other edge.
 *
 * So it is lifted out of flow and laid over the foot of the band. The section
 * is now exactly the height of the frame and the line has no ground of its
 * own. Centred because the "Watch the full reel" link that used to sit
 * opposite it has gone, and a lone line at one end of a space-between row
 * reads as something missing beside it.
 *
 * Legibility comes from the stage's own scrim, which covers the poster while
 * the poster is what is showing. The two retire together: by the time the film
 * is running and the scrim has gone, this has faded out as well. */
.e-reel__foot {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-6);
  padding-bottom: clamp(20px, 4vw, 44px);
  /* The copy is read, the picture behind it is not clickable anyway — but the
     play button underneath IS, and it fills the whole stage. Letting clicks
     through keeps the entire band a single target. */
  pointer-events: none;
}
.e-reel__foot p { margin: 0; font: var(--type-copy-lead); color: var(--text-on-dark-muted); max-width: 52ch; }

/* Out of the way once the film is running. It keeps its box rather than being
   removed from flow: collapsing it would pull the whole page up by the height
   of two lines at the exact moment someone has settled in to watch. */
.e-reel.is-playing .e-reel__foot {
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-base) var(--ease-out), visibility 0s linear var(--dur-base);
}
@media (prefers-reduced-motion: reduce) {
  .e-reel.is-playing .e-reel__foot { transition: none; }
}

/* --------------------------------------------------------------------------
   9. WorkRow — full-width rows, each led by a view window.
   -------------------------------------------------------------------------- */

.e-workgrid { display: flex; flex-direction: column; }

/* One project, one full-width row: window · facts · name. Rows are separated by
   a hairline and nothing else — no card, no border, no shadow. */
.e-work {
  display: grid;
  grid-template-columns: minmax(0, 46%) minmax(0, 1fr) auto;
  gap: var(--space-6);
  padding: var(--space-6) 0;
  border-top: 1px solid var(--line-hairline);
  text-decoration: none;
  color: inherit;
}
.e-workgrid > .e-work:last-child { border-bottom: 1px solid var(--line-hairline); }
.e-work:hover { text-decoration: none; color: inherit; }

/* The view window. Filled at rest, so the work reads before any interaction and
   on touch as well as desktop — hover only adds to what is already visible. */
.e-work__win {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-media);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
.e-work__win--ink    { background: var(--ink-900); color: var(--white); }
.e-work__win--brand  { background: var(--ebr-purple); color: var(--white); }
.e-work__win--accent { background: var(--ebr-yellow); color: var(--ink-900); }
.e-work__win--muted  { background: var(--ink-100); color: var(--ink-900); }

/* Until the stills exist the client name at heading size is the artwork. */
.e-work__name {
  font: var(--type-heading);
  letter-spacing: var(--tracking-title);
  transition: transform var(--dur-slow) var(--ease-out);
}
.e-work:hover .e-work__name { transform: scale(1.03); }
.e-work__win img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}
.e-work:hover .e-work__win img { transform: scale(1.03); }

/* A loop video in the window (homepage rows; build.mjs loopOf, js/work-loops.js).
   It covers the box the way a still would and takes the same hover. The client
   name it replaces stays in the markup, because the D1 CMS field lives on it,
   but it is not drawn: the row already names the client on the right. */
.e-work__vid {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}
.e-work:hover .e-work__vid { transform: scale(1.03); }
.e-work__win.has-media .e-work__name { display: none; }

/* Middle column — outcome at the top, services pinned to the bottom so they
   sit on the same line across every row regardless of outcome length. */
.e-work__mid {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-width: 0;
}
.e-work__outcome {
  font: var(--type-copy-lead);
  color: var(--text-strong);
  max-width: 30ch;
}
.e-work__services {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: auto;
}
.e-work__services span { font: var(--type-small); color: var(--text-muted); }

.e-work__right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  text-align: right;
}
.e-work__client {
  font: var(--type-heading);
  color: var(--text-strong);
  letter-spacing: var(--tracking-title);
  white-space: nowrap;
}
.e-work__year {
  font: var(--type-small);
  color: var(--text-muted);
  vertical-align: super;
  letter-spacing: 0;
}

.e-work__go {
  width: 40px; height: 40px;
  border-radius: var(--radius-pill);
  display: flex; align-items: center; justify-content: center;
  flex: none;
  background: var(--ink-100);
  color: var(--ink-900);
  transition: var(--transition-hover);
}
.e-work:hover .e-work__go { background: var(--ebr-purple); color: var(--white); }
.e-work__go svg { transition: var(--transition-transform); }
.e-work:hover .e-work__go svg { transform: translate(2px, -2px); }

/* 23 Oct 2026, Adrian: the outcome line, then the project name, then the year,
   stacked in one left-aligned column beside the window; services stay pinned
   to the bottom with the arrow at the right. The markup is unchanged (the D1
   CMS keys sit on .e-work__mid and .e-work__client), so the two wrappers step
   aside with display: contents and their children take named grid areas. */
.e-work {
  grid-template-columns: minmax(0, 46%) minmax(0, 1fr) auto;
  grid-template-rows: auto auto 1fr auto;
  grid-template-areas:
    "win outcome  outcome"
    "win client   client"
    "win .        ."
    "win services go";
  column-gap: var(--space-6);
  row-gap: var(--space-4);
}
.e-work__win { grid-area: win; }
.e-work__mid, .e-work__right { display: contents; }
.e-work__outcome { grid-area: outcome; }
.e-work__client { grid-area: client; white-space: normal; text-align: left; display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); }
.e-work__year { vertical-align: baseline; display: block; }
.e-work__services { grid-area: services; margin-top: 0; align-self: end; }
.e-work__go { grid-area: go; align-self: end; justify-self: end; }

.e-note {
  font: var(--type-small);
  color: var(--text-muted);
  padding-top: var(--space-6);
}

/* --------------------------------------------------------------------------
   10. Stats on a hairline
   -------------------------------------------------------------------------- */

.e-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-top: var(--border-hairline);
}
.e-stats--three { grid-template-columns: repeat(3, 1fr); }
/* Cells are flex columns so the qualifier can sit on the floor of the tallest
   cell. Without it, a label that wraps to two lines drops its own qualifier
   below its neighbour's and the hairlines stop lining up across the row. */
.e-stats > * {
  padding: var(--space-5) var(--space-5);
  border-right: var(--border-hairline);
  display: flex;
  flex-direction: column;
}
.e-stat__label { margin-bottom: auto; }
.e-stats > *:last-child { border-right: none; }

.e-stat__value {
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: var(--size-display-m);
  line-height: 1;
  letter-spacing: var(--tracking-display);
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}
.e-stat__label {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  padding-top: var(--space-3);
}
/* The qualifier says what the number is measured over. Sentence case, so it does
   not stack a second run of caps under the label, and separated by space alone.

   It carried a hairline of its own until 14 Aug 2026. Four short rules at four
   different lengths across the row read as a stutter rather than a structure —
   the "hairline on everything" tell. Space does the same job silently. */
.e-stat__qual {
  font: var(--type-small);
  color: var(--text-muted);
  margin-top: var(--space-4);
}

/* No rule above the row. The vertical dividers already say "four things", and on
   a full-bleed ground the section edge is separation enough. */
.e-stats--plain { border-top: none; }

/* The first cell sits flush with the shell edge so the row lines up with the
   section label above it, rather than starting one pad in. */
.e-stats > :first-child { padding-left: 0; }

.e-ground-ink .e-stat__value { color: var(--white); }
.e-ground-ink .e-stat__label { color: var(--text-on-dark-muted); }
.e-ground-ink .e-stat__qual  { color: var(--text-on-dark-muted); }
.e-ground-ink .e-stats,
.e-ground-ink .e-stats > * { border-color: var(--line-on-dark); }

/* Proof hands off to a booking. One line, one button, on a rule under the row. */
.e-handoff {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
  margin-top: var(--space-6);
  padding-top: var(--space-6);
  border-top: var(--border-hairline);
}
.e-handoff p {
  font: var(--type-copy-lead);
  color: var(--text-strong);
  margin: 0;
}
.e-ground-ink .e-handoff { border-color: var(--line-on-dark); }
.e-ground-ink .e-handoff p { color: var(--text-on-dark); }

/* --------------------------------------------------------------------------
   11. Client strip
   -------------------------------------------------------------------------- */

/* Auto-fit rather than a fixed 6-up: the strip carries the seven real client
   names and must not leave orphan cells. Swap the names for logo <img>s when
   real marks arrive — the cell geometry is already right. */
/* Six across, no cells and no borders. The strip used to box every client in a
   ruled cell — fourteen rules for seven names, sitting directly under the proof
   band. Spacing separates them now; a mark needs air around it, not a frame. */
.e-clients {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  /* Row gap tightened twice on Adrian's call: --space-7 (48px) to --space-6
     (32px), then to --space-5 (24px).

     ⚠ 24px, NOT 26px. The second request was another 20%, which is 25.6px, and
     there is no token there. A one-off literal in a single component is how a
     spacing scale stops being a scale — and 2px is invisible where being on the
     ladder is not. --space-5 is a 25% cut rather than 20%.

     The column gap deliberately stays at 32px, wider than the row gap, so the
     eye tracks across a row before dropping to the next one. */
  gap: var(--space-5) var(--space-6);
}

.e-client {
  /* The row box. It has to clear the tallest --mh in the set or that mark spills
     out of its cell and into the gap. The whole set was then scaled to 80%, so
     the tallest mark — Rare Heritage, square, plus its 20% — lands at 66px.
     Raise this whenever a mark is scaled past it: the overflow is silent,
     because nothing clips. */
  height: 66px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Capped on both axes, not sized on height. These marks range from 1.08:1 to
   6.88:1 — a wordmark nearly seven times wider than it is tall would swamp a
   square emblem beside it if height alone were matched. Each one fits inside
   the same box instead, so the wall reads as one set. */
.e-client__mark {
  /* ⚠ EACH MARK CARRIES ITS OWN CAP, SET INLINE AS --mh AND COMPUTED FROM ITS
     REAL ASPECT RATIO SO EVERY LOGO OCCUPIES ROUGHLY THE SAME AREA. A single
     shared cap cannot work across a set running 1.00:1 to 5.62:1 — the squares
     stop on height, the wordmarks stop on width, and the squares end up at half
     the visual weight. max-height rather than height, so a narrow viewport can
     shrink a mark on width without distorting it. */
  max-height: var(--mh, 44px);
  /* Now only a guard against a mark overflowing a narrow cell — the area
     formula does the sizing, not this. */
  max-width: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  display: block;
  /* ⚠ RAISED FROM .55 ON 29 AUG 2026. The dimming was set when every mark was
     a mid-grey SVG of matched density. The supplied AVIF set is not matched —
     the lightest marks in it (Neon Attack, Last Voyage) were disappearing into
     the paper at .55. This helps, but the real fix is density in the files:
     a mark this light needs a darker version, not a louder wall. */
  opacity: .62;
  filter: grayscale(1);
  transition: var(--transition-hover);
}
.e-client:hover .e-client__mark { opacity: 1; }

/* Note for whoever tries this next: Trikaya was supplied as a knockout — a
   solid black panel with the letterforms cut out of it as transparent holes —
   so it lands as a dark slab among ten grey marks. `invert()` does not fix it.
   Inverting turns the panel white and leaves the holes transparent, so the
   whole logo disappears. It needs a positive version of the file. */

/* The three extra rows. Same animation as the FAQ accordion — no new pattern. */
.e-clients__more {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-base) var(--ease-out);
}
.e-clients__more > div { overflow: hidden; }
.e-clients__more.is-open { grid-template-rows: 1fr; }
.e-clients--rest { padding-top: var(--space-6); }

.e-clients__expand {
  display: flex;
  justify-content: center;
  padding-top: var(--space-6);
}
.e-clients__expand .e-btn { gap: 10px; }

.e-ground-ink .e-client { color: var(--text-on-dark-muted); }
.e-ground-ink .e-client:hover { color: var(--white); }

/* --------------------------------------------------------------------------
   12. ServiceRow — oversized numerals as the scroll rhythm.
   -------------------------------------------------------------------------- */

/* Rules sit only between rows — four for five rows — so the list is divided
   without being fenced. The rule above the first row and below the last are
   gone; the section's own edges already do that work. */
.e-services { display: flex; flex-direction: column; }

.e-service {
  display: grid;
  /* ⚠ THE THIRD COLUMN IS RESERVED ON EVERY ROW, INCLUDING THE FIVE WITH NO
     ICON. Giving it only to the row that has one would make that row's middle
     column narrower than the other five, and the qualifier text would step in
     and out along the list. An empty 104px cell costs nothing and means the
     next icon drops straight in. */
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr) 152px;
  gap: var(--space-5) var(--space-7);
  align-items: center;
  padding: var(--space-7) 0;
}

/* The 3D icon slot. Sized in CSS rather than by the canvas, so the row height
   is decided before any script runs and nothing shifts when it mounts — this
   site measures its own layout shift. */
.e-service__icon {
  width: 152px;
  /* ⚠ HEIGHT ONLY ON THE ROWS THAT HAVE AN ICON. The column is reserved on all
     six so the middle column keeps one width down the list, but giving the
     empty cells a height too would push those rows 48px taller to hold
     nothing. */
  justify-self: end;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .42s var(--ease-out), transform .42s var(--ease-out);
  pointer-events: none;
}
[data-service-icon] .e-service__icon { height: 152px; }
.e-service:hover .e-service__icon,
.e-service:focus-within .e-service__icon { opacity: 1; transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .e-service__icon { transition: opacity .001s; transform: none; }
}
.e-service + .e-service { border-top: var(--border-hairline); }

/* The service name, kept small but present — someone scanning for "SEO" has to
   be able to find the word. */
.e-service__tag {
  display: block;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  padding-bottom: var(--space-3);
}

/* The outcome is the headline. This copy already existed on the page, set at
   body size under the service name; it was the best writing in the section and
   it was buried. */
.e-service__title {
  margin: 0;
  font: var(--weight-medium) 30px/1.12 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
  max-width: 22ch;
}

/* The qualifier — one line, so a reader can place themselves in it at a glance.
   This is the part that earns the enquiry; a service name cannot do it at any
   size of type. */
.e-service__qual {
  margin: 0;
  font: var(--type-lead);
  color: var(--text-copy);
  max-width: 34ch;
}

/* Capabilities block */
.e-caps { margin-top: var(--space-7); padding-top: var(--space-6); border-top: var(--border-hairline); }
.e-caps__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* Column gap steps down from --space-7 with the fourth column added. Four
     columns at 48px left each one too narrow to hold "Social media management"
     on a single line, and a discipline that wraps reads as two disciplines. */
  gap: var(--space-6);
  padding-top: var(--space-5);
}
.e-caps__head {
  margin: 0 0 var(--space-4);
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-strong);
}
/* The head is a button at every width so the mobile accordion has something to
   bind to. Above the mobile breakpoint it is stripped back to plain text and
   disabled by JS, so it neither looks nor behaves like a control. */
.e-caps__toggle {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  text-align: left;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: default;
}
.e-caps__icon { display: none; flex-shrink: 0; color: var(--text-muted); }

/* Open by default, so the block is complete with JS off and on every screen
   wider than the accordion breakpoint. Only mobile collapses it. */
/* Same three-part structure as the FAQ: the panel is the grid, the div inside it
   is what clips. The padding has to live on the list, not on the clipped div —
   padding on the element being collapsed survives a 0fr track and leaves the
   panel 24px tall when shut. */
.e-caps__panel { display: grid; grid-template-rows: 1fr; }
.e-caps__panel > div { overflow: hidden; }
.e-caps__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font: var(--type-copy-small);
  color: var(--text-copy);
}

/* --------------------------------------------------------------------------
   13. QuoteCard
   -------------------------------------------------------------------------- */

.e-quote {
  position: relative;
  margin: 0;
  background: var(--surface-card);
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  padding: var(--space-7);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-6);
  min-height: 260px;
}
.e-quote__figure {
  position: absolute;
  top: var(--space-5);
  right: var(--space-6);
  font: var(--weight-medium) 56px/1 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--ebr-purple);
}
.e-quote blockquote {
  margin: 0;
  max-width: 34ch;
  font: var(--weight-regular) 22px/1.35 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
}
.e-quote figcaption { font: var(--type-small); color: var(--text-muted); }
.e-quote figcaption strong { font-weight: var(--weight-regular); color: var(--text-strong); }

/* --------------------------------------------------------------------------
   13b. Testimonial rail — one row, scrolled sideways.

   Reference is Cursor's quote grid, held in a single row instead of six-up.
   Panels are a tint on the white ground, not cards: no border and no radius.
   The work grid gave up its card chrome and the logo wall gave up its boxed
   cells for the same reason — a rule drawn around a quote divides nothing, and
   the change of ground already does the separating.
   -------------------------------------------------------------------------- */

.e-tst__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-7);
  margin-bottom: var(--space-7);
}
.e-tst__intro { display: flex; flex-direction: column; gap: var(--space-4); }
.e-tst__title { max-width: 20ch; }

.e-tst__nav { display: flex; gap: var(--space-3); flex-shrink: 0; }
.e-tst__arrow {
  width: 44px; height: 44px;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  border: var(--border-hairline);
  background: none;
  color: var(--ink-900);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: var(--transition-hover);
}
.e-tst__arrow:hover:not(:disabled) { background: var(--ink-900); color: var(--white); border-color: var(--ink-900); }
/* Disabled rather than wrapping. A rail that silently loops makes it impossible
   to tell how much of it there is. */
.e-tst__arrow:disabled { opacity: .28; cursor: default; }
.e-tst__arrow--prev svg { transform: scaleX(-1); }

/* Native overflow plus snap. Touch, trackpad and shift-scroll all work with no
   script at all — JS only drives the arrow states and the fill below. */
.e-tst__rail {
  display: flex;
  gap: var(--space-5);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: var(--space-6);
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.e-tst__rail::-webkit-scrollbar { display: none; }

.e-tst {
  /* 340 + 24 gap against a ~1280 shell leaves three whole panels and most of a
     fourth. The cut panel is the affordance: a row ending flush reads as
     finished, a row ending mid-panel reads as scrollable. */
  flex: 0 0 340px;
  scroll-snap-align: start;
  margin: 0;
  background: var(--paper);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  min-height: 270px;
}

/* The client's mark, not a headshot — ten of them already ship in
   assets/clients/, and no portraits do. A face proves a person; a mark proves a
   company. Drop an <img class="e-tst__mark"> in place of this empty span. */
.e-tst__mark {
  display: block;
  height: 26px;
  width: 84px;
  background: var(--ink-100);
}
img.e-tst__mark {
  width: auto;
  max-width: 120px;
  max-height: 26px;
  object-fit: contain;
  object-position: left center;
  background: none;
  opacity: .55;
  filter: grayscale(1);
}

.e-tst__quote {
  margin: 0;
  flex: 1;
  font: var(--weight-regular) 17px/1.45 var(--font-display);
  letter-spacing: var(--tracking-body);
  color: var(--text-strong);
}
/* Placeholder styling, and it is meant to look like one. Delete the modifier
   with the placeholder text. */
.e-tst__quote--todo { color: var(--text-muted); font-style: italic; }

.e-tst__who { display: flex; flex-direction: column; gap: 2px; font: var(--type-small); color: var(--text-muted); }
.e-tst__who strong { font-weight: var(--weight-regular); color: var(--text-strong); }

.e-tst__prog { height: 2px; background: var(--line-hairline); position: relative; }
.e-tst__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 100%;
  background: var(--ink-900);
  transition: width var(--dur-base) var(--ease-out);
}

/* --------------------------------------------------------------------------
   14. Studio
   -------------------------------------------------------------------------- */

/* Centred, not top-aligned: the portrait is a good 200px taller than the copy
   beside it, and hanging the text from the top left a pool of dead space under
   "Meet the studio". Centred, the overhang splits evenly above and below and
   reads as composition rather than a column that ran out. */
.e-studio { display: grid; grid-template-columns: 1.15fr 1fr; gap: var(--space-8); align-items: center; }
.e-studio__col { display: flex; flex-direction: column; gap: var(--space-5); }
.e-studio__pull { margin: 0; font: var(--type-lead); color: var(--text-strong); max-width: 48ch; }
.e-studio__by { display: flex; align-items: center; }
.e-studio__byline { font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-muted); }
/* Was a fixed 520px grey box with a label in it. Now it holds a real portrait,
   so the frame takes its shape from the picture: 4:5 is a hair wider than the
   source's own 0.733, which means a few pixels off the bottom and nothing off
   the face. Height follows the column width, so the two columns stay level
   from tablet up without a magic number. */
.e-studio__photo {
  aspect-ratio: 4 / 5;
  margin: 0;
  border-radius: var(--radius-media);
  background: var(--ink-100);
  overflow: hidden;
}
.e-studio__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* High, because his face sits in the upper third of the frame. */
  object-position: 50% 30%;
}

.e-team {
  display: grid;
  grid-template-columns: repeat(4, 1fr);   /* eight people, two clean rows */
  gap: var(--space-6) var(--space-5);
  border-top: var(--border-hairline);
  padding-top: var(--space-6);
  margin-top: var(--space-8);
}
.e-team__member { display: flex; flex-direction: column; gap: 12px; margin: 0; }

/* The portrait and its hover take share one box. The still is an <img> in the
   markup; the clip is a <video> the script only builds on first hover, so a
   visitor who never hovers a face never downloads one. */
.e-team__portrait {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-media);
  background: var(--ink-100);
  overflow: hidden;
}
.e-team__portrait img,
.e-team__portrait video {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.e-team__portrait video {
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}
/* Only once the clip is decoded and running — swapping to a video that has not
   started yet shows a black frame where a face was. */
.e-team__portrait.is-playing video { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .e-team__portrait video { display: none; }
}
.e-team__name { font: var(--type-lead); color: var(--text-strong); }
.e-team__role { margin-top: 4px; font: var(--type-small); color: var(--text-muted); }

/* --------------------------------------------------------------------------
   15. Engagement cards
   -------------------------------------------------------------------------- */

.e-plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-5); align-items: stretch; }
.e-plan {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-6) var(--space-6);
  border-radius: var(--radius-md);
  border: 1px solid rgba(255,255,255,.18);
  background: transparent;
  transition: var(--transition-hover);
}
.e-plan:hover { border-color: rgba(255,255,255,.45); }
.e-plan--featured { background: var(--ebr-purple); border-color: transparent; }
.e-plan--featured:hover { background: var(--ebr-purple-press); border-color: transparent; }

.e-plan__head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.e-plan__name {
  font: var(--weight-medium) 28px/1.05 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--white);
}
.e-plan__badge {
  font: var(--type-label);
  font-size: 10px;
  letter-spacing: var(--tracking-label);
  color: var(--ink-900);
  background: var(--ebr-yellow);
  padding: 7px 10px;
  border-radius: var(--radius-pill);
  white-space: nowrap;
  text-transform: uppercase;
}
.e-plan__kicker { font: var(--weight-medium) 15px/1.3 var(--font-body); color: var(--ebr-purple-soft); }
.e-plan--featured .e-plan__kicker { color: var(--ebr-yellow); }
.e-plan__body { margin: 0; font: var(--type-copy); color: var(--text-on-dark-muted); }
.e-plan--featured .e-plan__body { color: rgba(255,255,255,.9); }
.e-plan__foot {
  margin-top: auto;
  padding-top: var(--space-5);
  border-top: 1px solid rgba(255,255,255,.2);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.e-plan__term { font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-on-dark-muted); }
.e-plan__price { font: var(--weight-medium) 19px/1.2 var(--font-body); color: var(--white); }

.e-plans__foot { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; padding-top: var(--space-6); }
.e-plans__foot span { font: var(--type-lead); color: var(--text-on-dark-muted); }

/* --------------------------------------------------------------------------
   16. Accordion
   -------------------------------------------------------------------------- */

.e-faq { border-top: var(--border-hairline); }
.e-faq__item { border-bottom: var(--border-hairline); }
.e-faq__q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  background: none;
  border: none;
  cursor: pointer;
  text-align: left;
  padding: var(--space-5) 0;
  font: var(--type-lead);
  letter-spacing: var(--tracking-body);
  color: var(--text-strong);
  transition: var(--transition-hover);
}
.e-faq__q:hover { color: var(--ebr-purple); }
.e-faq__item.is-open .e-faq__q { color: var(--ebr-purple); }
.e-faq__toggle {
  width: 30px; height: 30px;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  display: flex; align-items: center; justify-content: center;
  background: var(--ink-100);
  color: var(--ink-900);
  transition: var(--transition-hover);
}
.e-faq__item.is-open .e-faq__toggle { background: var(--ebr-purple); color: var(--white); }
.e-faq__toggle .e-icon-minus { display: none; }
.e-faq__item.is-open .e-faq__toggle .e-icon-plus { display: none; }
.e-faq__item.is-open .e-faq__toggle .e-icon-minus { display: block; }

.e-faq__a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-base) var(--ease-out);
}
.e-faq__item.is-open .e-faq__a { grid-template-rows: 1fr; }
.e-faq__a > div { overflow: hidden; }
.e-faq__a p { margin: 0; padding-bottom: var(--space-6); max-width: var(--measure); font: var(--type-copy); color: var(--text-copy-strong); }

/* --------------------------------------------------------------------------
   17. Insights
   -------------------------------------------------------------------------- */

.e-posts { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-5); }
.e-post { display: flex; flex-direction: column; gap: 14px; text-decoration: none; color: inherit; }
.e-post:hover { text-decoration: none; color: inherit; }
.e-post__still {
  height: 150px;
  border-radius: var(--radius-media);
  background: var(--ink-100);
  display: flex; align-items: center; justify-content: center;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-400);
  transition: var(--transition-hover);
}
.e-post:hover .e-post__still { background: var(--ebr-purple-wash); color: var(--ebr-purple); }
.e-post__title {
  font: var(--weight-medium) 19px/1.3 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
  transition: var(--transition-hover);
}
.e-post:hover .e-post__title { color: var(--ebr-purple); }

/* --------------------------------------------------------------------------
   18. Closing CTA
   -------------------------------------------------------------------------- */

/* The band clips its own decoration. Both layers are sized past the edges so
   nothing ever shows a boundary as it drifts. */
/* Was `#contact`. The work page reuses this whole band, so the block is a
   class now and the id is just the anchor. `overflow` is safe here — unlike
   the work banner, nothing inside this band is sticky. */
.e-cta { position: relative; overflow: hidden; isolation: isolate; }

.e-cta__inner { position: relative; z-index: 2; display: flex; flex-direction: column; gap: var(--space-6); }
.e-cta__actions { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
.e-cta__email { font: var(--type-lead); color: var(--white); text-decoration: none; border-bottom: 1px solid rgba(255,255,255,.4); padding-bottom: 2px; }
.e-cta__email:hover { color: var(--ebr-yellow); border-bottom-color: var(--ebr-yellow); text-decoration: none; }

/* --------------------------------------------------------------------------
   18b. Closing CTA — the living ground and the ribbons.

   The right 60% of this band was empty. Two layers fill it, neither of which
   means anything: a ground of three tonal fields drifting past each other, and
   three ribbons over the top.

   Three rules keep this out of stock-gradient territory, and all of them are
   easy to lose in a later edit:

     1. One hue. Violet, indigo, lilac. Nothing crosses into blue or pink — the
        moment it does, this reads as a SaaS hero rather than an EBR band.
     2. No two cycles share a factor. 34 / 46 / 58 on the ground, 19 / 27 / 33
        on the ribbons, so nothing ever settles into a visible loop.
     3. Slow enough to notice on the second look, not the first.

   Everything animates on `transform` alone, so the whole thing stays on the
   compositor. The one expensive part is the blur on the ribbon layer, which is
   why the layers are paused until the section is actually on screen.
   -------------------------------------------------------------------------- */

.e-cta__ground,
.e-cta__ribbons {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.e-cta__ground { z-index: 0; overflow: hidden; }
.e-cta__ribbons { z-index: 1; filter: blur(34px); opacity: .85; }
.e-cta__ribbons svg { width: 100%; height: 100%; display: block; }

/* Scrim. Not decoration — the pale ribbon drifts behind the headline and the
   11px "Start here" label, and white on a lightened violet drops under the
   contrast floor for small text. This pins the left column back to something
   near the base purple and fades out before the half-way point, so the right
   side keeps all its movement. It is an ::after so it paints over the ribbons
   at the same stacking level, and under the content at z-index 2.

   This one stays — unlike the work banner's, which was removed. There the
   ground sat behind `--text-muted` on paper with no headroom; here the copy is
   white on violet and the scrim only has to hold the left column near the base
   purple rather than hide the ground entirely. */
.e-cta::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(100deg,
    rgba(107, 0, 246, .82) 0%,
    rgba(107, 0, 246, .58) 26%,
    rgba(107, 0, 246, 0) 58%);
}

.e-cta__aur { position: absolute; border-radius: 50%; will-change: transform; }
.e-cta__aur--1 {
  width: 78%; height: 190%; left: 8%; top: -45%;
  background: radial-gradient(closest-side, rgba(150, 60, 255, .95), rgba(150, 60, 255, 0) 72%);
  animation: e-aur-1 34s ease-in-out infinite alternate;
}
.e-cta__aur--2 {
  width: 66%; height: 170%; left: 44%; top: -38%;
  background: radial-gradient(closest-side, rgba(60, 0, 150, .9), rgba(60, 0, 150, 0) 70%);
  animation: e-aur-2 46s ease-in-out infinite alternate;
}
.e-cta__aur--3 {
  width: 52%; height: 150%; left: -8%; top: -25%;
  background: radial-gradient(closest-side, rgba(190, 140, 255, .5), rgba(190, 140, 255, 0) 68%);
  animation: e-aur-3 58s ease-in-out infinite alternate;
}
@keyframes e-aur-1 {
  from { transform: translate3d(-14%, -6%, 0) scale(1); }
  to   { transform: translate3d(16%, 7%, 0) scale(1.22); }
}
@keyframes e-aur-2 {
  from { transform: translate3d(12%, 8%, 0) scale(1.15); }
  to   { transform: translate3d(-16%, -7%, 0) scale(.92); }
}
@keyframes e-aur-3 {
  from { transform: translate3d(8%, 10%, 0) scale(1.05); }
  to   { transform: translate3d(34%, -8%, 0) scale(1.3); }
}

/* Ribbons move further and faster than the ground, or they disappear into it.
   The wireframe ran them at 9% over 26s against a flat purple and they read as
   still — a moving ground raises the threshold for what counts as movement. */
.e-cta__rb { will-change: transform; transform-origin: 55% 50%; }
.e-cta__rb--1 { animation: e-rb-1 19s ease-in-out infinite alternate; }
.e-cta__rb--2 { animation: e-rb-2 27s ease-in-out infinite alternate; }
.e-cta__rb--3 { animation: e-rb-3 33s ease-in-out infinite alternate; }
@keyframes e-rb-1 {
  from { transform: translate3d(-7%, 5%, 0) rotate(-1.6deg) scale(1); }
  to   { transform: translate3d(8%, -6%, 0) rotate(1.4deg) scale(1.1); }
}
@keyframes e-rb-2 {
  from { transform: translate3d(6%, -5%, 0) rotate(1.2deg) scale(1.08); }
  to   { transform: translate3d(-8%, 6%, 0) rotate(-1.8deg) scale(.96); }
}
@keyframes e-rb-3 {
  from { transform: translate3d(-9%, -4%, 0) rotate(1deg) scale(1.06); }
  to   { transform: translate3d(7%, 6%, 0) rotate(-1.2deg) scale(1.16); }
}

/* Paused only when the band is scrolled away — `is-idle` is added by site.js §7b.
   The default is running, and that direction matters: if the observer never
   fires (JS off, an old browser, anything else) the band animates as designed
   and only the optimisation is lost. The other way round, one silent failure
   leaves a permanently static band and nobody finds out. Fail toward the thing
   that looks right. */
/* Was `#contact.is-idle`. The work banner reuses this ground, so the pause is
   keyed to the state rather than to one section id. */
.is-idle .e-cta__aur,
.is-idle .e-cta__rb { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .e-cta__aur,
  .e-cta__rb { animation: none; }
}
.e-spark {
  display: inline-block;
  width: .72em; height: .72em;
  background: var(--ebr-yellow);
  -webkit-mask: url(../assets/sparkle.svg) center / contain no-repeat;
  mask: url(../assets/sparkle.svg) center / contain no-repeat;
  margin-left: .12em;
  vertical-align: baseline;
}

/* --------------------------------------------------------------------------
   19. Footer — the closing statement.
   -------------------------------------------------------------------------- */

.e-footer { background: var(--ink-900); color: var(--text-on-dark); padding-top: var(--space-9); }

/* Mark and strapline together, which is what a lockup is. The edge-to-edge
   version that used to close the page was the largest thing on it by a wide
   margin, and it sat below the legal bar — after everything, saying nothing.
   Baseline-aligned rather than centred: the mark's own baseline sits with the
   statement's cap line. */
/* The mark on the left, "Say hello" set against it on the right (18 Sep 2026).
   `align-items: center` rather than baseline: the two sides are a picture and a
   small stack of links, which have no shared baseline to sit on. */
.e-footer__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  margin-bottom: var(--space-9);
  flex-wrap: wrap;
}

/* Right-aligned, and the social marks with it — a 40px hit target flush to the
   same edge as the address above it, or the row reads as two ragged columns. */
/* Social marks only, as of 18 Sep 2026 — the "Say hello" heading and the
   address came out, because the address is already offered twice below (the
   Contact column and the closing band) and a third copy on the same screen was
   asking the same question three times. What is left is a row of marks set
   against the lockup, so the top line of the footer is the brand at one end and
   where to find it at the other. */
.e-footer__hello { display: flex; align-items: center; }
.e-footer__hello .e-social { margin: 0; justify-content: flex-end; }
/* Optically matched to the statement's cap height rather than set to a round
   number — the wordmark is 3.1:1, so 116px wide lands it at roughly the height
   of the E beside it. */
.e-footer__mark { width: 116px; height: auto; display: block; flex-shrink: 0; }

/* The mark is a link home. The anchor has to be a flex item in its own right or
   it collapses to a text baseline and drags the mark off the statement's centre.
   Hover dims rather than underlines — the mark is an image, and a link
   underline under a wordmark reads as a mistake. */
.e-footer__home {
  display: flex;
  flex-shrink: 0;
  align-self: center;
  transition: var(--transition-hover);
}
.e-footer__home:hover { opacity: .72; text-decoration: none; }
.e-footer__statement {
  max-width: 24ch;
  margin: 0;
  font: var(--weight-medium) var(--size-display-m)/1.08 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--white);
}
/* Four columns, each as wide as it needs to be, with the space left over split
   evenly between them (18 Sep 2026, Adrian). Equal `1fr` tracks were already
   equal on paper, but a track is not a column of type: "Services" runs to 190px
   of text and "Contact" to 120px, so equal tracks left a ragged set of gaps and
   a 200px hole at the right-hand end while the social marks above sat flush to
   that edge. `space-between` puts the first column on the left margin, the last
   on the right, and the same gap between each pair. */
.e-footer__cols {
  display: grid;
  grid-template-columns: repeat(4, auto);
  justify-content: space-between;
  gap: var(--space-7);
  padding-bottom: var(--space-8);
}

/* The one column that would otherwise size itself to a single long line. */
.e-footer__ask .e-ask__note { max-width: 30ch; }

/* Below this the four no longer fit side by side and go back to wrapping as
   many equal columns as the width allows. */
@media (max-width: 1080px) {
  .e-footer__cols {
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    justify-content: stretch;
  }
}
.e-footer__coltitle {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
  padding-bottom: var(--space-4);
}
.e-footer__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.e-footer__list a { font: var(--type-small); color: var(--text-on-dark-muted); text-decoration: none; transition: var(--transition-hover); }
.e-footer__list a:hover { color: var(--white); text-decoration: none; }

.e-offices { list-style: none; margin: var(--space-5) 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.e-offices li { font: var(--type-small); color: var(--text-on-dark-muted); display: flex; gap: 12px; }
.e-offices .e-offices__city { color: var(--white); }
.e-offices time { font-variant-numeric: tabular-nums; }

.e-footer__bar { border-top: var(--border-on-dark); padding: var(--space-5) var(--page-margin) var(--space-6); }
.e-footer__bar > div {
  max-width: var(--max-width);
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
  gap: var(--space-5);
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
}
/* Social marks. 40px targets rather than 19px glyphs — the icon is what you
   see, the square is what you hit. */
.e-social { list-style: none; margin: var(--space-4) 0 0; padding: 0; display: flex; gap: var(--space-2); }
.e-social a {
  width: 40px; height: 40px;
  display: flex; align-items: center; justify-content: center;
  color: var(--text-on-dark-muted);
  border-radius: var(--radius-sm);
  transition: var(--transition-hover);
}
.e-social a:hover { color: var(--white); background: rgba(255, 255, 255, .07); }

/* Ask about us in AI. It had its own band under the columns until 18 Sep 2026;
   Adrian moved it into the grid as the third column, where "Say hello" was. No
   rule of its own any more — it is a column among columns now, and the band's
   border-top would have drawn a line through the middle of the row. */
.e-footer__ask { display: flex; flex-direction: column; }
.e-ask__note {
  margin: 0 0 var(--space-4);
  font: var(--type-small);
  color: var(--text-on-dark-muted);
  /* No measure here. 34ch was right across a half-width band; inside a 160px
     column it does nothing but sit wider than the column it is in. */
}

/* Two to a row (18 Sep 2026). Five assistants stacked ran the column far past
   the two beside it; paired, it ends level with them.

   `max-content` columns rather than `1fr`: equal columns would stretch
   "Grok" to the width of "Perplexity", and a pill is a label with a border
   round it, not a button that wants a grid cell. `justify-items: start` keeps
   the pair left-aligned when the second cell is empty on the last row. */
.e-ask__list {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(2, max-content);
  justify-items: start;
  gap: var(--space-2);
}
/* Labels never wrap inside a pill — a two-line "Perplexity" reads as a broken
   control rather than a tight fit. */
.e-ask__link { white-space: nowrap; }
.e-ask__link {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 10px 16px;
  border: var(--border-on-dark);
  border-radius: var(--radius-pill);
  font: var(--type-small);
  color: var(--text-on-dark-muted);
  text-decoration: none;
  transition: var(--transition-hover);
}
/* These are filled marks, not stroked glyphs like the rest of the sprite, so
   they read heavier at the same pixel size. Held at .62 rather than the .7 the
   stroke icons use, and lifted to full on hover. */
.e-ask__mark { flex-shrink: 0; opacity: .62; transition: var(--transition-hover); }
.e-ask__link:hover {
  color: var(--ink-900);
  background: var(--ebr-yellow);
  border-color: var(--ebr-yellow);
  text-decoration: none;
}
.e-ask__link:hover .e-ask__mark { opacity: 1; }

@media (max-width: 700px) {
  /* The marks stay opposite the lockup rather than wrapping under it: two 40px
     targets and a 116px wordmark fit on one line at 390px with room to spare,
     and the row reads as a lockup either way. */
  .e-footer__hello .e-social { justify-content: flex-end; }
}

/* --------------------------------------------------------------------------
   19b. The ticker — eleven facts, running.

   Ported from the live site, where the same eleven run in a rounded pill
   between the footer links and the legal bar. The pill did not come with it:
   this system has no floating capsules on flat fields, and here a rule does
   the same job with nothing left over.

   Which is the whole reason the legal bar gives its own rule up below. The
   footer already draws one above `.e-ask`; a second above the ticker divides
   the facts from the links, and a third above the bar would be three rules in
   about two hundred pixels — decoration, not division. Dropping the bar's rule
   keeps the count at two and lets the ticker and the legal line read as one
   closing strip, which is what they are.
   -------------------------------------------------------------------------- */

.e-tick {
  border-top: var(--border-on-dark);
  padding-block: var(--space-4);
  overflow: hidden;
}

/* The bar's own rule, given up to the ticker. Written as a sibling selector so
   it comes back by itself on any page that has a footer and no ticker. */
.e-tick + .e-footer__bar { border-top: 0; }

/* Two identical sets, so translating exactly half the track lands the second
   set precisely where the first began and the loop has no seam. `max-content`
   is what stops the flex line wrapping and collapsing the whole thing to one
   screen wide.

   Linear, and the only linear thing in the system. Every other curve here is
   `--ease-out`, which exists to make a movement start and stop; this one does
   neither, and easing it would make the facts visibly hesitate at each turn of
   the loop. */
.e-tick__track {
  display: flex;
  width: max-content;
  animation: e-tick-run 100s linear infinite;
}

/* Reading a moving line is a fight, so it stops when the pointer is on it —
   and when the keyboard reaches it, which is the half that matters. Hover
   alone is not a mechanism under WCAG 2.2.2: a keyboard or switch user has no
   way to invoke it, and this line runs for a hundred seconds beside the rest
   of the footer.

   The focus stop earns itself twice over. It pauses the line, and under
   reduced motion the ticker becomes a horizontal scroller — without a focus
   stop, ten of the eleven facts would be unreachable in exactly the mode meant
   to keep them readable. This is not the dead focus stop the beliefs rail used
   to carry; it does something in both states. */
.e-tick:hover .e-tick__track,
.e-tick:focus-within .e-tick__track { animation-play-state: paused; }

/* Inset, because the ticker is full-bleed: an outset ring on a box as wide as
   the viewport puts its left and right edges off screen and leaves two
   full-width rules dividing nothing. */
.e-tick:focus-visible {
  outline: 2px solid var(--ebr-yellow);
  outline-offset: -3px;
}

.e-tick__set {
  display: flex;
  align-items: center;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* The rule between facts is the only thing separating one from the next in a
   line that never breaks, so it is dividing something — which is the test. */
.e-tick__set li {
  font: var(--type-small);
  color: var(--text-on-dark-muted);
  white-space: nowrap;
  padding-inline: var(--space-6);
  border-inline-start: var(--border-on-dark);
}

@keyframes e-tick-run {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* Under reduced motion it stops being a ticker and becomes a line you scroll
   yourself. The facts stay reachable — they are content, not decoration, so
   hiding them would be the wrong way to hold still. */
@media (prefers-reduced-motion: reduce) {
  .e-tick__track { animation: none; }
  .e-tick { overflow-x: auto; }
}

@media (max-width: 700px) {
  .e-tick__set li { padding-inline: var(--space-5); }
}

/* --------------------------------------------------------------------------
   20. Contact slide-over — contact opens above the page, never a new route,
       so scroll position is never lost.
   -------------------------------------------------------------------------- */

.e-scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim-dark);
  z-index: 60;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-out);
}
.e-scrim.is-open { opacity: 1; pointer-events: auto; }

.e-panel {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: min(560px, 92vw);
  z-index: 61;
  background: var(--ink-900);
  color: var(--white);
  box-shadow: var(--shadow-panel);
  transform: translateX(100%);
  transition: transform var(--dur-base) var(--ease-out);
  padding: var(--space-7);
  overflow-y: auto;
}
.e-panel.is-open { transform: translateX(0); }
.e-panel__head { display: flex; justify-content: space-between; align-items: center; padding-bottom: var(--space-8); }
.e-panel__title { margin: 0 0 var(--space-5); font: var(--weight-medium) 40px/1.04 var(--font-display); letter-spacing: var(--tracking-display); color: var(--white); max-width: 14ch; }
.e-panel__note { font: var(--type-lead); color: var(--text-on-dark-muted); margin: 0 0 var(--space-7); }
.e-panel__form { display: flex; flex-direction: column; gap: var(--space-5); }

.e-field { display: flex; flex-direction: column; gap: 8px; }
.e-field label { font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-on-dark-muted); }
.e-field input,
.e-field textarea,
.e-field select {
  font: var(--type-body);
  font-family: var(--font-body);
  color: var(--white);
  background: transparent;
  border: none;
  border-bottom: 1px solid rgba(255,255,255,.3);
  border-radius: 0;
  padding: 10px 0;
  transition: var(--transition-hover);
}
.e-field input:focus,
.e-field textarea:focus,
.e-field select:focus { outline: none; border-bottom-color: var(--ebr-yellow); }
.e-field textarea { resize: vertical; min-height: 96px; }
.e-field select option { color: var(--ink-900); }

.e-iconbtn {
  width: 40px; height: 40px;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(255,255,255,.25);
  background: transparent;
  color: var(--white);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: var(--transition-hover);
}
.e-iconbtn:hover { background: var(--white); color: var(--ink-900); border-color: var(--white); }

.e-panel__sent { display: none; }
/* The popup's consent line (26 Oct 2026) — the enquiry service will not
   accept a submission without it — and the line that reports a failure. */
.e-panel__consent { display: flex; gap: var(--space-4); align-items: flex-start; font: var(--type-small); color: var(--text-on-dark-muted); cursor: pointer; }
.e-panel__consent input { width: 18px; height: 18px; flex-shrink: 0; margin-top: 2px; accent-color: var(--ebr-yellow); }
.e-panel__err { margin: 0; min-height: 0; font: var(--type-small); color: var(--ebr-yellow); }
.e-panel__err:empty { display: none; }
.e-panel.is-sent .e-panel__form,
.e-panel.is-sent .e-panel__note,
.e-panel.is-sent .e-panel__title { display: none; }
.e-panel.is-sent .e-panel__sent { display: block; }

/* --------------------------------------------------------------------------
   21. Sticky mobile call bar
   -------------------------------------------------------------------------- */

.e-callbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 38;
  display: none;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  padding: 12px var(--page-margin);
  background: rgba(28,28,28,.94);
  -webkit-backdrop-filter: var(--blur-glass);
  backdrop-filter: var(--blur-glass);
  transform: translateY(100%);
  transition: transform var(--dur-base) var(--ease-out);
}
.e-callbar.is-visible { transform: translateY(0); }
.e-callbar span { font: var(--type-small); color: var(--text-on-dark-muted); }

/* --------------------------------------------------------------------------
   22. Motion — content fades up 16px with an 80ms stagger.
       Applied only when JS is running, so the page is readable without it.
   -------------------------------------------------------------------------- */

.js .reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
  transition-delay: calc(var(--i, 0) * var(--stagger-step));
}
.js .reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js .reveal { opacity: 1; transform: none; transition: none; }
}

/* --------------------------------------------------------------------------
   23. Banner motion

   The three full-bleed bands — hero, showreel, closing CTA — carry a little
   more motion than the rest of the page. Nothing here adds an element or moves
   one: it is the same composition, reacting. Same easing curve, same rule that
   pointer effects are pointer-only and everything stops under reduced motion.
   -------------------------------------------------------------------------- */

/* Headline lines rise out from behind a clipping edge instead of fading up.
   The lines are measured and wrapped at runtime, so the copy is untouched. */
.js .e-hero__title.is-split {
  opacity: 1;
  transform: none;
  transition: none;
}
/* The clip box is grown downward and pulled back by the same amount, so
   descenders survive. At 0.94 leading the glyphs overflow the line box, and
   a bare overflow:hidden shears the tail off every g and y. */
.e-line {
  display: block;
  overflow: hidden;
  padding-bottom: .2em;
  margin-bottom: -.2em;
}
.e-line__inner {
  display: block;
  transform: translateY(120%);
  transition: transform var(--dur-slow) var(--ease-out);
  transition-delay: calc(var(--l, 0) * 90ms);
}
.e-hero__title.is-split.is-in .e-line__inner { transform: none; }

/* The opening screen drifts up a touch as it leaves. Set from scroll position,
   clamped small — this is depth, not a ride. */
.e-hero__inner { transform: translate3d(0, var(--hero-drift, 0px), 0); }

/* --- The hero ground ------------------------------------------------------

   One soft violet aura, centred on the axis the composition now sits on, that
   breathes slowly and leans toward the cursor.

   Three earlier treatments were built and pulled — a drifting column grid, a
   two-colour bloom with film grain, and a dot matrix. Each was a *pattern*
   behind the type, and the type won every time. This one is a single form, on
   the centre line, at roughly half the strength of the old bloom, and it moves
   with the same pointer that drives the headline — so the banner reacts as one
   thing rather than as a background and a foreground doing separate tricks.
   -------------------------------------------------------------------------- */

.e-hero { position: relative; overflow: hidden; }
.e-hero > .e-shell { position: relative; z-index: 2; }

.e-hero::before {
  content: "";
  position: absolute;
  z-index: 0;
  pointer-events: none;

  /* Centred, and deliberately smaller than the band. The first attempt was a
     wide, weak wash and it just tinted the whole banner lavender; the second
     was so faint it was invisible. Concentration is what makes it read — a
     tighter form at real strength is a glow behind the headline, where the
     same ink spread wide is only a colour cast on the paper. */
  left: 50%;
  top: 44%;
  width: min(820px, 92vw);
  aspect-ratio: 1;

  background: radial-gradient(circle at center, rgba(107, 0, 246, .14), transparent 58%);
  filter: blur(40px);

  transform:
    translate(-50%, -50%)
    translate3d(var(--aura-x, 0px), var(--aura-y, 0px), 0)
    scale(var(--aura-scale, 1));
  transition: transform var(--dur-slow) var(--ease-out);

  animation: e-aura 22s var(--ease-in-out) infinite alternate;
}

/* Scale as well as opacity. Opacity alone at this strength is a change you can
   measure but not notice; the form growing and shrinking is what you see. */
@keyframes e-aura {
  from { opacity: .7;  --aura-scale: .88; }
  to   { opacity: 1;   --aura-scale: 1.12; }
}

/* --aura-scale only animates smoothly if the browser knows its type. */
@property --aura-scale {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

@media (prefers-reduced-motion: reduce) {
  .e-hero::before { animation: none; transform: translate(-50%, -50%); }
}

/* The testimonial card leans very slightly toward the pointer. */
.e-vidcard {
  transform: translate3d(var(--px, 0px), var(--py, 0px), 0);
  transition: transform var(--dur-base) var(--ease-out);
}

/* Showreel: the play ring tracks the cursor across the stage and settles back
   to centre when you leave it. */
.e-reel__play {
  transform: translate3d(var(--rx, 0px), var(--ry, 0px), 0);
  transition: transform var(--dur-base) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.e-reel__stage:hover .e-reel__play { transform: translate3d(var(--rx, 0px), var(--ry, 0px), 0) scale(1.06); }

/* The hairline field slides as the band passes, so the flat colour reads as
   depth without a gradient or a texture. */
.e-reel__stage::before {
  background-position: var(--reel-drift, 0px) 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.e-reel__stage:hover::before { opacity: 1; }

.e-reel__stamp { transition: letter-spacing var(--dur-base) var(--ease-out), color var(--dur-fast) var(--ease-out); }
.e-reel__stage:hover .e-reel__stamp { letter-spacing: .28em; color: var(--white); }

/* Closing CTA: the spark turns into place once, on arrival. The star has
   four points and so is symmetrical every 90° — the sweep has to clear two
   quarter-turns to read as a turn at all. */
.e-spark {
  transform: rotate(-190deg);
  transition: transform var(--dur-reveal) var(--ease-out);
}
.reveal.is-in .e-spark { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .e-line__inner { transform: none; transition: none; }
  .e-hero__inner,
  .e-vidcard,
  .e-reel__play,
  .e-reel__stage:hover .e-reel__play { transform: none; }
  .e-spark { transform: none; transition: none; }

  /* Note for whatever background treatment lands here next: if it loops, it
     must be switched off with `animation: none`, not left to the global rule.
     tokens/base.css caps every animation at .01ms, and on an infinite loop
     that is not "off", it is a strobe. */
}

/* --------------------------------------------------------------------------
   24. Breakpoints
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-studio { grid-template-columns: 1fr; gap: var(--space-7); }
  .e-team { grid-template-columns: repeat(3, 1fr); }
  .e-plans { grid-template-columns: 1fr; }
  .e-posts { grid-template-columns: 1fr; }
  .e-service { gap: var(--space-5) var(--space-6); }

  /* Two up. Percentages rather than a pixel width from here down, so the peek
     of the next panel stays proportional instead of vanishing on the narrow end
     of the range. */
  .e-tst { flex-basis: 46%; }
  /* Ten logos over four columns is 4 + 4 + 2. A ragged last row is normal on a
     logo wall and reads better than stretching the tracks to force it even.

     The two-row clip that used to live here belonged to the thirty-slot
     version. It comes back with the expand control if the wall grows again. */
  .e-clients { grid-template-columns: repeat(4, 1fr); }
}

@media (max-width: 860px) {
  .e-nav { display: none; }
  .e-burger { display: flex; }
  .e-callbar { display: flex; }
  body { padding-bottom: 0; }

  .e-section { padding-block: clamp(56px, 9vw, 72px); }
  .e-hero { padding-block: 48px 56px; }

  .e-vidcard { gap: 14px; align-items: flex-start; }
  .e-vidcard__thumb { flex: 0 0 128px; height: 82px; }
  .e-vidcard__play { width: 32px; height: 32px; }
  .e-vidcard__stamp { display: none; }

  /* The band used to need a taller floor here, because at 320px the play
     affordance and the foot copy finished about 9px apart. A full screen is
     comfortably more than that on any phone held upright, so the floor is
     gone and `--reel-h: 100svh` from §8 stands. Landscape is the tight case
     and is handled below. */

  /* Stays centred — it is over the picture now, not a left-aligned block in a
     band of its own. Only the gap tightens. */
  .e-reel__foot { gap: 14px; }

  /* The row stacks: window first, then the facts, then the name and arrow on
     one line. Same order of importance as desktop, and nothing is hidden
     behind a hover that a phone cannot perform. */
  /* Phones (23 Oct 2026): window, then the same left column — outcome, name,
     year — then services with the arrow beside them. */
  .e-work {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto;
    grid-template-areas: "win win" "outcome outcome" "client client" "services go";
    column-gap: var(--space-4); row-gap: var(--space-3);
    padding: var(--space-5) 0;
  }
  .e-work__win { aspect-ratio: 4 / 3; margin-bottom: var(--space-2); }

  /* Two by two. Four across at 390px shrinks the numbers to label size, which
     is the one thing a stat cannot afford. */
  .e-stats { grid-template-columns: 1fr 1fr; }
  .e-stats > * { border-bottom: var(--border-hairline); }
  .e-stats > :nth-child(2n) { border-right: none; }
  .e-stats > :nth-child(2n+1) { padding-left: 0; }
  .e-stats > :nth-last-child(-n+2) { border-bottom: none; }
  .e-stats--three { grid-template-columns: 1fr 1fr; }

  .e-handoff { flex-direction: column; align-items: stretch; gap: var(--space-4); }
  .e-handoff .e-btn { width: 100%; }

  /* Three across. Six at 390px leaves each mark far below the width a logo stays
     legible at. Ten marks make four rows here rather than two — that is fine,
     nothing is clipped. */
  .e-clients { grid-template-columns: repeat(3, 1fr); }

  /* Name, outcome, qualifier stacked in the same order they read on desktop. */
  .e-service { grid-template-columns: 1fr; gap: var(--space-4); padding: var(--space-6) 0; align-items: start; }
  /* No hover below this width, so the slot would be a permanently invisible
     104px hole in a stacked row. */
  .e-service__icon { display: none; }
  .e-service__title { font-size: 21px; max-width: none; }
  .e-service__qual { font: var(--type-copy-small); max-width: none; }
  .e-caps__grid { grid-template-columns: 1fr 1fr; }

  .e-team { grid-template-columns: 1fr 1fr; }
  .e-section__head { flex-wrap: wrap; gap: var(--space-4); }
  .e-quote { padding: var(--space-6); min-height: 0; }
  .e-quote__figure { position: static; display: block; margin-bottom: var(--space-3); }

  .e-panel { padding: var(--space-6); }
  .e-footer__lockup { padding-bottom: 84px; }
  .e-footer__bar > div { flex-direction: column; gap: 8px; }
}

/* Phone only. Tablets keep inline buttons — a 550px-wide pill reads as a bug. */
@media (max-width: 560px) {
  .e-hero__actions { flex-direction: column; align-items: stretch; width: 100%; }
  .e-hero__actions .e-btn { width: 100%; }
  .e-cta__actions { flex-direction: column; align-items: flex-start; }
  .e-cta__actions .e-btn { width: 100%; }
  .e-plans__foot .e-btn { width: 100%; }
  .e-vidcard__meta { display: none; }

  /* One up, with the next panel showing. The arrows go rather than shrink —
     the thumb already does this job, and two 44px targets next to a heading
     eat the width the heading needs. */
  .e-tst { flex-basis: 84%; min-height: 240px; }
  .e-tst__nav { display: none; }
  .e-tst__head { display: block; }
  .e-tst__title { max-width: none; }

  /* Four collapsible rows. Two columns of six at 390px meant every discipline
     wrapped and the block ran past a full screen for content that is supporting
     detail, not the sell. Collapsed, the four category names still carry the
     range and the reader opens what they came for. */
  .e-caps__grid { grid-template-columns: 1fr; gap: 0; padding-top: var(--space-4); }

  /* One rule per row, and it divides two tap targets rather than decorating. */
  .e-caps__col { border-bottom: var(--border-hairline); }

  .e-caps__head { margin: 0; }
  .e-caps__toggle { padding: var(--space-4) 0; min-height: 44px; }

  /* Everything that collapses is gated on `.js`, the class index.html sets on
     <html> before first paint — the same guard the scroll-reveal uses. Without
     it the panels stay open and the block degrades to four stacked lists, which
     matters here because only Build carries `is-open` in the markup: gate this
     on the class and a reader with JS off can see one list in four. */
  .js .e-caps__toggle { cursor: pointer; }
  .js .e-caps__toggle:hover { color: var(--ebr-purple); }
  .js .e-caps__col.is-open .e-caps__toggle { color: var(--ebr-purple); }
  .js .e-caps__icon { display: flex; align-items: center; }
  .js .e-caps__col.is-open .e-caps__icon { color: var(--ebr-purple); }
  .js .e-caps__icon .e-icon-minus { display: none; }
  .js .e-caps__col.is-open .e-caps__icon .e-icon-plus { display: none; }
  .js .e-caps__col.is-open .e-caps__icon .e-icon-minus { display: block; }

  .js .e-caps__panel {
    grid-template-rows: 0fr;
    transition: grid-template-rows var(--dur-base) var(--ease-out);
  }
  .js .e-caps__col.is-open .e-caps__panel { grid-template-rows: 1fr; }

  /* The list gap has to clear the line-height once items start wrapping. At 6px,
     two lines of one discipline sat 20px apart while two separate disciplines sat
     26px apart — not enough of a difference to tell them apart, so "Social media
     management" read as two capabilities. 12px makes a new item unambiguous. */
  .e-caps__list { gap: 12px; padding-bottom: var(--space-5); }
}

@media (max-width: 420px) {
  .e-team { grid-template-columns: 1fr 1fr; gap: var(--space-4); }
  /* The client wall used to drop to two columns here, because the old cells
     held names — "Vantage Group" needs the width. Marks do not, and three
     across still leaves each one around 90px at 390. */

  /* The width cap is released once the cell itself is narrow. The cap exists to
     stop a 7:1 wordmark swamping a square emblem across a wide cell — a 90px
     cell already does that. Holding the desktop cap on top of it put Samsung at
     9px tall, under the size its letterforms survive. */
  .e-client__mark { max-width: 92%; }
}

/* ==========================================================================
   25. About page

   Only about.html uses anything in this block. The homepage never renders a
   `.e-abt` or `.e-proc`, and `.is-onbrand` is never set on its header.
   ========================================================================== */

/* --- 25a. The header, riding a brand banner -------------------------------

   about.html opens on a full purple field that runs to the top of the screen,
   so the header has to sit *in* that field rather than as a frosted strip
   above it. The class is written into the markup and removed by script the
   moment the banner clears the header (site.js §2) — that order matters, or
   the first paint shows a pale bar over violet and then corrects itself.

   `is-scrolled` still fires at 8px and would otherwise draw its hairline
   while the header is still deep in the banner. These rules are last in the
   file, and both selectors are the same specificity, so they win.
   -------------------------------------------------------------------------- */

.e-header.is-onbrand {
  background: var(--ebr-purple);
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  border-bottom-color: transparent;
}

/* Two marks stacked, one crossfading into the other. A single <img> swapped by
   script would flash while the second file loads; both are in the document from
   the start and only opacity moves. */
.e-header__logo { position: relative; }
.e-header__logo img { transition: opacity var(--dur-fast) var(--ease-out); }
.e-header__logo .e-logo--light {
  position: absolute;
  inset: 0;
  opacity: 0;
}
.e-header.is-onbrand .e-logo--dark  { opacity: 0; }
.e-header.is-onbrand .e-logo--light { opacity: 1; }

.e-header.is-onbrand .e-nav__link { color: rgba(255, 255, 255, .66); }
.e-header.is-onbrand .e-nav__link:hover { color: var(--white); }
.e-header.is-onbrand .e-nav__link.is-active {
  color: var(--white);
  border-bottom-color: var(--ebr-yellow);
}

/* Purple on purple is not a button. The house rule — yellow only on dark or
   purple — makes the swap for us. */
.e-header.is-onbrand .e-btn--primary {
  background: var(--ebr-yellow);
  border-color: var(--ebr-yellow);
  color: var(--ink-900);
}
.e-header.is-onbrand .e-btn--primary:hover {
  background: var(--ebr-yellow-press);
  border-color: var(--ebr-yellow-press);
}

.e-header.is-onbrand .e-burger span { background: var(--white); }
.e-header.is-onbrand .e-progress { background: var(--ebr-yellow); }

/* --- 25b. Banner ----------------------------------------------------------

   After fiasco.design's studio page. Their move is a flat field of the brand's
   signature colour, one centred statement, and photographs cropped by the
   edges of the screen so the composition reads as bigger than the window.

   The field is violet, not yellow. Yellow is this system's accent — it is only
   ever allowed on dark or on purple — so a yellow field would have nothing
   left to put a call to action in. Violet is the colour that plays the part
   Fiasco's yellow plays for them.
   -------------------------------------------------------------------------- */

.e-abt {
  position: relative;
  overflow: hidden;
  background: var(--ebr-purple);
  color: var(--text-on-dark);
  display: flex;
  align-items: center;
  min-height: min(94svh, 900px);
  padding-block: clamp(96px, 12vw, 168px);
}

/* Over the tiles, and clear of them. Everything in the banner that is read
   sits in this stack; everything decorative sits behind it. */
.e-abt__inner {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-6);
  text-align: center;
}
.e-abt .e-label { justify-content: center; }

/* Deliberately a size down from the homepage's display-l. The banner has to
   hold a statement, a lead, a button and six tiles inside one screen; at the
   full 5.4vw this ran four lines and pushed the button onto the fold. 20ch is
   as wide as the lines can go before they reach in under the mid-height tiles
   at 1440. */
.e-abt__title {
  max-width: 20ch;
  font-size: clamp(34px, 4.4vw, 64px);
}
.e-abt__lead {
  max-width: 40ch;
  color: rgba(255, 255, 255, .72);
}
.e-abt__actions {
  display: flex;
  gap: 18px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  padding-top: var(--space-2);
}
.e-abt__email {
  font: var(--type-small);
  color: rgba(255, 255, 255, .72);
  border-bottom: 1px solid rgba(255, 255, 255, .3);
  transition: var(--transition-hover);
}
.e-abt__email:hover {
  color: var(--white);
  border-bottom-color: var(--ebr-yellow);
  text-decoration: none;
}

/* --- Tiles ---

   ⚠ PLACEHOLDERS. Every one of these is an empty tonal block with a label
   saying what belongs in it. There is no studio photography in the repo. Each
   tile is sized and positioned for the shot named on it, so dropping real
   images in later is a matter of putting an <img> inside the span and deleting
   the label — the composition does not move.

   Two fills only, alternating: a darker violet and a light wash of white. A
   third tone would start reading as depth, and this is a flat-colour system. */
.e-abt__tiles {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

/* The container carries `.reveal` purely to borrow the observer — its own
   16px lift is cancelled here, and the children take the entrance instead so
   each one can keep its rotation. */
.js .e-abt__tiles.reveal {
  opacity: 1;
  transform: none;
  transition: none;
}

.e-abt__tile {
  position: absolute;
  display: flex;
  align-items: flex-end;
  padding: 12px;
  border-radius: var(--radius-media);
  background: var(--ebr-purple-press);
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: rgba(255, 255, 255, .38);
  overflow: hidden;
  /* A two-line label on a 126px tile reads as a broken word, and the second
     line lands where the crop is. One line, clipped if it has to be. */
  white-space: nowrap;
}
.e-abt__tile:nth-child(even) { background: rgba(255, 255, 255, .09); }



/* The entrance animates `opacity` and `scale`; the parallax below drives
   `translate`. They are separate CSS properties, not two halves of one
   `transform` string, which is the whole reason this works: a transform
   holding both would have to carry the scroll offset through a 720ms
   transition, and every scroll frame would arrive three-quarters of a second
   late. `translate` is deliberately absent from the transition list, so it
   lands on the frame it is written. */
.js .e-abt__tiles .e-abt__tile {
  opacity: 0;
  scale: .96;
  transition: opacity var(--dur-slow) var(--ease-out),
              scale var(--dur-slow) var(--ease-out);
  transition-delay: calc(var(--i, 0) * var(--stagger-step));
}
.js .e-abt__tiles.is-in .e-abt__tile {
  opacity: 1;
  scale: 1;
}

/* Parallax. `--py` is written by site.js §9d; with no script it stays 0 and
   the tiles simply sit where they are placed. */
.e-abt__tile { translate: 0 var(--py, 0px); }

/* Seven positions, counted and measured off the Fiasco reference rather than
   arranged by eye: one cropped by the top edge, two off the right, two off the
   left, three along the bottom. Their proportions vary as much as their
   placement — narrow portrait slivers against wide landscape crops — which is
   what stops a scatter of rectangles reading as a grid that slipped.

   Negative offsets are the point. A tile that runs off the edge reads as a
   photograph continuing past the window; a tile tucked neatly inside the page
   margin reads as a card.

   Tile 1 is cropped by the top of the banner, which is what `overflow: hidden`
   on `.e-abt` is for. It has to clear that edge by a real margin rather than a
   few pixels: the header is sticky, so it takes flow space *above* the banner
   rather than floating over it, and a tile that only just pokes past the edge
   ends up relying on the header's violet to hide its rounded corners. That
   works right up until the header is not violet — and then it reads as a card
   that happens to be near the top. At -14% the crop is the banner's, and the
   corners are nowhere near it.

   Nothing is rotated. Fiasco's are all axis-aligned; an earlier pass here gave
   them a 3–5° tilt, which was invention rather than reference and read as a
   pile of polaroids. The 18px corner stays — that is the design system's media
   radius, and the system governs the visuals even when the reference does not.

   The centre column is kept clear from 30% down: the label, the statement, the
   lead and the button all live there. The three bottom crops were measured
   against the button rather than eyeballed — tile 6 sits directly under it and
   clears its lower edge by 65px at 1440. */

/* `--d` is the parallax depth: the fraction of the page's own scroll each tile
   gives back, so a higher number lags further behind and reads as nearer the
   viewer. They are deliberately unrelated to size — a big tile drifting slowly
   and a small one drifting fast is what separates the plane, and matching
   depth to size just makes the whole thing zoom. */
.e-abt__tile--1 { left: 25%;  top: -14%;    width: clamp(150px, 19vw, 275px); aspect-ratio: 4/3; --i: 0; --d: .07; }
.e-abt__tile--2 { right: -3%; top: 10%;     width: clamp(116px, 15vw, 215px); aspect-ratio: 3/2; --i: 1; --d: .13; }
.e-abt__tile--3 { left: -5%;  top: 33%;     width: clamp(88px,  11vw, 155px); aspect-ratio: 3/4; --i: 2; --d: .05; }
.e-abt__tile--4 { right: -2%; top: 58%;     width: clamp(102px, 13vw, 185px); aspect-ratio: 3/4; --i: 3; --d: .16; }
.e-abt__tile--5 { left: -5%;  bottom: -8%;  width: clamp(146px, 19vw, 270px); aspect-ratio: 3/2; --i: 4; --d: .09; }
.e-abt__tile--6 { left: 37%;  bottom: -6%;  width: clamp(126px, 16vw, 225px); aspect-ratio: 3/2; --i: 5; --d: .18; }
.e-abt__tile--7 { left: 67%;  bottom: -5%;  width: clamp(108px, 14vw, 195px); aspect-ratio: 4/3; --i: 6; --d: .11; }

/* Cropped by the bottom of the screen, so the label goes to the top of the
   tile — at the bottom it sits below the crop, and a placeholder nobody can
   read is just a rectangle. */
.e-abt__tile--5,
.e-abt__tile--6,
.e-abt__tile--7 { align-items: flex-start; }

/* Bleeds off the left, so the label goes to the far corner or it bleeds too. */
.e-abt__tile--3 { justify-content: flex-end; }
.e-abt__tile--5 { justify-content: flex-end; }

/* --- 25c. Story ----------------------------------------------------------- */

.e-story {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-8) var(--space-7);
  align-items: start;
}
.e-story__body p {
  margin: 0;
  font: var(--type-copy);
  color: var(--text-copy);
  max-width: 48ch;
}
/* `p + p`, not `> * + *`. The reset above sets the shorthand, which carries a
   margin-top of its own at higher specificity — a universal sibling selector
   loses to it and the paragraphs run together. */
.e-story__body p + p { margin-top: var(--space-5); }

/* --- 25d. Process ---------------------------------------------------------

   UNUSED as of 24 Aug 2026. The About page's section 02 became "Four beliefs"
   in the Metalab arrangement (25f below) and nothing else on the site renders
   `.e-proc`. Kept because the block itself is sound and the framework may want
   a process grid back — but if it is still unused at ship, delete it.

   One rule across the top of the row and nothing between the steps. The
   numbers are already four hard left edges — a divider on each one would be
   the second thing doing the same job.
   -------------------------------------------------------------------------- */

.e-proc {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-6);
  border-top: var(--border-hairline);
  padding-top: var(--space-6);
}
.e-ground-ink .e-proc { border-top: var(--border-on-dark); }

.e-proc__n {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  color: var(--ebr-purple);
  margin-bottom: var(--space-4);
}
.e-ground-ink .e-proc__n { color: var(--ebr-yellow); }

.e-proc__title {
  margin: 0 0 10px;
  font: var(--type-lead);
  color: var(--text-strong);
}
.e-ground-ink .e-proc__title { color: var(--white); }

.e-proc__body {
  margin: 0;
  font: var(--type-copy-small);
  color: var(--text-copy);
}
.e-ground-ink .e-proc__body { color: var(--text-on-dark-muted); }

/* --- 25e. About-page responsive ------------------------------------------- */

@media (max-width: 1080px) {
  /* The tiles shrink with the viewport but 11px type does not, so the labels
     start outgrowing the tiles they sit in from about here down. */
  .e-abt__tile { padding: 9px; font-size: 9px; }
  /* Five. The narrow left sliver and the bottom-centre crop are the two that
     get squeezed first — 3 has nowhere to go once the page margin closes up,
     and 6 sits directly under the button. */
  .e-abt__tile--3, .e-abt__tile--6 { display: none; }
  .e-proc { grid-template-columns: 1fr 1fr; gap: var(--space-6) var(--space-7); }
}

@media (max-width: 860px) {
  .e-story { grid-template-columns: 1fr; gap: var(--space-6); }
  .e-story__body p { max-width: 60ch; }
}

@media (max-width: 700px) {
  /* Three. The statement is much taller here — five or six lines — so the
     corridor the tiles have to stay out of takes up most of the screen. */
  .e-abt { min-height: min(86svh, 760px); }
  .e-abt__tile--4, .e-abt__tile--7 { display: none; }
  .e-abt__tile--1 { left: 20%; top: -5%; }
  .e-abt__tile--2 { top: 7%; }
}

@media (max-width: 560px) {
  /* The one place this deliberately stops following the reference. Fiasco's
     top tile is cropped by the top of the screen; here the header is a solid
     violet bar 58px deep, and at phone height that bar eats the whole visible
     part of a top-cropped tile — it would simply not be there. So tile 1 moves
     to the left edge and crops sideways instead, which is the same idea in the
     only direction that has room.

     Label sizing is already handled at 1080 and carries down. */
  /* The bleed is deliberately shallow here. At -7% with a 120px tile, the
     right-aligned label "STUDIO · DUBAI" is wider than what is left on screen
     and loses its first two letters — a cropped photograph reads as a crop, a
     cropped word reads as a typo. */
  .e-abt__tile--1 { left: -4%;  top: 4%;    width: 132px; aspect-ratio: 4/3; justify-content: flex-end; }
  .e-abt__tile--2 { right: -5%; top: 8%;    width: 112px; }
  .e-abt__tile--5 { left: -6%;  bottom: -4%; width: 124px; }
  .e-proc { grid-template-columns: 1fr; gap: var(--space-5); }
}

/* --- 25f. Four beliefs — the Metalab arrangement --------------------------

   Reference: mobbin.com/sites/sections/26e93485-a46d-464b-b795-f4eb9101403c

   Headline held left with the link sitting on its last line, then four tall
   columns divided by full-height rules, each one bottom-anchored so the weight
   falls at the base and the air collects above it. Dark field.

   The rules run vertically and nothing runs across the top. That is the whole
   difference between this and `.e-proc` above, which does the opposite for the
   same reason — one set of rules per grid, never both.
   -------------------------------------------------------------------------- */

.e-bel__head {
  display: flex;
  align-items: flex-end;          /* link lands level with the headline's last line */
  justify-content: space-between;
  gap: var(--space-6);
  margin-bottom: var(--space-9);  /* the air Metalab puts above the columns */
}
.e-bel__lead { max-width: 19ch; }
.e-bel__link { flex-shrink: 0; padding-bottom: 6px; }

.e-bel {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}

.e-bel__col {
  display: flex;
  flex-direction: column;
  justify-content: space-between;  /* number pinned top, claim + cost pinned bottom */
  gap: var(--space-8);
  min-height: clamp(340px, 30vw, 460px);
  padding-inline: var(--space-6);
  /* The reference's rules run about 40px above the number and 35px below the
     last line, so they read as dividing the whole band rather than as brackets
     drawn round each block of text. */
  padding-block: 38px;
  border-inline-start: var(--border-on-dark);
}
.e-bel__col:first-child { padding-inline-start: 0; border-inline-start: 0; }
.e-bel__col:last-child  { padding-inline-end: 0; }

/* Number and the belief's looping icon, held together at the top. */
.e-bel__top { display: flex; flex-direction: column; gap: var(--space-5); }
.e-bel__icon {
  width: 96px;
  height: 96px;
  /* The clip's ground is the section's ink; a soft edge fade means a one-shade
     difference from the video encoder can never draw a box round it. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 4%, #000 96%, transparent), linear-gradient(to bottom, transparent, #000 4%, #000 96%, transparent);
  -webkit-mask-composite: source-in;
          mask-image: linear-gradient(to right, transparent, #000 4%, #000 96%, transparent), linear-gradient(to bottom, transparent, #000 4%, #000 96%, transparent);
          mask-composite: intersect;
}
.e-bel__icon video { display: block; width: 100%; height: 100%; object-fit: cover; background: var(--ink-900); }
@media (max-width: 700px) { .e-bel__icon { width: 76px; height: 76px; } }

.e-bel__n {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  color: var(--ebr-yellow);
}

.e-bel__stack { max-width: 34ch; }

.e-bel__title {
  margin: 0 0 var(--space-3);
  font: var(--weight-medium) var(--size-body-l)/1.25 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--white);
  text-wrap: pretty;
}

.e-bel__body {
  margin: 0;
  font: var(--type-copy-small);
  color: var(--text-on-dark-muted);
}

/* Motion. The claim lands, then the sentence naming what it costs us arrives a
   beat behind it — which is the order the block has to be read in to work.

   The column carries `.reveal` only to borrow the observer; it never moves
   itself, so the children can be timed independently of the column stagger. */

.js .e-bel__col.reveal { opacity: 1; transform: none; transition: none; }

.js .e-bel__col .e-bel__n,
.js .e-bel__col .e-bel__icon,
.js .e-bel__col .e-bel__title,
.js .e-bel__col .e-bel__body {
  opacity: 0;
  translate: 0 14px;
  transition: opacity var(--dur-slow) var(--ease-out),
              translate var(--dur-slow) var(--ease-out);
  transition-delay: calc(var(--i, 0) * var(--stagger-step));
}
/* The beat. 180ms is long enough to read as a second event and short enough
   that nobody waiting on it notices they are waiting. */
.js .e-bel__col .e-bel__body {
  transition-delay: calc(var(--i, 0) * var(--stagger-step) + 180ms);
}

.js .e-bel__col.is-in .e-bel__icon,
.js .e-bel__col.is-in .e-bel__n,
.js .e-bel__col.is-in .e-bel__title,
.js .e-bel__col.is-in .e-bel__body { opacity: 1; translate: none; }

@media (prefers-reduced-motion: reduce) {
  .js .e-bel__col .e-bel__n,
  .js .e-bel__col .e-bel__icon,
  .js .e-bel__col .e-bel__title,
  .js .e-bel__col .e-bel__body { opacity: 1; translate: none; transition: none; }
}

/* --- 25g. Four beliefs, responsive ---------------------------------------- */

@media (max-width: 1080px) {
  /* Two up. The rule now divides the pair, so it only belongs between them. */
  .e-bel { grid-template-columns: 1fr 1fr; row-gap: var(--space-9); }
  .e-bel__col { min-height: 0; gap: var(--space-7); }
  .e-bel__col:nth-child(odd)  { padding-inline-start: 0; border-inline-start: 0; }
  .e-bel__col:nth-child(even) { padding-inline-end: 0; }
  .e-bel__head { margin-bottom: var(--space-8); }
  .e-bel__lead { max-width: 24ch; }
}

@media (max-width: 700px) {
  /* Stacked. A vertical rule divides nothing here, so it becomes a horizontal
     one — same rule, doing the same job, turned ninety degrees. */
  .e-bel { grid-template-columns: 1fr; row-gap: 0; }
  .e-bel__col {
    gap: var(--space-4);
    padding-inline: 0;
    border-inline-start: 0;
    border-block-start: var(--border-on-dark);
    padding-block: var(--space-6);
  }
  .e-bel__col:first-child { border-block-start: 0; padding-block-start: 0; }
  .e-bel__stack { max-width: none; }
  .e-bel__head { display: block; }
  .e-bel__link { margin-top: var(--space-5); padding-bottom: 0; }
}

/* --- 25h. The pinned rail -------------------------------------------------

   Metalab's row is dragged sideways. This one is not: the section pins to the
   viewport and the page scroll drives the row across, so all four beliefs are
   covered before the page is allowed to move on. Dropbox does the same thing
   with its values (mobbin.com/sites/sections/41e782bc-…) — wide panels on a
   dark field, one or two in view at a time, rather than four narrow columns
   all visible at once.

   That reference is also why the columns got wider. Four 380px columns barely
   overflowed a 1440 viewport, which left about 150px of travel — a pin nobody
   would notice. At 38vw each belief gets a real share of the screen and the
   row runs about half a viewport wider than the glass, which is what makes the
   pin worth having.

   EVERY RULE HERE IS SCOPED TO `.is-pinned`, and `is-pinned` is added by
   script — never by a media query, and only once it has measured that the row
   actually overflows. That scoping is the whole fallback story: below 1081px,
   under reduced motion, and with JS off, not one declaration in this block
   applies and 25f's four-column grid renders instead. Scope any of it wider
   and the fallback silently becomes a horizontal scroller with two beliefs
   parked off-screen behind an overlay scrollbar, which is what it was before
   this comment existed to prevent it.
   -------------------------------------------------------------------------- */

/* The stage is a flex column and the shell carries `margin: 0 auto`. Auto
   margins on the cross axis cancel the stretch a flex item would otherwise
   get, so without this the shell sizes to its own max-content — which is the
   row — and then stops at `--max-width`. It sat at a flat 1440px on every
   viewport, and `body { overflow-x: hidden }` clipped the difference in
   silence: at 1200 the last belief and the "What we do" link were both off the
   right of the screen with no scrollbar to say so. */
.e-bel__pin.is-pinned .e-bel__stage > .e-shell { width: 100%; }

.e-bel__pin.is-pinned .e-bel__rail {
  /* Full-bleed so the row is cut by the screen edge rather than by the page
     margin — that cut is what says there is more to the right. The padding
     puts the first column back on the shell's left edge, and the last one back
     on its right edge at the end of the travel. */
  margin-inline: calc(var(--page-margin) * -1);
  padding-inline: var(--page-margin);
  /* Script drives the row, so the rail must not also be a scroller — two
     things moving one row would fight and desync the pin. */
  overflow: hidden;
}

/* Columns stop dividing the width and start setting it — this is what makes
   the row wider than the screen, and therefore what gives the pin its travel. */
.e-bel__pin.is-pinned .e-bel {
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: clamp(360px, 38vw, 620px);
  width: max-content;
  will-change: transform;
}

/* The section is as tall as one screen, plus the distance the row has to
   travel, plus a hold at the end.

   The hold is what makes the fourth belief readable. The row only has it fully
   on screen for the last stretch of the travel, so without a hold there are
   about ninety pixels of scroll between belief 04 arriving complete and the
   page moving on — one flick of a trackpad. The hold keeps the stage stuck
   after the row has stopped, so 04 sits still and finished for long enough to
   be read rather than glimpsed.

   Both are measured and written by script. The 0 fallbacks mean that if the
   script never runs the section is one screen tall and nothing sticks to
   anything. */
.e-bel__pin.is-pinned {
  padding-block: 0;
  height: calc(100svh + var(--bel-travel, 0px) + var(--bel-hold, 0px));
}

.e-bel__pin.is-pinned .e-bel__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* The top pad clears the sticky header, which floats over this field. */
  padding-block: clamp(92px, 12vh, 136px) clamp(44px, 7vh, 80px);
}

/* The head has to fit on one screen with the columns, so it gives up some of
   the air 25g gives it and the headline is allowed a wider measure — two lines
   instead of three. Vertical units throughout, because on this section the
   constraint is the height of the glass, not its width. */
.e-bel__pin.is-pinned .e-section__head { margin-bottom: clamp(14px, 2.5vh, 28px); }
.e-bel__pin.is-pinned .e-bel__head { margin-bottom: clamp(22px, 4vh, 52px); }
.e-bel__pin.is-pinned .e-bel__lead { max-width: 30ch; }

/* The columns fill whatever height is left rather than forcing 460px of it. */
.e-bel__pin.is-pinned .e-bel__col { min-height: 0; }

/* The reveal is left exactly as 25f sets it. An earlier version zeroed the
   column stagger on the theory that arrival was already staggered — it is not:
   three of the four columns are on screen the moment the pin engages, so all
   three would have landed at once. 25f's 80ms cascade is the right motion for
   them, and belief 04 still arrives on its own beat afterwards. */

/* --- 25j. Vision, mission, promise — Windsurf field, Tines panels ---------

   Two references mixed:
     · Windsurf — the deep saturated field, a centred statement, a narrow
       centred measure under it, and a lot of air. No gradient fan: this
       system has no gradients and never will.
     · Tines — three panels, copy held at the top, one large word anchoring
       the bottom of each.

   The panels share one field rather than taking Tines' three pastels. Three
   near-identical violets would read as an accident; the three words are what
   separates these panels. It is one line here if that call turns out wrong.

   No motion beyond the standard fade. This is the still point of the page —
   the three sentences we most want believed — and anything that slides or
   counts here reads as decoration on a promise.
   -------------------------------------------------------------------------- */

.e-label--centre { justify-content: center; }

.e-vmp__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
  text-align: center;
  margin-bottom: var(--space-9);
}
.e-vmp__lead {
  max-width: 22ch;
  text-wrap: balance;
}

.e-vmp {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-6);
}

.e-vmp__panel {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-9);
  min-height: clamp(320px, 26vw, 400px);
  padding: var(--space-6);
  border-radius: var(--radius-media);
  /* A wash, not a second hue — the same way the banner tiles sit on violet. */
  background: rgba(255, 255, 255, .08);
}

.e-vmp__top { display: flex; flex-direction: column; gap: var(--space-4); }

.e-vmp__claim {
  margin: 0;
  font: var(--weight-medium) var(--size-body-l)/1.3 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--white);
  text-wrap: pretty;
}

.e-vmp__body {
  margin: 0;
  font: var(--type-copy-small);
  color: var(--text-on-dark-muted);
}

/* Tines anchors each card on one large word. Where it puts an illustration
   opposite, this puts the index — which orders the three, rather than just
   filling the corner. */
.e-vmp__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
}

.e-vmp__word {
  font: var(--weight-medium) var(--size-display-m)/1 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--white);
}

/* Tines anchors the opposite corner with a small illustration. Ours is one of
   the three 3D marks, rendered flat at build time — see the README. Explicit
   width/height so the panel never reflows as they decode. */
.e-vmp__icon {
  display: block;
  width: 88px;
  height: 88px;
  flex-shrink: 0;
  /* Nudged down so the object sits ON the word's baseline rather than the
     transparent margin of its own box. */
  margin-bottom: -6px;
}

/* --- The marks turn toward the pointer ------------------------------------

   These are flat renders of solid objects, so the tilt is a CSS rotation of a
   picture rather than a real camera move. That is why the range is deliberately
   short — ±15° reads as the object turning to face you, and anything wider
   starts reading as a photograph being bent.

   This is the one thing that moves in this section, and it is worth being
   precise about why that does not contradict the note above. Scroll-triggered
   motion happens TO a reader; this happens BECAUSE of one. Nothing moves until
   a pointer is inside the panel, and it is the only reward for going near it.

   The panel owns the perspective so the rotation reads as depth rather than as
   a skew, and the origin sits below centre so the object pivots on its base
   like something standing on a shelf.
   -------------------------------------------------------------------------- */

.e-vmp__panel { perspective: 620px; }

.e-vmp__icon {
  transform-origin: 50% 72%;
  transform: rotateY(0deg) rotateX(0deg) translateZ(0);
  /* Long on the way back: leaving should feel like the object settling, not
     like it snapping to attention. The short duration is swapped in only while
     the pointer is actually inside. */
  transition: transform var(--dur-base) var(--ease-out);
}
.js .e-vmp__panel.is-tilting .e-vmp__icon {
  transition-duration: var(--dur-instant);
  transform: rotateY(var(--ry, 0deg)) rotateX(var(--rx, 0deg)) translateZ(16px);
}

@media (prefers-reduced-motion: reduce) {
  .e-vmp__icon,
  .js .e-vmp__panel.is-tilting .e-vmp__icon {
    transform: none;
    transition: none;
  }
}

/* --- 25k. Vision, mission, promise — responsive --------------------------- */

@media (max-width: 1080px) {
  .e-vmp { gap: var(--space-5); }
  .e-vmp__panel { padding: var(--space-5); gap: var(--space-7); }
}

@media (max-width: 860px) {
  /* One column. Three of these stacked is tall, but a two-up leaves an orphan
     and the word is the point — it needs the width to stay large. */
  .e-vmp { grid-template-columns: 1fr; }
  .e-vmp__icon { width: 76px; height: 76px; }
  .e-vmp__panel { min-height: 0; gap: var(--space-6); }
  .e-vmp__head { margin-bottom: var(--space-8); }
  .e-vmp__lead { max-width: 26ch; }
}

/* --- 25l. Origin — the Circle arrangement --------------------------------

   Reference: mobbin.com/sites/sections/e87913fa-9974-4209-9bce-9d930dec8fd4

   One wide photograph, then a four-stat row with NO dividers at all, then the
   statement. The unruled row is the whole borrow: on a site where hairlines
   carry every other grid, a row held together by space alone reads as a
   deliberate quiet rather than a missing rule.

   One departure: Circle centres each stat in its column. Ours stay flush left,
   because `.e-stats` is built so the first cell lines up with the section
   label above it, and centring would trade that alignment for a flourish.
   -------------------------------------------------------------------------- */

.e-orig__shot {
  position: relative;
  margin: 0 0 var(--space-8);
  aspect-ratio: 2.6 / 1;
  border-radius: var(--radius-media);
  background: var(--ink-100);
  display: grid;
  place-items: center;
  overflow: hidden;
}
/* The picture fills the frame; the centre carries the subject so the 3:2
   phone crop keeps it. */
/* An image with its words baked in must never be cropped: hold the frame
   to the picture's own ratio at every width. */
.e-orig__shot.e-orig__shot--keep { aspect-ratio: 2020 / 778; }
.e-orig__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; display: block; }
/* Placeholder marker. Goes when the photograph arrives; the chip stays. */
.e-orig__todo {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
/* Circle's caption sits inside the frame, bottom right, as a small pill. */
.e-orig__chip {
  position: absolute;
  right: var(--space-4);
  bottom: var(--space-4);
  padding: 7px 12px;
  border-radius: var(--radius-md);
  background: var(--white);
  font: var(--type-small);
  color: var(--text-strong);
}

/* --- The unruled stat row ------------------------------------------------- */

.e-stats--open {
  border-top: none;
  gap: var(--space-6);
}
.e-stats--open > * {
  border-right: none;
  padding: 0;
}
/* Centred in equal columns, which is what Circle actually does and what this
   row needed all along.

   It was flush left first, to line the opening number up with the section
   label. That left the last number stranded ~200px short of the picture's
   right edge, so the row read as narrower than the container above it. The
   next attempt turned only the last cell out to the right — which fixed the
   edges and broke the rhythm, because three cells then sat hard left and one
   hard right.

   Centring solves both at once: four equal columns, each stat on its column's
   axis, so the gaps between them are identical and the set is symmetrical
   about the same centre line as the photograph. The cost is that the opening
   number no longer sits under the section label, and that is the right trade —
   a row of four reads as a row, not as four things that each begin somewhere. */
.e-stats--open > * {
  align-items: center;
  text-align: center;
}
/* `.e-stats > :first-child` zeroes the left pad already; with no padding at all
   on any cell that rule has nothing left to do, and every cell now starts on
   its own column edge. */
.e-stats--big .e-stat__value { font-size: var(--size-display-l); }
.e-stats--big .e-stat__label { padding-top: var(--space-4); }

/* --- The story and the founder quote -------------------------------------- */

.e-orig__story { margin-top: var(--space-9); }

.e-orig__quote {
  margin: var(--space-8) 0 0;
  max-width: 40ch;
}
.e-orig__quote blockquote {
  margin: 0;
  font: var(--weight-regular) var(--size-title)/1.35 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
  text-wrap: pretty;
}
.e-orig__quote figcaption { margin-top: var(--space-5); }

/* --- 25m. Origin, responsive ---------------------------------------------- */

@media (max-width: 1080px) {
  .e-stats--open { gap: var(--space-5); }
  .e-orig__shot { aspect-ratio: 2.2 / 1; }
}

@media (max-width: 700px) {
  /* Two up. Four stats at phone width give each number about 70px, which is
     narrower than the numbers themselves. */
  .e-stats--open { grid-template-columns: 1fr 1fr; gap: var(--space-6) var(--space-5); }
  /* Still centred, just two columns of it. */
  .e-orig__shot { aspect-ratio: 3 / 2; }
  .e-orig__story { margin-top: var(--space-8); }
  /* 21 Oct 2026, Adrian: the four Origin figures (Founded / People / Projects
     at a time / Projects shipped) are hidden on phones only. */
  #origin .e-stats { display: none; }
}

/* --- 25n. Industries — the Zipline arrangement ---------------------------

   Reference: mobbin.com/sites/sections/0e905104-e78b-471c-b746-e7b43e59ba48

   A small label held far left, the sectors set as type in columns, bracketed
   by a rule top and bottom. No cards, no icons.

   The top rule is free — `.e-ground-paper + .e-ground-paper` above already
   draws a hairline between two paper sections, and section 04 is paper. Only
   the bottom one is set here, and it also separates this from the white logo
   wall underneath, which would otherwise meet it on an invisible edge.
   -------------------------------------------------------------------------- */

.e-ind__section { border-bottom: var(--border-hairline); }

.e-ind {
  display: grid;
  grid-template-columns: 320px 1fr;
  gap: var(--space-8);
  align-items: start;
}

.e-ind__lead { display: flex; flex-direction: column; gap: var(--space-5); }
.e-ind__head {
  margin: 0;
  font: var(--weight-medium) var(--size-body-l)/1.3 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
  text-wrap: pretty;
}
.e-ind__sub { margin: 0; font: var(--type-copy-small); color: var(--text-copy); }

/* Zipline leaves this column empty. At forty-one names the right side is tall
   and the empty half became a long drop of nothing, so it carries the picture.
   ⚠ Placeholder — the photograph does not exist yet. */
.e-ind__shot {
  position: relative;
  margin: var(--space-3) 0 0;
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-media);
  background: var(--ink-100);
  display: grid;
  place-items: center;
  overflow: hidden;
}
.e-ind__todo {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: center;
  padding: 0 12px;
}

/* The wall. Names only — no bullets, no rules between them, nothing but the
   list. Column flow rather than a grid so the names fill top-to-bottom and the
   count can change without leaving a hole. */
.e-ind__names {
  margin: 0;
  padding: 0;
  list-style: none;
  columns: 3;
  column-gap: var(--space-7);
}
.e-ind__names li {
  break-inside: avoid;
  margin-bottom: 14px;
  font: var(--weight-medium) clamp(17px, 1.5vw, 21px)/1.25 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
}

/* --- 25o. Industries, responsive ------------------------------------------ */

@media (max-width: 1080px) {
  .e-ind { grid-template-columns: 260px 1fr; gap: var(--space-7); }
  .e-ind__names { columns: 2; }
}

@media (max-width: 860px) {
  /* The standfirst goes full width above the wall rather than shrinking to a
     sliver beside it, and the picture moves out of the text column. */
  .e-ind { grid-template-columns: 1fr; gap: var(--space-7); }
  .e-ind__sub { max-width: 56ch; }
  .e-ind__shot { aspect-ratio: 16 / 9; }
}

@media (max-width: 560px) {
  .e-ind__names { columns: 2; column-gap: var(--space-5); }
}

/* ==========================================================================
   26. Services page

   Only services.html renders anything in this block. Nothing here is shared
   with the homepage or about.html.
   ========================================================================== */

/* --- 26a. The header, riding an ink banner --------------------------------

   Same mechanism as §25a and no new script: `data-header-brand` sits in the
   markup, and site.js §2 only ever takes `is-onbrand` away once the banner has
   cleared the header. The single difference is the field behind it, so
   `is-ondark` is written alongside `is-onbrand` and changes nothing but the
   fill. Everything else §25a already sets — white lockup, yellow active
   underline, yellow button, white burger, yellow progress rule — is correct on
   ink as it stands, because yellow is allowed on dark.

   Both classes are required by the selector, so when the banner scrolls past
   and `is-onbrand` goes, this stops applying on its own. `is-ondark` being
   left behind in the class list does nothing.
   -------------------------------------------------------------------------- */

/* Transparent, not filled. The field behind it is a picture and the header
   should be standing in front of the picture rather than covering the top of
   it — so the fill and the blur both come off, and everything in the header
   is then sitting directly on the nebula.

   That costs contrast, so the numbers were checked before the fill was
   removed. Measured against the brightest one per cent of the loop's top
   strip across every frame: the white wordmark is 5.50:1 and the yellow
   button 4.09:1, both fine. The nav links at their usual `.66` white were
   2.31:1, which is not — so on this header alone they run at `.88`, which
   measures 4.65:1. That is the one value here that is a correction rather
   than a translation.

   EVERYTHING BELOW FOLLOWS `--field-wash`, the same 0-to-1 the background runs
   on. This is the part that is easy to get wrong: `is-onbrand` stays on until
   the field's bottom clears the header, which is well AFTER the wash has
   finished — so a header that only knew about `is-onbrand` would be showing
   white type on a white background for the last stretch of the field. It has
   to be on the wash's curve, not the field's.

   `color-mix` rather than a class swapped at a threshold, because a threshold
   would be a visible flip halfway through a transition whose whole point is
   that it is not one. */
.e-header.is-onbrand.is-ondark {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* The glass, coming back. On a pseudo so its opacity can be driven without
   touching the header's own compositing, and behind the content rather than
   over it. */
.e-header.is-onbrand.is-ondark::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--glass-fill);
  -webkit-backdrop-filter: var(--blur-glass);
  backdrop-filter: var(--blur-glass);
  opacity: var(--field-wash, 0);
  pointer-events: none;
}

/* Everything the wash drives has its transition removed, and that is not an
   oversight. These properties normally ease over `--dur-fast` because they
   change on a state flip; here they change on a scroll position, a hundred
   times across the zone. Easing each of those steps makes the header trail the
   background it is supposed to be matching — the scroll IS the animation, and
   a second one laid over it only smears it.

   The cost is that hover on these four nav links is instant rather than eased,
   on this header alone. Worth it: the reader is looking at the header's
   relationship to the field, not at a 220ms ease on a link they are pointing
   at. */
.e-header.is-onbrand.is-ondark,
.e-header.is-onbrand.is-ondark .e-nav__link,
.e-header.is-onbrand.is-ondark .e-btn--primary,
.e-header.is-onbrand.is-ondark .e-burger span,
.e-header.is-onbrand.is-ondark .e-header__logo img { transition: none; }

/* The two marks are already a crossfade; this just hands the crossfade to the
   wash instead of to the on/off class. */
.e-header.is-onbrand.is-ondark .e-logo--light { opacity: calc(1 - var(--field-wash, 0)); }
.e-header.is-onbrand.is-ondark .e-logo--dark { opacity: var(--field-wash, 0); }

.e-header.is-onbrand.is-ondark .e-nav__link {
  color: color-mix(in srgb, var(--text-muted) calc(var(--field-wash, 0) * 100%), rgba(255, 255, 255, .88));
}
.e-header.is-onbrand.is-ondark .e-nav__link:hover,
.e-header.is-onbrand.is-ondark .e-nav__link.is-active {
  color: color-mix(in srgb, var(--ink-900) calc(var(--field-wash, 0) * 100%), var(--white));
}
.e-header.is-onbrand.is-ondark .e-nav__link.is-active {
  border-bottom-color: color-mix(in srgb, var(--ebr-purple) calc(var(--field-wash, 0) * 100%), var(--ebr-yellow));
}

.e-header.is-onbrand.is-ondark .e-burger span {
  background: color-mix(in srgb, var(--ink-900) calc(var(--field-wash, 0) * 100%), var(--white));
}
.e-header.is-onbrand.is-ondark .e-progress {
  background: color-mix(in srgb, var(--ebr-purple) calc(var(--field-wash, 0) * 100%), var(--ebr-yellow));
}

/* Yellow on the nebula, purple once the ground is paper — which is the same
   rule the rest of the site follows, just arrived at continuously. */
.e-header.is-onbrand.is-ondark .e-btn--primary {
  background: color-mix(in srgb, var(--ebr-purple) calc(var(--field-wash, 0) * 100%), var(--ebr-yellow));
  border-color: color-mix(in srgb, var(--ebr-purple) calc(var(--field-wash, 0) * 100%), var(--ebr-yellow));
  color: color-mix(in srgb, var(--white) calc(var(--field-wash, 0) * 100%), var(--ink-900));
}

/* --- 26b. Banner — after mymind -------------------------------------------

   mymind opens on a single word, "Why?", set to fill the screen, with the
   answer running underneath as ordinary body copy in a column pushed in from
   the left. The question is the whole composition: no button, no image, and
   nothing else on the screen competing for the first look. That grammar is
   what this page needed — the lo-fi asks for a question rather than a claim,
   and for the router below it to be the only thing worth clicking.

   Three departures from the reference, each deliberate:

   1. No orb. Theirs is a soft blurred gradient behind the word. This system
      uses flat colour fields and takes its depth from hairlines and colour,
      never from a blur or a shadow. The colour that would have lived in the
      orb goes into the question mark instead — one yellow glyph on ink, which
      is what the accent exists for.

   2. Ink, not violet. Violet is spoken for on this page: the router directly
      below uses it to mean "selected", and purple does one job per screen.
      Ink keeps it free, and it also stops this page opening on the same violet
      field as about.html.

   3. No question-word nav. mymind runs What / Why / How as dotted links across
      the top. Six symptom links up here would be six calls to action on the
      one section the lo-fi says must have none.
   -------------------------------------------------------------------------- */

/* `.e-ground-ink` is REQUIRED, and it now sits on `.e-svcfield` rather than
   here — see 26d. The fill is only half the job: the on-dark type colours for
   .e-display, .e-lead and .e-label all hang off the ground class, and without
   it somewhere above this section the heading renders ink on ink and
   disappears. That is a build cycle it has already cost once.

   No `background` here any more either. The field behind both sections is one
   surface, and a fill on this section would be a second surface sitting on top
   of it — invisible while both are ink, and the seam the moment the field
   carries anything else. */
.e-svcb {
  position: relative;
  color: var(--text-on-dark);
  display: flex;
  align-items: center;
  /* Deliberately short of a full screen: the top edge of the router has to show
     under the fold, because a question with nothing visible answering it is
     just a headline.

     Two things this has to get right, and the first draft got both wrong.
     `svh` is the *viewport*, which includes the 66px the sticky header sits
     over — so the header has to come off, or the section is a header taller
     than it thinks. And the padding has to be small enough that `min-height` is
     the binding constraint; at the previous clamp the padding alone made the
     section taller than the minimum, so the minimum never did anything and the
     banner ran 54px past the fold on a 722px window.

     The two now hand over to each other: on a tall window `min-height` binds
     and the padding is irrelevant, on a short one the padding binds and keeps
     the section under the fold. Measured at 1440x731 the section ends 62px
     above the fold; at 1440x900, 108px.

     --svcb-header is this file's own measurement of `.e-header` (16px padding
     twice, plus a 26px lockup, plus the button's height in practice), not a
     shared token. If the header's padding changes, this changes with it. */
  --svcb-header: 66px;
  min-height: calc(min(88svh, 840px) - var(--svcb-header));
  padding-block: clamp(48px, 4vw, 88px);
}

.e-svcb__inner {
  position: relative;
  z-index: 1;                    /* above the glow, which is z-index 0 */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-7);
  width: 100%;
}
.e-svcb .e-label { justify-content: center; }

/* The size comes from `.e-display--l` in the markup, which is what the
   homepage's and the About page's heroes both use. It was `--xl` — the only
   `--xl` heading on the site — and Adrian levelled it: three landing pages
   opening at three different sizes is three different voices. The mymind
   reference wanted the question monumental, and at 84px against an 800px
   measure it still is; it is just no longer louder than the two pages either
   side of it.

   NO MEASURE. The question ran on one line originally, then was forced to two
   by `max-width: 9ch` for two different reasons in turn — first so its centred
   left edge could not collide with the offset answer column, then because QA
   held that one line was too short to be monumental. The first reason went
   when the answer was centred. The second is Adrian's call and he has made it:
   one line.

   Nothing replaces the constraint, because nothing needs to. At the display-xl
   clamp the question is about 800px wide against a 1296px column, so it sits
   on one line by itself at every desktop width and wraps on its own where the
   column genuinely cannot hold it. `balance` is kept for that case — it only
   does anything once there are two lines to even. */
.e-svcb__q {
  text-align: center;
  text-wrap: balance;
}

/* Standing in for mymind's orb. One glyph. */
.e-svcb__mark { color: var(--ebr-yellow); }

/* --- The pointer's light, and why it is not here anymore -----------------

   There WAS a radial gradient in this block, following the pointer across the
   banner. It looked artificial, and it looked artificial for a reason worth
   writing down: it was a coloured disc painted OVER the video with no
   knowledge of what was underneath it. Where the nebula was bright it added
   magenta to magenta; where the frame was empty space it added magenta to
   nothing, and empty space does not glow.

   The light is now made in a shader out of the nebula's own pixels — see
   site.js §22 — so it brightens the cloud and leaves the black between the
   stars black. Nothing is painted over anything.
   -------------------------------------------------------------------------- */

/* The WebGL layer sits exactly where the video does and, once it is running,
   draws the video for it. The <video> stays in the document and keeps
   playing — it is the texture source, and it is also the fallback: if the
   context or the shaders fail, `is-gl` is never set and the video is simply
   still visible underneath. */
.e-svcfield__gl {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
}
.e-svcfield.is-gl .e-svcfield__gl { opacity: 1; }
/* Hidden with `visibility`, not `display` — a display:none video stops being a
   usable texture source. */
.e-svcfield.is-gl .e-svcfield__vid { visibility: hidden; }

/* The tail. A canvas rather than DOM nodes: a comet is a couple of hundred
   points being redrawn every frame, and two hundred elements with their own
   styles and transforms is two hundred things for the compositor to think
   about. One bitmap is one.

   It sits on the same layer as the glow and under the type, so the headline
   never has stars crossing it. */
.e-svcb__trail {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  pointer-events: none;
}

/* Nothing follows anything under reduced motion. The banner keeps the nebula;
   it just stops reacting. */
@media (prefers-reduced-motion: reduce) {
  .e-svcb__trail { display: none; }
}

/* Centred, on Adrian's call, and it is the right one for what this page has
   become. mymind offsets the answer to the left, and that offset was the trick
   the first draft was built around — the reply arriving somewhere the eye has
   to travel to rather than directly beneath the question.

   But the section below is now Zipline's, and Zipline's whole grammar is a
   centred stack: label, headline, then the control directly under it on the
   same axis. An offset answer between a centred question and a centred router
   put a kink in that axis two thirds of the way down, and the reader felt it
   without being able to name it. One axis, top to bottom.

   The measure still holds the block off the full width — centred and 48ch, not
   centred and edge to edge, which would make it a paragraph rather than a
   reply. */
.e-svcb__answer {
  max-width: 48ch;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
/* Both paragraphs are the same colour, as they are in the reference. The draft
   had the first one white, which added a second text colour at the same size
   for no reason and hung it on `:first-child` — a selector that would have
   moved silently the first time anything was inserted above it. */
.e-svcb__answer .e-lead { max-width: none; }

/* --- 26c. Banner, responsive ---------------------------------------------- */

@media (max-width: 860px) {
  .e-svcb { min-height: calc(min(84svh, 680px) - var(--svcb-header)); }
}

/* Short and wide. The banner's padding is `vw`-driven, so on a wide window it
   GROWS as the window gets wider while the height stays where it is — which is
   the opposite of what a section that must stay above the fold needs. The
   comment above claimed the padding binds on a short window and keeps it
   under; it does not, and at 1366x660 — a 1366x768 laptop with the browser's
   own chrome, which is about the commonest laptop viewport there is — the
   router's label was pushed off the bottom.

   700px is the height at which the two-line question, the answer and the top
   of the router stop fitting at full padding. Below it the padding goes
   height-driven, which recovers about 61px at 1366. */
@media (max-height: 700px) {
  .e-svcb { padding-block: clamp(24px, 3vh, 40px); }
  .e-svcb__inner { gap: var(--space-5); }
}

/* Landscape phone: wider still, and shorter than anything above covers. 375px
   of height cannot hold a display-xl question, its answer and the top of the
   router at any padding — the previous rule tried to do it with padding alone
   and left the banner 111px taller than the screen, so the answer itself was
   cut off. The question steps down a size instead. `--size-display-l` is the
   next token down, not a bespoke value: this is the same swap `.e-svca__service`
   makes at 1080, which is how this system handles a heading that no longer
   fits its box. */
@media (max-height: 560px) and (orientation: landscape) {
  .e-svcb { padding-block: clamp(12px, 4vh, 32px); }
  /* space-3, not space-4. At 812x375 the webfont build cleared the fold by
     0.3px, which is not a margin — the fallback stack is taller, `font-display:
     swap` means that render really happens, and measured with fallback metrics
     the same viewport overran by 27px. The narrower gap buys that back, and
     with it 780x360 and 740x360, which the wider gap also lost. */
  .e-svcb__inner { gap: var(--space-3); }
  .e-svcb__q { font-size: var(--size-display-l); }
}

@media (max-width: 560px) {
  .e-svcb__inner { gap: var(--space-6); }
}

/* --- 26d. The field — one surface under two sections ----------------------

   Zipline runs its sky unbroken from behind the headline, past the control,
   and down into the cards below. The two bands read as one place. Cut the
   field at the section boundary and the router stops being the answer to the
   question above it and becomes the next dark block down.

   So the fill lives on the wrapper and neither section carries its own.
   `.e-ground-ink` sets the same fill and this sets it again on purpose: the
   ground class is the thing that is easy to drop when markup gets edited, and
   losing the field's whole background to a class change is not a failure worth
   saving one declaration for. That is
   also what makes the background asset possible at all: one layer spanning
   both sections, rather than two layers that have to be kept in step across a
   boundary the reader can see.

   `isolation: isolate` gives the field its own stacking context, so the bg
   layer can sit under both sections without a z-index that has to know about
   the sticky header (40), the contact scrim and panel (60 and 61) or the intro
   curtain (999).
   -------------------------------------------------------------------------- */

.e-svcfield {
  position: relative;
  isolation: isolate;
  background: var(--ink-900);

  /* The field runs to the top of the PAGE, not to the top of the banner.

     The header is sticky, so it takes its 66px out of normal flow order and
     everything in `main` begins underneath it. That was invisible while the
     header was filled — but a transparent header over a picture needs the
     picture to be there, and at scroll 0 it was not: the top 66px behind the
     header was the paper page background, with the header's white wordmark and
     white nav sitting on it. The first thing a reader saw was the one frame
     where the header was unreadable.

     The negative margin pulls the field's box up under the header and the
     equal padding puts the banner back exactly where it was, so every
     measurement in 26b — the min-height, the fold clearance, the landscape
     cases — still holds without being touched. */
  --svcb-header: 66px;
  margin-top: calc(var(--svcb-header) * -1);
  padding-top: var(--svcb-header);
}

/* NO `overflow: hidden` HERE, and it matters. Clipping makes this a scroll
   container, and a scroll container is what `position: sticky` measures
   itself against — the video below would stick to the top of a box that never
   scrolls, which is to say it would not stick at all. It does not need
   clipping either: a sticky box cannot leave its containing block, and this
   box is the containing block. */
.e-svcfield__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* The fixed field. Sticky rather than `position: fixed` — fixed would escape
   the section and sit behind the whole page, and clipping it back would need
   a transform or a clip-path on an ancestor, either of which creates a new
   containing block and takes the sticky header down with it.

   Sticky costs nothing, releases by itself when `.e-svcfield` ends, and that
   end is the answer panel: the field runs behind the banner, the router and
   the panel, then stops. `100svh` rather than `100vh` so a mobile browser's
   collapsing address bar does not leave a strip of ink under it.

   The stage is what sticks, not the video, so the wash rides with it. Two
   sticky siblings would be two things to keep in step. */
.e-svcfield__stage {
  position: sticky;
  top: 0;
  height: 100svh;
}

.e-svcfield__vid {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The hand-off. Paper rather than white, and that is not a quibble: the
   services section below is `--ground-paper`, so washing to anything else
   would put a visible step exactly where this exists to remove one.

   `--field-wash` is written by site.js §19 from scroll position. The 0
   fallback means that with the script gone the field simply ends as it did
   before — a hard edge, but the right colours in the right order. */
.e-svcfield__wash {
  position: absolute;
  inset: 0;
  background: var(--paper);
  opacity: var(--field-wash, 0);
}

/* THERE IS NO SCRIM. There was one, at 65%, and it came out on Adrian's call:
   the nebula now runs at full strength and legibility is carried by the boxes
   instead — the router's tiles and the answer panel each take a solid ink
   fill, so their type sits on a known colour rather than on whatever frame of
   the loop happens to be behind it. The field shows between them.

   ⚠ WHAT THAT LEAVES UNCOVERED, because it is not nothing: the banner's
   heading and answer, and the router's label and heading, all sit directly on
   the video. Measured against the brightest half a per cent of this loop,
   white is 2.47:1 and the yellow question mark is 1.84:1. Both fail AA, and
   the yellow fails badly. The numbers are in _dev/video-pipeline.md and the
   smallest fix is a scrim behind those four blocks alone rather than the whole
   field. */

/* ALL THREE sections ride above the layer. Anything dropped into this wrapper
   must be listed here: a section left off computes `position: static`, paints
   below the bg layer, and is erased outright the moment the layer carries
   anything. `.e-svca` was left off when it landed and the whole answer panel
   vanished behind the asset — border, copy, figures, still and all — while the
   two sections above it were untouched. That is the exact failure this wrapper
   exists to prevent, and it is invisible until the asset arrives. */
.e-svcfield > .e-svcb,
.e-svcfield > .e-svcr,
.e-svcfield > .e-svca { position: relative; z-index: 1; }

/* --- 26e. The router — Zipline's stack, without Zipline's control ---------

   Reference: mobbin.com/sites/sections/ce7e2b2b-54f9-4a13-952e-4a895a0f7c30

   Label, headline, control, all centred on one axis, with the control close
   enough under the headline to read as belonging to it. Two departures, both
   called in the reference sheet before this was built:

     · Zipline's control is a two-item segmented pill. Six segments is not a
       pill — it is unreadable below about 1100px. What carries over is the
       proximity and the visible ownership, not the control.
     · The tiles hold a number and a sentence and nothing else. Detail inside a
       tile answers the question the tile is supposed to be asking.

   Purple is spent here, and only here. It means "selected" on this page, which
   is why 26b keeps the banner on ink and puts its accent in a single yellow
   glyph instead. Yellow returns on the tile numbers, which is legal on ink and
   legal again on the violet of the selected tile.
   -------------------------------------------------------------------------- */

/* No top padding. The question above has to run straight into the answer to
   it, which is the one thing the whole section is for. `--svcr-pad-end` is the
   number that gives way when the answer panel lands underneath — the panel has
   to touch these tiles, or the click and the change read as two events. */
.e-svcr {
  --svcr-pad-end: var(--section-y);
  padding-block: 0 var(--svcr-pad-end);
}

/* Once a panel is open the section padding between the tiles and it collapses
   to the grid's own row gap, so the panel reads as a seventh cell of the grid
   rather than as the next section down. That proximity is the mechanic the
   whole Zipline reference was taken for: the control has to visibly own the
   thing beneath it, or the click and the change read as two unrelated events.
   With nothing selected there is no panel to touch, so the padding stays. */
.e-svcfield.is-answering .e-svcr,
body.is-answering .e-svcr { --svcr-pad-end: var(--space-3); }

.e-svcr__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
}
.e-svcr .e-label { justify-content: center; }

.e-svcr__title {
  text-align: center;
  text-wrap: balance;
  max-width: 20ch;
}

/* 3x2. Six in three columns is the only arrangement that survives the number
   changing: at five it becomes 3-over-2 with the short row centred, at seven
   3-over-3-over-1. Two columns would make the sentences short and the block
   tall; six across would make them unreadable. */
.e-svcr__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
  width: 100%;
  margin-top: var(--space-2);
}

/* Number pinned top, sentence pinned bottom — the same grammar as the beliefs
   columns and the process grid, so a tile on this site reads the same way
   wherever it appears. */
/* `--border-on-dark` is a DIVIDER token — rgba(255,255,255,.18), which
   composites to 1.78:1 on ink. That is right for a hairline between two blocks
   of text and wrong for the boundary of something you are meant to click,
   where WCAG 1.4.11 asks for 3:1. .36 measures 3.0:1 and is still a hairline.

   Set as a variable and used by the panel below as well, so the panel keeps
   reading as one of these tiles grown rather than as a lighter object. */
.e-svcfield { --svc-line: 1px solid rgba(255, 255, 255, .36); }

.e-svcr__tile {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-6);
  min-height: clamp(148px, 13vw, 196px);
  padding: var(--space-5);
  text-align: start;
  /* Solid, not transparent. The tile is what makes its own type legible now
     that the field behind it is a moving picture — an opaque fill is the only
     background whose contrast can be relied on frame to frame. */
  background: var(--ink-900);
  border: var(--svc-line);
  border-radius: var(--radius-md);
  color: var(--text-on-dark);
  font: inherit;
  cursor: pointer;
  transition: var(--transition-hover);
}

.e-svcr__n {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  color: var(--ebr-yellow);
}

.e-svcr__say {
  font: var(--weight-medium) var(--size-body-l)/1.25 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--white);
  text-wrap: pretty;
}

/* Colour, never shadow. The border lifting to full white is the whole hover —
   the same move `.e-ask__link` makes, one field further down. */
.e-svcr__tile:hover { border-color: var(--white); }

.e-svcr__tile:focus-visible {
  outline: 2px solid var(--ebr-yellow);
  outline-offset: 2px;
}

/* Selected. Purple's one job on this page — but purple alone cannot carry the
   state: #6B00F6 against the ink around it is 2.40:1, under the 3:1 WCAG
   1.4.11 wants of a state indicator, and every other thing in the tile keeps
   its colour. So the border goes white with it. That is 17.04:1, it is the
   same move `:hover` already makes, and it means the selected tile is marked
   by its edge as well as its fill. */
.e-svcr__tile[aria-checked="true"] {
  background: var(--ebr-purple);
  border-color: var(--white);
}
.e-svcr__tile[aria-checked="true"]:hover { border-color: var(--white); }

/* --- 26f. Router, responsive ---------------------------------------------

   Three across needs the width. At two the sentences keep a workable measure;
   at one the tile stops being a tile and becomes a row, so the fixed height
   goes with it — six 148px boxes stacked is 900px of scrolling past things you
   have already rejected.
   -------------------------------------------------------------------------- */

/* 22 Oct 2026: seven symptoms since App Development got one. Four across
   reads as 4 + 3; three across would leave the seventh alone on a row. */
.e-svcr__grid:not(.e-svcr__grid--four) { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 1080px) {
  .e-svcr__grid { grid-template-columns: repeat(2, 1fr); }
  .e-svcr__grid:not(.e-svcr__grid--four) { grid-template-columns: repeat(2, 1fr); }
  /* An odd last tile takes the whole row rather than sitting half-empty. */
  .e-svcr__grid:not(.e-svcr__grid--four) > .e-svcr__tile:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

@media (max-width: 700px) {
  .e-svcr__grid, .e-svcr__grid:not(.e-svcr__grid--four) { grid-template-columns: 1fr; }
  .e-svcr__tile {
    min-height: 0;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-5);
  }
  .e-svcr__say { font-size: var(--size-body); }
}

/* --- 26g. The answer panel -----------------------------------------------

   One card, six states, two columns. Superpower's architecture: everything in
   one box so the panel cannot resize awkwardly as six different sets of
   content pass through it, and the call to action pinned to the bottom edge so
   it never moves. Retool's right-hand column: the case still with its evidence
   directly beneath it.

   1.15:1, not 1:1. The left column carries a description, three inclusions,
   two figures and the buttons; the right carries a still and six words of
   fact. An even split would leave the right column half air at every state.

   The panel is bordered, not filled. A lifted fill inside a dark field is a
   second surface, and the moment the field carries the background asset the
   panel would be the only place it does not show through. The hairline is
   enough to say "one object" — and it is the same hairline and the same radius
   as the tiles above, so the panel reads as one of them, grown.

   Purple is NOT spent here. It is already doing its one job on the selected
   tile, and a violet-bordered panel under a violet tile would be the same
   colour meaning two things at once. The connection is made by proximity
   instead, which is what the reference does anyway.
   -------------------------------------------------------------------------- */

/* Half of `--section-y` below the panel, and the services section takes a
   tight top to match — the two together were putting up to 320px of nothing
   between the answer and the list, which read as the page having finished.
   About 170px now.

   This is also the field's tail, and the tail is what the wash runs across, so
   shortening it makes the wash start earlier relative to the panel rather than
   shortening the wash itself. */
.e-svca { padding-block: 0 clamp(40px, 5vw, 80px); }

/* `height` is written by script during a swap and cleared afterwards — see
   site.js §17. `overflow: hidden` is what makes that animation possible and is
   only ever binding while the height is set. */
.e-svca__panel {
  /* Solid, for the same reason the tiles are: everything inside it is type on
     a dark ground, and that ground has to be a colour rather than a frame. */
  background: var(--ink-900);
  border: var(--svc-line);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: height var(--dur-base) var(--ease-out);
}

.e-svca__state {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: var(--space-7);
  padding: var(--space-6);
}

/* The no-JS story, and it is a CSS gate rather than a `hidden` attribute for a
   reason. The first version carried `hidden` in the markup and had script take
   it off — except script never did, it only ever set it, so with JS off the
   panel was a two-pixel empty box and six descriptions, eighteen inclusions,
   twelve figures and eighteen facts rendered as nothing at all.

   Inverted here. The markup carries no `hidden`, so with JS off every state
   renders and the section is a plain list of six services with their detail —
   a complete thing. `.js` is written on <html> before the page paints, so with
   JS on they are hidden with no flash to trade for it, and script only ever
   adds `is-open`. This is the same gate the rest of the site uses for anything
   that hides content, and for the same reason. */
.js .e-svca__state { display: none; }
.js .e-svca__state.is-open { display: grid; }

/* Shown until a symptom is chosen; gone once one is. Without JS there is
   nothing to choose and all six states are already open, so it never shows. */
.e-svca__cue {
  display: none;
  margin: 0;
  padding: var(--space-7) var(--space-6);
  font: var(--type-body);
  color: var(--text-on-dark-muted);
  text-align: center;
}
.js .e-svca__cue { display: block; }
.e-svcfield.is-answering .e-svca__cue,
body.is-answering .e-svca__cue { display: none; }

/* --- left column --------------------------------------------------------- */

/* The buttons are pushed to the bottom edge and everything above them takes
   the space it needs — that is the pinning, and it is the reason a longer
   description in one state does not move the CTA in another. */
.e-svca__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}
.e-svca__cta { margin-top: auto; }

.e-svca__lead { display: flex; flex-direction: column; gap: var(--space-3); }

/* Announced, never seen. Carries the service name alone so a selection change
   is three words of speech rather than the whole panel's ninety-four. */
.e-vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.e-svca__from {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
}

.e-svca__service {
  margin: 0;
  font: var(--weight-medium) var(--size-display-m)/1.08 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--white);
  text-wrap: balance;
}

.e-svca__desc {
  margin: 0;
  font: var(--type-copy);
  color: var(--text-on-dark-muted);
  max-width: 46ch;
}

/* Ticked inclusions. The tick is the sprite's, at body size, aligned to the
   first line's optical centre rather than its box — at 16px in a 1.5 line box
   a top-aligned mark sits visibly high. */
.e-svca__inc {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.e-svca__inc li {
  display: flex;
  gap: var(--space-3);
  font: var(--type-copy);
  color: var(--text-on-dark);
}
.e-svca__tick {
  flex-shrink: 0;
  margin-top: 4px;
  color: var(--ebr-yellow);
}

/* Two figures. The definition is the point, not the number — "typical
   timeline" means nothing without "from kickoff to launch" — so the definition
   gets the room and the unit sits under it, small and muted, rather than
   fighting the figure for the same line.

   Display-m, not the display-l the reference sheet first called for. The site
   already has a figure treatment in `.e-stat__value` and it is display-m; two
   figure sizes in one system is the inconsistency, and at 84px these would be
   the loudest thing on a page whose loudest thing is meant to be the question.
   Deliberate departure from that note, recorded here. */
.e-svca__figs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
  padding-top: var(--space-5);
  border-top: var(--border-on-dark);
}
.e-svca__fig { display: flex; flex-direction: column; gap: 6px; }
.e-svca__figv {
  font: var(--weight-medium) var(--size-display-m)/1 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--white);
}
.e-svca__figd {
  font: var(--type-small);
  color: var(--text-on-dark);
  text-wrap: pretty;
}
.e-svca__figu {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
}

.e-svca__cta {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  flex-wrap: wrap;
}
/* The second action is a link, not a second button. Two buttons side by side
   is two primary actions, and on this panel there is only one. */
.e-svca__talk {
  background: none;
  border: 0;
  padding: 0;
  font: var(--type-small);
  color: var(--text-on-dark-muted);
  text-decoration: underline;
  text-underline-offset: 4px;
  cursor: pointer;
  transition: var(--transition-hover);
}
.e-svca__talk:hover { color: var(--white); }

/* --- right column -------------------------------------------------------- */

.e-svca__aside {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* ⚠ Placeholder. Six case stills, none of them shot. */
.e-svca__shot {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, .05);
  display: grid;
  place-items: center;
  padding: var(--space-4);
}
.e-svca__todo {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
  text-align: center;
}

/* Harvest's fact rail, standing in for the quote block until the quotes are
   cleared. Label above value, hairline between rows — the rule divides one
   fact from the next, which is the test. */
.e-svca__facts {
  margin: 0;
  display: flex;
  flex-direction: column;
}
.e-svca__fact {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
  padding-block: var(--space-3);
  border-top: var(--border-on-dark);
}
.e-svca__fact:first-child { border-top: 0; padding-top: 0; }
.e-svca__fact dt {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
}
.e-svca__fact dd {
  margin: 0;
  font: var(--type-small);
  color: var(--text-on-dark);
  text-align: end;
}

/* Every hole on the site, marked the same way. Muted and dashed rather than a
   plausible number: a placeholder that looks like data is the one that reaches
   production.

   ⚠ IT HAS TO KNOW ITS GROUND. This was written for the answer panel, which is
   ink, so it was `--text-on-dark-muted` — 62% white. The first time a
   placeholder appeared on a white card it was white on white and simply was
   not there. The light value is the default now and the dark grounds opt in,
   which is the right way round: most of this site is paper. */
/* Doubled attribute selector, on purpose. A single `[data-todo]` is (0,1,0)
   and loses to any component rule that sets its own colour — `.e-dtc__name`
   set `--text-strong` and the placeholder came out as confident ink. A hole
   has to out-rank the thing it is a hole in. */
[data-todo][data-todo] { color: var(--text-muted); }
.e-ground-ink [data-todo][data-todo],
.e-ground-brand [data-todo][data-todo],
.e-svcfield [data-todo][data-todo] { color: var(--text-on-dark-muted); }

/* --- the swap ------------------------------------------------------------

   The risk is that a tall panel crossfading on every click reads as a page
   reload. So it is kept small and staggered: the outgoing state is simply
   gone, and the incoming one rises 8px as it fades, its parts 80ms apart, over
   a height that animates underneath them so nothing below the panel jumps.
   -------------------------------------------------------------------------- */

.js .e-svca__state.is-swapping > * > *,
.js .e-svca__state.is-swapping > * {
  opacity: 0;
  translate: 0 8px;
}
.js .e-svca__state > * {
  transition: opacity var(--dur-fast) var(--ease-out),
              translate var(--dur-fast) var(--ease-out);
}
.js .e-svca__body    { transition-delay: 0ms; }
.js .e-svca__aside   { transition-delay: var(--stagger-step); }

@media (prefers-reduced-motion: reduce) {
  /* The content simply replaces itself. No rise, no fade, and no height
     animation — a panel that resizes slowly is still motion. */
  .e-svca__panel { transition: none; }
  .js .e-svca__state > *,
  .js .e-svca__state.is-swapping > * {
    opacity: 1;
    translate: none;
    transition: none;
  }
}

/* --- 26h. Answer panel, responsive ---------------------------------------

   The two columns need the width. At 860 the still and the facts move under
   the copy rather than beside it — a 4:3 still in a column narrower than about
   300px is a strip, and the facts beside it are unreadable.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-svca__state { gap: var(--space-6); }
  .e-svca__service { font-size: var(--size-body-l); line-height: 1.25; }
}

@media (max-width: 860px) {
  .e-svca__state { grid-template-columns: 1fr; padding: var(--space-5); }
  /* Still first would put a placeholder box above the answer to the question
     that was just asked. The copy leads; the evidence follows it. */
  .e-svca__aside { order: 2; }
  .e-svca__shot { aspect-ratio: 16 / 9; }
}

@media (max-width: 560px) {
  .e-svca__figs { grid-template-columns: 1fr; gap: var(--space-4); }
  .e-svca__cta { gap: var(--space-4); }
}

/* --- 26i. How it runs — the Daylight band --------------------------------

   Reference: Daylight, a saturated full-bleed field carrying numbered steps as
   columns, the number inline with the step name and a hairline with a dot at
   its left end dividing the name from the description.

   PURPLE'S SECOND APPEARANCE ON THIS PAGE, and worth being explicit about,
   because the house rule says purple does one thing per screen. Above, on the
   router, purple is a MARK: it means "this is the one you picked". Here it is
   a GROUND. Two different jobs, and they are never in view together — the
   answer panel sits between them and this band is a screen further down. The
   band earning the strongest colour change on the page is the whole argument
   of the section: everything above it rearranges itself around what the reader
   picked, and this is the part that does not.

   If that reads as purple meaning two things, the fix is one line — the field
   becomes ink and the band loses its point, or the router's selection becomes
   something other than purple and section 26e changes instead. It is a real
   call and it is recorded here rather than buried.

   Every type colour comes free from `.e-ground-brand` in the markup: white
   display, muted lead and label, yellow label index. Nothing is set here that
   the ground class already sets.
   -------------------------------------------------------------------------- */

/* Violet is a much lighter ground than ink, and the shared muted text colour
   was built for ink. Measured on this field, `--text-on-dark-muted` gives
   **3.29:1** at 14px — under the 4.5 AA asks of normal text — so the band
   carries its own muted value at .86, which measures 5.45 and still sits a
   clear step below the pure white of the step names (7.10).

   ⚠ THE SAME TOKEN IS USED ON THE ABOUT PAGE'S VIOLET HERO, where `.e-lead`
   is 19px and therefore also normal text by WCAG's definition. It will be
   failing there for the same reason. Fixed locally here rather than at the
   token, because changing `.e-ground-brand` would restyle a page nobody asked
   me to touch — but it is a system-level problem and it is Adrian's call. */
.e-proq {
  --proq-muted: rgba(255, 255, 255, .86);
}

.e-proq__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
}
.e-proq .e-label {
  justify-content: center;
  color: var(--proq-muted);
}

.e-proq__title {
  text-align: center;
  text-wrap: balance;
  max-width: 22ch;
}

/* Four parallel things, so four columns. `ol` because the steps are ordered
   and a reader on a screen reader should be told so — the numbers in the
   markup are visible labels, not the list's own counters, which is why the
   default marker is off rather than restyled. */
.e-proq__steps {
  list-style: none;
  margin: var(--space-6) 0 0;
  padding: 0;
  width: 100%;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-6);
}

.e-proq__step { display: flex; flex-direction: column; }

/* Inline, not stacked. Daylight sets "01. Solar makes energy" as one line, and
   the index reads as part of the name rather than as a number sitting over it
   — which is what keeps the step name the first thing you read. */
.e-proq__name {
  margin: 0 0 var(--space-4);
  font: var(--weight-medium) var(--size-body-l)/1.25 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--white);
  text-wrap: pretty;
}
.e-proq__n { color: var(--ebr-yellow); }

/* The rule and its dot. One line per step, dividing the name from the
   description — the only line in this section, which is the point. The dot is
   Daylight's marker and the one element here that is not structural; it is a
   single rule to delete if it reads as decoration. */
.e-proq__body {
  position: relative;
  margin: 0;
  padding-top: var(--space-4);
  border-top: var(--border-on-dark);
  font: var(--type-copy-small);
  color: var(--proq-muted);
}
.e-proq__body::before {
  content: "";
  position: absolute;
  top: -2px;
  inset-inline-start: 0;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--ebr-yellow);
}

/* --- 26j. How it runs, responsive ---------------------------------------- */

@media (max-width: 1080px) {
  .e-proq__steps { grid-template-columns: repeat(2, 1fr); gap: var(--space-7) var(--space-6); }
}

@media (max-width: 700px) {
  .e-proq__steps { grid-template-columns: 1fr; gap: var(--space-6); }
}

/* --- 26k. Last objections — Amigo, on the shared accordion ---------------

   Reference: Amigo — the heading alone in a left column, hairline rows on the
   right, the first already open.

   THE ACCORDION ITSELF IS NOT REDEFINED HERE. Everything that makes it work
   lives in section 16 and site.js section 5, and this block reuses it whole.
   What follows is only the two-column arrangement Amigo adds, and three
   overrides scoped to `.e-obj` so the homepage's questions are untouched.
   -------------------------------------------------------------------------- */

/* Amigo's arrangement: the heading held in a narrow left column with the rows
   beside it. Same column width as the Industries block on the About page, so
   the two left-heading layouts on this site agree with each other. */
.e-obj__grid {
  display: grid;
  grid-template-columns: 320px 1fr;
  gap: var(--space-8);
  align-items: start;
}

.e-obj__lead { display: flex; flex-direction: column; gap: var(--space-5); }
.e-obj__title { max-width: 14ch; }
.e-obj__sub {
  margin: 0;
  font: var(--type-copy-small);
  color: var(--text-copy);
  max-width: 34ch;
}

/* THE SITE'S FIRST INLINE PROSE LINK, and it arrived purple.

   `ds/tokens/base.css:10` sets bare anchors to `--text-link`, which is violet.
   Nothing had ever put a link inside a paragraph before, so nothing had caught
   it. On the detail page purple is already spent — the process band is a
   purple field and the section indices are violet — and a third meaning inside
   body copy empties it of the first two. House rule: purple does one thing per
   screen.

   Ink with a hairline underline instead, which is the treatment this page's
   related-services links already use, so it degrades into a pattern the site
   has rather than a new one. `text-decoration: none` has to be restated on
   hover because base.css:11 sets `a:hover { text-decoration: underline }` and
   a bare element selector out-ranks nothing — it would double up with the
   border. */
.e-obj__sub a {
  color: var(--ink-900);
  text-decoration: none;
  border-bottom: 1px solid var(--line-hairline);
  padding-bottom: 1px;
  transition: var(--transition-hover);
}
.e-obj__sub a:hover,
.e-obj__sub a:focus-visible { border-bottom-color: currentColor; text-decoration: none; }

/* Override 1 — the open row is not purple on this page.

   The shared component colours the open question and fills its marker violet.
   That is correct on the homepage. Here purple already means "the symptom you
   picked" on the router and carries the whole process band above, and a third
   meaning would empty it of all three. Weight and air mark the open row
   instead, which is enough when it is also the only row with an answer under
   it. `--weight-medium` is the same step the site uses everywhere it needs a
   line to read heavier; it is not a new type size. */
.e-obj .e-faq__q:hover { color: var(--ink-900); }
.e-obj .e-faq__item.is-open .e-faq__q {
  color: var(--text-strong);
  font-weight: var(--weight-medium);
  padding-block: var(--space-6) var(--space-5);
}

/* Override 2 — no fill behind the marker. Amigo has no boxes and no fills, and
   a grey disc on a paper field is a box where the reference has a hairline. */
.e-obj .e-faq__toggle {
  width: auto;
  height: auto;
  background: none;
  color: var(--text-muted);
  border-radius: 0;
}
.e-obj .e-faq__item.is-open .e-faq__toggle {
  background: none;
  color: var(--ink-900);
}

/* Override 3 — the answer takes the row's measure rather than the page's.
   `--measure` is 62ch and the rows are narrower than that here, so without
   this the paragraph would set its own width and stop agreeing with the
   question above it. */
.e-obj .e-faq__a p { max-width: 68ch; }

/* --- 26l. Last objections, responsive ------------------------------------

   The left column stops earning its width before the rows do. Below 1080 the
   heading goes above them and the rows take the full measure, which is the
   arrangement the homepage's questions already use — so this degrades into a
   layout the site already has rather than into a new one.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-obj__grid { grid-template-columns: 1fr; gap: var(--space-7); }
  .e-obj__title { max-width: 22ch; }
  .e-obj__sub { max-width: 52ch; }
}



/* --- 26m. The services list — Phantom Studios ----------------------------

   Reference: mobbin.com/sites/sections/3cf723db-92c1-4601-b820-379757d44107

   Full-width rows on one field, divided by hairlines running the whole
   measure. Each row is a service name in display type, a paragraph beside it,
   a small outlined button under the paragraph, and a still at the right-hand
   end.

   PHANTOM'S LEFT GUTTER IS NOT HERE, on Adrian's call, and it is the right
   one. Phantom indents its display type by about a quarter of the width, which
   works there because nothing sits above the rows to disagree with. Here the
   label, the headline and the standfirst all start at the shell's left margin,
   so an indented service name put a second left edge in the same section and
   the block stopped reading as one thing. The names now start where the
   headline starts.

   Replaced a MindMarket version that was correct and too thin. This costs
   height and buys a reason to click.

   ⚠ Unlike MindMarket's, THIS REFERENCE'S HOVER IS NOT VERIFIED. Phantom's
   live site could not be found at any of the obvious domains, so there is no
   CSS to read. The hover below is built from what the still shows and what
   this system already does, not copied from the source — if it is wrong, it is
   wrong by inference rather than by mistake. The still also suggests the row
   itself may not be a link at all on Phantom, since every row carries its own
   button.
   -------------------------------------------------------------------------- */

/* Tight on top, normal below — see the note on `.e-svca`. */
.e-svcl { padding-block: clamp(48px, 6vw, 88px) var(--section-y); }

.e-svcl__lead {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-width: 46ch;
  margin-bottom: var(--space-8);
}
.e-svcl__title { max-width: 20ch; }
.e-svcl__sub {
  margin: 0;
  font: var(--type-copy-small);
  color: var(--text-copy);
  max-width: 44ch;
}

/* The rule above the first row is the only one this block adds beyond the row
   dividers themselves — it closes the list at the top so the first row is not
   the one row without a line above it. */
.e-svcl__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: var(--border-hairline);
}

.e-svcl__item { position: relative; }

/* Name, copy, still. Three columns at every width above 700 — the wide layout
   and the narrow one now differ only in their proportions and their type
   sizes, not in their structure. */
.e-svcl__link {
  display: grid;
  grid-template-columns: 5fr 6fr 4fr;
  gap: var(--space-6);
  align-items: start;
  padding-block: var(--space-5);
  border-bottom: var(--border-hairline);
  text-decoration: none;
  color: var(--text-strong);
  transition: border-color var(--dur-fast) var(--ease-out);
}
/* `ds/tokens/base.css` underlines every link on hover and outranks a bare
   class, so it has to be cancelled on the hover state itself. */
.e-svcl__link:hover,
.e-svcl__link:focus-visible { text-decoration: none; }

.e-svcl__name {
  grid-column: 1;
  margin: 0;
  font: var(--weight-medium) var(--size-display-m)/1.05 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--text-strong);
  text-wrap: balance;
}

.e-svcl__body {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-5);
}
.e-svcl__copy {
  margin: 0;
  font: var(--type-copy-small);
  color: var(--text-copy);
  max-width: 42ch;
}

/* A `span` wearing the button's clothes, because the row is the link and a
   link inside a link is not markup. It carries no hover of its own — the row
   drives it, below. */
.e-svcl__cta { pointer-events: none; }

/* ⚠ Placeholder. Six stills, none of them shot. Same treatment as the answer
   panel's and the About page's, so the gaps on this site all look like the
   same gap. */
/* Height set directly rather than by aspect ratio, because at six rows the
   still is what sets the row height and an aspect ratio hands that number to
   the column width. Phantom can afford a 4:3 still; it has three rows. Six of
   them at 4:3 made this section 2,517px — 3.2 screens for one block on a page
   that is already long. A fixed landscape height brings it to about two. */
.e-svcl__shot {
  grid-column: 3;
  height: clamp(104px, 9.5vw, 152px);
  border-radius: var(--radius-sm);
  background: var(--ink-100);
  display: grid;
  place-items: center;
  overflow: hidden;
}
.e-svcl__todo {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
/* 26 Oct 2026: the six stills are illustrations of each service, drawn from
   its film, on a transparent ground: the slot's own fill shows through, so the
   hover below still lifts it. Cover trims the sides at narrower widths; the
   key visual sits in the middle 85% of each image so nothing important goes. */
.e-svcl__img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* No grey panel behind a real still, at rest or on hover (Adrian, 26 Oct
   2026). Specific enough to beat the hover rule below; a slot still showing
   the placeholder label keeps its grey. */
.e-svcl__item .e-svcl__shot:has(.e-svcl__img),
.e-svcl__item:hover .e-svcl__shot:has(.e-svcl__img),
.e-svcl__item:focus-within .e-svcl__shot:has(.e-svcl__img) { background: transparent; }

/* The hover. Inferred, not copied — see the note above. Two moves, both of
   which this system already makes elsewhere: the outlined button fills, which
   is exactly what `.e-btn--outline:hover` does everywhere else on the site,
   and the still lifts from its flat field. Nothing scales, nothing bounces,
   and the row does not move. */
@media (hover: hover) {
  .e-svcl__item:hover .e-svcl__cta,
  .e-svcl__item:focus-within .e-svcl__cta {
    background: var(--ink-900);
    color: var(--white);
    border-color: var(--ink-900);
  }
  .e-svcl__item:hover .e-svcl__shot,
  .e-svcl__item:focus-within .e-svcl__shot { background: var(--ink-200, #E2E2E0); }
}

/* --- 26n. The services list, responsive ----------------------------------

   The gutter is the first thing to go — below 1080 it is holding space the row
   needs. Then the still, which at a third of a narrow row is a stamp rather
   than an image. Below 700 the row becomes a stack, which is the only
   arrangement six rows of this weight survive on a phone.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-svcl__link { gap: var(--space-5); }
  .e-svcl__name { font-size: var(--size-body-l); line-height: 1.25; }
  .e-svcl__title { max-width: 24ch; }
}

@media (max-width: 700px) {
  .e-svcl__link { grid-template-columns: 1fr; gap: var(--space-4); padding-block: var(--space-5); }
  .e-svcl__name,
  .e-svcl__body { grid-column: 1; }
  /* The still goes on a phone. Stacked, it adds about two hundred pixels to
     every row and took this section to 2,783px — three and a half screens of
     which half was grey boxes with the word "Still" on them. The name, the
     sentence and the button already do the row's job; the picture is the part
     that can wait for a wider screen.

     One line to bring back when the six stills are real and it is a
     photograph being hidden rather than a placeholder. */
  .e-svcl__shot { display: none; }
}


/* --- 26o. The closing band ------------------------------------------------

   Claude (mobbin.com/sites/sections/3ad59aee-…) for the architecture: an
   unbalanced two-column close, the ask on the left, a bordered panel on the
   right. Dovetail for the button pair — under the headline, left-aligned, the
   secondary legible without becoming a second primary.

   Both buttons are the site's own `.e-btn`, so nothing about them is defined
   here. The primary is `--accent` (yellow on ink, which is legal) and the
   secondary is `--outline --on-dark`, whose hairline is already the right one.
   -------------------------------------------------------------------------- */

.e-close__grid {
  display: grid;
  grid-template-columns: 7fr 5fr;
  gap: var(--space-8);
  align-items: start;
}

.e-close__ask { display: flex; flex-direction: column; gap: var(--space-5); }
.e-close__title { max-width: 16ch; }
.e-close__line { max-width: 46ch; }

.e-close__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--space-3);
}

/* The border is the whole reason Claude is the reference for this column.
   Without it the rail is a paragraph that happens to sit on the right; with
   it, it is a second object and the reader treats it as one. */
.e-close__rail {
  border: var(--border-on-dark);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.e-close__railcopy {
  margin: 0;
  font: var(--type-copy-small);
  color: var(--text-on-dark-muted);
}

/* Label above value, hairline between rows — the rule divides one fact from
   the next, which is the test. Same grammar as the answer panel's fact rail,
   because they are the same kind of thing. */
.e-close__facts { margin: 0; display: flex; flex-direction: column; }
.e-close__facts > div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
  padding-block: var(--space-3);
  border-top: var(--border-on-dark);
}
.e-close__facts > div:first-child { border-top: 0; padding-top: 0; }
.e-close__facts dt {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
}
.e-close__facts dd {
  margin: 0;
  font: var(--type-small);
  color: var(--text-on-dark);
  text-align: end;
}

/* --- 26p. The closing band, responsive ------------------------------------

   The rail goes under the ask rather than beside it, and it goes AFTER — a
   reader who is ready to book should reach the button before they reach the
   paragraph explaining what to do if they are not.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-close__grid { grid-template-columns: 1fr; gap: var(--space-7); }
  .e-close__title { max-width: 22ch; }
}

@media (max-width: 560px) {
  .e-close__actions { flex-direction: column; align-items: stretch; }
  .e-close__actions > * { justify-content: center; }

  /* `.e-btn` is `nowrap`, which is right for every other button on the site
     because every other button's label is two or three words. This one's is
     written at runtime and runs to "Book a call about performance marketing" —
     thirty-nine characters, uppercase, with label tracking. At 360 it was
     419px wide inside a 360px page and pushed 79px of horizontal overflow onto
     the document; at 320 it was 119px. It wraps here instead.

     Widened from `[data-close-cta]` to every button in the band. The detail
     template's closing CTA is written in the markup rather than at runtime, so
     it carries no `data-close-cta` — and it is just as long. Keying the fix to
     the attribute meant keying it to HOW the label got there rather than to
     how long it is, which is not what overflows. */
  .e-close__actions .e-btn {
    white-space: normal;
    text-align: center;
    line-height: 1.4;
  }
}

/* --------------------------------------------------------------------------
   27. Service detail template
   Only services/*.html renders anything in this block.
   -------------------------------------------------------------------------- */

/* --- 27a. The banner — after Koto -----------------------------------------

   Reference: mobbin.com/sites/sections/2ce48fe2-bea1-438e-a2aa-0b1af12f7996

   A small label held far left, the statement set right of centre rather than
   underneath it, a row of jump links into the page's own sections, and a
   full-bleed strip closing the band.

   The offset is the whole composition. Koto does not stack label over
   statement — it puts them on one line with a gap of empty page between, which
   is what makes the statement read as an answer to the label rather than as
   its subtitle. It is the same device the services banner used to have before
   it was centred, and it works here for the opposite reason: this page IS
   about one named thing, so naming it on the left and answering on the right
   is the page's argument in one line.
   -------------------------------------------------------------------------- */

/* The still gets the SAME air above it as below it, and that air is 64px
   rather than the section's 144.

   Three states got us here and the middle one was wrong in both directions.
   At 144 above and 144 below the still floated between two blocks belonging to
   neither. Flush at the top it read as tight, and it was still carrying 144
   below — so it was pinned to the banner and drifting away from the section it
   opens. Neither is symmetrical and the eye reads the asymmetry before it
   reads the image.

   `--space-8` both sides: enough that the still is a separate object, little
   enough that it stays well above where it started. The value is set in two
   places that have to agree — here, and `.e-dti__grid`'s margin-top below. */
.e-dth { padding-block: var(--section-y) var(--space-8); }

.e-dth__inner { display: flex; flex-direction: column; gap: var(--space-7); }

/* The breadcrumb merges into the band instead of taking a bordered strip of
   its own — Canva's argument from the reference sheet, and one less rule. */
/* 10px, not the 11px label token. A breadcrumb is orientation, not content —
   it should be findable and otherwise ignorable, and at 11px with 0.16em
   tracking this one ran the full 1296px of the shell and read as a heading.

   10px is not a new size in this system: `.e-btn--sm` already uses it. The
   tracking is em-based so it comes down with the size on its own. */
.e-dth__crumbs ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  font: var(--weight-medium) 10px/1 var(--font-body);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.e-dth__crumbs li + li::before { content: "/"; margin-inline-end: var(--space-3); color: var(--line-strong); }
.e-dth__crumbs a { color: var(--text-muted); text-decoration: none; }
.e-dth__crumbs a:hover { color: var(--ink-900); text-decoration: none; }
.e-dth__crumbs [aria-current] { color: var(--ink-900); }

.e-dth__head {
  display: grid;
  grid-template-columns: 4fr 8fr;
  gap: var(--space-8);
  align-items: start;
}

/* Koto sets its label small and far left, and lets the gap do the work. */
.e-dth__label {
  font: var(--weight-medium) var(--size-body-l)/1.2 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-muted);
}

.e-dth__say { display: flex; flex-direction: column; gap: var(--space-5); }
.e-dth__title { max-width: 18ch; }
.e-dth__lede { max-width: 52ch; }

/* Two figures, not a metrics wall — ClickUp's proportion, where the definition
   under the number does more work than the number. Same treatment as the
   answer panel on the index so a figure looks the same wherever it appears. */
.e-dth__figs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-7);
  padding-top: var(--space-5);
  border-top: var(--border-hairline);
}
.e-dth__fig { display: flex; flex-direction: column; gap: 6px; }
.e-dth__figv {
  font: var(--weight-medium) var(--size-display-m)/1 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--text-strong);
}
.e-dth__figd { font: var(--type-small); color: var(--text-muted); max-width: 22ch; text-wrap: pretty; }

.e-dth__cta { margin-top: var(--space-2); }

/* --- The closing strip ---------------------------------------------------

   Koto ends its band with a full-bleed image. ⚠ Placeholder — six hero stills,
   none of them shot. Same treatment as every other gap on this site.
   -------------------------------------------------------------------------- */

/* No top margin: it is flush with the top of the section it opens, which is
   the whole point of having moved it. The section drops its own top padding to
   match — see 27d. */
.e-dth__strip {
  height: clamp(180px, 26vw, 380px);
  background: var(--ink-100);
  display: grid;
  place-items: center;
  padding: var(--space-5);
}
.e-dth__todo {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: center;
}

/* --- 27b. Detail banner, responsive --------------------------------------

   The label gives up its column at 1080 and sits above the statement, which is
   the arrangement the offset degrades into rather than a second layout.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-dth__head { grid-template-columns: 1fr; gap: var(--space-5); }
  .e-dth__label { font-size: var(--size-small); text-transform: uppercase; letter-spacing: var(--tracking-label); }
  .e-dth__label br { display: none; }
  .e-dth__title { max-width: 22ch; }
}

@media (max-width: 560px) {
  .e-dth__figs { flex-direction: column; gap: var(--space-5); }

  /* The same `.e-btn` nowrap problem the closing band already hit, in a worse
     place. "Book a call about your website" is thirty CAPS characters with
     label tracking, so its min-content width is 327px — and because the button
     sits inside the banner's grid column, that 327px becomes the COLUMN's
     minimum. Every element in the banner then inherits it: the title, the lede
     and both figures all measured 327px inside a 280px shell and pushed 27px
     of overflow onto the document at 320.

     Not the button's own width, the column's. That is why the fix belongs on
     the button and the symptom appeared everywhere else. It wraps here, the
     same way the closing band's runtime label does. */
  .e-dth__cta .e-btn {
    white-space: normal;
    text-align: center;
    line-height: 1.4;
  }
}

/* --- 27c. What it involves — after OFF+BRAND ------------------------------

   Reference: mobbin.com/sites/sections/dd3b3bfe-d63e-47e7-b692-64cde2a3f494

   A small index sitting ABOVE the title rather than beside it, the title
   large, hairline dividers between rows, and a short column held far left as
   the only other thing on screen.

   The index above is the reference's move and it is what gives the title the
   full measure — a number in its own column, as the wireframe drew it, turns
   the left edge of the rows into a column of figures running down the page
   and takes 46px off every line of copy for the privilege.
   -------------------------------------------------------------------------- */

/* No top padding — the full-bleed still sits flush at the top of this section
   and provides the break from the banner above it. The bottom keeps the
   section rhythm. */
.e-dti { padding-block: 0 var(--section-y); }

/* The grid needs the air the section no longer has. */
.e-dti__grid {
  /* The other half of the still's equal air. Was `--section-y`; it has to
     match `.e-dth`'s bottom padding or the image is not centred between the
     two blocks it sits between. */
  margin-top: var(--space-8);
  display: grid;
  grid-template-columns: 4fr 7fr;
  gap: var(--space-8);
  align-items: start;
}

/* The sticky column. `top` clears the 66px header with air to spare, and it
   sticks only while its own section is on screen — a sticky block that runs to
   the footer is a block that follows you into the next argument. */
.e-dti__lead {
  position: sticky;
  top: 96px;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}
.e-dti__title { max-width: 18ch; }
.e-dti__intro {
  margin: 0;
  font: var(--type-copy);
  color: var(--text-copy);
  max-width: 44ch;
}

/* Belief 03 from the About page, stated as a list on the page where someone is
   deciding. Bordered because it is a different KIND of thing from the prose
   above it — a condition, not an argument. */
.e-dti__need {
  margin-top: var(--space-3);
  padding: var(--space-5);
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  background: var(--paper);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.e-dti__need ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.e-dti__need li {
  font: var(--type-copy-small);
  color: var(--text-copy-strong);
  padding-inline-start: var(--space-5);
  position: relative;
}
/* One mark, not a bullet glyph — the same square the system uses for its
   list marks elsewhere, sized to the cap height of the line it sits on. */
.e-dti__need li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: .55em;
  width: 6px;
  height: 1px;
  background: var(--ink-900);
}

/* --- the rows ------------------------------------------------------------ */

.e-dti__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: var(--border-hairline);
}

.e-dti__row {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding-block: var(--space-6);
  border-bottom: var(--border-hairline);
}

/* Muted, and it stays muted. The index orders the rows; it does not compete
   with them, which is why it is at label size rather than the large numeral
   the wireframe draws. */
.e-dti__n {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  color: var(--text-muted);
}

/* Sentence case at body-l, not the reference's large uppercase — six rows of
   shouting on a page that already opens on a display headline. Same treatment
   as the answer panel's inclusions on the services index. */
.e-dti__name {
  margin: 0;
  font: var(--weight-medium) var(--size-body-l)/1.25 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
  text-wrap: pretty;
}

.e-dti__body {
  margin: 0;
  font: var(--type-copy-small);
  color: var(--text-copy);
  max-width: 62ch;
}

/* --- 27d. What it involves, responsive -----------------------------------

   The sticky column stops being sticky before it stops being a column. Below
   1080 the two stack, and a sticky block at the top of a stack covers the rows
   it is introducing — so it is unstuck first, at the same breakpoint.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-dti__grid { grid-template-columns: 1fr; gap: var(--space-7); }
  .e-dti__lead { position: static; top: auto; }
  .e-dti__title { max-width: 24ch; }
  .e-dti__intro { max-width: 60ch; }
}

@media (max-width: 700px) {
  .e-dti__row { padding-block: var(--space-5); }
}

/* --- 27e. The step artefact ----------------------------------------------

   The one addition the detail template makes to the reused process band. The
   wireframe draws a yellow bar under each step — the thing that step actually
   hands the client — and it is the only part of the block that differs between
   the six pages. Without it there would be no reason to repeat the section
   here rather than link back to the index.

   Yellow on purple, which is exactly what the house rule allows, and the same
   pairing the step numbers already use two lines above it.

   This element does not exist on the services index, so nothing here can reach
   its band. It sits below `.e-proq__body` and above the row's bottom edge,
   separated by its own rule — the rule divides the description of the step
   from what the step produces, which are different kinds of statement.
   -------------------------------------------------------------------------- */

.e-proq__gives {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-4);
  border-top: var(--border-on-dark);
  font: var(--type-label);
  /* The label token is set solid, which is right for a one-line caps label and
     wrong the moment it wraps — and every one of these wraps. */
  line-height: 1.45;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ebr-yellow);
  text-wrap: pretty;
}

/* `margin-top: auto` in a flex column absorbs the free space above it, so the
   artefact sits on the bottom edge of every row however long the description
   above it runs — four artefacts on four different baselines reads as four
   mistakes. Nothing needs to be added to `.e-proq__step` for this: it is
   already a flex column, and touching a component the index shares to get an
   effect only this page needs would be the wrong trade. */
.e-proq__gives { margin-top: auto; }

@media (max-width: 700px) {
  /* Stacked, there is no shared baseline to hold, and pushing the artefact to
     the bottom of a full-width row just opens a gap. */
  .e-proq__gives { margin-top: var(--space-4); }
}

/* --- 27f. Proof — after Passionfroot -------------------------------------

   Reference: mobbin.com/sites/sections/d29a1362-be5e-4639-ba1d-79d293dbbe3f

   One wide card above two half-width ones. The wide card lays its panel beside
   the text; the half cards stack theirs above it. Three objects, one row of
   two, no orphan — which is what makes this arrangement fit two cases and a
   quote better than the wireframe's own two-rows drawing.
   -------------------------------------------------------------------------- */

.e-dtc__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-bottom: var(--space-8);
  max-width: 46ch;
}
.e-dtc__title { max-width: 20ch; }
.e-dtc__sub { margin: 0; font: var(--type-copy-small); color: var(--text-copy); max-width: 48ch; }

.e-dtc__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-6);
}
.e-dtc__case--wide { grid-column: 1 / -1; }

.e-dtc__case {
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  background: var(--white);
  overflow: hidden;
}

/* The wide one only. Passionfroot puts the panel beside the text at full
   width and above it at half — the same card, told twice at two sizes. */
.e-dtc__case--wide { display: grid; grid-template-columns: 5fr 6fr; align-items: stretch; }

.e-dtc__shot {
  background: var(--ink-100);
  display: grid;
  place-items: center;
  min-height: clamp(180px, 22vw, 300px);
  padding: var(--space-4);
}
.e-dtc__todo {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

.e-dtc__body {
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
}

.e-dtc__name {
  margin: 0;
  font: var(--weight-medium) var(--size-body-l)/1.25 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
  text-wrap: pretty;
}
.e-dtc__line { margin: 0; font: var(--type-copy-small); color: var(--text-copy); max-width: 46ch; }

/* Two figures, and the definition carries the meaning — "before" and "after"
   are the whole story of a rebuild. Same size as every other figure on the
   site so a number looks like a number wherever it appears. */
.e-dtc__figs {
  display: flex;
  gap: var(--space-6);
  width: 100%;
  padding-top: var(--space-4);
  border-top: var(--border-hairline);
}
.e-dtc__fig { display: flex; flex-direction: column; gap: 4px; }
.e-dtc__figv {
  font: var(--weight-medium) var(--size-display-m)/1 var(--font-display);
  letter-spacing: var(--tracking-display);
  color: var(--text-strong);
}
.e-dtc__figd {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* A span dressed as a button, not a link — there is nothing to link to until
   the cases exist, and a link to nothing is the dead affordance this site
   rejects everywhere else. It becomes an <a> when the case does. */
.e-dtc__cta { margin-top: auto; pointer-events: none; opacity: .55; }

/* --- 27g. Proof, responsive ----------------------------------------------

   The wide card gives up its side-by-side split before the grid gives up its
   columns — a 5:6 split inside a full-width card is fine at 1440 and a pair of
   slivers at 900.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-dtc__case--wide { grid-template-columns: 1fr; }
}

@media (max-width: 860px) {
  .e-dtc__grid { grid-template-columns: 1fr; gap: var(--space-5); }
  .e-dtc__title { max-width: 24ch; }
}

@media (max-width: 560px) {
  .e-dtc__figs { flex-direction: column; gap: var(--space-4); }
}

/* --- 27h. Related services — after Headspace ------------------------------

   Headspace (mobbin.com/sites/sections/…), chosen by Adrian. Three tall cards
   on a warm paper field: a wordmark at the top, three lines of plain copy, a
   small underlined link at the foot. Hairline borders. NO IMAGES AND NO ICONS,
   which is the whole reason it beat Oyster on the sheet — six service pages
   means eighteen card slots and there is no photography for any of them. A
   placeholder in a card this small is worse than no card.

   The wordmark becomes the service name in the 11px caps label the system
   already uses, so the card's top line is a component rather than a new
   treatment.

   ⚠ THE WIREFRAME'S THIRD LINE IS CUT. It draws a grey bar reading "the two
   together look like this"; the sheet called it the one to cut if the fifteen
   pairings prove hard to write, and they are hard — that bar is a second
   editorial line per card, thirty across the template, all of them
   hand-written. One line that earns its place beats two that repeat.

   No fill on the cards. Headspace has none, and a white card on paper here
   would make three boxes on a page that already has boxed cases above.
   -------------------------------------------------------------------------- */

.e-rel__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-5);
  flex-wrap: wrap;
  margin-bottom: var(--space-7);
}
.e-rel__headings { display: flex; flex-direction: column; gap: var(--space-5); }
.e-rel__title { max-width: 20ch; }

/* Top-right, per the wireframe. It goes to the index, which exists — the five
   card links below do not have that luxury and say so. */
.e-rel__all {
  margin-left: auto;
  font: var(--type-small);
  color: var(--ink-900);
  text-decoration: none;
  border-bottom: 1px solid var(--line-hairline);
  padding-bottom: 2px;
  transition: var(--transition-hover);
}
.e-rel__all:hover,
.e-rel__all:focus-visible { border-bottom-color: currentColor; text-decoration: none; }

.e-rel__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-6);
}

.e-rel__card {
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* The service name is this card's heading, so it is asked to carry more than
   a label normally does.

   ⚠ IT CANNOT GET A HEAVIER WEIGHT. The type system ships exactly two cuts —
   TT Hoves Pro Regular (400) and Medium (500) — and `--type-label` is already
   Medium. Asking for 600 or 700 does not load a bolder face, it makes the
   browser smear the Medium one; synthetic bold at 11px with label tracking
   looks cheap in precisely the way this site avoids. A real bold would mean
   licensing and loading a third cut.

   So it gets weight the other way: full ink instead of muted grey. At 11px
   caps that is a far bigger change in presence than 500→600 would have been,
   and it costs no new font file and no third weight. */
.e-rel__card .e-label span { color: var(--ink-900); }

.e-rel__line {
  margin: 0;
  font: var(--type-copy-small);
  color: var(--text-copy);
  text-wrap: pretty;
}

/* Pinned to the foot so three cards of unequal copy still line their links up
   — Headspace's cards are tall and the link is the last thing in each. */
.e-rel__link {
  margin-top: auto;
  align-self: flex-start;
  font: var(--type-small);
  color: var(--ink-900);
  text-decoration: none;
  border-bottom: 1px solid var(--line-hairline);
  padding-bottom: 2px;
  transition: var(--transition-hover);
}
.e-rel__link:hover,
.e-rel__link:focus-visible { border-bottom-color: currentColor; text-decoration: none; }

/* --- 27i. Related services, responsive ------------------------------------

   Three to one, with no two-column step. A 2+1 row leaves one card orphaned
   beside a gap, and these cards are a label and three lines — they do not need
   the width three of them have at 1440.
   -------------------------------------------------------------------------- */

@media (max-width: 860px) {
  .e-rel__grid { grid-template-columns: 1fr; gap: var(--space-5); }
}

@media (max-width: 700px) {
  .e-rel__all { margin-left: 0; }
}

/* --- 28a. Work banner — after OFF+BRAND -----------------------------------

   OFF+BRAND (mobbin.com/sites/sections/f890a5d7-…), chosen by Adrian, and
   built to its actual arrangement: a held left column with the label and the
   heading, and the content moving past it on the right.

   THE PIN IS THE POINT. The first version of this block flattened the
   reference into a heading with a four-across figure row underneath — which
   fits on one screen, so there was nothing for a sticky column to hold
   through. Stacking the figures is what gives the pin something to do: the
   heading stays on screen while each number is read on its own, so the four
   are read as one argument rather than glanced at as a row.

   The mechanics are `.e-dti__lead`'s, deliberately. That block came from this
   same reference on the service detail page, and a second sticky
   implementation would be two ways of doing one thing that drift apart.

   Two things kept from OFF+BRAND and one refused:

     · The page states its own project count, in the label. Five declared reads
       as a curated selection; five undeclared reads as everything we could
       find. It is a third span in the existing `.e-label`.
     · The count is NOT a pill. OFF+BRAND's links to a larger archive; there is
       no larger archive here, and a pill linking to the page you are already
       on is the dead affordance this site rejects everywhere else.
     · The gradient goes. Flat fields only.
   -------------------------------------------------------------------------- */

.e-wkh {
  padding-block: var(--section-y);
  /* `isolation`, and NO `overflow`. The heading column is sticky; an ancestor
     that clips becomes the scroll container and the pin dies silently. The
     field clips itself instead. */
  position: relative;
  isolation: isolate;
}

/* --- the living ground ---------------------------------------------------

   Reused from the homepage's closing band (css 18b), not rebuilt. Same
   classes, same keyframes, same three rules — one hue, no shared cycle
   factors, slow enough to notice on the second look. Only the tint changes,
   because that band is violet-on-purple and this one is violet-on-paper: at
   the homepage's alphas these discs would be three lilac clouds sitting on top
   of a white page rather than a ground you half-notice.

   The ribbon layer is left off on purpose — it is yellow, and yellow only goes
   on dark or purple.

   ⚠ An earlier version of this block used flat shapes on the argument that the
   system has no gradients. That was over-applied: the system has no gradients
   in its COMPONENTS, and one decorative ground that already ships. Matching it
   beats being consistent with a rule it was already an exception to.
   -------------------------------------------------------------------------- */

.e-wkh .e-cta__aur--1,
.e-wayh .e-cta__aur--1 {
  background: radial-gradient(closest-side, rgba(150, 60, 255, .22), rgba(150, 60, 255, 0) 72%);
}
.e-wkh .e-cta__aur--2,
.e-wayh .e-cta__aur--2 {
  background: radial-gradient(closest-side, rgba(107, 0, 246, .17), rgba(107, 0, 246, 0) 70%);
}
.e-wkh .e-cta__aur--3,
.e-wayh .e-cta__aur--3 {
  background: radial-gradient(closest-side, rgba(190, 140, 255, .26), rgba(190, 140, 255, 0) 68%);
}

/* NO SCRIM, and that is a reversal worth recording.

   The first version put a paper scrim over the text column, copying
   `#contact::after`. It worked and it was invisible — because it covered the
   left half, and the only aurora centre outside it sat behind the opaque
   purple boxes. The animation was correct and unseeable, which is the same as
   not building it.

   The scrim existed to protect `--text-muted` (4.72:1 on paper, 2.97:1 over
   the stacked discs). The better fix is upstream: give the secondary text real
   headroom instead of hiding the ground behind a sheet of paper. `--text-body`
   is one step darker, already in the system, and clears the floor over the
   discs at full strength — so the ground can run across the whole band.

   That is the trade Adrian was offered last time, taken. */

/* Content sits above both. */
.e-wkh__grid { position: relative; z-index: 2; }

.e-wkh__grid {
  display: grid;
  /* Even halves. OFF+BRAND's left column holds only a label so it can be
     narrow; this one holds the page's heading, and at 4fr the heading had a
     23-character measure and set to six lines. */
  grid-template-columns: 1fr 1fr;
  gap: var(--space-8);
  align-items: start;
}

/* Same `top` as the detail page's sticky column, for the same reason: it
   clears the 66px header with air to spare and releases at the end of its own
   section. */
.e-wkh__lead {
  position: sticky;
  top: 96px;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.e-wkh__label { align-items: baseline; }

/* Held apart from the label it belongs to by a hairline rather than a gap — at
   11px a gap alone reads as a typo rather than as a separate field. */
.e-wkh__count {
  margin-left: var(--space-4);
  padding-left: var(--space-4);
  border-left: var(--border-hairline);
  color: var(--text-body);
}

/* No max-width. The column is the measure — a `ch` cap on top of it is a
   second opinion about the same thing, and the two disagree at every width. */
.e-wkh__title { max-width: none; }
/* `--text-body`, not `--text-muted`. One step darker, already in the system,
   and it is what lets the drifting ground run behind this text at all: muted
   measures 4.72:1 on bare paper and 2.97:1 over the discs; body clears the
   floor with room over both. The hierarchy holds — the heading is `--ink-900`
   and this is `--ink-700`, still visibly secondary. */
.e-wkh__lede { margin: 0; max-width: 48ch; color: var(--text-copy-strong); }
.e-wkh__label span { color: var(--text-body); }

/* Placeholders here get the darker secondary too. `[data-todo][data-todo]`
   was written to out-rank component colours and it does — including the ones
   above — so it lands at `--text-muted` and becomes the one thing on this band
   that cannot survive the drifting ground behind it (2.97:1). A hole still has
   to be readable while it is a hole. */
.e-wkh [data-todo][data-todo] { color: var(--text-body); }

/* Two columns of two, the second dropped — the reference's offset, and it is
   LAYOUT rather than motion. A stagger built out of scroll position is a
   stagger that does not exist until you scroll and disappears without JS; a
   `margin-top` is simply true. The parallax below moves them from here, it
   does not create the arrangement. */
.e-wkh__figs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-5);
  align-items: start;
}
.e-wkh__fig:nth-child(even) { margin-top: var(--space-9); }

/* Parallax. `--par` is written by site.js §24; with no script it stays 0 and
   the boxes sit where they are placed.

   `translate`, not `transform` — same reason as the About banner's tiles in
   §25b: these boxes carry `.reveal`, which owns `transform`. Two rules writing
   one property means whichever lands second wins and the reveal either snaps
   or never arrives.

   The two columns take opposite signs so they separate as they pass rather
   than sliding together, which is the reference's effect. Odd is the left
   column and even the right, because the boxes are in reading order — the DOM
   stays 1, 2, 3, 4 and only the paint staggers.

   RATE RAISED, NOT RANGE. These were .07 / -.05 and read as almost still.
   Roughly doubled, so the drift reaches its limit in about half the scroll and
   the movement is legible while the band is still on screen.

   The caps in the script did have to change with it, though not the distance:
   at the old rate the right column never actually reached its limit inside
   this section, and at the new one it does. The two caps now add up to the
   96px offset — 56 down on the left, 40 up on the right — so the columns meet
   exactly at full drift and never cross. The motion may flatten the
   arrangement; it may not invert it. */
.e-wkh__fig { translate: 0 var(--par, 0px); }
.e-wkh__fig:nth-child(odd)  { --d: .15; }
.e-wkh__fig:nth-child(even) { --d: -.11; }

/* Boxed rather than divided, and the arrangement is why. In a stack, three
   hairlines between four figures divide something and that is enough. In a
   2x2 the same hairlines would draw a cross through the middle of a square —
   a shape, not a division — and the figures would read as one block quartered
   rather than as four objects. So each one gets its own edge.

   Hairline and radius, no fill and no shadow: the reference's cards are filled
   panels and that is the part to refuse. */
/* Purple, per Adrian. `.e-ground-brand` in the markup carries the fill and the
   on-dark type colours; only the border needs saying, because a hairline in
   ink on a purple field is a line drawn in the wrong colour. */
.e-wkh__fig {
  border: 1px solid var(--ebr-purple);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  /* The qualifier sits on the floor of the tallest box in its row, so the four
     read as a set rather than as four different heights. */
  min-height: clamp(200px, 20vw, 260px);
}
.e-wkh__fig .e-stat__qual { margin-top: auto; padding-top: var(--space-4); }

/* The stat trio has ink and on-dark variants but no brand one — nothing had
   put a figure on a purple field before.

   ⚠ AND IT CANNOT SIMPLY BORROW THE INK ONES. `--text-on-dark-muted` is 62%
   white, which composites to 12.9:1 over `--ink-900` and to **3.29:1** over
   `--ebr-purple` — under the 4.5 minimum, because the purple is nothing like
   as dark as it looks next to paper. 82% clears it at 5.0:1 and still reads as
   secondary against the solid white figure above it. */
.e-ground-brand .e-stat__value { color: var(--white); }
.e-ground-brand .e-stat__label,
.e-ground-brand .e-stat__qual { color: var(--text-on-dark-muted); }

/* The placeholder rule learned `.e-ground-ink` and `.e-svcfield` when the
   detail page arrived; a purple box is the third dark ground and needs the
   same treatment, or every figure here is ink on violet. */
.e-ground-brand [data-todo][data-todo] { color: var(--text-on-dark-muted); }

/* The figures carry the display size here. In the four-across row they were
   competing for width; in a column they have it, and the whole point of the
   arrangement is that each one is read rather than scanned. */
.e-wkh__fig .e-stat__value { font-size: var(--size-display-l); }

/* --- 28b. Work banner, responsive ----------------------------------------

   The pin is the arrangement, so it goes before the columns do — a sticky
   column in a single-column stack pins the heading over its own figures and
   traps the reader. At 1080 the heading returns to the top and the figures
   run under it, which is the arrangement the rest of the site already uses.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-wkh__grid { grid-template-columns: 1fr; gap: var(--space-7); }
  .e-wkh__lead { position: static; top: auto; }

  .e-wkh__title { max-width: 20ch; }
  .e-wkh__lede { max-width: 60ch; }
}

@media (max-width: 700px) {
  /* One column, and the min-height goes with it — a 200px box holding one
     number and two short lines is mostly empty at this width.

     The offset and the drift both go too. In a single column an offset is just
     a bigger gap, and a parallax that separates two columns has nothing to
     separate. The script checks the same 700px edge before it writes. */
  .e-wkh__figs { grid-template-columns: 1fr; gap: var(--space-4); }
  .e-wkh__fig { padding: var(--space-5); min-height: 0; }
  .e-wkh__fig:nth-child(even) { margin-top: 0; }
  .e-wkh__fig:nth-child(odd),
  .e-wkh__fig:nth-child(even) { --d: 0; }
  .e-wkh__fig .e-stat__qual { margin-top: 0; }
}

/* The site-wide reduced-motion rule kills durations, not a written custom
   property. The script already refuses to write `--par` when the user asks for
   less motion; this is the belt to that braces, and it also covers a value
   left behind if the setting changes mid-session. */
@media (prefers-reduced-motion: reduce) {
  .e-wkh__fig { translate: none; }
}

@media (max-width: 560px) {
  .e-wkh__count { margin-left: var(--space-3); padding-left: var(--space-3); }
}

/* --- 28c. The work grid — Koto's stagger, Option B's filter row -----------

   Koto (mobbin.com/sites/sections/d820c5ae-…) for the projects: a staggered
   two-up where the right column is dropped, so the page reads as a rhythm
   rather than as a table of pictures. Option B's ledger for the pill row above
   it.

   THE STAGGER IS A MARGIN, NOT A TRANSFORM — same call as the banner boxes.
   But it cannot be `nth-child(even)` here, because filtering hides cards and
   the DOM keeps counting them: filter to three and the offset lands on
   whichever card happens to be second in the source, not second on screen.
   site.js §25 re-applies `.is-offset` to the visible cards after every change.
   -------------------------------------------------------------------------- */

.e-wkg__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin-bottom: var(--space-6);
}

/* The count shares the title's row and its baseline. It was on the label's
   row, which put a figure about the work level with the section number — two
   unrelated pieces of information reading as a pair. */
.e-wkg__titlerow {
  display: flex;
  align-items: baseline;
  gap: var(--space-6);
  flex-wrap: wrap;
}
.e-wkg__title { max-width: 20ch; }
/* A fraction, so the two halves are weighted differently: the count is the
   answer and the total is the context. Same size, different colour — no second
   type size for a number. */
.e-wkg__showing {
  margin: 0 0 0 auto;
  font: var(--type-small);
  font-variant-numeric: tabular-nums;
}
.e-wkg__shown { font-weight: var(--weight-medium); color: var(--text-strong); }
.e-wkg__of { color: var(--text-muted); }

/* --- the filter row ------------------------------------------------------ */

.e-wkg__tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-7);
}

.e-wkg__tab {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-body);
  background: none;
  border: var(--border-hairline);
  border-radius: var(--radius-pill);
  padding: 9px 16px;
  cursor: pointer;
  transition: var(--transition-hover);
}
.e-wkg__tab:hover { color: var(--ink-900); border-color: var(--ink-400); }

/* Two labelled filter rows: Service, then Industry (26 Oct 2026). The label
   sits in a fixed column on desktop so both rows of pills start on one line;
   on phones it goes above its row. */
.e-wkg__filters {
  display: grid;
  gap: var(--space-4);
  margin-bottom: var(--space-7);
}
.e-wkg__filters .e-wkg__tabs { margin-bottom: 0; }
.e-wkg__frow {
  display: grid;
  grid-template-columns: 112px minmax(0, 1fr);
  align-items: center;
  gap: var(--space-4);
}
.e-wkg__frow[hidden] { display: none; }

/* The Industry row folds behind its label. The panel animates its height with
   the grid 0fr → 1fr trick, so no height is measured in script; the inner
   wrapper is what gets clipped. Closed panels are `inert` (set in site.js) so
   their pills cannot be tabbed into while invisible. */
.e-wkg__ftoggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  justify-self: start;
  background: none;
  border: 0;
  padding: 6px 0;
  cursor: pointer;
  text-align: left;
  transition: color .2s ease;
}
.e-wkg__ftoggle:hover,
.e-wkg__ftoggle[aria-expanded="true"] { color: var(--ink-900); }
.e-wkg__ftoggle:focus-visible { outline: 2px solid var(--ebr-purple); outline-offset: 4px; border-radius: 2px; }
.e-wkg__ftoggle { white-space: nowrap; position: relative; z-index: 1; }
/* Closed with a choice in force: the toggle names it, on one line, running
   into the (empty, collapsed) pill column. Open, the pills say it instead. */
.e-wkg__ftoggle[aria-expanded="true"] .e-wkg__fcurrent { display: none; }
.e-wkg__fcurrent:not(:empty) { color: var(--ink-900); }
.e-wkg__fcurrent:not(:empty)::before { content: "· "; color: var(--text-muted); }
.e-wkg__ficon { flex: none; transition: transform .3s cubic-bezier(.2,.7,.2,1); }
.e-wkg__ftoggle[aria-expanded="true"] .e-wkg__ficon { transform: rotate(45deg); }

.e-wkg__fpanel {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows .35s cubic-bezier(.2,.7,.2,1), opacity .25s ease;
}
.e-wkg__fpanel.is-open { grid-template-rows: 1fr; opacity: 1; }
.e-wkg__fpanelin { min-height: 0; overflow: hidden; }
/* Room for the pill borders and focus ring inside the clip. */
.e-wkg__fpanelin > .e-wkg__tabs { padding: 3px 3px 3px 0; }
@media (prefers-reduced-motion: reduce) {
  .e-wkg__fpanel, .e-wkg__ficon { transition: none; }
}
/* No script, no fold: the panel is simply open (and empty, and the row hidden). */
html:not(.js) .e-wkg__fpanel { grid-template-rows: 1fr; opacity: 1; }
.e-wkg__flabel {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
@media (max-width: 700px) {
  .e-wkg__frow { grid-template-columns: 1fr; gap: var(--space-3); }
}

/* Both filters together matched nothing. */
.e-wkg__empty {
  margin: calc(-1 * var(--space-4)) 0 var(--space-7);
  font: var(--type-body);
  color: var(--text-body);
}
.e-wkg__empty[hidden] { display: none; }
.e-wkg__reset {
  font: inherit;
  color: var(--ink-900);
  background: none;
  border: 0;
  padding: 0;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

/* Ink, not purple. Purple on this page already means "a figure" — the banner
   boxes are solid violet — and one colour cannot carry two meanings on one
   screen. Ink says "selected" without spending anything. */
.e-wkg__tab.is-on {
  background: var(--ink-900);
  border-color: var(--ink-900);
  color: var(--white);
}

/* --- the staggered grid -------------------------------------------------- */

.e-wkg__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-7) var(--space-6);
  align-items: start;
}
.e-wkg__card.is-offset { margin-top: var(--space-9); }
.e-wkg__card[hidden] { display: none; }

.e-wkg__card { display: flex; flex-direction: column; gap: var(--space-5); }

.e-wkg__shot {
  aspect-ratio: 4 / 3;
  background: var(--ink-100);
  border-radius: var(--radius-media);
  display: grid;
  place-items: center;
  /* The slot had NO rule for a real image, only for the grey placeholder, so
     the first still dropped in rendered at its intrinsic width — 1920px inside
     a 632px card — and took the page's horizontal scroll with it. Verified in
     the browser on 20 Oct 2026 before this was added.

     `overflow: hidden` rather than a radius on the image itself, so the crop
     and the rounded corner are one decision and a <picture> or a <video> in
     here later inherits both. */
  overflow: hidden;
  position: relative;
}

/* Cover, not contain. These stills are 16:10 screenshots going into a 4:3 slot
   (21:9 on the wide card), so something has to give: cover crops the long edge
   and fills the card, contain would letterbox it on grey and make every card a
   different shape. Crop the file to taste if the top of a screenshot matters —
   the CSS takes from both edges equally.

   ⚠ ABSOLUTE, NOT A GRID ITEM. In flow, `height: 100%` resolves against the
   grid ROW, and a row sized by its content grows past the `aspect-ratio` box
   whenever the image is taller than the slot. That is invisible on the 4:3
   cards — a 16:10 still is shorter than 4:3, so the row never grows — and
   wrong on the 21/9 wide card, where it measured 395px inside a 271px slot.
   Taking the image out of flow makes it fill the aspect box at both ratios. */
.e-wkg__shot img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.e-wkg__todo,
.e-wkg__figlabel {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

.e-wkg__body { display: flex; flex-direction: column; gap: var(--space-4); }

.e-wkg__top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); }
.e-wkg__name { margin: 0; font: var(--weight-medium) var(--size-body-l)/1.3 var(--font-display); letter-spacing: var(--tracking-title); color: var(--text-strong); }

/* A circle with an arrow, inert until there is a case to open — the same
   dressed-as-a-button span the proof block uses, for the same reason. */
.e-wkg__go {
  width: 34px; height: 34px;
  flex-shrink: 0;
  border: var(--border-hairline);
  border-radius: var(--radius-pill);
  display: grid;
  place-items: center;
  color: var(--text-muted);
}

.e-wkg__line { margin: 0; font: var(--type-copy-small); color: var(--text-copy-strong); text-wrap: pretty; }

/* The hairline divides the description from the evidence — two different kinds
   of statement, which is what earns the rule. */
.e-wkg__foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  /* Matches the 24px row gap that sits above the rule. A dividing rule with
     more air on one side than the other reads as a mistake, not a division. */
  padding-top: var(--space-5);
  border-top: var(--border-hairline);
}
.e-wkg__result { display: flex; flex-direction: column; gap: var(--space-3); }

/* The one purple on this section, and it is the one the wireframe asks for:
   the result figure. Nothing else here competes for it. */
.e-wkg__figure {
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: var(--size-display-m);
  line-height: 1;
  letter-spacing: var(--tracking-title);
  color: var(--ebr-purple);
}

.e-wkg__pills { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-3); }
.e-wkg__pills li {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-body);
  background: var(--ink-100);
  border-radius: var(--radius-pill);
  padding: 6px 11px;
}

/* --- the closing row ----------------------------------------------------- */

.e-wkg__closer {
  margin-top: var(--space-7);
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: var(--space-6);
  align-items: start;
}
/* When the fifth project filters out, the note must not sit in a 1.6fr slot
   with nothing beside it. site.js §25 sets this. */
.e-wkg__closer.is-solo { grid-template-columns: 1fr; }

.e-wkg__card--wide .e-wkg__shot { aspect-ratio: 21 / 9; }

.e-wkg__intake {
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  align-items: flex-start;
}
.e-wkg__intakecopy { margin: 0; font: var(--type-copy-small); color: var(--text-copy-strong); }

/* --- 28d. The work grid, responsive --------------------------------------

   The stagger goes at 860, with the columns. An offset inside a single column
   is a bigger gap and nothing else — the same call the banner boxes make, at
   the same breakpoint, so the page changes shape once rather than twice.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  .e-wkg__closer { grid-template-columns: 1fr; }
}

@media (max-width: 860px) {
  .e-wkg__grid { grid-template-columns: 1fr; gap: var(--space-6); }
  .e-wkg__card.is-offset { margin-top: 0; }
  .e-wkg__title { max-width: 24ch; }
}

@media (max-width: 560px) {
  .e-wkg__showing { margin-left: 0; }
  .e-wkg__foot { flex-direction: column; align-items: flex-start; gap: var(--space-4); }
}

/* --- 28e. The numbers band — after Employment Hero -------------------------

   A purple field carrying four aggregate figures. `.e-stats` does the grid and
   the type; only what the brand ground needs is written here.

   HELD LEFT AND NO BUTTON. Employment Hero centres everything and closes with
   a CTA; both were refused on the reference sheet. This band is evidence, and
   the offer is the closing band further down.
   -------------------------------------------------------------------------- */

.e-wkn__title { max-width: 20ch; margin-top: var(--space-5); }
.e-wkn__lede { margin-top: var(--space-5); max-width: 54ch; }

.e-wkn__figs { margin-top: var(--space-8); }

/* The figures are yellow — the one screen on this page where the house rule
   allows it, because yellow only goes on dark or purple. */
.e-wkn .e-stat__value {
  color: var(--ebr-yellow);
  font-size: var(--size-display-l);
}

/* `.e-stats` knew ink and paper but not brand: on a purple field its hairlines
   were being drawn in `--ink-200`, a pale grey line over violet. */
.e-ground-brand .e-stats,
.e-ground-brand .e-stats > * { border-color: var(--line-on-dark); }

/* No vertical rules, per the reference and the wireframe both. `.e-stats` puts
   a hairline between every cell, which earns its place on paper; at 18% white
   over a saturated field it is neither visible enough to divide anything nor
   invisible enough to be absent. The single rule above the row does the work
   and the gaps do the rest. */
.e-wkn .e-stats > * {
  border-right: 0;
  padding-inline: 0;
  padding-right: var(--space-6);
}

/* --- 28f. The numbers band, responsive ------------------------------------ */

@media (max-width: 860px) {
  .e-wkn .e-stats { grid-template-columns: 1fr 1fr; }
  .e-wkn .e-stats > * { padding-block: var(--space-5); }
  .e-wkn__title { max-width: 24ch; }
}

@media (max-width: 560px) {
  .e-wkn .e-stats { grid-template-columns: 1fr; }
}

/* --- 28g. Two brand grounds meeting -------------------------------------

   Completes a set: the site already draws a hairline where two grounds of the
   same value meet (`.e-ground-white + .e-ground-white` and the paper pair) and
   brand was the one ground with no such rule.

   It was added when the work page briefly ran its purple numbers band straight
   into a purple closing band. That is no longer the case — the closing band is
   ink now, because a hairline between two large fields of one hue is a seam
   rather than a separation and the right fix was not to draw a better line.
   The rule stays because the next two brand grounds that meet should not have
   to rediscover it. */

.e-ground-brand + .e-ground-brand { border-top: var(--border-on-dark); }
/* Proof and On camera are both ink and sit back to back on the homepage.
   Adrian (20 Oct 2026): no dividing line, and one even gap rather than two
   paddings stacked. So On camera drops its top padding and the gap becomes
   Proof's own bottom padding, the same as Proof's top. Phones only show On
   camera (Proof is hidden at 700px and below), so it keeps its padding
   there. */
@media (min-width: 701px) {
  .e-proof + #video-testimonials { padding-top: 0; }
}

/* ==========================================================================
   29. THE CASE STUDY TEMPLATE
   ========================================================================== */

/* --- 29a. The opening — after Twenty --------------------------------------

   BUILT AS TWENTY, which means the split: the still and the copy side by side,
   the still running off the left edge of the page. The wireframe stacks them
   and puts a full-bleed still underneath; taking the reference means taking
   the arrangement, not just borrowing its parts.

   The bleed survives — it moves from the bottom edge to the left one — so the
   page keeps its edge-to-edge moment and the opening reads as one object
   rather than a claim followed by a picture.

   (Section 29b, the standalone full-bleed still, is gone with it — the still
   is in here now.)
   -------------------------------------------------------------------------- */

.e-csh { padding-block: var(--section-y) 0; }
.e-csh__crumbs { margin-bottom: var(--space-7); }

.e-csh__split {
  display: grid;
  /* `minmax(0, …)` on both, or the panel's longest caps caption sets the
     column width and the split stops being a proportion. */
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  align-items: stretch;
  gap: var(--space-8);
}

/* The still bleeds off the left edge. No radius on that side — a rounded
   corner against the viewport edge is a corner with nothing to be rounded
   against. */
.e-csh__shot {
  background: var(--ink-100);
  border-radius: 0 var(--radius-media) var(--radius-media) 0;
  min-height: clamp(320px, 42vw, 620px);
  display: grid;
  place-items: center;
}
.e-csh__shottodo,
.e-csg__todo {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: center;
}

/* The panel keeps the page margin on its right; its left edge is the gap. */
.e-csh__panel {
  padding-right: var(--page-margin);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  justify-content: center;
  max-width: calc(var(--max-width) / 2);
}

.e-csh__sector { color: var(--text-muted); }
.e-csh__title { max-width: 18ch; }

/* Twenty's inset quote — behind a rule rather than boxed. Boxed it becomes a
   testimonial; inset it is the client talking before anything is claimed,
   which is what makes the figures beneath read as agreement. */
.e-csh__pull {
  margin: var(--space-3) 0 0;
  padding-left: var(--space-5);
  border-left: 2px solid var(--ebr-purple);
  font: var(--weight-medium) var(--size-body-l)/1.45 var(--font-display);
  letter-spacing: var(--tracking-title);
  max-width: 42ch;
  text-wrap: pretty;
}

/* Twenty's compact figure row: value, caps caption beneath, vertical rules
   between. `.e-stats` already is this — its dividers are the reference's, and
   `--plain` had removed the thing that stops four numbers reading as one
   sentence. */
.e-csh__figs { margin-top: var(--space-4); }
.e-csh__figs > * { padding-inline: var(--space-4); }
.e-csh__figs > :first-child { padding-left: 0; }
.e-csh__figs > :first-child .e-stat__value:not([data-todo]) { color: var(--ebr-purple); }

/* Twenty closes the panel on the person who said the quote. The circular arrow
   it puts opposite is not here: it leads to the case study, and this is the
   case study. */
.e-csh__said {
  margin-top: var(--space-4);
  padding-top: var(--space-5);
  border-top: var(--border-hairline);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  font: var(--type-small);
}
.e-csh__avatar {
  width: 34px; height: 34px;
  border-radius: var(--radius-pill);
  background: var(--ink-100);
  flex-shrink: 0;
}
.e-csh__name { color: var(--text-strong); }
.e-csh__role { color: var(--text-muted); }

/* --- 29c. The constraint — after Vizcom ----------------------------------

   Vizcom's three columns: a narrow intro on the left, then two facing lists.
   Its lists pair problem against solution; these pair WHAT THEY CAME WITH
   against WHAT WAS ACTUALLY WRONG, which keeps the structure and changes the
   argument from a feature table to a diagnosis — and keeps the solution where
   it belongs, in block 03.

   NO RED AND GREEN. Vizcom marks its two lists with a red cross and a green
   tick. Neither exists in this system, and adding both for one block is two
   new colours to maintain forever. The symptom takes a muted cross, the
   diagnosis a solid arrow — the same contrast made out of weight.
   -------------------------------------------------------------------------- */

.e-csc__grid {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 5fr) minmax(0, 5fr);
  gap: var(--space-8);
  align-items: start;
}
.e-csc__lead { display: flex; flex-direction: column; gap: var(--space-5); }
.e-csc__title { max-width: 14ch; }
.e-csc__body { margin: 0; font: var(--type-copy-small); color: var(--text-copy-strong); }

.e-csc__col { display: flex; flex-direction: column; gap: var(--space-5); }

/* Their words live in the intro column. They were at the head of "what they
   came to us with", which reads better in isolation and was wrong in place: it
   pushed that list ~90px below the facing one, and two lists that do not align
   row-for-row cannot be compared row-for-row. The pairing is the argument. */
.e-csc__said {
  margin: var(--space-3) 0 0;
  padding-left: var(--space-5);
  border-left: 2px solid var(--line-strong);
  font: var(--weight-medium) var(--size-body-l)/1.4 var(--font-display);
  letter-spacing: var(--tracking-title);
  text-wrap: pretty;
}

.e-csc__list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; }
.e-csc__item {
  display: flex;
  gap: var(--space-4);
  padding-block: var(--space-5);
  border-top: var(--border-hairline);
}
.e-csc__item:last-child { border-bottom: var(--border-hairline); }

/* The marker sits on the first line's optical centre rather than its box top —
   a 12px icon aligned to a 14px line box reads as floating. */
.e-csc__mark { flex-shrink: 0; margin-top: 5px; color: var(--text-muted); }
.e-csc__mark--found { color: var(--ink-900); }

.e-csc__itemtext { display: flex; flex-direction: column; gap: var(--space-3); font: var(--type-copy-small); color: var(--text-copy); }
.e-csc__itemtext strong { font-weight: var(--weight-medium); color: var(--text-strong); }

/* The spec answers "is this company like mine?", which is an introduction —
   so it lives in the intro column rather than beside the lists. */
.e-csc__spec {
  margin: var(--space-3) 0 0;
  background: var(--surface-muted);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}
.e-csc__spec > div { display: flex; flex-direction: column; gap: var(--space-3); }
.e-csc__spec dt {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.e-csc__spec dd { margin: 0; font: var(--type-small); color: var(--text-strong); }

/* The number on the problem closes the diagnosis column, because that is the
   column it measures. It is deliberately not in the opening figure row: that
   row is what the project achieved, this is what it started from, and together
   they would let a reader take the good one for both. */
.e-csc__prob {
  margin-top: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  align-items: flex-start;
}
.e-csc__probfig {
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: var(--size-display-m);
  line-height: 1;
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
}
.e-csc__problbl { font: var(--type-small); color: var(--text-muted); max-width: 32ch; }

/* --- 29d. What we changed — after Anima ----------------------------------

   Full-width rows divided by hairlines, not three columns: title and copy
   left, a wide gap, the artefact right.

   ROWS BEAT COLUMNS HERE and it is the part of the reference that costs
   nothing. Three columns force three moves of equal length; rows do not care,
   so a decision that needs sixty words stops having to be cut to match one
   that needs twenty.

   The right-hand slot is Anima's image slot, holding WHAT SHIPPED because of
   the decision — the artefact the wireframe drew as a yellow bar and could
   never show. ⚠ It costs three more stills per case study, fifteen across the
   template.
   -------------------------------------------------------------------------- */

.e-csm__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.e-csm__meta {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
}

.e-csm__title { max-width: 22ch; margin-top: var(--space-5); }

.e-csm__rows { margin-top: var(--space-8); }

/* One rule per row and one closing the set — three lines dividing three
   things, which is what earns them. */
.e-csm__row {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 4fr);
  gap: var(--space-9);
  align-items: start;
  padding-block: var(--space-7);
  border-top: var(--border-on-dark);
}
.e-csm__rows > :last-child { border-bottom: var(--border-on-dark); }

.e-csm__body { display: flex; flex-direction: column; gap: var(--space-4); align-items: flex-start; }

/* The numbered marker, kept from Grammarly: outlined rather than filled,
   because a filled yellow disc would be a second yellow object per row once
   the shipped label is counted. */
.e-csm__n {
  width: 34px;
  height: 34px;
  border: 1px solid var(--ebr-yellow);
  border-radius: var(--radius-pill);
  display: grid;
  place-items: center;
  font: var(--weight-medium) var(--size-small)/1 var(--font-display);
  color: var(--ebr-yellow);
}

/* ⚠ Same size as the section heading above. Anima inverts that — small header,
   large row titles, because the rows are the content. Flagged rather than
   decided: either these shrink or the section heading gives way to its label. */
.e-csm__rowtitle {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: var(--size-display-m);
  line-height: var(--leading-title);
  letter-spacing: var(--tracking-title);
  color: var(--white);
  max-width: 18ch;
  text-wrap: pretty;
}
.e-csm__copy { margin: 0; font: var(--type-copy-small); color: var(--text-on-dark-muted); max-width: 50ch; }

.e-csm__shipped { margin: 0; display: flex; flex-direction: column; gap: var(--space-4); }
/* A short landscape strip, which is Anima's proportion. At 16:10 the still was
   ~330px tall against ~200px of copy beside it, so every row carried 130px of
   void under its own argument — the row's height was being set by the picture
   rather than by what it says. */
.e-csm__shot {
  aspect-ratio: 21 / 9;
  border: var(--border-on-dark);
  border-radius: var(--radius-media);
  display: grid;
  place-items: center;
}
.e-csm__shottodo {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
  text-align: center;
}

/* The yellow survives as the caption's rule rather than as a bar of its own —
   it is naming what the still above it is. */
.e-csm__ship {
  padding-top: var(--space-4);
  border-top: 2px solid var(--ebr-yellow);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.e-csm__shiplbl {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ebr-yellow);
}
.e-csm__shipval { font: var(--type-small); color: var(--white); }

/* --- 29e. The work itself — after Pentagram ------------------------------

   Deliberately unequal ratios, every still captioned. The sizes are set here
   rather than left to the images, so the rhythm survives whatever gets shot.
   -------------------------------------------------------------------------- */

/* Pentagram's header: heading left, count opposite on the same baseline. */
.e-csg__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.e-csg__headings { display: flex; flex-direction: column; gap: var(--space-5); }
.e-csg__title { max-width: 20ch; }
.e-csg__count { margin: 0 0 0 auto; font: var(--type-small); }
.e-csg__set { margin-top: var(--space-8); display: flex; flex-direction: column; gap: var(--space-7); }
.e-csg__row { display: grid; gap: var(--space-6); }
/* PENTAGRAM'S PAIRING: a small still beside a much larger one, mirrored on
   the next row. The wireframe draws three different rhythms; this is one
   rhythm alternating, which the eye learns by the second row and then stops
   noticing — the point being to look at the work rather than the grid. */
/* ⚠ TOP-ALIGNED, NOT BOTTOM. This was `align-items: end`, which lines the two
   images up on their lower edge and lands both captions at the same height —
   a tidy row, and the opposite of what the reference does.

   Pentagram top-aligns images of genuinely different heights, so each caption
   sits directly under its own image and the two land at different levels. That
   staggering is the whole effect: it is what makes the page read as a
   considered scatter rather than a grid with one cell stretched. */
.e-csg__row--sl { grid-template-columns: 1fr 2fr; align-items: start; }
.e-csg__row--ls { grid-template-columns: 2fr 1fr; align-items: start; }

/* The narrow column carries two stacked stills against the wide column's one
   tall one. Two 3:2 plus a gap is 594px against 632px at 1440 — near enough
   level by arithmetic rather than by luck, so the rows close without a void
   under the small image. */
.e-csg__stack { display: flex; flex-direction: column; gap: var(--space-6); }

.e-csg__item { margin: 0; }
.e-csg__shot {
  background: var(--ink-100);
  border-radius: var(--radius-media);
  display: grid;
  place-items: center;
  aspect-ratio: 3 / 2;
}
/* ⚠ THE PROPORTIONS WERE INVERTED. The small still was portrait (4:5) and the
   large one wide (16:9), which makes the narrow column the TALL one and cancels
   the stagger — the two images finished at roughly the same depth.

   Pentagram's small item is a short landscape block and the large one is much
   taller, so the big image runs well past the small one and its caption drops
   with it. Small 3:2, large 4:3: at 1440 that is ~267px against ~600px. */
.e-csg__item--sm .e-csg__shot { aspect-ratio: 3 / 2; }
.e-csg__item--lg .e-csg__shot { aspect-ratio: 4 / 3; }

/* ⚠ NO CAPTIONS, on Adrian's instruction. The two-register caption (what it
   is, in ink; what it is doing differently, muted) is removed — and the
   wireframe's own note argued the other way: "an uncaptioned gallery is a mood
   board." That argument still stands; this is one block of markup to restore
   if it is ever wanted back. */

/* The third row is an equal pair — after two staggered rows the eye has the
   rhythm, and a third repetition of it would start to look like a rule rather
   than a rhythm. */
.e-csg__row--pair { grid-template-columns: 1fr 1fr; align-items: start; }

/* --- 29f. What moved — after Lightdash -----------------------------------

   Lightdash's own order: the quote first in a dark card, the figures below as
   three SEPARATE bordered cards. Joined into one grid — which is what this
   block was — three cells share their edges and read as a table; separated,
   each figure is its own object with its own claim.

   The dark card does structural work as well as holding a quote: after paper,
   purple, paper and white it is the hard stop that resets the eye before three
   numbers arrive.
   -------------------------------------------------------------------------- */

.e-csr__title { max-width: 20ch; margin-top: var(--space-5); }

.e-csr__quote {
  margin: var(--space-8) 0 0;
  border-radius: var(--radius-md);
  padding: var(--space-8);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "avatar said" "avatar who";
  gap: var(--space-5) var(--space-6);
  align-items: start;
}
.e-csr__avatar {
  grid-area: avatar;
  width: 56px; height: 56px;
  border-radius: var(--radius-pill);
  background: var(--line-on-dark);
}
.e-csr__said {
  grid-area: said;
  margin: 0;
  font: var(--weight-medium) var(--size-display-m)/1.3 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--white);
  max-width: 26ch;
  text-wrap: pretty;
}
.e-csr__who { grid-area: who; display: flex; align-items: baseline; gap: var(--space-4); flex-wrap: wrap; font: var(--type-small); }
.e-csr__name { color: var(--white); }
.e-csr__role { color: var(--text-on-dark-muted); }

/* Three separate cards, per the reference. */
.e-csr__grid {
  margin-top: var(--space-6);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-6);
}
.e-csr__cell {
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  display: flex;
  gap: var(--space-5);
  align-items: baseline;
}
.e-csr__fig {
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: var(--size-display-m);
  line-height: 1;
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
  flex-shrink: 0;
}
.e-csr__cell--first .e-csr__fig:not([data-todo]) { color: var(--ebr-purple); }
.e-csr__text { display: flex; flex-direction: column; gap: var(--space-3); }
.e-csr__lbl { font: var(--type-small); color: var(--text-strong); }

/* THE LINE THE WHOLE BLOCK IS FOR. Not one of the thirty references on the
   sheet had it — Zendesk came closest by writing long, specific labels. Set
   small and muted because it is not the claim, it is the receipt. */
.e-csr__basis {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--text-muted);
}

/* --- 29g. Related work & services — after Jasper and Canva ---------------- */

.e-csn__head {
  display: flex;
  align-items: flex-end;
  gap: var(--space-5);
  flex-wrap: wrap;
  margin-bottom: var(--space-7);
}
.e-csn__headings { display: flex; flex-direction: column; gap: var(--space-5); }
.e-csn__title { max-width: 16ch; }
.e-csn__all { margin-left: auto; }

.e-csn__next { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6); }
.e-csn__proj { display: flex; flex-direction: column; gap: var(--space-5); }
.e-csn__shot {
  background: var(--ink-100);
  border-radius: var(--radius-media);
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 9;
}
.e-csn__body { display: flex; flex-direction: column; gap: var(--space-4); }
.e-csn__name {
  margin: 0;
  font: var(--weight-medium) var(--size-body-l)/1.3 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
}
/* The figure sits UNDER the still, not over it — unlike Jasper. These stills
   are real work and typing over them competes with what they are showing. */
.e-csn__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  padding-top: var(--space-4);
  border-top: var(--border-hairline);
}
.e-csn__line { font: var(--type-copy-small); color: var(--text-copy); }

/* JASPER ENDS EVERY CARD ON A READ LINK. The wireframe relies on the card
   being clickable, which is fine on a mouse and invisible on a phone. Inert
   until the case exists, like every other placeholder control here. */
.e-csn__read {
  font: var(--type-small);
  color: var(--text-strong);
  border-bottom: 1px solid var(--line-hairline);
  padding-bottom: 2px;
  align-self: flex-start;
  opacity: .55;
}
.e-csn__fig {
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: var(--size-display-m);
  line-height: 1;
  letter-spacing: var(--tracking-title);
  color: var(--ebr-purple);
  flex-shrink: 0;
}

/* The service cards are `.e-rel` from the service detail page, reused whole. */
.e-csn__svcs {
  margin-top: var(--space-8);
  padding-top: var(--space-7);
  border-top: var(--border-hairline);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-6);
}

/* --- 29h. The case study, responsive -------------------------------------

   Two edges, matching the rest of the site: the two-column blocks give up at
   1080 and the three-column ones at 860.
   -------------------------------------------------------------------------- */

@media (max-width: 1080px) {
  /* The intro takes the full width and the two lists stay facing each other —
     the pairing IS the argument, so it is the last thing to give up. */
  .e-csc__grid { grid-template-columns: 1fr 1fr; gap: var(--space-7); }
  .e-csc__lead { grid-column: 1 / -1; }
  .e-csc__title { max-width: 22ch; }
  .e-csc__body { max-width: 62ch; }
  .e-csc__spec { flex-direction: row; flex-wrap: wrap; gap: var(--space-7); }
  .e-csc__spec > div { flex: 1 1 160px; }
  .e-csh__title { max-width: 24ch; }

  /* The split stacks and the still stops bleeding — at this width a
     left-bleeding image beside a narrow column is a sliver of picture next to
     a column of wrapped words. Full width above the panel instead. */
  .e-csh__split { grid-template-columns: 1fr; gap: var(--space-7); }
  .e-csh__shot { border-radius: var(--radius-media); margin-inline: var(--page-margin); min-height: clamp(240px, 42vw, 420px); }
  .e-csh__panel { padding-inline: var(--page-margin); max-width: none; }
}

@media (max-width: 860px) {
  /* The row stacks and the artefact goes under its own argument, which is the
     order it is read in anyway. */
  .e-csm__row { grid-template-columns: 1fr; gap: var(--space-6); }
  .e-csm__rowtitle { max-width: 22ch; }
  .e-csr__grid { grid-template-columns: 1fr; }
  .e-csn__svcs { grid-template-columns: 1fr; gap: var(--space-5); }
  .e-csn__next { grid-template-columns: 1fr; }
  .e-csg__row--sl,
  .e-csg__row--ls,
  .e-csg__row--pair { grid-template-columns: 1fr; }
  .e-csg__stack { gap: var(--space-6); }
  /* Stacked, the two sizes have nothing to stagger against, so they match. */
  .e-csg__item--lg .e-csg__shot { aspect-ratio: 3 / 2; }
  .e-csr__cell { flex-direction: column; align-items: flex-start; gap: var(--space-4); }
  /* The avatar drops above the quote rather than beside it — at this width a
     56px column next to a wrapped quote is mostly gutter. */
  .e-csr__quote { grid-template-columns: minmax(0, 1fr); grid-template-areas: "avatar" "said" "who"; padding: var(--space-6); }
}

@media (max-width: 700px) {
  .e-csg__count { margin-left: 0; }
  /* Only now do the two lists stack. Below this width they were two very
     narrow columns of wrapped fragments, which is worse than losing the
     side-by-side reading. */
  .e-csc__grid { grid-template-columns: 1fr; }
  .e-csr__quote { padding: var(--space-6); }
  .e-csn__all { margin-left: 0; }
}

@media (max-width: 560px) {
  /* Four figures will not sit in a row at this width; two by two, which is
     what `.e-stats` already does elsewhere on the site. */
  .e-csh__figs { grid-template-columns: 1fr 1fr; }
  .e-csh__figs > :nth-child(2n+1) { padding-left: 0; }
  .e-csc__spec { flex-direction: column; gap: var(--space-5); }
  .e-csn__foot { flex-direction: column; align-items: flex-start; gap: var(--space-4); }
}

/* ⚠ A PLACEHOLDER MUST NEVER LAND ON A FILLED CONTROL.

   The case study's closing CTA briefly carried `data-todo` because its label
   names the sector. On an ink ground that rule paints 62% white — which was
   then sitting on the button's own YELLOW fill, at a contrast nothing would
   pass. The audit missed it because it skips elements with their own
   background.

   The rule is the fix: a hole belongs on text, never on a FILLED control.
   Where such a label is not final, say so in the label instead.

   Scoped to the filled variants only. Written against `.e-btn` it also caught
   the service detail page's outline "Read the case" placeholder and darkened
   it from muted to body — a placeholder on an unfilled control has no contrast
   problem and should stay a hole. */
[data-todo].e-btn--accent,
[data-todo].e-btn--primary { color: inherit; }

/* The breadcrumb's current page is a placeholder until the client is named,
   and the crumb component sets its own colour. */
.e-dth__crumbs [data-todo][data-todo] { color: var(--text-muted); }

/* The one work card that has a case study behind it. The other four keep the
   inert span, so the arrow is only ever a control where there is somewhere to
   go. */
.e-wkg__namelink { color: inherit; text-decoration: none; }
.e-wkg__namelink:hover { color: var(--ebr-purple); text-decoration: none; }
.e-wkg__go--live { color: var(--ink-900); transition: var(--transition-hover); }
.e-wkg__go--live:hover { border-color: var(--ink-900); color: var(--ebr-purple); }

/* --- 30. Placeholders do not grey out headings ---------------------------

   `[data-todo]` mutes a hole so it cannot be mistaken for an answer, and that
   is right for a value, a caption or a line of body copy. Applied to a
   HEADING it does something else: it flattens the page's type hierarchy, so
   what should read as a heading reads as more grey text, and the design
   becomes unjudgeable while the copy is still being written.

   Headings keep their real colour. That they are provisional is carried by
   what they say — "What changed, stated as the change." is not going to be
   mistaken for finished copy — which is the same reasoning that keeps
   `data-todo` off a filled button.

   Written per ground rather than with `inherit`, because a heading on purple
   is white and a heading on paper is ink, and `inherit` would give the wrong
   one on at least one of them.
   -------------------------------------------------------------------------- */

/* Doubled attributes throughout, and it is not decoration: the placeholder
   rule is `[data-todo][data-todo]` at (0,2,0), so `h3[data-todo]` at (0,1,1)
   loses to it and the move titles came out muted white on the purple band.
   Anything overriding that rule has to out-rank it, not merely follow it. */
.e-display[data-todo][data-todo],
h3[data-todo][data-todo] { color: var(--text-strong); }

.e-ground-ink .e-display[data-todo][data-todo],
.e-ground-ink h3[data-todo][data-todo],
.e-ground-brand .e-display[data-todo][data-todo],
.e-ground-brand h3[data-todo][data-todo] { color: var(--white); }

/* ==========================================================================
   31. INSIGHTS — the listing and the article
   ========================================================================== */

/* --- 31a. The listing ----------------------------------------------------

   No wireframe exists for Insights, so this is assembled from what the site
   already does: the work page's banner shape and its stated count, and the
   homepage's `.e-post` card reused whole rather than redrawn.
   -------------------------------------------------------------------------- */

.e-ins__head {
  display: flex;
  align-items: flex-end;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.e-ins__headings { display: flex; flex-direction: column; gap: var(--space-5); }
.e-ins__title { max-width: 18ch; }
.e-ins__subtitle { max-width: 20ch; }
.e-ins__all { margin-left: auto; }

/* Same fraction as the work page's project count, same reason. */
/* Stacked variant: label above, then the title row carrying the count. */
.e-ins__head--stack { flex-direction: column; align-items: stretch; gap: var(--space-5); }
.e-ins__titlerow { display: flex; align-items: baseline; gap: var(--space-6); flex-wrap: wrap; }
.e-ins__count { margin: 0 0 0 auto; font: var(--type-small); font-variant-numeric: tabular-nums; }
.e-ins__shown { font-weight: var(--weight-medium); color: var(--text-strong); }
.e-ins__of { color: var(--text-muted); }

.e-ins__lede { margin-top: var(--space-6); max-width: 54ch; color: var(--text-copy-strong); }

.e-ins__grid { margin-top: var(--space-8); gap: var(--space-7) var(--space-6); }
.e-ins__grid--two { grid-template-columns: repeat(2, minmax(0, 1fr)); }

.e-ins__meta { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
.e-ins__date { font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-muted); }
.e-ins__blurb { font: var(--type-copy-small); color: var(--text-copy-strong); }

.e-ins__read {
  margin-top: var(--space-3);
  align-self: flex-start;
  font: var(--type-small);
  color: var(--text-strong);
  border-bottom: 1px solid var(--line-hairline);
  padding-bottom: 2px;
}
.e-ins__post:hover .e-ins__read { border-bottom-color: currentColor; }

/* Two of the three are not written. They keep the homepage's own titles —
   EBR's copy, not invented — but there is nothing behind them, so they are
   `<article>` not `<a>`, they do not take the hover, and the read link says
   what it is. A card that looks clickable and is not is the dead affordance
   this site rejects everywhere else. */
.e-ins__post--soon { cursor: default; }
/* The inert card is marked by its read link and its lack of hover, NOT by
   fading its own text — muted on the still's `--ink-100` panel is 4.39:1, and
   "not written yet" is not a reason to make a title hard to read. */
.e-ins__post--soon .e-post__title { color: var(--text-muted); }
.e-ins__read--soon { border-bottom-style: dashed; }

/* --- 31b. The article opening -------------------------------------------- */

.e-art { padding-block: var(--section-y); }
.e-art__crumbs { margin-bottom: var(--space-8); }
.e-art__head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-5); }
.e-art__title { max-width: 18ch; }
.e-art__lede { max-width: 50ch; color: var(--text-copy-strong); }

.e-art__meta {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  flex-wrap: wrap;
  font: var(--type-small);
  color: var(--text-muted);
  padding-top: var(--space-5);
  border-top: var(--border-hairline);
  width: 100%;
}
.e-art__by { color: var(--text-strong); }
.e-art__sep { color: var(--line-strong); }

/* Contained, not full bleed. The case study's edge-to-edge still is that
   template's one such moment; repeating it here spends a device that belongs
   to it. */
.e-art__shot {
  margin-top: var(--space-8);
  aspect-ratio: 21 / 9;
  background: var(--ink-100);
  border-radius: var(--radius-media);
  display: grid;
  place-items: center;
}
.e-art__shottodo {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: center;
}

/* --- 31c. `.e-prose` — the site's first long-form component ---------------

   Everything else on this site is laid out in columns of its own width. Prose
   is the one thing that needs a single line length it never exceeds, and that
   is what this component is: one measure, and styles for what an article
   actually contains, so writing a piece never requires writing CSS.

   `--measure` is 62 characters, which the design system already sets.
   -------------------------------------------------------------------------- */

/* A two-track grid, not a capped block. Text sits in the first track at the
   measure; the pull-quote and the figure span both, so they break out to the
   right instead of being squeezed into a reading column.

   That is what fills the page. Prose capped at `--measure` on a 1232px shell
   leaves ~590px of nothing beside it, and the two elements that want to be
   wider are exactly the two that were being made narrow. */
.e-prose {
  display: grid;
  grid-template-columns: minmax(0, var(--measure)) 1fr;
  gap: 0;
}
.e-prose > * { grid-column: 1; }
.e-prose > * + * { margin-top: var(--space-6); }

.e-prose p { margin: 0; font: var(--type-copy); color: var(--text-copy-strong); text-wrap: pretty; }

/* The standfirst: the first paragraph is set larger, which is the one place
   prose is allowed a second size. It is doing a different job from the body —
   it decides whether the rest gets read. */
.e-prose__stand { font: var(--type-lead) !important; color: var(--text-strong) !important; }

.e-prose h2 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--weight-medium);
  font-size: var(--size-display-m);
  line-height: var(--leading-title);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
  max-width: 20ch;
  text-wrap: pretty;
}
/* A heading needs more air above it than below — it belongs to what follows. */
.e-prose h2 { margin-top: var(--space-9); }
.e-prose h2 + * { margin-top: var(--space-5); }

.e-prose ul,
.e-prose ol { margin: 0; padding-left: 0; list-style: none; counter-reset: e-prose; }
.e-prose li {
  position: relative;
  padding-left: var(--space-7);
  padding-block: var(--space-4);
  border-top: var(--border-hairline);
  font: var(--type-copy);
  color: var(--text-copy-strong);
}
.e-prose li:last-child { border-bottom: var(--border-hairline); }
.e-prose ul li::before {
  content: "";
  position: absolute;
  left: 0; top: calc(var(--space-4) + 0.62em);
  width: 14px; height: 1px;
  background: var(--line-strong);
}
.e-prose ol li { counter-increment: e-prose; }
.e-prose ol li::before {
  content: counter(e-prose, decimal-leading-zero);
  position: absolute;
  left: 0; top: var(--space-4);
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  color: var(--ebr-purple);
}
.e-prose li strong { font-weight: var(--weight-medium); color: var(--text-strong); }

/* The pull-quote breaks the measure — which is the only reason it reads as a
   pull-quote and not as a paragraph someone indented. */
.e-prose__pull {
  grid-column: 1 / -1;
  margin: var(--space-9) 0;
  padding-left: var(--space-6);
  border-left: 2px solid var(--ebr-purple);
}
.e-prose__pull p {
  font: var(--weight-medium) var(--size-display-m)/1.3 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--text-strong);
  max-width: 26ch;
}

.e-prose__fig { grid-column: 1 / -1; margin: var(--space-9) 0; }
.e-prose__figshot {
  aspect-ratio: 16 / 9;
  background: var(--ink-100);
  border-radius: var(--radius-media);
  display: grid;
  place-items: center;
}
.e-prose__fig figcaption { margin-top: var(--space-4); font: var(--type-small); color: var(--text-muted); }

.e-prose a { color: var(--ink-900); text-decoration: none; border-bottom: 1px solid var(--line-hairline); }
.e-prose a:hover { border-bottom-color: currentColor; text-decoration: none; }

/* --- 31d. Insights, responsive ------------------------------------------- */

@media (max-width: 1080px) {
  .e-art__title { max-width: 22ch; }
}

@media (max-width: 1080px) {
  /* ⚠ `.e-posts` collapses to ONE column at 1080 for the homepage teaser, and
     that rule was landing on the listing too — so the listing went 3 → 1 at
     1080 and back to 2 at 860. A ladder that goes backwards is a ladder
     nobody wrote on purpose. The listing sets its own steps from here. */
  .e-ins__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .e-ins__title { max-width: 22ch; }
}

@media (max-width: 700px) {
  .e-ins__grid,
  .e-ins__grid--two { grid-template-columns: 1fr; }
  .e-ins__count,
  .e-ins__all { margin-left: 0; }
  /* One track below 700 — there is no room to break out of, so the quote and
     the figure simply take the column. */
  .e-prose { grid-template-columns: minmax(0, 1fr); }
}


/* --- 32. Placeholder labels on a muted panel -----------------------------

   Every "still not shot yet" label on the site sits in an `--ink-100` box, and
   `--text-muted` on that ground measures 4.39:1 against a 4.5 floor. One root
   cause, one fix. `.e-post__still` was worse again at 2.61:1 — it was set in
   `--ink-400`, which is a border grey.

   `--text-body` on the same ground is 9.36:1. These labels are announcing that
   something is missing, so being clearly legible is the job.

   Applied site-wide rather than only on the new pages: shipping Insights with
   a known failure to avoid touching the others would have been the wrong way
   round.
   -------------------------------------------------------------------------- */

.e-post__still,
.e-svcl__todo,
.e-orig__todo,
.e-ind__todo,
.e-dth__todo,
.e-dtc__todo,
.e-wkg__todo,
.e-csh__shottodo,
.e-csg__todo,
.e-art__shottodo,
.e-prose__figshot .e-art__shottodo,
/* and any hole sitting on a tinted panel — the case study's spec values were
   `--text-muted` on `--surface-muted`, the same 4.39:1 as the stills. */
/* The labels as well as the values — the rule only covered `[data-todo]`,
   which is the values, and left the `dt`s at 4.39:1 on the same panel. */
.e-csc__spec dt,
.e-csc__spec [data-todo][data-todo] { color: var(--text-body); }

/* ==========================================================================
   33. CONTACT — the form, after Mistral AI
   ========================================================================== */

/* ⚠ THE SECTION OPENS TIGHTER THAN EVERY OTHER ON THE SITE, and it has to.
   `--section-y` is 144px. The form IS the page and does not need an entrance,
   and a short top pad is now also what gives the pinned left column its travel.
   (The wireframe asked for no scroll to reach submit at 1440; that was dropped
   deliberately in favour of alignment — see `.e-con__form`.) */
.e-con { padding-block: var(--space-9) var(--section-y); }

/* Left column and form panel side by side. The form takes more than the
   wireframe's 1:1.15 — at that ratio the six service pills wrapped to three
   rows, and every row of pills is ~45px off the submit button's budget. */
.e-con__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.3fr);
  gap: var(--space-9);
  align-items: start;
}

/* `align-self: stretch` overrides the grid's `align-items: start`. It is load
   bearing: the column must span the full row height so the sticky cards inside
   it have somewhere to travel. Start-aligned, it shrinks to its own content and
   the pin computes but never moves. */
.e-con__lead {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  align-self: stretch;
}

/* ⚠ THE CARDS PIN, NOT THE WHOLE COLUMN — and the difference is measured, not
   stylistic. Pinning `.e-con__lead` meant pinning the headline and lede too,
   which made the pinned object 749–831px tall depending on where the lede
   rewraps. Add the 98px offset and it needed a 953px viewport at its worst;
   below that sticky does not fail cleanly, it holds for a few pixels and then
   drags the headline up under the sticky header. Almost no laptop is 953px.

   The cards alone are ~490px, so they pin on any ordinary screen. They are
   also the right thing to hold: the headline is an entrance and has done its
   work by field two, while "what happens next" and the email address are what
   someone wants to re-read at field five, which is exactly when they would
   otherwise have scrolled away.

   `.e-con__lead` must NOT be `align-self: start` for this — as a grid item it
   has to stretch to the row's full height, or the cards have no containing
   block to travel in and sticky computes but never moves. */
/* 880, not 700: the stack grew a third card (the video testimonial) and
   at 768–800px tall its foot sat below the fold while stuck. */
@media (min-width: 1081px) and (min-height: 880px) {
  .e-con__cards {
    position: sticky;
    top: calc(66px + var(--space-6));
  }
}
.e-con__title { max-width: 16ch; }
.e-con__lede { max-width: 44ch; color: var(--text-copy-strong); }

/* MISTRAL'S STACK OF BORDERED CARDS, not loose copy under a headline. Two
   rather than its four: it routes to four inboxes, EBR has one. */
.e-con__cards { margin-top: var(--space-5); display: flex; flex-direction: column; gap: var(--space-6); }
.e-con__card {
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  align-items: flex-start;
}

.e-con__steps { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--space-5); }
.e-con__steps li {
  position: relative;
  padding-left: var(--space-6);
  font: var(--type-copy-small);
  color: var(--text-copy-strong);
}
/* A small filled marker rather than a bullet glyph. The wireframe draws these
   purple, but purple already has a job on this screen — the email link and the
   submit button, which are the same job: reaching EBR. A marker that only
   counts three steps is decoration, and decoration does not get the accent. */
.e-con__steps li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.5em;
  width: 8px; height: 8px;
  border-radius: var(--radius-pill);
  background: var(--ink-900);
}

.e-con__email {
  font: var(--weight-medium) var(--size-body-l)/1.3 var(--font-display);
  letter-spacing: var(--tracking-title);
  color: var(--ebr-purple);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.e-con__email:hover { color: var(--ebr-purple); border-bottom-color: currentColor; text-decoration: none; }
.e-con__emailnote { margin: 0; font: var(--type-small); color: var(--text-muted); }

/* --- the form panel ------------------------------------------------------ */

/* ONE SIX-COLUMN GRID GOVERNS THE WHOLE FORM. Every control is sized in
   columns of this grid, so every left and right edge in the panel lands on the
   same set of vertical lines — a half field, a third of a pill row and a full
   textarea all stop dead against each other.

   The column gap must equal the gap inside the pill rows for that to hold. It
   is exact, not approximate: with six columns at gap g, a span-2 area measures
   2c + g, and a three-column grid of the same width gives (W - 2g)/3 — the two
   are the same number. Change one gap without the other and every pill row
   drifts a few pixels off the fields above it. The COLUMN gap is therefore
   pinned at 16px and is the one value here that cannot be opened up; the row
   gaps carry the breathing instead. */
.e-con__form {
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  background: var(--white);
  padding: var(--space-7);
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  /* Row gap is larger than column gap on purpose: down the page it separates
     one question from the next, across the page it joins parts of the same
     question. Equal gaps would read as a grid of unrelated boxes. */
  gap: var(--space-6) var(--space-4);
  align-items: start;
}
/* Full width unless a modifier says otherwise. */
.e-con__form > * { grid-column: 1 / -1; }
.e-cf--half { grid-column: span 3; }

.e-con__formhead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}
/* ⚠ MISTRAL ASTERISKS EVERY REQUIRED FIELD. This states the exception instead.
   On a six-field form nearly everything is required by definition, so five
   asterisks and one blank is noise where one sentence is information. */
.e-con__req { font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-muted); }

/* ⚠ NAMED `.e-cf`, NOT `.e-field`. `.e-field` already exists (css §~1841) and
   belongs to the dark contact overlay panel: its labels are 62% WHITE and its
   inputs are transparent with a bottom rule. Dropped onto this white panel it
   rendered three invisible fields — white labels on white, and inputs with no
   fill. The name was free; the collision was not. */
.e-cf { display: flex; flex-direction: column; gap: var(--space-4); position: relative; }
.e-cf__label {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-body);
  padding: 0;
}
.e-cf__input {
  font: var(--type-body);
  color: var(--text-strong);
  background: var(--paper);
  border: var(--border-hairline);
  border-radius: var(--radius-sm);
  /* 12px, not the 10px the fold constraint forced: an input and a pill are the
     same kind of target and should be the same height. */
  padding: 14px 16px;
  width: 100%;
  transition: var(--transition-hover);
}
.e-cf__input--area { resize: vertical; min-height: 156px; }
.e-cf__input:hover { border-color: var(--ink-400); }
.e-cf__input:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; border-color: transparent; }

/* MISTRAL'S CHARACTER COUNT, and it is the detail worth taking: a brief box
   with no limit invites a page nobody reads. Written by site.js §26. */
.e-con__count {
  align-self: flex-end;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.e-con__opt { text-transform: none; letter-spacing: 0; color: var(--text-muted); font-weight: var(--weight-regular); }

/* ⚠ NOT `display: flex` WITH `gap`. A rendered <legend> sits outside its
   fieldset's flex formatting context, so flex `gap` never applied to it and
   every pill row sat hard against its label at 0px while the plain <label>
   fields above kept their 12px. Block flow plus an explicit margin is the only
   thing that treats a legend like the label it is. */
.e-con__set { margin: 0; padding: 0; border: 0; display: block; }
.e-con__set > legend { display: block; float: none; width: 100%; padding: 0; margin-bottom: var(--space-4); }
/* ⚠ A GRID, NOT WRAPPING FLEX. Flex-wrap sizes each pill to its own label, so
   the row ended "Web design / Branding / AI automation / Performance / SEO"
   with "Maintenance" orphaned underneath at a width nothing else shared. Six
   equal cells in three columns give two full rows and put every pill edge on a
   column line of the form grid above. */
/* Column gap is locked to the form's column gap — the line alignment depends
   on the two being the same number. Row gap is free, and is set to the same
   12px that separates a label from its control, so everything *inside* one
   question is 12 and everything *between* questions is 24. Two values, one
   ratio; the old 16px row sat between the two and belonged to neither. */
.e-con__pills { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: var(--space-4); row-gap: var(--space-4); }
.e-con__pills--three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* Two pills at half width each — the same two lines as name and company. */
.e-con__pills--pair { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* The same pill the work page's filter row uses, in its selectable form. */
.e-con__pill { cursor: pointer; display: flex; }
.e-con__pill > span {
  /* Fills the grid cell, so a one-word pill and a four-word pill are the same
     size and centre on the same axis. */
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  text-align: center;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-body);
  border: var(--border-hairline);
  border-radius: var(--radius-pill);
  padding: 8px 13px;
  transition: var(--transition-hover);
}
.e-con__pill:hover > span { color: var(--ink-900); border-color: var(--ink-400); }
/* Purple for the selected pill, which is the wireframe's call and the meaning
   purple already carries on the services router. */
.e-con__pill input:checked + span {
  background: var(--ebr-purple);
  border-color: var(--ebr-purple);
  color: var(--white);
}
.e-con__pill input:focus-visible + span { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Mistral's consent line: a real sentence with a real link, not a tick-box
   with "I agree" beside it. */
.e-con__consent { display: flex; gap: var(--space-4); align-items: flex-start; font: var(--type-small); color: var(--text-muted); cursor: pointer; }
.e-con__consent input { width: 18px; height: 18px; flex-shrink: 0; margin-top: 2px; accent-color: var(--ebr-purple); }
.e-con__consent a { color: var(--text-strong); text-decoration: none; border-bottom: 1px solid var(--line-hairline); }
.e-con__consent a:hover { border-bottom-color: currentColor; text-decoration: none; }

.e-con__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  /* Matches the 24px row gap that sits above the rule. A dividing rule with
     more air on one side than the other reads as a mistake, not a division. */
  padding-top: var(--space-5);
  border-top: var(--border-hairline);
}
.e-con__footnote { font: var(--type-small); color: var(--text-muted); }

/* --- 33b. Contact, responsive ------------------------------------------- */

@media (max-width: 1080px) {
  .e-con__grid { grid-template-columns: 1fr; gap: var(--space-7); }
  /* Six pills across a narrow panel are 8 characters wide. Two columns. */
  .e-con__pills, .e-con__pills--three { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .e-cf--half { grid-column: 1 / -1; }
  .e-con__title { max-width: 22ch; }
  .e-con__lede { max-width: 60ch; }
  .e-con__cards { flex-direction: row; }
  .e-con__card { flex: 1 1 280px; }
}

@media (max-width: 700px) {
  .e-con__cards { flex-direction: column; }
  .e-con__form { padding: var(--space-6); }
  .e-con__foot { flex-direction: column; align-items: stretch; }
  .e-con__foot .e-btn { width: 100%; }
}

/* --- 33c. Contact 02, the calendar (after Shade) ------------------------- */

/* Two columns, form-panel proportions inverted: here the *left* carries the
   argument and the right is the instrument. Same 0.85/1.3 split as block 01 so
   the two sections' inner edges line up down the page. */
.e-cal__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.3fr);
  gap: var(--space-9);
  align-items: start;
}
.e-cal__lead { display: flex; flex-direction: column; gap: var(--space-6); }
.e-cal__title { max-width: 18ch; }
.e-cal__lede { max-width: 46ch; color: var(--text-copy-strong); }

/* The "what this is not" box. Same bordered card as block 01's two, so the
   left column of the page reads as one family down its whole length. */
.e-cal__not {
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  padding: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}
.e-cal__notlist { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--space-5); }
.e-cal__notlist li {
  position: relative;
  padding-left: var(--space-6);
  font: var(--type-copy-small);
  color: var(--text-copy-strong);
}
/* A short rule, not a dot. These are three denials, and a rule reads as a
   strike-through where a dot would read as a bullet point selling something. */
.e-cal__notlist li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.62em;
  width: 12px; height: 1px;
  background: var(--ink-400);
}

.e-cal__who { display: flex; align-items: center; gap: var(--space-5); }
.e-cal__face {
  flex: 0 0 auto;
  width: 64px; height: 64px;
  border-radius: var(--radius-pill);
  background: var(--ink-100);
  display: flex; align-items: center; justify-content: center;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-size: 9px;
  /* Same fix as §32: `[data-todo]` would otherwise set this in `--text-muted`,
     which is 4.39:1 on `--ink-100` — a fail. Placeholder labels on a muted
     field take `--text-body`. */
  color: var(--text-body);
}
.e-cal__face[data-todo][data-todo] { color: var(--text-body); }
.e-cal__whotext { display: flex; flex-direction: column; gap: var(--space-2); }
.e-cal__whoname { font: var(--weight-medium) var(--size-body-l)/1.3 var(--font-display); letter-spacing: var(--tracking-title); }
.e-cal__whorole { font: var(--type-small); color: var(--text-body); max-width: 34ch; }

/* --- the rebuilt embed --------------------------------------------------- */

.e-cal__panel {
  border: var(--border-hairline);
  border-radius: var(--radius-md);
  background: var(--paper);
  padding: var(--space-7);
  display: flex;
  flex-direction: column;
  /* The same 32 between parts / 16 inside a part as the form panel. */
  gap: var(--space-6);
}

.e-cal__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-6);
  flex-wrap: wrap;
  padding-bottom: var(--space-6);
  border-bottom: var(--border-hairline);
}
.e-cal__meta { display: flex; flex-direction: column; gap: var(--space-3); }
.e-cal__meeting { font: var(--weight-medium) var(--size-body-l)/1.3 var(--font-display); letter-spacing: var(--tracking-title); }
.e-cal__dur { font: var(--type-small); color: var(--text-body); }
.e-cal__tz { display: flex; flex-direction: column; gap: var(--space-3); text-align: right; }
.e-cal__tzname { font: var(--type-small); color: var(--text-body); font-variant-numeric: tabular-nums; }
.e-cal__tzhint { text-transform: none; letter-spacing: 0; color: var(--text-muted); font-weight: var(--weight-regular); }

.e-cal__set { margin: 0; padding: 0; border: 0; display: block; }
.e-cal__set > legend { display: block; float: none; width: 100%; padding: 0; margin-bottom: var(--space-4); }

/* ⚠ FIVE COLUMNS, NOT A MONTH. Shade shows a month because it books a month
   out; a month of empty cells on an agency calendar advertises an empty diary. */
.e-cal__days { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-4); }
.e-cal__slots { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-4); }

.e-cal__day, .e-cal__slot { cursor: pointer; display: flex; }
.e-cal__day > span, .e-cal__slot > span {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 56px;
  text-align: center;
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-body);
  background: var(--white);
  border: var(--border-hairline);
  /* 4px, the system's small radius — not the pill the service filters use.
     A date is a cell, not a tag. */
  border-radius: var(--radius-sm);
  transition: var(--transition-hover);
}
.e-cal__slot > span { min-height: 44px; font-variant-numeric: tabular-nums; }
.e-cal__dnum { font-size: 15px; letter-spacing: 0; text-transform: none; color: var(--ink-900); }
.e-cal__day:hover > span, .e-cal__slot:hover > span { border-color: var(--ink-400); }

/* ⚠ THE ONE PURPLE ON THIS SCREEN. The selected day, and nothing else — the
   confirm button is ink for exactly this reason. */
.e-cal__day input:checked + span {
  background: var(--ebr-purple);
  border-color: var(--ebr-purple);
  color: var(--white);
}
.e-cal__day input:checked + span .e-cal__dnum { color: var(--white); }
/* The chosen time is ink, per the sheet: purple picks the day, ink picks the
   time, and the two selections stay legible as different kinds of choice. */
.e-cal__slot input:checked + span {
  background: var(--ink-900);
  border-color: var(--ink-900);
  color: var(--white);
}
.e-cal__day input:focus-visible + span,
.e-cal__slot input:focus-visible + span { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Taken slots are a flat grey field, not a struck-through or faded live one:
   an unavailable time should read as furniture, never as something to retry. */
.e-cal__slot--taken { cursor: not-allowed; }
/* ⚠ CONTRAST NOTE. `--ink-400` here measured 2.61:1. These are inactive
   controls (the input carries `disabled`), which WCAG 1.4.3 exempts, but 2.61
   is low enough to be a readability problem regardless of what the spec allows.
   `--ink-500` takes it to 4.14 — still clearly "off", and the strike-through
   carries the meaning without relying on colour at all. It is short of 4.5, and
   the only way to clear that is a grey dark enough to read as available, which
   would defeat the cell. Flagged rather than quietly left at 2.61. */
.e-cal__slot--taken > span {
  background: var(--ink-100);
  border-color: transparent;
  color: var(--ink-500);
  text-decoration: line-through;
}
.e-cal__slot--taken:hover > span { border-color: transparent; }

.e-cal__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  padding-top: var(--space-6);
  border-top: var(--border-hairline);
}
.e-cal__chosen { font: var(--type-small); color: var(--text-body); }
.e-cal__wire { margin: 0; font: var(--type-small); }
.e-cal__panel .e-btn[disabled] { opacity: .45; cursor: not-allowed; }
.e-cal__panel .e-btn[disabled]:hover { background: var(--ink-900); border-color: var(--ink-900); }

/* --- 33d. Contact 02, responsive ---------------------------------------- */

@media (max-width: 1080px) {
  .e-cal__grid { grid-template-columns: 1fr; gap: var(--space-7); }
  .e-cal__title { max-width: 24ch; }
  .e-cal__lede { max-width: 60ch; }
}

@media (max-width: 700px) {
  .e-cal__panel { padding: var(--space-6); }
  /* Five day-cells at 320px are 48px wide and the weekday label stops fitting.
     Three across, wrapping, keeps the numeral readable. */
  .e-cal__days { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .e-cal__slots { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .e-cal__head { flex-direction: column; }
  .e-cal__tz { text-align: left; }
  .e-cal__foot { flex-direction: column; align-items: stretch; }
  .e-cal__foot .e-btn { width: 100%; }
}



/* --- 33g. Contact form status line -------------------------------------- */
/* The footnote beside the submit button doubles as the form's reply. It is the
   spot someone is already looking at when they press send, so a message there
   needs no attention drawn to it. Error state is colour AND wording — the
   sentence says what to do next, so colour is never carrying it alone. */
.e-con__footnote.is-error { color: var(--ebr-purple); }
.e-con__form.is-sent .e-con__form > *:not(.e-con__foot) { opacity: .5; }
.e-con__form.is-sent .e-cf__input,
.e-con__form.is-sent .e-con__pill { pointer-events: none; }

/* --- 34. Ways to work with us -------------------------------------------

   Four additions, and only four. Blocks 01, 05, 06, 07 and 08 of that page
   are existing components used as they stand, so they contribute nothing
   here — which is the whole reason the page was scoped the way it was.

   34a. The expanded plan card
   ---------------------------
   .e-plan already exists and is already used on the homepage's ink band.
   This EXTENDS it rather than replacing it: a visitor arriving from the
   homepage should recognise these as the same three objects, so the name,
   kicker, body and foot are untouched and only the new parts are styled. */

.e-plans--full { gap: var(--space-4); align-items: stretch; }
.e-plan--full { gap: var(--space-4); }

/* The foot is pushed down by `margin-top:auto` in the base component. On the
   full card the "not this if" block sits BELOW that foot, so the auto margin
   has to move to the first group instead — otherwise the foot eats the space
   and the closing line hangs off the bottom rule. */
.e-plan--full .e-plan__foot { margin-top: 0; }
.e-plan--full .e-plan__group:first-of-type { margin-top: auto; }

.e-plan__sub {
  font: var(--type-label);
  font-size: 10px;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ebr-purple-soft);
  margin-bottom: 10px;
}
.e-plan--featured .e-plan__sub { color: var(--ebr-yellow); }

.e-plan__say { margin: 0; font: var(--type-copy); color: var(--text-on-dark-muted); }
.e-plan--featured .e-plan__say { color: rgba(255,255,255,.9); }

.e-plan__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.e-plan__list li {
  position: relative;
  padding-left: 18px;
  font: var(--type-copy);
  color: var(--text-on-dark-muted);
}
.e-plan--featured .e-plan__list li { color: rgba(255,255,255,.9); }
/* A dot, not a hairline. The house rule is that a rule has to divide
   something; between list items there is nothing to divide. */
.e-plan__list li::before {
  content: "";
  position: absolute;
  left: 0; top: .62em;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: currentColor;
  opacity: .5;
}

.e-plan__not { padding-top: var(--space-4); border-top: 1px solid rgba(255,255,255,.2); }

/* Copy that is owed, not copy that is written. Italic and dimmed so it cannot
   be mistaken for signed-off text at a glance, and so it is obvious in a
   screenshot which lines are still pending. */
.e-todo { font-style: italic; opacity: .62; }

/* 34c. The inclusion grid
   ----------------------
   Six things true of every engagement. No rules between them: they are a set,
   not a sequence, and a grid of hairlines here would be decoration. */
.e-incl {
  list-style: none; margin: var(--space-7) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: var(--space-6) var(--space-6);
}
.e-incl__name { margin: 0 0 8px; font: var(--type-lead); color: var(--text-strong); }
.e-incl__body { margin: 0; font: var(--type-copy); color: var(--text-copy); max-width: 34ch; }

@media (max-width: 1080px) {
  .e-incl { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 860px) {
  .e-plans--full { grid-template-columns: 1fr; }
  .e-plan--full .e-plan__group:first-of-type { margin-top: 0; }
}
@media (max-width: 700px) {
  .e-incl { grid-template-columns: 1fr; gap: var(--space-5); }
  .e-incl__body { max-width: none; }
}

/* 34d. The four-tile router
   ------------------------
   .e-svcr__grid is three columns, which is right for the six symptoms on the
   Services page and wrong for four situations here — it left tile 04 alone on
   a row beside two empty cells. Two-up instead, which also gives these longer
   sentences a better measure. */
.e-svcr__grid--four { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 860px) {
  .e-svcr__grid--four { grid-template-columns: 1fr; }
}

/* 34e. The banner's living ground, and the page's section rhythm
   -------------------------------------------------------------

   The banner reuses the work page's paper-tinted aurora field rather than
   introducing a third one — the tint rules above are shared, not copied, so
   there is one definition of "this ground, on paper".

   `isolation` and NO `overflow`: .e-cta__ground clips itself, and an ancestor
   that clips is an ancestor that scrolls. */
.e-wayh {
  padding-block: var(--section-y);
  position: relative;
  isolation: isolate;
}

/* ⚠ THE LEDE IS PROMOTED OFF `--text-muted` ON THIS BAND, AND ONLY THIS BAND.

   The aurora is ANIMATED, so a contrast check against the paper it starts on
   is the wrong test — the discs drift, and every part of the text column
   eventually sits under a denser part of the wash. The honest test is the
   worst case: all three auroras overlapping at peak alpha, which composites to
   about rgb(201,162,248).

   Against that: the heading (--text-strong) holds at 8.1:1 and needs nothing.
   The lede at --text-muted falls to 2.43:1, well under the 4.5 body threshold,
   and it fails invisibly — the animation is slow, so the frame where it goes
   unreadable is not the frame anyone is looking at when the page is reviewed.
   --text-body clears it at 5.18:1 and is the smallest change that does. */
.e-wayh .e-lead { color: var(--text-copy-strong); }

/* --- the router as a section in its own right ---------------------------

   .e-svcr is built for the Services page, where it sits inside the hero field
   directly under the banner and so opens with zero top padding. Standing on
   its own as a paper section it has to pay the normal section rhythm, or it
   collides with whatever precedes it — which is exactly what it did under the
   purple process band.

   Scoped by `.e-ground-paper`, which the Services router does not carry: a
   router that IS its own ground behaves like a section, one that sits inside
   the field does not. */
.e-ground-paper.e-svcr { padding-top: var(--section-y); }

/* The answer panel closes the pair, so it carries the section's bottom rhythm
   rather than the shorter tail it uses inside the Services field. */
.e-ground-paper.e-svca { padding-bottom: var(--section-y); }

/* ⚠ NO HAIRLINE BETWEEN THE ROUTER AND ITS PANEL. The site draws one wherever
   two grounds of the same value meet, so that two sections do not read as one
   field. Here they SHOULD read as one field: the router is a control and the
   panel is its output, and a line between them says they are two things the
   reader has to consider separately. */
.e-svcr + .e-svca { border-top: 0; }

/* ==========================================================================
   35. Homepage hero — the Earth film

   Replaces the paper hero on index.html (16 Sep 2026). A full-bleed looping
   film — an astronaut on the Moon, Earth rising — with as little type over it
   as the page can open on.

   REFERENCES
   - Grok, "Understand the Universe" (Mobbin section 6857a406): the claim sits
     in the dark half of the frame, the light source owns the other half, and
     the supporting line and the one call to action sit low on the far side.
     That diagonal — words top-left, action bottom-right, subject between them
     — is the whole composition here.
   - Cosmos (Mobbin 75a25da0): on a space ground, one claim and one button is
     enough. The second button became a text link; the testimonial card left.
   - services.html §26a: the transparent header over a film, and the idle
     video loader, both reused rather than rebuilt.

   WHY NOT CENTRED. This hero was centred at Adrian's request on 10 Aug 2026,
   and this is a deliberate departure from it. The loop was measured before
   choosing (mean brightness of all 180 frames on a 32x16 grid): the left of
   the frame is near-black (<= 22/255) from the top down to the horizon, the
   astronaut stands at 25-35% across and 47-95% down, and Earth's lit limb is
   upper centre-right. A centred block lands on Earth and clips the astronaut
   — the film would be the backdrop to the type instead of the other way
   round. To go back to centred, delete the grid rules in 35c; the markup
   does not change.

   The paper-hero rules in §7 and the aura in the "hero ground" block are
   left in place and switched off below, not deleted: every selector here is
   `.e-hero--film`, so removing that one class from the markup restores the
   old hero exactly.
   ========================================================================== */

/* --- 35a. The section ---------------------------------------------------- */

.e-hero.e-hero--film {
  /* The header's height, as §26 measures it (16px padding twice, a 26px
     lockup, the button's height in practice). Not a shared token — if the
     header's padding changes, this and --svcb-header change together. */
  --hero-header: 66px;

  position: relative;
  isolation: isolate;
  background: var(--ink-900);
  color: var(--text-on-dark);
  padding: 0;

  /* Up under the sticky header, for the same reason as .e-svcfield: the
     header is transparent here, so the film has to be behind it at scroll 0
     or the first thing anyone sees is a white wordmark on the paper page. */
  margin-top: calc(var(--hero-header) * -1);

  /* A full screen, header included — the film is the opening screen. */
  min-height: 100svh;
}

/* The violet aura belonged to the paper hero. On a film it would be a purple
   smudge on the Moon. */
.e-hero.e-hero--film::before { content: none; animation: none; }

/* --- 35b. The film ------------------------------------------------------- */

.e-hero--film .e-hero__film {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;

  /* The still. It is the film's own first frame, so when the video fades up
     over it nothing appears to change. A background rather than a `poster`
     attribute so the stylesheet can pick the cut: a phone never downloads the
     landscape still, and a desktop never downloads the portrait one. */
  background: #030406 url("../assets/video/earth-loop-poster.webp") 50% 50% / cover no-repeat;
}

.e-hero__vid {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;

  /* Invisible until it is actually playing — see site.js §18. Reduced motion
     and Save-Data never attach a source, so for those readers this stays at
     zero for good and the still is the banner. */
  opacity: 0;
  transition: opacity 700ms var(--ease-out);
}
.e-hero__vid.is-playing { opacity: 1; }

/* Wider than the loop's own 2:1, the frame has to lose height somewhere.
   Lose it from the bottom: Earth's limb is 3% from the top of the frame and
   the first thing to go would be the top of the planet. */
@media (min-aspect-ratio: 21/10) {
  .e-hero--film .e-hero__film { background-position: 50% 30%; }
  .e-hero__vid { object-position: 50% 30%; }
}

/* A portrait screen gets the portrait cut — a 720x960 crop that keeps the
   astronaut AND the left of the Earth. The landscape loop cover-cropped into
   a phone keeps only its middle third, which has neither. site.js §18 uses
   the same query to choose the video. */
@media (max-aspect-ratio: 1/1) {
  .e-hero--film .e-hero__film {
    background-image: url("../assets/video/earth-loop-portrait-poster.webp");
  }
}

/* Two washes, each doing one job, and only where the job is.

   Top: the header rides the film with no fill. Earth's lit limb reaches the
   top edge of the frame right behind the nav, and white nav type at .88 on the
   brightest part of that limb is not legible — so the top of the frame is
   pulled down a little, over a fixed 200px rather than a percentage, because
   the header is a fixed height and a percentage would darken half a phone.

   Bottom: the supporting line and the button sit on the Moon's surface, which
   is a mid grey, and body copy needs 4.5:1 on it. The same wash also lands the
   frame on exactly ink-900 at its bottom edge, which is the ground of the
   showreel band directly below — so the film runs into the reel instead of
   stopping against it. */
.e-hero__scrim {
  position: absolute;
  inset: 0;
  background:
    /* A pool of shade behind the foot only. The full-width wash below was
       enough on average (8.3:1 median behind the supporting line) but not on
       the Moon's brightest highlights, where QA measured 2.85:1. Darkening the
       whole bottom band to fix that would put the astronaut in shadow too, so
       the extra shade sits where the copy is and nowhere else. */
    radial-gradient(ellipse 48% 42% at 88% 86%, rgba(3, 4, 6, .78), rgba(3, 4, 6, 0) 72%),
    /* The claim's side. The 18 Sep 2026 loop puts Earth big and bright in the
       middle of the frame: measured worst-case across all 599 frames, the
       headline's column runs 90-130 where the previous loop was near-black, and
       the lit limb beyond it reaches 239, so the type needs shade to sit on.

       It is an ellipse anchored to the top-left corner rather than a wash down
       the whole left side, because the astronaut stands at 16-30% across and
       44% down — directly below the claim, not behind it. A side wash reached
       the type and the figure equally and put him in shadow; this is centred on
       the type at 24% down and is gone by the time it reaches his helmet, so he
       keeps his own light. The lit limb and the whole right half are untouched
       either way. */
    radial-gradient(ellipse 62% 46% at 6% 24%,
      rgba(3, 4, 6, .86) 0%,
      rgba(3, 4, 6, .52) 52%,
      rgba(3, 4, 6, 0) 78%),
    /* The top strip of this loop is dark on its own (worst case 51), so the
       header needs only a light touch. It was .8 over 240px for the previous
       footage, whose lit limb reached the top edge behind the nav. */
    linear-gradient(to bottom, rgba(3, 4, 6, .38) 0, rgba(3, 4, 6, 0) 200px);
}

/* THE BASE FADE — phones only.
 *
 * It used to run at every width, ending the banner on flat `--ink-900`. On a
 * wide screen that is now wrong: the showreel below opens on the nebula still,
 * so a hard band of flat ink sat between the film and the picture and the two
 * sections read as two. The blend is handled from the showreel's side instead
 * (`.e-reel__bridge`), and the film is left to run to the banner's edge.
 *
 * On a phone it still earns its place, and removing it there would break the
 * layout rather than just the join: §35d turns the film into a band at the top
 * and puts the supporting line and the button on the ink BELOW it. This fade
 * is the hand-off between the two. Hence `::after` rather than deleting the
 * layer — the mid/end stops §35d sets keep working untouched. */
.e-hero__scrim::after {
  content: "";
  position: absolute;
  inset: 0;
  display: none;
  background: linear-gradient(to top,
    var(--ink-900) 0,
    color-mix(in srgb, var(--ink-900) 70%, transparent) var(--hero-scrim-mid, 16%),
    color-mix(in srgb, var(--ink-900) 0%, transparent) var(--hero-scrim-end, 46%));
}
@media (max-width: 700px) {
  .e-hero__scrim::after { display: block; }
}

/* --- 35c. The type ------------------------------------------------------- */

.e-hero--film .e-hero__inner {
  position: relative;
  z-index: 2;
  min-height: 100svh;

  /* Undo §7's centred column. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  gap: 0;
  text-align: left;

  padding-top: calc(var(--hero-header) + clamp(24px, 7svh, 88px));
  padding-bottom: clamp(36px, 7svh, 76px);
}
.e-hero--film .e-hero__inner > * { margin-inline: 0; }

/* Top left, in the dark half. */
.e-hero__head {
  /* Both columns. Column 2 exists only for the foot, three rows down, but a
     grid sizes a column by everything in it — confined to column 1, the head
     was left whatever the foot's 38ch did not take, which on a 768px tablet was
     280px and a four-line headline. The title's own max-width is the measure. */
  grid-column: 1 / -1;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-5);
}
.e-hero--film .e-label { justify-content: flex-start; }

/* 14ch holds the claim to three lines and, at the full 84px, to the left 50%
   of a 1440 screen — the dark half. Any wider and the end of the first line
   runs onto Earth's terminator. */
/* Balanced, so the claim breaks as three even lines rather than two long
   ones and an orphaned "needs." — the orphan was the line landing on the
   astronaut's helmet. */
.e-hero--film .e-hero__title { max-width: 14ch; text-wrap: balance; }

/* Bottom right, low, on the surface. */
.e-hero__foot {
  grid-column: 2;
  grid-row: 3;
  justify-self: end;
  max-width: 38ch;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}
.e-hero--film .e-hero__lead { max-width: none; color: rgba(255, 255, 255, .92); }
.e-hero--film .e-hero__actions {
  justify-content: flex-start;
  gap: var(--space-6);
  padding-top: 0;
}

/* Yellow on dark — the house rule the header already follows on this film. */
.e-hero--film .e-btn--primary {
  background: var(--ebr-yellow);
  border-color: var(--ebr-yellow);
  color: var(--ink-900);
}
.e-hero--film .e-btn--primary:hover {
  background: var(--ebr-yellow-press);
  border-color: var(--ebr-yellow-press);
  color: var(--ink-900);
}

/* Short laptop screens. In landscape the film is scaled to the screen's
   height, so the astronaut's helmet sits at a fixed ~49% of the height — but
   the headline is sized from the width, so on a 1440x731 screen its last line
   came down onto the helmet. Capping the size by height keeps the claim above
   him on any landscape screen; at 1440x900 and taller the cap does not bite. */
@media (min-aspect-ratio: 1/1) and (min-width: 701px) {
  .e-hero--film .e-hero__title { font-size: min(var(--size-display-l), 8.2svh); }
}

/* Portrait tablets get the portrait cut and the desktop diagonal, but at
   5.4vw the display size is small enough that 14ch breaks the claim into four
   short lines. The whole top of the portrait cut is dark sky, so it can run
   wider here without reaching anything lit. */
@media (max-aspect-ratio: 1/1) and (min-width: 701px) {
  .e-hero--film .e-hero__title { max-width: 18ch; }
}

/* --- 35d. Phones --------------------------------------------------------

   The diagonal does not survive a 390px screen: the astronaut stands in the
   bottom half of the portrait cut, so anything anchored low lands on him. So
   the film becomes a band at the top with the claim over its dark sky, and
   the supporting line and the button come out from under it onto the ink
   below, where the wash has already landed the frame. Everything the film
   is for stays uncovered.

   --film-h is the band's height below the header. The head is given exactly
   that much room so the foot starts where the film ends, with no script. */
@media (max-width: 700px) {
  .e-hero.e-hero--film {
    --film-h: min(122vw, 68svh);
    --hero-scrim-mid: 9%;
    --hero-scrim-end: 26%;
    min-height: 0;
  }

  .e-hero--film .e-hero__film {
    bottom: auto;
    height: calc(var(--hero-header) + var(--film-h));
  }

  .e-hero--film .e-hero__inner {
    min-height: 0;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto;
    padding-top: calc(var(--hero-header) + 28px);
    padding-bottom: 48px;
  }

  .e-hero__head { min-height: calc(var(--film-h) - 28px); gap: var(--space-4); }
  .e-hero--film .e-hero__title { max-width: 12ch; }

  .e-hero__foot {
    grid-column: 1;
    grid-row: 2;
    justify-self: stretch;
    max-width: none;
    padding-top: var(--space-3);
  }
  .e-hero--film .e-hero__actions { gap: var(--space-4); }

  /* §-wide phone rule stretches every hero action to full width. Right for
     the button; a text link stretched across the screen just drags its
     underline with it. */
  .e-hero--film .e-hero__actions .e-arrow { align-self: flex-start; }
}

/* Desktop (1081px and up, the site's tablet edge): the supporting line and
   its two actions are hidden — Adrian, 20 Oct 2026. Tablets and phones keep
   them. The header's Book a call button is still there on desktop. */
@media (min-width: 1081px) {
  .e-hero--film .e-hero__foot { display: none; }
}

/* The closing headline breaks by hand on wide screens so the two sentences
   sit one per line. A phone is too narrow for that to hold, so the break is
   dropped and text-wrap: balance takes over. */
@media (max-width: 700px) { .e-cta__br { display: none; } }

/* ==========================================================================
   37. The touch — scroll-scrubbed sequence, and the white ending

   21 Sep 2026, Adrian. The band above the footer stops being a still and
   becomes 73 frames scrubbed by scroll (site.js §13). It ends on a white
   flare, and everything after it — the closing band and the footer — is white
   with dark type, so the flare reads as the page opening out rather than as a
   video that stopped.
   ========================================================================== */

/* --- 37a. The band ------------------------------------------------------- */

/* Tall, because the height IS the scrub: this is how much scroll the 73 frames
   are spread across. 300svh gives roughly 2.7 screens of travel, which at a
   normal wheel puts the whole sequence at about three seconds — the length of
   the clip. Shorter and it flicks past; longer and the reader is scrolling
   through treacle waiting for the hands to meet. */
.e-touch {
  position: relative;
  height: 300svh;
  background: #000;
}

/* One screen, pinned, while the band scrolls past behind it.
   NO `overflow: hidden` on .e-touch — clipping makes it a scroll container and
   sticky then has nothing to stick to. The stylesheet has been caught by that
   twice already (§25b, and the hero). */
.e-touch__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

.e-touch__canvas { display: block; width: 100%; height: 100%; }

/* Reduced motion and Save-Data get the first frame and no scroll journey. */
.e-touch.is-still { height: 100svh; }

/* Without JavaScript nothing ever paints the canvas, and 300svh of black is a
   void, not a design. The band is decorative (aria-hidden), so it simply goes:
   the cream section above meets the white one below with nothing lost. `js` is
   set by an inline script in <head>, so this never flashes. */
html:not(.js) .e-touch { display: none; }

/* --- 37b. The closing band, on white ------------------------------------- */

.e-cta.e-cta--light {
  background: var(--white);
  color: var(--text-strong);
  min-height: 0;
  display: block;
}
.e-cta--light .e-cta__ground,
.e-cta--light .e-cta__ribbons { display: none; }
.e-cta.e-cta--light::after { content: none; }

.e-cta--light .e-cta__inner { align-items: center; text-align: center; }
.e-cta--light .e-label { justify-content: center; color: var(--text-muted); }
.e-cta--light .e-cta__title { max-width: 24ch; color: var(--text-strong); text-wrap: balance; }
.e-cta--light .e-cta__actions { justify-content: center; }

/* Purple, not yellow. The house rule is that yellow only ever sits on dark or
   on purple — on white it is a highlighter pen. Same button, correct ground. */
.e-cta--light .e-btn--accent {
  background: var(--ebr-purple);
  border-color: var(--ebr-purple);
  color: var(--white);
}
.e-cta--light .e-btn--accent:hover {
  background: var(--ebr-purple-press);
  border-color: var(--ebr-purple-press);
  color: var(--white);
}
.e-cta--light .e-spark { background: var(--ebr-purple); }

.e-cta--light .e-cta__email {
  color: var(--text-strong);
  border-bottom-color: var(--line-hairline);
}
.e-cta--light .e-cta__email:hover { color: var(--ebr-purple); border-bottom-color: var(--ebr-purple); }

/* --- 37c. The footer, on white ------------------------------------------

   A translation, not a redesign: every value here is the light-ground token
   that matches the on-dark one it replaces, so the footer keeps its structure
   and only changes ground. */

.e-footer.e-footer--light { background: var(--white); color: var(--text-strong); }

.e-footer--light .e-footer__coltitle,
.e-footer--light .e-footer__list a,
.e-footer--light .e-offices li,
.e-footer--light .e-ask__note,
.e-footer--light .e-social a { color: var(--text-muted); }

.e-footer--light .e-footer__list a:hover,
.e-footer--light .e-social a:hover { color: var(--ebr-purple); }
.e-footer--light .e-social a:hover { background: rgba(107, 0, 246, .07); }

.e-footer--light .e-offices .e-offices__city { color: var(--text-strong); }

/* The mark flips with the ground — the white lockup is invisible here. */
.e-footer--light .e-footer__mark { content: url("../assets/wordmark.svg"); }

.e-footer--light .e-arrow--on-dark {
  color: var(--text-strong);
  border-bottom-color: var(--line-hairline);
}
.e-footer--light .e-arrow--on-dark:hover { color: var(--ebr-purple); border-bottom-color: var(--ebr-purple); }

.e-footer--light .e-ask__link {
  border-color: var(--line-hairline);
  color: var(--text-muted);
}
.e-footer--light .e-ask__link:hover {
  background: var(--ebr-purple);
  border-color: var(--ebr-purple);
  color: var(--white);
}
.e-footer--light .e-ask__mark { opacity: .75; }

.e-footer--light .e-footer__bar,
.e-footer--light .e-tick { border-color: var(--line-hairline); }
.e-footer--light .e-footer__bar > div,
.e-footer--light .e-tick__set li { color: var(--text-muted); }
.e-footer--light .e-tick__set li::after { background: var(--line-hairline); }

@media (max-width: 700px) {
  .e-cta--light .e-cta__actions { align-items: stretch; width: 100%; }
  .e-cta--light .e-cta__email { align-self: center; }
}

/* ==========================================================================
   6b. Header over the open mobile menu (Adrian, 20 Oct 2026)

   The menu is a full paper sheet under the header. On a page that opens on a
   dark or purple banner the header still carries `is-onbrand`, which left a
   white lockup and white close lines on paper. While the menu is open the
   header goes clear (the paper shows through) with the dark mark and ink
   lines. `#header` so it outranks the four-class banner rules in §25a/§26a.
   ========================================================================== */
#header.is-menuopen {
  background: transparent;
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  border-bottom-color: transparent;
}
#header.is-menuopen::after { opacity: 0; }
#header.is-menuopen .e-logo--dark  { opacity: 1; }
#header.is-menuopen .e-logo--light { opacity: 0; }
#header.is-menuopen .e-burger span { background: var(--ink-900); }
/* The button follows the same rule: yellow only on dark or purple, so on the
   paper sheet it goes back to the standard purple. */
#header.is-menuopen .e-btn--primary {
  background: var(--ebr-purple);
  border-color: var(--ebr-purple);
  color: var(--white);
}
#header.is-menuopen .e-btn--primary:hover {
  background: var(--ebr-purple-press);
  border-color: var(--ebr-purple-press);
  color: var(--white);
}

/* Homepage Proof band (stats + its Book a call hand-off): hidden on phones,
   700px and below, the site's phone edge (Adrian, 20 Oct 2026). Tablets and
   desktop keep it. */
@media (max-width: 700px) {
  .e-proof { display: none; }
}

/* Phones (700px and below): the header's Book a call button is hidden while
   the menu is closed and appears when the hamburger opens it (Adrian,
   20 Oct 2026). The bottom call bar still offers the booking on phones.
   Tablets keep the button in the header. */
@media (max-width: 700px) {
  .e-header:not(.is-menuopen) .e-header__actions > [data-contact] { display: none; }
}

/* ==========================================================================
   36. What we do on phones — the swipe deck (Option A, Adrian 20 Oct 2026)

   700px and below only. The six service rows become a row of cards you swipe
   sideways: an ink panel with a still of the service's 3D icon, the service
   name, its headline, and a link. The qualifier sentence is dropped here (it
   stays on tablet and desktop). The four capability accordions become four
   chips, each opening a bottom sheet (§36c, built by site.js §4c).

   Same markup as every other width — the CMS keys are not duplicated — so
   this block is restyling, plus three small additions that are hidden above
   700px: the per-card link, the deck bar and the chip row.
   ========================================================================== */

/* Hidden everywhere until the phone rules below switch them on. */
.e-service__link,
.e-svcdeck,
.e-capchips { display: none; }

@media (max-width: 700px) {

  /* --- 36a. The row --------------------------------------------------- */

  /* Bleeds to the screen edges so the next card is cut by the glass, not by
     the page margin; the padding puts the first card back on the margin. */
  .e-services {
    flex-direction: row;
    gap: 12px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--page-margin);
    margin-inline: calc(-1 * var(--page-margin));
    padding: 0 var(--page-margin) 4px;
    scrollbar-width: none;
    -ms-overflow-style: none;
    overscroll-behavior-x: contain;
  }
  .e-services::-webkit-scrollbar { display: none; }

  /* ~58px of the next card shows at 390. The cap keeps a peek on a 320px
     phone too, where a flat 300px card would fill the screen. */
  .e-service {
    flex: 0 0 min(300px, calc(100vw - 88px));
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
    background: var(--white);
    border-radius: 16px;
    overflow: hidden;
    scroll-snap-align: start;
    align-items: stretch;
  }
  .e-service + .e-service { border-top: 0; }

  /* Cards off to the right never cross the viewport, so the scroll reveal
     would leave them invisible. The row as a whole still fades in. */
  .js .e-services .e-service.reveal { opacity: 1; transform: none; }

  /* --- 36b. The card -------------------------------------------------- */

  /* The 3D slot, reused as the picture panel. A still, not three.js: ~8 KB a
     card instead of 400 KB, which is why the live icons never load here. */
  .e-service__icon {
    display: block;
    order: -1;
    width: 100%;
    height: 200px;
    justify-self: auto;
    opacity: 1;
    transform: none;
    transition: none;
    background: var(--ink-900) no-repeat center / 176px auto;
  }
  [data-service-icon] .e-service__icon { height: 200px; }
  [data-service-icon="web"]                 .e-service__icon { background-image: url("../assets/icons/services/web.webp"); }
  [data-service-icon="website-maintenance"] .e-service__icon { background-image: url("../assets/icons/services/website-maintenance.webp"); }
  [data-service-icon="ai-automation"]       .e-service__icon { background-image: url("../assets/icons/services/ai-automation.webp"); }
  [data-service-icon="seo-aeo-geo"]         .e-service__icon { background-image: url("../assets/icons/services/seo-aeo-geo.webp"); }
  [data-service-icon="social-performance"]  .e-service__icon { background-image: url("../assets/icons/services/social-performance.webp"); }

  /* App Development has no 3D icon yet, so it gets a flat one in the same
     vocabulary: a purple phone and the yellow spark from the logo. */
  .e-service:not([data-service-icon]) .e-service__icon { position: relative; }
  .e-service:not([data-service-icon]) .e-service__icon::before {
    content: "";
    position: absolute;
    left: 50%; top: 50%;
    width: 64px; height: 108px;
    margin: -54px 0 0 -32px;
    border-radius: 14px;
    background: var(--ebr-purple);
    box-shadow: inset 0 0 0 6px #7D1FFF;
    transform: rotate(-8deg);
  }
  .e-service:not([data-service-icon]) .e-service__icon::after {
    content: "";
    position: absolute;
    left: calc(50% + 30px); top: calc(50% - 74px);
    width: 30px; height: 28px;
    background: var(--ebr-yellow);
    -webkit-mask: url("../assets/sparkle.svg") no-repeat center / contain;
            mask: url("../assets/sparkle.svg") no-repeat center / contain;
  }

  .e-service > div:not(.e-service__icon) { padding: 20px 20px 0; }

  /* The service name leads; the headline sits under it as the one line. */
  /* Scoped to the card: the Capabilities label below reuses this class. */
  .e-service .e-service__tag {
    font: var(--weight-medium) 20px/1.3 var(--font-display);
    letter-spacing: normal;
    text-transform: none;
    color: var(--text-strong);
    padding-bottom: 8px;
  }
  .e-service__title {
    font: var(--weight-regular) 15px/1.45 var(--font-body);
    letter-spacing: normal;
    color: var(--text-muted);
  }
  .e-service__qual { display: none; }

  .e-service__link {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    margin-top: auto;
    padding: 12px 20px 12px;
    font: var(--weight-medium) 15px/1 var(--font-body);
    color: var(--ebr-purple);
    text-decoration: none;
  }
  .e-service__link:hover { color: var(--ebr-purple-press); text-decoration: none; }
  /* App Development has no page yet, so no link: keep its card the same
     height as the rest without a dead control. */
  .e-service:not(:has(.e-service__link))::after { content: ""; display: block; min-height: 56px; margin-top: auto; }

  /* --- The bar: counter, dots, arrows. Shown only once site.js §4b has
     wired it (`is-ready`), so a failed script never leaves dead buttons. */
  .e-svcdeck.is-ready {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    margin-top: var(--space-5);
  }
  .e-svcdeck__count {
    margin: 0;
    min-width: 3.2em;
    font: var(--weight-medium) 13px/1 var(--font-body);
    letter-spacing: .04em;
    color: var(--text-strong);
  }
  .e-svcdeck__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .e-svcdeck__dots { display: flex; align-items: center; gap: 6px; margin-right: auto; }
  .e-svcdeck__dots span {
    width: 6px; height: 6px;
    border-radius: 3px;
    background: #BDBDBD;
    transition: width var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
  }
  .e-svcdeck__dots span.is-on { width: 20px; background: var(--ebr-purple); }
  .e-svcdeck__nav { display: flex; gap: 8px; }
  .e-svcdeck__btn {
    width: 44px; height: 44px;
    display: flex; align-items: center; justify-content: center;
    padding: 0;
    border: 0;
    border-radius: 22px;
    background: var(--ink-900);
    color: var(--white);
    cursor: pointer;
  }
  .e-svcdeck__btn:disabled { background: #ECEAE5; color: #9A9A9A; cursor: default; }

  /* --- 36c. Capabilities: chips, and the sheet they open -------------- */

  /* No rule above the block here: the deck already ends the list. The chips
     replace the accordion only once site.js §4c has built them (`has-chips`);
     if it cannot, the accordion stays. */
  .e-caps { border-top: 0; margin-top: var(--space-7); padding-top: 0; }
  .e-caps.has-chips .e-capchips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: var(--space-4); }
  .e-caps.has-chips .e-caps__grid { display: none; }
  .e-capchip {
    min-height: 44px;
    padding: 0 16px;
    border: 0;
    border-radius: 22px;
    background: var(--white);
    font: var(--weight-medium) 14px/1 var(--font-body);
    color: var(--text-strong);
    cursor: pointer;
  }
  .e-capchip:hover { color: var(--ebr-purple); }
}

@media (max-width: 700px) and (prefers-reduced-motion: reduce) {
  .e-services { scroll-behavior: auto; }
  .e-svcdeck__dots span { transition: none; }
}

/* The sheet is a modal <dialog> in the top layer, so the sticky header and the
   call bar sit under it without z-index bookkeeping. Esc closes it natively. */
.e-capsheet {
  position: fixed;
  inset: auto 0 0 0;
  width: 100%;
  max-width: none;
  max-height: 80svh;
  margin: 0;
  padding: 10px var(--page-margin) calc(28px + env(safe-area-inset-bottom));
  box-sizing: border-box;
  border: 0;
  border-radius: 16px 16px 0 0;
  background: var(--white);
  color: var(--text-strong);
  overflow-y: auto;
}
.e-capsheet:focus { outline: none; }
.e-capsheet::backdrop { background: rgba(28, 28, 28, .45); }
.e-capsheet[open] { animation: e-capsheet-in var(--dur-base) var(--ease-out); }
@keyframes e-capsheet-in { from { transform: translateY(100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .e-capsheet[open] { animation: none; } }
.e-capsheet__grip { display: block; width: 36px; height: 4px; margin: 0 auto; border-radius: 2px; background: #BDBDBD; }
.e-capsheet__head { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; min-height: 44px; }
.e-capsheet__title { margin: 0; font: var(--weight-medium) 22px/1.25 var(--font-display); }
.e-capsheet__close {
  width: 44px; height: 44px;
  margin-right: -12px;
  display: flex; align-items: center; justify-content: center;
  padding: 0; border: 0; background: none; color: var(--text-strong); cursor: pointer;
}
.e-capsheet__list {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  font: var(--weight-regular) 17px/1.3 var(--font-body);
}

/* ==========================================================================
   38. Video testimonials (homepage, Adrian 20 Oct 2026)

   After Function's "Real people. Real results." (Mobbin): portrait cards,
   the person and their one line laid over the bottom of the frame, one play
   mark in the middle. Four across on desktop; a swipe row below 1081px,
   the same native-scroll-plus-snap pattern as the About page's quote rail.
   Plays in the showreel lightbox — site.js §8b.
   ========================================================================== */

.e-vtst__head { margin-bottom: var(--space-7); }
.e-vtst__title { margin-top: var(--space-4); }

/* One column per card that is showing: three films sit three across, a fourth
   (or an un-hidden placeholder) makes it four, with no empty slot left over. */
.e-vtst__rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: var(--space-5);
}

.e-vtst { margin: 0; min-width: 0; }
/* A card can be parked with the `hidden` attribute (index.html, card 4). */
.e-vtst[hidden] { display: none; }

/* The whole card is the control. */
.e-vtst__play {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  padding: 0;
  border: 0;
  border-radius: var(--radius-media);
  overflow: hidden;
  background: #262626;
  color: var(--white);
  text-align: left;
  font: inherit;
  cursor: pointer;
}
[data-vtst][data-placeholder] .e-vtst__play { cursor: default; }
/* Inset and white: the rail scrolls sideways, which clips anything drawn
   outside the card, and the site's purple ring is under 3:1 on ink. */
.e-vtst__play:focus-visible { outline: 2px solid var(--white); outline-offset: -4px; }

/* The poster: a still when there is one, cropped to the card. The bottom
   third darkens so the caption reads on any footage. */
.e-vtst__poster { position: absolute; inset: 0; }
.e-vtst__poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
.e-vtst__poster::after {
  content: "";
  position: absolute;
  inset: 35% 0 0 0;
  background: linear-gradient(to bottom, rgba(28, 28, 28, 0), rgba(28, 28, 28, .7) 45%, rgba(28, 28, 28, .92));
}

/* The placeholder says so on the card, in yellow — allowed here, this is ink. */
.e-vtst__badge {
  position: absolute;
  top: 14px; left: 14px;
  padding: 6px 10px;
  border-radius: var(--radius-pill);
  background: var(--ebr-yellow);
  color: var(--ink-900);
  font: var(--weight-medium) 11px/1 var(--font-body);
  letter-spacing: .08em;
  text-transform: uppercase;
}
[data-vtst]:not([data-placeholder]) .e-vtst__badge { display: none; }

.e-vtst__icon {
  position: absolute;
  left: 50%; top: 42%;
  width: 56px; height: 56px;
  margin: -28px 0 0 -28px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, .16);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
  color: var(--white);
  transition: background-color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.e-vtst__icon svg { margin-left: 3px; }
[data-vtst]:not([data-placeholder]) .e-vtst__play:hover .e-vtst__icon { background: var(--ebr-purple); transform: scale(1.06); }
[data-vtst][data-placeholder] .e-vtst__icon { opacity: .45; }

.e-vtst__cap {
  position: absolute;
  left: 18px; right: 18px; bottom: 18px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.e-vtst__cap strong { font: var(--weight-medium) 16px/1.3 var(--font-body); color: var(--white); }
.e-vtst__cap span { font: var(--type-small); color: var(--text-on-dark-muted); }
.e-vtst__cap .e-vtst__line { margin-top: 6px; color: var(--white); }
/* Over real footage the muted line needs more weight than over flat ink. */
[data-vtst]:not([data-placeholder]) .e-vtst__cap span { color: rgba(255, 255, 255, .82); }
[data-vtst]:not([data-placeholder]) .e-vtst__cap .e-vtst__line { color: var(--white); }
/* Placeholder text keeps its "hole" colour but must stay readable on the
   darkened frame. */
.e-vtst__cap [data-todo][data-todo] { color: var(--text-on-dark-muted); }

@media (prefers-reduced-motion: reduce) {
  .e-vtst__icon { transition: none; }
  [data-vtst]:not([data-placeholder]) .e-vtst__play:hover .e-vtst__icon { transform: none; }
}

/* Below desktop: a swipe row, bleeding to the screen edge, next card cut. */
@media (max-width: 1080px) {
  .e-vtst__rail {
    display: flex;
    gap: var(--space-4);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--page-margin);
    margin-inline: calc(-1 * var(--page-margin));
    padding: 0 var(--page-margin) 4px;
    scrollbar-width: none;
    -ms-overflow-style: none;
    overscroll-behavior-x: contain;
  }
  .e-vtst__rail::-webkit-scrollbar { display: none; }
  .e-vtst { flex: 0 0 min(300px, 42vw); scroll-snap-align: start; }
}
@media (max-width: 700px) {
  .e-vtst { flex-basis: min(280px, calc(100vw - 96px)); }
  .e-vtst__head { margin-bottom: var(--space-6); }
}

/* --- The lightbox, opened for a testimonial (site.js §8b) --------------- */

/* Phone-recorded clips are tall. Same conversion as the 16:9 rule above:
   the wrapper is exactly as wide as the video so the close button sits on
   its corner. */
.e-lightbox.is-portrait .e-lightbox__inner {
  width: min(440px, 100%);
  max-width: calc((100vh - var(--space-7) * 2) * 9 / 16);
}
.e-lightbox.is-portrait .e-lightbox__frame { aspect-ratio: 9 / 16; }
.e-lightbox__frame video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  background: #000;
}

/* ==========================================================================
   38b. One video, featured — service pages, Ways to work, About

   After Mews' "Hear it from our hoteliers" (Mobbin): the film on one side,
   the strongest sentence from it on the other. The card is the homepage's
   `.e-vtst` without its overlaid caption; the person is named in the copy.
   ========================================================================== */

.e-vtstf__grid {
  display: grid;
  grid-template-columns: minmax(0, 360px) minmax(0, 1fr);
  gap: var(--space-8, 64px);
  align-items: center;
}
.e-vtstf__card { width: 100%; }
.e-vtstf__copy { display: flex; flex-direction: column; gap: var(--space-5); max-width: 40ch; }
.e-vtstf__copy .e-display { margin: 0; }
.e-vtstf__quote { margin: 0; font: var(--type-lead); }
.e-vtstf__who { margin: 0; display: flex; flex-direction: column; gap: 4px; font: var(--type-small); }
.e-vtstf__who strong { font: var(--weight-medium) 16px/1.3 var(--font-body); }
.e-ground-ink .e-vtstf__quote { color: var(--white); }
.e-ground-ink .e-vtstf__who strong { color: var(--white); }
.e-ground-ink .e-vtstf__who span { color: var(--text-on-dark-muted); }
.e-vtstf__who span { color: var(--text-muted); }

@media (max-width: 860px) {
  .e-vtstf__grid { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
  .e-vtstf__card { max-width: 360px; }
  /* Copy first on a narrow screen: the claim, then the film that backs it. */
  .e-vtstf__copy { order: -1; }
}

/* ==========================================================================
   38c. One video, inline — the contact page's card stack, homepage pricing

   A small portrait thumbnail and one line, the whole row one button. Light
   by default (it sits in a bordered card); `--panel` gives it its own dark
   panel for an ink section.
   ========================================================================== */

.e-vtsti { width: 100%; }
.e-vtsti__btn {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  border-radius: var(--radius-md);
}
[data-vtst][data-placeholder] .e-vtsti__btn { cursor: default; }
.e-vtsti__btn:focus-visible { outline: 2px solid var(--ebr-purple); outline-offset: 4px; }

.e-vtsti__thumb {
  position: relative;
  flex: 0 0 96px;
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-media);
  overflow: hidden;
  background: #262626;
  display: grid;
  place-items: center;
}
.e-vtsti__thumb .e-vtst__poster::after { display: none; }
.e-vtsti__icon {
  position: relative;
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: rgba(255, 255, 255, .16);
  color: var(--white);
  transition: background-color var(--dur-fast) var(--ease-out);
}
.e-vtsti__icon svg { margin-left: 2px; }
[data-vtst]:not([data-placeholder]) .e-vtsti__btn:hover .e-vtsti__icon { background: var(--ebr-purple); }
[data-vtst][data-placeholder] .e-vtsti__icon { opacity: .45; }

.e-vtsti__copy { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; min-width: 0; }
.e-vtsti__quote { font: var(--type-body); color: var(--text-strong); }
.e-vtsti__who { font: var(--type-small); color: var(--text-muted); }
.e-vtsti__who strong { font-weight: var(--weight-medium); color: var(--text-strong); }

/* On a light ground the placeholder marker is an ink pill: yellow is only
   ever allowed on ink or purple. */
.e-vtsti .e-vtst__badge { position: static; background: var(--ink-900); color: var(--white); }

.e-vtsti--panel { padding: var(--space-5); border-radius: var(--radius-md); background: #262626; }
.e-vtsti--panel .e-vtst__badge { background: var(--ebr-yellow); color: var(--ink-900); }
.e-vtsti--panel .e-vtsti__thumb { background: var(--ink-900); }
.e-vtsti--panel .e-vtsti__quote,
.e-vtsti--panel .e-vtsti__who strong { color: var(--white); }
.e-vtsti--panel .e-vtsti__who { color: var(--text-on-dark-muted); }
.e-vtsti--panel [data-todo][data-todo] { color: var(--text-on-dark-muted); }
.e-vtsti--panel .e-vtsti__btn:focus-visible { outline-color: var(--white); }

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

/* Homepage pricing: the value-for-money clip sits between the plans and the
   call to action. */
.e-plans__vtst { margin-top: var(--space-5); }

/* ==========================================================================
   39. The studio, homepage — after TinyWins (Studio 3, Adrian 21 Oct 2026)

   Purple field (`.e-ground-brand`, which already lifts muted text to 82%
   white — 5:1 on #6B00F6). The headline is set like every other section
   heading (`.e-display--m`). Then three equal portraits, then the quote and the numbers.
   No rules anywhere; space divides it.
   ========================================================================== */

/* Set like every other section heading (`.e-display--m`, sentence case) —
   Adrian, 21 Oct 2026. Only the measure is local. */
.e-std__title { margin: var(--space-5) 0 0; max-width: 24ch; text-wrap: balance; }

.e-std__team {
  list-style: none;
  margin: var(--space-8) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-6);
}
.e-std__member { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.e-std__photo {
  display: block;
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-media);
  overflow: hidden;
  background: #4A00AB;
}
.e-std__photo img,
.e-std__photo video { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 15%; }
/* The About page's hover take (site.js §15, `.e-team__portrait`) runs here as
   is; these two lines only keep this card's 4:5 frame and purple backing over
   that component's 3:4 and grey. */
.e-std__photo.e-team__portrait { aspect-ratio: 4 / 5; background: #4A00AB; }
/* TinyWins sets the role small beside the name; the same here, baseline-led. */
.e-std__who { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
.e-std__who strong { font: var(--weight-medium) 18px/1.3 var(--font-body); color: var(--white); }
.e-std__who span { font: var(--type-small); color: var(--text-on-dark-muted); }

.e-std__foot {
  margin-top: var(--space-8);
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: var(--space-8);
  align-items: end;
}
.e-std__quote { margin: 0; display: flex; flex-direction: column; gap: var(--space-4); }
.e-std__pull { margin: 0; font: var(--type-lead); color: var(--white); max-width: 46ch; }
.e-std__byline {
  font: var(--type-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-on-dark-muted);
}
.e-std__side { display: flex; flex-direction: column; gap: var(--space-5); align-items: flex-start; }
.e-std__stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-5); width: 100%; }
.e-std__stats > div { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.e-std__num { font: var(--weight-medium) clamp(32px, 3.2vw, 44px)/1 var(--font-display); color: var(--white); }
.e-std__lbl { font: var(--type-small); color: var(--text-on-dark-muted); }
.e-std__link { min-height: 44px; display: inline-flex; align-items: center; }
.e-std__link svg { color: var(--ebr-yellow); }

/* Below desktop the cards are too narrow to promise the role a place beside
   every name; one stacks and the next doesn't, so stack all three. */
@media (max-width: 1080px) {
  .e-std__who { flex-direction: column; align-items: flex-start; gap: 2px; }
}

@media (max-width: 860px) {
  .e-std__foot { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); align-items: start; }
}

/* Phones: the three faces become a swipe row, the next one cut by the glass —
   stacked, three portraits are 1,300px of faces before the quote. */
@media (max-width: 700px) {
  .e-std__team {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--page-margin);
    margin: var(--space-7) calc(-1 * var(--page-margin)) 0;
    padding: 0 var(--page-margin) 4px;
    scrollbar-width: none;
    overscroll-behavior-x: contain;
  }
  .e-std__team::-webkit-scrollbar { display: none; }
  /* Keyboard users scroll the row by focusing it (nothing inside is focusable). */
  .e-std__team:focus-visible { outline: 2px solid var(--ebr-yellow); outline-offset: 4px; }
  .e-std__member { flex: 0 0 min(264px, calc(100vw - 104px)); scroll-snap-align: start; }
  .e-std__who { flex-direction: column; align-items: flex-start; gap: 2px; }
  .e-std__stats { gap: var(--space-4); }
}

/* 21 Oct 2026, Adrian: the three figures (2019 / 25 / 3) are hidden on phones
   only. The quote, byline and "Meet the studio" link stay. */
@media (max-width: 700px) {
  .e-std__stats { display: none; }
}

/* ==========================================================================
   27x. Service detail, 23 Oct 2026 — the banner film and the stage tiles.
   The banner now closes on a full-width film frame (the whole frame plays
   it, in the showreel's pop-up), "What it involves" is a bento of stage
   tiles (Option 2 "Playground bento", refs Framer, Sunday, Ada), and How it
   runs is gone from these pages. Own class names: site.js wires the
   homepage's .e-reel__stage itself and must not pick these up.
   ========================================================================== */

/* --- the film ------------------------------------------------------------ */
.e-dth:has(.e-dtf) { padding-bottom: 0; }
.e-dtf {
  position: relative;
  margin-top: var(--space-8);
  height: clamp(360px, 52vw, 86svh);
  overflow: hidden;
  background: var(--ink-900);
}
.e-dtf__loop { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
.e-dtf__stage {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px;
  border: 0; padding: 0; background: transparent; color: var(--white); cursor: pointer; z-index: 1;
}
/* The same flat hairline field the showreel sits on until there is a picture. */
.e-dtf__stage::before {
  content: ""; position: absolute; inset: 0;
  background-image: linear-gradient(to right, rgba(255,255,255,.05) 1px, transparent 1px);
  background-size: 96px 100%;
}
.e-dtf.has-loop .e-dtf__stage::before { background: linear-gradient(to bottom, rgba(14,10,22,.25), rgba(14,10,22,.55)); }
/* The thumbnail (25 Oct 2026): a still of the film's key art. It already carries
   the hairline field, so the stage drops its own; a silent loop, once there is
   one, has its own poster and replaces it. */
.e-dtf__poster, .e-dtf__poster img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; display: block; }
.e-dtf:has(.e-dtf__poster) .e-dtf__stage::before { background: none; }
.e-dtf.has-loop .e-dtf__poster { display: none; }
.e-dtf__play {
  position: relative; width: 96px; height: 96px; border-radius: var(--radius-pill);
  border: 1px solid rgba(255,255,255,.35);
  display: flex; align-items: center; justify-content: center;
  transition: var(--transition-hover);
}
.e-dtf__stage:hover .e-dtf__play,
.e-dtf__stage:focus-visible .e-dtf__play { background: var(--ebr-purple); border-color: var(--ebr-purple); transform: scale(1.06); }
.e-dtf__stage:focus-visible { outline: 2px solid var(--white); outline-offset: -6px; }
.e-dtf__stamp {
  position: relative; font: var(--type-label); letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--text-on-dark-muted); transition: letter-spacing .4s ease, color .2s ease;
}
.e-dtf__stage:hover .e-dtf__stamp { letter-spacing: .26em; color: var(--white); }

/* The "on its way" panel inside the shared pop-up. */
.e-lightbox__frame .e-soon {
  width: 100%; height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  background: var(--ink-900); color: var(--white); text-align: center; padding: 24px;
  font: var(--type-lead);
}
.e-lightbox__frame .e-soon small { font: var(--type-small); color: var(--text-on-dark-muted); max-width: 34ch; }

/* --- the tiles ----------------------------------------------------------- */
.e-dti--bento { padding-block: var(--section-y); }
.e-dtb__head {
  display: grid; grid-template-columns: 7fr 5fr; gap: var(--space-8);
  align-items: end; margin-bottom: var(--space-7);
}
.e-dtb__lead { display: flex; flex-direction: column; gap: var(--space-5); }
.e-dtb__title { max-width: 18ch; margin: 0; }
.e-dtb__aside { display: flex; flex-direction: column; gap: var(--space-4); }
.e-dtb__intro { margin: 0; font: var(--type-copy); color: var(--text-copy); max-width: 46ch; }
.e-dtb__hint { margin: 0; font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-strong); }
.e-dtb__hint[hidden] { display: none; }

.e-dtb__grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: var(--space-4);
}
.e-dtb__tile {
  grid-column: span 4;
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-6);
  background: var(--paper);
  border-radius: var(--radius-lg);
  min-width: 0;
  transition: background-color .25s ease, transform .25s ease;
}
.e-dtb__tile--5 { grid-column: span 5; }
.e-dtb__tile--7 { grid-column: span 7; }
.e-dtb__tile.has-clip { cursor: pointer; }
.e-dtb__tile.has-clip:hover { transform: translateY(-2px); }
.e-dtb__top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); min-height: 36px; }
.e-dtb__n { font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-muted); }
.e-dtb__play {
  width: 36px; height: 36px; border-radius: var(--radius-pill);
  border: var(--border-hairline); background: var(--white); color: var(--ink-900);
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  transition: var(--transition-hover);
}
.e-dtb__play[hidden] { display: none; }
.e-dtb__play:hover, .e-dtb__play[aria-pressed="true"] { background: var(--ink-900); color: var(--white); border-color: var(--ink-900); }
.e-dtb__name {
  margin: 0; font: var(--weight-medium) var(--size-body-l)/1.25 var(--font-display);
  letter-spacing: var(--tracking-title); color: var(--text-strong); text-wrap: pretty;
}
.e-dtb__body { margin: 0; font: var(--type-copy-small); color: var(--text-copy); max-width: 60ch; }
.e-dtb__media {
  margin-top: auto; aspect-ratio: 16 / 9; border-radius: var(--radius-md);
  background: var(--ink-100); overflow: hidden; display: grid; place-items: center; padding: var(--space-4);
}
.e-dtb__tile .e-dtb__body + .e-dtb__media,
.e-dtb__tile .e-dtb__seg + .e-dtb__media { margin-top: auto; }
.e-dtb__body { margin-bottom: var(--space-3); }
.e-dtb__media video { width: 100%; height: 100%; object-fit: cover; display: block; grid-area: 1 / 1; }
.e-dtb__tile.has-clip .e-dtb__media { padding: 0; }
/* The Design tile keeps its switchable stills; its clip covers them only while playing. */
.e-dtb__media[data-views] video { opacity: 0; transition: opacity .2s ease; }
.e-dtb__tile.is-playing .e-dtb__media[data-views] video { opacity: 1; }
/* 24 Oct 2026: the Design tile's views are real stills now (each clip's own
   poster frame), each with a clip of its own over it; the switch hides the
   other two. `display` above would otherwise beat the [hidden] attribute. */
.e-dtb__still { width: 100%; height: 100%; object-fit: cover; display: block; grid-area: 1 / 1; }
.e-dtb__still[hidden], .e-dtb__media video[hidden] { display: none; }
.e-dtb__todo { grid-area: 1 / 1; font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-muted); text-align: center; }
.e-dtb__todo[hidden] { display: none; }
/* §28b Compact bento (26 Oct 2026): the clip panel is about 25% shorter
   (16:9 → 64:27). The motion graphics fill their frame top to bottom, so they
   scale down to fit instead of being cropped; their grey matches the panel's,
   so the extra width at the sides reads as one surface. */
.e-dtb__media { aspect-ratio: 64 / 27; position: relative; }
/* Pinned to the panel: as grid items they sized the row to their own 16:9
   height and got cropped by the shorter panel. */
.e-dtb__media video, .e-dtb__still { position: absolute; inset: 0; object-fit: contain; }

/* The Design tile's three-way switch — the active segment goes ink. */
.e-dtb__seg { display: inline-flex; align-self: flex-start; gap: 2px; padding: 3px; border-radius: var(--radius-pill); background: var(--white); }
.e-dtb__seg button {
  border: 0; background: transparent; cursor: pointer; padding: 7px 12px; border-radius: var(--radius-pill);
  font: var(--type-label); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--text-muted);
  transition: background-color .2s ease, color .2s ease;
}
.e-dtb__seg button:hover { color: var(--ink-900); }
.e-dtb__seg button[aria-pressed="true"] { background: var(--ink-900); color: var(--white); }
.e-dtb__seg button:focus-visible, .e-dtb__play:focus-visible { outline: 2px solid var(--ebr-purple); outline-offset: 2px; }

/* "What we need from you" is the one ink tile: the visitor's part of the work. */
.e-dtb__tile--need { background: var(--ink-900); }
.e-dtb__tile--need .e-dtb__n { color: var(--text-on-dark-muted); }
.e-dtb__tile--need .e-dtb__name { color: var(--white); }
.e-dtb__need { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.e-dtb__need li { font: var(--type-copy-small); color: var(--text-on-dark-muted); padding-inline-start: var(--space-5); position: relative; }
.e-dtb__need li::before { content: ""; position: absolute; inset-inline-start: 0; top: .6em; width: 6px; height: 1px; background: var(--ebr-yellow); }

@media (max-width: 1080px) {
  .e-dtb__head { grid-template-columns: 1fr; gap: var(--space-5); align-items: start; }
  .e-dtb__tile, .e-dtb__tile--5, .e-dtb__tile--7 { grid-column: span 6; }
  .e-dtb__tile--need { grid-column: span 12; }
}
@media (max-width: 700px) {
  .e-dtf { height: clamp(300px, 72vw, 70svh); margin-top: var(--space-7); }
  .e-dtf__play { width: 76px; height: 76px; }
  .e-dtb__grid { grid-template-columns: 1fr; }
  .e-dtb__tile, .e-dtb__tile--5, .e-dtb__tile--7, .e-dtb__tile--need { grid-column: auto; padding: var(--space-5); }
}
@media (prefers-reduced-motion: reduce) {
  .e-dtf__play, .e-dtf__stamp, .e-dtb__tile, .e-dtb__seg button { transition: none; }
  .e-dtb__tile.has-clip:hover, .e-dtf__stage:hover .e-dtf__play { transform: none; }
}

/* --- 27y. The film as the banner (23 Oct 2026) ----------------------------
   The text banner is gone; the film frame opens the page. It runs up under the
   sticky, see-through header (same device as .e-hero--film and .e-svcfield)
   and fills the first screen. site.js §2 drops the header's on-dark state once
   the frame has scrolled past ([data-header-brand]). */
.e-dth.e-dth--film {
  --dthf-header: 66px;
  padding: 0;
  margin-top: calc(var(--dthf-header) * -1);
  background: var(--ink-900);
}
.e-dth--film .e-dtf { margin-top: 0; height: 100svh; min-height: 480px; }
@media (max-width: 700px) {
  /* The header is 59px tall up to 700px, not 66: pulling up 66 here hid the
     top 7px of the frame above the screen. */
  .e-dth.e-dth--film { --dthf-header: 59px; }
  .e-dth--film .e-dtf { height: 88svh; min-height: 420px; margin-top: 0; }
}

/* §27z Breadcrumb in the What it involves section (23 Oct 2026). The film
   banner has no text, so the trail opens the first text section instead. */
.e-dtb__crumbs { margin-bottom: var(--space-7); }

/* §27z2 Bold phrases in the service detail copy (25 Oct 2026). Only the 400 and
   500 cuts are loaded, so a bare <strong> would be faked at 700. */
.e-dtb__intro strong, .e-dtb__body strong, .e-faq__a strong, .e-rel__line strong { font-weight: var(--weight-medium); color: var(--text-strong); }
.e-close__line strong, .e-close__railcopy strong { font-weight: var(--weight-medium); color: var(--white); }

/* ==========================================================================
   36s. What we do above 700px — the spotlight index (Idea 1, Adrian 26 Oct 2026)

   Built by site.js §4d, and switched on only by its `is-on` class, so with
   no script (or at phone width, where §36's swipe deck takes over) the six
   rows render exactly as before. The six names become one big type index on
   the left; the rows themselves are stacked in one cell on the right as the
   stage, and only the lit one shows. The stage picture is that service
   page's own "What it involves" animations, played in order (site.js §4d);
   the stills and the App Development phone below are only the fallback for
   a service with no films. The 3D icons are not used here any more.
   The Capabilities block below is untouched.
   ========================================================================== */

.e-spot__index { display: none; }

@media (min-width: 701px) {
  .e-spot.is-on {
    /* Unlit names pass 3:1 against paper as large text, never as body. */
    --spot-fade: #858582;
    display: grid;
    grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
    gap: var(--space-6) var(--space-8);
    align-items: start;
    padding-top: var(--space-4);
  }

  /* --- The index ------------------------------------------------------ */
  .e-spot.is-on .e-spot__index {
    display: flex;
    flex-direction: column;
    list-style: none;
    margin: 0;
    padding: 0;
  }
  .e-spot__name {
    display: inline-flex;
    align-items: center;
    gap: .28em;
    padding: 2px 0;
    font: var(--weight-medium) clamp(34px, 4.4vw, 64px)/1.1 var(--font-display);
    letter-spacing: -.035em;
    color: var(--spot-fade);
    text-decoration: none;
    transition: color .45s var(--ease-out), transform .55s var(--ease-out);
  }
  .e-spot__name:hover { text-decoration: none; }
  .e-spot__name.is-near { transform: translateX(4px); }
  .e-spot__name.is-lit { color: var(--text-strong); transform: translateX(12px); }
  .e-spot__arrow {
    flex: none;
    width: .6em;
    height: .6em;
    color: var(--ebr-purple);
    opacity: 0;
    transform: translateX(-.4em);
    transition: opacity .35s var(--ease-out), transform .5s var(--ease-out);
  }
  .e-spot__name.is-lit .e-spot__arrow { opacity: 1; transform: none; }
  .e-spot__name:focus-visible {
    outline: none;
    text-decoration: underline;
    text-decoration-color: var(--ebr-purple);
    text-decoration-thickness: 3px;
    text-underline-offset: 8px;
  }

  /* --- The stage ------------------------------------------------------ */
  .e-spot.is-on .e-services {
    display: grid;
    transform: translateY(var(--spot-y, 0px));
    transition: transform .7s var(--ease-out);
  }
  .e-spot.is-on .e-service,
  .js .e-spot.is-on .e-service.reveal {
    grid-area: 1 / 1;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: 0;
    visibility: hidden;
    opacity: 0;
    transform: none;
    transition: opacity .35s var(--ease-out), visibility 0s linear .35s;
  }
  .e-spot.is-on .e-service + .e-service { border-top: 0; }
  .e-spot.is-on .e-service.is-lit,
  .js .e-spot.is-on .e-service.reveal.is-lit {
    visibility: visible;
    opacity: 1;
    transition: opacity .5s var(--ease-out);
  }

  /* The 3D slot becomes the stage picture: one 16:9 panel, the same shape
     and ground as the films' own tiles on the service pages. */
  .e-spot.is-on .e-service__icon {
    /* display:block beats §24's "no icon under 860", which was written for
       stacked rows; on the stage the picture is the point. */
    display: block;
    order: -1;
    position: relative;
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    margin-bottom: var(--space-5);
    justify-self: auto;
    opacity: 1;
    transform: none;
    transition: none;
    border-radius: 16px;
    background: var(--white) no-repeat center / auto 66%;
    overflow: hidden;
  }
  .e-spot.is-on [data-service-icon] .e-service__icon { height: auto; }
  .e-spot.is-on [data-service-icon="web"]                 .e-service__icon { background-image: url("../assets/icons/services/web.webp"); }
  .e-spot.is-on [data-service-icon="website-maintenance"] .e-service__icon { background-image: url("../assets/icons/services/website-maintenance.webp"); }
  .e-spot.is-on [data-service-icon="ai-automation"]       .e-service__icon { background-image: url("../assets/icons/services/ai-automation.webp"); }
  .e-spot.is-on [data-service-icon="seo-aeo-geo"]         .e-service__icon { background-image: url("../assets/icons/services/seo-aeo-geo.webp"); }
  .e-spot.is-on [data-service-icon="social-performance"]  .e-service__icon { background-image: url("../assets/icons/services/social-performance.webp"); }
  .e-spot.is-on .e-service .e-service__icon.has-film { background: var(--ink-100); }
  .e-spot.is-on .e-service .e-service__icon.has-film::before,
  .e-spot.is-on .e-service .e-service__icon.has-film::after { content: none; }
  .e-spot__film {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }

  /* App Development: the flat purple phone and yellow spark from the phone
     cards (§36b), drawn larger for the stage. */
  .e-spot.is-on .e-service:not([data-service-icon]) .e-service__icon::before {
    content: "";
    position: absolute;
    left: 50%; top: 50%;
    width: 22%; aspect-ratio: 64 / 108;
    transform: translate(-50%, -50%) rotate(-8deg);
    border-radius: 18px;
    background: var(--ebr-purple);
    box-shadow: inset 0 0 0 7px #7D1FFF;
  }
  .e-spot.is-on .e-service:not([data-service-icon]) .e-service__icon::after {
    content: "";
    position: absolute;
    left: 59%; top: 17%;
    width: 10%; aspect-ratio: 30 / 28;
    background: var(--ebr-yellow);
    -webkit-mask: url("../assets/sparkle.svg") no-repeat center / contain;
            mask: url("../assets/sparkle.svg") no-repeat center / contain;
  }

  .e-spot.is-on .e-service__title { max-width: 20ch; }
  .e-spot.is-on .e-service__qual { margin-top: var(--space-4); max-width: 38ch; }
  .e-spot.is-on .e-service__link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-top: var(--space-5);
    font: var(--weight-medium) 16px/1 var(--font-body);
    color: var(--ebr-purple);
    text-decoration: none;
  }
  .e-spot.is-on .e-service__link:hover { color: var(--ebr-purple-press); }

  /* The lit row's words rise in, one beat apart. Whole elements, not split
     words: the CMS edits these nodes, so the script never rewrites them. */
  .e-spot.is-on .e-service.is-lit > div:not(.e-service__icon),
  .e-spot.is-on .e-service.is-lit .e-service__qual,
  .e-spot.is-on .e-service.is-lit .e-service__link { animation: e-spot-rise .6s var(--ease-out) both; }
  .e-spot.is-on .e-service.is-lit .e-service__qual { animation-delay: .08s; }
  .e-spot.is-on .e-service.is-lit .e-service__link { animation-delay: .16s; }
}

@media (min-width: 701px) and (max-width: 960px) {
  .e-spot.is-on { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-5) var(--space-6); }
  .e-spot__name { font-size: clamp(26px, 3.9vw, 38px); }
  .e-spot.is-on .e-service__title { font-size: 24px; }
}

@keyframes e-spot-rise {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .e-spot__name, .e-spot__arrow, .e-spot.is-on .e-services,
  .e-spot.is-on .e-service, .js .e-spot.is-on .e-service.reveal { transition: none; }
  .e-spot__name.is-near, .e-spot__name.is-lit { transform: none; }
  .e-spot.is-on .e-services { transform: none; }
  .e-spot.is-on .e-service.is-lit > div:not(.e-service__icon),
  .e-spot.is-on .e-service.is-lit .e-service__qual,
  .e-spot.is-on .e-service.is-lit .e-service__link { animation: none; }
}

/* Ways to work: On camera (copied from the homepage) follows the ink models
   band, so it drops its top padding and the models band's bottom padding is
   the one gap between them (Adrian, 26 Oct 2026). */
#models + #video-testimonials { padding-top: 0; }
