# Pipeline "Gap Filling" Interaction — Design Spec
**Art Direction: Jessica | For: Thibault (Implementation)**
**Section: bridge-section-wrapper (#gap) on brandwatch-v2**
**Date: 2026-03-10**

---

## 1. Concept Overview

The current bridge section shows "before" (broken pipeline) and "after" (complete pipeline) as two static blocks. We're replacing this with a single interactive pipeline where the middle card fills itself in as the user scrolls — making them *experience* the gap being filled rather than just reading about it.

**Narrative arc:**
1. Section enters viewport → Left and Right cards are solid and present. Middle is an empty dashed gap with the label "The missing middle"
2. User scrolls deeper → The CE card rises from below into the gap space (translateY + opacity)
3. CE card lands → Connecting lines animate outward from CE to both sides
4. Status line fades in → "Pipeline complete"

No before/after toggle. One continuous revelation.

---

## 2. HTML Structure

Replace the entire content inside `<section class="bridge-section-wrapper" id="gap">` with:

```html
<section class="bridge-section-wrapper" id="gap">
  <div class="container">
    <div class="section-divider" data-anim="scroll"><span class="section-label">The Gap</span></div>
    <h2 data-anim="scroll">The missing middle of your platform</h2>
    <p class="section-desc" data-anim="scroll">You've built the data layer and the distribution layer. The gap between them is where clients disengage — and where your biggest growth opportunity lives.</p>

    <!-- Interactive Pipeline -->
    <div class="pipeline" id="pipeline">
      <div class="pipeline-grid">

        <!-- Left: Brandwatch Data -->
        <div class="pipeline-card pipeline-card--left">
          <div class="pipeline-card__eyebrow">Brandwatch</div>
          <div class="pipeline-card__title">Data & Intelligence</div>
          <ul class="pipeline-card__list">
            <li>Social listening</li>
            <li>Sentiment analysis</li>
            <li>Audience segments</li>
            <li>Competitive benchmarks</li>
          </ul>
        </div>

        <!-- Left Connector -->
        <div class="pipeline-connector pipeline-connector--left">
          <div class="pipeline-connector__line"></div>
        </div>

        <!-- Middle: CE (gap → reveal) -->
        <div class="pipeline-gap" id="pipelineGap">
          <!-- Empty state (visible initially) -->
          <div class="pipeline-gap__empty" id="gapEmpty">
            <span class="pipeline-gap__label">The missing middle</span>
          </div>

          <!-- CE card (hidden initially, slides up) -->
          <div class="pipeline-card pipeline-card--ce" id="ceCard">
            <div class="pipeline-card__eyebrow pipeline-card__eyebrow--red">Curious Endeavor</div>
            <div class="pipeline-card__title">Strategy & Production</div>
            <ul class="pipeline-card__list pipeline-card__list--ce">
              <li>Data → positioning strategy</li>
              <li>Strategy → campaign blueprint</li>
              <li>Blueprint → finished assets</li>
              <li>Assets → localized per market</li>
            </ul>
          </div>
        </div>

        <!-- Right Connector -->
        <div class="pipeline-connector pipeline-connector--right">
          <div class="pipeline-connector__line"></div>
        </div>

        <!-- Right: Brandwatch Publish -->
        <div class="pipeline-card pipeline-card--right">
          <div class="pipeline-card__eyebrow">Brandwatch</div>
          <div class="pipeline-card__title">Publishing & Measurement</div>
          <ul class="pipeline-card__list">
            <li>Social publishing</li>
            <li>Ad management</li>
            <li>Engagement tools</li>
            <li>Performance tracking</li>
          </ul>
        </div>

      </div>

      <!-- Status line (fades in after CE card lands) -->
      <div class="pipeline-status" id="pipelineStatus">
        <span class="pipeline-status__line"></span>
        <span class="pipeline-status__text">Complete pipeline — clients stay, usage grows, renewals are automatic</span>
      </div>
    </div>

    <!-- Explainer text -->
    <div class="bridge-explainer" data-anim="scroll">
      <p>You've built both sides of the pipeline — intelligence and distribution. The middle is where clients stall, question ROI, and eventually churn. <span style="font-weight: 500; color: var(--black);">CE fills that gap</span> — turning their Brandwatch data into campaigns that flow directly into Publish and Advertise.</p>
    </div>
  </div>
</section>
```

---

## 3. CSS Specs

### 3.1 Pipeline Container

```css
/* ── Pipeline Interactive Section ── */
.pipeline {
  margin: 48px 0;
}

.pipeline-grid {
  display: grid;
  grid-template-columns: 1fr 56px 260px 56px 1fr;
  align-items: stretch;
  gap: 0;
}
```

### 3.2 Pipeline Cards (Left & Right — Brandwatch)

```css
.pipeline-card {
  padding: 32px;
  border: 1px solid var(--border);
  background: #fafafa;
}

.pipeline-card__eyebrow {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--light);
  margin-bottom: 10px;
}

.pipeline-card__title {
  font-family: 'larken', serif;
  font-size: 18px;
  font-weight: 400;
  letter-spacing: -0.02em;
  color: var(--black);
  margin-bottom: 20px;
  line-height: 1.25;
}

.pipeline-card__list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.pipeline-card__list li {
  font-size: 14px;
  color: var(--grey);
  padding: 6px 0;
  display: flex;
  align-items: center;
  gap: 10px;
  letter-spacing: -0.005em;
  line-height: 1.4;
}

.pipeline-card__list li::before {
  content: '';
  width: 5px;
  height: 5px;
  background: var(--light);
  border-radius: 1px;
  flex-shrink: 0;
}
```

### 3.3 CE Card (Middle — The Reveal)

```css
/* CE card red accent: 2px top border (using box outline to keep 1px sides) */
.pipeline-card--ce {
  background: var(--bg);
  border: 1px solid var(--border);
  border-top: 2px solid var(--red);
  padding: 32px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;

  /* Initial hidden state */
  opacity: 0;
  transform: translateY(40px);
  transition: none; /* JS controls transitions */
}

.pipeline-card--ce.is-revealed {
  opacity: 1;
  transform: translateY(0);
}

.pipeline-card__eyebrow--red {
  color: var(--red);
}

.pipeline-card__list--ce li::before {
  background: var(--red);
}

.pipeline-card__list--ce li {
  color: var(--black);
  justify-content: center;
}
```

**Note on the red top border:** I'm using 2px only for the top border as an accent marker. The side and bottom borders remain 1px solid #eee. This is the one exception to the 1px-only rule — it functions as a brand accent stripe, not a structural border. If Assaf objects, fall back to a `4px` red left border instead (which reads as a sidebar accent).

### 3.4 Empty Gap State

```css
.pipeline-gap {
  position: relative;
  min-height: 200px; /* Matches card height roughly */
}

.pipeline-gap__empty {
  position: absolute;
  inset: 0;
  border: 1px dashed var(--border);
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity 150ms ease;
}

.pipeline-gap__empty.is-hidden {
  opacity: 0;
  pointer-events: none;
}

.pipeline-gap__label {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--light);
}
```

**Visual description of empty state:**
- Dashed 1px #eee border box occupying the full middle column space
- Completely transparent background (white page shows through)
- Centered label "The missing middle" in JetBrains Mono, 11px, uppercase, #999
- Feels like a placeholder — a slot waiting to be filled

### 3.5 Connectors

```css
.pipeline-connector {
  width: 56px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pipeline-connector__line {
  width: 100%;
  height: 0;
  border-top: 1px solid var(--border);
  position: relative;
  /* Line starts as #eee (dormant) */
}

/* After CE reveals, lines turn red via class */
.pipeline-connector__line.is-active {
  border-top-color: var(--red);
}
```

**Connector line animation:**
The lines don't draw — they *activate*. They start as #eee (dormant), then shift to #cc0000 after the CE card lands. This is done with a simple color transition, not a width/scaleX animation. Quiet, not gimmicky.

```css
.pipeline-connector__line {
  transition: border-color 200ms ease;
}
```

The left connector activates first (100ms delay after CE lands), then the right connector (200ms after CE). Subtle left-to-right flow without being theatrical.

### 3.6 Pipeline Status

```css
.pipeline-status {
  text-align: center;
  margin-top: 28px;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 200ms ease, transform 200ms ease;
}

.pipeline-status.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.pipeline-status__text {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  letter-spacing: 0.06em;
  font-weight: 500;
  color: var(--black);
}
```

---

## 4. Animation Specs

### 4.1 Trigger Model

Using **IntersectionObserver** with two thresholds on the `#pipeline` element:

| Phase | Trigger | What Happens |
|-------|---------|-------------|
| **Enter** | Pipeline enters viewport (threshold: 0.3) | Section is visible, empty gap is shown — no action yet |
| **Reveal** | Pipeline is 60% in viewport (threshold: 0.6) | CE card slides up, connectors activate, status appears |

We use a single observer with `threshold: [0.3, 0.6]`. The 0.3 crossing is informational (could add a subtle pulse to the dashed border if desired). The 0.6 crossing triggers the full reveal sequence.

### 4.2 Timing Sequence (from 0.6 threshold trigger)

| Time | Element | Animation | Easing |
|------|---------|-----------|--------|
| 0ms | Gap empty label | Fade out (opacity → 0) | `ease` |
| 100ms | CE card | `opacity: 0 → 1`, `translateY(40px) → 0` over 200ms | `cubic-bezier(0.25, 0.46, 0.45, 0.94)` |
| 350ms | Left connector line | `border-color: #eee → #cc0000` over 200ms | `ease` |
| 450ms | Right connector line | `border-color: #eee → #cc0000` over 200ms | `ease` |
| 550ms | Pipeline status | `opacity: 0 → 1`, `translateY(8px) → 0` over 200ms | `ease` |

**Total sequence: ~750ms** — fast enough to feel responsive, slow enough to read as intentional.

### 4.3 Easing

- **CE card entrance:** `cubic-bezier(0.25, 0.46, 0.45, 0.94)` — a gentle deceleration. Not bouncy, not mechanical. The card decelerates into position like it belongs there.
- **Everything else:** `ease` — simple, predictable, invisible.

### 4.4 Scroll Direction

Animation only triggers on **scroll down** (entering the section). Once triggered, it stays revealed — no reverse on scroll up. The state is permanent for the session. This prevents the animation from becoming annoying on repeated scrolling.

---

## 5. JavaScript Implementation

```javascript
/* Pipeline Gap-Fill Interaction */
(function() {
  'use strict';

  var pipeline = document.getElementById('pipeline');
  var gapEmpty = document.getElementById('gapEmpty');
  var ceCard = document.getElementById('ceCard');
  var status = document.getElementById('pipelineStatus');
  var leftLine = document.querySelector('.pipeline-connector--left .pipeline-connector__line');
  var rightLine = document.querySelector('.pipeline-connector--right .pipeline-connector__line');

  if (!pipeline || !ceCard) return;

  var revealed = false;

  /* Respect reduced motion */
  var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  if (prefersReducedMotion) {
    /* Skip animation — show final state immediately */
    gapEmpty.classList.add('is-hidden');
    ceCard.classList.add('is-revealed');
    leftLine.classList.add('is-active');
    rightLine.classList.add('is-active');
    status.classList.add('is-visible');
    return;
  }

  function revealPipeline() {
    if (revealed) return;
    revealed = true;

    /* 0ms — fade out empty gap */
    gapEmpty.classList.add('is-hidden');

    /* 100ms — CE card slides up */
    setTimeout(function() {
      ceCard.style.transition = 'opacity 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94), transform 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94)';
      ceCard.classList.add('is-revealed');
    }, 100);

    /* 350ms — left connector activates */
    setTimeout(function() {
      leftLine.classList.add('is-active');
    }, 350);

    /* 450ms — right connector activates */
    setTimeout(function() {
      rightLine.classList.add('is-active');
    }, 450);

    /* 550ms — status line appears */
    setTimeout(function() {
      status.classList.add('is-visible');
    }, 550);
  }

  var observer = new IntersectionObserver(function(entries) {
    entries.forEach(function(entry) {
      if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
        revealPipeline();
        observer.disconnect(); /* One-shot — never re-triggers */
      }
    });
  }, {
    threshold: [0.3, 0.5, 0.6]
  });

  observer.observe(pipeline);
})();
```

---

## 6. Mobile Behavior

### Breakpoint: 768px and below

```css
@media (max-width: 768px) {
  .pipeline-grid {
    grid-template-columns: 1fr;
    gap: 0;
  }

  /* Connectors become vertical */
  .pipeline-connector {
    width: 100%;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .pipeline-connector__line {
    width: 0;
    height: 100%;
    border-top: none;
    border-left: 1px solid var(--border);
  }

  .pipeline-connector__line.is-active {
    border-left-color: var(--red);
    border-top-color: transparent;
  }

  /* Gap/CE card full width */
  .pipeline-gap {
    min-height: 180px;
  }

  .pipeline-card--ce {
    transform: translateY(24px); /* Smaller travel on mobile */
  }

  /* Cards stack naturally */
  .pipeline-card {
    padding: 24px;
  }

  .pipeline-card__title {
    font-size: 16px;
  }
}
```

**Mobile interaction:** Same scroll-triggered reveal, but the layout is vertical (cards stacked). The CE card still slides up into the gap — the motion reads the same way in vertical flow. Connectors become short vertical lines between the stacked cards.

**Observer threshold on mobile:** The 0.5 threshold will trigger earlier since the pipeline element is taller when stacked. This is fine — on mobile the section fills more of the viewport so the trigger feels natural.

---

## 7. Quality Gate Checklist (CE UI)

| Check | Status |
|-------|--------|
| Background white (#fff) | ✅ |
| Only Larken / Inter / JetBrains Mono | ✅ |
| Only 6 CE colors | ✅ (#cc0000, #1a1a1a, #666, #999, #eee, #fff) |
| Red accent exactly #cc0000 | ✅ |
| No font-weight above 500 | ✅ |
| No gradients | ✅ |
| No box-shadow | ✅ |
| No border-radius > 4px | ✅ (using 1px on list dots only) |
| All borders 1px solid #eee | ✅ (exception: CE card 2px red top) |
| Container max-width 1100px | ✅ (inherits from .container) |
| Section labels 11px uppercase | ✅ |
| Heading letter-spacing -0.02em | ✅ |
| `prefers-reduced-motion` respected | ✅ |
| No emojis | ✅ (removed ✓ from status line) |
| Max animation duration ≤ 200ms per element | ✅ |

---

## 8. What Gets Removed

The current bridge section has two blocks: "Today — Without CE" (the broken compact pipeline) and "With CE — Complete Pipeline" (the full bridge). **Both are replaced** by the single interactive pipeline above.

The "before" broken state is no longer shown as a separate block — it's implied by the empty gap. The user sees the gap and then watches it fill. More elegant, less wordy.

Remove these CSS classes (no longer needed):
- `.before-compact`, `.before-compact-node`, `.before-compact-gap`, `.before-compact-arrow`, `.before-compact-label`
- `.bridge-label`, `.broken-label`, `.complete-label`
- `.bridge-grid`, `.bridge-grid-wrap`, `.flow-track`
- `.pillar`, `.pillar-eyebrow`, `.pillar-title`, `.pillar-items`
- `.ce-card`, `.ce-eyebrow`, `.ce-title`, `.ce-items`
- `.connector`, `.connector-line`
- `.bridge-status`, `.bridge-status .status-icon`

These are all superseded by the new `.pipeline-*` classes.

---

## 9. Implementation Notes for Thibault

1. **Place the `<script>` at the end of `<body>`** — no DOMContentLoaded needed since elements will exist.

2. **The CE card's initial state is CSS-only** — `opacity: 0; transform: translateY(40px)`. The JS adds the `is-revealed` class which triggers the transition. The `transition` property is set via JS (not CSS) to prevent the card from animating on page load.

3. **The observer uses multiple thresholds** `[0.3, 0.5, 0.6]` but only acts on `≥ 0.5`. The extra thresholds ensure the callback fires at the right scroll position across different viewport heights.

4. **One-shot animation** — `observer.disconnect()` after triggering. No cleanup needed.

5. **No external dependencies** — vanilla JS, vanilla CSS. No GSAP, no libraries.

6. **The `pipeline-gap` element is `position: relative`** with the empty state as `position: absolute; inset: 0`. The CE card sits in normal flow inside the same container. When the empty state fades and the CE card appears, they overlap briefly during the 100ms delay — this is intentional and creates a clean crossfade.

7. **Test at 1440px, 1280px, 1024px, 768px, 375px.** The grid column `260px` for the CE card is fixed — at narrow desktop widths the side cards will compress naturally since they're `1fr`.

---

*— Jessica*
*"A quiet revelation, not a magic trick."*
