# Landing Reveal

> A preloader that counts the files your hero really needs while its images rise onto a pile. At 100 the hero image comes up last and grows into place, then the title rises. Click or any key skips. No GSAP.

Preloaders & transitions · preloader, intro, hero, page load, counter, image stack · HTML, CSS, vanilla JS · no dependencies · updated 2026-10-21
Demo: https://kodza.dev/components/landing-reveal/

Takes the whole screen (or reacts to the page scroll): give it a page, not a small box.

## Files

The code below, served as files: byte for byte what the Kodza site copies (the CSS includes its theme guard).

- [`kodza-landing-reveal.css`](https://kodza.dev/registry/landing-reveal/kodza-landing-reveal.css) · 31.6 KB
- [`kodza-landing-reveal.js`](https://kodza.dev/registry/landing-reveal/kodza-landing-reveal.js) · 27.1 KB
- [`kodza-landing-reveal.html`](https://kodza.dev/registry/landing-reveal/kodza-landing-reveal.html) · 3.0 KB · the markup, to adapt and paste into your page

**Can you run shell commands? Download the CSS and the JS, don't retype them.** Retyping costs thousands of tokens and can drop or change lines of the CSS. Put the files where your stack expects them (Use it says where: change the `-o` paths):

```bash
curl -fsSL --create-dirs -o kodza/kodza-landing-reveal.css https://kodza.dev/registry/landing-reveal/kodza-landing-reveal.css
curl -fsSL --create-dirs -o kodza/kodza-landing-reveal.js https://kodza.dev/registry/landing-reveal/kodza-landing-reveal.js
```

Then load the files from your page as Use it shows, instead of pasting the content. The HTML is the part you adapt (text, links, modifiers): paste it from the Code section. Copy the CSS and the JS from the Code section only if you can't run commands, exactly as they are. (Windows PowerShell: `curl.exe`.)

## API

- Root: `.kodza-landing-reveal`
- Variables (set them on the root element itself: it declares its own defaults):
  - `--kz-landing-reveal-bg: #ffffff` — the loader's paper: your page colour, so the hand-over is seamless
  - `--kz-landing-reveal-fg: #0a0a0a` — the loader's counter and labels
  - `--kz-landing-reveal-muted: rgba(10, 10, 10, 0.62)`
  - `--kz-landing-reveal-line: rgba(10, 10, 10, 0.1)`
  - `--kz-landing-reveal-text: #f2f2f0` — the hero, over its image
  - `--kz-landing-reveal-text-muted: rgba(242, 242, 240, 0.74)`
  - `--kz-landing-reveal-veil: rgba(10, 10, 10, 0.6)` — under the text, so it reads on any image
  - `--kz-landing-reveal-button: #f2f2f0`
  - `--kz-landing-reveal-on-button: #0a0a0a`
  - `--kz-landing-reveal-card: clamp(148px, 26vmin, 260px)` — width of the pile
  - `--kz-landing-reveal-radius: 0px` — the hero image: 0 = edge to edge
  - `--kz-landing-reveal-height: 100svh` — the hero; take your header off if it sits above
  - `--kz-landing-reveal-side: 24px`
  - `--kz-landing-reveal-min: 2.2s` — 100 never comes sooner, even with every file cached
  - `--kz-landing-reveal-max: 2.8s` — nor later: past it, the hero comes in without the late files
  - `--kz-landing-reveal-ease: cubic-bezier(0.65, 0, 0.35, 1)` — the hero image growing into place
- Attributes:
  - `data-caption` (e.g. `display: block`, `display: grid`, `z-index: 9999`, `float: left`, `border-radius`, `steps(5, end)`)
  - `data-enter` (e.g. `down`)
  - `data-once`
- Lifecycle: `kodza:init` / `kodza:destroy` on `document`, with `{ detail: { root } }` to target a container.

## Keyboard

What each key does, by where the focus is. Keep it working when you adapt the HTML: same elements, same classes.

- Anywhere, while the intro plays (while `.kodza-landing-reveal[aria-busy="true"]` is on the page):
  - `Enter` / `Tab`: Skips the intro, like any other key
  - `Space` / `PageUp` / `PageDown` / `Home` / `End` / `ArrowUp` / `ArrowDown` / `ArrowLeft` / `ArrowRight`: Skips it too, the page stays where it is

## Dark mode

The values Kodza uses on a dark background (`#0a0a0a`). For a page that follows the system theme, add them after the component's CSS:

```css
@media (prefers-color-scheme: dark) {
  .kodza-landing-reveal {
    --kz-landing-reveal-bg: #0a0a0a;
    --kz-landing-reveal-fg: #f2f2f0;
    --kz-landing-reveal-muted: rgba(242, 242, 240, 0.62);
    --kz-landing-reveal-line: rgba(242, 242, 240, 0.14);
  }
}
```

A site with its own theme switch puts the same rules under its dark selector instead (`[data-theme="dark"] .kodza-landing-reveal { … }`); a single dark section, under that section's class.

Its images have dark versions next to them (`kz-block-1400.webp` → `kz-block-dark-1400.webp`). To swap them on a dark page: `<picture><source media="(prefers-color-scheme: dark)" srcset="…-dark-….webp"><img …></picture>`.

## Use it

### Plain HTML page

- CSS: `kodza/kodza-landing-reveal.css` next to the page (the Files commands as they are), linked in `<head>`: `<link rel="stylesheet" href="kodza/kodza-landing-reveal.css">`. Without a shell, paste it in a `<style>` in `<head>` instead. It only touches `kodza-` classes.
- HTML: where the component goes. Paste it as many times as you need: instances never interfere.
- JS: `kodza/kodza-landing-reveal.js`, loaded with `<script src="kodza/kodza-landing-reveal.js" defer></script>` in `<head>`. Without a shell, paste it in a `<script>` before `</body>` (or in `<head>`: it waits for the page). One copy per page, whatever the number of instances.
- Restyle with the `--kz-…` variables, set on the component's root element itself (the root declares its own defaults, so a value on an ancestor is ignored), and with the modifiers / attributes listed under API.
- Font: it takes the page's own. The Kodza look is Geist: `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap">`.
- Content added later (AJAX navigation, a SPA route, a builder preview): start it with `document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root: container } }))`; before removing it, the same call with `'kodza:destroy'` stops it and puts the original markup back.

### Claude artifact (claude.ai)

- Paste the CSS in a `<style>` and the JS in a `<script>` at the end, in the artifact itself: it loads stylesheets only from Google Fonts and scripts only from a few CDNs, so no `<link>` or `<script src>` to Kodza.
- Writing the artifact file with a shell? Insert the downloaded files (Files) with a command instead of retyping them: a line `KODZA-CSS` alone inside the `<style>`, `KODZA-JS` inside the `<script>`, then `sed -e '/^KODZA-CSS$/{r kodza/kodza-landing-reveal.css' -e 'd' -e '}' -e '/^KODZA-JS$/{r kodza/kodza-landing-reveal.js' -e 'd' -e '}' page.html > artifact.html`.
- Font: load Geist from Google Fonts (allowed): `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap">`.
- Images: the artifact blocks every other host, `kodza.dev` included, without an error. Embed each one as a `data:` URI, with a command if you can run one (`curl -s <url> | base64`; the 1400 px files weigh 10 to 30 KB), never by typing base64 out. If you can't run commands, keep the `<img>` with its `width` and `height`, and tell the user the picture stays blank inside the artifact.
- Dark mode: the viewer sets `data-theme="dark"` or `"light"` on `<html>`, or nothing when it follows the system. Write the Dark mode rules twice: inside `@media (prefers-color-scheme: dark)` with `:root:not([data-theme="light"])` in front of each selector, and with `:root[data-theme="dark"]` in front of each selector.

### WordPress

- HTML: a **Custom HTML** block (or a pattern / template part). WordPress keeps this markup as it is only for users allowed to post unfiltered HTML (Administrators; Super Admins on multisite): for anyone else it strips tags such as `<svg>`, `<input>` or `<dialog>`, so add it from the theme (a pattern or template part) instead.
- This one spans the page: a block theme keeps a Custom HTML block to the width of the text column, so put it inside a **Group** block set to **Full width**, with **Inner blocks use content width** turned off.
- CSS and JS: the downloaded files go in the theme, under `wp-content/themes/<your-theme>/kodza/` (the child theme if there is one), enqueued from its functions.php (or a code-snippets plugin):
  ```php
  add_action('wp_enqueue_scripts', function () {
    $dir = get_stylesheet_directory() . '/kodza/';
    $uri = get_stylesheet_directory_uri() . '/kodza/';
    wp_enqueue_style('kodza-landing-reveal', $uri . 'kodza-landing-reveal.css', [], filemtime($dir . 'kodza-landing-reveal.css'));
    wp_enqueue_script('kodza-landing-reveal', $uri . 'kodza-landing-reveal.js', [], filemtime($dir . 'kodza-landing-reveal.js'), true);
  });
  ```
  Scoped to `kodza-` classes, the CSS never touches the theme. One copy of the JS, whatever the number of blocks.
- No access to the theme files? The CSS can also go, as it is, in **Appearance → Customize → Additional CSS** (classic theme) or **Styles → Additional CSS** in the Site Editor (block theme).
- Theme with AJAX page transitions: fire `kodza:init` on the new content after each swap (and `kodza:destroy` on the old one).
- Performance plugin (WP Rocket, LiteSpeed Cache, Perfmatters…) with **Delay JavaScript execution** turned on: add `kodza` to the scripts it must not delay. Otherwise the component waits for the visitor's first scroll, tap or mouse move, and that first click can be lost.
- Colours: it takes the colour of the text around it (or keeps its own monochrome): nothing to point at the theme.
- WordPress turns straight quotes and `--` into typographic ones in the text of a Custom HTML block (not inside `<code>` or `<pre>`).
- Put it first in the page, in a full-width Group, and load the script in the <head> if you can (it then hides a data-once loader before the first paint). The loader's paper is --kz-landing-reveal-bg: set it to your page colour.

#### With Elementor

- HTML: an **HTML** widget (Elementor shows it to administrators only). It keeps the code exactly as pasted, straight quotes included.
- This one spans the page: give the widget its own container, **Content Width: Full Width**, padding 0.
- CSS and JS: as above. Elementor's Site Settings → Theme Style styles every button, field, link and image of the page, components included; the theme guard at the end of the CSS keeps the component's buttons, fields and link colours as they are.
- The Elementor editor redraws each widget after the page has loaded: without the script below, the component stays still in the editor preview (the published page works). Add it once per site, in the footer like the component's script (`kodza/elementor-editor.js`, or a code-snippets plugin):

```js
// Kodza in the Elementor editor: the editor redraws each HTML widget after the page has loaded.
// Start Kodza in what it draws, stop it in what it replaces or removes. Once per site.
window.addEventListener('elementor/frontend/init', () => {
  if (!elementorFrontend.isEditMode()) return;
  class KodzaWidget extends elementorModules.frontend.handlers.Base {
    onInit() {
      super.onInit();
      this.kodza = [...this.$element[0].children];
      document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root: this.$element[0] } }));
    }
    onDestroy() {
      super.onDestroy();
      this.kodza.forEach((root) => document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } })));
    }
  }
  elementorFrontend.elementsHandler.attachHandler('html', KodzaWidget);
});
```

- No child theme: everything can go in the HTML widget instead, `<style>` with the CSS, the HTML, then `<script>` with the JS. It works on the page and in the editor without the script above. This one reads the page around it, and a script in the widget runs before the rest of the page exists: put its `<script>` in an HTML widget at the bottom of the page instead.

### Shopify

- Files: `assets/kodza-landing-reveal.css` and `assets/kodza-landing-reveal.js` (the Files commands with `-o assets/…`).
- In the section: `{{ 'kodza-landing-reveal.css' | asset_url | stylesheet_tag }}` and `<script src="{{ 'kodza-landing-reveal.js' | asset_url }}" defer></script>`, then the HTML.
- The theme editor re-renders sections: restart them with
  `document.addEventListener('shopify:section:load', (e) => document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root: e.target } })));`
  and the same with `shopify:section:unload` → `kodza:destroy`.

### React / Next.js

- Files, side by side (e.g. in `components/kodza/`): `LandingReveal.tsx` (below), the CSS block under Code saved as `landing-reveal.css` and the JS block as `landing-reveal.js`, both as they are. The HTML block is already inside the component: skip it.
- Use it: `import { LandingReveal } from '@/components/kodza/LandingReveal';` then `<LandingReveal />`. Props: `vars` (the `--kz-…` variables listed under API, e.g. `vars={{ '--kz-landing-reveal-bg': … }}`), `className`, `style` and the other props of its `<section>` (`onClick`, `aria-…`).
- Next.js App Router: it is a Client Component already (`'use client'`). Pages Router: Next.js takes global CSS only in `pages/_app`, so move the `import './landing-reveal.css'` line there.
- Safe under StrictMode, client-side navigation and prop changes: when a prop changes, the `key` gives React a fresh element, `kodza:destroy` stops the script on the old one and `kodza:init` starts it on the new one.

```tsx
'use client';
// Kodza — Landing Reveal, for React. From https://kodza.dev/components/landing-reveal/
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Next to this file: landing-reveal.css and landing-reveal.js, the component's CSS and JS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
// The script changes this markup once React has rendered it (it adds layers, splits text, moves
// nodes), so React must not patch it afterwards. When a prop changes, the key gives React a fresh
// element: kodza:destroy stops the script on the old one, kodza:init starts it on the new one.
import { useRef, useEffect, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './landing-reveal.css';

export type LandingRevealProps = Omit<ComponentPropsWithoutRef<'section'>, "children" | "vars"> & {
  /** Settings, as CSS variables on the root */
  vars?: {
    /** the loader's paper: your page colour, so the hand-over is seamless — default: #ffffff */
    "--kz-landing-reveal-bg"?: string | number;
    /** the loader's counter and labels — default: #0a0a0a */
    "--kz-landing-reveal-fg"?: string | number;
    /** default: rgba(10, 10, 10, 0.62) */
    "--kz-landing-reveal-muted"?: string | number;
    /** default: rgba(10, 10, 10, 0.1) */
    "--kz-landing-reveal-line"?: string | number;
    /** the hero, over its image — default: #f2f2f0 */
    "--kz-landing-reveal-text"?: string | number;
    /** default: rgba(242, 242, 240, 0.74) */
    "--kz-landing-reveal-text-muted"?: string | number;
    /** under the text, so it reads on any image — default: rgba(10, 10, 10, 0.6) */
    "--kz-landing-reveal-veil"?: string | number;
    /** default: #f2f2f0 */
    "--kz-landing-reveal-button"?: string | number;
    /** default: #0a0a0a */
    "--kz-landing-reveal-on-button"?: string | number;
    /** width of the pile — default: clamp(148px, 26vmin, 260px) */
    "--kz-landing-reveal-card"?: string | number;
    /** the hero image: 0 = edge to edge — default: 0px */
    "--kz-landing-reveal-radius"?: string | number;
    /** the hero; take your header off if it sits above — default: 100svh */
    "--kz-landing-reveal-height"?: string | number;
    /** default: 24px */
    "--kz-landing-reveal-side"?: string | number;
    /** 100 never comes sooner, even with every file cached — default: 2.2s */
    "--kz-landing-reveal-min"?: string | number;
    /** nor later: past it, the hero comes in without the late files — default: 2.8s */
    "--kz-landing-reveal-max"?: string | number;
    /** the hero image growing into place — default: cubic-bezier(0.65, 0, 0.35, 1) */
    "--kz-landing-reveal-ease"?: string | number;
  };
  className?: string;
};

export function LandingReveal({ vars, className, style, ...rest }: LandingRevealProps) {
  const ref = useRef<ComponentRef<'section'>>(null);
  const classes = ["kodza-landing-reveal", className].filter(Boolean).join(' ');
  // Everything the markup is made of: when it changes, React gets a fresh element (see the top)
  const key = JSON.stringify([classes, vars, style, rest]);

  // Starts the script on this element once React has rendered it, stops it when it goes
  useEffect(() => {
    const root = ref.current;
    let live = true;
    // @ts-ignore: a plain script, no types to find
    import('./landing-reveal.js').then(() => {
      if (live) document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root } }));
    });
    return () => {
      live = false;
      document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } }));
    };
  }, [key]);

  return (
    <section key={key} {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
      <div className="kodza-landing-reveal__loader" aria-hidden="true">
        <p className="kodza-landing-reveal__brand">off<span>·</span>by<span>·</span>one</p>
        <p className="kodza-landing-reveal__tally"><span className="kodza-landing-reveal__done">0</span>/<span className="kodza-landing-reveal__total">0</span> files</p>
        <div className="kodza-landing-reveal__deck">
          <img className="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-block-1400.webp" alt="" data-caption="display: block" width="1120" height="1400" />
          {' '}
          <img className="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-columns-1400.webp" alt="" data-caption="display: grid" width="1120" height="1400" />
          {' '}
          <img className="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-stack-1400.webp" alt="" data-caption="z-index: 9999" width="1400" height="1050" />
          {' '}
          <img className="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-float-1400.webp" alt="" data-caption="float: left" width="1400" height="933" />
          {' '}
          <img className="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-arch-1400.webp" alt="" data-caption="border-radius" width="1400" height="1400" />
        </div>
        <p className="kodza-landing-reveal__caption" />
        <p className="kodza-landing-reveal__count">000</p>
        <p className="kodza-landing-reveal__note">Loading the good bits</p>
      </div>
      <header className="kodza-landing-reveal__bar">
        <a className="kodza-landing-reveal__logo" href="#">off<span>·</span>by<span>·</span>one</a>
        <nav className="kodza-landing-reveal__nav" aria-label="Main" data-enter="down">
          <a href="#">Work</a>
          {' '}
          <a href="#">Studio</a>
          {' '}
          <a href="#">Journal</a>
        </nav>
        <a className="kodza-landing-reveal__button kodza-landing-reveal__button--small" href="#" data-enter="down">Start a project</a>
      </header>
      <div className="kodza-landing-reveal__hero">
        <p className="kodza-landing-reveal__eyebrow" data-enter="">Studio · est. 2019, give or take</p>
        <div className="kodza-landing-reveal__grid">
          <h1 className="kodza-landing-reveal__title">
            <span className="kodza-landing-reveal__line">Hello,</span>
            {' '}
            <span className="kodza-landing-reveal__line">first paint.</span>
          </h1>
          <div className="kodza-landing-reveal__side">
            <p className="kodza-landing-reveal__lead" data-enter="">Small studio, big deploys. Sites that load fast, look sharp and survive the client's nephew with admin access.</p>
            <p className="kodza-landing-reveal__actions" data-enter="">
              <a className="kodza-landing-reveal__button" href="#">See the work</a>
              {' '}
              <a className="kodza-landing-reveal__button kodza-landing-reveal__button--ghost" href="#">Book a call</a>
            </p>
          </div>
        </div>
        <figure className="kodza-landing-reveal__media">
          <img className="kodza-landing-reveal__image" src="https://kodza.dev/media/kz-steps-dark-2400.webp" alt="Concrete steps climbing out of the dark" width="2400" height="1600" data-caption="steps(5, end)" />
          <figcaption data-enter="">
            <span>The last image on the pile</span>
            <span>steps(5, end)</span>
          </figcaption>
        </figure>
      </div>
    </section>
  );
}
```

## Code

```html
<section class="kodza-landing-reveal">
  <div class="kodza-landing-reveal__loader" aria-hidden="true">
    <p class="kodza-landing-reveal__brand">off<span>·</span>by<span>·</span>one</p>
    <p class="kodza-landing-reveal__tally"><span class="kodza-landing-reveal__done">0</span>/<span class="kodza-landing-reveal__total">0</span> files</p>
    <div class="kodza-landing-reveal__deck">
      <img class="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-block-1400.webp" alt="" data-caption="display: block" width="1120" height="1400">
      <img class="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-columns-1400.webp" alt="" data-caption="display: grid" width="1120" height="1400">
      <img class="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-stack-1400.webp" alt="" data-caption="z-index: 9999" width="1400" height="1050">
      <img class="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-float-1400.webp" alt="" data-caption="float: left" width="1400" height="933">
      <img class="kodza-landing-reveal__card" src="https://kodza.dev/media/kz-arch-1400.webp" alt="" data-caption="border-radius" width="1400" height="1400">
    </div>
    <p class="kodza-landing-reveal__caption"></p>
    <p class="kodza-landing-reveal__count">000</p>
    <p class="kodza-landing-reveal__note">Loading the good bits</p>
  </div>
  <header class="kodza-landing-reveal__bar">
    <a class="kodza-landing-reveal__logo" href="#">off<span>·</span>by<span>·</span>one</a>
    <nav class="kodza-landing-reveal__nav" aria-label="Main" data-enter="down">
      <a href="#">Work</a>
      <a href="#">Studio</a>
      <a href="#">Journal</a>
    </nav>
    <a class="kodza-landing-reveal__button kodza-landing-reveal__button--small" href="#" data-enter="down">Start a project</a>
  </header>
  <div class="kodza-landing-reveal__hero">
    <p class="kodza-landing-reveal__eyebrow" data-enter>Studio · est. 2019, give or take</p>
    <div class="kodza-landing-reveal__grid">
      <h1 class="kodza-landing-reveal__title">
        <span class="kodza-landing-reveal__line">Hello,</span>
        <span class="kodza-landing-reveal__line">first paint.</span>
      </h1>
      <div class="kodza-landing-reveal__side">
        <p class="kodza-landing-reveal__lead" data-enter>Small studio, big deploys. Sites that load fast, look sharp and survive the client's nephew with admin access.</p>
        <p class="kodza-landing-reveal__actions" data-enter>
          <a class="kodza-landing-reveal__button" href="#">See the work</a>
          <a class="kodza-landing-reveal__button kodza-landing-reveal__button--ghost" href="#">Book a call</a>
        </p>
      </div>
    </div>
    <figure class="kodza-landing-reveal__media">
      <img class="kodza-landing-reveal__image" src="https://kodza.dev/media/kz-steps-dark-2400.webp" alt="Concrete steps climbing out of the dark" width="2400" height="1600" data-caption="steps(5, end)">
      <figcaption data-enter><span>The last image on the pile</span><span>steps(5, end)</span></figcaption>
    </figure>
  </div>
</section>
```

```css
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Landing Reveal
   A preloader that counts the files your hero really needs, while its images rise one by one
   onto a pile, each with its caption (data-caption). At 100 the hero image comes up last and grows
   until it fills the hero, the title rises from its mask, then the rest comes in. The logo is there
   from the first frame, where the hero has it. A click or any key skips it. Mark what comes in
   after the title with data-enter (data-enter="down" for what drops from the top); add data-once
   to play it only once per visit. Everything is tuned with the --kz-landing-reveal-* variables. */
.kodza-landing-reveal {
  --kz-landing-reveal-bg: #ffffff; /* the loader's paper: your page colour, so the hand-over is seamless */
  --kz-landing-reveal-fg: #0a0a0a; /* the loader's counter and labels */
  --kz-landing-reveal-muted: rgba(10, 10, 10, 0.62);
  --kz-landing-reveal-line: rgba(10, 10, 10, 0.1);
  --kz-landing-reveal-text: #f2f2f0; /* the hero, over its image */
  --kz-landing-reveal-text-muted: rgba(242, 242, 240, 0.74);
  --kz-landing-reveal-veil: rgba(10, 10, 10, 0.6); /* under the text, so it reads on any image */
  --kz-landing-reveal-button: #f2f2f0;
  --kz-landing-reveal-on-button: #0a0a0a;
  --kz-landing-reveal-card: clamp(148px, 26vmin, 260px); /* width of the pile */
  --kz-landing-reveal-radius: 0px; /* the hero image: 0 = edge to edge */
  --kz-landing-reveal-height: 100svh; /* the hero; take your header off if it sits above */
  --kz-landing-reveal-side: 24px;
  --kz-landing-reveal-min: 2.2s; /* 100 never comes sooner, even with every file cached */
  --kz-landing-reveal-max: 2.8s; /* nor later: past it, the hero comes in without the late files */
  --kz-landing-reveal-ease: cubic-bezier(0.65, 0, 0.35, 1); /* the hero image growing into place */

  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  min-height: var(--kz-landing-reveal-height);
  color: var(--kz-landing-reveal-text);
  text-align: left;
}

/* ---------- The loader: over the whole screen, from the very first paint ---------- */
/* Its rules never depend on the hero: while it plays, it waits at the end of the page */
.kodza-landing-reveal__loader {
  position: fixed;
  inset: 0;
  z-index: 100000;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto 1fr auto;
  align-items: end;
  gap: 16px;
  box-sizing: border-box;
  margin: 0;
  padding: 20px var(--kz-landing-reveal-side, 24px);
  overflow: hidden;
  color: var(--kz-landing-reveal-fg, #0a0a0a);
  font-size: 15px;
  line-height: 1.4;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* The paper, apart from what lies on it, so it can lift while the last card grows */
.kodza-landing-reveal__loader::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--kz-landing-reveal-bg, #ffffff);
}

.kodza-landing-reveal__loader[hidden] {
  display: none;
}

/* No script (blocked, or very late): the page is never kept behind the loader */
.kodza-landing-reveal:not(.is-ready) .kodza-landing-reveal__loader {
  animation: kodza-landing-reveal-failsafe 0.5s 3s both;
}

/* data-once, already seen this visit: the script says so before the first paint */
[data-kodza-landing-reveal="seen"] .kodza-landing-reveal[data-once] .kodza-landing-reveal__loader {
  display: none;
}

/* A hero that comes after the first one (an app's next page, a theme's Ajax content): straight
   on the hero, without a flash of loader. The script says so once the first one has been through */
[data-kodza-landing-reveal="played"] .kodza-landing-reveal:not(.is-ready) .kodza-landing-reveal__loader {
  display: none;
}

@keyframes kodza-landing-reveal-failsafe {
  to {
    visibility: hidden;
    opacity: 0;
  }
}

.kodza-landing-reveal__caption,
.kodza-landing-reveal__tally,
.kodza-landing-reveal__note,
.kodza-landing-reveal__eyebrow,
.kodza-landing-reveal__media figcaption {
  font-family: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0;
}

.kodza-landing-reveal__tally,
.kodza-landing-reveal__note,
.kodza-landing-reveal__count {
  position: relative;
  z-index: 1; /* above the cards on their way in */
  margin: 0;
}

/* The hero's logo, where the hero has it, from the very first frame (the script puts it on the
   pixel). White in difference: dark on the paper, light once the hero image is under it */
.kodza-landing-reveal__brand {
  position: absolute;
  top: 28px;
  left: var(--kz-landing-reveal-side, 24px);
  z-index: 3;
  margin: 0;
  color: #ffffff;
  font-size: 20px;
  font-weight: 600;
  line-height: normal;
  letter-spacing: -0.04em;
  white-space: nowrap;
  mix-blend-mode: difference;
}

.kodza-landing-reveal__brand span {
  opacity: 0.6;
}

.kodza-landing-reveal__tally {
  grid-column: 2;
  grid-row: 1;
  align-self: start;
  justify-self: end;
  color: var(--kz-landing-reveal-muted, rgba(10, 10, 10, 0.62));
  font-variant-numeric: tabular-nums;
}

/* What the script brings in waits out of sight until it starts counting */
.kodza-landing-reveal__loader:not(.is-counting) .kodza-landing-reveal__tally,
.kodza-landing-reveal__loader:not(.is-counting) .kodza-landing-reveal__count,
.kodza-landing-reveal__loader:not(.is-counting) .kodza-landing-reveal__note,
.kodza-landing-reveal__loader:not(.is-counting) .kodza-landing-reveal__caption {
  visibility: hidden;
}

/* Under the pile, the caption of the image on top: each new one rolls in from below */
.kodza-landing-reveal__caption {
  position: absolute;
  top: calc(50% + var(--kz-landing-reveal-card, 220px) * 0.625 + 14px);
  left: calc(50% - var(--kz-landing-reveal-card, 220px) / 2);
  z-index: 1;
  display: grid;
  width: var(--kz-landing-reveal-card, 220px);
  margin: 0;
  overflow: hidden;
  color: var(--kz-landing-reveal-muted, rgba(10, 10, 10, 0.62));
}

.kodza-landing-reveal__caption span {
  grid-area: 1 / 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transform: translateY(110%);
}

.kodza-landing-reveal__note {
  grid-column: 2;
  grid-row: 3;
  padding-bottom: 0.5em;
  color: var(--kz-landing-reveal-muted, rgba(10, 10, 10, 0.62));
  text-align: right;
}

/* 000 to 100: one wheel per digit, rolled like an odometer */
.kodza-landing-reveal__count {
  grid-column: 1;
  grid-row: 3;
  display: flex;
  font-size: clamp(80px, 13vw, 176px);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.06em;
  font-variant-numeric: tabular-nums;
}

.kodza-landing-reveal__wheel {
  display: block;
  height: 1em;
  overflow: hidden;
  overflow: visible clip; /* the roll is masked, never the side of a digit (tight spacing) */
}

.kodza-landing-reveal__digits {
  display: block;
  white-space: pre;
  will-change: transform;
}

/* The pile, in the middle of the screen: the images rise one over the other from an invisible
   edge, like lines of text from their mask */
.kodza-landing-reveal__deck {
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--kz-landing-reveal-card, 220px);
  aspect-ratio: 4 / 5;
  translate: -50% -50%;
}

.kodza-landing-reveal__card {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  margin: 0;
  border-radius: 10px;
  object-fit: cover;
  background: var(--kz-landing-reveal-bg, #ffffff); /* opaque even before the image paints */
  clip-path: inset(0 0 100% 0); /* below its edge until the script brings it up */
}

/* The hero image, copied into the loader: it comes up last onto the pile, then grows into place */
.kodza-landing-reveal__last {
  position: absolute;
  display: block;
  max-width: none;
  margin: 0;
  object-fit: cover;
  background: var(--kz-landing-reveal-bg, #ffffff);
  transform-origin: 50% 50%;
}

.kodza-landing-reveal__last[hidden] {
  display: none;
}

/* ---------- The hero: its image fills it, the text sits on it ---------- */
.kodza-landing-reveal__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 20px var(--kz-landing-reveal-side);
}

.kodza-landing-reveal__logo {
  border-radius: 4px;
  color: inherit;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.04em;
  text-decoration: none;
  white-space: nowrap;
}

.kodza-landing-reveal__logo span {
  color: var(--kz-landing-reveal-text-muted);
}

.kodza-landing-reveal__nav {
  display: flex;
  gap: 2px;
}

.kodza-landing-reveal__nav a {
  display: inline-flex;
  align-items: center;
  height: 40px;
  padding: 0 14px;
  border-radius: 999px;
  color: inherit;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.01em;
  text-decoration: none;
  transition: background-color 0.2s ease;
}

.kodza-landing-reveal__nav a:hover {
  background: color-mix(in srgb, currentColor 14%, transparent);
}

.kodza-landing-reveal__button {
  display: inline-flex;
  align-items: center;
  height: 48px;
  padding: 0 22px;
  border-radius: 999px;
  background: var(--kz-landing-reveal-button);
  color: var(--kz-landing-reveal-on-button);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.01em;
  text-decoration: none;
  white-space: nowrap;
  transition: opacity 0.2s ease;
}

.kodza-landing-reveal__button:hover {
  opacity: 0.86;
}

.kodza-landing-reveal__button--small {
  height: 40px;
  padding: 0 18px;
}

.kodza-landing-reveal__button--ghost {
  background: transparent;
  color: inherit;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 40%, transparent);
}

.kodza-landing-reveal__logo:focus-visible,
.kodza-landing-reveal__nav a:focus-visible,
.kodza-landing-reveal__button:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

/* The text at the bottom, above the caption row */
.kodza-landing-reveal__hero {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 32px var(--kz-landing-reveal-side) 96px;
}

.kodza-landing-reveal__eyebrow {
  margin: 0 0 20px;
  color: var(--kz-landing-reveal-text-muted);
}

.kodza-landing-reveal__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  align-items: end;
  gap: 28px 64px;
}

.kodza-landing-reveal__title {
  margin: 0;
  font-size: clamp(44px, min(8vw, 14vh), 116px); /* the hero fits a short screen too */
  font-weight: 600;
  line-height: 0.92;
  letter-spacing: -0.055em;
}

/* Each line rises from behind its own edge */
.kodza-landing-reveal__line {
  display: block;
}

.kodza-landing-reveal__side {
  display: grid;
  justify-items: start;
  gap: 24px;
  padding-bottom: 4px;
}

.kodza-landing-reveal__lead {
  max-width: 30em;
  margin: 0;
  color: var(--kz-landing-reveal-text-muted);
  font-size: clamp(16px, 1.4vw, 18px);
  line-height: 1.5;
  text-wrap: pretty;
}

.kodza-landing-reveal__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0;
}

/* The image fills the whole hero, under a veil that keeps the text legible */
.kodza-landing-reveal__media {
  position: absolute;
  inset: 0;
  z-index: -1;
  margin: 0;
}

.kodza-landing-reveal__media::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--kz-landing-reveal-radius);
  background:
    linear-gradient(to top, var(--kz-landing-reveal-veil), color-mix(in srgb, var(--kz-landing-reveal-veil) 75%, transparent) 38%, transparent 74%),
    linear-gradient(to bottom, var(--kz-landing-reveal-veil), transparent 22%);
  pointer-events: none;
}

.kodza-landing-reveal__image {
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  margin: 0;
  border-radius: var(--kz-landing-reveal-radius);
  object-fit: cover;
}

/* Hidden while its copy plays the last card, shown the moment the copy lands on it */
.kodza-landing-reveal.is-intro:not(.is-landed) .kodza-landing-reveal__image {
  visibility: hidden;
}

.kodza-landing-reveal__media figcaption {
  position: absolute;
  right: var(--kz-landing-reveal-side);
  bottom: 20px;
  left: var(--kz-landing-reveal-side);
  z-index: 1;
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding-top: 12px;
  border-top: 1px solid color-mix(in srgb, currentColor 22%, transparent);
  color: var(--kz-landing-reveal-text-muted);
}

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

@media (max-width: 640px) {
  .kodza-landing-reveal__nav {
    display: none;
  }

  .kodza-landing-reveal__media figcaption span + span {
    display: none;
  }
}

/* Reduced motion: no intro, the hero straight away */
@media (prefers-reduced-motion: reduce) {
  .kodza-landing-reveal__loader {
    display: none;
  }

  .kodza-landing-reveal__nav a,
  .kodza-landing-reveal__button {
    transition-duration: 0.01ms !important;
  }
}

/* Theming example — on a dark site, the loader's paper turns black:
.dark-site .kodza-landing-reveal {
  --kz-landing-reveal-bg: #0a0a0a;
  --kz-landing-reveal-fg: #f2f2f0;
  --kz-landing-reveal-muted: rgba(242, 242, 240, 0.62);
  --kz-landing-reveal-line: rgba(242, 242, 240, 0.14);
}
*/

/* kodza-guard a4777eb8b884 — generated by Kodza, do not edit.
   Theme guard: keeps a site's own CSS (WordPress themes, Bootstrap, CSS resets…) off this
   component. :not(#_) is always true and weighs as much as an id, so these lines win over
   theme rules made of classes and tags. Customise the component through its --kz-* variables. */

/* What the browser gives, where the component sets it only in some states: at the lowest weight, so its own rules below put it back */

:where(.kodza-landing-reveal__logo):not(#_) {
  outline: revert;
  outline-offset: revert;
}

:where(.kodza-landing-reveal__button):not(#_) {
  outline: revert;
  box-shadow: revert;
  outline-offset: revert;
}

:where(.kodza-landing-reveal__nav a:where(:not([class*="kodza-"]))):not(#_) {
  outline: revert;
  background-color: revert;
  background-image: revert;
  outline-offset: revert;
}

/* What the browser gives, on everything the component leaves to it */

.kodza-landing-reveal__bar:not(#_) {
  border: revert;
  margin: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__logo:not(#_) {
  border: revert;
  margin: revert;
  padding: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-family: revert;
  font-style: revert;
  line-height: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__nav:not(#_) {
  border: revert;
  margin: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__button:not(#_) {
  border: revert;
  margin: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-family: revert;
  font-style: revert;
  line-height: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__hero:not(#_),
.kodza-landing-reveal__grid:not(#_),
.kodza-landing-reveal__line:not(#_),
.kodza-landing-reveal__side:not(#_),
.kodza-landing-reveal__done:not(#_),
.kodza-landing-reveal__total:not(#_),
.kodza-landing-reveal__deck:not(#_),
.kodza-landing-reveal__wheel:not(#_),
.kodza-landing-reveal__digits:not(#_),
.kodza-landing-reveal__logo span:where(:not([class*="kodza-"])):not(#_),
.kodza-landing-reveal__media span:where(:not([class*="kodza-"]):not(.kodza-landing-reveal__media [class*="kodza-"] *)):not(#_),
.kodza-landing-reveal__brand span:where(:not([class*="kodza-"])):not(#_),
.kodza-landing-reveal__caption span:where(:not([class*="kodza-"])):not(#_) {
  box-sizing: revert;
}

.kodza-landing-reveal__eyebrow:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-style: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__title:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-style: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__lead:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-height: revert;
  font-family: revert;
  font-weight: revert;
  font-style: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__actions:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__media:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__image:not(#_) {
  border: revert;
  padding: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__brand:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-family: revert;
  font-style: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__tally:not(#_),
.kodza-landing-reveal__caption:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-style: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__card:not(#_) {
  border: revert;
  padding: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__count:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-style: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__note:not(#_) {
  border: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  padding-top: revert;
  padding-right: revert;
  padding-left: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-style: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__last:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__nav a:where(:not([class*="kodza-"])):not(#_) {
  border: revert;
  margin: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  box-shadow: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-family: revert;
  font-style: revert;
  line-height: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__media figcaption:where(:not([class*="kodza-"]):not(.kodza-landing-reveal__media [class*="kodza-"] *)):not(#_) {
  margin: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  padding-right: revert;
  padding-bottom: revert;
  padding-left: revert;
  border-right-width: revert;
  border-bottom-width: revert;
  border-left-width: revert;
  border-right-style: revert;
  border-bottom-style: revert;
  border-left-style: revert;
  border-right-color: revert;
  border-bottom-color: revert;
  border-left-color: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-style: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-landing-reveal__logo:not(#_):where(:not([hidden])),
.kodza-landing-reveal__eyebrow:not(#_):where(:not([hidden])),
.kodza-landing-reveal__title:not(#_):where(:not([hidden])),
.kodza-landing-reveal__lead:not(#_):where(:not([hidden])),
.kodza-landing-reveal__media:not(#_):where(:not([hidden])),
.kodza-landing-reveal__brand:not(#_):where(:not([hidden])),
.kodza-landing-reveal__tally:not(#_):where(:not([hidden])),
.kodza-landing-reveal__note:not(#_):where(:not([hidden])) {
  display: revert;
}

.kodza-landing-reveal__logo:not(#_):where([hidden]),
.kodza-landing-reveal__eyebrow:not(#_):where([hidden]),
.kodza-landing-reveal__title:not(#_):where([hidden]),
.kodza-landing-reveal__lead:not(#_):where([hidden]),
.kodza-landing-reveal__media:not(#_):where([hidden]),
.kodza-landing-reveal__brand:not(#_):where([hidden]),
.kodza-landing-reveal__tally:not(#_):where([hidden]),
.kodza-landing-reveal__note:not(#_):where([hidden]) {
  display: none;
}

.kodza-landing-reveal__loader:not(#_)::before,
.kodza-landing-reveal__media:not(#_)::after {
  box-sizing: revert;
}

/* The component's own rules, one step stronger: real buttons and fields, what it sets on tags themes style (links, lists, headings, images…), and the root width (a theme may narrow it, never widen it) */

.kodza-landing-reveal__caption:not(#_),
.kodza-landing-reveal__tally:not(#_),
.kodza-landing-reveal__note:not(#_),
.kodza-landing-reveal__eyebrow:not(#_),
.kodza-landing-reveal__media figcaption:not(#_) {
  font-family: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0;
}

.kodza-landing-reveal__tally:not(#_),
.kodza-landing-reveal__note:not(#_),
.kodza-landing-reveal__count:not(#_) {
  position: relative;
  margin: 0;
}

.kodza-landing-reveal__brand:not(#_) {
  position: absolute;
  margin: 0;
  color: #ffffff;
  font-size: 20px;
  font-weight: 600;
  line-height: normal;
  letter-spacing: -0.04em;
}

.kodza-landing-reveal__tally:not(#_) {
  color: var(--kz-landing-reveal-muted, rgba(10, 10, 10, 0.62));
}

.kodza-landing-reveal__caption:not(#_) {
  position: absolute;
  display: grid;
  margin: 0;
  color: var(--kz-landing-reveal-muted, rgba(10, 10, 10, 0.62));
}

.kodza-landing-reveal__note:not(#_) {
  padding-bottom: 0.5em;
  color: var(--kz-landing-reveal-muted, rgba(10, 10, 10, 0.62));
  text-align: right;
}

.kodza-landing-reveal__count:not(#_) {
  display: flex;
  font-size: clamp(80px, 13vw, 176px);
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.06em;
}

.kodza-landing-reveal__card:not(#_) {
  position: absolute;
  display: block;
  max-width: none;
  margin: 0;
  border-radius: 10px;
  background: var(--kz-landing-reveal-bg, #ffffff);
}

.kodza-landing-reveal__last:not(#_) {
  position: absolute;
  display: block;
  max-width: none;
  margin: 0;
  background: var(--kz-landing-reveal-bg, #ffffff);
}

.kodza-landing-reveal__last[hidden]:not(#_) {
  display: none;
}

.kodza-landing-reveal__bar:not(#_) {
  display: flex;
  padding: 20px var(--kz-landing-reveal-side);
}

.kodza-landing-reveal__logo:not(#_) {
  border-radius: 4px;
  color: inherit;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.04em;
  text-decoration: none;
}

.kodza-landing-reveal__nav:not(#_) {
  display: flex;
}

.kodza-landing-reveal__nav a:not(#_) {
  display: inline-flex;
  padding: 0 14px;
  border-radius: 999px;
  color: inherit;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.01em;
  text-decoration: none;
  transition: background-color 0.2s ease;
}

.kodza-landing-reveal__nav a:hover:not(#_) {
  background: color-mix(in srgb, currentColor 14%, transparent);
}

.kodza-landing-reveal__button:not(#_) {
  display: inline-flex;
  padding: 0 22px;
  border-radius: 999px;
  background: var(--kz-landing-reveal-button);
  color: var(--kz-landing-reveal-on-button);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.01em;
  text-decoration: none;
  transition: opacity 0.2s ease;
}

.kodza-landing-reveal__button--small:not(#_) {
  padding: 0 18px;
}

.kodza-landing-reveal__button--ghost:not(#_) {
  background: transparent;
  color: inherit;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 40%, transparent);
}

.kodza-landing-reveal__logo:focus-visible:not(#_),
.kodza-landing-reveal__nav a:focus-visible:not(#_),
.kodza-landing-reveal__button:focus-visible:not(#_) {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

.kodza-landing-reveal__eyebrow:not(#_) {
  margin: 0 0 20px;
  color: var(--kz-landing-reveal-text-muted);
}

.kodza-landing-reveal__title:not(#_) {
  margin: 0;
  font-size: clamp(44px, min(8vw, 14vh), 116px);
  font-weight: 600;
  line-height: 0.92;
  letter-spacing: -0.055em;
}

.kodza-landing-reveal__lead:not(#_) {
  max-width: 30em;
  margin: 0;
  color: var(--kz-landing-reveal-text-muted);
  font-size: clamp(16px, 1.4vw, 18px);
  line-height: 1.5;
}

.kodza-landing-reveal__actions:not(#_) {
  display: flex;
  margin: 0;
}

.kodza-landing-reveal__media:not(#_) {
  position: absolute;
  margin: 0;
}

.kodza-landing-reveal__image:not(#_) {
  display: block;
  max-width: none;
  margin: 0;
  border-radius: var(--kz-landing-reveal-radius);
}

.kodza-landing-reveal__media figcaption:not(#_) {
  position: absolute;
  display: flex;
  padding-top: 12px;
  border-top: 1px solid color-mix(in srgb, currentColor 22%, transparent);
  color: var(--kz-landing-reveal-text-muted);
}

@media (max-width: 640px) {
  .kodza-landing-reveal__nav:not(#_) {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .kodza-landing-reveal__nav a:not(#_),
  .kodza-landing-reveal__button:not(#_) {
    transition-duration: 0.01ms !important;
  }
}
```

```js
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Landing Reveal
// A preloader for the first visit: it counts the files the hero really needs (its images,
// decoded, and the fonts) while images come up one by one onto a pile. At 100 the hero image
// comes up last and grows into its place, the title rises from its mask, then the rest comes in.
// A click or any key skips it; data-once plays it once per visit; "kodza:replay" plays it again.
// A script that comes after the page (Next) still plays it; the heroes that come later (an app's
// next page) open straight on the hero.
// Built in three parts, so the other reveals of the family only swap the middle one:
//   1. counting  — real progress, paced between a minimum and a maximum time
//   2. hand-over — this reveal's own move (here: the pile, then its last image into the hero)
//   3. entrance  — the title lines, then everything marked data-enter
(() => {
  const SELECTOR = '.kodza-landing-reveal';
  const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
  const RISE = 'cubic-bezier(0.22, 1, 0.36, 1)';
  const SWING = 'cubic-bezier(0.65, 0, 0.35, 1)';
  const SEEN = 'kodza-landing-reveal'; // sessionStorage key, for data-once
  const VARS = ['bg', 'fg', 'muted', 'line', 'card', 'side']; // what the loader takes along when it leaves the hero
  const HOLD_KEYS = [' ', 'PageUp', 'PageDown', 'Home', 'End', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
  const BEAT = 600; // one image every 0.6 s: each one is seen, then covered
  const LAST = 820; // the hero image rises onto the pile while the count ends: 100 as it settles
  const inOut = (p) => (p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2); // SWING, in JS
  let playing = null; // one intro at a time on a page

  // Seen already this visit: the loader of a data-once hero must not show, not even for a frame
  try {
    if (sessionStorage.getItem(SEEN)) document.documentElement.setAttribute('data-kodza-landing-reveal', 'seen');
  } catch {
    // storage blocked: the intro simply plays
  }

  const seconds = (value, fallback) => {
    const n = parseFloat(value);
    if (!isFinite(n)) return fallback;
    return /ms$/.test(value) ? n : n * 1000;
  };

  // The counter: one wheel per digit (0 to 9, then 0 again), turned like an odometer: a digit
  // only moves on when the one to its right goes from 9 to 0
  const wheels = (count) => {
    count.textContent = '';
    return [0, 1, 2].map(() => {
      const wheel = document.createElement('span');
      const digits = document.createElement('span');
      wheel.className = 'kodza-landing-reveal__wheel';
      digits.className = 'kodza-landing-reveal__digits';
      digits.textContent = '0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n0';
      wheel.append(digits);
      count.append(wheel);
      return digits;
    });
  };

  const roll = (digits, value) => {
    // Each number holds, then clicks over to the next one: legible even while it counts
    const whole = Math.floor(value);
    const step = Math.max(0, (value - whole - 0.6) / 0.4);
    const ones = (whole + step * step * (3 - 2 * step)) % 10;
    const carry = Math.max(0, ones - 9);
    const turns = [value >= 100 ? 1 : Math.max(0, value - 99), (Math.floor(value / 10) % 10) + carry, ones];
    digits.forEach((strip, i) => {
      strip.style.transform = `translateY(${(-turns[i]).toFixed(3)}em)`;
    });
  };

  // One instance. Everything it starts is tied to `signal`, so kodza:destroy stops it all
  function mount(root, signal) {
    const loader = root.querySelector('.kodza-landing-reveal__loader');
    if (!loader) return;
    const image = root.querySelector('.kodza-landing-reveal__image');
    const media = root.querySelector('.kodza-landing-reveal__media');
    const deck = loader.querySelector('.kodza-landing-reveal__deck');
    const cards = [...loader.querySelectorAll('.kodza-landing-reveal__card')];
    const count = loader.querySelector('.kodza-landing-reveal__count');
    const done = loader.querySelector('.kodza-landing-reveal__done');
    const total = loader.querySelector('.kodza-landing-reveal__total');
    const small = [...loader.querySelectorAll('.kodza-landing-reveal__tally, .kodza-landing-reveal__note, .kodza-landing-reveal__caption')];
    const brand = loader.querySelector('.kodza-landing-reveal__brand');
    const logo = root.querySelector('.kodza-landing-reveal__logo');
    const caption = loader.querySelector('.kodza-landing-reveal__caption');
    const lines = [...root.querySelectorAll('.kodza-landing-reveal__line')];
    const items = [...root.querySelectorAll('[data-enter]')].filter((el) => !loader.contains(el));
    const digits = count ? wheels(count) : [];
    // One caption per image of the pile (data-caption), the hero image's last
    const notes = caption ? [...cards, image].map((img) => {
      const span = document.createElement('span');
      span.textContent = img?.dataset.caption || '';
      caption.append(span);
      return span;
    }) : [];
    const home = [loader.parentNode, loader.nextSibling];
    const css = (name) => getComputedStyle(root).getPropertyValue(`--kz-landing-reveal-${name}`).trim();

    // The last card is the hero image itself, copied: same file, same crop, so the hand-over
    // ends exactly on it
    let last = null;
    if (image) {
      last = image.cloneNode(false);
      last.className = 'kodza-landing-reveal__last';
      last.alt = '';
      last.removeAttribute('loading');
      last.hidden = true;
      loader.append(last);
    }

    let run = null; // the intro being played

    // The page stays where it is while the loader is up: the wheel, a swipe or the scroll keys
    // move nothing. A dragged scrollbar is put back
    loader.addEventListener('wheel', (event) => event.preventDefault(), { passive: false, signal });
    loader.addEventListener('touchmove', (event) => event.preventDefault(), { passive: false, signal });
    loader.addEventListener('click', () => skip(), { signal });
    window.addEventListener('scroll', () => {
      if (run?.hold && (scrollX !== run.hold[0] || scrollY !== run.hold[1])) window.scrollTo({ left: run.hold[0], top: run.hold[1], behavior: 'instant' });
    }, { signal });
    // Any key skips it (shortcuts with Ctrl, Cmd or Alt still work, and Tab still moves on)
    document.addEventListener('keydown', (event) => {
      if (!run || event.ctrlKey || event.metaKey || event.altKey || /^(Shift|Control|Alt|Meta|CapsLock|Fn)$/.test(event.key)) return;
      if (run.hold && HOLD_KEYS.includes(event.key)) event.preventDefault();
      skip();
    }, { signal });
    // Once the loader is gone, a click anywhere hurries the entrance along
    document.addEventListener('pointerdown', () => run?.phase === 'enter' && skip(), { signal });
    root.addEventListener('kodza:replay', () => !motion.matches && start(true), { signal });
    // Back from another page (bfcache) halfway through: the hero as it ends up
    window.addEventListener('pageshow', (event) => event.persisted && run && end(), { signal });

    // ---------- 1. Counting ----------

    // What the hero really waits for: its images (decoded if they are still coming, so they
    // paint at once) and the fonts. A file counts once it is in, or once it failed: a broken one
    // never holds the page
    const track = () => {
      const jobs = [...cards, image].filter(Boolean).map((img) => (img.complete && img.naturalWidth ? Promise.resolve() : img.decode()));
      const token = run;
      if (document.fonts) {
        jobs.push(document.fonts.ready);
        document.fonts.ready.then(() => run === token && fit()); // the logo moves with its font
      }
      jobs.forEach((job) => job.then(() => {}, () => {}).then(() => {
        if (run === token) run.done++;
      }));
      if (total) total.textContent = String(jobs.length);
      if (done) done.textContent = '0';
      return jobs.length;
    };

    // The number follows the files and is never ahead of them. Once they are all in (and not
    // before the minimum, nor after the maximum), the hero image rises onto the pile and the
    // count ends with it: 100 the moment it settles
    const tick = (now) => {
      if (!run || (run.phase !== 'load' && run.phase !== 'last')) return;
      run.t0 = run.t0 || now; // a tab opened in the background starts counting when it is shown
      const t = now - run.t0;
      const dt = Math.min(64, now - (run.at || now));
      run.at = now;
      if (run.phase === 'load') {
        const pace = 1 - Math.pow(1 - Math.min(1, t / run.min), 2);
        const target = Math.floor(Math.min(run.done / run.total, pace) * 100) / 100;
        run.value += (target - run.value) * (1 - Math.exp(-dt / 110));
        if (Math.abs(target - run.value) < 0.004) run.value = target; // waiting for a file: a whole number, not a digit half rolled
        const ready = run.done >= run.total && t >= run.min - LAST;
        // (never on top of an image still rising, unless the maximum is reached)
        if ((ready && now >= run.beat - BEAT * 0.25) || t >= run.max - LAST) {
          if (!rise(now)) return;
        } else if (now >= run.beat) {
          reveal(cards[run.next % cards.length]);
          run.beat = now + BEAT;
        }
      } else {
        const p = Math.min(1, (now - run.from[1]) / LAST);
        run.value = run.from[0] + (1 - run.from[0]) * inOut(p);
      }
      roll(digits, run.value * 100);
      // The files shown keep up with the number, so the two never contradict each other
      const shown = Math.min(run.done, Math.floor(run.value * run.total + 1e-6));
      if (done && shown !== run.shown) done.textContent = String((run.shown = shown));
      if (run.phase === 'last' && run.value >= 1) {
        handover();
        return;
      }
      run.frame = requestAnimationFrame(tick);
    };

    // ---------- 2. Hand-over: the pile ----------

    const stack = () => run.order.forEach((card, i) => (card.style.zIndex = String(run.order.length - i)));

    // One pile: each image rises over the one before from behind its own bottom edge, like a line
    // of text from its mask (no frame, nothing shows before it). `lead`: a head start to wait for
    const reveal = (card, lead = 0) => {
      if (!card) return;
      run.order = [card, ...run.order.filter((other) => other !== card)]; // top first
      stack();
      run.next++;
      say(cards.indexOf(card), lead);
      // (the edge it rises from is as round as the image: the pile below keeps its corners)
      const round = `round ${parseFloat(getComputedStyle(card).borderTopLeftRadius) || 0}px`;
      run.anims.push(card.animate([
        { transform: 'translateY(100%)', clipPath: `inset(0% 0% 100% 0% ${round})` },
        { transform: 'translateY(0%)', clipPath: `inset(0% 0% 0% 0% ${round})` },
      ], { duration: 800, delay: lead, easing: SWING, fill: 'both' }));
    };

    const add = (el, frames, delay, duration, easing = RISE, extra = {}) => {
      const animation = el.animate(frames, { delay, duration, easing, fill: 'both', ...extra });
      run.anims.push(animation);
      return animation;
    };

    // The caption under the pile rolls with the image: the old one up, the new one from below
    const say = (i, lead = 0) => {
      const next = notes[i];
      if (!next || !next.textContent) return;
      const before = notes[run.said];
      if (before && before !== next) add(before, [{ transform: 'translateY(0%)' }, { transform: 'translateY(-110%)' }], lead, 800, SWING);
      add(next, [{ transform: 'translateY(110%)' }, { transform: 'translateY(0%)' }], lead, 800, SWING);
      run.said = i;
    };

    // The loader's own entrance: the digits rise from their masks one by one, then the small print
    const arrive = (lead = 0) => {
      const up = (el, delay) => el && add(el, [
        { transform: 'translateY(100%)', clipPath: 'inset(0% -30% 100% -30%)' },
        { transform: 'translateY(0%)', clipPath: 'inset(0% -30% 0% -30%)' },
      ], lead + delay, 900);
      digits.forEach((strip, i) => up(strip.parentNode, 80 + i * 70));
      small.filter((el) => el !== caption).forEach((el, i) => up(el, 240 + i * 80));
    };

    // The hero's logo, on the pixel where the hero has it: the header is there from the start
    const fit = () => {
      if (!brand || !logo) return;
      brand.innerHTML = logo.innerHTML;
      const box = logo.getBoundingClientRect();
      const frame = loader.getBoundingClientRect();
      const k = loader.offsetWidth ? frame.width / loader.offsetWidth : 1;
      const style = getComputedStyle(logo);
      Object.assign(brand.style, {
        left: `${(box.left - frame.left) / k}px`,
        top: `${(box.top - frame.top) / k}px`,
        fontFamily: style.fontFamily,
        fontSize: style.fontSize,
        fontWeight: style.fontWeight,
        letterSpacing: style.letterSpacing,
      });
    };

    // The last image of the pile is the hero image itself: its copy sits on the hero image,
    // scaled down and cut to the size of the frame (the same crop as object-fit: cover there),
    // and rises like the others while the count ends
    const rise = (now) => {
      const box = image?.getBoundingClientRect();
      if (!last || !box || !box.width || !box.height) {
        skip();
        return false;
      }
      run.phase = 'last';
      run.from = [run.value, now];
      const frame = loader.getBoundingClientRect();
      const k = loader.offsetWidth ? frame.width / loader.offsetWidth : 1; // a scaled page
      const spot = deck.getBoundingClientRect();
      const w = box.width / k;
      const h = box.height / k;
      const cw = spot.width / k;
      const ch = spot.height / k;
      const s = Math.max(cw / w, ch / h);
      const dx = (spot.left + spot.width / 2 - box.left - box.width / 2) / k;
      const dy = (spot.top + spot.height / 2 - box.top - box.height / 2) / k;
      const round = parseFloat(getComputedStyle(cards[0] || deck).borderTopLeftRadius) || 0;
      Object.assign(last.style, {
        left: `${(box.left - frame.left) / k}px`,
        top: `${(box.top - frame.top) / k}px`,
        width: `${w}px`,
        height: `${h}px`,
        objectPosition: getComputedStyle(image).objectPosition,
        zIndex: '1',
      });
      last.hidden = false;
      // On the pile, `below` (0 to 1) of its height still under the edge it rises from, like the
      // other images: moved down, and cut at that edge
      const fh = ch / s;
      const side = ((w - cw / s) / 2).toFixed(2);
      const edge = (h - fh) / 2;
      const r = (round / s).toFixed(2);
      const piled = (below) => ({
        transform: `translate(${dx.toFixed(2)}px, ${(dy + below * ch).toFixed(2)}px) scale(${s.toFixed(4)})`,
        clipPath: `inset(${edge.toFixed(2)}px ${side}px ${(edge + below * fh).toFixed(2)}px ${side}px round ${r}px)`,
      });
      // When the growing image will be under the logo, as a share of the growth (the paper must
      // stay until then: the logo is drawn in difference, it would vanish on the bare page)
      let cover = 0.3;
      if (brand) {
        const b = brand.getBoundingClientRect();
        const need = (from, to, edge) => (from === to ? (edge ? 0 : 1) : Math.min(1, Math.max(0, (from - edge) / (from - to))));
        const e = Math.max(
          b.left < box.left || b.top < box.top || b.right > box.right || b.bottom > box.bottom ? 1 : 0,
          spot.left > b.left ? need(spot.left, box.left, b.left) : 0,
          spot.top > b.top ? need(spot.top, box.top, b.top) : 0,
          spot.right < b.right ? need(spot.right, box.right, b.right) : 0,
          spot.bottom < b.bottom ? need(spot.bottom, box.bottom, b.bottom) : 0,
        );
        // the time it takes on the growth's curve (in-out cubic), with a little margin
        cover = Math.min(1, (e < 0.5 ? Math.cbrt(e / 4) : 1 - Math.cbrt(2 * (1 - e)) / 2) + 0.04);
      }
      run.geo = { dx, dy, cover, settled: piled(0) };
      say(notes.length - 1);
      add(last, [piled(1), run.geo.settled], 0, LAST, SWING);
      return true;
    };

    // At 100: the counter rolls away while the hero image grows into its place in the page, the
    // pile riding under it and the paper lifting. Then the entrance
    const handover = () => {
      run.phase = 'handover';
      const { dx, dy, cover, settled } = run.geo;
      const radius = parseFloat(getComputedStyle(image).borderTopLeftRadius) || 0;
      const ease = css('ease') || SWING;
      const land = 300; // a beat on 100, the hero image settled on the pile
      const grow = 900; // then the hero image grows until it fills the hero

      // (cut above and below only: a digit may spill a little sideways)
      digits.forEach((strip, i) => add(strip.parentNode, [{ transform: 'translateY(0%)', clipPath: 'inset(0% -30% 0% -30%)' }, { transform: 'translateY(-100%)', clipPath: 'inset(100% -30% 0% -30%)' }], land + i * 30, 500, SWING));
      small.forEach((el, i) => add(el, [{ transform: 'translateY(0%)', clipPath: 'inset(0% 0% 0% 0%)' }, { transform: 'translateY(-100%)', clipPath: 'inset(100% 0% 0% 0%)' }], land + 40 + i * 40, 480, SWING));
      // (filled forwards only: until it starts, the rise still shows)
      const growth = add(last, [settled, { transform: 'translate(0px, 0px) scale(1)', clipPath: `inset(0px 0px 0px 0px round ${radius}px)` }], land, grow, ease, { fill: 'forwards' });
      // The paper goes once the image is under the logo, by the time the image has landed
      const lift = Math.max(0.3, cover);
      add(loader, [{ opacity: 1 }, { opacity: 0 }], land + grow * lift, Math.max(1, grow * Math.min(0.45, 1 - lift)), 'ease', { pseudoElement: '::before' });
      // The pile rides under the growing image and lets go of it: nothing is left to take away
      add(deck, [
        { transform: 'translate(0px, 0px)', opacity: 1 },
        { opacity: 0, offset: 0.6 },
        { transform: `translate(${(-dx).toFixed(2)}px, ${(-dy).toFixed(2)}px)`, opacity: 0 },
      ], land, grow, ease, { fill: 'forwards' });
      // The image fills the hero: the text and its veil come in once it has landed, on top of it
      const enterAt = land + grow;
      run.entrance = media ? [add(media, [{ opacity: 0 }, { opacity: 1 }], enterAt, 500, 'ease', { pseudoElement: '::after' })] : [];
      lines.forEach((line, i) => run.entrance.push(add(line, [
        { transform: 'translateY(105%)', clipPath: 'inset(-20% -10% 100% -10%)' },
        { transform: 'translateY(0%)', clipPath: 'inset(-20% -10% -30% -10%)' },
      ], enterAt + 120 + i * 110, 1000)));
      const after = enterAt + 280;
      items.forEach((el, i) => {
        const y = el.dataset.enter === 'down' ? -12 : 14;
        run.entrance.push(add(el, [{ opacity: 0, transform: `translateY(${y}px)` }, { opacity: 1, transform: 'translateY(0px)' }], after + i * 50, 600));
      });

      // The copy has landed on the hero image: the real one shows, the loader goes
      const token = run;
      growth.finished.then(() => {
        if (run !== token || run.phase !== 'handover') return;
        run.phase = 'enter';
        run.hold = null;
        root.classList.add('is-landed');
        putBack();
      }, () => {});
      Promise.all([growth, ...run.entrance].map((animation) => animation.finished)).then(() => run === token && end(), () => {});
    };

    // ---------- 3. Entrance, and the way out ----------

    // The loader leaves the end of the page and goes back into the hero, hidden
    const putBack = () => {
      loader.getAnimations({ subtree: true }).forEach((animation) => animation.cancel());
      loader.hidden = true;
      loader.classList.remove('is-counting');
      loader.removeAttribute('style');
      if (last) {
        last.hidden = true;
        last.removeAttribute('style');
      }
      cards.forEach((card) => card.removeAttribute('style'));
      brand?.removeAttribute('style');
      roll(digits, 0);
      digits.forEach((strip) => strip.removeAttribute('style'));
      if (loader.parentNode !== home[0]) home[0].insertBefore(loader, home[1]?.parentNode === home[0] ? home[1] : null);
    };

    // Everything as it ends up, at once
    const end = () => {
      if (!run) return;
      cancelAnimationFrame(run.frame);
      run.anims.forEach((animation) => animation.cancel());
      run = null;
      if (playing === end) playing = null;
      putBack();
      root.classList.remove('is-intro', 'is-landed');
      root.removeAttribute('aria-busy');
    };

    // Skipped: the hero is set as it ends up, the loader stops where it is and lifts off it like
    // a curtain. During the entrance, the rest just plays faster
    const skip = () => {
      if (!run || run.phase === 'skip') return;
      if (run.phase === 'enter') {
        run.entrance.forEach((animation) => animation.updatePlaybackRate(6));
        return;
      }
      cancelAnimationFrame(run.frame);
      run.phase = 'skip';
      run.hold = null;
      run.entrance.forEach((animation) => animation.cancel());
      run.anims.forEach((animation) => animation.playState !== 'idle' && animation.pause());
      root.classList.add('is-landed');
      loader.style.pointerEvents = 'none';
      const token = run;
      // From where the curtain is (halfway down when the replay is skipped): none would not move
      const from = getComputedStyle(loader).clipPath;
      const lift = loader.animate([{ clipPath: from.startsWith('inset') ? from : 'inset(0% 0% 0% 0%)' }, { clipPath: 'inset(0% 0% 100% 0%)' }], { duration: 620, easing: SWING, fill: 'forwards' });
      run.anims.push(lift);
      lift.finished.then(() => run === token && end(), () => {});
    };

    const start = (replay) => {
      if (playing && playing !== end) playing();
      end();
      if (!image || !last) return;
      if (root.hasAttribute('data-once')) {
        try {
          sessionStorage.setItem(SEEN, '1');
        } catch {
          // storage blocked: it plays on every page
        }
      }
      run = {
        phase: 'load', anims: [], entrance: [], order: [], next: 0, said: -1, value: 0, done: 0, shown: -1, total: 0, t0: 0, at: 0, beat: 0, frame: 0, hold: null,
        min: seconds(css('min'), 2200),
        max: 0,
      };
      run.max = Math.max(run.min + 300, seconds(css('max'), 2800));
      playing = end;
      // The loader goes to the end of the page while it plays: a transformed parent cannot trap
      // it, the styles of a theme's content cannot reach it. It takes the hero's settings along
      VARS.forEach((name) => loader.style.setProperty(`--kz-landing-reveal-${name}`, css(name)));
      loader.style.fontFamily = getComputedStyle(root).fontFamily;
      document.body.append(loader);
      loader.hidden = false;
      fit();
      root.setAttribute('aria-busy', 'true');
      run.hold = [scrollX, scrollY];
      roll(digits, 0);
      run.total = track();
      loader.classList.add('is-counting');
      const token = run;
      const begin = () => {
        if (run !== token || run.phase !== 'load') return;
        root.classList.add('is-intro');
        // Played again from further down: the hero comes back to the top, behind the loader
        if (replay) {
          const top = root.getBoundingClientRect().top + scrollY;
          window.scrollTo({ left: 0, top: top < innerHeight ? 0 : top, behavior: 'instant' });
          run.hold = [scrollX, scrollY];
          fit();
        }
        if (!replay) {
          arrive(0);
          reveal(cards[0], 60);
          run.beat = performance.now() + 60 + BEAT;
        }
        run.frame = requestAnimationFrame(tick);
      };
      if (!replay) {
        begin();
        return;
      }
      // Played again: the paper first comes down over the page, like a curtain, and the first
      // image is on its way up as it lands
      const drop = loader.animate([{ clipPath: 'inset(0% 0% 100% 0%)' }, { clipPath: 'inset(0% 0% 0% 0%)' }], { duration: 650, easing: SWING });
      run.anims.push(drop);
      arrive(360);
      reveal(cards[0], 420);
      run.beat = performance.now() + 420 + BEAT;
      drop.finished.then(begin, () => {});
    };

    // When it plays: its loader is still up on the screen, painted with the page and not yet let
    // go by the stylesheet's failsafe (3 s without a script). However late the script comes (an
    // app like Next loads it once the page is in), the show starts on the paper already there.
    // For a hero at the top of the page, unless the visitor came for a #section further down
    const up = () => {
      if (!loader.getClientRects().length) return false;
      const failsafe = loader.getAnimations().find((animation) => animation.animationName === 'kodza-landing-reveal-failsafe');
      return !failsafe || failsafe.currentTime < failsafe.effect.getTiming().delay;
    };
    const ahead = () => {
      try {
        const target = location.hash && document.getElementById(decodeURIComponent(location.hash.slice(1)));
        return !target || root.contains(target);
      } catch {
        return true;
      }
    };
    const seen = () => {
      try {
        return root.hasAttribute('data-once') && !!sessionStorage.getItem(SEEN);
      } catch {
        return false;
      }
    };
    const top = root.getBoundingClientRect().top + scrollY < innerHeight * 0.5;
    if (up() && !motion.matches && top && ahead() && !seen()) start(false);
    else loader.hidden = true;
    // One hero has been through: those that come later (an app's next page, a theme's Ajax
    // content, kodza:init again) open straight on the hero, their loader hidden by the
    // stylesheet before it paints. Once this pass is over, so the other heroes of the page are
    // judged like this one
    queueMicrotask(() => document.documentElement.setAttribute('data-kodza-landing-reveal', 'played'));

    // Stopped halfway (kodza:destroy): the hero as it ends up, the loader back in place
    signal.addEventListener('abort', end);
  }

  // Kodza lifecycle, the same in every component: `kodza:init` starts the instances inside
  // event.detail.root (the whole page by default), `kodza:destroy` stops them and puts their
  // markup back. For themes and apps that swap content without a page load.
  const instances = new WeakMap();
  const find = (scope = document) => [scope, ...scope.querySelectorAll(SELECTOR)].filter((el) => el.matches?.(SELECTOR));
  const init = (scope) => find(scope).forEach((root) => {
    if (root.classList.contains('is-ready')) return; // already running, maybe from another copy of this script
    const controller = new AbortController();
    instances.set(root, { controller, html: root.innerHTML, attrs: [...root.attributes].map((a) => [a.name, a.value]) });
    mount(root, controller.signal);
    root.classList.add('is-ready');
  });
  const destroy = (scope) => find(scope).forEach((root) => {
    const instance = instances.get(root);
    if (!instance) return;
    instances.delete(root);
    instance.controller.abort();
    [...root.attributes].forEach((a) => root.removeAttribute(a.name));
    instance.attrs.forEach(([name, value]) => root.setAttribute(name, value));
    root.innerHTML = instance.html;
  });
  document.addEventListener('kodza:init', (event) => init(event.detail?.root));
  document.addEventListener('kodza:destroy', (event) => destroy(event.detail?.root));
  init();
  // Loaded in <head>, the script runs before the markup exists: once more when it is parsed
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init(), { once: true });
})();
```

Free to use, from Kodza (https://kodza.dev).
