# Blocks — Hero

A full-bleed photo or video hero: a Display title, an optional subtitle and up to two
Button atoms over a darkening gradient, with an optional award pill and up to three
USPs. Rebuilt from production's `HeroBlock` Storyblok component (rendered by
`components/Hero.vue`) on Gazelle tokens only. The Blocks → Hero screen emits this exact
markup.

## Metadata

- **Category:** Blocks
- **Family:** Heroes
- **Status:** Stable (Vimeo not covered; model price is a hardcoded sample, not live data)
- **Storyblok component:** `HeroBlock`
- **Production source:** `gazelle-int-website/app/storyblok/HeroBlock.vue` →
  `app/components/Hero.vue`
- **Source:** `docs/blocks-registry.js` (canonical markup), `docs/atom-classes.js`
  (Button atom), `tailwind.css` (theme)

## When to use

- The first section of a landing, campaign or model page, with one strong photograph or
  a silent looping video.
- At most one hero per page. Its title is the page's `<h1>`.

## When not to use

- Below other content. The hero is sized to the viewport and assumes it opens the page.
- Over a light or busy image without testing legibility. The overlay and gradient are
  tuned for photography; don't lighten them.

## Storyblok fields

| Field | Type | Covered | Notes |
| --- | --- | --- | --- |
| `media` | asset (image) | Yes | Required. Background image; also the video poster. |
| `mobile_image` | asset (image) | Yes | Replaces `media` below tablet. |
| `video` | asset (video) | Yes | Adds `<video>` and the play/pause button. |
| `mobile_video` | asset (video) | Yes | Second `<source>` below tablet. |
| `video_type`, `vimeo_video_id`, `vimeo_mobile_video_id` | option / text | **No** | Vimeo loads a third-party player. |
| `title` | richtext | Yes | Rendered as plain text in the `<h1>`, without rich-text styles. |
| `subtitle` | text | Yes | A `<p>` under the title. Production renders it as `<h3>`. |
| `award_image`, `award_title`, `award_subtitle`, `award_count`, `award_link` | asset / text / number / multilink | Yes | The award pill. The count adds a `+n` bubble. |
| `primary_cta` | bloks → LinkItem (max 1) | Yes | Button atom: Tertiary · Large. |
| `secondary_cta` | bloks → LinkItem (max 1) | Yes | Button atom: White · Large. |
| `show_model_price` | boolean | Yes* | Shows a hardcoded sample "From / Per month from" price. *Static text only — production wires live Algolia model data via `BasePrice`. |
| `usp_list` | bloks → HeroBlockUspItem (max 3) | Yes | Icon + label. With USPs the content moves to the top and centres. |
| `custom_padding`, `padding_top`, `padding_bottom` | boolean | Yes | Applied as **margin** (see below). |

## Anatomy

```html
<section class="relative flex h-hero min-h-112 w-full flex-col overflow-hidden tablet:min-h-128 mb-section-sm laptop:mb-section-lg">
  <picture class="pointer-events-none absolute inset-0">
    <source media="(max-width: 767px)" srcset="…mobile.jpg">          <!-- mobile_image -->
    <img src="…hero.jpg" alt="" class="size-full object-cover">
  </picture>
  <div class="pointer-events-none absolute inset-0 bg-black/20" aria-hidden="true"></div>
  <div class="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 backdrop-blur-sm mask-t-from-0%" aria-hidden="true"></div>
  <div class="pointer-events-none absolute inset-x-0 bottom-0 h-1/2 bg-linear-to-b from-black/0 via-black/60 to-black/90" aria-hidden="true"></div>

  <div class="relative mx-auto flex w-full max-w-content grow flex-col px-gutter pb-16 laptop:pb-20 items-start justify-end">
    <div class="flex w-full flex-col items-start">
      <!-- award pill (optional) -->
      <h1 class="mb-5 w-full type-display-sm text-fg-inverse tablet:mb-6 tablet:type-display-lg">…</h1>
      <p class="mb-6 w-full type-h2-sm text-fg-inverse tablet:mb-10 tablet:type-h2-lg">…</p>
      <!-- model price (optional) -->
      <div class="flex flex-col items-center gap-4 tablet:flex-row tablet:gap-6">
        <!-- Button atom as <a>: Tertiary · Large, then White · Large -->
      </div>
    </div>
    <!-- USP list (optional) -->
  </div>
  <!-- play/pause button (video only) -->
</section>
```

