/*
 * THE STYLESHEET.
 *
 * One file, no framework, no build step. The same restraint as the application — a monochrome
 * accent, hairline borders, space rather than boxes — at web scale rather than desktop scale.
 *
 * **Light, and only light.** The palette is defined once on bare `:root` and is what every visitor
 * gets; there is no toggle, no second rendition, and no colour declared inside a media query. A
 * visitor whose laptop is in dark mode still sees the page the brand is designed as.
 *
 * The software is dark. This page is not, and the screenshots on it are photographs of the dark
 * application sitting on a light page — which is the honest relationship between the two, rather
 * than a site pretending to be the product.
 *
 * Four other rules held throughout:
 *
 * - **Space does the work that boxes used to.** The page argues with typography and air rather
 *   than by putting every idea in a card. Sections are tall, headlines are large, and there are
 *   far fewer borders than in a normal SaaS layout.
 * - **The page must survive without JavaScript.** `details/summary` for the FAQ, a real form for
 *   checkout, and a page that reads correctly with scripting off. Scroll reveals hide content only
 *   once the script that can un-hide it has run.
 * - **Motion is opt-out at the token level.** Every transition, reveal and looping demo is behind
 *   `--motion`, and `prefers-reduced-motion` sets it to zero once, at the top, rather than being
 *   remembered at fifty call sites.
 * - **Nothing scrolls sideways.** Wide content — the comparison table, the data tables, the code
 *   blocks — scrolls inside its own container. The body never does.
 */

/* =========================================================================
   Tokens — dark is the default
   ========================================================================= */

:root {
  --font-sans:
    -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI Variable Text', 'Segoe UI',
    'Helvetica Neue', system-ui, sans-serif;
  --font-display:
    -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI Variable Display', 'Segoe UI',
    system-ui, sans-serif;
  --font-mono: 'SF Mono', 'Cascadia Mono', Menlo, Consolas, ui-monospace, monospace;

  /*
   * Bone, not white.
   *
   * The same argument the charcoal made, inverted. Pure #fff is what most reads as "unstyled
   * default": it has no material to it, a shadow on it looks like dirt rather than depth, and
   * every hairline has to be darker than it should be to register at all. An off-white with the
   * faintest cool cast gives the page something to sit on, so the ruling can stay a whisper.
   *
   * The lift direction inverts with the ground. On the dark palette a surface rose *towards*
   * white to read as raised; here it rises to white itself, and the sunken surface — inputs, code
   * blocks — drops below the canvas instead.
   */
  --canvas: hsl(190 14% 97.6%);
  --canvas-tint: hsl(200 14% 95.4%);
  /* The hero's ruling: a low-contrast cool grey, around a tenth of the way to the ink. */
  --rule: hsl(200 12% 87%);
  --surface: hsl(0 0% 100%);
  --surface-sunken: hsl(200 16% 95.6%);
  --surface-hover: hsl(200 14% 92.5%);
  --ink: hsl(205 22% 12%);
  --ink-2: hsl(205 12% 34%);
  /* 42%, not 52%: the footnotes and footer headings use this on `--canvas`, and it has to clear
     4.5:1 there rather than merely look grey. */
  --ink-3: hsl(205 10% 42%);
  --line: hsl(205 14% 89%);
  --line-strong: hsl(205 12% 74%);

  /*
   * THE ACCENT IS A VALUE, NOT A HUE.
   *
   * There was a mint here, and it was the wrong idea for this brand. A saturated accent is how a
   * page announces that it is a *product page*; the reference this design answers to is
   * essentially monochrome, and so is every page that still looks considered in five years. So the
   * accent is contrast — near black on the bone ground — and what sits on it is near white.
   *
   * Both halves flip together with the ground, and they must. `--on-accent` is what prints *on*
   * the accent, so a dark accent takes light ink and a light accent takes dark ink. Flipping one
   * without the other is the single most common way a palette swap ships an unreadable primary
   * button, and it is invisible in a diff.
   */
  --accent: hsl(205 24% 13%);
  --accent-hover: hsl(205 28% 8%);
  --accent-ink: hsl(205 22% 17%);
  --accent-tint: hsl(205 22% 94.5%);
  --accent-line: hsl(205 14% 78%);
  --on-accent: hsl(190 20% 98%);

  /*
   * The semantic colours stay, and stay quiet.
   *
   * These are not brand: they mark a duplicated row, a value a spreadsheet would destroy, a
   * decision the software refuses to take on your behalf. Removing them would cost the page its
   * only honest use of colour. They are heavily desaturated instead — enough to read as "wrong"
   * and "settled" beside a monochrome page, nowhere near enough to shout.
   */
  --success: hsl(0 0% 32%);
  --success-ink: hsl(0 0% 24%);
  --success-tint: hsl(205 22% 95%);
  --success-line: hsl(205 14% 80%);
  --warning-tint: hsl(38 62% 95.5%);
  --warning-line: hsl(38 44% 78%);
  --warning-ink: hsl(33 68% 30%);
  --danger-ink: hsl(8 58% 40%);
  --danger-tint: hsl(8 64% 96.5%);
  --danger-line: hsl(8 48% 84%);

  /*
   * A shadow is worth having again.
   *
   * On the near-black ground these were almost invisible and depth came from a hairline and a lift
   * in surface value instead. On bone they read, so they carry the lift — but tinted with the
   * ink's hue rather than pure black, because a neutral-black shadow on a cool off-white goes
   * muddy. Kept shallow: this page still argues with space, not with elevation.
   */
  --shadow-sm: 0 1px 2px hsl(205 24% 16% / 0.06);
  --shadow-md: 0 2px 4px hsl(205 24% 16% / 0.05), 0 8px 20px -6px hsl(205 24% 16% / 0.1);
  --shadow-lg: 0 2px 6px hsl(205 24% 16% / 0.06), 0 20px 44px -12px hsl(205 24% 16% / 0.13);
  --shadow-xl: 0 4px 12px hsl(205 24% 16% / 0.07), 0 40px 80px -20px hsl(205 24% 16% / 0.16);
  /* The one glow on the page, behind the hero — now a shade rather than a light. */
  --glow: hsl(205 30% 30% / 0.05);

  --radius-sm: 7px;
  --radius: 10px;
  --radius-lg: 16px;
  --radius-xl: 22px;
  --wrap: 1560px;
  --gutter: clamp(1.25rem, 4.6vw, 4.5rem);
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  /* Overdamped: decelerating hard, never overshooting. Nothing on this page should bounce. */
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  /*
   * Evenly distributed, for the long changes rather than the arrivals.
   *
   * `--ease-soft` is an exponential out: it covers eighty per cent of its distance in the first
   * quarter of its time. That is exactly right for something *landing* — a panel rising into
   * place — and exactly wrong for something that is meant to be watched happening. The ground
   * changing temperature on it went from charcoal to nearly bone in six hundred milliseconds of a
   * two-and-a-half second animation and then crawled, which is a slow change that looks fast.
   */
  --ease-even: cubic-bezier(0.45, 0.05, 0.35, 1);
  --motion: 1;

  /*
   * The bottom bar's height, and a token because six other things have to agree with it.
   *
   * A fixed bar at the bottom of the screen takes that height away from every other bottom-anchored
   * thing on the page, and getting one of them wrong is invisible until somebody is looking at a
   * phone: the footer hidden behind the bar, the "copied" confirmation appearing underneath it, the
   * hero centring its copy into a space it does not have. `--bar-total` is the number the rest of
   * the stylesheet asks for, because the safe-area inset is part of what the bar occupies.
   *
   * Zero above 1024px, where the bar is `display: none` and the header does the work — so every
   * `calc()` that subtracts it is simply subtracting nothing there, and none of them need a media
   * query of their own.
   */
  --bar-h: 0px;
  --bar-total: calc(var(--bar-h) + env(safe-area-inset-bottom));

  /*
   * The header's height, and the mirror image of the bar above.
   *
   * The header is `position: sticky` at `top: 0`, which means it is *out of the way of layout and
   * in the way of the eye*: anything pinned to the top of the viewport is laid out under the full
   * screen height and then has its first seventy-odd pixels painted over. The hero's pinned frame
   * did exactly that — it centred a screen-tall composition in `100svh` and the top of the
   * recording came to rest behind the bar, with the same distance going spare at the bottom.
   *
   * `--header-h` is the row; `--header-total` is what the page actually loses at the top, which is
   * the row plus the hairline under it. Zero at 1024px and below, where the header is
   * `display: none` and the bottom bar does the work — the two are never on screen together, so
   * every `calc()` that subtracts both is only ever subtracting one.
   */
  --header-h: 72px;
  --header-total: calc(var(--header-h) + 1px);

  color-scheme: light;
}

@media (max-width: 1024px) {
  :root {
    --bar-h: 58px;
    --header-total: 0px;
  }
}

/*
 * THERE IS NO DARK PALETTE.
 *
 * This page was dark for most of its life and is now light. The part of that worth recording is
 * that it was a *palette* change and nothing else: every colour comes from a token above, so the
 * ground moved without a single rule in the other four thousand lines being touched. Five colour
 * literals existed outside the tokens and four of them were inside `mask-image`, where only the
 * alpha matters. That property is what to protect — not the colours.
 *
 * There is deliberately no second rendition. No rule branches on a theme, no colour is declared
 * inside `prefers-color-scheme`, and a visitor whose laptop is in dark mode still sees the page
 * the brand is designed as. `color-scheme: light` on `:root` tells the browser what to paint its
 * scrollbars and form controls in.
 *
 * The application itself is still dark, and the screenshots and the recording on this page are
 * photographs of it. That contrast is intentional: this is the page *about* the software, not a
 * mock-up of it.
 */

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion: 0;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* =========================================================================
   Reset and base
   ========================================================================= */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The sticky header is 72px; without this an anchor lands under it. */
  scroll-padding-top: 6.5rem;
  /* And an anchor near the bottom lands under the bar, on the widths that have one. */
  scroll-padding-bottom: var(--bar-total);
  /*
   * Reserve the scrollbar's space permanently, so the width of the page cannot depend on whether
   * something is currently overflowing. macOS overlay scrollbars take no space either way, so this
   * costs nothing there, and on a classic scrollbar it is the difference between a stable layout
   * and a fifteen-pixel jump the CLS budget would catch.
   */
  scrollbar-gutter: stable;
}

/*
 * No header to clear at the widths that have none, so the top padding goes back to nothing. Left
 * at 6.5rem it would drop every anchor a hundred and four pixels below where it belongs — and
 * `#how`, the hero's own second button, is one of them.
 */
@media (max-width: 1024px) {
  html {
    scroll-padding-top: 0.75rem;
  }
}

body {
  margin: 0;
  /*
   * The bar is `position: fixed`, so it is out of flow and nothing knows it is there. Without this
   * the last thing on every page — the footer, its legal text, the locale switch — sits behind it.
   */
  padding-bottom: var(--bar-total);
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

/*
 * Display type: large, light, and left.
 *
 * 700 was the first draft and it was the single biggest reason the page read as an American SaaS
 * template — heavy display type is *punchy*, and punchy is the opposite of what this brand is
 * saying. At 350 the same words are calm, the size does the emphasis, and the counters stay open.
 * `text-wrap: balance` is off for h1 on purpose: a hand-placed line break says more than an
 * even-looking rag.
 */
h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 350;
  letter-spacing: -0.028em;
  line-height: 1.08;
  margin: 0 0 0.5em;
  text-wrap: balance;
}

h1 {
  font-size: clamp(2.7rem, 6.8vw, 5.4rem);
  letter-spacing: -0.038em;
  line-height: 1.02;
  text-wrap: normal;
}
h2 {
  font-size: clamp(1.95rem, 4.2vw, 3.15rem);
  letter-spacing: -0.032em;
}
h3 {
  font-size: clamp(1.1rem, 1.8vw, 1.26rem);
  font-weight: 500;
  letter-spacing: -0.014em;
  line-height: 1.3;
}

p {
  margin: 0 0 1.1em;
  text-wrap: pretty;
}

a {
  color: var(--accent-ink);
  text-decoration-color: color-mix(in srgb, var(--accent-ink) 34%, transparent);
  text-underline-offset: 0.18em;
}

a:hover {
  text-decoration-color: currentColor;
}

img,
video {
  max-width: 100%;
  height: auto;
  display: block;
}

/*
 * `<figure>` carries a default margin of `1em 40px` in every browser.
 *
 * On a 320-pixel screen that is 80px of margin on an element already as wide as its column, which
 * pushed the product windows past the viewport and made the homepage — and only the homepage —
 * scroll sideways on a phone.
 */
figure {
  margin: 0;
}

:focus-visible {
  outline: 2.5px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

::selection {
  background: color-mix(in srgb, var(--accent) 30%, transparent);
  color: var(--ink);
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--surface);
  color: var(--ink);
  padding: 0.7rem 1.1rem;
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}

.skip:focus {
  left: 0.75rem;
  top: 0.75rem;
}

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.wrap.narrow {
  max-width: 780px;
}

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

/*
 * The wordmark.
 *
 * Light, wide and quiet. The weight is the whole point: at 600 the name competed with the mark
 * and the page read as a loud American product page; at 300 it reads as a Nordic one. Tracking is
 * opened slightly because a light weight at small size closes up.
 */
