---
import type { RoadmapItemWithAttribution, RoadmapStatus } from '../lib/db';
import { computeRouteLayout, travelledStopFor } from '../lib/roadmap-layout';

interface Props {
  items: RoadmapItemWithAttribution[];
  viewportWidth?: number;   // SSR fallback for the layout math
}

const { items, viewportWidth = 1100 } = Astro.props;

// Align the first milestone with the left edge of the page's content column
// (matches the LatestDispatchBanner below). --content-max is 72rem = 1152px.
const CONTENT_MAX = 1152;
const DEFAULT_PADDING = 60;
const paddingLeft = Math.max(DEFAULT_PADDING, (viewportWidth - CONTENT_MAX) / 2);

// Trailing room past the last milestone = a quarter of the viewport, so the
// final item can scroll until it sits halfway between the right edge and the
// screen centre (~0.75 of the viewport). The drawn line extends the same
// distance so it keeps going as that item arrives. (Client recompute redoes
// this with the real viewport; this is the SSR fallback.)
const trailing = Math.round(viewportWidth * 0.25);
const layout = computeRouteLayout({
  itemCount: items.length,
  viewportWidth,
  paddingLeft,
  paddingRight: trailing,
  tailLength: trailing,
});
const travelledStop = travelledStopFor(items.map(i => i.status));

const STATUS_LABEL: Record<RoadmapStatus, string> = {
  shipping:    'SHIPPING',
  in_beta:     'IN BETA',
  planned:     'PLANNED',
  exploring:   'EXPLORING',
  considering: 'CONSIDERING',
};
const STATUS_LABEL_COLOR: Record<RoadmapStatus, string> = {
  shipping:    '#6d8c7c',
  in_beta:     '#b96b58',
  planned:     '#5a6d83',
  exploring:   '#b4b2a9',
  considering: '#b4b2a9',
};
const STATUS_DOT_COLOR: Record<RoadmapStatus, string> = {
  shipping:    '#6d8c7c',
  in_beta:     '#b96b58',
  planned:     '#5a6d83',
  exploring:   '#b4b2a9',
  considering: '#d4d2c8',
};

// "You are here" — the most recent shipping item. -1 if nothing has shipped yet.
let lastShippingIndex = -1;
items.forEach((it, i) => { if (it.status === 'shipping') lastShippingIndex = i; });