The four layers stack in DOM order: image, a 20% black overlay, a frosted blur masked to
fade in towards the bottom, and a black gradient behind the text. The content is `relative`
and comes last, so it sits on top without z-index.

**With USPs or the model price** the content column centres its text
(production: `hasUspList || showModelPrice`). USPs additionally swap
`items-start justify-end` for `items-center justify-between pt-30` on the outer column,
moving the block to the top to leave room for the USP grid below; the price alone keeps
the block anchored to the bottom, just centred. The USP
list is `grid grid-cols-3 gap-4 desktop:w-1/2`. Each item is a frosted card on mobile and
tablet (`rounded-md bg-white/30 p-4 backdrop-blur-sm`) and plain from laptop. It holds a
48px Iconoir glyph (`size-12`, `stroke-width="0.875"`) and a `type-h4-sm tablet:type-h4-lg`
label.

**Model price** is a hardcoded sample (`€ 2.499` / `€ 39,–` per month), rendered as
`type-h4-sm tablet:type-h4-lg` for the price and `type-caption-sm tablet:type-caption-lg
text-fg-inverse/80` with an info-circle icon for the monthly line — production's
`BasePrice` reads a linked model's live price, discount and finance rates from Algolia,
and the info icon opens a monthly-payment modal; none of that data or behaviour is
reproduced here.

**Video** replaces `<picture>` with
`<video muted loop playsinline disablepictureinpicture preload="none" poster="…">`, plus a
`(max-width: 767px)` source when there's a mobile video. A round play/pause `<button>`
(`size-6 tablet:size-8`, `bg-black/50`, white on hover, with a `before:-inset-1.5` hit area)
sits bottom-right. **Behaviour (not markup):** product JS starts playback on mount and
toggles it from the button, updating its `aria-label`. Don't autoplay under
`prefers-reduced-motion`.

**Spacing** — the hero is full-bleed, so its rhythm is margin, not padding:

| `custom_padding` | `padding_top` | `padding_bottom` | Classes |
| --- | --- | --- | --- |
| off | — | — | `mb-section-sm laptop:mb-section-lg` |
| on | on | — | adds `mt-section-sm laptop:mt-section-lg` |
| on | — | on | adds `mb-section-sm laptop:mb-section-lg` |
| on | off | off | *(none)* |

## Tokens

- Layout: `h-hero` (`--hero-height`, 85svh), `min-h-112` / `tablet:min-h-128` (448 / 512px
  on the 4px grid), `max-w-content`, `px-gutter`, `*-section-sm` / `*-section-lg`.
- Type: `type-display-sm` → `type-display-lg` (title), `type-h2-sm` → `type-h2-lg`
  (subtitle), `type-h4-sm` → `type-h4-lg` (price and USP labels), `type-caption-sm` →
  `type-caption-lg` (award pill, monthly price line).
- Colour: `text-fg-inverse` (and `/80`), `black` / `white` with opacity for overlays and
  glass, `ring-focus-ring`.
- Atoms: Button (Tertiary · Large, White · Large) from `docs/atom-classes.js`, unchanged.

## Production mapping