.wordmark {
  font-family: var(--font-display);
  font-weight: 300;
  letter-spacing: 0.012em;
  line-height: 1;
  white-space: nowrap;
}

/* The mono voice, used for anything that is a value out of a spreadsheet. */
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* =========================================================================
   Buttons and controls
   ========================================================================= */

.btn {
  --btn-bg: var(--surface);
  --btn-ink: var(--ink);
  --btn-line: var(--line-strong);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.7rem 1.2rem;
  border: 1px solid var(--btn-line);
  border-radius: var(--radius);
  background: var(--btn-bg);
  color: var(--btn-ink);
  font: inherit;
  font-weight: 500;
  font-size: 0.95rem;
  letter-spacing: -0.002em;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition:
    transform calc(0.14s * var(--motion)) var(--ease),
    background-color calc(0.14s * var(--motion)) var(--ease),
    border-color calc(0.14s * var(--motion)) var(--ease),
    box-shadow calc(0.14s * var(--motion)) var(--ease);
}

.btn:hover {
  background: var(--surface-hover);
  transform: translateY(calc(-1px * var(--motion)));
}

.btn:active {
  transform: translateY(0);
}

.btn--primary {
  --btn-bg: var(--accent);
  --btn-ink: var(--on-accent);
  --btn-line: transparent;
}

.btn--primary:hover {
  --btn-bg: var(--accent-hover);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent);
}

.btn--ghost {
  --btn-bg: transparent;
  --btn-line: var(--line-strong);
}

.btn--lg {
  padding: 1rem 1.8rem;
  font-size: 1rem;
  border-radius: var(--radius);
}

.btn--sm {
  padding: 0.5rem 0.95rem;
  font-size: 0.88rem;
}

.btn--block {
  width: 100%;
}

.btn[disabled],
.btn[aria-disabled='true'] {
  opacity: 0.55;
  pointer-events: none;
}

.icon-btn {
  display: inline-grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
  transition: background-color calc(0.14s * var(--motion)) var(--ease);
}

.icon-btn:hover {
  background: var(--surface-hover);
  color: var(--ink);
}

.link {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent-ink);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

/*
 * 1rem, not the 0.98rem this wanted to be. Safari on iOS zooms the viewport when a field smaller
 * than 16px takes focus, and it does not zoom back out -- so tapping the email field on /pricing
 * threw the buyer into a magnified, sideways-scrolling page at the one moment the page is asking
 * for a card. The two hundredths of a rem were not worth that.
 */
input[type='email'],
input[type='text'],
select {
  width: 100%;
  padding: 0.72rem 0.85rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--ink);
  font: inherit;
  font-size: 1rem;
}

input::placeholder {
  color: var(--ink-3);
}

input:focus-visible,
select:focus-visible {
  outline: 2.5px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
}

label {
  display: block;
  font-size: 0.88rem;
  font-weight: 600;
  margin-bottom: 0.35rem;
}

.field-help {
  font-size: 0.84rem;
  color: var(--ink-3);
  margin: 0.4rem 0 0;
}

.field-error {
  font-size: 0.86rem;
  color: var(--danger-ink);
  margin: 0.45rem 0 0;
}

/* =========================================================================
   Header
   ========================================================================= */

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--canvas) 78%, transparent);
  backdrop-filter: saturate(160%) blur(16px);
  -webkit-backdrop-filter: saturate(160%) blur(16px);
  border-bottom: 1px solid transparent;
  transition: border-color calc(0.2s * var(--motion)) var(--ease);
}

.site-header.is-stuck {
  border-bottom-color: var(--line);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: 2rem;
  height: var(--header-h);
}

/* The lockup: mark plus drawn wordmark, at the one spacing that reads as a single object. */
.brand {
  display: inline-flex;
  align-items: center;
  color: var(--ink);
  text-decoration: none;
}

.lockup {
  display: inline-flex;
  align-items: center;
}

.lockup .logo,
.lockup .wordmark {
  display: block;
  flex: none;
}

.site-nav {
  display: flex;
  gap: 0.4rem;
  margin-left: auto;
}

.site-nav__link {
  padding: 0.45rem 0.8rem;
  border-radius: var(--radius-sm);
  color: var(--ink-2);
  text-decoration: none;
  font-size: 0.93rem;
  font-weight: 500;
}

.site-nav__link:hover {
  background: var(--surface-hover);
  color: var(--ink);
}

.site-nav__link.is-current {
  color: var(--ink);
}

.site-header__actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/*
 * The header's call to action is outlined, not filled.
 *
 * A filled mint pill in the corner competes with the filled mint pill in the hero — two equally
 * loud asks on one screen, which is how a calm page starts looking eager. Outlined, it is still
 * plainly a button and still the second thing the eye finds after the headline.
 */
.site-header__actions .btn--primary {
  --btn-bg: transparent;
  --btn-ink: var(--ink);
  --btn-line: var(--line-strong);
}

.site-header__actions .btn--primary:hover {
  --btn-bg: var(--surface-hover);
  --btn-line: color-mix(in srgb, var(--accent) 50%, var(--line-strong));
  box-shadow: none;
}

/* =========================================================================
   The bottom bar — the whole navigation at 1024px and below
   ========================================================================= */

/*
 * ONE BAR, AT THE BOTTOM, WHERE THE THUMB IS.
 *
 * At 1024px and below the header is not collapsed, it is gone, and this is the only chrome on the
 * page. See `bottomBar()` in `layout.js` for why these four slots and why the mark is the home tab.
 *
 * What this replaced: `.site-nav { display: none }` below 760px, with the three links folded into a
 * `<details>` disclosure in the top-right corner and the call to action beside it. Two problems
 * with that, and the second is the expensive one. A tablet at 834px was *above* the breakpoint, so
 * it got the full desktop inline navigation on a touch screen. And on a phone the one thing the
 * page exists to sell sat in the furthest corner from the hand holding it, behind a tap.
 *
 * The height is a token because six other things have to know it — see the collisions section at
 * the end of this file.
 */
.bottom-bar {
  display: none;
}

@media (max-width: 1024px) {
  .site-header {
    display: none;
  }

  .bottom-bar {
    position: fixed;
    inset: auto 0 0 0;
    /*
     * Between the header's 40 and the toast's 60. The toast has to stay above this: it is the
     * confirmation that a licence key was copied, and a confirmation hidden behind the bar is
     * worse than no confirmation at all.
     */
    z-index: 50;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr)) auto;
    align-items: center;
    gap: 0.4rem;
    height: var(--bar-h);
    box-sizing: content-box;
    padding-inline: var(--gutter);
    /*
     * The inset is *added* to a real padding rather than being the whole of it.
     *
     * `viewport-fit=cover` has been in the meta tag all along and nothing in this stylesheet ever
     * read `env()`, so content has been running under the iOS home indicator this entire time.
     * This is the first thing to respect it. Safari also reports the bottom inset as 0 whenever
     * its own toolbar is hidden, so 0 has to be a survivable answer rather than the only spacing.
     */
    padding-bottom: env(safe-area-inset-bottom);
    background: color-mix(in srgb, var(--canvas) 88%, transparent);
    backdrop-filter: saturate(160%) blur(16px);
    -webkit-backdrop-filter: saturate(160%) blur(16px);
    border-top: 1px solid var(--line);
  }
}

/*
 * A tab is an icon over a word. Never an icon alone: an unlabelled glyph in a navigation bar is
 * guesswork, and only a house and a magnifying glass are really exempt from that — which is none
 * of these three.
 */
.bottom-bar__tab {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 0.2rem;
  min-height: 48px;
  padding: 0.3rem 0.2rem;
  border-radius: var(--radius-sm);
  color: var(--ink-3);
  text-decoration: none;
  font-size: 0.69rem;
  font-weight: 500;
  line-height: 1.1;
  text-align: center;
}