function trailingLine(item: RoadmapItemWithAttribution): string | null {
  if (item.metadata_text && item.metadata_text.trim().length > 0) return item.metadata_text;
  if (item.attributed.length > 0) {
    const names = item.attributed.map(a => a.name.split(' ')[0]);
    if (names.length === 1) return `Shaped by ${names[0]}`;
    if (names.length === 2) return `Shaped by ${names[0]} and ${names[1]}`;
    return `Shaped by ${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
  }
  return null;
}

// Stringified x position of the 'you are here' milestone for the
// initial-scroll logic in the nav script. -1 → 0 (no scroll offset).
const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex] : 0;
---
<section class="route" aria-label="Roadmap route" data-initial-x={initialShippingX}>

  <!-- The route — desktop horizontal. .rr-fullbleed escapes the parent
       .page max-width so the route can span the actual viewport while
       the header above and legend below stay centred in the content
       column. -->
  <div class="rr-wrap rr-fullbleed rr-desktop" data-item-count={items.length}>
    <div class="rr-scroll" id="rr-scroll">
      <div class="rr-scroll-inner">
        <div class="rr-track" id="rr-track" style={`width: ${layout.trackWidth}px; height: 420px;`}>
        <svg class="rr-path" id="rr-path-svg" width={layout.trackWidth} height="420" aria-hidden="true">
          <defs>
            <linearGradient id="rr-path-gradient" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" stop-color="#2a2520" stop-opacity="0.55"/>
              <stop offset={String(travelledStop)} stop-color="#2a2520" stop-opacity="0.55"/>
              <stop offset={String(Math.min(1, travelledStop + 0.06))} stop-color="#2a2520" stop-opacity="0.15"/>
              <stop offset="1" stop-color="#2a2520" stop-opacity="0.15"/>
            </linearGradient>
          </defs>
          {layout.pathD && (
            <path id="rr-path-d" d={layout.pathD} fill="none" stroke="url(#rr-path-gradient)" stroke-width="1.25" stroke-linecap="round"/>
          )}
        </svg>

        {items.map((item, i) => (
          <div
            class="rr-milestone"
            data-y={layout.itemY[i]}
            style={`left: ${layout.itemX[i]}px; top: ${layout.itemY[i]}px;`}
          >
            <div
              class:list={['rr-dot', { 'rr-current': i === lastShippingIndex }]}
              style={`background:${STATUS_DOT_COLOR[item.status]};`}
              aria-hidden="true"
            ></div>

            <div class:list={['rr-attach', `rr-attach-${layout.cardSide[i]}`]}>
              <div class="rr-connector" aria-hidden="true"></div>
              <a class="rr-card" tabindex="0" href={`#item-${item.id}`} id={`item-${item.id}`}>
                <p class="rr-eyebrow" style={`color:${STATUS_LABEL_COLOR[item.status]};`}>
                  {item.target ? `${item.target.toUpperCase()} · ` : ''}{STATUS_LABEL[item.status]}
                </p>
                <p class="rr-card-title">{item.title}</p>
                <div class="rr-more">
                  {item.description && <p class="rr-desc">{item.description}</p>}
                  {trailingLine(item) && <p class="rr-trail">{trailingLine(item)}</p>}
                </div>
              </a>
            </div>
          </div>
        ))}
        </div>
      </div>
    </div>

    <div class="rr-fade-left" id="rr-fade-l" aria-hidden="true"></div>
    <div class="rr-fade-right" id="rr-fade-r" aria-hidden="true"></div>

    <!-- Single forward-only advance affordance anchored to the right
         viewport edge. There's no left arrow on purpose — the path
         reads left-to-right and the user's instinct after looking at
         a milestone is 'what's next?', not 'what came before?'. -->
    <button
      type="button"
      class="rr-advance"
      id="rr-advance"
      aria-label="Further along the route"
    >
      <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
        <path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
      </svg>
    </button>
  </div>

  <!-- Legend lives in /roadmap.astro now so it returns to centred
       content-column width below the full-bleed route. -->

  <!-- Mobile vertical timeline -->
  <ol class="rr-mobile" aria-label="Roadmap timeline">
    {items.map((item, i) => (
      <li class="rrm-row">
        <div class="rrm-track-col" aria-hidden="true">
          <span class="rrm-dot" style={`background:${STATUS_DOT_COLOR[item.status]};`}></span>
          {i < items.length - 1 && <span class="rrm-line"></span>}
        </div>
        <div class="rrm-body">
          <p class="rrm-eyebrow" style={`color:${STATUS_LABEL_COLOR[item.status]};`}>
            {item.target ? `${item.target.toUpperCase()} · ` : ''}{STATUS_LABEL[item.status]}
          </p>
          <p class="rrm-title">{item.title}</p>
          {item.description && <p class="rrm-desc">{item.description}</p>}
          {trailingLine(item) && <p class="rrm-trail">{trailingLine(item)}</p>}
        </div>
      </li>
    ))}
  </ol>
</section>

