# Autoplay Hero with Progress — a hero on a clock, and the clock made visible

## Goal

Build a full-bleed hero carousel that advances **on a timer**, with one progress tick per slide
filling over exactly that slide's duration, and controls that actually work.

Use it when the front page has to show three or four things and none outranks the others: a
season's programme, a line-up, a schedule.

## Tech

**No GSAP, no ScrollTrigger, no Lenis — deliberately.** This hero is driven by a clock, not by the
scroll, and wiring a time-driven component to a scroll library is pretending. Plain ES modules and
one `requestAnimationFrame` loop.

> How the effect was identified in the first place: on the reference site the progress bar read
> **40% scrolling down and 86% scrolling back up at the same scroll position**. That is only
> possible if time is the driver. Nothing in the DOM says so — it took capturing frames on the way
> down and on the way up to see it.

## Structure

```html
<header class="hero" aria-roledescription="carousel">
  <div class="deck">
    <figure class="slide is-on"><img …><figcaption>…</figcaption></figure>
    …four of them…
  </div>
  <div class="bar" role="group" aria-label="Carousel controls">
    <div class="ticks" aria-hidden="true"><i><b></b></i>…</div>
    <div class="keys">
      <button data-go="-1">‹</button><button data-toggle>⏸</button><button data-go="1">›</button>
    </div>
  </div>
</header>
```

## The four decisions that ARE this component

### 1. One rAF loop drives BOTH the advance and the bar.

```js
const paso = (t) => {
  raf = requestAnimationFrame(paso);
  if (pausado || !visible || !enPantalla) { t0 = t; return; }
  const p = Math.min(1, (t - t0) / DURACION);
  ticks[actual].querySelector("b").style.width = `${p * 100}%`;
  if (p >= 1) ir(actual + 1);
};
```

The obvious alternative — a CSS transition on the bar plus a `setInterval` for the slide — has two
clocks, and they come apart the first time the tab is backgrounded or the reader pauses. After
that the bar **lies** about when the next slide lands, which is worse than having no bar.

### 2. Resuming rebases the epoch; it does not preserve elapsed time.

Note the `t0 = t` inside the paused branch. When the loop is idle it keeps moving the origin
forward, so coming back from a hidden tab resumes exactly where it stopped instead of jumping
several slides at once.

### 3. It stops when nobody is looking.

```js
document.addEventListener("visibilitychange", () => { visible = !document.hidden; });
new IntersectionObserver(([e]) => { enPantalla = e.isIntersecting; }, { threshold: 0.25 }).observe(deck);
```

A four-slide hero that keeps cycling at the bottom of a long page is battery spent on nothing.

### 4. Manual advance restarts the clock.

`ir()` sets `t0 = performance.now()`. Without it the slide you just jumped to lasts whatever was
left of the previous one, which reads as a bug even though nothing is broken.

## Crossfade, not slide

```css
.slide { opacity: 0; visibility: hidden; transition: opacity .7s ease, visibility 0s linear .7s; }
.slide.is-on { opacity: 1; visibility: visible; transition: opacity .7s ease, visibility 0s; }
```

With full-bleed photography a translate reads as a jolt, because the eye tracks the image edge
rather than the content. The delayed `visibility` is what keeps the outgoing slide out of the tab
order without killing the fade.

## Accessibility

- `aria-hidden` on every non-active `<figure>`, or a screen reader announces all four sessions at
  once.
- The pause button's label and glyph both change with state.
- `:focus-visible` outline on the controls in the accent colour — this is a component people
  operate.

## Reduced motion

Stop the autoplay, keep the controls. Do **not** hide them: the reader still gets every slide, by
hand, which is exactly what the setting asks for.

## Adapting

Change the slide count (three to five), the duration (4–7 s; below 4 nobody finishes reading the
caption), the palette, the type. Keep: one rAF for both jobs, the epoch rebase, the two pause
conditions, the clock restart on manual advance, and opacity-only transitions.