.bottom-bar__tab span {
  /* Danish is the constraint here: `Spørgsmål`, `Prøv gratis`. One line, or the bar grows a row. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.bottom-bar__tab svg {
  display: block;
}

/*
 * The current tab is marked by *value*, not by a hue or a pill.
 *
 * This page spends its entire colour budget on the four semantic values inside the product's own
 * screenshots. A coloured tab here would be the only brand hue on the site, and it would be spent
 * on bookkeeping.
 */
.bottom-bar__tab.is-current {
  color: var(--ink);
}

.bottom-bar__cta {
  justify-self: end;
  min-height: 44px;
  padding-inline: 1rem;
  font-size: 0.85rem;
  white-space: nowrap;
}

/*
 * Below 390px the four slots stop fitting at full width — the same point at which the header used
 * to drop its wordmark, and for the same reason. The action keeps its label and the tabs give up
 * padding, because the action is the point.
 */
@media (max-width: 389px) {
  .bottom-bar {
    padding-inline: 0.75rem;
    gap: 0.15rem;
  }

  .bottom-bar__cta {
    padding-inline: 0.75rem;
    font-size: 0.8rem;
  }

  .bottom-bar__tab {
    font-size: 0.66rem;
  }
}

/*
 * Touch targets.
 *
 * The header's call to action is 40px tall at `--sm`, under the 44px minimum every platform's
 * guidance gives — and it is the primary action on the busiest element of the page. Keyed to a
 * coarse pointer rather than to width, because a small window on a laptop does not need bigger
 * buttons and a large tablet does.
 */
@media (pointer: coarse) {
  .site-header__actions .btn,
  .icon-btn {
    min-height: 44px;
  }
  .icon-btn {
    min-width: 44px;
  }
  .site-nav__link,
  .site-footer__col a {
    padding-block: 0.55rem;
  }
}

/*
 * Below about 390px the lockup, the theme toggle and the call to action stop fitting on one line —
 * and Danish makes it worse, because every label is a word or two longer. The wordmark goes first:
 * the mark alone still says whose site this is, and the call to action must not shrink.
 */
@media (max-width: 389px) {
  .brand .wordmark {
    display: none;
  }
  .site-header__inner {
    gap: 0.6rem;
  }
  .btn--sm {
    padding-inline: 0.7rem;
  }
}

/* =========================================================================
   Sections
   ========================================================================= */

.section {
  padding-block: clamp(5rem, 12vw, 9rem);
}

/*
 * There is no tint any more.
 *
 * Alternating tinted sections is how a page ends up looking like a stack of slides. The whole
 * document is now one ground, and sections are separated by a hairline and by space — which is
 * what makes the vertical rhythm read as one continuous sheet rather than as bands.
 */
.section--tint {
  border-top: 1px solid var(--line);
}

.section--tight {
  padding-block: clamp(3rem, 6vw, 4.5rem);
}

/*
 * The section immediately below the hero.
 *
 * The hero already ends with its own generous padding, and adding a full section's worth on top
 * left a hole rather than breathing room.
 */
.section--after-hero {
  padding-top: clamp(0.5rem, 2vw, 1.75rem);
}

/* A hairline that separates two sections of the same value without a tint change. */
.section--ruled {
  border-top: 1px solid var(--line);
}

.section-head {
  max-width: 42rem;
  margin-bottom: clamp(2.25rem, 5vw, 3.5rem);
}

/*
 * Kept as a hook, but it no longer centres.
 *
 * Pricing and the questions used to be centred blocks in an otherwise left-ranged page, which is
 * the layout equivalent of changing your mind halfway down. Everything ranges left now; the
 * class survives so the templates did not all have to change.
 */
.section-head--centre {
  margin-inline: 0;
  text-align: left;
}

/*
 * Eyebrows are grey, not mint.
 *
 * Eight accent-coloured labels down a page is eight things competing with the one accent that
 * matters — the call to action. They keep the small rule, which is the page's single ornament,
 * and give up the colour.
 */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 1.1rem;
  font-size: 0.74rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* A short accent rule before an eyebrow, which is the page's one repeated ornament. */
.eyebrow::before {
  content: '';
  width: 1.75rem;
  height: 1px;
  background: currentColor;
  opacity: 0.5;
  flex: none;
}

.lede {
  font-size: clamp(1rem, 1.4vw, 1.12rem);
  color: var(--ink-2);
  max-width: 54ch;
  line-height: 1.62;
}

/* Scroll reveal. Elements start visible and are hidden by script only when it can undo it, so a
   page with no JavaScript — or one whose script fails — shows everything. */
[data-reveal] {
  opacity: 1;
}

.js [data-reveal] {
  /* 0.001 rather than 0: an element that is *exactly* transparent is treated as hidden by some
     assistive technology and by contrast tooling. This keeps it in the accessibility tree while
     still reading as a fade. */
  opacity: calc(0.001 + (1 - var(--motion)));
  transform: translateY(calc(16px * var(--motion)));
  transition:
    opacity calc(0.7s * var(--motion)) var(--ease),
    transform calc(0.7s * var(--motion)) var(--ease);
}

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* =========================================================================
   Hero
   ========================================================================= */

.hero {
  position: relative;
  isolation: isolate;

  /*
   * The grid pitch, declared here rather than on the field.
   *
   * Four layers draw on these coordinates — the ruling, the ambient cells, the blocks and the
   * paper — and three of them are siblings of the field rather than children of it, so a token
   * declared there is simply not in scope for them. The paper's ruling silently did not render at
   * all until this moved up: `background-size: var(--cell-w)` with nothing to resolve is an
   * invalid declaration, not a visible error.
   *
   * A shallow grid: cells four times wider than tall. A square grid is graph paper; the proportion
   * is the whole character of this one, because a wide flat cell reads as a *row*, which is what
   * this product is about.
   */
  --cell-w: 93px;
  --cell-h: 23px;
}

.hero__frame {
  position: relative;
  overflow: clip;
  /* Generous, and deliberately so: the field needs room above the type or the page opens on a
     wall of words. This is the unpinned case — no script, or motion turned off. */
  padding-top: clamp(4rem, 13vh, 9rem);
  padding-bottom: clamp(3rem, 7vw, 5rem);
}

/*
 * THE RUNWAY — the transport for the whole sequence.
 *
 * A screen-tall frame pinned inside a taller runway. The distance the runway adds beyond one
 * screen is the distance the sequence plays across, and that is the entire mechanism: no clock,
 * nothing held, the reader's own scroll is the transport.
 *
 * `view-timeline-name` publishes the runway's progress through the viewport as a timeline that the
 * frame's children animate against. `contain` is the range that matters. For a subject *taller*
 * than the viewport it is defined as the span in which the subject completely covers it — which is
 * exactly the span in which the frame is pinned. So `contain 0%` → `contain 100%` is pinned-start
 * → pinned-end at every viewport size, with no magic numbers to keep in sync.
 *
 * This replaced a hold. The page used to answer the first wheel gesture by freezing the document
 * for five and a half seconds while a timed choreography played; `site.js` has the note on what
 * that cost and why it is gone. The short version is that a page which takes your scroll is the
 * one thing a page must not do, and mapping the scroll gets the same picture without asking.
 *
 * Two and a fifth screens. The extra 1.2 is what the sequence spends, and it is a judgement rather
 * than a measurement: long enough that the whole thing is not over in a twitch, short enough that
 * nobody has to work to get out of it. Touch gets more, because a thumb covers less ground per
 * gesture than a wheel does.
 *
 * **This is not the pacing control, and trying to use it as one is how it briefly became 3.2
 * screens.** The complaint was that the handover went past too fast, and lengthening the runway
 * does slow it — by charging the reader more scrolling for the same animation, which is the one
 * thing a scroll-linked sequence must not do. How fast the picture *moves* is set by the damping
 * in `site.js`; this only sets how much of the page the sequence occupies. Tune them separately.
 */
.js .hero__runway {
  height: 220svh;
  /* The driver below is a child, and the things that animate are grandchildren; a timeline name is
     only visible to the subtree of the element that declares it, so it is published here. */
  position: relative;
  timeline-scope: --hero;
}

/*
 * THE DRIVER — a scroll container nobody scrolls, which is the whole trick.
 *
 * The sequence used to hang off `view-timeline-name` on the runway itself: the browser mapped the
 * runway's progress through the viewport straight onto the animations, on the compositor, with no
 * script at all. That was right about everything except pace. A view timeline is *position*, and
 * position is exactly as fast as the reader's fingers — a momentum flick advances nine hundred
 * pixels in one frame, so the choreography did not play fast, it teleported. There was no knob to
 * turn: the only way to make the picture move more slowly was to make the reader scroll further
 * for it, which is charging them for the fix.
 *
 * So the timeline no longer reads the page. It reads this: one pixel square, clipped, empty, and
 * scrollable to exactly 1000px. `site.js` eases its `scrollTop` toward wherever the reader's real
 * scroll says the sequence should be. Scroll position still decides the destination — flick and
 * the whole thing plays, scroll back and it runs backwards, stop halfway and it holds halfway —
 * but a *rate* now sits between the gesture and the picture, and that rate is a number in one
 * place rather than a property of somebody's trackpad.
 *
 * What this keeps from the CSS-only version: every animation is still declared in this stylesheet,
 * still driven by a real timeline, still composited. The script sets one `scrollTop` per frame and
 * reads nothing back. It is not a scrub loop — there is no `getBoundingClientRect` per element, no
 * style written per element, and nothing here is on the main thread that was not already.
 */
.js .hero__driver {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
  pointer-events: none;
  scroll-timeline-name: --hero;
  scroll-timeline-axis: block;
}

/* The scrollable distance, and the only number the script multiplies progress by. */
.js .hero__driver::before {
  content: '';
  display: block;
  width: 1px;
  height: 1001px;
}

.js .hero__frame {
  position: sticky;
  top: 0;
  /*
   * The screen *minus the bar*, because the bar is fixed and the frame would otherwise centre its
   * copy into fifty-eight pixels it does not have — pushing the headline, the buttons and the
   * recording all half a bar's height too low, on the screens with the least room to spare.
   * `--bar-total` is zero above 1024px, so this is plain `100svh` wherever there is no bar.
   */
  height: calc(100svh - var(--bar-total));
  /* Pinned, so the copy centres in the screen rather than sitting under a fixed padding. */
  display: grid;
  align-content: center;
  /*
   * ...and *the screen minus the header*, for the same reason and at the other end. The header is
   * sticky at `top: 0` and so is this, so the frame's first seventy-three pixels are laid out
   * underneath it and painted over by it. Centring in the whole of `100svh` put the top of the
   * recording behind the bar with the same distance going spare underneath the buttons — see the
   * note on `--header-total`. Padding rather than a shorter box: the frame still has to *be* a
   * screen tall, because that is what the pin and the `contain` range are measured against.
   */
  padding-top: var(--header-total);
  padding-bottom: 0;
}

@media (pointer: coarse) {
  .js .hero__runway {
    height: 300svh;
  }
}

/*
 * NO VIEW TIMELINES: THE RECORDING GOES BACK INTO THE FLOW.
 *
 * Not a degradation worth apologising for — it is the page reduced motion has always been given,
 * and it is already covered by tests. What must never happen is the *pin* without the timeline:
 * that would hold a screen-tall frame in place while nothing on it ever moved, which is a page
 * that looks broken rather than one that is plain.
 *
 * Probed with `scroll()` *and* `timeline-scope`, because this sequence needs both and they did not
 * ship together. `animation-timeline` alone is not enough: the timeline is declared on the driver
 * and consumed by the driver's siblings' descendants, which only works where a name can be
 * published up to a common ancestor. A browser with the first and not the second would apply the
 * pin and then hold a screen-tall frame still while nothing on it ever moved — a page that looks
 * broken rather than one that is plain, which is the exact failure this block exists to prevent.
 */
@supports not ((animation-timeline: scroll()) and (timeline-scope: --probe)) {
  .js .hero__runway {
    height: auto;
  }

  .js .hero__frame {
    position: static;
    height: auto;
    padding-top: clamp(4rem, 13vh, 9rem);
    padding-bottom: clamp(3rem, 7vw, 5rem);
  }

  /*
   * Everything the sequence would have animated, at the value it would have ended on. Without
   * this the copy keeps its `visibility: visible` start frame — fine — but the recording keeps
   * its `opacity: 0` one, and the hero is a headline over an empty box.
   */
  .js .hero__sheet {
    display: none;
  }

  .js .hero__reveal {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    clip-path: none;
    pointer-events: auto;
  }

  .js .hero__caption,
  .js .hero__reveal-actions {
    opacity: 1;
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .js .hero__runway {
    height: auto;
  }

  .js .hero__frame {
    position: static;
    height: auto;
    padding-top: clamp(4rem, 13vh, 9rem);
    padding-bottom: clamp(3rem, 7vw, 5rem);
  }
}

.hero__field {
  position: absolute;
  inset: -14% -12% auto -12%;
  height: 122%;
  pointer-events: none;
  z-index: -1;
  -webkit-mask-image: radial-gradient(ellipse 95% 78% at 50% 38%, #000 0%, transparent 82%);
  mask-image: radial-gradient(ellipse 95% 78% at 50% 38%, #000 0%, transparent 82%);
}

/*
 * A finer pitch on a phone, scaled from the desktop one.
 *
 * Kept at roughly the same *number* of columns across the viewport as the desktop layout gets,
 * rather than the same absolute size: a 93px cell on a 390px screen would give four columns and
 * the grid would read as a few stray lines instead of a sheet.
 */
@media (max-width: 760px) {
  .hero {
    --cell-w: 51px;
    --cell-h: 13px;
  }

  /*
   * On a phone the headline wraps onto more lines and starts higher, so only the upper part of the
   * top band is still clear of it. Seventeen cells were sitting on the headline at 390px before
   * this rule.
   */
  .hero__cell:not(.hero__cell--phone) {
    display: none;
  }
}

/*
 * Between 761 and 1180 the copy is nearly the full width — ink to `fx` 0.78, buttons to 0.87 —
 * against a visible field that ends at 0.903. There is no room beside it at those widths, so the
 * cells that live there are hidden rather than squeezed onto the type.
 */
@media (max-width: 1199px) {
  .hero__cell--wide {
    display: none;
  }
}

/*
 * Film grain.
 *
 * Fractal noise at a low alpha, over the whole field. It is the difference between "a black
 * rectangle" and "a surface": it kills the banding in the mask's gradient and gives the blurred
 * cells something to sit in.
 */
.hero__field::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
  opacity: 0.05;
  mix-blend-mode: overlay;
}

/* A lit cell, spanning two columns and softened until it stops looking like a component. */
/*
 * A lit cell.
 *
 * `round(down, …)` snaps a fractional position onto the cell grid, so a cell placed at "63% of
 * the way across" lands exactly on a rule rather than a few pixels off it. That is what lets the
 * positions in `heroField()` be pitch-independent: the arithmetic happens here, against whatever
 * `--cell-w` and `--cell-h` currently are.
 *
 * The `+ 1px` and `- 1px` inset the fill inside its rules; a fill that covers the ruling looks
 * like a rendering artefact, and the point is that these are cells.
 */
.hero__cell {
  position: absolute;
  left: calc(round(down, var(--fx) * 100%, var(--cell-w)) + 1px);
  top: calc(round(down, var(--fy) * 100%, var(--cell-h)) + 1px);
  width: calc(round(nearest, var(--fw) * 100%, var(--cell-w)) - 1px);
  height: calc(var(--cell-h) - 1px);
  opacity: 0;
  filter: blur(2px);
}

/*
 * Without `round()` the cells still land, just not snapped to a rule.
 *
 * Safari only shipped it in 15.4 and Firefox in 118. An unsnapped cell is a slightly soft edge on
 * a blurred decoration that nobody will notice; the alternative — no cells at all — would leave
 * older browsers looking at a flat black rectangle.
 */
@supports not (left: round(down, 10px, 3px)) {
  .hero__cell {
    left: calc(var(--fx) * 100% + 1px);
    top: calc(var(--fy) * 100% + 1px);
    width: calc(var(--fw) * 100% - 1px);
  }
}

/*
 * An ambient cell, at the alpha the reference actually uses.
 *
 * Measured off its canvas: the fills peak between 2 and 6 out of 255 — under one to two and a
 * half per cent. This was at nine, which is where "a surface that is not quite even" becomes "a
 * lit rectangle", and no amount of scattering fixes a rectangle that is too bright to begin with.
 *
 * The field's radial mask takes roughly another 55% out of these two zones, so 5.5% at the top of
 * the gradient arrives at about 2.5% on the glass.
 */
/*
 * Retuned for the bone ground, and the numbers had to move a long way.
 *
 * These percentages were measured against the charcoal, where `--ink` was near white: 5.5% of it
 * was a lit rectangle because light-on-dark gains contrast fast at low alpha. `--ink` is now near
 * black and the same 5.5% is a smudge — dark-on-light climbs much more slowly near white, so the
 * fill needs roughly double the alpha to read as the same *material*.
 *
 * Still a gradient and still blurred: the point is a surface that is not quite even, not a set of
 * rectangles. The field's radial mask takes roughly another 55% out at the edges.
 */
.hero__cell--fill {
  background: linear-gradient(
    155deg,
    color-mix(in srgb, var(--ink) 11%, transparent) 0%,
    color-mix(in srgb, var(--ink) 3.5%, transparent) 70%,
    transparent 100%
  );
  filter: blur(4px);
}

/*
 * The cursor.
 *
 * One cell of the path at a time, for about half a second — bright, because it is almost never
 * there. An outline rather than a fill: a *selected* cell is what the product draws when it is
 * looking at something, and it is what the reference draws too.
 */
.hero__cell--step {
  border: 1px solid color-mix(in srgb, var(--ink) 62%, transparent);
  box-shadow: 0 0 22px -8px color-mix(in srgb, var(--ink) 22%, transparent);
  filter: blur(0.2px);
}

/* The ones that arrive out of line. An outline, because a *selected* cell is what the product
   draws when it is looking at something. */
.hero__cell--snap {
  border: 1px solid color-mix(in srgb, var(--ink) 52%, transparent);
  box-shadow: 0 0 30px -10px color-mix(in srgb, var(--ink) 18%, transparent);
  filter: blur(0.4px);
}

/*
 * Every cell on its own clock.
 *
 * One shared cycle — which is what this had, at 16s and before that 26 — means every cell peaks
 * at the same point of its own loop, and the field develops a detectable rhythm however carefully
 * the delays are scattered. `--dur` is set per cell in `heroField()`, spread across 9–26 seconds,
 * and those share no common multiple inside any time a visitor will spend on this page.
 *
 * The delays are negative, which starts each animation already part-way through: the field is
 * alive in the first frame rather than filling in over half a minute.
 */
.js .hero__cell--fill {
  animation: cell-lit var(--dur, 16s) var(--ease) infinite;
  animation-delay: var(--delay);
}

.js .hero__cell--step {
  animation: cell-step var(--dur, 23s) var(--ease) infinite;
  animation-delay: var(--delay);
}

.js .hero__cell--snap {
  animation: cell-snap var(--dur, 26s) var(--ease) infinite;
  animation-delay: var(--delay);
}

/*
 * Up slowly, hold, down slowly.
 *
 * The reference ramps its fills in steps of one alpha unit over seconds at a time — nothing ever
 * switches. Against a 9–26 second duration these percentages come to a 1–3 second rise and a
 * 1.5–4.5 second fall, so a cell is perceptibly *arriving* rather than appearing, and the spread
 * of durations gives the same 0.2-to-9-second range of lit times that was measured there.
 *
 * Lit for about a third of the cycle, which across 63 cells keeps roughly twenty up at once — the
 * reference holds twenty-six of a slightly larger field.
 */
@keyframes cell-lit {
  0%,
  4% {
    opacity: 0;
  }
  16%,
  34% {
    opacity: 1;
  }
  52%,
  100% {
    opacity: 0;
  }
}

/*
 * A hop. Under half a second, then dark for the rest of the cycle.
 *
 * The percentages are small because the cycle is long: at 23s this is 140ms of rise, 160ms of
 * hold and 160ms of fall. Five of these staggered 0.52s apart make one cell-by-cell walk across
 * the grid, and then nothing there for twenty seconds.
 */
@keyframes cell-step {
  0%,
  0.6% {
    opacity: 0;
  }
  1.2%,
  1.9% {
    opacity: 1;
  }
  2.6%,
  100% {
    opacity: 0;
  }
}

@keyframes cell-snap {
  0%,
  1% {
    opacity: 0;
    transform: translate(calc(var(--cell-w) * 0.55), calc(var(--cell-h) * -1));
  }
  2.5%,
  5% {
    opacity: 0.75;
    transform: translate(calc(var(--cell-w) * 0.55), calc(var(--cell-h) * -1));
  }
  7%,
  11% {
    opacity: 0.75;
    transform: translate(0, 0);
  }
  14%,
  100% {
    opacity: 0;
    transform: translate(0, 0);
  }
}

/*
 * With motion off the field is still, and the cells that would have moved are shown already in
 * place. A static grid with a few lit cells is a perfectly good background; it is the movement
 * that is the enhancement.
 */
@media (prefers-reduced-motion: reduce) {
  .hero__cell {
    opacity: 0;
    animation: none !important;
    transform: none !important;
  }

  /*
   * Every fifth cell, rather than all of them. Seventy-five cells lit at once is a checkerboard,
   * which is a fair description of what this rule produced when the field held twenty.
   */
  .hero__cell:nth-of-type(5n + 2) {
    opacity: 0.7;
  }
}

/* One soft light, low and left, behind the type. Neutral rather than mint: a coloured wash behind
   a headline is the thing that makes a dark page look like a template. */
/*
 * THE SEQUENCE.
 *
 * Every moving part animates against `--hero`, the view timeline declared on the runway above,
 * across its own slice of the pin. There is no progress value in script, no frame loop and no
 * clock — the browser interpolates each of these against its own scroll offset, on the compositor.
 *
 * Ranges rather than delays, as fractions of the time the frame is pinned:
 *
 *     0 → 40%    the headline lifts and goes
 *     8 → 40%    the ambient cells fade with it
 *    10 → 55%    a band of blocks crosses the grid, left to right
 *    42 → 72%    the recording opens along the ruling
 *    50 → 76%    its caption
 *    54 → 80%    its buttons
 *    80 → 100%   settled, and held there until the frame unpins
 *
 * Every phase overlaps its neighbour, and that is not decoration: without the overlaps the screen
 * is empty between one phase and the next, which reads as a page that has broken rather than one
 * that is changing. The last fifth is deliberately still — arriving at the finished state at the
 * exact moment the hero unpins would give nobody time to see it.
 *
 * **`both` fill on all of them**, so each holds its first frame before its range and its last frame
 * after. That is what pins the end state now. There is no `is-done` class any more, because there
 * is no longer a moment called done: scrolled backwards, the whole thing runs backwards.
 *
 * **`linear` on all of them, and that is deliberate.** Easing a scroll-linked animation eases it
 * against *distance* rather than time, so the picture stops tracking the fingers — the wheel moves
 * a constant amount and the screen answers at a changing rate, which reads as lag. The curves this
 * page uses everywhere else are right for animations that own their clock and wrong for these.
 */

/* ------------------------------------------------------------------ the copy leaves */

/*
 * It has to be *gone* rather than half-gone. The recording arrives in the same box, so a
 * half-faded headline over an arriving panel is two things competing for one rectangle.
 *
 * `visibility` is in the keyframes rather than in a separate end-state rule because there is no
 * end-state rule any more. It is a discrete property, so it flips at a keyframe rather than
 * interpolating: held visible to 99% and hidden at 100%, the copy leaves the tab order exactly
 * when it leaves the screen, and comes back the moment the reader scrolls up again.
 */
.js .hero__inner {
  animation: hero-copy-out linear both;
  animation-timeline: --hero;
  animation-range: 0% 40%;
}

.js .hero__cells {
  animation: hero-fade-out linear both;
  animation-timeline: --hero;
  animation-range: 8% 40%;
}

/*
 * The lift starts at once and the fade waits.
 *
 * The scroll has to be *answered* before anything disappears, or the first thing the reader gets
 * for their gesture is absence. So the headline starts moving on the first pixel and only then
 * begins to go.
 */
@keyframes hero-copy-out {
  0% {
    opacity: 1;
    transform: translateY(0);
    visibility: visible;
  }
  38% {
    opacity: 1;
  }
  99% {
    visibility: visible;
  }
  100% {
    opacity: 0;
    transform: translateY(-28px);
    visibility: hidden;
  }
}

@keyframes hero-fade-out {
  to {
    opacity: 0;
  }
}

/* ------------------------------------------------------------------ the blocks */

/*
 * A sparse set of blocks on the grid's own coordinates — 110 of them, not a tiling of the field.
 *
 * Each carries `--delay`, which is its column times a fixed step plus a little jitter, so what
 * crosses the screen is a band with a ragged edge rather than a ruled line sweeping it.
 */
.hero__sheet {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  display: none;
}

.js .hero__sheet {
  display: block;
}

.sheet__cell {
  position: absolute;
  left: calc(var(--x) * var(--cell-w) + 1px);
  top: calc(var(--y) * var(--cell-h) + 1px);
  width: calc(var(--w) * var(--cell-w) - 1px);
  height: calc(var(--cell-h) - 1px);
  /*
   * A fixed light tone rather than one that follows the ground. The band has left the screen
   * before the ground is light, so the two never have to agree — which removes a `color-mix` per
   * block per frame and one whole class of "is this still legible" question.
   */
  background: color-mix(in srgb, var(--ink) 11%, transparent);
  opacity: 0;
}

/*
 * `--delay` is a percentage of the pin, not a duration — see `heroSheet()` in `components.js`.
 * Adding it to both ends of the range slides the block's whole life along the timeline, which is
 * what a delay used to do and what a scroll-linked animation has no other way to express.
 */
.js .sheet__cell {
  animation: hero-block linear both;
  animation-timeline: --hero;
  animation-range: calc(10% + var(--delay)) calc(26% + var(--delay));
}

/* In quickly, hold, out slowly. Leaving deliberately is what makes the band read as considered. */
@keyframes hero-block {
  0% {
    opacity: 0;
  }
  26% {
    opacity: var(--tone);
  }
  52% {
    opacity: var(--tone);
  }
  100% {
    opacity: 0;
  }
}

/* ------------------------------------------------------------------ the ruling */

/*
 * One set of lines, and it used to be two.
 *
 * While the ground travelled from charcoal to bone there had to be a light ruling and a dark one
 * crossfading, because any single rule colour interpolated between them passes through the same
 * mid grey the ground does, and the grid vanished exactly where the screen most needed structure
 * in it. That whole problem was created by the ground changing temperature. It no longer does —
 * the hero sits on `--canvas` like every other section — so there is one ruling, in one colour,
 * and it simply stays.
 *
 * It dims as the recording opens rather than leaving. The panel needs the screen; the grid is what
 * the panel is arriving *onto*, and taking it away entirely leaves the recording floating.
 *
 * Shares the field's mask, so the grid fades at the edges instead of stopping in a line.
 */
.hero__rules {
  position: absolute;
  inset: -14% -12% auto -12%;
  height: 122%;
  z-index: -2;
  pointer-events: none;
  background-size: var(--cell-w) var(--cell-h);
  background-image:
    linear-gradient(to right, var(--rule) 1px, transparent 1px),
    linear-gradient(to bottom, var(--rule) 1px, transparent 1px);
  -webkit-mask-image: radial-gradient(ellipse 95% 78% at 50% 38%, #000 0%, transparent 82%);
  mask-image: radial-gradient(ellipse 95% 78% at 50% 38%, #000 0%, transparent 82%);
}

.js .hero__rules {
  animation: hero-rule-dim linear both;
  animation-timeline: --hero;
  animation-range: 42% 72%;
}

@keyframes hero-rule-dim {
  to {
    opacity: 0.35;
  }
}

@keyframes hero-fade-in {
  to {
    opacity: 1;
  }
}

/* ------------------------------------------------------------------ the recording */

/*
 * Stacked over the copy inside the frame rather than placed below it, so it arrives in the screen
 * the visitor is already looking at instead of somewhere below the fold they have yet to reach.
 */
.js .hero__reveal {
  position: absolute;
  /*
   * The frame's own `padding-top` does not reach this: an absolutely positioned box resolves
   * `inset` against its containing block's *padding box*, so `inset: 0` here spanned the header's
   * seventy-three pixels however much padding the frame carried. It has to clear the bar itself.
   */
  inset: var(--header-total) 0 0;
  display: grid;
  align-content: center;
  opacity: 0;
  /*
   * `visibility` as well as `opacity`, and this is not belt and braces.
   *
   * An element at `opacity: 0` with `pointer-events: none` is gone for the mouse but still in the
   * tab order and still in the accessibility tree — so the pause button and both of these buttons
   * were focusable and announced while completely invisible. `visibility: hidden` is the only
   * declaration that takes an element out of the tab order without taking its box away.
   *
   * Both are driven from the keyframes now (discrete properties, flipped at a keyframe rather than
   * interpolated), so they come back on their own when the reader scrolls up.
   */
  visibility: hidden;
  pointer-events: none;
}

.js .hero__reveal {
  animation: hero-reveal linear both;
  animation-timeline: --hero;
  animation-range: 42% 72%;
}

/*
 * THE PANEL OPENS ALONG THE RULING.
 *
 * This was sixteen pixels of lift and a fade — which is the scroll-reveal every page has, and on a
 * panel this size it reads as a card being slid onto the screen from a library of cards.
 *
 * It is a clip now: a band on the grid that opens outwards to the full frame. Six rows of the
 * hero's own `--cell-h` at each edge, so the shutter starts and finishes *on the ruling the panel
 * is arriving onto* rather than at an arbitrary fraction — the same idea as the ambient cells,
 * which snap their own coordinates to the pitch with `round()`. The recording is revealed by the
 * grid instead of drifting up through it, which is the thing the product actually does.
 *
 * `clip-path` rather than height or a wrapper with `overflow`: it is composited, it never touches
 * layout, and it takes the rounded corner with it via `round`, so the radius is correct at every
 * frame instead of appearing at the end.
 *
 * The scale is 1.015, which is almost nothing and is doing real work — it keeps the picture moving
 * while the shutter opens, so the panel reads as settling rather than as a mask being pulled off
 * something already parked. Never from `scale(0)`: nothing in the world appears from nothing.
 */
.js .hero__caption {
  animation: hero-fade-in linear both;
  animation-timeline: --hero;
  animation-range: 50% 76%;
}

/*
 * The last thing to arrive.
 *
 * Four points behind the caption rather than the four hundred milliseconds it used to be. A
 * stagger is there to give the eye an order to read things in, and past about a tenth of the
 * sequence it stops doing that and just makes the page feel slow.
 */
.js .hero__reveal-actions {
  animation: hero-settle linear both;
  animation-timeline: --hero;
  animation-range: 54% 80%;
}

@keyframes hero-reveal {
  0% {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }
  1% {
    visibility: visible;
  }
  100% {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
  }
}

@keyframes hero-settle {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/*
 * Editorial rather than demo-sized, and wider than it was.
 *
 * The first term used to be `81vw` against a 74rem cap, which at 1280 × 800 resolved to 755px —
 * a panel about three fifths of the window, centred in a full screen, which is the proportion of a
 * card rather than of a surface. It runs to the page gutter now, the same measure every other
 * section is set to, so the recording is as wide as the headline was.
 *
 * The third term is what stops it letterboxing. The recording is 16:10, so a width chosen on its
 * own leaves the panel taller or wider than the picture and the difference shows as dead ground
 * down one side. Sizing from the height that is actually free, times the aspect ratio, means the
 * picture fills its frame at every viewport.
 *
 * **"Free" now means free of the header as well**, which is the whole of the fix: the screen this
 * measured was `100svh`, the space the panel is centred in is `100svh` minus the bar, and the
 * difference came off the top of the recording. The 15rem is what sits under the panel — the two
 * 1.5rem gaps, the three-line caption and the buttons measure 11.2rem in both locales — plus
 * about 1.9rem of air above and below it. It was 18rem when it was also silently absorbing the
 * header; taking the header out of one term and the slack out of the other leaves the panel the
 * size it already was, in the place it should have been.
 */
.js .hero__reveal .hero__media {
  width: min(100%, 86rem, calc((100svh - var(--bar-total) - var(--header-total) - 15rem) * 1.6));
  margin-inline: auto;
  gap: 1.5rem;
  justify-items: center;
}

.js .hero__reveal video,
.js .hero__reveal img {
  width: 100%;
  height: auto;
}

/*
 * No shadow.
 *
 * It had one — a soft drop under the panel — and a drop shadow is what makes a rectangle read as
 * a card lying *on* the page rather than as part of it. `components.js` argues the same case for
 * screenshots and refuses to draw invented window chrome around them; the hero was doing exactly
 * what that note forbids, one element away. A hairline and the space around it are the framing,
 * which is what the comment above has always claimed.
 */
.js .hero__reveal .hero__caption {
  max-width: 34rem;
  margin-inline: auto;
  text-align: center;
  line-height: 1.5;
  opacity: 0;
}

/*
 * On a phone the recording is a full-bleed 4:5 crop rather than a 16:10 panel, which is taller
 * than it is wide — so the width calculation above has nothing to say about it and the stack ran
 * off the bottom of the pinned frame. Cap it by height instead, and tighten what sits underneath.
 */
@media (pointer: coarse) {
  .js .hero__reveal .hero__media {
    width: 100%;
    gap: 1.1rem;
  }

  /*
   * 42svh, down from 46. The frame is a bar shorter than it used to be and the caption and buttons
   * underneath still need their room; the recording is the part with slack in it.
   */
  .js .hero__reveal video,
  .js .hero__reveal img {
    max-height: 42svh;
    object-fit: cover;
  }

  .js .hero__reveal .hero__caption {
    font-size: 0.8rem;
    line-height: 1.65;
  }

  .js .hero__reveal-actions {
    width: 100%;
  }

  .js .hero__reveal-actions .btn {
    flex: 1 1 12rem;
    justify-content: center;
  }
}

/*
 * The buttons the sequence ends on.
 *
 * Without them it ends on a recording, a caption and nothing to click: the hero's own call to
 * action faded out in the first half second and never came back.
 */
.hero__reveal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  justify-content: center;
}

.js .hero__reveal-actions {
  opacity: 0;
}

/* ------------------------------------------------------------------ the end state */

/*
 * There is no end-state block any more, and its absence is the point.
 *
 * There used to be thirty lines of `.hero.is-done …  { animation: none; opacity: 1 }` pinning every
 * final value, because the sequence was one-shot and something had to hold the last frame after
 * the animations had finished — and had to be able to cancel them mid-flight when somebody skipped.
 *
 * `animation-fill-mode: both` against a scroll timeline does all of that for free. Past the end of
 * its range every animation holds its last frame; before the start of it, its first. Skipping is
 * just scrolling faster, and it lands on the same frame the slow way would. There is nothing to
 * pin and nothing to cancel.
 */

@media print {
  .hero__sheet,
  .hero__cells,
  .hero__inner {
    display: none;
  }

  .js .hero__runway {
    height: auto;
  }

  .js .hero__frame {
    position: static;
    height: auto;
  }

  /* `clip-path` as well now: printed mid-scroll, the panel would otherwise go to paper with six
     rows of the grid clipped off each end of it. */
  .js .hero__reveal,
  .js .hero__reveal {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    clip-path: none;
    animation: none !important;
    animation-timeline: none !important;
  }
}

/* ------------------------------------------------------------------ the header */

/*
 * The header used to turn with the ground.
 *
 * While the hero travelled from charcoal to bone, the header was the one element that stayed put
 * through the whole sequence and so the one that would have been left near-white on paper. It had
 * a whole `is-papered` state — the links, the icon button, the call to action and every shape in
 * the mark, each recoloured, plus an IntersectionObserver in `site.js` whose only job was to take
 * the class off again on the way back up.
 *
 * The ground does not change temperature any more. The header sits on `--canvas` from the top of
 * the page to the bottom of it, so all of that is gone: no state, no observer, no second set of
 * colours to keep in step with the first.
 */

/* ------------------------------------------------------------------ motion off */

/*
 * With motion off there is no sequence at all: the hero is a headline followed by a recording,
 * both present from the start, which is a perfectly good page and the one the static-image path
 * in `heroMedia()` was written for.
 */
@media (prefers-reduced-motion: reduce) {
  /*
   * `animation: none`, EXPLICITLY, AND THIS IS A TRAP WORTH NAMING.
   *
   * The global reduced-motion override at the top of this file collapses every animation with
   * `animation-duration: 0.001ms !important`, and for six years that was enough to stop anything
   * on this page moving. **A scroll-linked animation ignores `animation-duration` entirely** — its
   * progress comes from the timeline, not from a clock — so every one of these would have gone on
   * running, driven by the reader's scroll, under a setting that exists to say "do not do that".
   *
   * The `!important` is not decoration either: the rules being overridden are no more specific
   * than these, but they are not less, and this must win outright rather than by ordering.
   */
  .js .hero__inner,
  .js .hero__cells,
  .js .sheet__cell,
  .js .hero__rules,
  .js .hero__reveal,
  .js .hero__caption,
  .js .hero__reveal-actions {
    animation: none !important;
    animation-timeline: none !important;
  }

  .js .hero__sheet {
    display: none;
  }

  .js .hero__inner {
    visibility: visible;
    opacity: 1;
    transform: none;
  }

  .js .hero__rules {
    opacity: 1;
  }

  .js .hero__reveal {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    clip-path: none;
    pointer-events: auto;
  }

  .js .hero__reveal .hero__media,
  .js .hero__reveal video,
  .js .hero__reveal img {
    max-height: none;
  }

  .js .hero__reveal .hero__caption,
  .js .hero__reveal-actions {
    opacity: 1;
  }

  .js .hero__reveal .hero__caption {
    color: var(--ink-3);
    text-align: left;
    margin-inline: 0;
  }

  .js .hero__reveal-actions {
    justify-content: flex-start;
  }

  .js .hero__reveal-actions .btn--primary {
    --btn-bg: var(--accent);
    --btn-ink: var(--on-accent);
  }

  .js .hero__reveal-actions .btn--ghost {
    color: var(--ink);
    border-color: var(--line-strong);
  }
}

.hero__glow {
  position: absolute;
  left: 30%;
  top: 6%;
  width: min(1200px, 118%);
  aspect-ratio: 1.5;
  transform: translateX(-50%);
  background: radial-gradient(ellipse at center, var(--glow) 0%, transparent 70%);
}

.hero__inner {
  position: relative;
  display: grid;
  gap: clamp(2.5rem, 5vw, 4rem);
}

/* The headline and its paragraph keep a measure even though the page is now full-bleed. */
.hero__copy {
  max-width: 60rem;
}

.hero h1 {
  max-width: 14ch;
  margin-bottom: 0.4em;
}

/*
 * The clause the page turns on.
 *
 * It used to be mint. Colouring half a headline is emphasis by decoration, and on a page whose
 * argument is restraint it was the loudest thing on the screen. It stays plain: the size, the
 * line break and the full stop are already the emphasis.
 */
.hero h1 em {
  font-style: normal;
  color: inherit;
}

.hero .lede {
  max-width: 42ch;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  margin-top: 0.5rem;
}

.hero__points {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.6rem;
  margin: 1.75rem 0 0;
  padding: 0;
  font-size: 0.88rem;
  color: var(--ink-3);
}

.hero__points li {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

.tick {
  color: var(--success);
  flex: none;
}

/* ------------------------------------------------------------- product window */

/*
 * The frame the product sits in.
 *
 * Deliberately not a drawing of macOS or Windows chrome: the application has its own titlebar, and
 * inventing a fake one around a real screenshot is the exact thing that makes product shots look
 * untrustworthy. This is a neutral bezel — a hairline, a radius and a shadow — that reads as "a
 * window" without claiming to be a particular operating system's.
 */
.product-window {
  position: relative;
  width: 100%;
  max-width: 1040px;
  border-radius: clamp(10px, 1.4vw, 16px);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  box-shadow: var(--shadow-xl);
  overflow: hidden;
  isolation: isolate;
}

.product-window img,
.product-window video {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  object-position: top left;
  background: var(--surface-sunken);
}

/*
 * The hero media fills the column.
 *
 * `.product-window` caps itself at 1040px, which was right when the page was a centred 1180
 * column and wrong the moment it became full-bleed: the demo stopped two thirds of the way
 * across and read as neither edge-to-edge nor centred. In the hero it takes the whole measure,
 * which is also the only place on the page where a 1280-wide recording is shown near 1:1.
 */
.hero__media {
  width: 100%;
  display: grid;
  gap: 1.1rem;
}

/*
 * Not a caption any more.
 *
 * It was 0.84rem in `--ink-3` because it sat under a screenshot and explained it. The screenshot
 * is gone and this is what the sequence resolves to, so it is the sentence, not a footnote to one.
 */
.hero__caption {
  font-size: clamp(1.15rem, 2.2vw, 1.55rem);
  line-height: 1.5;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0;
  max-width: 44ch;
}

/*
 * The demo, on a phone — and the crop that used to be here is gone.
 *
 * It cropped the recording to a 4:5 portrait slice centred on where the drift dialog appeared,
 * which was the right answer to the film it was written for: a 1280-wide application window shown
 * 358 pixels across is a grey blur, and a crop at roughly twice the scale at least let one panel
 * of it be read.
 *
 * **The film is composed now, and the crop removes the composition.** Every shot in it is already
 * a close-up — framed in the edit on the one panel that shot is about — and every shot carries a
 * line of type across the bottom. A 4:5 centre crop threw away half of each frame and the whole of
 * every caption, which is the one part of the film that is legible on a phone at any size. So the
 * phone gets the frame as it was cut.
 *
 * See `scripts/demo-storyboard.mjs` for what the shots are and why.
 */
/*
 * The body screenshots, on a phone.
 *
 * A 1440-wide application window cannot be made legible in a 390-pixel column — to keep 14px app
 * text above ten pixels there you could show about five hundred pixels of the source, which is a
 * third of one panel and says nothing. So these are not pretending: they are cropped past the
 * navigation rail so the frame is filled with the part that carries the argument — issue cards,
 * recipe rows — rather than with an empty left rail, and the copy beside them states every count
 * they contain in words.
 */
@media (max-width: 760px) {
  .block__media .product-window img {
    aspect-ratio: 5 / 4;
    object-position: 24% top;
  }
}

@media (max-width: 760px) {
}

/* =========================================================================
   Proof blocks — the alternating spine of the page
   ========================================================================= */

/*
 * One layout, used by every block between the hero and the pricing.
 *
 * `minmax(0, …)` in both tracks: a `1fr` track has an automatic minimum equal to its content, so
 * a wide table or a mono value refuses to shrink and pushes the page sideways on a phone. This is
 * what lets the scrollers inside actually scroll.
 */
.block {
  display: grid;
  gap: clamp(2.25rem, 5vw, 4rem);
  align-items: center;
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 920px) {
  .block {
    grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
  }
  /*
   * Flipping the order alone would hand the evidence the *narrower* column, because the track
   * proportions stay where they are. The proportions are swapped with it, so the evidence is the
   * larger half either way round — which is the point of showing it.
   */
  .block--flip {
    grid-template-columns: minmax(0, 1.18fr) minmax(0, 0.82fr);
  }
  .block--flip .block__media {
    order: -1;
  }
}

.block__copy h2 {
  max-width: 20ch;
}

.block__copy .lede {
  max-width: 46ch;
}

.block__media {
  min-width: 0;
}

.block__list {
  list-style: none;
  margin: 1.4rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
}

.block__list li {
  display: flex;
  gap: 0.65rem;
  align-items: flex-start;
  font-size: 0.97rem;
  color: var(--ink-2);
}

.block__list .tick {
  margin-top: 0.24rem;
}

.block__note {
  margin: 1.4rem 0 0;
  font-size: 0.86rem;
  color: var(--ink-3);
  max-width: 44ch;
}

/* A pair of small term/detail points under a block's copy. */
.block__points {
  display: grid;
  gap: 1.15rem;
  margin-top: 1.5rem;
}

.block__point dt {
  font-weight: 650;
  margin-bottom: 0.15rem;
}

.block__point dd {
  margin: 0;
  color: var(--ink-2);
  font-size: 0.95rem;
}

/* =========================================================================
   Proof 2 — messy in, neat out
   ========================================================================= */

/*
 * The tidy animation.
 *
 * Four rows of real fixture values that disagree, snapping into one convention. It is a *diagram*
 * rather than a demo: no application chrome, no invented interface, and every before/after pair is
 * a value that genuinely appears in `marketing/fixtures` and a value the product genuinely writes.
 *
 * Built from two stacked spans per cell whose opacity crossfades on one shared keyframe timeline,
 * plus a translate that starts the "before" column ragged and lands it on the grid. The whole
 * cycle is 9s with a long hold at each end, because the point is legibility, not motion.
 */
.tidy {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
}

.tidy__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.8rem 1.1rem;
  border-bottom: 1px solid var(--line);
  background: var(--surface-sunken);
  font-size: 0.8rem;
  color: var(--ink-3);
}

.tidy__state {
  font-weight: 650;
  color: var(--ink-2);
}

.tidy__rows {
  display: grid;
}

.tidy__row {
  display: grid;
  grid-template-columns: minmax(6.5rem, 0.8fr) minmax(0, 1fr);
  gap: 1rem;
  align-items: center;
  padding: 1.05rem 1.2rem;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
}

.tidy__row:last-child {
  border-bottom: 0;
}

.tidy__label {
  font-size: 0.8rem;
  color: var(--ink-3);
  font-family: var(--font-mono);
  letter-spacing: -0.01em;
}

/* The cell holds both states in the same box, so nothing reflows as they swap. */
.tidy__cell {
  position: relative;
  display: grid;
  min-width: 0;
}

.tidy__val {
  grid-area: 1 / 1;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1rem;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tidy__val--before {
  color: var(--danger-ink);
}

.tidy__val--after {
  color: var(--success-ink);
  opacity: 0;
}

/*
 * The animation runs only when motion is allowed *and* the block has been seen, so a visitor
 * who never scrolls this far is not paying for an off-screen loop. `.is-in` comes from the same
 * reveal observer the rest of the page uses.
 */
.js .tidy.is-in .tidy__val--before {
  animation: tidy-out calc(9s / var(--motion)) var(--ease) infinite;
}

.js .tidy.is-in .tidy__val--after {
  animation: tidy-in calc(9s / var(--motion)) var(--ease) infinite;
}

/* The rows resolve one after another, which is what makes it read as settling rather than as a
   single crossfade. */
.js .tidy.is-in .tidy__row:nth-child(1) .tidy__val {
  animation-delay: 0s;
}
.js .tidy.is-in .tidy__row:nth-child(2) .tidy__val {
  animation-delay: 0.22s;
}
.js .tidy.is-in .tidy__row:nth-child(3) .tidy__val {
  animation-delay: 0.44s;
}
.js .tidy.is-in .tidy__row:nth-child(4) .tidy__val {
  animation-delay: 0.66s;
}

@keyframes tidy-out {
  0%,
  32% {
    opacity: 1;
    transform: translateX(0);
  }
  44%,
  88% {
    opacity: 0;
    transform: translateX(-6px);
  }
  96%,
  100% {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes tidy-in {
  0%,
  32% {
    opacity: 0;
    transform: translateX(6px);
  }
  44%,
  88% {
    opacity: 1;
    transform: translateX(0);
  }
  96%,
  100% {
    opacity: 0;
    transform: translateX(6px);
  }
}

/* The state label in the header swaps with the rows. */
.js .tidy.is-in .tidy__state span {
  animation: tidy-label calc(9s / var(--motion)) steps(1, end) infinite;
}

.js .tidy.is-in .tidy__state span:last-child {
  animation-name: tidy-label-alt;
}

.tidy__state span:last-child {
  display: none;
}

@keyframes tidy-label {
  0%,
  38% {
    display: inline;
  }
  39%,
  92% {
    display: none;
  }
  93%,
  100% {
    display: inline;
  }
}

@keyframes tidy-label-alt {
  0%,
  38% {
    display: none;
  }
  39%,
  92% {
    display: inline;
  }
  93%,
  100% {
    display: none;
  }
}

/* With motion off, both states are shown as a static before → after pair instead. */
@media (prefers-reduced-motion: reduce) {
  .tidy__row {
    grid-template-columns: minmax(6rem, 0.7fr) minmax(0, 1fr) minmax(0, 1fr);
  }
  .tidy__cell {
    display: block;
  }
  .tidy__val--after {
    opacity: 1;
  }
  .tidy__state span:last-child {
    display: inline;
  }
}

/* =========================================================================
   Proof 4 — it refuses to guess
   ========================================================================= */

/*
 * The ambiguity card.
 *
 * One value, two readings, and the product declining to pick. The wording is the detector's own:
 * `ambiguous-dates` in `packages/core/src/detect/issues.ts` says "A value like 01/02/2026 could be
 * 1 February or 2 January" and carries `needsDecision: true`, which is what puts the decision in
 * front of a person instead of in a log.
 */
.ambig {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: clamp(1.5rem, 4vw, 2.25rem);
  display: grid;
  gap: 1.25rem;
  justify-items: center;
  text-align: center;
}

.ambig__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: clamp(2rem, 6vw, 3.1rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  color: var(--ink);
  line-height: 1;
}

.ambig__forks {
  display: grid;
  gap: 0.6rem;
  width: 100%;
  max-width: 22rem;
}

.ambig__fork {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.6rem 0.9rem;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  font-size: 0.95rem;
  color: var(--ink-2);
}

/* The two readings take the accent in turn, so the eye sees the fork rather than a list. */
.js .ambig.is-in .ambig__fork {
  animation: ambig-pulse calc(4.4s / var(--motion)) var(--ease) infinite;
}

.js .ambig.is-in .ambig__fork:last-child {
  animation-delay: calc(2.2s / var(--motion));
}

@keyframes ambig-pulse {
  0%,
  44%,
  100% {
    border-color: var(--line-strong);
    color: var(--ink-2);
    background: var(--surface-sunken);
  }
  8%,
  36% {
    border-color: var(--accent-line);
    color: var(--ink);
    background: var(--accent-tint);
  }
}

.ambig__verdict {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--warning-line);
  border-radius: 999px;
  background: var(--warning-tint);
  color: var(--warning-ink);
  font-size: 0.86rem;
  font-weight: 650;
}

.ambig__caption {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink-3);
  max-width: 34ch;
}

/* =========================================================================
   Proof 5 — identifiers survive
   ========================================================================= */

/*
 * Two columns of the same values: what a spreadsheet does to them, and what the product writes.
 *
 * The values are the `Invoice No` column from `marketing/fixtures`, which exists so this claim has
 * a detector firing on a real file behind it — `identifier-risk` reports every one of these.
 */
.ident {
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

@media (min-width: 560px) {
  .ident {
    grid-template-columns: 1fr 1fr;
  }
}

.ident__side {
  background: var(--surface);
  padding: 1.25rem 1.35rem;
  min-width: 0;
}

.ident__side--good {
  background: color-mix(in srgb, var(--success-tint) 60%, var(--surface));
}

.ident__title {
  margin: 0 0 0.9rem;
  font-size: 0.78rem;
  font-weight: 650;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.ident__side--good .ident__title {
  color: var(--success-ink);
}

.ident__rows {
  display: grid;
  gap: 0.45rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ident__rows li {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 0.95rem;
  letter-spacing: -0.01em;
  color: var(--danger-ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ident__side--good .ident__rows li {
  color: var(--ink);
}

.ident__foot {
  margin: 1rem 0 0;
  font-size: 0.8rem;
  color: var(--ink-3);
}

/*
 * The real warning, across the full width of the section.
 *
 * A crop of the product's own panel rather than a window, because the comparison above says what
 * the damage is and this says that the software actually looks for it. It sits outside the two
 * columns and spans the whole wrap: at 620px the card's own body text renders about seven pixels
 * tall and reads as a grey smudge, which is decoration pretending to be evidence. Full width it
 * is legible, which is the only reason to show it at all.
 */
.crop-shot {
  margin-top: clamp(2rem, 5vw, 3rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

.crop-shot figcaption {
  padding: 0.7rem 1.1rem;
  border-top: 1px solid var(--line);
  font-size: 0.8rem;
  color: var(--ink-3);
  background: var(--surface-sunken);
}

/*
 * On a phone this strip is a smudge, and it goes.
 *
 * It is an 8.5:1 crop whose entire content is small type. At 390px the card's body text renders
 * at about four pixels — it stops being evidence and becomes a grey texture that implies
 * evidence, which is worse than showing nothing. The before/after values above it are set in
 * real type at a real size and carry the same argument on their own.
 */
@media (max-width: 760px) {
  .crop-shot {
    display: none;
  }
}

.crop-shot img {
  width: 100%;
  height: auto;
  display: block;
}

/* =========================================================================
   Proof 6 — the numbers
   ========================================================================= */

/*
 * The one place the page cites figures.
 *
 * Every number here came out of the product's own performance suite on a stated machine, and the
 * footnote says which. This section exists *instead of* testimonials: there are no customers to
 * quote yet, and a measured figure with its conditions attached is worth more than an invented
 * quotation anyway.
 */
.metrics {
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

@media (min-width: 620px) {
  .metrics {
    grid-template-columns: 1fr 1fr;
  }

  /*
   * One figure does not go in a two-column grid. A lone child leaves the second track empty, and
   * because the gap colour is the border colour, the empty half renders as a filled panel beside
   * the number rather than as nothing. Single column, and the figure takes the size it earns.
   */
  .metrics--solo {
    grid-template-columns: 1fr;
  }
}

.metrics--solo .metric {
  padding: clamp(1.8rem, 4vw, 2.6rem);
}

.metrics--solo .metric strong {
  font-size: clamp(2.6rem, 7vw, 4.2rem);
}

.metrics--solo .metric span {
  margin-top: 0.5rem;
  font-size: 0.95rem;
}

.metric {
  background: var(--surface);
  padding: 1.4rem 1.5rem;
}

.metric strong {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 4vw, 2.4rem);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.metric span {
  display: block;
  margin-top: 0.3rem;
  font-size: 0.89rem;
  color: var(--ink-2);
}

.footnote {
  font-size: 0.83rem;
  color: var(--ink-3);
  margin: 1.2rem 0 0;
}

/* =========================================================================
   Recipes
   ========================================================================= */

.recipe-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 1.4rem 0 0;
  padding: 0;
  list-style: none;
}

.recipe-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.45rem 0.85rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  font-size: 0.88rem;
  font-weight: 550;
  color: var(--ink-2);
}

.recipe-chip svg {
  color: var(--accent);
}

.callout {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 1.35rem 1.45rem;
}

.callout h3 {
  margin-bottom: 0.4rem;
}

.callout p:last-child {
  margin-bottom: 0;
}

/*
 * The drift callout carries the page's most important paragraph, so it is set apart by a rule and
 * by space rather than by a colour wash. A tinted panel says "aside"; this one is the point.
 */
.callout--info {
  border: 0;
  border-left: 1px solid var(--line-strong);
  border-radius: 0;
  background: none;
  padding: 0.2rem 0 0.2rem 1.6rem;
}

.callout--warn {
  border-color: var(--warning-line);
  background: var(--warning-tint);
}

.callout--warn h3,
.callout--warn p {
  color: var(--warning-ink);
}

.callout--good {
  border-color: var(--success-line);
  background: var(--success-tint);
}

/* =========================================================================
   Trust strip
   ========================================================================= */

/*
 * Where the data goes, in three lines rather than a section.
 *
 * "Nothing is uploaded" is the first objection every buyer of local file software has, so it
 * cannot be left to the FAQ — but it does not need a whole screen either. The full argument is a
 * click away on the privacy page.
 */
.trust {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  padding: clamp(1.5rem, 4vw, 2.25rem) 0 0;
  border-top: 1px solid var(--line);
  margin-top: clamp(2.5rem, 6vw, 4rem);
}

.trust__item dt {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  font-weight: 650;
  font-size: 0.97rem;
  margin-bottom: 0.3rem;
}

.trust__item dt .tick {
  margin-top: 0.24rem;
}

.trust__item dd {
  /* Aligned under the term's text, not under its tick. */
  margin: 0 0 0 1.7rem;
  color: var(--ink-2);
  font-size: 0.9rem;
}

/* =========================================================================
   Data tables and badges
   ========================================================================= */

.ba-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.83rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.data-table th {
  text-align: left;
  font-weight: 650;
  color: var(--ink-3);
  padding: 0.45rem 1rem;
  border-bottom: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 0.75rem;
}

.data-table td {
  padding: 0.42rem 1rem;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
  color: var(--ink);
  font-family: var(--font-mono);
}

.data-table tr:last-child td {
  border-bottom: 0;
}

.data-table .is-odd {
  color: var(--danger-ink);
}

.data-table .is-dupe td {
  background: var(--danger-tint);
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--danger-ink) 60%, transparent);
  color: var(--ink-3);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.73rem;
  font-weight: 650;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-2);
}

.badge--good {
  border-color: var(--success-line);
  background: var(--success-tint);
  color: var(--success-ink);
}

.badge--warn {
  border-color: var(--warning-line);
  background: var(--warning-tint);
  color: var(--warning-ink);
}

/* =========================================================================
   Cards (used by the pricing page and the download page)
   ========================================================================= */

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(265px, 1fr));
  gap: 1rem;
}

.card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 1.45rem;
}

.card h3 {
  font-size: 1.03rem;
  margin-bottom: 0.4rem;
}

.card p {
  margin: 0;
  color: var(--ink-2);
  font-size: 0.94rem;
}

/* =========================================================================
   Comparison table (pricing / FAQ pages)
   ========================================================================= */

.compare-scroll {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  -webkit-overflow-scrolling: touch;
}

.compare {
  width: 100%;
  min-width: 640px;
  border-collapse: collapse;
  font-size: 0.94rem;
}

.compare th,
.compare td {
  padding: 1rem 1.15rem;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--line);
}

.compare thead th {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-3);
  background: var(--surface-sunken);
  font-weight: 700;
}

.compare tbody tr:last-child th,
.compare tbody tr:last-child td {
  border-bottom: 0;
}

.compare th[scope='row'] {
  font-weight: 650;
  width: 22%;
}

.compare td:last-child {
  background: color-mix(in srgb, var(--accent-tint) 65%, transparent);
  color: var(--ink);
}

.compare td:nth-child(2) {
  color: var(--ink-2);
}

/* =========================================================================
   Pricing
   ========================================================================= */

/*
 * THE THREE CARDS SHARE ONE SET OF ROWS.
 *
 * Equal-height cards and `margin-top: auto` on the form put every *button* on the same line, and
 * for a while that was taken as the alignment being solved. It was not. Everything between the
 * card's top edge and that button still landed wherever its own text left it: Yearly's tagline
 * wraps to two lines and Lifetime carries a "Founder price" row under the price, so the three
 * feature lists began at three different heights — about 733, 757 and 783 on a 1280 page. On a
 * page whose entire argument is *aligned things*, three ragged list tops is the detail that gives
 * it away.
 *
 * So the cards are a subgrid. The rows — name, tagline, price, the optional promotion, features,
 * form, VAT note — are declared once here and every card maps onto them, which makes each row as
 * tall as the tallest card needs and puts the equivalent element of all three on the same line.
 * The features row is the `1fr`, so it absorbs the slack and the button still sits at the bottom.
 *
 * The alternative was reserving heights in CSS — `min-height: 2lh` on the tagline and so on — and
 * it is wrong for this page specifically: `/da/priser` sets the same cards in Danish, where every
 * label is a word or two longer and the wrap points are different. A reserved height is a guess
 * about text; a subgrid is a measurement of it.
 *
 * Where subgrid is unsupported the `grid-template-rows` declaration is dropped, the cards fall
 * back to ordinary grid items, and `margin-top: auto` on the form keeps the buttons aligned —
 * exactly the behaviour this replaced.
 */
.plans {
  display: grid;
  gap: 1.1rem;
  grid-template-columns: repeat(auto-fit, minmax(275px, 1fr));
  /* name · tagline · price · features · form · VAT note. */
  grid-template-rows: auto auto auto 1fr auto auto;
  align-items: stretch;
}

/*
 * The promotion row exists only while a promotion is running.
 *
 * `planCard` emits it on every card or on none, so this branches once for the whole section: with
 * an offer on, a fourth track is opened between the price and the features and the cards span
 * seven rows instead of six. Reserving the track unconditionally would leave a row gap under
 * every price for as long as there is no offer — 0.9rem of space held open for nothing.
 */
.plans:has(.plan__promo) {
  grid-template-rows: auto auto auto auto 1fr auto auto;
}

.plans:has(.plan__promo) .plan {
  grid-row: span 7;
}

.plan {
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 1.9rem 1.7rem;
  display: grid;
  grid-row: span 6;
  grid-template-rows: subgrid;
  /*
   * The card's own row gap, not the 1.1rem gutter between cards. A subgrid inherits the parent's
   * gaps unless it sets its own, and inheriting here would space the inside of a card as widely
   * as the cards are spaced from each other.
   */
  row-gap: 0.9rem;
  min-width: 0;
}

/*
 * The recommended plan is marked, not shouted.
 *
 * It used to be a mint-tinted card with a mint outline, a mint flag and a mint button, next to two
 * more mint buttons — four uses of the accent in one row, which is how a price table starts
 * looking like a pitch. Now the card is the same surface as its neighbours and is distinguished by
 * a single hairline in the accent. The only filled thing in the section is its own button.
 */
.plan--recommended {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}

.plan__flag {
  position: absolute;
  top: -0.6rem;
  left: 1.55rem;
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.15rem 0.6rem;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--line));
  border-radius: 999px;
  background: var(--canvas);
  color: var(--accent-ink);
}

.plan__name {
  font-family: var(--font-display);
  font-size: 1.16rem;
  font-weight: 450;
  letter-spacing: -0.02em;
  margin: 0;
}

.plan__tagline {
  margin: 0;
  color: var(--ink-2);
  font-size: 0.92rem;
}

/*
 * `margin: 0` here and on the promotion, because these are `<p>` and the document gives every
 * paragraph a 1.1em bottom margin. Inside the subgrid that margin is a second spacing system
 * fighting the row gap — it made the space under the price about twice the space above it, and
 * it would make an *empty* promotion row a visible one.
 */
.plan__price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
}

.plan__amount {
  font-family: var(--font-display);
  font-size: clamp(2.1rem, 4vw, 2.7rem);
  font-weight: 350;
  letter-spacing: -0.035em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.plan__period {
  color: var(--ink-2);
  font-size: 0.92rem;
}

.plan__was {
  color: var(--ink-3);
  font-size: 0.9rem;
  text-decoration: line-through;
}

.plan__promo {
  font-size: 0.8rem;
  font-weight: 650;
  color: var(--success-ink);
  margin: 0;
}

/*
 * `align-content: start`, because this list is now the card's flexible row.
 *
 * The features row is the `1fr` in the subgrid, so the list is stretched to whatever height the
 * tallest card leaves — and a grid whose rows are `auto` answers that stretch by sharing the slack
 * out between them. The effect was a second misalignment in place of the one the subgrid fixed:
 * Lifetime's four features sat 31px apart and Yearly's three sat 42px apart, so no two cards had
 * their third line on the same baseline.
 *
 * Packing to the start keeps one gap for every card and drops the slack where it belongs, in the
 * space above the button.
 */
.plan__features {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  align-content: start;
  gap: 0.45rem;
  font-size: 0.93rem;
  color: var(--ink-2);
}

.plan__features li {
  display: flex;
  gap: 0.55rem;
  align-items: flex-start;
}

.plan__features .tick {
  margin-top: 0.2rem;
}

.plan form {
  margin-top: auto;
}

/*
 * The closed-sales cell: the note and the button, occupying the one track the form occupies when
 * sales are open. `margin-top: auto` for the same reason `.plan form` has it — the cell sits at
 * the bottom of the card so the buttons line up across three cards whose feature lists differ in
 * length.
 */
.plan__closed-cta {
  margin-top: auto;
  display: grid;
  gap: 0.75rem;
}

.plan__closed {
  margin: 0;
  color: var(--ink-2);
}

.plan__vat {
  font-size: 0.79rem;
  color: var(--ink-3);
  margin: 0;
}

.pricing-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem;
  margin-bottom: 1.6rem;
}

.pricing-bar label {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink-2);
  font-weight: 550;
}