<script>
  // Vanilla route runtime. Two concerns:
  //  1. Nav: arrow buttons, edge fades, initial-scroll into shipping
  //  2. Viewport-aware layout — SSR uses a 1100px fallback for the math;
  //     on the client we know the real viewport, so we recompute itemX
  //     positions + SVG path d + track width on mount and on (debounced)
  //     resize. itemY values come from data-y on each milestone (path
  //     amplitude doesn't change with viewport, only the horizontal spread).
  const MIN_SPACING = 320;
  const PADDING_X   = 60;
  const CONTENT_MAX = 1152;  // matches --content-max (72rem)
  const MID_Y       = 210;   // vertical centreline = track height (420) / 2

  document.querySelectorAll<HTMLElement>('.route').forEach((section) => {
    const scroll = section.querySelector<HTMLElement>('#rr-scroll');
    const wrap   = section.querySelector<HTMLElement>('.rr-wrap');
    const track  = section.querySelector<HTMLElement>('#rr-track');
    const svg    = section.querySelector<SVGSVGElement>('#rr-path-svg');
    const pathD  = section.querySelector<SVGPathElement>('#rr-path-d');
    const milestones = Array.from(section.querySelectorAll<HTMLElement>('.rr-milestone'));
    const fadeL  = section.querySelector<HTMLElement>('#rr-fade-l');
    const fadeR  = section.querySelector<HTMLElement>('#rr-fade-r');
    const advance = section.querySelector<HTMLButtonElement>('#rr-advance');
    if (!scroll || !track || !svg) return;

    const itemCount = milestones.length;
    const itemY: number[] = milestones.map(m => Number(m.dataset.y ?? 0));
    // Current horizontal positions, kept in sync by recompute() — used by
    // the scroll-proximity focus effect.
    let itemXs: number[] = [];

    /** Recompute trackWidth + itemX[] + pathD using the live viewport. */
    function recompute() {
      const vw = window.innerWidth;
      const targetUsableWidth = vw * 0.80;
      const dataDrivenWidth   = (itemCount - 1) * MIN_SPACING;
      const usableWidth       = Math.max(targetUsableWidth, dataDrivenWidth);
      // Match the SSR offset — first item aligns with the content-column
      // left edge so the route lines up with the dispatch banner below.
      const paddingLeft       = Math.max(PADDING_X, (vw - CONTENT_MAX) / 2);
      // Trailing room = a quarter of the viewport so the final milestone can
      // scroll until it sits halfway to the screen centre (~0.75 of vw).
      const trailing          = Math.round(vw * 0.25);
      const trackWidth        = paddingLeft + usableWidth + trailing;

      const itemX: number[] = [];
      for (let i = 0; i < itemCount; i += 1) {
        itemX.push(
          itemCount === 1
            ? paddingLeft + usableWidth / 2
            : paddingLeft + (i / (itemCount - 1)) * usableWidth,
        );
      }

      // Bezier path: control points at the segment midpoint x with control
      // y values matching the prior and next milestone (keeps the tangent
      // flat at each dot — the "river" feel from the layout helper).
      let d = '';
      if (itemCount > 0) {
        d = `M ${itemX[0]} ${itemY[0]}`;
        for (let i = 1; i < itemCount; i += 1) {
          const cx = (itemX[i - 1] + itemX[i]) / 2;
          d += ` C ${cx} ${itemY[i - 1]}, ${cx} ${itemY[i]}, ${itemX[i]} ${itemY[i]}`;
        }
        // Trailing tail: continue the line past the last milestone, easing
        // back to the centreline so it keeps going as that item scrolls in.
        const lastX = itemX[itemCount - 1];
        const lastY = itemY[itemCount - 1];
        const tailEndX = lastX + trailing;
        const tcx = (lastX + tailEndX) / 2;
        d += ` C ${tcx} ${lastY}, ${tcx} ${MID_Y}, ${tailEndX} ${MID_Y}`;
      }

      // Apply.
      track!.style.width = `${trackWidth}px`;
      svg!.setAttribute('width', String(trackWidth));
      if (pathD && d) pathD.setAttribute('d', d);
      milestones.forEach((m, i) => { m.style.left = `${itemX[i]}px`; });
      itemXs = itemX;
    }

    /* Scroll-proximity focus: emphasise the milestone nearest the centre
       of the viewport and let those toward the edges recede + dim. Driven
       every frame that the track moves (via updateNav), so movement feels
       alive rather than a flat pan. Not parallax — every milestone still
       tracks the scroll 1:1; only scale + opacity shift with position. */
    function updateFocus() {
      if (!scroll || itemXs.length === 0) return;
      const center = scroll.scrollLeft + scroll.clientWidth / 2;
      const half = Math.max(1, scroll.clientWidth / 2);
      milestones.forEach((m, i) => {
        const t = Math.min(1, Math.abs((itemXs[i] ?? 0) - center) / half);
        const scale = (1 - 0.10 * t).toFixed(3);
        const op = (1 - 0.42 * t).toFixed(3);
        m.style.transform = `translate(-50%, -50%) scale(${scale})`;
        m.style.opacity = op;
      });
    }

    /* Edge state — fades + advance disable. */
    function updateNav() {
      const max = scroll!.scrollWidth - scroll!.clientWidth;
      const atStart = scroll!.scrollLeft <= 2;
      const atEnd   = scroll!.scrollLeft >= max - 2;
      if (fadeL) fadeL.style.opacity = atStart ? '0' : '1';
      if (fadeR) fadeR.style.opacity = atEnd ? '0' : '1';
      if (advance) advance.classList.toggle('rr-at-end', atEnd);
      updateFocus();
    }

    /* ── Unified scroll handling: wheel, drag, animated glide. ──
       No CSS scroll-snap and no scroll-behavior: smooth — both fight
       the JS-driven smooth motion. Drag has momentum; wheel translates
       vertical to horizontal; arrow click runs a cubic-ease animation. */

    let isDragging = false;
    let dragStartX = 0;
    let dragStartScrollLeft = 0;
    let dragTotalMovement = 0;
    let lastMoveX = 0;
    let lastMoveTime = 0;
    let velocity = 0;          // px/ms, signed (positive = pointer moving right)
    let momentumRAF: number | null = null;
    let animateRAF: number | null = null;
    let wheelRAF: number | null = null;
    let wheelTarget = 0;       // eased target scrollLeft for wheel/trackpad input

    function cancelAnims() {
      if (momentumRAF !== null) { cancelAnimationFrame(momentumRAF); momentumRAF = null; }
      if (animateRAF !== null)  { cancelAnimationFrame(animateRAF);  animateRAF  = null; }
      if (wheelRAF !== null)    { cancelAnimationFrame(wheelRAF);    wheelRAF    = null; }
    }

    function animateScrollTo(target: number, durationMs: number) {
      cancelAnims();
      const start     = scroll!.scrollLeft;
      const delta     = target - start;
      const startTime = performance.now();
      const easeOut   = (t: number) => 1 - Math.pow(1 - t, 3);
      const step = () => {
        const t = Math.min(1, (performance.now() - startTime) / durationMs);
        scroll!.scrollLeft = start + delta * easeOut(t);
        updateNav();
        if (t < 1) animateRAF = requestAnimationFrame(step);
        else       animateRAF = null;
      };
      animateRAF = requestAnimationFrame(step);
    }

    // Wheel — vertical wheel becomes horizontal scroll on this element.
    // Trackpads sending horizontal deltaX go through unchanged (1:1, no scaling).
    scroll.addEventListener('wheel', (e) => {
      const dx = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
      if (dx === 0) return;
      e.preventDefault();

      // Drop any drag-momentum or arrow glide that's mid-flight, but keep
      // building onto the wheel target so quick successive ticks accumulate
      // distance and the glide stays continuous.
      if (momentumRAF !== null) { cancelAnimationFrame(momentumRAF); momentumRAF = null; }
      if (animateRAF !== null)  { cancelAnimationFrame(animateRAF);  animateRAF  = null; }

      const max = scroll!.scrollWidth - scroll!.clientWidth;
      const base = wheelRAF !== null ? wheelTarget : scroll!.scrollLeft;
      wheelTarget = Math.max(0, Math.min(max, base + dx));

      // Ease scrollLeft toward the target each frame (~0.2 of the remaining
      // distance), so the wheel feels like a smooth glide rather than a jump.
      if (wheelRAF === null) {
        const step = () => {
          const diff = wheelTarget - scroll!.scrollLeft;
          if (Math.abs(diff) < 0.5) {
            scroll!.scrollLeft = wheelTarget;
            wheelRAF = null;
            updateNav();
            return;
          }
          scroll!.scrollLeft += diff * 0.2;
          updateNav();
          wheelRAF = requestAnimationFrame(step);
        };
        wheelRAF = requestAnimationFrame(step);
      }
    }, { passive: false });

    // Drag — pointer events; momentum on release.
    scroll.addEventListener('pointerdown', (e) => {
      if (e.button !== undefined && e.button !== 0) return;
      // Don't start a drag when the click target is the advance button.
      if (advance && advance.contains(e.target as Node)) return;

      isDragging          = true;
      dragStartX          = e.pageX;
      dragStartScrollLeft = scroll!.scrollLeft;
      dragTotalMovement   = 0;
      lastMoveX           = e.pageX;
      lastMoveTime        = performance.now();
      velocity            = 0;

      cancelAnims();
      try { scroll!.setPointerCapture(e.pointerId); } catch { /* not all envs */ }
      scroll!.classList.add('rr-dragging');
    });

    scroll.addEventListener('pointermove', (e) => {
      if (!isDragging) return;
      const dx = e.pageX - dragStartX;
      scroll!.scrollLeft = dragStartScrollLeft - dx;
      dragTotalMovement = Math.max(dragTotalMovement, Math.abs(dx));

      const now = performance.now();
      const dt = now - lastMoveTime;
      if (dt > 0) velocity = (e.pageX - lastMoveX) / dt;
      lastMoveX = e.pageX;
      lastMoveTime = now;

      updateNav();
    });

    function endDrag() {
      if (!isDragging) return;
      isDragging = false;
      scroll!.classList.remove('rr-dragging');

      // Click vs drag: anything under 5px total movement is a click —
      // skip momentum and let the underlying card's <a> handle the click.
      if (dragTotalMovement < 5) return;

      // Otherwise it's a real drag — suppress the synthetic click that
      // follows so a drag-then-release-over-a-card doesn't navigate.
      const suppressClick = (ev: Event) => {
        ev.stopPropagation();
        ev.preventDefault();
        scroll!.removeEventListener('click', suppressClick, true);
      };
      scroll!.addEventListener('click', suppressClick, true);

      // Momentum: signed velocity, decay 0.93 per frame, stop under 0.4 px/frame.
      // Direction inverted because dragging right moves scrollLeft left.
      let v = -velocity * 16;
      const step = () => {
        if (Math.abs(v) < 0.4) { momentumRAF = null; return; }
        scroll!.scrollLeft += v;
        v *= 0.93;
        updateNav();
        momentumRAF = requestAnimationFrame(step);
      };
      momentumRAF = requestAnimationFrame(step);
    }

    scroll.addEventListener('pointerup', endDrag);
    scroll.addEventListener('pointercancel', endDrag);

    // Advance arrow — animated glide of 60% viewport width.
    advance?.addEventListener('click', () => {
      const target = Math.min(
        scroll!.scrollLeft + scroll!.clientWidth * 0.6,
        scroll!.scrollWidth - scroll!.clientWidth,
      );
      animateScrollTo(target, 480);
    });

    scroll.addEventListener('scroll', updateNav, { passive: true });

    // Debounced resize → recompute layout + refresh nav state. 120ms is
    // long enough to coalesce drag-resize events without feeling laggy.
    let resizeTimer: number | undefined;
    window.addEventListener('resize', () => {
      if (resizeTimer !== undefined) window.clearTimeout(resizeTimer);
      resizeTimer = window.setTimeout(() => { recompute(); updateNav(); }, 120);
    });

    // Initial mount: recompute with the real viewport, then anchor at the
    // start so the first milestone aligns with the content-column left edge.
    // (The "you are here" highlight on the most-recent shipping milestone is
    // still visible — but it's no longer the scroll anchor.)
    recompute();
    scroll.scrollLeft = 0;
    setTimeout(updateNav, 50);
    updateNav();

    // Three-pulse hint on the advance arrow ~100ms after layout settles
    // so the user notices the affordance once and then it sits quietly.
    setTimeout(() => advance?.classList.add('rr-hint'), 100);
  });
