# Header Grid

> A site header placed on your grid: brand, links and actions each take the columns you name, one link per column if you like. Still by default; a gliding line, hide on scroll or tighten on scroll as options.

Header · header, nav, navbar, grid, sticky header, mobile menu, wp_nav_menu · HTML, CSS, vanilla JS · no dependencies · updated 2026-11-02
Demo: https://kodza.dev/components/header-grid/
Source on GitHub: https://github.com/kodza-dev/kodza/tree/main/components/header-grid

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

## Files

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

- [`kodza-header-grid.css`](https://kodza.dev/registry/header-grid/kodza-header-grid.css) · 51.3 KB
- [`kodza-header-grid.js`](https://kodza.dev/registry/header-grid/kodza-header-grid.js) · 22.6 KB
- [`kodza-header-grid.html`](https://kodza.dev/registry/header-grid/kodza-header-grid.html) · 1.5 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-header-grid.css https://kodza.dev/registry/header-grid/kodza-header-grid.css
curl -fsSL --create-dirs -o kodza/kodza-header-grid.js https://kodza.dev/registry/header-grid/kodza-header-grid.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-header-grid`
- Variables (set them on the root element itself: it declares its own defaults):
  - `--kz-header-grid-bg: transparent` — give it your page colour once it sticks
  - `--kz-header-grid-fg: #0a0a0a` — the brand, the page you are on, the link you point at
  - `--kz-header-grid-muted: rgba(10, 10, 10, 0.62)` — the notes
  - `--kz-header-grid-link: var(--kz-header-grid-muted)` — the other links
  - `--kz-header-grid-rule-color: var(--kz-header-grid-fg)` — the rules under the cells
  - `--kz-header-grid-ink: #0a0a0a` — the buttons
  - `--kz-header-grid-on-ink: #ffffff`
  - `--kz-header-grid-soft: rgba(10, 10, 10, 0.06)` — the Menu button
  - `--kz-header-grid-columns: minmax(0, 1fr) auto minmax(0, 1fr)` — an auto column centres what it holds
  - `--kz-header-grid-gap: 16px` — between the columns
  - `--kz-header-grid-brand-column: 1`
  - `--kz-header-grid-nav-column: 2`
  - `--kz-header-grid-actions-column: 3`
  - `--kz-header-grid-align: center` — end sets every zone on the bottom edge
  - `--kz-header-grid-width: 1200px` — the row, centred; none = the full width
  - `--kz-header-grid-pad: clamp(16px, 4cqw, 24px)` — on both sides
  - `--kz-header-grid-pad-block: 0px` — above and below (two values: above, then below)
  - `--kz-header-grid-height: 72px` — at least
  - `--kz-header-grid-brand-size: 18px`
  - `--kz-header-grid-mark: 1.5px` — the line under it, and under the link you point at
  - `--kz-header-grid-rule: 0px` — with --cells, the rule under every cell; 0 = none
  - `--kz-header-grid-position: static` — sticky keeps it at the top
  - `--kz-header-grid-top: var(--wp-admin--admin-bar--height, 0px)` — where it sticks: under the WordPress admin bar
  - `--kz-header-grid-breakpoint: 900px` — below it, data-mobile takes over
  - `--kz-header-grid-duration: 0.42s` — --line: the line gliding
  - `--kz-header-grid-lag: 0.08s` — how long its back edge waits
  - `--kz-header-grid-ease: cubic-bezier(0.22, 1, 0.36, 1)`
  - `--kz-header-grid-hide-after: 600px` — --hide: how far you read before it goes
  - `--kz-header-grid-tighten: 16px` — --tighten: the height the bar loses
  - `--kz-header-grid-tighten-after: 40px` — how far you scroll before it tightens
  - `--kz-header-grid-shrink: 0.86` — the brand's size once tight; 1 = it stays
  - `--kz-header-grid-menu-bg: #0a0a0a`
  - `--kz-header-grid-menu-fg: #f2f2f0`
  - `--kz-header-grid-menu-muted: rgba(242, 242, 240, 0.62)`
  - `--kz-header-grid-menu-line: rgba(242, 242, 240, 0.16)`
  - `--kz-header-grid-menu-side: 24px`
  - `--kz-header-grid-menu-size: calc(clamp(34px, min(10cqw, calc((100cqh - 240px) / 6)), 96px) * var(--kz-font-display-scale, 1))`
  - `--kz-header-grid-menu-duration: 0.8s`
  - `--kz-header-grid-menu-ease: cubic-bezier(0.76, 0, 0.24, 1)`
- Modifiers: `.kodza-header-grid--cells`, `.kodza-header-grid--hide`, `.kodza-header-grid--line`, `.kodza-header-grid--tighten`
- Attributes:
  - `data-mobile` (e.g. `menu`, `wrap`, `scroll`)
- 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 the Menu button, below the breakpoint (focus in `.kodza-header-grid__toggle`):
  - `Enter` / `Space`: Opens or closes the shutter
- In the shutter (focus in `.kodza-header-grid__panel a`):
  - `Escape`: Rolls it back up, focus back on Menu
  - `Tab`: Past the last link, rolls it up and goes on with the page

## 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-header-grid {
    --kz-header-grid-fg: #f2f2f0;
    --kz-header-grid-muted: rgba(242, 242, 240, 0.62);
    --kz-header-grid-soft: rgba(255, 255, 255, 0.08);
    --kz-header-grid-ink: #f2f2f0;
    --kz-header-grid-on-ink: #0a0a0a;
    --kz-header-grid-menu-bg: #f2f2f0;
    --kz-header-grid-menu-fg: #0a0a0a;
    --kz-header-grid-menu-muted: rgba(10, 10, 10, 0.62);
    --kz-header-grid-menu-line: rgba(10, 10, 10, 0.14);
  }
}
```

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

Every colour is a variable on the header: set --kz-header-grid-fg, -muted, -soft, -ink and -on-ink for a light bar on a dark site (the demo's dark band has the recipe), and -bg once it sticks. The shutter has its own --kz-header-grid-menu-* set (bg, fg, muted, line), so it can stay dark or turn to paper.

## Use it

### Plain HTML page

- CSS: `kodza/kodza-header-grid.css` next to the page (the Files commands as they are), linked in `<head>`: `<link rel="stylesheet" href="kodza/kodza-header-grid.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.
- Where it goes, step by step. (1) At the top of the page, as its only header. (2) Name your grid: --kz-header-grid-columns takes any grid-template-columns, and -brand-column, -nav-column and -actions-column any grid-column (2 / 5 spans three columns); an auto column centres what it holds. For your site's own grid, give it the same columns, gap (-gap) and side margin (-pad), and -width: none. (3) Add kodza-header-grid--cells to put each link and each note in a column of its own, on a rule (-rule, -mark). (4) Motion is opt-in: kodza-header-grid--line, --hide, --tighten (the last two stick it to the top: give it -bg). (5) Phones: data-mobile="menu", "wrap" or "scroll" below -breakpoint (900px); without it the row stays, set fewer columns in your own media query. Delete the Menu button and the panel if you don't use the menu.
- JS: `kodza/kodza-header-grid.js`, loaded with `<script src="kodza/kodza-header-grid.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. A site with its own type sets it once for every Kodza component, on `:root`, never by editing the CSS (unset, nothing changes): `--kz-font-display`, the display face (headings, big numbers, wordmarks); `--kz-font-display-weight`, its weight; `--kz-font-display-scale`, a multiplier on its sizes (1 by default); `--kz-font-display-tracking`, its letter-spacing; `--kz-font-display-case`, its case (`uppercase`…); `--kz-font-label`, the face of the small labels (meta, captions, counters, hints, footer links, button labels); `--kz-font-label-size`, their size, one for all of them (`11px`…); `--kz-font-label-weight`, their weight; `--kz-font-label-tracking`, their letter-spacing; `--kz-font-label-case`, their case (`uppercase`…; code and units keep theirs); `--kz-font-label-leading`, their line-height (`1.45`…; buttons, pills and keys keep their box); `--kz-font-mono`, the monospace face (code, labels, counters). 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">`.
- Corners: a site with its own shapes sets them once for every Kodza component, on `:root`, never by editing the CSS (unset, nothing changes): `--kz-radius-scale` multiplies every corner (`0` square, `0.5` softer, `1` Kodza); `--kz-radius-pill` sets the pills (`0` squares them).
- Motion: a site sets its pace once for every Kodza component, on `:root`, never by editing the CSS or the JS: `--kz-motion-scale` multiplies every duration (`0.5` twice as fast, `2` twice as slow, `0` no motion at all, loops stopped, like reduced motion). Unset, nothing changes.
- 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-header-grid.css' -e 'd' -e '}' -e '/^KODZA-JS$/{r kodza/kodza-header-grid.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">`. A site with its own type sets it once for every Kodza component, on `:root`, never by editing the CSS (unset, nothing changes): `--kz-font-display`, the display face (headings, big numbers, wordmarks); `--kz-font-display-weight`, its weight; `--kz-font-display-scale`, a multiplier on its sizes (1 by default); `--kz-font-display-tracking`, its letter-spacing; `--kz-font-display-case`, its case (`uppercase`…); `--kz-font-label`, the face of the small labels (meta, captions, counters, hints, footer links, button labels); `--kz-font-label-size`, their size, one for all of them (`11px`…); `--kz-font-label-weight`, their weight; `--kz-font-label-tracking`, their letter-spacing; `--kz-font-label-case`, their case (`uppercase`…; code and units keep theirs); `--kz-font-label-leading`, their line-height (`1.45`…; buttons, pills and keys keep their box); `--kz-font-mono`, the monospace face (code, labels, counters).
- Corners: a site with its own shapes sets them once for every Kodza component, on `:root`, never by editing the CSS (unset, nothing changes): `--kz-radius-scale` multiplies every corner (`0` square, `0.5` softer, `1` Kodza); `--kz-radius-pill` sets the pills (`0` squares them).
- Motion: a site sets its pace once for every Kodza component, on `:root`, never by editing the CSS or the JS: `--kz-motion-scale` multiplies every duration (`0.5` twice as fast, `2` twice as slow, `0` no motion at all, loops stopped, like reduced motion). Unset, nothing changes.
- 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.
- Where it goes, step by step. (1) At the top of the page, as its only header. (2) Name your grid: --kz-header-grid-columns takes any grid-template-columns, and -brand-column, -nav-column and -actions-column any grid-column (2 / 5 spans three columns); an auto column centres what it holds. For your site's own grid, give it the same columns, gap (-gap) and side margin (-pad), and -width: none. (3) Add kodza-header-grid--cells to put each link and each note in a column of its own, on a rule (-rule, -mark). (4) Motion is opt-in: kodza-header-grid--line, --hide, --tighten (the last two stick it to the top: give it -bg). (5) Phones: data-mobile="menu", "wrap" or "scroll" below -breakpoint (900px); without it the row stays, set fewer columns in your own media query. Delete the Menu button and the panel if you don't use the menu.

### 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-header-grid', $uri . 'kodza-header-grid.css', [], filemtime($dir . 'kodza-header-grid.css'));
    wp_enqueue_script('kodza-header-grid', $uri . 'kodza-header-grid.js', [], filemtime($dir . 'kodza-header-grid.js'), true);
  });
  ```
  Scoped to `kodza-` classes, the CSS never touches the theme. One copy of the JS, whatever the number of blocks.
- No access to the theme files? The CSS can also go, as it is, in **Appearance → Customize → Additional CSS** (classic theme) or **Styles → Additional CSS** in the Site Editor (block theme).
- Theme with AJAX page transitions: fire `kodza:init` on the new content after each swap (and `kodza:destroy` on the old one).
- Performance plugin (WP Rocket, LiteSpeed Cache, Perfmatters…) with **Delay JavaScript execution** turned on: add `kodza` to the scripts it must not delay. Otherwise the component waits for the visitor's first scroll, tap or mouse move, and that first click can be lost.
- Colours: it takes the colour of the text around it (or keeps its own monochrome): nothing to point at the theme.
- WordPress turns straight quotes and `--` into typographic ones in the text of a Custom HTML block (not inside `<code>` or `<pre>`).
- Put it in the site header (Site Editor → Patterns → Header, or the theme's header.php) in place of the theme's own header. The links can come from wp_nav_menu(['theme_location' => 'primary', 'container' => false, 'items_wrap' => '%3$s']) inside the ul, with the link class added by the nav_menu_link_attributes filter; WordPress already writes aria-current="page" on the current one. The shutter is written from those links: nothing else to fill. --kz-header-grid-top already keeps a sticky header under the admin bar.

#### 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-header-grid.css` and `assets/kodza-header-grid.js` (the Files commands with `-o assets/…`).
- In the section: `{{ 'kodza-header-grid.css' | asset_url | stylesheet_tag }}` and `<script src="{{ 'kodza-header-grid.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`.
- Where it goes, step by step. (1) At the top of the page, as its only header. (2) Name your grid: --kz-header-grid-columns takes any grid-template-columns, and -brand-column, -nav-column and -actions-column any grid-column (2 / 5 spans three columns); an auto column centres what it holds. For your site's own grid, give it the same columns, gap (-gap) and side margin (-pad), and -width: none. (3) Add kodza-header-grid--cells to put each link and each note in a column of its own, on a rule (-rule, -mark). (4) Motion is opt-in: kodza-header-grid--line, --hide, --tighten (the last two stick it to the top: give it -bg). (5) Phones: data-mobile="menu", "wrap" or "scroll" below -breakpoint (900px); without it the row stays, set fewer columns in your own media query. Delete the Menu button and the panel if you don't use the menu.