.pricing-bar select {
  width: auto;
  padding-block: 0.45rem;
  /* 16px for the same reason the base field is: under it, iOS zooms on focus. */
  font-size: 1rem;
}

.guarantees {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  list-style: none;
  padding: 0;
  margin: 2rem 0 0;
  font-size: 0.9rem;
  color: var(--ink-2);
}

.guarantees li {
  display: inline-flex;
  gap: 0.4rem;
  align-items: center;
}

.mock-banner {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  border: 1px solid var(--warning-line);
  background: var(--warning-tint);
  color: var(--warning-ink);
  border-radius: var(--radius);
  padding: 0.8rem 1rem;
  font-size: 0.88rem;
  margin-bottom: 1.4rem;
}

.mock-banner strong {
  flex: none;
  /* Two words, one line. Wrapping "TEST MODE" turns the clearest label on the page into a ragged
     two-line block that reads as a mistake. */
  white-space: nowrap;
  font-weight: 800;
  letter-spacing: 0.04em;
}

/* =========================================================================
   FAQ
   ========================================================================= */

.faq {
  display: grid;
  gap: 0.5rem;
  max-width: 800px;
  margin-inline: auto;
}

.faq details {
  border-bottom: 1px solid var(--line);
}

/* A hairline list rather than a stack of cards: six boxes in a row is the thing that makes a page
   feel like a form. */