</script>

<style>
  /* ── Desktop route ──────────────────────────────────────────────── */
  .rr-wrap { position: relative; }

  /* Escape the parent .page max-width so the route can use the actual
     viewport width. The headline, dispatch banner, section header, and
     legend all stay centred at content width — only the route widens. */
  .rr-fullbleed {
    width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
  }
  .rr-scroll {
    /* overflow-x: auto + overflow-y: visible lets hovered cards expand
       above/below the track without being clipped. .rr-scroll-inner is
       the spec-recommended belt-and-braces wrapper in case a browser
       misbehaves on the combination.
       NO scroll-snap-type and NO scroll-behavior: smooth — both fight
       the JS drag-momentum + animated-glide implementation below. The
       path is meant to glide continuously, not click into fixed
       positions. */
    overflow-x: auto;
    overflow-y: visible;
    scrollbar-width: none;
    padding: 60px 80px 80px;

    /* Drag affordance: cursor + suppress native horizontal swipe so
       horizontal drag triggers our handler while vertical drag still
       scrolls the page. user-select stops drag from selecting text. */
    cursor: grab;
    touch-action: pan-y;
    user-select: none;
  }
  .rr-scroll::-webkit-scrollbar { display: none; }
  .rr-scroll.rr-dragging { cursor: grabbing; }
  /* Pointer-events off the cards mid-drag — prevents accidental hover
     reveal while the track is being dragged past. */
  .rr-scroll.rr-dragging .rr-card { pointer-events: none; }
  .rr-scroll-inner { /* structural — keeps the track on its own layer */ }
  .rr-track { position: relative; }
  .rr-path { position: absolute; top: 0; left: 0; pointer-events: none; }

  .rr-milestone {
    position: absolute;
    /* Inline transform/opacity are driven per-frame from JS based on each
       milestone's distance from the viewport centre, so the track comes
       alive as you move it (centre milestone emphasised, edges recede).
       The short ease softens the per-frame updates into a glide. */
    transform: translate(-50%, -50%);
    transition: transform .2s ease-out, opacity .2s ease-out;
    will-change: transform, opacity;
  }
  /* A hovered/focused card always reads at full size and brightness,
     regardless of where it sits along the route — overrides the inline
     focus styles JS sets. */
  .rr-milestone:has(.rr-card:hover),
  .rr-milestone:has(.rr-card:focus-visible) {
    transform: translate(-50%, -50%) scale(1) !important;
    opacity: 1 !important;
    z-index: 10;
  }

  .rr-dot {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    box-shadow: 0 0 0 5px var(--background);   /* halo cuts the path under the dot */
    transition: transform .25s ease, box-shadow .25s ease;
  }
  .rr-dot.rr-current {
    transform: scale(1.3);
    box-shadow:
      0 0 0 5px var(--background),              /* cream halo */
      0 0 0 6px rgba(185, 107, 88, 0.45);       /* terracotta ring outside */
  }

  /* Hover-on-card animates the sibling dot too. :has() is fine on every
     evergreen browser we target; older Firefox just doesn't grow the dot. */
  .rr-milestone:has(.rr-card:hover) .rr-dot,
  .rr-milestone:has(.rr-card:focus-visible) .rr-dot {
    transform: scale(1.15);
  }
  .rr-milestone:has(.rr-card:hover) .rr-dot.rr-current,
  .rr-milestone:has(.rr-card:focus-visible) .rr-dot.rr-current {
    transform: scale(1.4);
  }

  .rr-attach {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .rr-attach-below { top: 7px; }    /* hangs down from the dot */
  .rr-attach-above { bottom: 7px; flex-direction: column-reverse; }

  .rr-connector {
    width: 1px;
    height: 30px;
    background: rgba(0, 0, 0, 0.18);
  }

  .rr-card {
    display: block;
    width: 240px;
    padding: 14px 16px;
    border-radius: 10px;
    background: transparent;
    color: inherit;
    text-decoration: none;
    border-bottom: none;
    transition:
      transform .35s cubic-bezier(.2,.7,.3,1),
      box-shadow .35s ease,
      background .25s ease;
    cursor: pointer;
  }
  .rr-card:hover,
  .rr-card:focus-visible {
    background: var(--surface-card);
    box-shadow:
      0 12px 32px -16px rgba(42, 37, 32, 0.25),
      0 0 0 0.5px var(--surface-card-border);
    transform: translateY(-2px);
    z-index: 10;
    border-bottom: none;
    outline: none;
  }

  .rr-eyebrow {
    font-family: var(--font-sans);
    font-size: 11px;
    letter-spacing: 1.4px;
    text-transform: uppercase;
    margin: 0 0 7px;
    font-weight: 600;
  }
  .rr-card-title {
    font-family: var(--font-serif);
    font-size: 20px;
    line-height: 1.25;
    color: var(--on-surface);
    margin: 0;
  }
  .rr-more {
    max-height: 0;
    opacity: 0;
    overflow: hidden;
    transition:
      max-height .35s ease,
      opacity .25s ease,
      margin-top .35s ease;
    margin-top: 0;
  }
  .rr-card:hover .rr-more,
  .rr-card:focus-visible .rr-more {
    max-height: 340px;
    opacity: 1;
    margin-top: 12px;
  }
  .rr-desc {
    font-family: var(--font-sans);
    font-size: 14px;
    line-height: 1.6;
    color: var(--on-surface-variant);
    margin: 0 0 10px;
  }
  .rr-trail {
    font-family: var(--font-sans);
    font-size: 10px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--on-surface-muted);
    margin: 0;
  }

  /* ── Advance arrow ─────────────────────────────────────────────── */
  .rr-advance {
    position: absolute;
    right: 32px;
    top: 50%;
    transform: translateY(-50%);
    width: 48px;
    height: 48px;
    border-radius: 50%;
    border: 1px solid var(--pigment-terracotta);
    background: var(--background);
    color: var(--pigment-terracotta);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    z-index: 5;
    transition: background .2s ease,
                color      .2s ease,
                opacity    .25s ease,
                transform  .25s ease;
  }
  .rr-advance:hover,
  .rr-advance:focus-visible {
    background: var(--pigment-terracotta);
    color: var(--background);
    outline: none;
    transform: translateY(-50%) scale(1.06);
  }
  .rr-advance[disabled],
  .rr-advance.rr-at-end {
    opacity: 0.25;
    pointer-events: none;
  }
  /* Three-pulse hint on first load — fires once, then stops. */
  @keyframes rr-advance-pulse {
    0%, 100% { box-shadow: 0 0 0 0   rgba(185, 107, 88, 0); }
    50%      { box-shadow: 0 0 0 8px rgba(185, 107, 88, 0.15); }
  }
  .rr-advance.rr-hint {
    animation: rr-advance-pulse 1.4s ease-in-out 3;
  }

  /* Edge fades cover only the track itself — the top/bottom padding
     zones (60/80) on .rr-scroll exist so hover cards can overflow there
     without clipping, so the fades shouldn't paint over them. */
  .rr-fade-left, .rr-fade-right {
    position: absolute;
    top: 60px;
    bottom: 80px;
    pointer-events: none;
    transition: opacity .25s ease;
  }
  .rr-fade-left {
    left: 0; width: 60px;
    background: linear-gradient(to left, transparent, var(--background));
    opacity: 0;
  }
  .rr-fade-right {
    right: 0; width: 90px;
    background: linear-gradient(to right, transparent, var(--background));
  }

  /* ── Mobile vertical timeline ──────────────────────────────────── */
  .rr-mobile { display: none; }

  @media (max-width: 767px) {
    .rr-desktop { display: none; }
    .rr-mobile {
      display: block;
      list-style: none;
      padding: 0;
      margin: 0;
    }

    .rrm-row {
      display: grid;
      grid-template-columns: 32px 1fr;
      gap: 16px;
      align-items: start;
    }
    .rrm-track-col {
      position: relative;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 0;
      min-height: 100%;
      padding-top: 6px;
    }
    .rrm-dot {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      flex-shrink: 0;
    }
    .rrm-line {
      width: 1px;
      flex: 1;
      min-height: 28px;
      background: rgba(0, 0, 0, 0.18);
      margin-top: 4px;
    }
    .rrm-body {
      display: flex;
      flex-direction: column;
      gap: 6px;
      padding-bottom: 28px;
    }
    .rrm-eyebrow {
      font-family: var(--font-sans);
      font-size: 11px;
      letter-spacing: 1.4px;
      text-transform: uppercase;
      margin: 0;
      font-weight: 600;
    }
    .rrm-title {
      font-family: var(--font-serif);
      font-size: 21px;
      line-height: 1.25;
      color: var(--on-surface);
      margin: 0;
    }
    .rrm-desc {
      font-family: var(--font-sans);
      font-size: 15px;
      line-height: 1.6;
      color: var(--on-surface-variant);
      margin: 0;
    }
    .rrm-trail {
      font-family: var(--font-sans);
      font-size: 10px;
      letter-spacing: 1px;
      text-transform: uppercase;
      color: var(--on-surface-muted);
      margin: 0;
    }
  }
</style>