| Part | Production | Design system | Decision |
| --- | --- | --- | --- |
| Height | `calc(85vh − header)`, `min-h-[450px] tablet:min-h-[510px]` | `h-hero min-h-112 tablet:min-h-128` | New layout token (85svh); the header offset is left to the site; floors snapped to 448 / 512 |
| Spacing | `mt-07/laptop:mt-11`, `mb-07/laptop:mb-11`; default bottom only | `mt-/mb-section-sm laptop:*-section-lg` | Same values via section tokens, still margin |
| Content frame | `c-container-small absolute left-1/2 -translate-x-1/2 px-05 desktop-xl:px-00` | `mx-auto max-w-content px-gutter` in a flex column | Flex instead of absolute positioning; the gutter exception is dropped |
| Content offset | `bottom-10 laptop:bottom-11` (64 / 80px); `top-12` (120px) with USPs | `pb-16 laptop:pb-20`; `pt-30` | Same values on the 4px grid |
| Title | `text-lg-3 tablet:text-huge-2` (40 → 80px), `leading-[1.1]`, tight | `type-display-sm tablet:type-display-lg` (64 → 80px), leading 100%, −5% | Display role by the Small → Large rule; larger than production on mobile |
| Subtitle | `<h3>`, `text-xl tablet:text-lg-4` (32 → 48px) | `<p>`, `type-h2-sm tablet:type-h2-lg` (32 → 40px) | Closest role; not a heading, as it doesn't start a section |
| Title/subtitle gaps | `mb-05 tablet:mb-06`, `mb-06 tablet:mb-08` | `mb-5 tablet:mb-6`, `mb-6 tablet:mb-10` | Exact |
| Overlay | `bg-main-black-500/20` | `bg-black/20` | Pure black; `main-black-500` is production's ink |
| Bottom gradient | `::after`, 55% tall, `from-main-black-500/0 via-/60 to-/90` | `h-1/2 bg-linear-to-b from-black/0 via-black/60 to-black/90` | 55% → 50% (a ratio, not a token) |
| Frosted fade | `::before`, `backdrop-filter: blur(10px)` + hand-written mask | `backdrop-blur-sm mask-t-from-0%` | 10 → 8px on Tailwind's blur scale (**no blur tokens yet**) |
| Buttons | `BaseButton` `blur` / `white` themes | Button atom Tertiary · Large / White · Large | Atoms reused verbatim; Tertiary is the DS ghost for photos (oyster border and label, not white); Large at every width |
| Award pill | `bg-[rgba(0,0,0,0.2)] backdrop-blur-[5px]`, `gap-[10px]`, image `size-[18px] p-[3px]`, `text-xs tablet:text-sm` | `bg-black/20 backdrop-blur-xs`, `gap-2`, image `size-6 p-0.5`, `type-caption-sm tablet:type-caption-lg` | Snapped to the grid and the Caption role (12 → 14px, was 14 → 16px) |
| Award count | `size-05 tablet:size-06`, `text-[10px] tablet:text-[12px]` | `size-5 tablet:size-6`, `type-caption-sm` | Caption Small at every width |
| USP item | `rounded-lg bg-main-white-500/30 backdrop-blur p-04`; icon `size-08 laptop:size-09`; label `text-sm/md/lg` | `rounded-md bg-white/30 backdrop-blur-sm p-4`; icon `size-12`; `type-h4-sm tablet:type-h4-lg` | Icon on a defined icon size (48px); label on the H4 role |
| USP width | `desktop:tablet:w-1/2` | `desktop:w-1/2` | Same intent, valid variant |
| Play button | `bg-[rgba(0,0,0,0.5)] backdrop-blur-[5px] h-06 tablet:h-[32px]`, `before:-inset-[6px]` | `bg-black/50 backdrop-blur-xs size-6 tablet:size-8 before:-inset-1.5` | Snapped; the focus ring is added |
| Model price | `BasePrice`, live Algolia model price/discount/finance, info icon opens a modal | `type-h4-sm tablet:type-h4-lg` price + `type-caption-sm tablet:type-caption-lg text-fg-inverse/80` monthly line with a static info-circle icon | Hardcoded sample text (`€ 2.499` / `€ 39,–`); no live data, discount or modal |
| Vimeo | `vue-vimeo-player` | — | **Not covered** |

## Usage

- Use a photo with a calm lower third. The title and buttons sit there, over the gradient.
- Keep the title to two lines on mobile. `type-display-sm` is 64px.
- Give `mobile_image` a portrait crop when the subject would be cut off at 375px.
- Don't restyle the buttons. They are the Button atom; change the atom if they need to
  change.

## Cross-references

- `specs/foundations/layout.md` — `h-hero`, container, gutter, section rhythm, Small → Large.
- `specs/foundations/typography.md` — the Display, H2, H4 and Caption roles.
- `specs/atoms/button.md` — the Tertiary and White styles.
- `specs/icons/icons.md` — icon sizes and stroke weights.