.faq details:first-child {
  border-top: 1px solid var(--line);
}

.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem 0.25rem;
  cursor: pointer;
  font-weight: 620;
  font-size: 1.02rem;
  letter-spacing: -0.014em;
  list-style: none;
}

.faq summary::-webkit-details-marker {
  display: none;
}

.faq summary::after {
  content: '';
  flex: none;
  width: 10px;
  height: 10px;
  border-right: 2px solid var(--ink-3);
  border-bottom: 2px solid var(--ink-3);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform calc(0.18s * var(--motion)) var(--ease);
}

.faq details[open] summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}

.faq summary:hover {
  color: var(--accent-ink);
}

.faq__answer {
  padding: 0 0.25rem 1.25rem;
  color: var(--ink-2);
  font-size: 0.97rem;
  margin: 0;
  max-width: 68ch;
}

/* =========================================================================
   Final CTA
   ========================================================================= */

/*
 * The closing call to action is a ruled block, not a coloured card.
 *
 * A mint-gradient box at the foot of the page was the last piece of "template" left: the argument
 * is already made by then, and a panel that lights up to ask for the click undoes the calm the
 * rest of the page spent nine sections building. A rule, a large light headline and the button.
 */
.final-cta {
  position: relative;
  border-top: 1px solid var(--line);
  padding: clamp(3.5rem, 9vw, 6rem) 0 0;
}

