/* ════════════════════════════════════════════════════════════
   CYTHÈRE — home page
   ------------------------------------------------------------
   The first half of this file is the ORIGINAL engine page's own
   stylesheet, copied verbatim. That is the point: Jim asked for the
   home page to be built in the engine page's language, so it uses
   the engine page's actual components — .e-hero, .e-section,
   .e-head, .e-card, .e-out-card, .e-btn, .nav-pill — not a
   lookalike.

   engine.html keeps its own inline copy and is never linked to this
   file. Copy, never share: engine.html is finished.

   index.html loads the same four OG stylesheets the engine page
   loads, then this one on top.
   ════════════════════════════════════════════════════════════ */

/* Pure black; kill base.css's dot-grid + grain so the sky is truly empty. */
html { background: #000 !important; }
body { background: #000; color: var(--text); }
body::before, body::after { display: none !important; }

/* ── NAV: absent over the hero, arrives once you leave orbit ── */
nav {
  opacity: 0; transform: translateY(-8px); pointer-events: none;
  transition: opacity 500ms var(--ease-out), transform 500ms var(--ease-out);
}
nav.is-visible { opacity: 1; transform: none; }
nav.is-visible .nav-pill { pointer-events: all; }
html:not(.js) nav { opacity: 1; transform: none; }

/* ── HERO ─────────────────────────────────── */
#e-hero {
  position: relative; z-index: 2; overflow: hidden;
  min-height: 100svh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center;
  padding: clamp(5rem, 12vh, 8rem) var(--gutter) clamp(7rem, 16vh, 11rem);
}

.e-hero-inner {
  position: relative; z-index: 4; width: 100%; max-width: 46rem;
  display: flex; flex-direction: column; align-items: center;
  margin-bottom: clamp(2rem, 8vh, 6rem);
}

/* The mark, lit exactly the way the home hero lights it: ONE blurred
   ellipse behind the shell and no filter whatsoever on the image.
   The image used to carry stacked drop-shadow()s, and iOS gives a
   filtered element a limited filter region — the halo got cut off at that
   rectangle and read as a glowing square. The home page never had the
   problem because it glows with a sibling gradient instead, so this now
   does the same. The gradient is an ellipse matched to the box and fades
   out before every edge (transparent at 66%, where the axes reach ~92%). */
.e-crest {
  position: relative;
  width: clamp(62px, 6.6vw, 98px);
  margin-bottom: clamp(1.6rem, 3.4vh, 2.6rem);
}
.e-crest::before {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 300%; height: 300%;
  transform: translate(-50%, -50%);
  background:
    radial-gradient(ellipse at center, rgba(255,255,255,0.46) 0%, transparent 36%),
    radial-gradient(ellipse at center,
      rgba(236,244,255,0.30) 0%,
      rgba(190,214,255,0.16) 30%,
      rgba(150,185,255,0.06) 50%,
      transparent 66%);
  filter: blur(18px);
  pointer-events: none;
  z-index: 0;
}
.e-crest img {
  position: relative; z-index: 1;
  width: 100%; height: auto; display: block;
}

/* hero copy sits a quarter smaller than the first pass */
.e-hero-title {
  font-family: var(--font-display); font-weight: 600; color: #fff;
  font-size: clamp(1.72rem, 4.05vw, 3.11rem);
  line-height: 0.99; letter-spacing: -0.035em;
}
.e-hero-title em { font-style: normal; color: #fff; }

.e-hero-sub {
  margin-top: clamp(1.15rem, 2.6vh, 1.8rem);
  font-family: var(--font-body); font-weight: 400;
  font-size: clamp(0.72rem, 0.83vw, 0.78rem); line-height: 1.68;
  color: rgba(196,206,226,0.62); max-width: 22rem;
}

.e-actions { margin-top: clamp(1.9rem, 4.4vh, 3.1rem); display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; justify-content: center; }
.e-btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-family: var(--font-body); font-weight: 500; font-size: var(--size-sm);
  padding: 0.7rem 1.4rem; border-radius: 999px;
  transition: transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
/* the hero button glows like it is standing in the light */
.e-btn--primary {
  color: #06080d; background: #fff;
  padding: 0.8rem 1.9rem;
  box-shadow:
    0 0 0 1px rgba(255,255,255,0.55),
    0 0 30px 4px rgba(226,238,255,0.30),
    0 0 90px 22px rgba(176,202,255,0.16);
}
.e-btn--primary:hover {
  transform: translateY(-2px);
  box-shadow:
    0 0 0 1px rgba(255,255,255,0.75),
    0 0 40px 6px rgba(232,242,255,0.42),
    0 0 120px 30px rgba(176,202,255,0.24);
}
.e-btn--ghost { color: rgba(214,220,236,0.8); border: 1px solid rgba(150,170,220,0.20); }
.e-btn--ghost:hover { color: #fff; border-color: rgba(150,175,255,0.5); }

/* the quiet line near the bottom of the frame */
.e-hero-foot {
  position: absolute; left: 50%; bottom: clamp(2.2rem, 8vh, 5rem); transform: translateX(-50%);
  z-index: 4;
  font-family: var(--font-body); font-weight: 500; font-size: 0.9rem;
  color: rgba(255,255,255,0.88); white-space: nowrap;
  transition: color var(--dur-base) var(--ease-out);
}
.e-hero-foot:hover { color: #fff; }

/* ── ORBIT: the arc, its atmosphere, and drifting dust ─
   No overflow:hidden anywhere — every glow fades to transparent (=> pure
   black) on its own, so there is no hard clip edge between sections.
   Three layers: coloured atmosphere (blue port side, green starboard),
   the white light column above the apex, then the arc itself. */
.e-orbit { position: absolute; z-index: 1; left: 0; right: 0; bottom: 0; height: 72vh; pointer-events: none; }

/* Every glow is an ellipse that fades out well inside its own box, so no
   layer ever ends on a hard edge. What cuts them off is the planet itself:
   the arc SVG carries a black fill below the curve and paints last, so the
   horizon occludes the atmosphere exactly along its own line. */
.e-orbit::before {
  content: ''; position: absolute; z-index: 1; left: 50%; bottom: 0; transform: translateX(-50%);
  width: min(1700px, 108vw); height: 64vh;
  background:
    /* Port side: violet, not blue. Same value and saturation as the blue it
       replaces — only the hue is turned, ~231° to ~265° — so the balance
       against the green to starboard is unchanged. */
    radial-gradient(34% 34% at 25% 72%, rgba(140,86,222,0.414), transparent 72%),
    radial-gradient(36% 34% at 75% 72%, rgba(70,176,150,0.306), transparent 72%),
    /* the faint wash over the whole limb, turned with it — leaving this blue
       would have read as a patch that got missed */
    radial-gradient(50% 30% at 50% 76%, rgba(168,136,240,0.135), transparent 76%);
  filter: blur(28px);
}
/* the white core — its centre sits just below the apex, like a sun behind
   the limb, so only its upper half ever clears the horizon */
.e-orbit::after {
  content: ''; position: absolute; z-index: 2; left: 50%; bottom: 0; transform: translateX(-50%);
  width: min(1500px, 92vw); height: 64vh;
  background: radial-gradient(30% 26% at 50% 68%,
    rgba(238,246,255,0.726) 0%,
    rgba(208,226,255,0.352) 26%,
    rgba(160,190,250,0.132) 52%,
    transparent 78%);
}
.e-arc { position: absolute; z-index: 3; left: -2%; right: -2%; bottom: 0; width: 104%; height: 24vh; }
/* dust ONLY in the light column above the apex — there is no starfield on
   this page, so specks outside the white core would read as stray dots */
#dust-canvas { position: absolute; z-index: 4; left: 50%; bottom: 22vh; transform: translateX(-50%); width: 34vw; height: 26vh; }

/* ── SECTIONS (dark, breathing) ───────────── */
.e-section { position: relative; z-index: 2; max-width: var(--max-w); margin: 0 auto; padding: clamp(5.5rem, 13vh, 9.5rem) var(--gutter); }
.e-head { max-width: 42rem; margin: 0 auto; text-align: center; }
.e-head .label { display: block; margin-bottom: var(--sp-3); }
.e-h2 {
  font-family: var(--font-display); font-weight: 600; color: var(--text);
  font-size: clamp(1.2rem, 2.1vw, 1.6rem); letter-spacing: var(--track-tight); line-height: 1.25;
}
.e-h2 em { font-style: normal; color: var(--accent-blue); }
.e-lead { margin: var(--sp-4) auto 0; font-family: var(--font-body); font-weight: 300; font-size: var(--size-base); line-height: 1.7; color: var(--text-muted); max-width: 34rem; }

.e-grid { display: grid; gap: var(--sp-4); margin-top: var(--sp-8); }
.e-grid--3 { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 860px) {
  .e-grid--3 { grid-template-columns: 1fr; }

  /* Rather than a tall stack, show one tier at a time — reveal.js adds
     .is-rotating and cycles every 3s. All three share one grid cell, so
     the row is as tall as the tallest and nothing jumps on the swap. */
  .e-grid.is-rotating { grid-template-rows: 1fr; }
  .e-grid.is-rotating > .e-card {
    grid-area: 1 / 1;
    opacity: 0;
    visibility: hidden;
    transform: none;
  }
  .e-grid.is-rotating > .e-card.is-showing { opacity: 1; visibility: visible; }
  /* must outrank the reveal-group transition list restated further down,
     which is 4 classes deep and would otherwise win */
  .js .e-grid.is-rotating.reveal-group > .e-card {
    transition: opacity 700ms var(--ease-out);
    transition-delay: 0s;
  }
}

.e-card {
  position: relative;
  background: linear-gradient(180deg, rgba(18,24,38,0.5), rgba(8,11,18,0.5));
  border: 1px solid var(--border); border-radius: 18px;
  padding: var(--sp-6) var(--sp-4) var(--sp-4);
}
/* The white sheet is a pseudo-element that fades in, NOT a background
   swap: the card's rest state is a gradient, and a gradient cannot
   interpolate to a flat colour — it would jump no matter the duration. */
.e-card::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: #fff; opacity: 0; z-index: 0;
  transition: opacity 900ms cubic-bezier(0.4, 0, 0.2, 1);
}
.e-card:hover::after { opacity: 1; }

.e-card-num { position: relative; z-index: 1; font-family: var(--font-body); font-size: var(--size-xs); font-weight: 500; letter-spacing: var(--track-wide); color: var(--accent-blue); }
.e-card-title { position: relative; z-index: 1; margin-top: var(--sp-3); font-family: var(--font-display); font-weight: 600; font-size: clamp(1.05rem, 1.25vw, 1.25rem); line-height: 1.28; color: var(--text); }
.e-card-desc { position: relative; z-index: 1; margin-top: var(--sp-3); font-family: var(--font-body); font-weight: 300; color: var(--text-muted); font-size: var(--size-sm); line-height: 1.7; }
.e-card-num, .e-card-title, .e-card-desc { transition: color 900ms cubic-bezier(0.4, 0, 0.2, 1); }

.e-card--featured { border-color: rgba(255,255,255,0.22); box-shadow: 0 0 0 1px rgba(255,255,255,0.06), 0 24px 80px rgba(140,170,230,0.08); background: linear-gradient(180deg, rgba(24,30,44,0.55), rgba(8,11,18,0.55)); }
.e-tag {
  position: absolute; top: -0.7rem; left: var(--sp-4);
  font-size: 0.6rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: #06080d; background: #fff; padding: 4px 10px; border-radius: 999px; z-index: 2;
  transition: color 900ms cubic-bezier(0.4, 0, 0.2, 1), background-color 900ms cubic-bezier(0.4, 0, 0.2, 1), border-color 900ms cubic-bezier(0.4, 0, 0.2, 1);
}
.e-tag--soon { color: rgba(255,255,255,0.55); background: transparent; border: 1px solid var(--border); }

/* Hover flips the whole card: white sheet, black type. The tag has to
   invert too or it vanishes into the sheet it is sitting on. */
.e-card:hover {
  border-color: #fff;
  transform: translateY(-3px);
  box-shadow: 0 0 0 1px rgba(255,255,255,0.5), 0 24px 70px rgba(180,205,255,0.16);
}
.e-card:hover .e-card-num  { color: rgba(6,8,13,0.55); }
.e-card:hover .e-card-title { color: #06080d; }
.e-card:hover .e-card-desc { color: rgba(6,8,13,0.72); }
.e-card:hover .e-tag { color: #fff; background-color: #06080d; }
/* Both tags hang half over the card's top edge, so on hover they have to
   read against black AND against the white sheet — a dark pill does both;
   dark text on transparent would vanish in the overhanging half. */
.e-card:hover .e-tag--soon { color: rgba(255,255,255,0.72); background-color: #06080d; border-color: #06080d; }

/* animations.css's `.js .reveal-group > *` is two classes deep, so it
   outranks `.e-card` and REPLACES its transition list — border and shadow
   were getting no transition at all, which is what made the flip snap.
   Restate the list here at higher specificity. The hover properties get a
   zero delay so a card never has to wait out the reveal stagger. */
.js .e-grid.reveal-group > .e-card {
  transition-property: opacity, filter, transform, border-color, box-shadow;
  transition-duration: 1100ms, 900ms, 900ms, 900ms, 900ms;
  transition-timing-function:
    cubic-bezier(0.22, 1, 0.36, 1), cubic-bezier(0.22, 1, 0.36, 1),
    cubic-bezier(0.4, 0, 0.2, 1), cubic-bezier(0.4, 0, 0.2, 1), cubic-bezier(0.4, 0, 0.2, 1);
}
.js .e-grid.reveal-group.visible > .e-card:nth-child(2) { transition-delay: 140ms, 140ms, 0ms, 0ms, 0ms; }
.js .e-grid.reveal-group.visible > .e-card:nth-child(3) { transition-delay: 280ms, 280ms, 0ms, 0ms, 0ms; }

/* ── HOW IT WORKS: the home page's Process slider, centred ── */
.e-process { max-width: 40rem; margin: var(--sp-8) auto 0; text-align: left; }
.e-process .process-slider { min-height: 210px; padding: 0; }
.e-process .process-slide-title { font-size: clamp(1.2rem, 2.2vw, 1.6rem); }
.e-process .process-slide-desc { font-size: var(--size-sm); }
.e-process .process-nav { margin-top: var(--sp-4); }
@media (max-width: 560px) { .e-process .process-slider { min-height: 260px; } }

/* ── OUTPUT ───────────────────────────────── */
.e-out-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-4); margin-top: var(--sp-8); }
@media (max-width: 720px) { .e-out-grid { grid-template-columns: 1fr; } }
.e-out-card {
  position: relative; aspect-ratio: 16/9; border-radius: 16px; overflow: hidden;
  border: 1px solid var(--border); background: linear-gradient(135deg, #0a1018, #05070d);
  display: flex; flex-direction: column; justify-content: flex-end; padding: var(--sp-4);
  transition: border-color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
/* At rest the still IS the card — full frame, nothing over it. The scrim
   and the type only arrive on hover, so the work is what you look at. */
.e-out-still {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}
.e-out-card::before {
  content: ''; position: absolute; inset: 0; z-index: 1; opacity: 0;
  background: linear-gradient(to top, rgba(3,5,9,0.94) 0%, rgba(3,5,9,0.55) 45%, rgba(3,5,9,0.15) 78%);
  transition: opacity var(--dur-base) var(--ease-out);
}
a.e-out-card:hover { border-color: rgba(255,255,255,0.22); transform: translateY(-3px); box-shadow: 0 24px 70px rgba(0,0,0,0.5); }
a.e-out-card:hover::before { opacity: 1; }
a.e-out-card:hover .e-out-still { transform: scale(1.03); }

.e-out-kicker, .e-out-title, .e-out-watch {
  position: relative; z-index: 2;
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
a.e-out-card:hover .e-out-kicker,
a.e-out-card:hover .e-out-title,
a.e-out-card:hover .e-out-watch { opacity: 1; transform: none; }
.e-out-title { transition-delay: 40ms; }
.e-out-watch { transition-delay: 80ms; }

.e-out-kicker { font-size: var(--size-xs); font-weight: 500; letter-spacing: var(--track-wide); text-transform: uppercase; }
.e-out-title { margin-top: 0.5rem; font-family: var(--font-display); font-weight: 600; font-size: clamp(1rem, 1.25vw, 1.2rem); color: var(--text); line-height: 1.25; }
.e-out-watch { margin-top: 0.6rem; font-family: var(--font-body); font-size: var(--size-sm); color: var(--text-muted); }

/* No hover on touch — the scrim and the copy are simply always there */
@media (hover: none) {
  .e-out-card::before { opacity: 1; }
  .e-out-kicker, .e-out-title, .e-out-watch { opacity: 1; transform: none; }
}

/* ── FINAL CTA ────────────────────────────── */
#e-cta { text-align: center; }
.e-cta-h { font-family: var(--font-display); font-weight: 600; font-size: clamp(1.4rem, 2.6vw, 2rem); letter-spacing: var(--track-tight); color: var(--text); }
.e-cta-h em { font-style: normal; color: var(--accent-blue); }
.e-cta-sub { margin: var(--sp-4) auto 0; font-family: var(--font-body); font-weight: 300; color: var(--text-muted); max-width: 32rem; }
#e-cta .e-actions { justify-content: center; }

footer.e-footer { position: relative; z-index: 2; border-top: 1px solid var(--border); padding: var(--sp-6) var(--gutter); }
.e-footer-inner { max-width: var(--max-w); margin: 0 auto; display: flex; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; color: var(--text-faint); font-size: var(--size-sm); }
.e-footer-inner a { color: var(--text-muted); }
.e-footer-inner a:hover { color: var(--text); }

/* ── PALETTE, last word ────────────────────────
   No gold on this page at all, and blue is spent exactly once: the two
   words that close it ("your engine."). Everything that used to be an
   accent is white at a lower opacity instead. Keep this block last — it
   has to outrank the definitions above it. */
.label { color: rgba(255,255,255,0.42); }
.e-h2 em { color: #fff; }
.e-card-num { color: rgba(255,255,255,0.48); }
.e-out-kicker { color: rgba(255,255,255,0.55); }
.process-slide-num { color: rgba(255,255,255,0.48); }
.process-slide-time {
  color: rgba(255,255,255,0.55);
  background: rgba(255,255,255,0.04);
  border-color: rgba(255,255,255,0.14);
}
.process-dot { background: rgba(255,255,255,0.22); }
.process-dot.active { background: #fff; }
.process-nav-btn:hover { background: rgba(255,255,255,0.10); border-color: rgba(255,255,255,0.28); }
/* the one blue thing left */
.e-cta-h em { color: var(--accent-blue); }


:root { --signal: #3BE081; }   /* the rail signal green */

/* ════════════════════════════════════════════════════════════
   HOME — additions on top of the engine page's components
   ------------------------------------------------------------
   No concrete: Jim dropped it, because procedural CSS noise never
   reads as a real poured surface. Brutalism here comes from
   STRUCTURE and SCALE instead — full-bleed rules, bare index
   numbers at display size, heavy type, no ornament.
   ════════════════════════════════════════════════════════════ */

/* ── Projects: stacked rows, never cards ─────────────────── */
.proj-list { margin-top: clamp(2.5rem, 6vw, 4rem); border-top: 2px solid rgba(255, 255, 255, 0.5); }

.proj {
  position: relative;
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr) 12rem 8rem;
  gap: clamp(1rem, 2.5vw, 2rem);
  align-items: baseline;
  padding: clamp(1.75rem, 3.2vw, 2.75rem) 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.13);
  transition: padding-left var(--dur-base) var(--ease-out);
}

.proj-n {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.6rem, 3vw, 2.4rem);
  line-height: 1;
  letter-spacing: var(--track-tight);
  color: rgba(255, 255, 255, 0.30);
  transition: color var(--dur-base) var(--ease-out);
}

.proj-title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.5rem, 3vw, 2.4rem);
  line-height: 1.1;
  letter-spacing: var(--track-tight);
  color: #fff;
}

.proj-desc { margin-top: 0.6rem; font-size: var(--size-base); color: rgba(255, 255, 255, 0.62); max-width: 46ch; }

/* The one or two phrases that carry each row, lifted out of the 62% grey the
   rest of the line sits in. Same `.k` the engine block already uses for the
   same job, so a scanned line reads the same way on both. */
.proj-desc .k { color: #fff; font-weight: 500; }

.proj-meta,
.proj-go {
  font-size: var(--size-xs);
  letter-spacing: var(--track-wider);
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45);
  transition: color var(--dur-base) var(--ease-out);
}

.proj-go { justify-self: end; text-align: right; }

.proj::after {
  content: '';
  position: absolute;
  left: 0; bottom: -1px;
  width: 100%; height: 1px;
  background: #fff;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-base) var(--ease-out);
}

a.proj:hover { padding-left: var(--sp-3); }
a.proj:hover::after { transform: scaleX(1); }
a.proj:hover .proj-n { color: #fff; }
a.proj:hover .proj-meta,
a.proj:hover .proj-go { color: rgba(255, 255, 255, 0.8); }

@media (max-width: 820px) {
  .proj { grid-template-columns: 3rem minmax(0, 1fr); row-gap: 0.5rem; }
  .proj-meta, .proj-go { grid-column: 2; justify-self: start; text-align: left; }
}

/* ── Case study, framed like an engine panel ─────────────── */
.case-panel {
  margin-top: clamp(3.5rem, 8vw, 6rem);
  padding-top: clamp(2rem, 4vw, 3rem);
  display: grid;
  grid-template-columns: minmax(0, 0.72fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  border-top: 2px solid rgba(255, 255, 255, 0.5);
}

.case-plate {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-3);
  padding: clamp(1.75rem, 3.5vw, 2.75rem) 0 clamp(1.75rem, 3.5vw, 2.75rem) clamp(1.25rem, 2.5vw, 2rem);
  border-left: 2px solid rgba(255, 255, 255, 0.5);
}

.case-plate-name {
  font-family: var(--font-display); font-weight: 800; color: #fff;
  font-size: clamp(1.35rem, 2.3vw, 1.8rem); line-height: 1.15; letter-spacing: var(--track-tight);
}

.case-meta {
  font-size: var(--size-xs); letter-spacing: var(--track-wider);
  text-transform: uppercase; color: rgba(255, 255, 255, 0.5);
}

.case-body h3 {
  margin-top: var(--sp-2);
  font-size: clamp(1.5rem, 2.8vw, 2.1rem); line-height: 1.14; letter-spacing: var(--track-tight);
}

.case-body p { margin-top: var(--sp-3); font-size: var(--size-lg); color: rgba(255, 255, 255, 0.8); }




@media (max-width: 860px) { .case-panel { grid-template-columns: minmax(0, 1fr); } }

/* ── Engine: its own full-width block ────────────────────── */
.engine-block {
  border-top: 2px solid rgba(255, 255, 255, 0.5);
  border-bottom: 2px solid rgba(255, 255, 255, 0.5);
}

.engine-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
}

/* The one blue thing on the page. Jim's call, and he is right about why:
   on an otherwise black page a single lit accent is what separates modern
   from merely dark. It is the same --accent-blue the engine page already
   spends on exactly one phrase, so the two pages stay one system. */
.engine-giant {
  display: block;
  font-family: var(--font-display); font-weight: 800;
  font-size: clamp(2.6rem, 7vw, 5.5rem);
  line-height: 0.98; letter-spacing: -0.035em;
  color: var(--accent-blue);
  text-shadow:
    0 0 34px rgba(90, 171, 240, 0.45),
    0 0 90px rgba(90, 171, 240, 0.22);
}

.engine-rot { position: relative; min-height: 7.5rem; margin-top: var(--sp-4); }

/* Both paragraphs share one absolute box, so a symmetric crossfade holds two
   legible texts on top of each other at 0.5 for half the fade. Asymmetric
   instead: the outgoing one is gone before the incoming one starts. Same fix
   the hero caption already carries. */
.engine-bio {
  position: absolute; inset: 0; margin: 0;
  font-size: var(--size-lg); line-height: var(--lead-normal);
  color: rgba(255, 255, 255, 0.72);
  opacity: 0; transition: opacity 260ms var(--ease-out);
}

.engine-bio.active { opacity: 1; transition: opacity 320ms var(--ease-out) 240ms; }
.engine-bio .k { color: #fff; font-weight: 500; }

.engine-dots { display: flex; gap: 0.45rem; margin-top: var(--sp-3); }
.engine-dots i {
  width: 7px; height: 7px; border-radius: 999px; background: rgba(255, 255, 255, 0.22);
  transition: width var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
}
.engine-dots i.active { width: 22px; background: #fff; }





@media (max-width: 860px) { .engine-grid { grid-template-columns: minmax(0, 1fr); } }

/* ── Services: a PINNED section, one at a time ───────────────
   The scroll effect Jim picked. The track is tall; the panel
   inside it sticks to the viewport and the services step through
   it as you scroll past. */
.svc-track { position: relative; height: 340vh; }

.svc-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(1.5rem, 4vh, 3rem);
}

/* Copy left, counter right, on the same rhythm as the rail below — this was
   the one section that stopped at the halfway mark while every other block
   on the page reaches both edges, which read as unfinished rather than quiet. */
.svc-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(1.5rem, 5vw, 4rem);
}

.svc-stage { position: relative; min-height: 15rem; }

.svc {
  position: absolute; inset: 0;
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 520ms var(--ease-out), transform 520ms var(--ease-out);
  pointer-events: none;
}

.svc.active { opacity: 1; transform: none; pointer-events: auto; }

/* The numeral is deliberately near the floor of legibility. It is a position
   marker, not a heading — it should register from the corner of the eye and
   never compete with the discipline it belongs to. */
.svc-count {
  display: flex; align-items: baseline; gap: 0.1em;
  font-family: var(--font-display); font-weight: 800;
  line-height: 0.82; letter-spacing: -0.05em;
  white-space: nowrap;
}

.svc-count-n {
  font-size: clamp(4rem, 13vw, 11rem);
  color: rgba(255, 255, 255, 0.11);
  font-variant-numeric: tabular-nums;
  transition: color 420ms var(--ease-out);
}

.svc-count-t {
  font-size: clamp(1rem, 2.4vw, 1.9rem);
  color: rgba(255, 255, 255, 0.22);
}

/* Last step reached: the counter warms to the same signal the rail is about
   to fire, so the two arrivals read as one event instead of two. */
.svc-body.is-last .svc-count-n { color: rgba(59, 224, 129, 0.30); }

.svc-title {
  font-family: var(--font-display); font-weight: 800; color: #fff;
  font-size: clamp(1.8rem, 4vw, 3rem); line-height: 1.06; letter-spacing: -0.03em;
}

.svc-desc { margin-top: var(--sp-3); font-size: var(--size-lg); color: rgba(255,255,255,0.7); max-width: 42ch; }

/* One continuous bar, filled smoothly by --svc-p. It stays neutral the
   whole way and only goes green the moment it completes — the green is
   the arrival, not the progress. */
.svc-rail {
  position: relative;
  height: 2px;
  background: rgba(255, 255, 255, 0.12);
}

.svc-rail::after {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(var(--svc-p, 0) * 100%);
  background: rgba(255, 255, 255, 0.55);
  transition: background 700ms var(--ease-out), box-shadow 700ms var(--ease-out);
}

.svc-rail.is-full::after {
  background: var(--signal);
  box-shadow:
    0 0 10px rgba(59, 224, 129, 0.9),
    0 0 30px rgba(59, 224, 129, 0.5);
}

@media (max-width: 820px) {
  .svc-track { height: 300vh; }
  /* No room for a side-by-side on a phone: the counter drops above the copy
     and shrinks to a marker, rather than squeezing the discipline title. */
  .svc-body { grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); }
  .svc-count { order: -1; align-self: start; }
  .svc-count-n { font-size: clamp(2.4rem, 12vw, 3.6rem); color: rgba(255,255,255,0.20); }
}

/* The engine head sat centred (the .e-head default) above a left-aligned
   giant — two different axes in one block. Align them. */
.engine-block .e-head { text-align: left; margin-inline: 0; }
.engine-block .e-lead { margin-inline: 0; }

/* ── The reason position:sticky did nothing ──────────────────
   OG base.css sets `body { overflow-x: hidden }`. That makes the
   computed overflow-y `auto`, which turns body into a scroll
   container — and a sticky element then sticks to BODY's scrollport,
   which never scrolls, because the scrolling happens on <html>. The
   pin just rode along with the page.

   `clip` does the same horizontal clipping WITHOUT creating a scroll
   container, so sticky binds to the viewport again. base.css is
   frozen, so the override lives here. */
body { overflow-x: clip; }

/* Same axis fix as the engine block: the pinned head was centred over
   left-aligned content. */
.svc-pin .e-head { text-align: left; margin-inline: 0; }
.svc-pin .e-lead { margin-inline: 0; }

/* ════════════════════════════════════════════════════════════
   ONE SCROLL SIGNAL
   ------------------------------------------------------------
   --set runs 0 → 1 as the hero leaves. One value, set once per
   frame by js/home.js, drives everything that used to be a
   separate effect: the orbit dissolves, the light that was in
   orbit settles into the page behind it.
   ════════════════════════════════════════════════════════════ */

.e-orbit {
  opacity: calc(1 - var(--set, 0));
  transition: opacity 120ms linear;
}

/* The light the orbit gave up, banked at the foot of the page. */
body::before {
  content: '';
  position: fixed;
  left: 50%;
  bottom: -34vh;
  width: min(150vw, 1700px);
  height: 68vh;
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 0;
  opacity: var(--set, 0);
  background: radial-gradient(ellipse at 50% 100%,
    rgba(206, 226, 255, 0.11) 0%,
    rgba(180, 205, 255, 0.05) 34%,
    transparent 68%);
}


/* ── Word rise ───────────────────────────────────────────────
   Headlines arrive word by word from behind their own baseline.
   The wrappers are built in JS from text nodes only, so <em> and
   <br> inside a headline survive the split. */
/* The mask that makes each word rise out of nothing. Every display line on
   this page is set at a line-height below 1, so the line box is SHORTER than
   the font's own ascent + descent and the glyphs spill past it — which meant
   `overflow: hidden` sheared the tail off every g, y, p and j. The mask is
   given room below the baseline, and that room is taken straight back out of
   the layout so line spacing is untouched. */
.rw {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.22em;
  margin-bottom: -0.22em;
}

.rw > i {
  display: inline-block;
  font-style: inherit;
  /* Has to clear the padding too, or the top of the word peeks under the
     mask before it starts moving: 100% + 0.22em/line-height, rounded up. */
  transform: translateY(132%);
  transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: var(--rw-d, 0ms);
}

.is-risen .rw > i { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .rw > i { transform: none !important; transition: none !important; }
}


/* ── Case teaser ─────────────────────────────────────────────
   The case moved to its own page. What is left here is an
   entrance, not a summary. */
.teaser {
  position: relative;
  display: block;
  margin-top: clamp(3rem, 7vw, 5rem);
  padding: clamp(2rem, 4vw, 3rem) 0;
  border-top: 2px solid rgba(255, 255, 255, 0.5);
  border-bottom: 1px solid rgba(255, 255, 255, 0.13);
  transition: padding-left var(--dur-base) var(--ease-out);
}

.teaser-label {
  display: block;
  font-size: var(--size-xs); letter-spacing: var(--track-wider);
  text-transform: uppercase; color: rgba(255, 255, 255, 0.45);
}

.teaser-line {
  margin-top: var(--sp-3);
  font-family: var(--font-display); font-weight: 800; color: #fff;
  font-size: clamp(1.5rem, 3.4vw, 2.8rem); line-height: 1.1; letter-spacing: -0.03em;
  max-width: 24ch;
}

.teaser-go {
  display: inline-block;
  margin-top: var(--sp-3);
  font-size: var(--size-xs); letter-spacing: var(--track-wider);
  text-transform: uppercase; color: rgba(255, 255, 255, 0.5);
  transition: color var(--dur-base) var(--ease-out);
}

.teaser::after {
  content: '';
  position: absolute; left: 0; bottom: -1px;
  width: 100%; height: 1px; background: #fff;
  transform: scaleX(0); transform-origin: left center;
  transition: transform var(--dur-base) var(--ease-out);
}

.teaser:hover { padding-left: var(--sp-3); }
.teaser:hover::after { transform: scaleX(1); }
.teaser:hover .teaser-go { color: #fff; }


/* ── One frame → a month ─────────────────────────────────────
   The signature. A single frame sits alone and huge; scrolling
   past resolves the other twenty-nine around it and shrinks it
   into its slot. What the engine does, performed. */
.mult { position: relative; }

/* A transform does not take layout space, so a scaled-up stage would
   let the read-out land in the middle of the grid. The window reserves
   the room and clips the oversized stage; the read-out sits under it. */
.mult-window {
  position: relative;
  aspect-ratio: 6 / 4.2;
  overflow: hidden;
  display: grid;
  align-content: center;
}

.mult-stage {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: clamp(4px, 0.7vw, 9px);
  transform: scale(var(--mult-s, 5.2));
  transform-origin: 50% 50%;
  will-change: transform;
}

.mult-stage i {
  aspect-ratio: 16 / 10;
  border: 1px solid rgba(255, 255, 255, 0.34);
  background: rgba(255, 255, 255, 0.02);
  opacity: var(--o, 0);
  transition: opacity 340ms var(--ease-out);
}

/* The brief: the one frame that is there from the start. */
.mult-stage i.seed {
  background: rgba(255, 255, 255, 0.92);
  border-color: #fff;
  opacity: 1;
}

.mult-read {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  font-size: var(--size-xs); letter-spacing: var(--track-wider);
  text-transform: uppercase; color: rgba(255, 255, 255, 0.5);
}

.mult-n {
  font-family: var(--font-display); font-weight: 800; color: #fff;
  font-size: clamp(1.6rem, 3vw, 2.4rem); letter-spacing: var(--track-tight);
  font-variant-numeric: tabular-nums;
}



/* ── Case teaser: text, but at scale ─────────────────────────
   No image. The line is the whole thing, so it is set at display
   size and given the room to land. */
.teaser { padding-block: clamp(3rem, 6vw, 5rem); }

.teaser-line {
  margin-top: var(--sp-4);
  font-size: clamp(2rem, 5.6vw, 4.4rem);
  line-height: 1.02;
  letter-spacing: -0.038em;
  max-width: 18ch;
}

.teaser-go { margin-top: var(--sp-4); font-size: var(--size-sm); }


/* Real frames, once they exist in images/frames/. */
.mult-stage i.shot {
  background: #0b0b0b center / cover no-repeat;
  border-color: rgba(255, 255, 255, 0.22);
}

.mult-stage i.seed.shot { background-color: #0b0b0b; border-color: rgba(255, 255, 255, 0.6); }

/* The seed's white "brief" state fades out as the month assembles. */
.mult-stage i.seed.shot::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, 0.92);
  opacity: var(--seed-fade, 1);
}

.mult-stage i.seed.shot { position: relative; }

/* ════════════════════════════════════════════════════════════
   MOBILE GUTTER, PUT BACK
   ------------------------------------------------------------
   Frozen sections.css carries, at max-width 639:

     #about, #services, #projects, #process, #contact
       { padding: var(--sp-8) 0; }

   which was correct in v1, where every one of those sections wrapped
   its content in a .container that supplied the side padding. The v2
   home page puts content straight into .e-section, so this rule strips
   the gutter and five of the seven sections run edge to edge on a
   phone — headlines and body copy touching both screen edges. Measured
   at 390px: hero and engine 24px, the other five 0px.

   sections.css is frozen, so the gutter goes back here. Same ID
   specificity, later stylesheet, so it wins on document order. The
   block padding is left alone — OG's tightened mobile rhythm is right,
   and #services keeps its inline padding-block:0 for the pin. */
@media (max-width: 639px) {
  #about, #services, #projects, #process, #contact { padding-inline: var(--gutter); }
}

/* ── The smallest type, on the hardest screen ────────────────
   `--size-xs` is a fixed rem in the frozen tokens, so every eyebrow label
   — "Selected work", "The studio", "The product", "What we do", "How we
   work" — measured 9.9px on a phone. Uppercase, widely tracked and dimmed,
   at 9.9px, is the one place on this site where reading is actually work.
   Lifted for small screens only; the desktop sizing is untouched. */
@media (max-width: 700px) {
  .label,
  .svc-count-t,
  .mult-read .mult-l,
  .teaser-label,
  .proj-meta,
  .cs-ledger dt,
  .cs-plate figcaption,
  .cs-ba figcaption,
  .cs-figure-l { font-size: 0.72rem; }
}


/* ════════════════════════════════════════════════════════════
   INTRO — the v1 intro, in v2's colours
   ------------------------------------------------------------
   The whole mechanism (the glide, .intro-reveal, .intro-hold,
   .intro-handoff, the paused hero copy) already lives in the frozen
   css/animations.css, which this page loads. Nothing is re-implemented
   here. What changes is only what v2 changed about the brand: the
   shell is the white silhouette, the halo is white, and there is no
   blue anywhere. Everything below is an override of an OG rule.
   ════════════════════════════════════════════════════════════ */

/* OG sizes the box to v1's hero logo. The glide must end on THIS page's
   crest, so the box is laid out at the crest's exact width — that is what
   makes the landing scale(1) and the handoff invisible. */
.intro-logo-box { width: clamp(62px, 6.6vw, 98px); }

/* The halo of the alone-moment. OG's is blue and pulses; this one is the
   crest's own white, held still — the shell is meant to sit there, not
   breathe. It dissolves as the shell leaves for the hero. */
.intro-glow {
  width: 300px; height: 300px;
  background: radial-gradient(circle at center,
    rgba(255, 255, 255, 0.34) 0%,
    rgba(228, 240, 255, 0.13) 38%,
    transparent 70%);
  filter: blur(30px);
  animation: none;
  opacity: 0.9;
}

/* The travelling halo: this is the copy that fades IN during the glide and
   back OUT at the handoff, so it has to read as the crest's own halo by the
   time it lands. Drop-shadows rather than a pseudo-element, because that is
   what the frozen rules animate. */
.intro-logo-lit {
  filter:
    drop-shadow(0 0 9px  rgba(255, 255, 255, 0.72))
    drop-shadow(0 0 26px rgba(226, 239, 255, 0.40))
    drop-shadow(0 0 62px rgba(186, 212, 255, 0.20));
}

/* OG hides `.hero-logo` during the intro; this page's crest is `.e-crest`.
   It comes back up over 500ms at the handoff, exactly as the overlay's lit
   copy goes down — the two halos are translucent and additive, so crossing
   them linearly keeps the total at 1 and there is no glow pulse on landing.
   The overlay's opaque shell sits on top throughout, so the shell itself
   never dims; removing the node then swaps two identical rasters. */
html.intro-active .e-crest { opacity: 0; }
.e-crest { transition: opacity 500ms linear; }

/* The hero copy waits behind the curtain instead of playing to nobody. */
html.intro-active .e-crest,
html.intro-active .e-hero-title,
html.intro-active .e-hero-sub,
html.intro-active .e-hero .e-actions,
html.intro-active .e-hero-foot {
  opacity: 0;
  /* Second half of the dead man's switch. The curtain has its own CSS
     fallback, but lifting it on a page whose hero is still held at zero
     would only reveal an empty screen — so everything the intro hides
     brings itself back at the same moment. On the normal path `intro-active`
     is gone by ~3.2s, which cancels this animation long before it fires. */
  animation: introFailShow 700ms ease 5200ms forwards;
}

@keyframes introFailShow { to { opacity: 1; } }

/* Comes up DURING the glide, not after it. The 220ms wait plus a 900ms fade
   meant the hero copy only reached full opacity once the shell had already
   landed — and since that copy is the page's largest paint, Chrome was
   timing the whole load from it. Starting immediately and finishing faster
   pulls the largest paint forward by about a second without changing what
   the intro looks like: the curtain is still dissolving while it happens. */
html.intro-done .e-hero-title,
html.intro-done .e-hero-sub,
html.intro-done .e-hero .e-actions,
html.intro-done .e-hero-foot {
  opacity: 1;
  transition: opacity 620ms var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  #intro, #exit { display: none !important; }
  .e-crest, html.intro-active .e-crest { opacity: 1; transition: none; }
}
