# Hero Search

> A hero with a real search card: Buy and Rent swap the budget list under a sliding pill, the place field rolls through its examples, the selects unroll like Kodza Select, and the result count rolls in the button.

Hero · hero, section, search, form, real estate, booking, filters, select · HTML, CSS, vanilla JS · no dependencies · updated 2026-10-25
Demo: https://kodza.dev/components/hero-search/

## Files

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

- [`kodza-hero-search.css`](https://kodza.dev/registry/hero-search/kodza-hero-search.css) · 45.1 KB
- [`kodza-hero-search.js`](https://kodza.dev/registry/hero-search/kodza-hero-search.js) · 25.8 KB
- [`kodza-hero-search.html`](https://kodza.dev/registry/hero-search/kodza-hero-search.html) · 4.2 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-hero-search.css https://kodza.dev/registry/hero-search/kodza-hero-search.css
curl -fsSL --create-dirs -o kodza/kodza-hero-search.js https://kodza.dev/registry/hero-search/kodza-hero-search.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-hero-search`
- Variables (set them on the root element itself: it declares its own defaults):
  - `--kz-hero-search-ink: #0a0a0a`
  - `--kz-hero-search-paper: #f2f2f0`
  - `--kz-hero-search-dim: rgba(10, 10, 10, 0.62)`
  - `--kz-hero-search-faint: rgba(10, 10, 10, 0.6)`
  - `--kz-hero-search-surface: #ffffff`
  - `--kz-hero-search-line: rgba(10, 10, 10, 0.1)`
  - `--kz-hero-search-field: rgba(10, 10, 10, 0.14)`
  - `--kz-hero-search-field-hover: rgba(10, 10, 10, 0.3)`
  - `--kz-hero-search-soft: rgba(10, 10, 10, 0.06)`
  - `--kz-hero-search-shadow: 0 24px 48px -24px rgba(10, 10, 10, 0.24)`
  - `--kz-hero-search-pop: 0 18px 40px -16px rgba(10, 10, 10, 0.3)`
  - `--kz-hero-search-bg: transparent`
  - `--kz-hero-search-width: 1240px`
  - `--kz-hero-search-gap: clamp(32px, 5vw, 80px)`
  - `--kz-hero-search-pad-y: clamp(72px, 9vw, 140px)`
  - `--kz-hero-search-gutter: clamp(16px, 4vw, 40px)`
  - `--kz-hero-search-radius: 16px`
  - `--kz-hero-search-ratio: 4 / 5`
  - `--kz-hero-search-height: 56px`
  - `--kz-hero-search-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace`
  - `--kz-hero-search-ease: cubic-bezier(0.22, 1, 0.36, 1)`
- Attributes:
  - `data-examples` (e.g. `Le Havre, Lisbon, Nantes, Porto`)
  - `data-mode` (e.g. `buy`, `rent`)
- 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.

- On Buy or Rent (focus in `.kodza-hero-search__radio`):
  - `ArrowLeft` / `ArrowRight`: Switches between Buy and Rent, and the budget list with it
- On Type or Budget, while it's open (focus in `.kodza-hero-search__trigger`, while `.kodza-hero-search__select.is-open` is on the page):
  - `ArrowDown` / `ArrowUp`: Next or previous option, no looping
  - `Home` / `End`: First or last option
  - `PageDown` / `PageUp`: Five options down or up
  - a letter: Jumps to the option starting with that letter
  - `Enter` / `Space`: Picks the highlighted option and closes
  - `Tab`: Picks the highlighted option and moves on
- On Type or Budget (focus in `.kodza-hero-search__trigger`):
  - `Enter` / `Space` / `ArrowDown` / `ArrowUp`: Opens it on the current choice
  - a letter: Picks the option starting with that letter, no opening
- Anywhere, while a list is open (while `.kodza-hero-search__select.is-open` is on the page):
  - `Escape`: Closes it, focus back on the field
- In the place field (focus in `.kodza-hero-search__input`):
  - `Enter`: Sends the search

## 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-hero-search {
    --kz-hero-search-ink: #f2f2f0;
    --kz-hero-search-paper: #0a0a0a;
    --kz-hero-search-dim: rgba(242, 242, 240, 0.64);
    --kz-hero-search-faint: rgba(242, 242, 240, 0.6);
    --kz-hero-search-surface: #161616;
    --kz-hero-search-line: rgba(255, 255, 255, 0.12);
    --kz-hero-search-field: rgba(255, 255, 255, 0.14);
    --kz-hero-search-field-hover: rgba(255, 255, 255, 0.3);
    --kz-hero-search-soft: rgba(255, 255, 255, 0.08);
    --kz-hero-search-shadow: 0 24px 48px -24px rgba(0, 0, 0, 0.7);
    --kz-hero-search-pop: 0 18px 40px -16px rgba(0, 0, 0, 0.8);
  }
}
```

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

Its images have dark versions next to them (`kz-overflow-1400.webp` → `kz-overflow-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-hero-search.css` next to the page (the Files commands as they are), linked in `<head>`: `<link rel="stylesheet" href="kodza/kodza-hero-search.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-hero-search.js`, loaded with `<script src="kodza/kodza-hero-search.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-hero-search.css' -e 'd' -e '}' -e '/^KODZA-JS$/{r kodza/kodza-hero-search.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.
- Forms: there is nowhere to send them. Handle `submit` in your script with `event.preventDefault()` and show the result in the page.

### 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-hero-search', $uri . 'kodza-hero-search.css', [], filemtime($dir . 'kodza-hero-search.css'));
    wp_enqueue_script('kodza-hero-search', $uri . 'kodza-hero-search.js', [], filemtime($dir . 'kodza-hero-search.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>`).

#### 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.

### Shopify

- Files: `assets/kodza-hero-search.css` and `assets/kodza-hero-search.js` (the Files commands with `-o assets/…`).
- In the section: `{{ 'kodza-hero-search.css' | asset_url | stylesheet_tag }}` and `<script src="{{ 'kodza-hero-search.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/`): `HeroSearch.tsx` (below), the CSS block under Code saved as `hero-search.css` and the JS block as `hero-search.js`, both as they are. The HTML block is already inside the component: skip it.
- Use it: `import { HeroSearch } from '@/components/kodza/HeroSearch';` then `<HeroSearch />`. Props: `vars` (the `--kz-…` variables listed under API, e.g. `vars={{ '--kz-hero-search-ink': … }}`), `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 './hero-search.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 — Hero Search, for React. From https://kodza.dev/components/hero-search/
// 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: hero-search.css and hero-search.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 './hero-search.css';

export type HeroSearchProps = Omit<ComponentPropsWithoutRef<'section'>, "children" | "vars"> & {
  /** Settings, as CSS variables on the root */
  vars?: {
    /** default: #0a0a0a */
    "--kz-hero-search-ink"?: string | number;
    /** default: #f2f2f0 */
    "--kz-hero-search-paper"?: string | number;
    /** default: rgba(10, 10, 10, 0.62) */
    "--kz-hero-search-dim"?: string | number;
    /** default: rgba(10, 10, 10, 0.6) */
    "--kz-hero-search-faint"?: string | number;
    /** default: #ffffff */
    "--kz-hero-search-surface"?: string | number;
    /** default: rgba(10, 10, 10, 0.1) */
    "--kz-hero-search-line"?: string | number;
    /** default: rgba(10, 10, 10, 0.14) */
    "--kz-hero-search-field"?: string | number;
    /** default: rgba(10, 10, 10, 0.3) */
    "--kz-hero-search-field-hover"?: string | number;
    /** default: rgba(10, 10, 10, 0.06) */
    "--kz-hero-search-soft"?: string | number;
    /** default: 0 24px 48px -24px rgba(10, 10, 10, 0.24) */
    "--kz-hero-search-shadow"?: string | number;
    /** default: 0 18px 40px -16px rgba(10, 10, 10, 0.3) */
    "--kz-hero-search-pop"?: string | number;
    /** default: transparent */
    "--kz-hero-search-bg"?: string | number;
    /** default: 1240px */
    "--kz-hero-search-width"?: string | number;
    /** default: clamp(32px, 5vw, 80px) */
    "--kz-hero-search-gap"?: string | number;
    /** default: clamp(72px, 9vw, 140px) */
    "--kz-hero-search-pad-y"?: string | number;
    /** default: clamp(16px, 4vw, 40px) */
    "--kz-hero-search-gutter"?: string | number;
    /** default: 16px */
    "--kz-hero-search-radius"?: string | number;
    /** default: 4 / 5 */
    "--kz-hero-search-ratio"?: string | number;
    /** default: 56px */
    "--kz-hero-search-height"?: string | number;
    /** default: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace */
    "--kz-hero-search-mono"?: string | number;
    /** default: cubic-bezier(0.22, 1, 0.36, 1) */
    "--kz-hero-search-ease"?: string | number;
  };
  className?: string;
};

export function HeroSearch({ vars, className, style, ...rest }: HeroSearchProps) {
  const ref = useRef<ComponentRef<'section'>>(null);
  const classes = ["kodza-hero-search", 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('./hero-search.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-hero-search__inner">
        <div className="kodza-hero-search__copy">
          <p className="kodza-hero-search__eyebrow">
            <span className="kodza-hero-search__rise">Homes to buy and rent · updated hourly</span>
          </p>
          <h1 className="kodza-hero-search__title">
            <span className="kodza-hero-search__line">
              <span className="kodza-hero-search__rise">Find a home,</span>
            </span>
            {' '}
            <span className="kodza-hero-search__line">
              <span className="kodza-hero-search__rise">not a brochure.</span>
            </span>
          </h1>
          <p className="kodza-hero-search__lead">Real floor plans, real prices and the street before the visit. When it says cosy, we say small.</p>
          <form className="kodza-hero-search__form" action="/search" method="get" role="search">
            <fieldset className="kodza-hero-search__modes">
              <legend className="kodza-hero-search__sr">Looking to</legend>
              {' '}
              <label className="kodza-hero-search__mode">
                <input className="kodza-hero-search__radio" type="radio" name="mode" defaultValue="buy" defaultChecked />
                <span>Buy</span>
              </label>
              {' '}
              <label className="kodza-hero-search__mode">
                <input className="kodza-hero-search__radio" type="radio" name="mode" defaultValue="rent" />
                <span>Rent</span>
              </label>
            </fieldset>
            <div className="kodza-hero-search__fields">
              <label className="kodza-hero-search__field kodza-hero-search__field--where">
                <span className="kodza-hero-search__label">Where</span>
                {' '}
                <input className="kodza-hero-search__input" type="text" name="where" placeholder="Le Havre" autoComplete="off" data-examples="Le Havre, Lisbon, Nantes, Porto" />
              </label>
              {' '}
              <label className="kodza-hero-search__field kodza-hero-search__select">
                <span className="kodza-hero-search__label">Type</span>
                <select className="kodza-hero-search__native" name="type" aria-label="Type" defaultValue="">
                  <option value="">Any type</option>
                  <option value="house">House</option>
                  <option value="flat">Flat</option>
                  <option value="loft">Loft</option>
                  <option value="studio">Studio</option>
                </select>
              </label>
              {' '}
              <label className="kodza-hero-search__field kodza-hero-search__select">
                <span className="kodza-hero-search__label">Budget</span>
                <select className="kodza-hero-search__native" name="max_price" data-mode="buy" aria-label="Budget" defaultValue="">
                  <option value="">Any price</option>
                  <option value="200000">Up to €200k</option>
                  <option value="350000">Up to €350k</option>
                  <option value="500000">Up to €500k</option>
                  <option value="800000">Up to €800k</option>
                </select>
                <select className="kodza-hero-search__native" name="max_rent" data-mode="rent" aria-label="Budget" defaultValue="">
                  <option value="">Any rent</option>
                  <option value="800">Up to €800 / mo</option>
                  <option value="1200">Up to €1,200 / mo</option>
                  <option value="1800">Up to €1,800 / mo</option>
                  <option value="2500">Up to €2,500 / mo</option>
                </select>
              </label>
            </div>
            <button className="kodza-hero-search__submit" type="submit">Show <span className="kodza-hero-search__count">128</span> homes<svg className="kodza-hero-search__arrow" viewBox="0 0 16 16" aria-hidden="true"> <path d="M2.5 8h10.5M9 3.5 13.5 8 9 12.5" /> </svg></button>
            <p className="kodza-hero-search__note">Free to search. No account, no agent calling you at dinner.</p>
          </form>
        </div>
        <figure className="kodza-hero-search__media">
          <div className="kodza-hero-search__frame">
            <div className="kodza-hero-search__curtain">
              <div className="kodza-hero-search__hold">
                <img src="https://kodza.dev/media/kz-overflow-1400.webp" alt="A two-storey concrete house, its upper floor cantilevered over the ground" width="1400" height="1400" />
              </div>
            </div>
            <p className="kodza-hero-search__tag">
              <span className="kodza-hero-search__meta">Sainte-Adresse · 4 rooms · 132 m²</span>
              {' '}
              <span className="kodza-hero-search__price">
                <span data-mode="buy">€465,000</span>
                <span data-mode="rent">€1,750 / mo</span>
              </span>
            </p>
          </div>
          <figcaption className="kodza-hero-search__caption">fig. 01 — 132 m², measured, not estimated</figcaption>
        </figure>
      </div>
    </section>
  );
}
```

## Code

```html
<section class="kodza-hero-search">
  <div class="kodza-hero-search__inner">
    <div class="kodza-hero-search__copy">
      <p class="kodza-hero-search__eyebrow"><span class="kodza-hero-search__rise">Homes to buy and rent · updated hourly</span></p>
      <h1 class="kodza-hero-search__title"><span class="kodza-hero-search__line"><span class="kodza-hero-search__rise">Find a home,</span></span> <span class="kodza-hero-search__line"><span class="kodza-hero-search__rise">not a brochure.</span></span></h1>
      <p class="kodza-hero-search__lead">Real floor plans, real prices and the street before the visit. When it says cosy, we say small.</p>
      <form class="kodza-hero-search__form" action="/search" method="get" role="search">
        <fieldset class="kodza-hero-search__modes">
          <legend class="kodza-hero-search__sr">Looking to</legend>
          <label class="kodza-hero-search__mode"><input class="kodza-hero-search__radio" type="radio" name="mode" value="buy" checked /><span>Buy</span></label>
          <label class="kodza-hero-search__mode"><input class="kodza-hero-search__radio" type="radio" name="mode" value="rent" /><span>Rent</span></label>
        </fieldset>
        <div class="kodza-hero-search__fields">
          <label class="kodza-hero-search__field kodza-hero-search__field--where">
            <span class="kodza-hero-search__label">Where</span>
            <input class="kodza-hero-search__input" type="text" name="where" placeholder="Le Havre" autocomplete="off" data-examples="Le Havre, Lisbon, Nantes, Porto" />
          </label>
          <label class="kodza-hero-search__field kodza-hero-search__select">
            <span class="kodza-hero-search__label">Type</span>
            <select class="kodza-hero-search__native" name="type" aria-label="Type">
              <option value="" selected>Any type</option>
              <option value="house">House</option>
              <option value="flat">Flat</option>
              <option value="loft">Loft</option>
              <option value="studio">Studio</option>
            </select>
          </label>
          <label class="kodza-hero-search__field kodza-hero-search__select">
            <span class="kodza-hero-search__label">Budget</span>
            <select class="kodza-hero-search__native" name="max_price" data-mode="buy" aria-label="Budget">
              <option value="" selected>Any price</option>
              <option value="200000">Up to €200k</option>
              <option value="350000">Up to €350k</option>
              <option value="500000">Up to €500k</option>
              <option value="800000">Up to €800k</option>
            </select>
            <select class="kodza-hero-search__native" name="max_rent" data-mode="rent" aria-label="Budget">
              <option value="" selected>Any rent</option>
              <option value="800">Up to €800 / mo</option>
              <option value="1200">Up to €1,200 / mo</option>
              <option value="1800">Up to €1,800 / mo</option>
              <option value="2500">Up to €2,500 / mo</option>
            </select>
          </label>
        </div>
        <button class="kodza-hero-search__submit" type="submit">Show <span class="kodza-hero-search__count">128</span> homes<svg class="kodza-hero-search__arrow" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 8h10.5M9 3.5 13.5 8 9 12.5" /></svg></button>
        <p class="kodza-hero-search__note">Free to search. No account, no agent calling you at dinner.</p>
      </form>
    </div>
    <figure class="kodza-hero-search__media">
      <div class="kodza-hero-search__frame">
        <div class="kodza-hero-search__curtain"><div class="kodza-hero-search__hold"><img src="https://kodza.dev/media/kz-overflow-1400.webp" alt="A two-storey concrete house, its upper floor cantilevered over the ground" width="1400" height="1400" /></div></div>
        <p class="kodza-hero-search__tag"><span class="kodza-hero-search__meta">Sainte-Adresse · 4&nbsp;rooms · 132&nbsp;m²</span> <span class="kodza-hero-search__price"><span data-mode="buy">€465,000</span><span data-mode="rent">€1,750 / mo</span></span></p>
      </div>
      <figcaption class="kodza-hero-search__caption">fig. 01 — 132&nbsp;m², measured, not estimated</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 — Hero Search
   A hero that searches on arrival: a tight headline, its intro, then a search card, with the
   picture of a listing on the right. Buy and Rent are native radios under an ink pill that slides
   from one to the other. The place field rolls through its examples (data-examples) while it is
   empty; Type and Budget are real selects, dressed up as in Kodza Select (a list that unrolls, a
   highlight that slides, a tick, the value that rolls). Each mode has its own budget select
   (data-mode): the field shows the chosen mode's one, the other is disabled, so the form only
   sends one. A price written per mode (data-mode, in the listing's tag) rolls with it.
   The count in the button rolls too: write the new number into .kodza-hero-search__count
   (on the form's change event, from your own results). The form sends itself as GET to its
   action, with or without the script. On arrival the lines rise out of their masks while the
   picture opens from the top edge down, as in Hero Split. Everything is tuned with the
   --kz-hero-search-* variables. */
.kodza-hero-search {
  --kz-hero-search-ink: #0a0a0a;
  --kz-hero-search-paper: #f2f2f0;
  --kz-hero-search-dim: rgba(10, 10, 10, 0.62);
  --kz-hero-search-faint: rgba(10, 10, 10, 0.6);
  --kz-hero-search-surface: #ffffff;
  --kz-hero-search-line: rgba(10, 10, 10, 0.1);
  --kz-hero-search-field: rgba(10, 10, 10, 0.14);
  --kz-hero-search-field-hover: rgba(10, 10, 10, 0.3);
  --kz-hero-search-soft: rgba(10, 10, 10, 0.06);
  --kz-hero-search-shadow: 0 24px 48px -24px rgba(10, 10, 10, 0.24);
  --kz-hero-search-pop: 0 18px 40px -16px rgba(10, 10, 10, 0.3);
  --kz-hero-search-bg: transparent;
  --kz-hero-search-width: 1240px;
  --kz-hero-search-gap: clamp(32px, 5vw, 80px);
  --kz-hero-search-pad-y: clamp(72px, 9vw, 140px);
  --kz-hero-search-gutter: clamp(16px, 4vw, 40px);
  --kz-hero-search-radius: 16px;
  --kz-hero-search-ratio: 4 / 5;
  --kz-hero-search-height: 56px;
  --kz-hero-search-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
  --kz-hero-search-ease: cubic-bezier(0.22, 1, 0.36, 1);

  container: kodza-hero-search / inline-size;
  box-sizing: border-box;
  width: 100%;
  padding-block: var(--kz-hero-search-pad-y);
  padding-left: max(var(--kz-hero-search-gutter), env(safe-area-inset-left, 0px));
  padding-right: max(var(--kz-hero-search-gutter), env(safe-area-inset-right, 0px));
  background: var(--kz-hero-search-bg);
  color: var(--kz-hero-search-ink);
}

.kodza-hero-search__inner {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: var(--kz-hero-search-gap);
  align-items: center;
  max-width: var(--kz-hero-search-width);
  margin: 0 auto;
}

.kodza-hero-search__copy {
  min-width: 0;
}

.kodza-hero-search__eyebrow {
  margin: 0 0 22px;
  overflow: hidden;
  font-family: var(--kz-hero-search-mono);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--kz-hero-search-faint);
}

.kodza-hero-search__title {
  margin: 0 0 24px;
  font-size: clamp(40px, 5.6vw, 84px);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.05em;
  color: var(--kz-hero-search-ink);
}

/* Each line is its own mask: the words rise from behind its bottom edge */
.kodza-hero-search__line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.08em;
  margin-bottom: -0.08em;
}

.kodza-hero-search__rise {
  display: block;
}

.kodza-hero-search__lead {
  max-width: 34ch;
  margin: 0 0 clamp(28px, 3vw, 40px);
  font-size: clamp(17px, 1.45vw, 19px);
  line-height: 1.55;
  color: var(--kz-hero-search-dim);
  text-wrap: pretty;
}

/* The search card. Open, a list rises above what follows the card, the picture included */
.kodza-hero-search__form {
  container: kodza-hero-search-form / inline-size;
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  display: grid;
  gap: 12px;
  max-width: 640px;
  margin: 0;
  padding: 16px;
  border-radius: var(--kz-hero-search-radius);
  background: var(--kz-hero-search-surface);
  box-shadow: 0 0 0 1px var(--kz-hero-search-line), var(--kz-hero-search-shadow);
}

/* Buy / Rent: the ink pill covers the whole row and is cut down to the chosen mode, with a
   paper copy of the labels cut to the same shape on top */
.kodza-hero-search__modes {
  position: relative;
  justify-self: start;
  display: inline-flex;
  min-width: 0;
  margin: 0;
  padding: 4px;
  border: 0;
  border-radius: 999px;
  background: var(--kz-hero-search-surface);
  box-shadow: inset 0 0 0 1px var(--kz-hero-search-field);
}

.kodza-hero-search__indicator,
.kodza-hero-search__mirror {
  position: absolute;
  inset: 0;
  border-radius: 999px;
  clip-path: inset(4px var(--kz-hero-search-r, 100%) 4px var(--kz-hero-search-l, 0) round 999px);
  pointer-events: none;
}

.kodza-hero-search__indicator {
  background: var(--kz-hero-search-ink);
}

.kodza-hero-search__mirror {
  box-sizing: border-box;
  display: flex;
  padding: 4px;
  color: var(--kz-hero-search-paper);
}

.kodza-hero-search.is-live .kodza-hero-search__indicator,
.kodza-hero-search.is-live .kodza-hero-search__mirror {
  transition: clip-path 0.5s var(--kz-hero-search-ease);
}

.kodza-hero-search__mode,
.kodza-hero-search__ghost {
  display: inline-flex;
  align-items: center;
  height: 36px;
  padding: 0 18px;
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
}

.kodza-hero-search__mode {
  position: relative;
  border-radius: 999px;
  color: var(--kz-hero-search-dim);
  cursor: pointer;
  transition: color 0.3s var(--kz-hero-search-ease);
}

.kodza-hero-search__mode:hover,
.kodza-hero-search__mode:has(.kodza-hero-search__radio:checked) {
  color: var(--kz-hero-search-ink);
}

/* Without the script, the chosen mode is simply filled */
.kodza-hero-search:not(.is-ready) .kodza-hero-search__mode:has(.kodza-hero-search__radio:checked) {
  background: var(--kz-hero-search-ink);
  color: var(--kz-hero-search-paper);
}

.kodza-hero-search__mode:has(.kodza-hero-search__radio:focus-visible) {
  outline: 2px solid var(--kz-hero-search-ink);
  outline-offset: 2px;
}

/* The native radio stays, invisible, over its label: clicks and arrow keys are its own */
.kodza-hero-search__radio {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

/* Where, Type and Budget on one row; on a narrower card the place takes its own row */
.kodza-hero-search__fields {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr) minmax(0, 1.25fr);
  gap: 8px;
}

/* The box of Kodza Text Field: the small label on top, the value below */
.kodza-hero-search__field {
  position: relative;
  display: block;
  min-width: 0;
  color: var(--kz-hero-search-ink);
}

.kodza-hero-search__label {
  position: absolute;
  top: 10px;
  left: 16px;
  z-index: 1;
  color: var(--kz-hero-search-dim);
  font-size: 12px;
  font-weight: 400;
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
}

.kodza-hero-search__input,
.kodza-hero-search__native,
.kodza-hero-search__trigger {
  box-sizing: border-box;
  display: flex;
  align-items: flex-end;
  width: 100%;
  height: var(--kz-hero-search-height);
  margin: 0;
  padding: 25px 16px 9px;
  border: 0;
  border-radius: 12px;
  background: var(--kz-hero-search-surface);
  box-shadow: inset 0 0 0 1px var(--kz-hero-search-field);
  color: inherit;
  font: inherit;
  font-size: 15px;
  font-weight: 400;
  line-height: 22px;
  text-align: left;
  appearance: none;
  transition: box-shadow 0.25s ease;
}

.kodza-hero-search__native,
.kodza-hero-search__trigger {
  padding-right: 44px;
  cursor: pointer;
}

.kodza-hero-search__native {
  background: var(--kz-hero-search-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6.5 8 10.5l4-4' fill='none' stroke='%23999' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 16px center / 16px no-repeat;
}

.kodza-hero-search__trigger {
  padding-top: 0;
}

.kodza-hero-search__input:hover,
.kodza-hero-search__native:hover,
.kodza-hero-search__trigger:hover {
  box-shadow: inset 0 0 0 1px var(--kz-hero-search-field-hover);
}

.kodza-hero-search__input:focus,
.kodza-hero-search__native:focus-visible,
.kodza-hero-search__trigger:focus-visible,
.kodza-hero-search__select.is-open .kodza-hero-search__trigger {
  outline: none;
  box-shadow: inset 0 0 0 1.5px var(--kz-hero-search-ink), 0 0 0 4px var(--kz-hero-search-soft);
}

.kodza-hero-search__input::placeholder {
  color: var(--kz-hero-search-dim);
  opacity: 1;
}

/* The rolling example lies exactly where the placeholder is; the placeholder steps back while it rolls */
.kodza-hero-search__example {
  position: absolute;
  right: 16px;
  bottom: 9px;
  left: 16px;
  display: grid;
  height: 22px;
  overflow: clip;
  color: var(--kz-hero-search-dim);
  font-size: 15px;
  line-height: 22px;
  visibility: hidden;
  pointer-events: none;
}

.kodza-hero-search__field.is-rolling .kodza-hero-search__example {
  visibility: visible;
}

.kodza-hero-search__field.is-rolling .kodza-hero-search__input::placeholder {
  color: transparent;
}

/* Each mode its own budget: without the script, the radios show the right one */
.kodza-hero-search:not(.is-ready):has(.kodza-hero-search__radio[value="rent"]:checked) [data-mode="buy"],
.kodza-hero-search:not(.is-ready):has(.kodza-hero-search__radio[value="buy"]:checked) [data-mode="rent"] {
  display: none;
}

/* A select, dressed up. A number, not auto, so it can wait for its list to roll up before
   dropping back; the closed list keeps its size, ready to unroll, but never lengthens the page */
.kodza-hero-search__select {
  z-index: 0;
  contain: layout;
  transition: z-index 0s linear 0.35s;
}

.kodza-hero-search__select.is-open {
  z-index: 20;
  transition-delay: 0s;
}

/* Enhanced: the native select steps back (still in the form), the button takes its place */
.kodza-hero-search.is-ready .kodza-hero-search__native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  clip-path: inset(50%);
}

.kodza-hero-search__native[hidden] {
  display: none;
}

.kodza-hero-search__value {
  display: inline-grid;
  min-width: 0;
  overflow: hidden;
}

.kodza-hero-search__value > span,
.kodza-hero-search__example > span {
  grid-area: 1 / 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.kodza-hero-search__chevron {
  position: absolute;
  top: calc(var(--kz-hero-search-height) / 2 - 8px);
  right: 16px;
  width: 16px;
  height: 16px;
  fill: none;
  stroke: var(--kz-hero-search-dim);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  pointer-events: none;
  transition: transform 0.35s var(--kz-hero-search-ease);
}

.kodza-hero-search__select.is-open .kodza-hero-search__chevron {
  transform: rotate(180deg);
}

/* The list unrolls from the field */
.kodza-hero-search__list {
  position: absolute;
  top: calc(var(--kz-hero-search-height) + 8px);
  right: 0;
  left: 0;
  z-index: 20;
  box-sizing: border-box;
  min-width: 180px;
  max-height: 280px;
  margin: 0;
  padding: 6px;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-radius: 14px;
  background: var(--kz-hero-search-surface);
  box-shadow: 0 0 0 1px var(--kz-hero-search-field), var(--kz-hero-search-pop);
  list-style: none;
  visibility: hidden;
  opacity: 0;
  clip-path: inset(0 0 100% 0 round 14px);
  transform: translateY(-4px);
  transition:
    clip-path 0.35s var(--kz-hero-search-ease),
    opacity 0s linear 0.35s,
    transform 0.35s var(--kz-hero-search-ease),
    visibility 0s linear 0.35s;
}

.kodza-hero-search__select.is-open .kodza-hero-search__list {
  visibility: visible;
  opacity: 1;
  clip-path: inset(-40px -40px -60px -40px round 14px);
  transform: none;
  transition-delay: 0s;
}

.kodza-hero-search__select.is-above .kodza-hero-search__list {
  top: auto;
  bottom: calc(var(--kz-hero-search-height) + 8px);
  clip-path: inset(100% 0 0 0 round 14px);
  transform: translateY(4px);
}

.kodza-hero-search__select.is-above.is-open .kodza-hero-search__list {
  clip-path: inset(-60px -40px -40px -40px round 14px);
  transform: none;
}

.kodza-hero-search__highlight {
  position: absolute;
  top: 0;
  right: 6px;
  left: 6px;
  height: var(--kz-hl-h, 36px);
  border-radius: 8px;
  background: var(--kz-hero-search-soft);
  opacity: var(--kz-hl-on, 0);
  transform: translateY(var(--kz-hl-y, 0));
  pointer-events: none;
}

.kodza-hero-search__select.is-open .kodza-hero-search__highlight {
  transition: transform 0.22s var(--kz-hero-search-ease), opacity 0.15s ease;
}

.kodza-hero-search__option {
  position: relative;
  display: flex;
  align-items: center;
  height: 36px;
  margin: 0;
  padding: 0 10px 0 32px;
  border-radius: 8px;
  color: var(--kz-hero-search-ink);
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}

.kodza-hero-search__option[aria-disabled="true"] {
  color: var(--kz-hero-search-dim);
  cursor: not-allowed;
}

/* The tick of the chosen option draws itself */
.kodza-hero-search__option svg {
  position: absolute;
  left: 10px;
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset 0.15s ease-in;
}

.kodza-hero-search__option[aria-selected="true"] svg {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 0.35s var(--kz-hero-search-ease) 0.05s;
}

/* The button says how many homes the search finds; the number rolls when the page changes it */
.kodza-hero-search__submit {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  height: 52px;
  margin: 0;
  padding: 0 24px;
  border: 0;
  border-radius: 999px;
  background: var(--kz-hero-search-ink);
  color: var(--kz-hero-search-paper);
  font-family: inherit;
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}

.kodza-hero-search__submit:focus-visible {
  outline: 2px solid var(--kz-hero-search-ink);
  outline-offset: 3px;
}

.kodza-hero-search__count {
  position: relative;
  display: inline-grid;
  justify-items: start;
  overflow: clip;
  overflow-clip-margin: 2px;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.kodza-hero-search__count > span {
  grid-area: 1 / 1;
}

.kodza-hero-search__arrow {
  flex: none;
  width: 16px;
  height: 16px;
  margin-left: 4px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 0.3s var(--kz-hero-search-ease);
}

@media (hover: hover) {
  .kodza-hero-search__submit:hover .kodza-hero-search__arrow {
    transform: translateX(4px);
  }
}

.kodza-hero-search__note {
  margin: 0;
  font-family: var(--kz-hero-search-mono);
  font-size: 12px;
  line-height: 1.5;
  color: var(--kz-hero-search-faint);
  text-align: center;
  text-wrap: balance;
}

.kodza-hero-search__media {
  min-width: 0;
  margin: 0;
}

/* The curtain: the middle layer slides down from above while the inner one slides up into
   place, so the picture is uncovered from the top edge down and never moves itself */
.kodza-hero-search__frame {
  position: relative;
  max-width: 100%;
  aspect-ratio: var(--kz-hero-search-ratio);
  overflow: hidden;
  border-radius: var(--kz-hero-search-radius);
}

.kodza-hero-search__curtain,
.kodza-hero-search__hold {
  height: 100%;
}

.kodza-hero-search__curtain {
  overflow: hidden;
}

.kodza-hero-search__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The listing on the picture: where, how big, and the price of the chosen mode */
.kodza-hero-search__tag {
  position: absolute;
  bottom: 16px;
  left: 16px;
  box-sizing: border-box;
  display: grid;
  justify-items: start;
  gap: 6px;
  max-width: calc(100% - 32px);
  margin: 0;
  padding: 12px 16px 13px;
  border-radius: 12px;
  background: var(--kz-hero-search-surface);
  box-shadow: 0 0 0 1px var(--kz-hero-search-line), var(--kz-hero-search-shadow);
  color: var(--kz-hero-search-ink);
}

.kodza-hero-search__meta {
  font-family: var(--kz-hero-search-mono);
  font-size: 12px;
  line-height: 1.3;
  color: var(--kz-hero-search-dim);
}

.kodza-hero-search__price {
  display: inline-grid;
  overflow: clip;
  font-size: 22px;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.kodza-hero-search__price > [data-mode] {
  grid-area: 1 / 1;
  white-space: nowrap;
}

.kodza-hero-search.is-ready .kodza-hero-search__price > [data-mode]:not(.is-on) {
  visibility: hidden;
}

.kodza-hero-search__caption {
  margin-top: 12px;
  font-family: var(--kz-hero-search-mono);
  font-size: 12.5px;
  color: var(--kz-hero-search-faint);
  text-wrap: pretty;
}

.kodza-hero-search__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Waiting for its entrance: the script sets is-shown and plays the motion in the same frame */
@media (prefers-reduced-motion: no-preference) {
  .kodza-hero-search.is-ready:not(.is-shown) .kodza-hero-search__rise {
    transform: translateY(105%);
  }

  .kodza-hero-search.is-ready:not(.is-shown) .kodza-hero-search__curtain {
    transform: translateY(-100%);
  }

  .kodza-hero-search.is-ready:not(.is-shown) .kodza-hero-search__hold {
    transform: translateY(100%);
  }

  .kodza-hero-search.is-ready:not(.is-shown) .kodza-hero-search__lead,
  .kodza-hero-search.is-ready:not(.is-shown) .kodza-hero-search__form,
  .kodza-hero-search.is-ready:not(.is-shown) .kodza-hero-search__tag,
  .kodza-hero-search.is-ready:not(.is-shown) .kodza-hero-search__caption {
    opacity: 0;
    transform: translateY(20px);
  }
}

/* A narrower card: the place takes the whole row, Type and Budget share the next one */
@container kodza-hero-search-form (max-width: 520px) {
  .kodza-hero-search__fields {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }

  .kodza-hero-search__field--where {
    grid-column: 1 / -1;
  }
}

@container kodza-hero-search-form (max-width: 340px) {
  .kodza-hero-search__fields {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* One column: the picture goes under the search, wider than tall */
@container kodza-hero-search (max-width: 700px) {
  .kodza-hero-search__inner {
    grid-template-columns: minmax(0, 1fr);
  }

  .kodza-hero-search__frame {
    aspect-ratio: 4 / 3;
  }
}

@media (prefers-reduced-motion: reduce) {
  .kodza-hero-search *,
  .kodza-hero-search *::before,
  .kodza-hero-search *::after {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
    animation-duration: 0.01ms !important;
  }
}

/* Theming example — on a dark background:
.dark-section .kodza-hero-search {
  --kz-hero-search-ink: #f2f2f0;
  --kz-hero-search-paper: #0a0a0a;
  --kz-hero-search-dim: rgba(242, 242, 240, 0.64);
  --kz-hero-search-faint: rgba(242, 242, 240, 0.6);
  --kz-hero-search-surface: #161616;
  --kz-hero-search-line: rgba(255, 255, 255, 0.12);
  --kz-hero-search-field: rgba(255, 255, 255, 0.14);
  --kz-hero-search-field-hover: rgba(255, 255, 255, 0.3);
  --kz-hero-search-soft: rgba(255, 255, 255, 0.08);
  --kz-hero-search-shadow: 0 24px 48px -24px rgba(0, 0, 0, 0.7);
  --kz-hero-search-pop: 0 18px 40px -16px rgba(0, 0, 0, 0.8);
}
*/

/* kodza-guard 3f61d7b50cad — 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-hero-search__eyebrow):not(#_),
:where(.kodza-hero-search__title):not(#_),
:where(.kodza-hero-search__lead):not(#_),
:where(.kodza-hero-search__modes):not(#_),
:where(.kodza-hero-search__sr):not(#_),
:where(.kodza-hero-search__radio):not(#_),
:where(.kodza-hero-search__option):not(#_),
:where(.kodza-hero-search__note):not(#_),
:where(.kodza-hero-search__media):not(#_),
:where(.kodza-hero-search__tag):not(#_),
:where(.kodza-hero-search__caption):not(#_),
:where(.kodza-hero-search__hold img:where(:not([class*="kodza-"]))):not(#_) {
  transition-duration: revert;
  transition-delay: revert;
}

:where(.kodza-hero-search__mode):not(#_) {
  outline: revert;
  background-color: revert;
  background-image: revert;
  outline-offset: revert;
}

:where(.kodza-hero-search__field):not(#_),
:where(.kodza-hero-search__highlight):not(#_) {
  transition: revert;
}

:where(.kodza-hero-search__input):not(#_),
:where(.kodza-hero-search__trigger):not(#_) {
  outline: revert;
}

:where(.kodza-hero-search__native):not(#_) {
  outline: revert;
  position: revert;
}

:where(.kodza-hero-search__submit):not(#_) {
  outline: revert;
  outline-offset: revert;
  transition-duration: revert;
  transition-delay: revert;
}

:where(.kodza-hero-search__eyebrow):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__title):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__lead):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__sr):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__radio):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__chevron):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__list):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__highlight):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__arrow):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__note):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__media):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__caption):not(#_):where(:not([hidden])),
:where(.kodza-hero-search__option svg:where(:not([class*="kodza-"]))):not(#_):where(:not([hidden])) {
  display: revert;
}

:where(.kodza-hero-search__eyebrow):not(#_):where([hidden]),
:where(.kodza-hero-search__title):not(#_):where([hidden]),
:where(.kodza-hero-search__lead):not(#_):where([hidden]),
:where(.kodza-hero-search__sr):not(#_):where([hidden]),
:where(.kodza-hero-search__radio):not(#_):where([hidden]),
:where(.kodza-hero-search__chevron):not(#_):where([hidden]),
:where(.kodza-hero-search__list):not(#_):where([hidden]),
:where(.kodza-hero-search__highlight):not(#_):where([hidden]),
:where(.kodza-hero-search__arrow):not(#_):where([hidden]),
:where(.kodza-hero-search__note):not(#_):where([hidden]),
:where(.kodza-hero-search__media):not(#_):where([hidden]),
:where(.kodza-hero-search__caption):not(#_):where([hidden]),
:where(.kodza-hero-search__option svg:where(:not([class*="kodza-"]))):not(#_):where([hidden]) {
  display: none;
}

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

.kodza-hero-search__inner:not(#_),
.kodza-hero-search__copy:not(#_),
.kodza-hero-search__rise:not(#_),
.kodza-hero-search__line:not(#_),
.kodza-hero-search__indicator:not(#_),
.kodza-hero-search__ghost:not(#_),
.kodza-hero-search__fields:not(#_),
.kodza-hero-search__label:not(#_),
.kodza-hero-search__example:not(#_),
.kodza-hero-search__value:not(#_),
.kodza-hero-search__count:not(#_),
.kodza-hero-search__frame:not(#_),
.kodza-hero-search__curtain:not(#_),
.kodza-hero-search__hold:not(#_),
.kodza-hero-search__meta:not(#_),
.kodza-hero-search__price:not(#_),
.kodza-hero-search__mode span:where(:not([class*="kodza-"])):not(#_),
.kodza-hero-search__example span:where(:not([class*="kodza-"])):not(#_),
.kodza-hero-search__value span:where(:not([class*="kodza-"])):not(#_),
.kodza-hero-search__count span:where(:not([class*="kodza-"])):not(#_),
.kodza-hero-search__price span:where(:not([class*="kodza-"])):not(#_) {
  box-sizing: revert;
}

.kodza-hero-search__eyebrow:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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;
  line-height: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__title:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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-family: revert;
  font-style: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__lead:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__modes:not(#_) {
  outline: revert;
  text-decoration: revert;
  list-style: revert;
  box-sizing: revert;
  float: 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;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__sr:not(#_) {
  border: revert;
  margin: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__mode:not(#_) {
  border: revert;
  margin: revert;
  text-decoration: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  box-shadow: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-family: 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-hero-search__radio:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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;
  transition-property: revert;
  transition-timing-function: revert;
  white-space: revert;
  min-height: revert;
}

.kodza-hero-search__field:not(#_) {
  border: revert;
  margin: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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-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-hero-search__input:not(#_),
.kodza-hero-search__trigger:not(#_) {
  text-decoration: revert;
  list-style: revert;
  float: revert;
  position: revert;
  outline-offset: revert;
  max-width: revert;
  max-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  white-space: revert;
  min-height: revert;
}

.kodza-hero-search__native:not(#_) {
  text-decoration: revert;
  list-style: revert;
  float: revert;
  outline-offset: revert;
  max-width: revert;
  max-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  white-space: revert;
  min-height: revert;
}

.kodza-hero-search__chevron:not(#_),
.kodza-hero-search__option svg:where(:not([class*="kodza-"])):not(#_) {
  border: revert;
  margin: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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-hero-search__list:not(#_) {
  border: revert;
  outline: revert;
  text-decoration: revert;
  float: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: 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-hero-search__highlight:not(#_) {
  border: revert;
  margin: revert;
  padding: revert;
  outline: revert;
  text-decoration: revert;
  list-style: revert;
  box-sizing: revert;
  float: 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-hero-search__option:not(#_) {
  border: revert;
  outline: revert;
  text-decoration: 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;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__submit:not(#_) {
  text-decoration: revert;
  list-style: revert;
  float: revert;
  position: revert;
  box-shadow: revert;
  appearance: revert;
  max-width: revert;
  max-height: 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;
  transition-property: revert;
  transition-timing-function: revert;
  min-height: revert;
}

.kodza-hero-search__arrow:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  margin-top: revert;
  margin-right: revert;
  margin-bottom: 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-hero-search__note:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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-weight: revert;
  font-style: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__media:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__tag:not(#_) {
  border: revert;
  outline: revert;
  text-decoration: revert;
  list-style: revert;
  float: revert;
  outline-offset: revert;
  appearance: revert;
  max-height: 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;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__caption:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  margin-right: revert;
  margin-bottom: revert;
  margin-left: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: 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;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-hero-search__native option:where(:not([class*="kodza-"])):not(#_) {
  box-sizing: revert;
  color: revert;
}

.kodza-hero-search__hold img:where(:not([class*="kodza-"])):not(#_) {
  border: revert;
  margin: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: 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;
  transition-property: revert;
  transition-timing-function: 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-hero-search__eyebrow:not(#_) {
  margin: 0 0 22px;
  font-family: var(--kz-hero-search-mono);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--kz-hero-search-faint);
}

.kodza-hero-search__title:not(#_) {
  margin: 0 0 24px;
  font-size: clamp(40px, 5.6vw, 84px);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.05em;
  color: var(--kz-hero-search-ink);
}

.kodza-hero-search__lead:not(#_) {
  max-width: 34ch;
  margin: 0 0 clamp(28px, 3vw, 40px);
  font-size: clamp(17px, 1.45vw, 19px);
  line-height: 1.55;
  color: var(--kz-hero-search-dim);
}

.kodza-hero-search__modes:not(#_) {
  position: relative;
  display: inline-flex;
  margin: 0;
  padding: 4px;
  border: 0;
  border-radius: 999px;
  background: var(--kz-hero-search-surface);
  box-shadow: inset 0 0 0 1px var(--kz-hero-search-field);
}

.kodza-hero-search.is-live .kodza-hero-search__indicator:not(#_),
.kodza-hero-search.is-live .kodza-hero-search__mirror:not(#_) {
  transition: clip-path 0.5s var(--kz-hero-search-ease);
}

.kodza-hero-search__mode:not(#_) {
  display: inline-flex;
  padding: 0 18px;
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
}

.kodza-hero-search__mode:not(#_) {
  position: relative;
  border-radius: 999px;
  color: var(--kz-hero-search-dim);
  transition: color 0.3s var(--kz-hero-search-ease);
}

.kodza-hero-search__mode:hover:not(#_),
.kodza-hero-search__mode:has(.kodza-hero-search__radio:checked):not(#_) {
  color: var(--kz-hero-search-ink);
}

.kodza-hero-search:not(.is-ready) .kodza-hero-search__mode:has(.kodza-hero-search__radio:checked):not(#_) {
  background: var(--kz-hero-search-ink);
  color: var(--kz-hero-search-paper);
}

.kodza-hero-search__mode:has(.kodza-hero-search__radio:focus-visible):not(#_) {
  outline: 2px solid var(--kz-hero-search-ink);
  outline-offset: 2px;
}

.kodza-hero-search__radio:not(#_) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.kodza-hero-search__field:not(#_) {
  position: relative;
  display: block;
  color: var(--kz-hero-search-ink);
}

.kodza-hero-search__input:not(#_),
.kodza-hero-search__native:not(#_),
.kodza-hero-search__trigger:not(#_) {
  box-sizing: border-box;
  display: flex;
  align-items: flex-end;
  width: 100%;
  height: var(--kz-hero-search-height);
  margin: 0;
  padding: 25px 16px 9px;
  border: 0;
  border-radius: 12px;
  background: var(--kz-hero-search-surface);
  box-shadow: inset 0 0 0 1px var(--kz-hero-search-field);
  color: inherit;
  font: inherit;
  font-size: 15px;
  font-weight: 400;
  line-height: 22px;
  text-align: left;
  appearance: none;
  transition: box-shadow 0.25s ease;
}

.kodza-hero-search__native:not(#_),
.kodza-hero-search__trigger:not(#_) {
  padding-right: 44px;
  cursor: pointer;
}

.kodza-hero-search__native:not(#_) {
  background: var(--kz-hero-search-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6.5 8 10.5l4-4' fill='none' stroke='%23999' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 16px center / 16px no-repeat;
}

.kodza-hero-search__trigger:not(#_) {
  padding-top: 0;
}

.kodza-hero-search__input:hover:not(#_),
.kodza-hero-search__native:hover:not(#_),
.kodza-hero-search__trigger:hover:not(#_) {
  box-shadow: inset 0 0 0 1px var(--kz-hero-search-field-hover);
}

.kodza-hero-search__input:focus:not(#_),
.kodza-hero-search__native:focus-visible:not(#_),
.kodza-hero-search__trigger:focus-visible:not(#_),
.kodza-hero-search__select.is-open .kodza-hero-search__trigger:not(#_) {
  outline: none;
  box-shadow: inset 0 0 0 1.5px var(--kz-hero-search-ink), 0 0 0 4px var(--kz-hero-search-soft);
}

.kodza-hero-search__input:not(#_)::placeholder {
  color: var(--kz-hero-search-dim);
  opacity: 1;
}

.kodza-hero-search__field.is-rolling .kodza-hero-search__input:not(#_)::placeholder {
  color: transparent;
}

.kodza-hero-search:not(.is-ready):has(.kodza-hero-search__radio[value="rent"]:checked) [data-mode="buy"]:not(#_),
.kodza-hero-search:not(.is-ready):has(.kodza-hero-search__radio[value="buy"]:checked) [data-mode="rent"]:not(#_) {
  display: none;
}

.kodza-hero-search__select:not(#_) {
  transition: z-index 0s linear 0.35s;
}

.kodza-hero-search__select.is-open:not(#_) {
  transition-delay: 0s;
}

.kodza-hero-search.is-ready .kodza-hero-search__native:not(#_) {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  clip-path: inset(50%);
}

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

.kodza-hero-search__chevron:not(#_) {
  position: absolute;
  transition: transform 0.35s var(--kz-hero-search-ease);
}

.kodza-hero-search__list:not(#_) {
  position: absolute;
  box-sizing: border-box;
  max-height: 280px;
  margin: 0;
  padding: 6px;
  border-radius: 14px;
  background: var(--kz-hero-search-surface);
  box-shadow: 0 0 0 1px var(--kz-hero-search-field), var(--kz-hero-search-pop);
  list-style: none;
  transition: clip-path 0.35s var(--kz-hero-search-ease),
    opacity 0s linear 0.35s,
    transform 0.35s var(--kz-hero-search-ease),
    visibility 0s linear 0.35s;
}

.kodza-hero-search__select.is-open .kodza-hero-search__list:not(#_) {
  transition-delay: 0s;
}

.kodza-hero-search__highlight:not(#_) {
  position: absolute;
  border-radius: 8px;
  background: var(--kz-hero-search-soft);
}

.kodza-hero-search__select.is-open .kodza-hero-search__highlight:not(#_) {
  transition: transform 0.22s var(--kz-hero-search-ease), opacity 0.15s ease;
}

.kodza-hero-search__option:not(#_) {
  position: relative;
  display: flex;
  margin: 0;
  padding: 0 10px 0 32px;
  border-radius: 8px;
  color: var(--kz-hero-search-ink);
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
}

.kodza-hero-search__option[aria-disabled="true"]:not(#_) {
  color: var(--kz-hero-search-dim);
}

.kodza-hero-search__option svg:not(#_) {
  position: absolute;
  transition: stroke-dashoffset 0.15s ease-in;
}

.kodza-hero-search__option[aria-selected="true"] svg:not(#_) {
  transition: stroke-dashoffset 0.35s var(--kz-hero-search-ease) 0.05s;
}

.kodza-hero-search__submit:not(#_) {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  height: 52px;
  margin: 0;
  padding: 0 24px;
  border: 0;
  border-radius: 999px;
  background: var(--kz-hero-search-ink);
  color: var(--kz-hero-search-paper);
  font-family: inherit;
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
}

.kodza-hero-search__submit:focus-visible:not(#_) {
  outline: 2px solid var(--kz-hero-search-ink);
  outline-offset: 3px;
}

.kodza-hero-search__arrow:not(#_) {
  margin-left: 4px;
  transition: transform 0.3s var(--kz-hero-search-ease);
}

.kodza-hero-search__note:not(#_) {
  margin: 0;
  font-family: var(--kz-hero-search-mono);
  font-size: 12px;
  line-height: 1.5;
  color: var(--kz-hero-search-faint);
  text-align: center;
}

.kodza-hero-search__media:not(#_) {
  margin: 0;
}

.kodza-hero-search__frame img:not(#_) {
  display: block;
}

.kodza-hero-search__tag:not(#_) {
  position: absolute;
  box-sizing: border-box;
  display: grid;
  max-width: calc(100% - 32px);
  margin: 0;
  padding: 12px 16px 13px;
  border-radius: 12px;
  background: var(--kz-hero-search-surface);
  box-shadow: 0 0 0 1px var(--kz-hero-search-line), var(--kz-hero-search-shadow);
  color: var(--kz-hero-search-ink);
}

.kodza-hero-search__caption:not(#_) {
  margin-top: 12px;
  font-family: var(--kz-hero-search-mono);
  font-size: 12.5px;
  color: var(--kz-hero-search-faint);
}

.kodza-hero-search__sr:not(#_) {
  position: absolute;
  padding: 0;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .kodza-hero-search *:not(#_),
  .kodza-hero-search *:not(#_)::before,
  .kodza-hero-search *:not(#_)::after {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
    animation-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 — Hero Search
// Buy and Rent stay native radios: an ink pill, with a paper copy of the labels cut to the same
// shape, slides from one to the other. Type and Budget keep their real <select>: the list of Kodza
// Select is built over each one (options read again when the page changes them, "change" fired on
// the real one) and its value rolls. A field may hold one select per mode (data-mode): the chosen
// mode's one is on, the other disabled, so the form only sends one; the listing's price, written
// per mode, rolls with it. While the place is empty and nothing in the form is in use, its
// examples (data-examples) roll one after the other, the placeholder saying the one on screen. A
// number the page writes into the count rolls in, the button easing to its new width. The
// entrance is Hero Split's, played once the section is in view, its picture decoded and the fonts
// in; "kodza:replay" plays it again, from the first example.
(() => {
  const SELECTOR = '.kodza-hero-search';
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const GLIDE = 'cubic-bezier(0.6, 0, 0.1, 1)'; // slow start, long settle
  const SOFT = 'cubic-bezier(0.25, 1, 0.5, 1)';
  const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)';
  const EVERY = 2600; // ms between two examples
  const FIRST = 3200; // ms from the start of the entrance to the first example
  const tick = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7" pathLength="1"/></svg>';
  const chevron = '<svg class="kodza-hero-search__chevron" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6.5 8 10.5l4-4"/></svg>';
  let uid = 0;

  const decoded = (root) => Promise.all([...root.querySelectorAll('img')].map((img) => img.decode().catch(() => {})));
  const span = (text) => Object.assign(document.createElement('span'), { textContent: text });
  const letter = (event) => event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;

  // One instance, with its own state: two copies on a page never interfere. Everything it
  // starts is tied to `signal` (listeners get { signal }, observers and timers stop on its
  // 'abort' event), so kodza:destroy stops it all.
  function mount(root, signal) {
    const q = (s) => root.querySelector(s);
    const form = q('.kodza-hero-search__form');
    const modes = q('.kodza-hero-search__modes');
    const radios = [...root.querySelectorAll('.kodza-hero-search__radio')];
    const input = q('.kodza-hero-search__input');
    const where = input?.closest('.kodza-hero-search__field');
    const count = q('.kodza-hero-search__count');
    const prices = [...root.querySelectorAll('.kodza-hero-search__price > [data-mode]')];
    const fields = [];
    const entrance = [];

    const stop = (list) => {
      list.forEach((a) => a.cancel());
      list.length = 0;
    };
    const animate = (list, el, keyframes, options) => {
      if (!el) return null;
      const animation = el.animate(keyframes, { easing: GLIDE, fill: 'backwards', ...options });
      list.push(animation);
      return animation;
    };

    // The old text rises out of its window as the new one comes from below. A change that lands
    // mid-roll drops the one already leaving, and the old one leaves from where it stands
    const roll = (box, text, moving, duration = 700) => {
      const old = box.lastElementChild;
      if (old?.textContent === text) return;
      [...box.children].forEach((el) => el !== old && el.remove());
      const next = span(text);
      box.append(next);
      if (!old) return;
      if (!moving || reduceMotion) return old.remove();
      const y = new DOMMatrixReadOnly(getComputedStyle(old).transform).m42;
      old.getAnimations().forEach((a) => a.cancel());
      const timing = { duration, easing: EASE };
      old.animate([{ transform: `translateY(${y}px)` }, { transform: 'translateY(-110%)' }], { ...timing, fill: 'forwards' }).finished.then(() => old.remove(), () => {});
      next.animate([{ transform: 'translateY(110%)' }, { transform: 'none' }], timing);
    };

    // Buy / Rent: the pill and its paper copy are cut to the chosen label
    const indicator = Object.assign(document.createElement('span'), { className: 'kodza-hero-search__indicator' });
    const mirror = Object.assign(document.createElement('span'), { className: 'kodza-hero-search__mirror' });
    [indicator, mirror].forEach((el) => el.setAttribute('aria-hidden', 'true'));
    radios.forEach((radio) => {
      const ghost = span(radio.closest('label')?.textContent.trim() ?? '');
      ghost.className = 'kodza-hero-search__ghost';
      mirror.append(ghost);
    });
    modes?.prepend(indicator);
    modes?.append(mirror);
    const chosen = () => radios.find((radio) => radio.checked) ?? radios[0];
    const mode = () => chosen()?.value;
    const place = () => {
      const label = chosen()?.closest('label');
      if (!label || !modes.offsetWidth) return;
      modes.style.setProperty('--kz-hero-search-l', `${label.offsetLeft}px`);
      modes.style.setProperty('--kz-hero-search-r', `${modes.offsetWidth - label.offsetLeft - label.offsetWidth}px`);
    };
    place();
    // The pill only slides once it stands in place: never on arrival or after a resize
    requestAnimationFrame(() => requestAnimationFrame(() => {
      if (!signal.aborted) root.classList.add('is-live');
    }));
    const resize = new ResizeObserver(() => place());
    if (modes) resize.observe(modes);

    // Where: its examples roll while it is empty, nothing in the form has the focus, no list is
    // open, and the section is on screen with the tab in front
    const examples = (input?.dataset.examples ?? '').split(',').map((s) => s.trim()).filter(Boolean);
    const example = Object.assign(document.createElement('span'), { className: 'kodza-hero-search__example' });
    example.setAttribute('aria-hidden', 'true');
    let index = 0;
    let timer = 0;
    let shown = false;
    let visible = false;
    const rolling = examples.length > 1 && !reduceMotion;
    const busy = () => form?.contains(document.activeElement) || fields.some((field) => field.isOpen());
    const free = () => rolling && shown && visible && !document.hidden && !input.value && !busy();
    const schedule = (delay = EVERY) => {
      clearTimeout(timer);
      where?.classList.toggle('is-rolling', rolling && !input.value && document.activeElement !== input);
      if (free()) timer = setTimeout(next, delay);
    };
    const next = () => {
      if (!free()) return;
      index = (index + 1) % examples.length;
      input.placeholder = examples[index];
      roll(example, examples[index], true);
      schedule();
    };
    if (rolling) {
      input.placeholder = examples[0];
      example.append(span(examples[0]));
      input.after(example);
      input.addEventListener('input', () => schedule(), { signal });
      ['focusin', 'focusout'].forEach((type) => form?.addEventListener(type, () => schedule(), { signal }));
      document.addEventListener('visibilitychange', () => schedule(), { signal });
      const seen = new IntersectionObserver((entries) => {
        visible = entries.some((entry) => entry.isIntersecting);
        schedule();
      });
      seen.observe(input);
      signal.addEventListener('abort', () => seen.disconnect());
    }

    // Type and Budget: the list of Kodza Select over the real select. Keyboard as in the ARIA
    // "select-only combobox" pattern: the focus stays on the field, arrows move, letters jump,
    // Enter picks, Escape closes
    const dress = (field) => {
      const natives = [...field.querySelectorAll('.kodza-hero-search__native')];
      if (!natives.length) return;
      const label = field.querySelector('.kodza-hero-search__label');
      const id = `kodza-hero-search-${++uid}`;
      const current = () => natives.find((select) => select.dataset.mode === mode()) ?? natives[0];
      let native = current();

      // The field: a button that says the current value
      const trigger = document.createElement('button');
      trigger.type = 'button';
      trigger.className = 'kodza-hero-search__trigger';
      trigger.setAttribute('role', 'combobox');
      trigger.setAttribute('aria-haspopup', 'listbox');
      trigger.setAttribute('aria-expanded', 'false');
      trigger.setAttribute('aria-controls', `${id}-list`);
      if (label) {
        label.id = `${id}-label`;
        trigger.setAttribute('aria-labelledby', `${id}-label`);
      }
      const value = Object.assign(document.createElement('span'), { className: 'kodza-hero-search__value' });
      trigger.append(value);
      trigger.insertAdjacentHTML('beforeend', chevron);

      // The list, from the real options (built again whenever the page changes them)
      const list = document.createElement('ul');
      list.className = 'kodza-hero-search__list';
      list.id = `${id}-list`;
      list.setAttribute('role', 'listbox');
      if (label) list.setAttribute('aria-labelledby', `${id}-label`);
      const highlight = document.createElement('li');
      highlight.className = 'kodza-hero-search__highlight';
      highlight.setAttribute('role', 'presentation');
      list.append(highlight);
      let options = [];
      const entries = new Map();
      const build = () => {
        options = [];
        entries.clear();
        [...list.children].forEach((child) => child !== highlight && child.remove());
        [...native.options].forEach((option) => {
          if (option.hidden) return; // a placeholder: "nothing chosen yet" is not a choice
          const item = document.createElement('li');
          item.className = 'kodza-hero-search__option';
          item.id = `${id}-option-${options.length}`;
          item.setAttribute('role', 'option');
          item.innerHTML = tick;
          item.append(option.textContent.trim());
          if (option.disabled || option.closest('optgroup')?.disabled) item.setAttribute('aria-disabled', 'true');
          list.append(item);
          const entry = { option, item };
          options.push(entry);
          entries.set(item, entry);
        });
      };
      build();
      natives.at(-1).after(trigger, list);
      natives.forEach((select) => {
        select.tabIndex = -1;
        select.setAttribute('aria-hidden', 'true');
      });

      const enabled = () => options.filter(({ item }) => item.getAttribute('aria-disabled') !== 'true');
      const isOpen = () => field.classList.contains('is-open');
      let active = null;

      const sync = (moving) => {
        options.forEach(({ option, item }) => item.setAttribute('aria-selected', String(option.selected)));
        const picked = native.selectedOptions[0];
        roll(value, picked && !picked.hidden ? picked.textContent.trim() : '', moving, 420);
      };

      const light = (entry, scroll = true) => {
        active = entry;
        if (!entry) {
          trigger.removeAttribute('aria-activedescendant');
          return list.style.setProperty('--kz-hl-on', 0);
        }
        trigger.setAttribute('aria-activedescendant', entry.item.id);
        list.style.setProperty('--kz-hl-y', `${entry.item.offsetTop}px`);
        list.style.setProperty('--kz-hl-h', `${entry.item.offsetHeight}px`);
        list.style.setProperty('--kz-hl-on', 1);
        // Scroll the list only (scrollIntoView would also nudge the page), with the list's own
        // padding above the very first row
        if (scroll) {
          const top = entry.item.previousElementSibling === highlight ? 0 : entry.item.offsetTop;
          const bottom = entry.item.offsetTop + entry.item.offsetHeight;
          if (top < list.scrollTop) list.scrollTop = top;
          else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
        }
      };

      const open = () => {
        if (isOpen()) return;
        // Not enough room below and more above: the list opens upwards. A closed list that changes
        // side gets there at once, so it always unrolls from the field
        const box = trigger.getBoundingClientRect();
        const above = box.bottom + Math.min(280, list.scrollHeight) + 16 > innerHeight && box.top > innerHeight / 2;
        if (above !== field.classList.contains('is-above')) {
          list.style.transition = 'none';
          field.classList.toggle('is-above', above);
          void list.offsetWidth;
          list.style.transition = '';
        }
        field.classList.add('is-open');
        trigger.setAttribute('aria-expanded', 'true');
        light(options.find(({ option }) => option.selected) || enabled()[0]);
        schedule();
      };

      const close = () => {
        if (!isOpen()) return;
        field.classList.remove('is-open');
        trigger.setAttribute('aria-expanded', 'false');
        light(null);
        schedule();
      };

      const pick = (entry) => {
        if (!entry || entry.item.getAttribute('aria-disabled') === 'true') return;
        const changed = !entry.option.selected;
        entry.option.selected = true;
        sync(true);
        close();
        if (changed) native.dispatchEvent(new Event('change', { bubbles: true }));
      };

      // Typing a few letters jumps to the first matching option
      let typed = '';
      let typedTimer = 0;
      const typeAhead = (char) => {
        clearTimeout(typedTimer);
        typed += char.toLowerCase();
        typedTimer = setTimeout(() => (typed = ''), 600);
        const pool = enabled();
        const start = active ? pool.indexOf(active) + (typed.length === 1 ? 1 : 0) : 0;
        const order = [...pool.slice(start), ...pool.slice(0, start)];
        const match = order.find(({ item }) => item.textContent.trim().toLowerCase().startsWith(typed));
        if (!match) return;
        if (isOpen()) light(match);
        else pick(match);
      };

      trigger.addEventListener('click', () => (isOpen() ? close() : open()), { signal });
      trigger.addEventListener('keydown', (event) => {
        const pool = enabled();
        const at = pool.indexOf(active);
        if (!isOpen()) {
          if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
            event.preventDefault();
            open();
          } else if (letter(event)) {
            typeAhead(event.key);
          }
          return;
        }
        const moves = { ArrowDown: at + 1, ArrowUp: at - 1, Home: 0, End: pool.length - 1, PageDown: at + 5, PageUp: at - 5 };
        if (event.key in moves) {
          event.preventDefault();
          light(pool[Math.max(0, Math.min(pool.length - 1, moves[event.key]))]);
        } else if (event.key === 'Enter' || event.key === ' ') {
          event.preventDefault();
          pick(active);
        } else if (event.key === 'Tab') {
          pick(active);
        } else if (letter(event)) {
          typeAhead(event.key);
        }
      }, { signal });

      const entryAt = (event) => entries.get(event.target.closest?.('.kodza-hero-search__option'));
      list.addEventListener('pointermove', (event) => {
        const entry = entryAt(event);
        if (entry && active !== entry && entry.item.getAttribute('aria-disabled') !== 'true') light(entry, false);
      }, { signal });
      list.addEventListener('click', (event) => pick(entryAt(event)), { signal });
      // Keep the list from stealing the focus from the field, and the wrapping <label> from
      // sending clicks to the hidden select: a click on the label text focuses the field
      list.addEventListener('mousedown', (event) => event.preventDefault(), { signal });
      field.addEventListener('click', (event) => {
        if (trigger.contains(event.target)) return;
        event.preventDefault();
        if (!list.contains(event.target)) trigger.focus();
      }, { signal });
      document.addEventListener('pointerdown', (event) => !field.contains(event.target) && close(), { signal });
      // Escape closes it wherever the focus is: opened with a click, it stays on the field
      // (Chrome) or on the page (Safari)
      document.addEventListener('keydown', (event) => {
        if (event.key !== 'Escape' || !isOpen()) return;
        event.preventDefault();
        close();
        trigger.focus();
      }, { signal });
      // The real select changed from elsewhere (a form reset, a script): follow it
      natives.forEach((select) => select.addEventListener('change', () => select === native && sync(true), { signal }));
      native.form?.addEventListener('reset', () => setTimeout(() => sync(true)), { signal });

      // The page changed the options (counts, disabled ones, a new list): build the list again,
      // keep the highlight on the same option, and show the value without a roll. An attribute
      // written again with the value it already had changes nothing
      let queued = false;
      const changed = (record) => record.type !== 'attributes' || record.oldValue !== record.target.getAttribute(record.attributeName);
      const observer = new MutationObserver((records) => {
        if (queued || !records.some(changed)) return;
        queued = true;
        queueMicrotask(() => {
          queued = false;
          if (signal.aborted) return;
          const was = active?.option;
          build();
          sync(false);
          if (isOpen()) light(options.find(({ option }) => option === was) || options.find(({ option }) => option.selected) || enabled()[0], false);
        });
      });
      natives.forEach((select) => observer.observe(select, { childList: true, subtree: true, characterData: true, attributes: true, attributeOldValue: true, attributeFilter: ['disabled', 'hidden', 'label'] }));
      signal.addEventListener('abort', () => observer.disconnect());

      // The mode changed: its own select comes on, the other one is switched off, and the value rolls
      const follow = (moving) => {
        const on = current();
        natives.forEach((select) => {
          select.hidden = select !== on;
          select.disabled = select !== on;
        });
        observer.takeRecords();
        if (on === native) return;
        native = on;
        close();
        build();
        sync(moving);
      };
      follow(false);
      sync(false);
      fields.push({ isOpen, close, follow });
    };
    root.querySelectorAll('.kodza-hero-search__select').forEach(dress);

    // The listing's price: the chosen mode's one, rolling in when the mode changes
    const price = (moving) => {
      const on = prices.find((el) => el.dataset.mode === mode()) ?? prices[0];
      const was = prices.find((el) => el.classList.contains('is-on'));
      prices.forEach((el) => el.classList.toggle('is-on', el === on));
      if (!moving || reduceMotion || !was || was === on) return;
      const y = new DOMMatrixReadOnly(getComputedStyle(was).transform).m42;
      prices.forEach((el) => el.getAnimations().forEach((a) => a.cancel()));
      const timing = { duration: 700, easing: EASE };
      was.animate([{ visibility: 'visible', transform: `translateY(${y}px)` }, { visibility: 'visible', transform: 'translateY(-110%)' }], timing);
      on.animate([{ transform: 'translateY(110%)' }, { transform: 'none' }], timing);
    };
    price(false);

    radios.forEach((radio) => radio.addEventListener('change', () => {
      place();
      fields.forEach((field) => field.follow(true));
      price(true);
    }, { signal }));

    // The count: a number the page writes in rolls, and the button eases to its new width
    if (count) {
      let current = span(count.textContent.trim());
      count.replaceChildren(current);
      const counted = new MutationObserver(() => {
        const fresh = [...count.childNodes].filter((node) => node.nodeType === Node.TEXT_NODE);
        if (!fresh.length) return; // our own spans coming and going
        const text = fresh.map((node) => node.textContent).join('').trim();
        count.replaceChildren(current);
        const from = count.offsetWidth;
        roll(count, text, true, 420);
        current = count.lastElementChild;
        const to = current.offsetWidth;
        counted.takeRecords();
        if (reduceMotion || from === to) return;
        count.getAnimations().forEach((a) => a.cancel());
        count.animate([{ width: `${from}px` }, { width: `${to}px` }], { duration: 420, easing: EASE });
      });
      counted.observe(count, { childList: true });
      signal.addEventListener('abort', () => counted.disconnect());
    }

    signal.addEventListener('abort', () => {
      clearTimeout(timer);
      resize.disconnect();
      stop(entrance);
    });

    if (reduceMotion) {
      root.classList.add('is-shown');
      return;
    }

    const rise = (el, delay, duration) => animate(entrance, el, [{ transform: 'translateY(105%)' }, { transform: 'none' }], { duration, delay });
    const play = async () => {
      await decoded(root);
      if (signal.aborted) return;
      stop(entrance);
      fields.forEach((field) => field.close());
      // The examples start again from the first one, hidden while the form comes in
      if (rolling) {
        clearTimeout(timer);
        index = 0;
        input.placeholder = examples[0];
        roll(example, examples[0], false);
      }
      root.classList.add('is-shown');
      rise(q('.kodza-hero-search__eyebrow .kodza-hero-search__rise'), 0, 1000);
      root.querySelectorAll('.kodza-hero-search__line .kodza-hero-search__rise').forEach((line, i) => rise(line, 110 + i * 125, 1170));
      // The curtain: both layers on transforms, the picture settles from a slight zoom
      animate(entrance, q('.kodza-hero-search__curtain'), [{ transform: 'translateY(-100%)' }, { transform: 'none' }], { duration: 1350, delay: 200 });
      animate(entrance, q('.kodza-hero-search__hold'), [{ transform: 'translateY(100%)' }, { transform: 'none' }], { duration: 1350, delay: 200 });
      animate(entrance, q('.kodza-hero-search__frame img'), [{ transform: 'scale(1.2)' }, { transform: 'none' }], { duration: 2000, delay: 200, easing: SOFT });
      [q('.kodza-hero-search__lead'), form].forEach((el, i) => {
        animate(entrance, el, [{ opacity: 0, transform: 'translateY(20px)' }, { opacity: 1, transform: 'none' }], { duration: 900, delay: 700 + i * 110, easing: SOFT });
      });
      // The listing's tag lands once the picture is uncovered
      animate(entrance, q('.kodza-hero-search__tag'), [{ opacity: 0, transform: 'translateY(20px)' }, { opacity: 1, transform: 'none' }], { duration: 900, delay: 1150, easing: SOFT });
      // The faint caption is fully there halfway up: never read half-faded
      animate(entrance, q('.kodza-hero-search__caption'), [{ opacity: 0, transform: 'translateY(20px)' }, { opacity: 1, offset: 0.5 }, { opacity: 1, transform: 'none' }], { duration: 900, delay: 920, easing: SOFT });
      // The examples wait for the card to settle
      shown = true;
      if (rolling) schedule(FIRST);
    };

    root.addEventListener('kodza:replay', play, { signal });
    const io = new IntersectionObserver((entries) => {
      if (!entries.some((entry) => entry.isIntersecting)) return;
      io.disconnect();
      play();
    }, { rootMargin: '0px 0px -15% 0px' });
    signal.addEventListener('abort', () => io.disconnect());
    Promise.all([document.fonts?.ready, decoded(root)]).then(() => {
      if (!signal.aborted) io.observe(root);
    });
  }

  // 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. Each running root
  // keeps its markup as written under KODZA, shared by every Kodza script on the page: a Kodza
  // component inside another one is saved as written, not half-started, and stopped with it.
  const KODZA = Symbol.for('kodza');
  const instances = new WeakMap();
  const find = (scope = document) => [scope, ...scope.querySelectorAll(SELECTOR)].filter((el) => el.matches?.(SELECTOR));
  const attrsOf = (el) => [...el.attributes].map((a) => [a.name, a.value]);
  const put = (el, saved) => {
    [...el.attributes].forEach((a) => el.removeAttribute(a.name));
    saved.attrs.forEach(([name, value]) => el.setAttribute(name, value));
    el.innerHTML = saved.html;
  };
  // The markup inside a root as written: Kodza components already running in it give back their own
  const written = (root) => {
    const all = [...root.querySelectorAll('*')];
    if (!all.some((el) => el[KODZA])) return root.innerHTML;
    const copy = root.cloneNode(true);
    const twins = copy.querySelectorAll('*');
    let last = null;
    all.forEach((el, i) => {
      if (!el[KODZA] || last?.contains(el)) return;
      last = el;
      put(twins[i], el[KODZA]);
    });
    return copy.innerHTML;
  };
  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();
    const saved = { html: written(root), attrs: attrsOf(root), stop: () => destroy(root) };
    instances.set(root, { controller, saved });
    root[KODZA] = saved;
    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);
    // The Kodza components running inside it stop first: once its markup is put back, their own
    // script could not find them any more
    root.querySelectorAll('*').forEach((el) => el[KODZA]?.stop());
    instance.controller.abort();
    delete root[KODZA];
    put(root, instance.saved);
  });
  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).