.final-cta h2 {
  max-width: 16ch;
}

.final-cta .lede {
  max-width: 48ch;
}

.final-cta .hero__actions {
  margin-top: 1.9rem;
}

/* =========================================================================
   Footer
   ========================================================================= */

.site-footer {
  border-top: 1px solid var(--line);
  background: var(--canvas-tint);
  padding-block: clamp(2.75rem, 5vw, 4rem) 1.5rem;
  /* The final-call-to-action section already carries a full section's padding below its card, so
     a second helping here read as a hole rather than as air. */
  margin-top: clamp(1rem, 2vw, 2rem);
}

.site-footer__inner {
  display: grid;
  gap: 2.25rem;
}

@media (min-width: 800px) {
  .site-footer__inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
    align-items: start;
  }
}

.site-footer__tagline {
  color: var(--ink-2);
  font-size: 0.92rem;
  margin: 1rem 0 0;
  max-width: 32ch;
}

.site-footer__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 1.5rem;
}

.site-footer__col h2 {
  font-family: var(--font-sans);
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 0.7rem;
}

.site-footer__col a {
  display: block;
  color: var(--ink-2);
  text-decoration: none;
  font-size: 0.92rem;
  padding: 0.18rem 0;
}

.site-footer__col a:hover {
  color: var(--ink);
  text-decoration: underline;
}