### React / Next.js

- Files, side by side (e.g. in `components/kodza/`): `HeaderGrid.tsx` (below), the CSS block under Code saved as `header-grid.css` and the JS block as `header-grid.js`, both as they are. The HTML block is already inside the component: skip it.
- Use it: `import { HeaderGrid } from '@/components/kodza/HeaderGrid';` then `<HeaderGrid />`. Props: `variant` ("cells", "hide", "line", "tighten", or several in an array), `mobile` (`data-mobile`: "menu", "wrap", "scroll"), `vars` (the `--kz-…` variables listed under API, e.g. `vars={{ '--kz-header-grid-bg': … }}`), `className`, `style` and the other props of its `<header>` (`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 './header-grid.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.
- Built from your data (tabs, rows, options)? Add what the markup is made of to the `key`, as plain values (ids, text, numbers). What the visitor changes (the open tab, the sort, what they type) is the script's: follow it with `onClick` or `onInput` on the root, and change it from your code as a visitor would (click the tab; set the field's value, then dispatch a bubbling `input` event). Fields stay uncontrolled (`defaultValue`, `name`).

```tsx
'use client';
// Kodza — Header Grid, for React. From https://kodza.dev/components/header-grid/
// 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: header-grid.css and header-grid.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.
// data-kodza-manual keeps the script from starting it on its own: the effect below does, once
// React has hydrated the markup (on a page that streams in, the script can load first).
// Built from your data (tabs, rows, options)? Put what the markup is made of in the key, as plain
// values (ids, text, numbers): a change starts the script again on fresh markup. What the visitor
// changes (the open tab, the sort, what they type) is the script's: keep it out of the key, follow
// it with onClick or onInput on the root, and change it from your code as a visitor would (click
// the tab; set the field's value, then dispatch a bubbling input event).
import { useRef, useEffect, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './header-grid.css';

type Variant = "cells" | "hide" | "line" | "tighten";

export type HeaderGridProps = Omit<ComponentPropsWithoutRef<'header'>, "children" | "variant" | "vars" | "mobile"> & {
  /** Modifier class(es): .kodza-header-grid--cells, .kodza-header-grid--hide, .kodza-header-grid--line, .kodza-header-grid--tighten */
  variant?: Variant | Variant[];
  /** data-mobile on the root (default: menu) */
  mobile?: "menu" | "wrap" | "scroll";
  /** Settings, as CSS variables on the root */
  vars?: {
    /** give it your page colour once it sticks — default: transparent */
    "--kz-header-grid-bg"?: string | number;
    /** the brand, the page you are on, the link you point at — default: #0a0a0a */
    "--kz-header-grid-fg"?: string | number;
    /** the notes — default: rgba(10, 10, 10, 0.62) */
    "--kz-header-grid-muted"?: string | number;
    /** the other links — default: var(--kz-header-grid-muted) */
    "--kz-header-grid-link"?: string | number;
    /** the rules under the cells — default: var(--kz-header-grid-fg) */
    "--kz-header-grid-rule-color"?: string | number;
    /** the buttons — default: #0a0a0a */
    "--kz-header-grid-ink"?: string | number;
    /** default: #ffffff */
    "--kz-header-grid-on-ink"?: string | number;
    /** the Menu button — default: rgba(10, 10, 10, 0.06) */
    "--kz-header-grid-soft"?: string | number;
    /** an auto column centres what it holds — default: minmax(0, 1fr) auto minmax(0, 1fr) */
    "--kz-header-grid-columns"?: string | number;
    /** between the columns — default: 16px */
    "--kz-header-grid-gap"?: string | number;
    /** default: 1 */
    "--kz-header-grid-brand-column"?: string | number;
    /** default: 2 */
    "--kz-header-grid-nav-column"?: string | number;
    /** default: 3 */
    "--kz-header-grid-actions-column"?: string | number;
    /** end sets every zone on the bottom edge — default: center */
    "--kz-header-grid-align"?: string | number;
    /** the row, centred; none = the full width — default: 1200px */
    "--kz-header-grid-width"?: string | number;
    /** on both sides — default: clamp(16px, 4cqw, 24px) */
    "--kz-header-grid-pad"?: string | number;
    /** above and below (two values: above, then below) — default: 0px */
    "--kz-header-grid-pad-block"?: string | number;
    /** at least — default: 72px */
    "--kz-header-grid-height"?: string | number;
    /** default: 18px */
    "--kz-header-grid-brand-size"?: string | number;
    /** the line under it, and under the link you point at — default: 1.5px */
    "--kz-header-grid-mark"?: string | number;
    /** with --cells, the rule under every cell; 0 = none — default: 0px */
    "--kz-header-grid-rule"?: string | number;
    /** sticky keeps it at the top — default: static */
    "--kz-header-grid-position"?: string | number;
    /** where it sticks: under the WordPress admin bar — default: var(--wp-admin--admin-bar--height, 0px) */
    "--kz-header-grid-top"?: string | number;
    /** below it, data-mobile takes over — default: 900px */
    "--kz-header-grid-breakpoint"?: string | number;
    /** --line: the line gliding — default: 0.42s */
    "--kz-header-grid-duration"?: string | number;
    /** how long its back edge waits — default: 0.08s */
    "--kz-header-grid-lag"?: string | number;
    /** default: cubic-bezier(0.22, 1, 0.36, 1) */
    "--kz-header-grid-ease"?: string | number;
    /** --hide: how far you read before it goes — default: 600px */
    "--kz-header-grid-hide-after"?: string | number;
    /** --tighten: the height the bar loses — default: 16px */
    "--kz-header-grid-tighten"?: string | number;
    /** how far you scroll before it tightens — default: 40px */
    "--kz-header-grid-tighten-after"?: string | number;
    /** the brand's size once tight; 1 = it stays — default: 0.86 */
    "--kz-header-grid-shrink"?: string | number;
    /** default: #0a0a0a */
    "--kz-header-grid-menu-bg"?: string | number;
    /** default: #f2f2f0 */
    "--kz-header-grid-menu-fg"?: string | number;
    /** default: rgba(242, 242, 240, 0.62) */
    "--kz-header-grid-menu-muted"?: string | number;
    /** default: rgba(242, 242, 240, 0.16) */
    "--kz-header-grid-menu-line"?: string | number;
    /** default: 24px */
    "--kz-header-grid-menu-side"?: string | number;
    /** default: calc(clamp(34px, min(10cqw, calc((100cqh - 240px) / 6)), 96px) * var(--kz-font-display-scale, 1)) */
    "--kz-header-grid-menu-size"?: string | number;
    /** default: 0.8s */
    "--kz-header-grid-menu-duration"?: string | number;
    /** default: cubic-bezier(0.76, 0, 0.24, 1) */
    "--kz-header-grid-menu-ease"?: string | number;
  };
  className?: string;
};

export function HeaderGrid({ variant, mobile = "menu", vars, className, style, ...rest }: HeaderGridProps) {
  const ref = useRef<ComponentRef<'header'>>(null);
  const classes = ["kodza-header-grid", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-header-grid--${v}`), 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, mobile, 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('./header-grid.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 (
    <header key={key} {...rest} className={classes} data-kodza-manual data-mobile={mobile} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
      <div className="kodza-header-grid__row">
        <a className="kodza-header-grid__brand" href="#" aria-label="Kiln, home">
          <svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true">
            <path d="M1 1h8v8H1zM11 1h8v8h-8zM1 11h8v8H1z" />
            <circle cx="15" cy="15" r="4" />
          </svg>
          <span>kiln</span>
        </a>
        <nav className="kodza-header-grid__nav" aria-label="Main">
          <ul className="kodza-header-grid__links">
            <li>
              <a className="kodza-header-grid__link" href="#" aria-current="page">Work</a>
            </li>
            <li>
              <a className="kodza-header-grid__link" href="#">Services</a>
            </li>
            <li>
              <a className="kodza-header-grid__link" href="#">Studio</a>
            </li>
            <li>
              <a className="kodza-header-grid__link" href="#">Journal</a>
            </li>
            <li>
              <a className="kodza-header-grid__link" href="#">Contact</a>
            </li>
          </ul>
        </nav>
        <div className="kodza-header-grid__actions">
          <a className="kodza-header-grid__button" href="#">Start a project</a>
          {' '}
          <button className="kodza-header-grid__toggle" type="button" aria-expanded="false">
            <span className="kodza-header-grid__lines" aria-hidden="true" />
            <span className="kodza-header-grid__label">
              <span className="kodza-header-grid__say">Menu</span>
              <span className="kodza-header-grid__say" aria-hidden="true">Close</span>
            </span>
          </button>
          <div className="kodza-header-grid__panel" popover="" aria-label="Menu">
            <nav className="kodza-header-grid__list" aria-label="Main" />
            <div className="kodza-header-grid__foot" />
          </div>
        </div>
      </div>
    </header>
  );
}
```

- Where it goes, step by step. (1) At the top of the page, as its only header. (2) Name your grid: --kz-header-grid-columns takes any grid-template-columns, and -brand-column, -nav-column and -actions-column any grid-column (2 / 5 spans three columns); an auto column centres what it holds. For your site's own grid, give it the same columns, gap (-gap) and side margin (-pad), and -width: none. (3) Add kodza-header-grid--cells to put each link and each note in a column of its own, on a rule (-rule, -mark). (4) Motion is opt-in: kodza-header-grid--line, --hide, --tighten (the last two stick it to the top: give it -bg). (5) Phones: data-mobile="menu", "wrap" or "scroll" below -breakpoint (900px); without it the row stays, set fewer columns in your own media query. Delete the Menu button and the panel if you don't use the menu.

## Code

```html
<header class="kodza-header-grid" data-mobile="menu">
  <div class="kodza-header-grid__row">
    <a class="kodza-header-grid__brand" href="#" aria-label="Kiln, home"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true"><path d="M1 1h8v8H1zM11 1h8v8h-8zM1 11h8v8H1z"/><circle cx="15" cy="15" r="4"/></svg><span>kiln</span></a>
    <nav class="kodza-header-grid__nav" aria-label="Main">
      <ul class="kodza-header-grid__links">
        <li><a class="kodza-header-grid__link" href="#" aria-current="page">Work</a></li>
        <li><a class="kodza-header-grid__link" href="#">Services</a></li>
        <li><a class="kodza-header-grid__link" href="#">Studio</a></li>
        <li><a class="kodza-header-grid__link" href="#">Journal</a></li>
        <li><a class="kodza-header-grid__link" href="#">Contact</a></li>
      </ul>
    </nav>
    <div class="kodza-header-grid__actions">
      <a class="kodza-header-grid__button" href="#">Start a project</a>
      <button class="kodza-header-grid__toggle" type="button" aria-expanded="false"><span class="kodza-header-grid__lines" aria-hidden="true"></span><span class="kodza-header-grid__label"><span class="kodza-header-grid__say">Menu</span><span class="kodza-header-grid__say" aria-hidden="true">Close</span></span></button>
      <div class="kodza-header-grid__panel" popover aria-label="Menu">
        <nav class="kodza-header-grid__list" aria-label="Main"></nav>
        <div class="kodza-header-grid__foot"></div>
      </div>
    </div>
  </div>
</header>
```

```css
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Header Grid
   The header that sits on your site's grid. Three zones, the brand, the links and the actions,
   each on the columns you give it, so its edges fall where the page's do.
   - Where things go: --kz-header-grid-columns, then one column per zone (-brand-column,
     -nav-column, -actions-column). Add kodza-header-grid--cells and each link and each note takes
     a column of its own, on its own rule.
   - Still by default. Three motions, one modifier each, all at the pace of --kz-motion-scale:
     --line (a line glides under the links), --hide (gone while you read down, back when you
     scroll up), --tighten (the bar loses a little height once you scroll). --hide and --tighten
     stick it to the top.
   - Phones, below --kz-header-grid-breakpoint: data-mobile="menu" (a shutter behind a Menu
     button), "wrap" (the links on a second line) or "scroll" (a second line that scrolls
     sideways). Without data-mobile the row stays a row: give it fewer columns in your own media
     query; only the notes step aside.
   Everything is tuned with the --kz-header-grid-* variables (--kz-header-grid-menu-* for the
   shutter). A site grid of six columns, one link per column, as on a Swiss studio site:
     --kz-header-grid-columns: repeat(6, minmax(0, 1fr)); --kz-header-grid-nav-column: 2 / 5;
     --kz-header-grid-actions-column: 5 / 7; --kz-header-grid-rule: 1px; --kz-header-grid-mark: 3px;
     --kz-header-grid-align: end; with the --cells modifier. */
.kodza-header-grid {
  /* Colours */
  --kz-header-grid-bg: transparent; /* give it your page colour once it sticks */
  --kz-header-grid-fg: #0a0a0a; /* the brand, the page you are on, the link you point at */
  --kz-header-grid-muted: rgba(10, 10, 10, 0.62); /* the notes */
  --kz-header-grid-link: var(--kz-header-grid-muted); /* the other links */
  --kz-header-grid-rule-color: var(--kz-header-grid-fg); /* the rules under the cells */
  --kz-header-grid-ink: #0a0a0a; /* the buttons */
  --kz-header-grid-on-ink: #ffffff;
  --kz-header-grid-soft: rgba(10, 10, 10, 0.06); /* the Menu button */
  /* Where things go: the row is a grid, each zone takes the columns you name */
  --kz-header-grid-columns: minmax(0, 1fr) auto minmax(0, 1fr); /* an auto column centres what it holds */
  --kz-header-grid-gap: 16px; /* between the columns */
  --kz-header-grid-brand-column: 1;
  --kz-header-grid-nav-column: 2;
  --kz-header-grid-actions-column: 3;
  --kz-header-grid-align: center; /* end sets every zone on the bottom edge */
  --kz-header-grid-width: 1200px; /* the row, centred; none = the full width */
  --kz-header-grid-pad: clamp(16px, 4cqw, 24px); /* on both sides */
  --kz-header-grid-pad-block: 0px; /* above and below (two values: above, then below) */
  --kz-header-grid-height: 72px; /* at least */
  --kz-header-grid-brand-size: 18px;
  /* The page you are on */
  --kz-header-grid-mark: 1.5px; /* the line under it, and under the link you point at */
  --kz-header-grid-rule: 0px; /* with --cells, the rule under every cell; 0 = none */
  /* Where it stays */
  --kz-header-grid-position: static; /* sticky keeps it at the top */
  --kz-header-grid-top: var(--wp-admin--admin-bar--height, 0px); /* where it sticks: under the WordPress admin bar */
  --kz-header-grid-breakpoint: 900px; /* below it, data-mobile takes over */
  /* Motion */
  --kz-header-grid-duration: 0.42s; /* --line: the line gliding */
  --kz-header-grid-lag: 0.08s; /* how long its back edge waits */
  --kz-header-grid-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --kz-header-grid-hide-after: 600px; /* --hide: how far you read before it goes */
  --kz-header-grid-tighten: 16px; /* --tighten: the height the bar loses */
  --kz-header-grid-tighten-after: 40px; /* how far you scroll before it tightens */
  --kz-header-grid-shrink: 0.86; /* the brand's size once tight; 1 = it stays */
  /* The shutter, with data-mobile="menu" */
  --kz-header-grid-menu-bg: #0a0a0a;
  --kz-header-grid-menu-fg: #f2f2f0;
  --kz-header-grid-menu-muted: rgba(242, 242, 240, 0.62);
  --kz-header-grid-menu-line: rgba(242, 242, 240, 0.16);
  --kz-header-grid-menu-side: 24px;
  --kz-header-grid-menu-size: calc(clamp(34px, min(10cqw, calc((100cqh - 240px) / 6)), 96px) * var(--kz-font-display-scale, 1));
  --kz-header-grid-menu-duration: 0.8s;
  --kz-header-grid-menu-ease: cubic-bezier(0.76, 0, 0.24, 1);

  position: var(--kz-header-grid-position);
  top: var(--kz-header-grid-top);
  z-index: 60;
  display: block;
  box-sizing: border-box;
  width: 100%;
  margin: 0;
  padding: 0;
  background: var(--kz-header-grid-bg);
  color: var(--kz-header-grid-fg);
  font-size: 15px;
  line-height: 1.4;
  text-align: left;
  container-type: inline-size;
}

/* The row: the grid every zone is placed on */
.kodza-header-grid__row {
  display: grid;
  grid-template-columns: var(--kz-header-grid-columns);
  align-items: var(--kz-header-grid-align);
  column-gap: var(--kz-header-grid-gap);
  box-sizing: border-box;
  max-width: var(--kz-header-grid-width);
  min-height: var(--kz-header-grid-height);
  margin: 0 auto;
  padding-block: var(--kz-header-grid-pad-block);
  padding-inline: var(--kz-header-grid-pad);
}

/* ---------- The three zones ---------- */

.kodza-header-grid__brand {
  position: relative;
  display: inline-flex;
  grid-column: var(--kz-header-grid-brand-column);
  grid-row: 1;
  align-items: center;
  justify-self: start;
  gap: 8px;
  box-sizing: border-box;
  min-height: 40px;
  margin: 0;
  border-radius: calc(6px * var(--kz-radius-scale, 1));
  color: var(--kz-header-grid-fg);
  font-family: var(--kz-font-display, inherit);
  font-size: calc(var(--kz-header-grid-brand-size) * var(--kz-font-display-scale, 1));
  font-weight: var(--kz-font-display-weight, 600);
  line-height: 1;
  letter-spacing: var(--kz-font-display-tracking, -0.04em);
  text-transform: var(--kz-font-display-case, inherit);
  text-decoration: none;
  white-space: nowrap;
  transform-origin: var(--kz-header-grid-origin, left center);
}

.kodza-header-grid__brand svg {
  display: block;
  flex: none;
  fill: currentColor;
}

/* The links keep 14 px of room on each side for the pointer; the nav gives it back, so the first
   and the last word sit on the edges of their columns, like the rest of the page */
.kodza-header-grid__nav {
  position: relative;
  display: block;
  grid-column: var(--kz-header-grid-nav-column);
  grid-row: 1;
  justify-self: start;
  min-width: 0;
  margin: 0 -14px;
  padding: 0;
}

.kodza-header-grid__links {
  display: flex;
  align-items: center;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.kodza-header-grid__links > li {
  display: block;
  margin: 0;
  padding: 0;
  list-style: none;
}

.kodza-header-grid__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  height: 40px;
  margin: 0;
  padding: 0 14px;
  border-radius: calc(8px * var(--kz-radius-scale, 1));
  color: var(--kz-header-grid-link);
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 15px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-transform: var(--kz-font-label-case, inherit);
  text-decoration: none;
  white-space: nowrap;
  transition: color calc(0.2s * var(--kz-motion-scale, 1));
}

.kodza-header-grid__link:focus-visible,
.kodza-header-grid__link[aria-current="page"] {
  color: var(--kz-header-grid-fg);
}

/* The mark under the page you are on (and the link you point at): it fades in, it never moves */
.kodza-header-grid__link::after,
.kodza-header-grid__brand::after {
  content: "";
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 8px;
  height: var(--kz-header-grid-mark);
  border-radius: calc(1px * var(--kz-radius-scale, 1));
  background: var(--kz-header-grid-fg);
  opacity: 0;
  pointer-events: none;
  transition: opacity calc(0.2s * var(--kz-motion-scale, 1));
}

/* The brand only marks the page in --cells, where it is a cell like the links */
.kodza-header-grid__brand::after {
  content: none;
}

.kodza-header-grid__link[aria-current="page"]::after,
.kodza-header-grid__link:focus-visible::after {
  opacity: 1;
}

@media (hover: hover) {
  .kodza-header-grid__link:hover {
    color: var(--kz-header-grid-fg);
  }

  .kodza-header-grid__link:hover::after {
    opacity: 1;
  }
}

.kodza-header-grid__brand:focus-visible,
.kodza-header-grid__link:focus-visible {
  outline: 2px solid var(--kz-header-grid-fg);
  outline-offset: 2px;
}

.kodza-header-grid__actions {
  display: flex;
  grid-column: var(--kz-header-grid-actions-column);
  grid-row: 1;
  align-items: center;
  justify-self: end;
  gap: 8px;
  min-width: 0;
  margin: 0;
}

/* A short text in the bar: an address, an issue number, the next opening */
.kodza-header-grid__note {
  position: relative;
  margin: 0;
  color: var(--kz-header-grid-muted);
  font-family: var(--kz-font-label, var(--kz-font-mono, "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace));
  font-size: var(--kz-font-label-size, 12px);
  font-weight: var(--kz-font-label-weight, inherit);
  line-height: var(--kz-font-label-leading, 1.4);
  letter-spacing: var(--kz-font-label-tracking, inherit);
  text-transform: var(--kz-font-label-case, inherit);
  white-space: nowrap;
}

.kodza-header-grid__button {
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  height: 40px;
  margin: 0;
  padding: 0 18px;
  border-radius: var(--kz-radius-pill, 999px);
  background: var(--kz-header-grid-ink);
  color: var(--kz-header-grid-on-ink);
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 15px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-decoration: none;
  text-transform: var(--kz-font-label-case, inherit);
  white-space: nowrap;
  transition: background-color calc(0.2s * var(--kz-motion-scale, 1));
}

@media (hover: hover) {
  .kodza-header-grid__button:hover {
    background: color-mix(in srgb, var(--kz-header-grid-ink) 84%, transparent);
  }
}

.kodza-header-grid__button:focus-visible {
  outline: 2px solid var(--kz-header-grid-ink);
  outline-offset: 3px;
}

/* ---------- --cells: one column per link and per note, each on its rule ---------- */

/* Subgrids keep the row's own gap (normal), so every cell lands on a column of the site */
.kodza-header-grid--cells .kodza-header-grid__nav,
.kodza-header-grid--cells .kodza-header-grid__actions {
  display: grid;
  grid-template-columns: subgrid;
  align-items: var(--kz-header-grid-align);
  align-self: stretch;
  justify-self: stretch;
  gap: normal;
}

.kodza-header-grid--cells .kodza-header-grid__nav {
  margin: 0;
}

.kodza-header-grid--cells .kodza-header-grid__links {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: var(--kz-header-grid-align);
  gap: normal;
}

/* Every cell sits on its rule (drawn inside, so nothing moves when it thickens) */
.kodza-header-grid--cells .kodza-header-grid__brand,
.kodza-header-grid--cells .kodza-header-grid__link,
.kodza-header-grid--cells .kodza-header-grid__note {
  display: flex;
  justify-self: stretch;
  height: auto;
  min-height: 0;
  padding: 0 0 10px;
  border-radius: 0;
  box-shadow: inset 0 calc(-1 * var(--kz-header-grid-rule)) 0 var(--kz-header-grid-rule-color);
}

.kodza-header-grid--cells .kodza-header-grid__brand {
  align-items: flex-end;
}

.kodza-header-grid--cells .kodza-header-grid__link {
  align-items: flex-end;
  line-height: var(--kz-font-label-leading, 1.4);
  white-space: normal;
}

.kodza-header-grid--cells .kodza-header-grid__note {
  display: block;
}

.kodza-header-grid--cells .kodza-header-grid__link::after,
.kodza-header-grid--cells .kodza-header-grid__brand::after {
  content: "";
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 0;
}

.kodza-header-grid--cells .kodza-header-grid__brand[aria-current="page"]::after,
.kodza-header-grid--cells .kodza-header-grid__brand:focus-visible::after {
  opacity: 1;
}

@media (hover: hover) {
  .kodza-header-grid--cells .kodza-header-grid__brand:hover::after {
    opacity: 1;
  }
}

/* ---------- --line: the mark glides from link to link ---------- */

/* The script adds the line and sets where its two edges go. Each edge has its own transition, so
   the one in front leaves first and the other catches up */
.kodza-header-grid__line {
  position: absolute;
  bottom: 8px;
  left: 0;
  right: 0;
  height: var(--kz-header-grid-mark);
  clip-path: inset(0 0 0 var(--kz-header-grid-line-left, 0px));
  pointer-events: none;
}

.kodza-header-grid__line::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: calc(1px * var(--kz-radius-scale, 1));
  background: var(--kz-header-grid-fg);
  clip-path: inset(0 var(--kz-header-grid-line-right, 100%) 0 0);
}

.kodza-header-grid--cells .kodza-header-grid__line {
  bottom: 0;
}

.kodza-header-grid--cells .kodza-header-grid__line::after {
  border-radius: 0;
}

.kodza-header-grid.is-ready .kodza-header-grid__line,
.kodza-header-grid.is-ready .kodza-header-grid__line::after {
  transition: clip-path calc(var(--kz-header-grid-duration) * var(--kz-motion-scale, 1)) var(--kz-header-grid-ease);
}

.kodza-header-grid.is-ready .kodza-header-grid__line.is-rightward,
.kodza-header-grid.is-ready .kodza-header-grid__line.is-leftward::after {
  transition-delay: calc(var(--kz-header-grid-lag) * var(--kz-motion-scale, 1));
}

.kodza-header-grid.is-ready .kodza-header-grid__line.is-instant,
.kodza-header-grid.is-ready .kodza-header-grid__line.is-instant::after {
  transition: none;
}

/* The line does the marking: the links' own marks step aside (they come back below the
   breakpoint, where the line has no room to glide) */
.kodza-header-grid--line.is-ready:not(.is-compact) .kodza-header-grid__link::after {
  visibility: hidden;
}

.kodza-header-grid.is-compact .kodza-header-grid__line {
  display: none;
}

/* ---------- --hide and --tighten: stuck to the top, and moved by the script ---------- */

.kodza-header-grid--hide,
.kodza-header-grid--tighten {
  position: sticky;
}

/* Where the header starts in the page (put there by the script): it tells how far you went */
.kodza-header-grid__spot {
  display: block;
  height: 1px;
  margin: 0 0 -1px;
  padding: 0;
  visibility: hidden;
  pointer-events: none;
}

.kodza-header-grid.is-ready {
  transition: translate calc(0.5s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}

.kodza-header-grid.is-ready .kodza-header-grid__row,
.kodza-header-grid.is-ready .kodza-header-grid__brand {
  transition:
    translate calc(0.6s * var(--kz-motion-scale, 1)) cubic-bezier(0.33, 1, 0.68, 1),
    scale calc(0.6s * var(--kz-motion-scale, 1)) cubic-bezier(0.33, 1, 0.68, 1);
}

/* Tight: the whole bar slides up by what it loses, its content comes back down by its share of
   it (both set by the script), so the box keeps its height and the page under it never jumps */
.kodza-header-grid.is-tight {
  translate: 0 calc(-1 * var(--kz-header-grid-lift, 0px));
  transition-duration: calc(0.6s * var(--kz-motion-scale, 1));
  transition-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
}

.kodza-header-grid.is-tight .kodza-header-grid__row {
  translate: 0 var(--kz-header-grid-drop, 0px);
}

.kodza-header-grid.is-tight .kodza-header-grid__brand {
  scale: var(--kz-header-grid-shrink);
}

/* Hidden: it leaves slower than it comes back */
.kodza-header-grid.is-hidden {
  translate: 0 calc(-100% - 1px);
  transition-duration: calc(0.8s * var(--kz-motion-scale, 1));
  transition-timing-function: cubic-bezier(0.65, 0, 0.35, 1);
}

/* ---------- The Menu button (data-mobile="menu", below the breakpoint) ---------- */

/* Two lines meet, then cross; the label rolls without changing the width */
.kodza-header-grid__toggle {
  display: none;
  align-items: center;
  gap: 10px;
  box-sizing: border-box;
  height: 40px;
  margin: 0;
  padding: 0 18px 0 16px;
  border: 0;
  border-radius: var(--kz-radius-pill, 999px);
  background: var(--kz-header-grid-soft);
  color: var(--kz-header-grid-fg);
  font: inherit;
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 15px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-transform: var(--kz-font-label-case, revert);
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.kodza-header-grid__toggle:focus-visible {
  outline: 2px solid var(--kz-header-grid-fg);
  outline-offset: 3px;
}

.kodza-header-grid__lines {
  position: relative;
  flex: none;
  width: 14px;
  height: 8px;
}

.kodza-header-grid__lines::before,
.kodza-header-grid__lines::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.5px;
  border-radius: calc(1px * var(--kz-radius-scale, 1));
  background: currentColor;
  /* On its own layer: drawn the same way at rest and while turning, so no snap at the end */
  will-change: transform;
  /* Closing: they uncross first, then part */
  transition:
    rotate calc(0.22s * var(--kz-motion-scale, 1)) cubic-bezier(0.65, 0, 0.35, 1),
    translate calc(0.22s * var(--kz-motion-scale, 1)) cubic-bezier(0.65, 0, 0.35, 1) calc(0.18s * var(--kz-motion-scale, 1)),
    clip-path calc(0.3s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}

.kodza-header-grid__lines::before {
  top: 0;
}

/* Whole pixels: both lines sit on the pixel grid the same way. Whole at rest, as an inset: the
   pointer slides it to a shorter one (from none it would jump) */
.kodza-header-grid__lines::after {
  top: 6px;
  clip-path: inset(0 0 0 0);
}

@media (hover: hover) {
  .kodza-header-grid__toggle:hover .kodza-header-grid__lines::after {
    clip-path: inset(0 45% 0 0);
  }
}

.kodza-header-grid__toggle[aria-expanded="true"] .kodza-header-grid__lines::before,
.kodza-header-grid__toggle[aria-expanded="true"] .kodza-header-grid__lines::after {
  clip-path: inset(0 0 0 0);
  transition:
    translate calc(0.2s * var(--kz-motion-scale, 1)) cubic-bezier(0.65, 0, 0.35, 1),
    rotate calc(0.3s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1) calc(0.17s * var(--kz-motion-scale, 1)),
    clip-path calc(0.3s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}

.kodza-header-grid__toggle[aria-expanded="true"] .kodza-header-grid__lines::before {
  translate: 0 3px;
  rotate: 45deg;
}

.kodza-header-grid__toggle[aria-expanded="true"] .kodza-header-grid__lines::after {
  translate: 0 -3px;
  rotate: -45deg;
}

.kodza-header-grid__label {
  display: grid;
  margin: -3px 0;
  padding: 3px 0;
  overflow: hidden;
}

/* The script rolls the words both ways: the old one goes up, the new one comes up from below */
.kodza-header-grid__say {
  grid-area: 1 / 1;
}

.kodza-header-grid__say + .kodza-header-grid__say {
  translate: 0 115%;
}

.kodza-header-grid__toggle[aria-expanded="true"] .kodza-header-grid__say {
  translate: 0 -115%;
}

.kodza-header-grid__toggle[aria-expanded="true"] .kodza-header-grid__say + .kodza-header-grid__say {
  translate: 0 0;
}

/* ---------- The shutter (data-mobile="menu") ---------- */

/* From the bottom of the header to the bottom of the screen, rolled up until it opens. A native
   popover, so Escape, a click outside and the focus going back to Menu are the browser's own */
.kodza-header-grid__panel {
  position: fixed;
  inset: var(--kz-header-grid-from, 0px) 0 0 0;
  box-sizing: border-box;
  width: 100%;
  height: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0 var(--kz-header-grid-menu-side);
  border: 0;
  overflow: hidden auto;
  overscroll-behavior: contain;
  background: var(--kz-header-grid-menu-bg);
  color: var(--kz-header-grid-menu-fg);
  font: inherit;
  font-size: 15px;
  line-height: 1.4;
  text-align: left;
  container-type: size;
  z-index: 99990;
  /* Here and not on the open state: the panel keeps its rows while it rolls back up */
  grid-template-rows: 1fr auto;
  -webkit-font-smoothing: antialiased;
  clip-path: inset(0 0 100% 0);
  /* Closing: quicker than it came down */
  transition:
    clip-path calc(var(--kz-header-grid-menu-duration) * 0.7 * var(--kz-motion-scale, 1)) var(--kz-header-grid-menu-ease),
    display calc(var(--kz-header-grid-menu-duration) * 0.7 * var(--kz-motion-scale, 1)) allow-discrete,
    overlay calc(var(--kz-header-grid-menu-duration) * 0.7 * var(--kz-motion-scale, 1)) allow-discrete;
}

.kodza-header-grid__panel:popover-open {
  display: grid;
  clip-path: inset(0 0 0 0);
  transition:
    clip-path calc(var(--kz-header-grid-menu-duration) * var(--kz-motion-scale, 1)) var(--kz-header-grid-menu-ease),
    display calc(var(--kz-header-grid-menu-duration) * var(--kz-motion-scale, 1)) allow-discrete,
    overlay calc(var(--kz-header-grid-menu-duration) * var(--kz-motion-scale, 1)) allow-discrete;
}

@starting-style {
  .kodza-header-grid__panel:popover-open {
    clip-path: inset(0 0 100% 0);
  }
}

.kodza-header-grid__list {
  align-self: center;
  display: grid;
  margin: 0;
  padding: 28px 0;
}

/* Each row: its hairline draws in, then the word rises from its mask */
.kodza-header-grid__item {
  position: relative;
  display: grid;
  grid-template-columns: clamp(44px, 9cqw, 132px) minmax(0, 1fr);
  align-items: start;
  margin: 0;
  padding: 14px 0 10px;
  color: inherit;
  text-decoration: none;
  outline: none;
}

.kodza-header-grid__item::before,
.kodza-header-grid__item:last-child::after {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: var(--kz-header-grid-menu-line);
  scale: 0 1;
  transform-origin: left center;
  transition: scale calc(0.3s * var(--kz-motion-scale, 1)) cubic-bezier(0.65, 0, 0.35, 1);
}

/* The last row closes the list with a line under it, so the list is framed top and bottom */
.kodza-header-grid__item:last-child::after {
  inset: auto 0 0;
}

.kodza-header-grid__index {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-top: 0.4em;
  font-family: var(--kz-font-label, var(--kz-font-mono, "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace));
  font-size: var(--kz-font-label-size, 12px);
  font-weight: var(--kz-font-label-weight, inherit);
  line-height: var(--kz-font-label-leading, 1.4);
  letter-spacing: var(--kz-font-label-tracking, inherit);
  color: var(--kz-header-grid-menu-muted);
  opacity: 0;
  transition:
    opacity calc(0.2s * var(--kz-motion-scale, 1)),
    color calc(0.25s * var(--kz-motion-scale, 1));
}

/* The page you are on: its number lights up, with a dot */
.kodza-header-grid__item[aria-current="page"] .kodza-header-grid__index {
  color: var(--kz-header-grid-menu-fg);
}

.kodza-header-grid__item[aria-current="page"] .kodza-header-grid__index::after {
  content: "";
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
}

/* The mask has the size of its word, so its room for descenders (g, j, p, q, y) and its step
   aside are measured on the word */
.kodza-header-grid__mask {
  display: block;
  margin: -0.06em 0 -0.24em;
  padding: 0.06em 0 0.24em;
  overflow: hidden;
  font-size: var(--kz-header-grid-menu-size);
  transition: translate calc(0.45s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}

.kodza-header-grid__word {
  display: block;
  font-family: var(--kz-font-display, inherit);
  font-size: var(--kz-header-grid-menu-size);
  font-weight: var(--kz-font-display-weight, 500);
  line-height: 0.95;
  letter-spacing: var(--kz-font-display-tracking, -0.045em);
  text-transform: var(--kz-font-display-case, inherit);
  translate: 0 135%;
  /* Closing: the words drop back into their masks while the shutter rolls up */
  transition: translate calc(0.32s * var(--kz-motion-scale, 1)) cubic-bezier(0.65, 0, 0.35, 1);
}

/* Under the list: the bar's notes and buttons, copied by the script */
.kodza-header-grid__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
  padding: 16px 0 22px;
  border-top: 1px solid var(--kz-header-grid-menu-line);
  opacity: 0;
  translate: 0 8px;
  transition:
    opacity calc(0.2s * var(--kz-motion-scale, 1)),
    translate calc(0.3s * var(--kz-motion-scale, 1)) cubic-bezier(0.65, 0, 0.35, 1);
}

.kodza-header-grid__foot:empty {
  display: none;
}

.kodza-header-grid__fact {
  margin: 0;
  color: var(--kz-header-grid-menu-muted);
  font-family: var(--kz-font-label, var(--kz-font-mono, "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace));
  font-size: var(--kz-font-label-size, 12px);
  font-weight: var(--kz-font-label-weight, inherit);
  line-height: var(--kz-font-label-leading, 1.4);
  letter-spacing: var(--kz-font-label-tracking, inherit);
  text-transform: var(--kz-font-label-case, inherit);
}

.kodza-header-grid__action {
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  height: 40px;
  margin: 0;
  padding: 0 18px;
  border-radius: var(--kz-radius-pill, 999px);
  background: var(--kz-header-grid-menu-fg);
  color: var(--kz-header-grid-menu-bg);
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 15px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-decoration: none;
  text-transform: var(--kz-font-label-case, inherit);
  white-space: nowrap;
}

.kodza-header-grid__action:focus-visible {
  outline: 2px solid var(--kz-header-grid-menu-fg);
  outline-offset: 3px;
}

/* Open: each part starts when the edge reaches it (the script sets --kz-header-grid-delay) */
.kodza-header-grid__panel.is-open .kodza-header-grid__item::before {
  scale: 1 1;
  transition: scale calc(0.9s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1) var(--kz-header-grid-delay, 0s);
}

.kodza-header-grid__panel.is-open .kodza-header-grid__item:last-child::after {
  scale: 1 1;
  transition: scale calc(0.9s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1) var(--kz-header-grid-delay-end, 0s);
}

.kodza-header-grid__panel.is-open .kodza-header-grid__index {
  opacity: 1;
  transition:
    opacity calc(0.6s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1) calc(var(--kz-header-grid-delay, 0s) + 0.12s * var(--kz-motion-scale, 1)),
    color calc(0.25s * var(--kz-motion-scale, 1));
}

.kodza-header-grid__panel.is-open .kodza-header-grid__word {
  translate: 0 0;
  transition: translate calc(0.75s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1) var(--kz-header-grid-delay, 0s);
}

.kodza-header-grid__panel.is-open .kodza-header-grid__foot {
  opacity: 1;
  translate: 0 0;
  transition:
    opacity calc(0.6s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1) var(--kz-header-grid-delay, 0s),
    translate calc(0.7s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1) var(--kz-header-grid-delay, 0s);
}

/* Pointed at: the word steps aside and its number lights up */
@media (hover: hover) {
  .kodza-header-grid__item:hover .kodza-header-grid__mask {
    translate: 0.14em 0;
  }

  .kodza-header-grid__item:hover .kodza-header-grid__index {
    color: var(--kz-header-grid-menu-fg);
  }
}

.kodza-header-grid__item:focus-visible .kodza-header-grid__mask {
  translate: 0.14em 0;
}

.kodza-header-grid__item:focus-visible .kodza-header-grid__index {
  color: var(--kz-header-grid-menu-fg);
}

.kodza-header-grid__item:focus-visible .kodza-header-grid__word {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.12em;
}

/* ---------- Below the breakpoint (is-compact, set by the script) ---------- */

/* The notes step aside, whatever the phone layout, and a zone of notes only goes with them (it
   would keep asking for its columns) */
.kodza-header-grid.is-compact .kodza-header-grid__note,
.kodza-header-grid.is-compact .kodza-header-grid__actions:not(:has(> :not(.kodza-header-grid__note))) {
  display: none;
}

/* menu, wrap and scroll: the brand and the actions share the first line */
.kodza-header-grid.is-compact[data-mobile] .kodza-header-grid__row {
  grid-template-columns: minmax(0, 1fr) auto;
}

.kodza-header-grid.is-compact[data-mobile] .kodza-header-grid__brand {
  grid-column: 1;
}

.kodza-header-grid.is-compact[data-mobile] .kodza-header-grid__actions {
  display: flex;
  grid-column: 2;
  justify-self: end;
}

.kodza-header-grid.is-compact[data-mobile] .kodza-header-grid__nav {
  display: block;
  grid-column: 1 / -1;
  grid-row: 2;
  justify-self: stretch;
  margin: 0;
}

.kodza-header-grid.is-compact[data-mobile] .kodza-header-grid__links {
  display: flex;
  gap: 0 20px;
}

/* menu: the links wait in the shutter */
.kodza-header-grid.is-compact[data-mobile="menu"] .kodza-header-grid__nav {
  display: none;
}

.kodza-header-grid.is-compact[data-mobile="menu"] .kodza-header-grid__toggle {
  display: inline-flex;
}

/* wrap and scroll: the links on a second line, aligned on the brand */
.kodza-header-grid.is-compact[data-mobile="wrap"] .kodza-header-grid__row,
.kodza-header-grid.is-compact[data-mobile="scroll"] .kodza-header-grid__row {
  grid-template-rows: minmax(var(--kz-header-grid-height), auto) auto;
  min-height: 0;
}

.kodza-header-grid.is-compact[data-mobile="wrap"] .kodza-header-grid__link,
.kodza-header-grid.is-compact[data-mobile="scroll"] .kodza-header-grid__link {
  padding-inline: 0;
}

.kodza-header-grid.is-compact[data-mobile="wrap"] .kodza-header-grid__link::after,
.kodza-header-grid.is-compact[data-mobile="scroll"] .kodza-header-grid__link::after {
  left: 0;
  right: 0;
}

.kodza-header-grid.is-compact[data-mobile="wrap"] .kodza-header-grid__links {
  flex-wrap: wrap;
}

/* scroll: one line that runs to the edges, fading there */
.kodza-header-grid.is-compact[data-mobile="scroll"] .kodza-header-grid__nav {
  margin: -4px calc(-1 * var(--kz-header-grid-pad));
  padding: 4px var(--kz-header-grid-pad);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--kz-header-grid-pad), #000 calc(100% - var(--kz-header-grid-pad)), transparent);
  mask-image: linear-gradient(to right, transparent, #000 var(--kz-header-grid-pad), #000 calc(100% - var(--kz-header-grid-pad)), transparent);
}

.kodza-header-grid.is-compact[data-mobile="scroll"] .kodza-header-grid__nav::-webkit-scrollbar {
  display: none;
}

.kodza-header-grid.is-compact[data-mobile="scroll"] .kodza-header-grid__links {
  flex-wrap: nowrap;
  width: max-content;
}

/* menu, on the narrowest phones: the buttons wait in the shutter too, only Menu stays */
@container (max-width: 519px) {
  .kodza-header-grid.is-compact[data-mobile="menu"] .kodza-header-grid__button {
    display: none;
  }
}

/* Before the script: the default breakpoint, 900 px, so nothing jumps when it starts (a container
   query can't read a variable; the script then follows --kz-header-grid-breakpoint) */
@container (max-width: 899px) {
  .kodza-header-grid:not(.is-ready) .kodza-header-grid__note,
  .kodza-header-grid:not(.is-ready) .kodza-header-grid__actions:not(:has(> :not(.kodza-header-grid__note))) {
    display: none;
  }

  .kodza-header-grid[data-mobile]:not(.is-ready) .kodza-header-grid__row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .kodza-header-grid[data-mobile]:not(.is-ready) .kodza-header-grid__brand {
    grid-column: 1;
  }

  .kodza-header-grid[data-mobile]:not(.is-ready) .kodza-header-grid__actions {
    display: flex;
    grid-column: 2;
    justify-self: end;
  }

  .kodza-header-grid[data-mobile]:not(.is-ready) .kodza-header-grid__nav {
    display: block;
    grid-column: 1 / -1;
    grid-row: 2;
    justify-self: stretch;
    margin: 0;
  }

  .kodza-header-grid[data-mobile]:not(.is-ready) .kodza-header-grid__links {
    display: flex;
    gap: 0 20px;
  }

  .kodza-header-grid[data-mobile="menu"]:not(.is-ready) .kodza-header-grid__nav {
    display: none;
  }

  .kodza-header-grid[data-mobile="menu"]:not(.is-ready) .kodza-header-grid__toggle {
    display: inline-flex;
  }

  .kodza-header-grid[data-mobile="wrap"]:not(.is-ready) .kodza-header-grid__row,
  .kodza-header-grid[data-mobile="scroll"]:not(.is-ready) .kodza-header-grid__row {
    grid-template-rows: minmax(var(--kz-header-grid-height), auto) auto;
    min-height: 0;
  }

  .kodza-header-grid[data-mobile="wrap"]:not(.is-ready) .kodza-header-grid__link,
  .kodza-header-grid[data-mobile="scroll"]:not(.is-ready) .kodza-header-grid__link {
    padding-inline: 0;
  }

  .kodza-header-grid[data-mobile="wrap"]:not(.is-ready) .kodza-header-grid__link::after,
  .kodza-header-grid[data-mobile="scroll"]:not(.is-ready) .kodza-header-grid__link::after {
    left: 0;
    right: 0;
  }

  .kodza-header-grid[data-mobile="wrap"]:not(.is-ready) .kodza-header-grid__links {
    flex-wrap: wrap;
  }

  .kodza-header-grid[data-mobile="scroll"]:not(.is-ready) .kodza-header-grid__nav {
    margin: -4px calc(-1 * var(--kz-header-grid-pad));
    padding: 4px var(--kz-header-grid-pad);
    overflow-x: auto;
    scrollbar-width: none;
  }

  .kodza-header-grid[data-mobile="scroll"]:not(.is-ready) .kodza-header-grid__links {
    flex-wrap: nowrap;
    width: max-content;
  }
}

@media (prefers-reduced-motion: reduce) {
  .kodza-header-grid.is-ready,
  .kodza-header-grid.is-ready .kodza-header-grid__row,
  .kodza-header-grid.is-ready .kodza-header-grid__brand,
  .kodza-header-grid.is-ready .kodza-header-grid__line,
  .kodza-header-grid.is-ready .kodza-header-grid__line::after,
  .kodza-header-grid__panel,
  .kodza-header-grid__panel *,
  .kodza-header-grid__panel *::before,
  .kodza-header-grid__panel *::after,
  .kodza-header-grid__toggle *,
  .kodza-header-grid__toggle *::before,
  .kodza-header-grid__toggle *::after {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
}

/* kodza-guard 01ff5c1ad8db — 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-header-grid):not(#_) {
  transition: revert;
}

:where(.kodza-header-grid__brand):not(#_) {
  padding: revert;
  outline: revert;
  transition: revert;
  box-shadow: revert;
  outline-offset: revert;
}

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

:where(.kodza-header-grid__button):not(#_),
:where(.kodza-header-grid__toggle):not(#_) {
  outline: revert;
  outline-offset: revert;
}

:where(.kodza-header-grid__list):not(#_),
:where(.kodza-header-grid__item):not(#_) {
  transition-duration: revert;
  transition-delay: revert;
}

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

:where(.kodza-header-grid__note):not(#_) {
  padding: revert;
  border-radius: revert;
  box-shadow: revert;
}

:where(.kodza-header-grid__note):not(#_):where(:not([hidden])) {
  display: revert;
}

:where(.kodza-header-grid__note):not(#_):where([hidden]) {
  display: none;
}

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

.kodza-header-grid:not(#_) {
  border: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  list-style: 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-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-header-grid__brand:not(#_),
.kodza-header-grid__link:not(#_) {
  border: revert;
  list-style: revert;
  float: revert;
  background-color: revert;
  background-image: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-style: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

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

.kodza-header-grid__links:not(#_),
.kodza-header-grid__links li:where(:is(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))):not(.kodza-header-grid__links :not(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))) *)):not(#_) {
  border: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-header-grid__actions:not(#_),
.kodza-header-grid__lines:not(#_),
.kodza-header-grid__label:not(#_),
.kodza-header-grid__say:not(#_),
.kodza-header-grid__index:not(#_),
.kodza-header-grid__mask:not(#_),
.kodza-header-grid__word:not(#_),
.kodza-header-grid__foot:not(#_),
.kodza-header-grid__line:not(#_),
.kodza-header-grid__brand span:where(:not([class*="kodza-"])):not(#_) {
  box-sizing: revert;
}

.kodza-header-grid__button:not(#_) {
  border: revert;
  list-style: revert;
  float: revert;
  position: revert;
  box-shadow: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-style: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-header-grid__toggle:not(#_) {
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  float: revert;
  position: revert;
  box-shadow: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  min-height: revert;
}

.kodza-header-grid__list:not(#_) {
  border: 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-header-grid__item:not(#_) {
  border: revert;
  border-radius: 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;
  transition-property: revert;
  transition-timing-function: revert;
}

.kodza-header-grid__action:not(#_) {
  border: revert;
  list-style: revert;
  float: revert;
  position: revert;
  box-shadow: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-style: 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-header-grid__note:not(#_) {
  border: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  background-color: revert;
  background-image: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  font-style: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

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

.kodza-header-grid__link:not(#_)::after,
.kodza-header-grid__brand:not(#_)::after,
.kodza-header-grid__line:not(#_)::after,
.kodza-header-grid__lines:not(#_)::before,
.kodza-header-grid__lines:not(#_)::after,
.kodza-header-grid__item:not(#_)::before,
.kodza-header-grid__item:not(#_)::after,
.kodza-header-grid__index:not(#_)::after {
  box-sizing: revert;
}

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

.kodza-header-grid:not(#_) {
  position: var(--kz-header-grid-position);
  display: block;
  box-sizing: border-box;
  padding: 0;
  background: var(--kz-header-grid-bg);
  color: var(--kz-header-grid-fg);
  font-size: 15px;
  line-height: 1.4;
  text-align: left;
}

.kodza-header-grid__brand:not(#_) {
  position: relative;
  display: inline-flex;
  box-sizing: border-box;
  margin: 0;
  border-radius: calc(6px * var(--kz-radius-scale, 1));
  color: var(--kz-header-grid-fg);
  font-family: var(--kz-font-display, inherit);
  font-size: calc(var(--kz-header-grid-brand-size) * var(--kz-font-display-scale, 1));
  font-weight: var(--kz-font-display-weight, 600);
  line-height: 1;
  letter-spacing: var(--kz-font-display-tracking, -0.04em);
  text-transform: var(--kz-font-display-case, inherit);
  text-decoration: none;
}

.kodza-header-grid__brand svg:not(#_) {
  display: block;
}

.kodza-header-grid__nav:not(#_) {
  position: relative;
  display: block;
  margin: 0 -14px;
  padding: 0;
}

.kodza-header-grid__links:not(#_) {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
}

.kodza-header-grid__links > li:not(#_) {
  display: block;
  margin: 0;
  padding: 0;
  list-style: none;
}

.kodza-header-grid__link:not(#_) {
  position: relative;
  display: inline-flex;
  box-sizing: border-box;
  margin: 0;
  padding: 0 14px;
  border-radius: calc(8px * var(--kz-radius-scale, 1));
  color: var(--kz-header-grid-link);
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 15px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-transform: var(--kz-font-label-case, inherit);
  text-decoration: none;
  transition: color calc(0.2s * var(--kz-motion-scale, 1));
}

.kodza-header-grid__link:focus-visible:not(#_),
.kodza-header-grid__link[aria-current="page"]:not(#_) {
  color: var(--kz-header-grid-fg);
}

@media (hover: hover) {
  .kodza-header-grid__link:hover:not(#_) {
    color: var(--kz-header-grid-fg);
  }
}

.kodza-header-grid__brand:focus-visible:not(#_),
.kodza-header-grid__link:focus-visible:not(#_) {
  outline: 2px solid var(--kz-header-grid-fg);
  outline-offset: 2px;
}

.kodza-header-grid__note:not(#_) {
  position: relative;
  margin: 0;
  color: var(--kz-header-grid-muted);
  font-family: var(--kz-font-label, var(--kz-font-mono, "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace));
  font-size: var(--kz-font-label-size, 12px);
  font-weight: var(--kz-font-label-weight, inherit);
  line-height: var(--kz-font-label-leading, 1.4);
  letter-spacing: var(--kz-font-label-tracking, inherit);
  text-transform: var(--kz-font-label-case, inherit);
}

.kodza-header-grid__button:not(#_) {
  display: inline-flex;
  box-sizing: border-box;
  margin: 0;
  padding: 0 18px;
  border-radius: var(--kz-radius-pill, 999px);
  background: var(--kz-header-grid-ink);
  color: var(--kz-header-grid-on-ink);
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 15px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-decoration: none;
  text-transform: var(--kz-font-label-case, inherit);
  transition: background-color calc(0.2s * var(--kz-motion-scale, 1));
}

@media (hover: hover) {
  .kodza-header-grid__button:hover:not(#_) {
    background: color-mix(in srgb, var(--kz-header-grid-ink) 84%, transparent);
  }
}

.kodza-header-grid__button:focus-visible:not(#_) {
  outline: 2px solid var(--kz-header-grid-ink);
  outline-offset: 3px;
}

.kodza-header-grid--cells .kodza-header-grid__nav:not(#_) {
  display: grid;
}

.kodza-header-grid--cells .kodza-header-grid__nav:not(#_) {
  margin: 0;
}

.kodza-header-grid--cells .kodza-header-grid__links:not(#_) {
  display: grid;
}

.kodza-header-grid--cells .kodza-header-grid__brand:not(#_),
.kodza-header-grid--cells .kodza-header-grid__link:not(#_),
.kodza-header-grid--cells .kodza-header-grid__note:not(#_) {
  display: flex;
  padding: 0 0 10px;
  border-radius: 0;
  box-shadow: inset 0 calc(-1 * var(--kz-header-grid-rule)) 0 var(--kz-header-grid-rule-color);
}

.kodza-header-grid--cells .kodza-header-grid__link:not(#_) {
  line-height: var(--kz-font-label-leading, 1.4);
}

.kodza-header-grid--cells .kodza-header-grid__note:not(#_) {
  display: block;
}

.kodza-header-grid--hide:not(#_),
.kodza-header-grid--tighten:not(#_) {
  position: sticky;
}

.kodza-header-grid.is-ready:not(#_) {
  transition: translate calc(0.5s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}

.kodza-header-grid.is-ready .kodza-header-grid__brand:not(#_) {
  transition: translate calc(0.6s * var(--kz-motion-scale, 1)) cubic-bezier(0.33, 1, 0.68, 1),
    scale calc(0.6s * var(--kz-motion-scale, 1)) cubic-bezier(0.33, 1, 0.68, 1);
}

.kodza-header-grid.is-tight:not(#_) {
  transition-duration: calc(0.6s * var(--kz-motion-scale, 1));
  transition-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
}

.kodza-header-grid.is-hidden:not(#_) {
  transition-duration: calc(0.8s * var(--kz-motion-scale, 1));
  transition-timing-function: cubic-bezier(0.65, 0, 0.35, 1);
}

.kodza-header-grid__toggle:not(#_) {
  display: none;
  align-items: center;
  gap: 10px;
  box-sizing: border-box;
  height: 40px;
  margin: 0;
  padding: 0 18px 0 16px;
  border: 0;
  border-radius: var(--kz-radius-pill, 999px);
  background: var(--kz-header-grid-soft);
  color: var(--kz-header-grid-fg);
  font: inherit;
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 15px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-transform: var(--kz-font-label-case, revert);
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.kodza-header-grid__toggle:focus-visible:not(#_) {
  outline: 2px solid var(--kz-header-grid-fg);
  outline-offset: 3px;
}

.kodza-header-grid__list:not(#_) {
  display: grid;
  margin: 0;
  padding: 28px 0;
}

.kodza-header-grid__item:not(#_) {
  position: relative;
  display: grid;
  margin: 0;
  padding: 14px 0 10px;
  color: inherit;
  text-decoration: none;
  outline: none;
}

.kodza-header-grid__action:not(#_) {
  display: inline-flex;
  box-sizing: border-box;
  margin: 0;
  padding: 0 18px;
  border-radius: var(--kz-radius-pill, 999px);
  background: var(--kz-header-grid-menu-fg);
  color: var(--kz-header-grid-menu-bg);
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 15px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-decoration: none;
  text-transform: var(--kz-font-label-case, inherit);
}

.kodza-header-grid__action:focus-visible:not(#_) {
  outline: 2px solid var(--kz-header-grid-menu-fg);
  outline-offset: 3px;
}

.kodza-header-grid.is-compact .kodza-header-grid__note:not(#_) {
  display: none;
}

.kodza-header-grid.is-compact[data-mobile] .kodza-header-grid__nav:not(#_) {
  display: block;
  margin: 0;
}

.kodza-header-grid.is-compact[data-mobile] .kodza-header-grid__links:not(#_) {
  display: flex;
}

.kodza-header-grid.is-compact[data-mobile="menu"] .kodza-header-grid__nav:not(#_) {
  display: none;
}

.kodza-header-grid.is-compact[data-mobile="menu"] .kodza-header-grid__toggle:not(#_) {
  display: inline-flex;
}

.kodza-header-grid.is-compact[data-mobile="wrap"] .kodza-header-grid__link:not(#_),
.kodza-header-grid.is-compact[data-mobile="scroll"] .kodza-header-grid__link:not(#_) {
  padding-inline: 0;
}

.kodza-header-grid.is-compact[data-mobile="scroll"] .kodza-header-grid__nav:not(#_) {
  margin: -4px calc(-1 * var(--kz-header-grid-pad));
  padding: 4px var(--kz-header-grid-pad);
}

@container (max-width: 519px) {
  .kodza-header-grid.is-compact[data-mobile="menu"] .kodza-header-grid__button:not(#_) {
    display: none;
  }
}

@container (max-width: 899px) {
  .kodza-header-grid:not(.is-ready) .kodza-header-grid__note:not(#_) {
    display: none;
  }
}

@container (max-width: 899px) {
  .kodza-header-grid[data-mobile]:not(.is-ready) .kodza-header-grid__nav:not(#_) {
    display: block;
    margin: 0;
  }
}

@container (max-width: 899px) {
  .kodza-header-grid[data-mobile]:not(.is-ready) .kodza-header-grid__links:not(#_) {
    display: flex;
  }
}

@container (max-width: 899px) {
  .kodza-header-grid[data-mobile="menu"]:not(.is-ready) .kodza-header-grid__nav:not(#_) {
    display: none;
  }
}

@container (max-width: 899px) {
  .kodza-header-grid[data-mobile="menu"]:not(.is-ready) .kodza-header-grid__toggle:not(#_) {
    display: inline-flex;
  }
}

@container (max-width: 899px) {
  .kodza-header-grid[data-mobile="wrap"]:not(.is-ready) .kodza-header-grid__link:not(#_),
  .kodza-header-grid[data-mobile="scroll"]:not(.is-ready) .kodza-header-grid__link:not(#_) {
    padding-inline: 0;
  }
}

@container (max-width: 899px) {
  .kodza-header-grid[data-mobile="scroll"]:not(.is-ready) .kodza-header-grid__nav:not(#_) {
    margin: -4px calc(-1 * var(--kz-header-grid-pad));
    padding: 4px var(--kz-header-grid-pad);
  }
}

@media (prefers-reduced-motion: reduce) {
  .kodza-header-grid.is-ready:not(#_),
  .kodza-header-grid.is-ready .kodza-header-grid__brand:not(#_),
  .kodza-header-grid__panel *:where(nav, a):not(#_) {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !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 — Header Grid
// The CSS places the zones on the grid; the script adds what CSS can't do on its own:
// - is-compact below --kz-header-grid-breakpoint (a container query can't read a variable);
// - with --line, one line under the links: it glides to the link you point at, front edge first,
//   and goes back under the page you are on (the engine of Header Classic);
// - with --hide and --tighten, how far the page went and which way, read on a mark right before
//   the header, so it works in any scrolling box;
// - with data-mobile="menu", the shutter (a native popover), written from the bar's own links,
//   notes and buttons, so a menu changed in the CMS changes both;
// - with data-mobile="scroll", the page you are on brought into view.
// Everything you tune is a --kz-header-grid-* variable of the CSS; the numbers below are the
// script's own.
(() => {
  const SELECTOR = '.kodza-header-grid';
  const STEP = 6; // px: a scroll smaller than this never turns --hide around
  const ROLL = 500; // ms: Menu rolling to Close, at the site's pace
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  // The site's pace, --kz-motion-scale (set on :root, read on the instance): every duration of the
  // script times it, and 0 moves nothing, like reduced motion
  const paceOf = (el) => {
    const pace = parseFloat(getComputedStyle(el).getPropertyValue('--kz-motion-scale'));
    return pace >= 0 ? pace : 1;
  };
  const c = (name) => `.kodza-header-grid__${name}`;

  // A length variable in px (px, rem or em), or the fallback
  const lengthOf = (el, name, fallback) => {
    const raw = getComputedStyle(el).getPropertyValue(name).trim();
    const value = parseFloat(raw);
    if (!(value >= 0)) return fallback;
    if (/rem$/.test(raw)) return value * parseFloat(getComputedStyle(document.documentElement).fontSize);
    if (/em$/.test(raw)) return value * parseFloat(getComputedStyle(el).fontSize);
    return value;
  };

  // When (0 to 1 of the duration) a cubic-bezier curve gets to the progress p
  const timeAt = ([x1, y1, x2, y2], p) => {
    const at = (a, b, s) => 3 * a * s * (1 - s) ** 2 + 3 * b * s * s * (1 - s) + s ** 3;
    let lo = 0;
    let hi = 1;
    for (let i = 0; i < 24; i++) {
      const mid = (lo + hi) / 2;
      if (at(y1, y2, mid) < p) lo = mid;
      else hi = mid;
    }
    return at(x1, x2, (lo + hi) / 2);
  };

  // A wheel or a swipe only scrolls what can scroll in the panel, under the pointer
  const scrollsInside = (panel, target, x, y, dy) => {
    for (let el = target; el instanceof Element && panel.contains(el); el = el.parentElement) {
      const box = el.getBoundingClientRect();
      if (x < box.left || x > box.right || y < box.top || y > box.bottom) continue;
      if (!/(auto|scroll)/.test(getComputedStyle(el).overflowY) || el.scrollHeight <= el.clientHeight + 1) continue;
      if (dy > 0 ? el.scrollTop + el.clientHeight < el.scrollHeight - 1 : el.scrollTop > 0) return true;
    }
    return false;
  };

  // One instance, with its own state: two copies on a page never interfere. Everything it
  // starts is tied to `signal`, so kodza:destroy stops it all.
  function mount(root, signal) {
    const pace = paceOf(root);
    const still = reduceMotion || !pace; // the still path: no motion asked, by the visitor or the site
    const has = (name) => root.classList.contains(`kodza-header-grid--${name}`);
    const nav = root.querySelector(c('nav'));
    const links = [...root.querySelectorAll(c('link'))];
    const current = () => links.find((link) => link.getAttribute('aria-current') === 'page') || null;
    const toggle = root.querySelector(c('toggle'));
    const panel = root.querySelector(c('panel'));
    let open = false;
    let compact = false;

    // ---------- The line (--line) ----------
    let line = null;
    let at = null; // the link the line is under
    let over = null; // the link under the pointer or the focus
    if (has('line') && nav && links.length) {
      line = document.createElement('span');
      line.className = 'kodza-header-grid__line';
      line.setAttribute('aria-hidden', 'true');
      nav.append(line);
    }

    // A link's word in the nav's own pixels: a parent scaled by transform does not shift it
    const span = (link) => {
      const n = nav.getBoundingClientRect();
      const scale = n.width / nav.offsetWidth || 1;
      const b = link.getBoundingClientRect();
      const style = getComputedStyle(link);
      return {
        left: (b.left - n.left) / scale + (parseFloat(style.paddingLeft) || 0),
        right: (n.right - b.right) / scale + (parseFloat(style.paddingRight) || 0),
      };
    };
    const set = ({ left, right }) => {
      line.style.setProperty('--kz-header-grid-line-left', `${left}px`);
      line.style.setProperty('--kz-header-grid-line-right', `${right}px`);
    };
    // A line with no length, in the middle of a link's word
    const dot = (link) => {
      const s = span(link);
      const mid = (s.left + nav.offsetWidth - s.right) / 2;
      return { left: mid, right: nav.offsetWidth - mid };
    };
    const instantly = (fn) => {
      line.classList.add('is-instant');
      fn();
      getComputedStyle(line, '::after').clipPath; // both edges settle before the transitions come back
      void line.offsetWidth;
      line.classList.remove('is-instant');
    };
    const go = (link, instant) => {
      if (!line || !line.getClientRects().length) return; // below the breakpoint the links mark themselves
      if (instant) {
        instantly(() => set(link ? span(link) : { left: 0, right: nav.offsetWidth }));
      } else if (link && !at) {
        // From nowhere: it grows out of the middle of the word
        line.classList.remove('is-rightward', 'is-leftward');
        instantly(() => set(dot(link)));
        set(span(link));
      } else if (link) {
        // From a link to the next: the edge in front leaves first
        if (link !== at) {
          const rightward = links.indexOf(link) > links.indexOf(at);
          line.classList.toggle('is-rightward', rightward);
          line.classList.toggle('is-leftward', !rightward);
        }
        set(span(link));
      } else if (at) {
        // Back to nowhere: it shrinks into the middle of the last word
        line.classList.remove('is-rightward', 'is-leftward');
        set(dot(at));
      }
      at = link;
    };

    if (line) {
      nav.addEventListener('pointerover', (event) => {
        const link = event.target.closest?.(c('link'));
        if (!link || link === over) return;
        over = link;
        go(link);
      }, { signal });
      nav.addEventListener('pointerleave', () => {
        over = null;
        go(current());
      }, { signal });
      nav.addEventListener('focusin', (event) => {
        const link = event.target.closest?.(c('link'));
        if (!link) return;
        over = link;
        go(link);
      }, { signal });
      nav.addEventListener('focusout', (event) => {
        if (nav.contains(event.relatedTarget)) return;
        over = null;
        go(current());
      }, { signal });
      document.fonts?.ready.then(() => !signal.aborted && fit());
    }

    // ---------- Scroll (data-mobile="scroll"): the page you are on, in view ----------
    const reveal = () => {
      const link = current();
      if (!compact || root.dataset.mobile !== 'scroll' || !link || nav.scrollWidth <= nav.clientWidth) return;
      const n = nav.getBoundingClientRect();
      const scale = n.width / nav.offsetWidth || 1;
      const b = link.getBoundingClientRect();
      const left = nav.scrollLeft + (b.left - n.left) / scale - (nav.clientWidth - b.width / scale) / 2;
      nav.scrollTo({ left: Math.max(0, left), behavior: 'instant' });
    };

    // ---------- How far the page went (--hide, --tighten) ----------
    let spot = null;
    let lastY = null;
    let frame = 0;

    // Tight, the bar slides up by what it loses (never more than the room around its content) and
    // its content comes back down by its share of that room, so it keeps its place: centred stays
    // centred, a row on the bottom edge stays on it. Measured at rest, in layout pixels
    const shape = () => {
      if (!has('tighten')) return;
      const items = [...root.querySelectorAll(`${c('brand')}, ${c('link')}, ${c('note')}, ${c('button')}, ${c('toggle')}`)]
        .filter((el) => el.getClientRects().length);
      if (!items.length) return;
      const offset = (el, side) => {
        let value = 0;
        for (let e = el; e && e !== root; e = e.offsetParent) value += side === 'top' ? e.offsetTop : e.offsetLeft;
        return value;
      };
      let above = Infinity;
      let below = Infinity;
      items.forEach((el) => {
        const top = offset(el, 'top');
        above = Math.min(above, top);
        below = Math.min(below, root.offsetHeight - top - el.offsetHeight);
      });
      above = Math.max(0, above);
      below = Math.max(0, below);
      const room = above + below;
      const lift = Math.min(lengthOf(root, '--kz-header-grid-tighten', 16), room);
      root.style.setProperty('--kz-header-grid-lift', `${lift}px`);
      root.style.setProperty('--kz-header-grid-drop', `${room ? (lift * below) / room : 0}px`);
      // The brand shrinks towards where it sits: its side of the row, or its middle when centred,
      // and the edge the row sits on
      const brand = root.querySelector(c('brand'));
      if (brand) {
        const middle = offset(brand, 'left') + brand.offsetWidth / 2 - root.offsetWidth / 2;
        const x = Math.abs(middle) < 4 ? 'center' : middle < 0 ? 'left' : 'right';
        const y = room ? `${Math.round((above / room) * 100)}%` : 'center';
        root.style.setProperty('--kz-header-grid-origin', `${x} ${y}`);
      }
    };

    const read = () => {
      frame = 0;
      if (!spot) return;
      const top = parseFloat(getComputedStyle(root).top) || 0;
      const y = top - spot.getBoundingClientRect().top; // how far past its place in the page
      if (has('tighten')) {
        const tight = y > lengthOf(root, '--kz-header-grid-tighten-after', 40);
        if (tight !== root.classList.contains('is-tight')) {
          if (tight) shape();
          root.classList.toggle('is-tight', tight);
        }
      }
      if (has('hide')) {
        if (lastY === null) lastY = y;
        const delta = y - lastY;
        if (Math.abs(delta) < STEP) return;
        lastY = y;
        // Down, past --kz-header-grid-hide-after, and not while you use it from the keyboard or
        // its menu is open; up, it comes back
        const far = y > lengthOf(root, '--kz-header-grid-hide-after', 600);
        const busy = open || root.querySelector(':focus-visible');
        root.classList.toggle('is-hidden', delta > 0 && far && !busy);
      }
    };
    const schedule = () => {
      if (!frame) frame = requestAnimationFrame(read);
    };

    if (has('hide') || has('tighten')) {
      spot = document.createElement('span');
      spot.className = 'kodza-header-grid__spot';
      spot.setAttribute('aria-hidden', 'true');
      root.before(spot);
      document.addEventListener('scroll', schedule, { capture: true, passive: true, signal });
      // The keyboard comes in: it shows up
      root.addEventListener('focusin', () => root.classList.remove('is-hidden'), { signal });
      signal.addEventListener('abort', () => {
        cancelAnimationFrame(frame);
        spot.remove();
      }, { once: true });
    }

    // ---------- The breakpoint, and everything that follows the width ----------
    function fit() {
      compact = root.clientWidth < lengthOf(root, '--kz-header-grid-breakpoint', 900);
      root.classList.toggle('is-compact', compact);
      // No Menu button left on screen: the shutter goes with it
      if (open && toggle && !toggle.getClientRects().length) panel.hidePopover();
      if (line) {
        at = over || current();
        go(at, true);
      }
      reveal();
      if (!root.classList.contains('is-tight')) shape();
    }
    const watch = new ResizeObserver(() => fit());
    watch.observe(root);
    signal.addEventListener('abort', () => watch.disconnect());
    fit();
    read(); // already down the page (a reload, a link to an anchor): in place from the first frame

    // ---------- The shutter (data-mobile="menu") ----------
    const list = root.querySelector(c('list'));
    if (!toggle || !panel || !list || !('popover' in panel)) return;
    toggle.popoverTargetElement = panel;

    // The menu is written from the bar: its links, then its notes and buttons under them
    links.forEach((link, i) => {
      const item = document.createElement('a');
      item.className = 'kodza-header-grid__item';
      ['href', 'target', 'rel', 'aria-current'].forEach((name) => {
        if (link.hasAttribute(name)) item.setAttribute(name, link.getAttribute(name));
      });
      const index = document.createElement('span');
      index.className = 'kodza-header-grid__index';
      index.setAttribute('aria-hidden', 'true');
      index.textContent = String(i + 1).padStart(2, '0');
      const mask = document.createElement('span');
      mask.className = 'kodza-header-grid__mask';
      const word = document.createElement('span');
      word.className = 'kodza-header-grid__word';
      word.textContent = link.textContent.trim();
      mask.append(word);
      item.append(index, mask);
      list.append(item);
    });
    const foot = panel.querySelector(c('foot'));
    if (foot) {
      root.querySelectorAll(`${c('actions')} > ${c('note')}, ${c('actions')} > ${c('button')}`).forEach((el) => {
        const copy = el.cloneNode(true);
        copy.className = el.matches(c('note')) ? 'kodza-header-grid__fact' : 'kodza-header-grid__action';
        foot.append(copy);
      });
    }

    const words = [...toggle.querySelectorAll(c('say'))];
    const parts = [...panel.querySelectorAll(`${c('item')}, ${c('foot')}`)];
    let started = 0;
    let held = null;
    const rolls = new Map();

    // The shutter starts under the header, wherever it is (stuck, tightened)
    const place = () => {
      const edge = Math.max(root.getBoundingClientRect().bottom, toggle.getBoundingClientRect().bottom);
      panel.style.setProperty('--kz-header-grid-from', `${Math.max(0, Math.round(edge))}px`);
    };

    // Each row starts when the edge of the shutter gets to it (the delays are set at the site's pace)
    const time = () => {
      const style = getComputedStyle(root);
      const raw = style.getPropertyValue('--kz-header-grid-menu-duration').trim();
      const duration = still ? 0 : (parseFloat(raw) * (/ms$/.test(raw) ? 1 : 1000) || 800) * pace;
      const match = style.getPropertyValue('--kz-header-grid-menu-ease').match(/cubic-bezier\(([^)]+)\)/);
      const curve = match ? match[1].split(',').map(Number) : [0.76, 0, 0.24, 1];
      const box = panel.getBoundingClientRect();
      const late = performance.now() - started;
      const when = (y) => {
        const p = Math.min(1, Math.max(0, (y - box.top) / (box.height || 1)));
        return `${Math.round(Math.max(0, duration * timeAt(curve, p) - late - 40 * pace))}ms`;
      };
      parts.forEach((part) => {
        part.style.setProperty('--kz-header-grid-delay', when(part.getBoundingClientRect().top));
      });
      // The line under the last row too
      const last = list.lastElementChild;
      if (last) list.style.setProperty('--kz-header-grid-delay-end', when(last.getBoundingClientRect().bottom));
    };

    // Menu rolls to Close and back the same way: the old word goes up, the new one comes from below.
    // A click during the roll turns it back from where the words are
    const roll = () => {
      if (still || words.length < 2) return;
      words.forEach((word, i) => {
        const coming = i === Number(open);
        const from = rolls.has(word) ? getComputedStyle(word).translate : coming ? '0 115%' : '0 0';
        rolls.get(word)?.cancel();
        const run = word.animate(
          { translate: [from, coming ? '0 0' : '0 -115%'] },
          { duration: ROLL * pace, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' },
        );
        rolls.set(word, run);
        run.finished.then(() => rolls.get(word) === run && rolls.delete(word), () => {});
      });
    };

    // While it is open the page stays where it is
    const hold = () => {
      held = new AbortController();
      const s = held.signal;
      const where = [scrollX, scrollY];
      let last = 0;
      document.addEventListener('wheel', (event) => {
        if (!scrollsInside(panel, event.target, event.clientX, event.clientY, event.deltaY)) event.preventDefault();
      }, { passive: false, signal: s });
      document.addEventListener('touchstart', (event) => (last = event.touches[0].clientY), { passive: true, signal: s });
      document.addEventListener('touchmove', (event) => {
        const touch = event.touches[0];
        const dy = last - touch.clientY;
        last = touch.clientY;
        if (!scrollsInside(panel, event.target, touch.clientX, touch.clientY, dy)) event.preventDefault();
      }, { passive: false, signal: s });
      // A scrollbar dragged anyway: the page goes back to where it was
      window.addEventListener('scroll', () => {
        if (scrollX !== where[0] || scrollY !== where[1]) window.scrollTo({ left: where[0], top: where[1], behavior: 'instant' });
      }, { signal: s });
      window.addEventListener('resize', () => place(), { signal: s });
    };
    const release = () => {
      held?.abort();
      held = null;
    };

    panel.addEventListener('beforetoggle', (event) => {
      open = event.newState === 'open';
      toggle.setAttribute('aria-expanded', String(open));
      words.forEach((word, i) => (i === Number(open) ? word.removeAttribute('aria-hidden') : word.setAttribute('aria-hidden', 'true')));
      roll();
      if (open) {
        root.classList.remove('is-hidden');
        started = performance.now();
        place();
        hold();
      } else {
        panel.classList.remove('is-open');
        release();
      }
    }, { signal });

    panel.addEventListener('toggle', (event) => {
      if (event.newState !== 'open' || !open) return;
      time();
      panel.classList.add('is-open');
    }, { signal });

    // Tab past the last link, or a link followed: the shutter rolls back up
    panel.addEventListener('focusout', (event) => {
      const next = event.relatedTarget;
      if (open && next && !panel.contains(next) && next !== toggle) panel.hidePopover();
    }, { signal });
    panel.addEventListener('click', (event) => {
      if (open && event.target.closest('a[href]')) panel.hidePopover();
    }, { signal });

    signal.addEventListener('abort', () => {
      rolls.forEach((run) => run.cancel());
      release();
      if (open) panel.hidePopover();
    });
  }

  // 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. Two
  // components on one root (a reveal on a stat card) both start, and stop together.
  const KODZA = Symbol.for('kodza');
  const RUNNING = Symbol.for(`kodza ${SELECTOR}`); // a root this component runs on, for every copy of this script
  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) => {
    // Already running, maybe from another copy of this script. Marked is-ready with no Kodza component
    // running on it, a root is a copy of a started one (a cloned slide): it is left as it is
    if (root[RUNNING] || (root.classList.contains('is-ready') && !root[KODZA])) return;
    // Rendered by React (data-kodza-manual): React starts it, with an init aimed at that markup once
    // it has hydrated it. Any earlier start would change markup React has not taken over yet
    const owner = root.closest('[data-kodza-manual]');
    if (owner && !(scope && owner.contains(scope))) return;
    const controller = new AbortController();
    // Another Kodza component already running on this root: its markup as written is kept, and the
    // two stop together
    const before = root[KODZA];
    const saved = { html: before ? before.html : written(root), attrs: before ? before.attrs : attrsOf(root), stop: () => destroy(root) };
    if (before) before.above = saved;
    instances.set(root, { controller, saved, before });
    root[KODZA] = saved;
    root[RUNNING] = true;
    mount(root, controller.signal);
    root.classList.add('is-ready');
  });
  const destroy = (scope) => find(scope).forEach((root) => {
    const instance = instances.get(root);
    if (!instance) return;
    instances.delete(root);
    instance.saved.above?.stop(); // a Kodza component started after it on the same root stops first
    // 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[RUNNING];
    // Started on top of another one: that one stops next, and puts the markup back
    if (instance.before) {
      delete instance.before.above;
      root[KODZA] = instance.before;
      return instance.before.stop();
    }
    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).