.site-footer__base {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
  margin-top: 2.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}

.footer__entity,
.footer__rights {
  margin: 0;
  font-size: 0.81rem;
  color: var(--ink-3);
}

.footer__entity--missing {
  color: var(--warning-ink);
}

.locale-switch {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.81rem;
  color: var(--ink-3);
}

.locale-switch__link {
  color: var(--ink-2);
}

/* =========================================================================
   Download page
   ========================================================================= */

.page-head {
  position: relative;
  padding-block: clamp(3rem, 7vw, 4.5rem) clamp(1.5rem, 3vw, 2rem);
}

/* The section directly under a page head, for the same reason as `--after-hero`. */
.section--after-head {
  padding-top: clamp(0.5rem, 1.5vw, 1.25rem);
}

.downloads {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}

/*
 * One platform is the normal case, and `auto-fit` is wrong for it.
 *
 * `repeat(auto-fit, minmax(260px, 1fr))` collapses to a single column when there is one card, and
 * a single column of a 1560px wrap is a 1560px card: four short spec rows stretched until the
 * label and its value are a hand's width apart, a button the length of the screen, and a checksum
 * with room to spare. It read as a page with something missing from it.
 *
 * A card is a card. It stops at a readable measure and sits in the middle, and the moment there
 * are two platforms the rule stops applying and the grid behaves as before.
 */
.downloads:has(.download:only-child) {
  max-width: 34rem;
  /* Left, not centred: the heading and the lede above it are left-aligned, and a centred card
     under left-aligned copy reads as a layout that has come apart rather than as composition. */
  margin-inline: 0 auto;
}

.download {
  border: 1px solid var(--line);
  border-radius: var(--radius-xl);
  background: var(--surface);
  padding: 1.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.download--primary {
  border-color: var(--accent-line);
  background: color-mix(in srgb, var(--accent-tint) 40%, var(--surface));
}

.download__icon {
  color: var(--ink-2);
}

.download h2,
.download h3 {
  margin: 0;
  font-size: 1.15rem;
}

.download__detail {
  margin: 0;
  color: var(--ink-2);
  font-size: 0.92rem;
}

.download__meta {
  list-style: none;
  margin: 0.3rem 0 0.6rem;
  padding: 0;
  display: grid;
  gap: 0.25rem;
  font-size: 0.84rem;
  color: var(--ink-3);
}

.download__meta li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
}

.download__meta code {
  font-family: var(--font-mono);
  font-size: 0.76rem;
  word-break: break-all;
  text-align: right;
  color: var(--ink-2);
}

.download .btn {
  margin-top: auto;
}

.download__note {
  margin: 0;
  font-size: 0.83rem;
  color: var(--ink-3);
}

.download__others {
  font-size: 1.05rem;
  margin: 0 0 0.8rem;
  color: var(--ink-2);
}

.checksum {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  margin-top: 0.6rem;
}

/*
 * A checksum is metadata, not an input.
 *
 * A filled, bordered, rounded box the width of the card is the shape of a text field, so it read
 * as something to type into — on a page whose only real action is the button directly above it.
 * It is a value the page is *telling* you, and one almost nobody checks, so it looks like the
 * footnote it is: quiet mono on the card's own ground, no border, no fill.
 */
.checksum code {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.01em;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* =========================================================================
   Checkout, success, forms
   ========================================================================= */

.panel {
  border: 1px solid var(--line);
  border-radius: var(--radius-xl);
  background: var(--surface);
  padding: clamp(1.5rem, 4vw, 2.25rem);
}

.panel--narrow {
  max-width: 560px;
  margin-inline: auto;
}

.form-row {
  margin-bottom: 1rem;
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: center;
  margin-top: 1.2rem;
}

.key-block {
  font-family: var(--font-mono);
  font-size: 0.86rem;
  line-height: 1.5;
  word-break: break-all;
  padding: 1rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  user-select: all;
}

.steps {
  counter-reset: step;
  list-style: none;
  margin: 1.6rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.9rem;
}

.steps li {
  counter-increment: step;
  display: grid;
  grid-template-columns: 28px 1fr;
  gap: 0.8rem;
  align-items: start;
}

.steps li::before {
  content: counter(step);
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  background: var(--accent-tint);
  color: var(--accent-ink);
  border: 1px solid var(--accent-line);
  font-size: 0.82rem;
  font-weight: 700;
}

.steps strong {
  display: block;
}

.steps span {
  color: var(--ink-2);
  font-size: 0.93rem;
}

.order-summary {
  display: grid;
  gap: 0.35rem;
  margin-top: 1.4rem;
  padding-top: 1.2rem;
  border-top: 1px solid var(--line);
  font-size: 0.9rem;
}

.order-summary div {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
}

.order-summary dt {
  color: var(--ink-3);
}

.order-summary dd {
  margin: 0;
  font-weight: 600;
}

.status-note {
  font-size: 0.88rem;
  color: var(--ink-2);
  margin: 1rem 0 0;
}

.status-note--warn {
  color: var(--warning-ink);
}

/* =========================================================================
   Prose (legal pages)
   ========================================================================= */

.prose {
  max-width: 70ch;
}

.prose h2 {
  font-size: 1.4rem;
  margin-top: 2.2rem;
}

.prose h3 {
  font-size: 1.08rem;
  margin-top: 1.6rem;
}

.prose ul,
.prose ol {
  padding-left: 1.3rem;
  color: var(--ink-2);
}

.prose li {
  margin-bottom: 0.35rem;
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.92rem;
  margin: 1.2rem 0;
}

.prose th,
.prose td {
  text-align: left;
  padding: 0.6rem 0.8rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

.prose code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--surface-sunken);
  padding: 0.1em 0.35em;
  border-radius: 4px;
}

/* =========================================================================
   Mock checkout (test mode only)
   ========================================================================= */

.mock-page {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 2rem 1rem;
  background: var(--canvas-tint);
}

.mock-card {
  width: 100%;
  max-width: 430px;
  border: 1px solid var(--line);
  border-radius: var(--radius-xl);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  padding: 1.75rem;
}

.mock-card h1 {
  font-size: 1.35rem;
}

.mock-line {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.92rem;
}

.mock-actions {
  display: grid;
  gap: 0.55rem;
  margin-top: 1.4rem;
}

/* =========================================================================
   Toast
   ========================================================================= */

/*
 * Above the bar, not behind it.
 *
 * This is the confirmation that a licence key was copied — the one moment on the site where a
 * customer needs to be told something worked. Left at a flat `1.5rem` it appears underneath the
 * bottom bar on every phone, which is the same as not appearing. `z-index: 60` keeps it over the
 * bar's 50 as well; both halves are needed, because clearing it vertically is no use if the bar
 * paints on top.
 */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(1.5rem + var(--bar-total));
  transform: translateX(-50%);
  z-index: 60;
  background: var(--ink);
  color: var(--canvas);
  padding: 0.6rem 1.1rem;
  border-radius: 999px;
  font-size: 0.88rem;
  font-weight: 550;
  box-shadow: var(--shadow-lg);
}

/* =========================================================================
   Error pages
   ========================================================================= */

/* The order page opens with a panel rather than a heading, so it needs less air above it. */
.order-page main > .section {
  padding-top: clamp(1.5rem, 3vw, 2.5rem);
}

.error-page {
  min-height: 58vh;
  display: grid;
  place-items: center;
  text-align: center;
  padding-block: 4rem;
}

.error-page h1 {
  max-width: 16ch;
  margin-inline: auto;
}

/* ===========================================================================
   THE RECIPE, RUNNING.

   A clean file becomes column rules; a messy export is matched against them
   and repaired. Behaviour is in `site.js` — this file only describes rest
   states and the two colours that carry meaning.

   Salmon is a value being repaired. Amber is one the recipe would not resolve
   on its own. Nothing else in here is allowed to be coloured, because if every
   cell is interesting then the one that matters is not.
   =========================================================================== */

.recipe-run {
  display: grid;
  /* minmax(0, …) rather than the implicit `auto`: the demo card below carries a fixed track list,
     and an auto track sizes to it, which pushed the card straight past the page gutters. */
  grid-template-columns: minmax(0, 1fr);
  gap: 2rem;
  align-items: center;
}

@media (min-width: 60rem) {
  .recipe-run {
    grid-template-columns: minmax(0, 0.82fr) minmax(0, 1fr);
    gap: 3.5rem;
  }
}

.recipe-run__copy h3 {
  margin: 0.35rem 0 0.75rem;
}

.recipe-run__copy p {
  color: var(--ink-2);
  margin: 0 0 0.75rem;
}

/*
 * The quiet aside under a block of copy — the privacy note here, the "read and dropped" note on
 * the learn-from-a-good-file callout. It was `.learn__note` until that diagram came off the page;
 * the rule outlived the block it was named after, so it is named for what it does instead.
 */
.recipe-run__note {
  color: var(--ink-3);
  font-size: 0.92rem;
  border-left: 2px solid var(--line-strong);
  padding-left: 0.7rem;
}

/*
 * A DARK ISLAND ON A LIGHT PAGE.
 *
 * The application is dark. These panels are depictions of it — a moving screenshot — so they keep
 * its ground while the page around them does not. A light mock-up of a dark program would be the
 * page telling a small lie about what you are about to install.
 *
 * It is a token redeclaration and nothing else, which is the whole argument for having had tokens.
 * Every rule inside this panel already reads its colours from these names, so restating them on
 * the container flips several hundred lines of descendants without one `.on-dark .thing` override
 * existing anywhere. `color-scheme` travels with them, so a scrollbar or a form control inside a
 * panel is painted dark rather than in the page's light chrome.
 *
 * These are the values the whole page carried before it went light; the file header explains why
 * it no longer does.
 */
.on-dark,
.nt,
.ambig,
.ident,
.tidy {
  --canvas: hsl(180 4% 5.3%);
  --canvas-tint: hsl(200 5% 7.2%);
  --rule: hsl(200 5% 14.5%);
  --surface: hsl(200 6% 9.2%);
  --surface-sunken: hsl(200 6% 7.2%);
  --surface-hover: hsl(200 5% 14%);
  --ink: hsl(205 12% 96%);
  --ink-2: hsl(205 8% 74%);
  --ink-3: hsl(205 7% 61%);
  --line: hsl(205 6% 14%);
  --line-strong: hsl(205 6% 25%);
  --accent: hsl(0 0% 95%);
  --accent-hover: hsl(0 0% 100%);
  --accent-ink: hsl(0 0% 90%);
  --accent-tint: hsl(205 6% 11.5%);
  --accent-line: hsl(205 5% 27%);
  --on-accent: hsl(205 12% 6%);
  --success: hsl(0 0% 78%);
  --success-ink: hsl(0 0% 88%);
  --success-tint: hsl(205 6% 10.5%);
  --success-line: hsl(205 5% 23%);
  --warning-tint: hsl(38 14% 11%);
  --warning-line: hsl(38 12% 24%);
  --warning-ink: hsl(40 34% 68%);
  --danger-ink: hsl(8 38% 63%);
  --danger-tint: hsl(8 16% 11%);
  --danger-line: hsl(8 14% 24%);
  --shadow-sm: 0 1px 2px hsl(0 0% 0% / 0.4);
  --shadow-md: 0 2px 4px hsl(0 0% 0% / 0.4), 0 8px 20px -6px hsl(0 0% 0% / 0.5);
  --shadow-lg: 0 2px 6px hsl(0 0% 0% / 0.45), 0 20px 44px -12px hsl(0 0% 0% / 0.6);

  color-scheme: dark;
  color: var(--ink);
}

.nt {
  /* Back to the value measured on the dark ground, because that is the ground again in here. */
  --nt-broken: hsl(6 38% 66%);
  --nt-flagged: var(--warning-ink);
  --nt-scan: var(--ink);
  --nt-sweep: var(--surface-hover);

  width: 100%;
  box-sizing: border-box;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px 24px 18px;
}

.nt__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}

.nt__stage {
  font-size: 11px;
  letter-spacing: 0.14em;
  font-weight: 500;
  color: var(--ink-2);
}

.nt__file {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 11px;
  color: var(--ink-3);
}

.nt__rail {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  min-height: 26px;
  margin-bottom: 16px;
}

.nt__chip {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 10.5px;
  color: var(--accent-ink);
  background: var(--accent-tint);
  border: 1px solid var(--accent-line);
  border-radius: 999px;
  padding: 4px 10px;
  opacity: 0;
  transform: translateY(7px) scale(0.96);
  transition:
    opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

.nt__chip.is-on {
  opacity: 1;
  transform: none;
}

.nt__stack {
  position: relative;
}

.nt__scan {
  position: absolute;
  left: -12px;
  right: -12px;
  top: 0;
  height: 1px;
  background: var(--nt-scan);
  opacity: 0;
  transition:
    top 0.45s cubic-bezier(0.4, 0, 0.2, 1),
    opacity 0.3s ease;
  pointer-events: none;
}

.nt__row {
  display: grid;
  grid-template-columns: var(--nt-cols);
  opacity: 0;
  transform: translateY(5px);
  transition:
    opacity 0.45s cubic-bezier(0.16, 1, 0.3, 1),
    transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

.nt__row.is-on {
  opacity: 1;
  transform: none;
}

.nt__row.is-ghost {
  opacity: 0.16;
}

.nt__head {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--ink-3);
  padding: 0 10px;
  height: 26px;
  display: flex;
  align-items: center;
  transition:
    color 0.3s ease,
    background 0.45s ease;
}

.nt__cell {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 13px;
  color: var(--ink);
  padding: 0 10px;
  height: 34px;
  display: flex;
  align-items: center;
  white-space: nowrap;
  transition:
    color 0.3s ease,
    background 0.45s ease;
}

.nt__cell.is-broken {
  color: var(--nt-broken);
}

.nt__cell.is-flagged {
  color: var(--nt-flagged);
}

.nt__cell.is-active,
.nt__head.is-active {
  background: var(--nt-sweep);
}

/*
 * The question, printed.
 *
 * Without this the amber cell reads as a failure. With it, the panel ends on the sentence the
 * product is built around: it knew the rule and still would not guess.
 */
.nt__ask {
  margin: 14px 0 0;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--warning-ink);
  animation: nt-ask-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.nt__ask b {
  font-weight: 400;
  color: var(--ink);
}

@keyframes nt-ask-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .nt__ask {
    animation: none;
  }
}

.nt__meter {
  display: flex;
  flex-wrap: wrap;
  gap: 22px;
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 11px;
  color: var(--ink-3);
}

.nt__meter b {
  font-weight: 400;
  color: var(--ink);
}

/*
 * Phones. Above this width the declared track list fits inside the card with room to spare; below
 * it, 340px of fixed columns does not, and the last column is the one that matters -- it is where
 * `Denmark` becomes `DK`. Fractions of whatever width the card actually has, weighted by what each
 * column holds: the date is the longest value, the country code the shortest.
 *
 * min-width: 0 lets the tracks hold their share against nowrap cells, and overflow: hidden keeps
 * anything that still does not fit inside the card rather than past the edge of the page.
 */
@media (max-width: 34rem) {
  .nt {
    padding: 18px 16px 14px;
  }

  /* Measured, not guessed: the widest thing each column holds across the whole loop, at the sizes
     set below, in the ratio 1 : 1.46 : 1.31 : 0.89. The header is the long one in three of the
     four columns -- `order_date` and `Total (DKK)` are wider than any value under them. */
  .nt__row {
    grid-template-columns:
      minmax(0, 1fr) minmax(0, 1.46fr)
      minmax(0, 1.31fr) minmax(0, 0.89fr);
  }

  .nt__head,
  .nt__cell {
    min-width: 0;
    overflow: hidden;
    padding: 0 4px;
  }

  .nt__head {
    font-size: 9px;
    letter-spacing: 0.04em;
  }

  .nt__cell {
    font-size: 11px;
  }

  .nt__meter {
    gap: 14px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .nt__row,
  .nt__chip,
  .nt__scan {
    transition: none;
  }
}
