/* ===================================================================
   SELF-HOSTED WEBFONTS - GENERATED, DO NOT EDIT BY HAND

   Written by tools/fonts.mjs. Re-run it to change a weight or a subset;
   editing this file by hand means the next run silently reverts you.

   build.mjs prepends this to styles.css so the page makes one request
   for CSS, and the faces live under /assets, which carries a year of
   immutable caching. Renaming a weight therefore means a new filename,
   which is exactly what the generated names do.

   Latin and latin-ext only. Both languages on this site are Latin, and
   the other ten subsets Google splits these faces into would be files
   nobody ever fetches.

   Bricolage Grotesque, Hanken Grotesk and JetBrains Mono are all under
   the SIL Open Font License - see public/assets/fonts/LICENSE.
   =================================================================== */

@font-face {
  font-family: "Bricolage Grotesque";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/bricolage-grotesque-latin-ext-9903537b.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Bricolage Grotesque";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/assets/fonts/bricolage-grotesque-latin-b34fc8c1.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/assets/fonts/hanken-grotesk-latin-ext-768af292.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("/assets/fonts/hanken-grotesk-latin-e9201edd.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/fonts/jetbrains-mono-latin-ext-879df931.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/assets/fonts/jetbrains-mono-latin-cb182fee.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ===================================================================
   Immerion landing — one stylesheet, no framework.

   Section order matches the page: tokens, reset, primitives, nav, hero,
   capability strip, problem, services, how, faq, final cta, footer.
   =================================================================== */

/* ===================================================================
   Metric-matched fallbacks.

   Without these the page reflows when the webfonts arrive: Bricolage is
   1.26% wider than Arial, so a headline set in the fallback wraps to one
   fewer line and everything below it jumps 52.5px the moment the real
   face lands. Measured, not guessed - each ratio is the width of the same
   string in both faces at the same size.

   This is what next/font generates automatically, and it is the only
   reason the reference does not have the same jump.
   =================================================================== */
@font-face {
  font-family: "Bricolage Fallback";
  src: local("Arial");
  size-adjust: 101.26%;
  ascent-override: 93%;
  descent-override: 27%;
  line-gap-override: 0%;
}

/* Two faces, split by weight. One size-adjust cannot serve both: Hanken is
   100.84% of Arial at 400 and 95.48% at 600, and the lists in the services
   block mix the two on the same line. With a single face the Spanish page
   still moved 23px when the real font landed. */
@font-face {
  font-family: "Hanken Fallback";
  font-weight: 400 500;
  src: local("Arial");
  size-adjust: 100.84%;
  ascent-override: 100%;
  descent-override: 30%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Hanken Fallback";
  font-weight: 600 700;
  src: local("Arial Bold"), local("Arial");
  size-adjust: 95.48%;
  ascent-override: 100%;
  descent-override: 30%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "JetBrains Fallback";
  src: local("Courier New");
  size-adjust: 99.98%;
  ascent-override: 102%;
  descent-override: 30%;
  line-gap-override: 0%;
}

/* ---------- Tokens ---------- */
:root {
  /* Warm paper, not clinical white. Ink, not black. One blue, used sparingly. */
  --paper: #f6f5f2;
  --paper-2: #ffffff;
  --ink: #14130f;
  --ink-soft: #3a3833;
  --muted: #726e66;
  --muted-2: #6b675e; /* 5.1:1 on paper — where --muted has no headroom */
  --card: var(--paper-2);

  /* Paper at explicit alphas. NEVER write `transparent` in a gradient that
     sits behind text: it parses as rgba(0,0,0,0) — black — so any tool or
     browser path that samples the worst stop reads ink against black. */
  --paper-0: rgba(246, 245, 242, 0);
  --paper-60: rgba(246, 245, 242, 0.6);
  --paper-70: rgba(246, 245, 242, 0.7);

  /* Hairlines live in the same optical system as the shadows: alpha ink, so
     a border darkens with its surface instead of sitting painted on top. */
  --line: rgba(20, 19, 15, 0.09);
  --line-2: rgba(20, 19, 15, 0.17);

  --font-sans: "Hanken Grotesk", "Hanken Fallback", ui-sans-serif, system-ui,
    sans-serif;
  --font-heading: "Bricolage Grotesque", "Bricolage Fallback", ui-sans-serif,
    system-ui, sans-serif;
  --font-mono: "JetBrains Mono", "JetBrains Fallback", ui-monospace, monospace;

  --accent: #2f49e6; /* 6.0:1 on paper — passes AA as body text */
  --accent-soft: #edeffe;

  /* The filled button inverts inside an ink band without a second class. */
  --primary: var(--ink);
  --primary-fg: var(--paper);

  /* Fluid modular scale. Every heading comes from here. */
  --text-eyebrow: 0.75rem;
  --text-display: clamp(2.35rem, 1.45rem + 2.8vw, 3.9rem);
  --text-h2: clamp(2rem, 4.4vw, 3.125rem);
  --text-h3: clamp(1.1875rem, 1.05rem + 0.55vw, 1.5rem);
  --text-lead: clamp(1.0625rem, 0.98rem + 0.35vw, 1.25rem);

  /* Flat, not fluid: one 92px band on every section shell, at every width. */
  --band: 5.75rem;
  --maxw: 1120px;

  /* The only easing curves on this site. Never ease-in. */
  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-soft: cubic-bezier(0.22, 1, 0.36, 1);

  /* Depth: warm light on paper. The ambient layer is tinted toward the paper
     family, never black, with a large negative spread so it tucks under the
     surface instead of smearing sideways. */
  --sh-amb: 74, 65, 54;
  --sh-ink: 20, 19, 15;
  --sh-1: 0 6px 16px -8px rgba(var(--sh-amb), 0.28),
    0 1px 3px -1px rgba(var(--sh-ink), 0.05);
  --sh-2: 0 18px 40px -22px rgba(var(--sh-amb), 0.3),
    0 2px 8px -2px rgba(var(--sh-ink), 0.05),
    0 0 0 1px rgba(var(--sh-ink), 0.045);
  --sh-3: 0 30px 70px -30px rgba(var(--sh-amb), 0.34),
    0 3px 10px -3px rgba(var(--sh-ink), 0.06),
    0 0 0 1px rgba(var(--sh-ink), 0.05);
}

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

html {
  -webkit-text-size-adjust: 100%;
}

/* Smooth scrolling is the one piece of motion that was outside the reduced
   motion guard, and it is the worst one to leave out: an anchor jump moves
   the entire viewport, which is exactly what triggers vestibular symptoms.
   Opted in rather than opted out, so a browser that never reports a
   preference gets the instant jump. */
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

/* The bar is sticky at 64px, so an anchor landing flush with the viewport
   top lands underneath it. Every jump target clears it. Raise the bar and
   raise this. */
:target,
[id] {
  scroll-margin-top: 84px;
}

body {
  margin: 0;
  background-color: var(--paper);
  color: var(--ink-soft);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.5;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3 {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.05;
  color: var(--ink);
  text-wrap: balance;
}

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

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

a {
  color: inherit;
  text-decoration: none;
}

img,
svg {
  display: block;
  max-width: 100%;
}

button {
  font: inherit;
  color: inherit;
}

button:not(:disabled) {
  cursor: pointer;
}

::selection {
  background-color: var(--accent);
  color: #fff;
}

/* An outline, never a zero-offset coloured box-shadow — that is a glow. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Small text carries an explicit line-height wherever the reference leaves it
   to a Tailwind size utility: text-xs is 12/16 and text-sm is 14/20, neither
   of which is the 1.5 the body would otherwise hand down. A 1px row is not
   visible on its own, but it accumulates down a column. */

/* ---------- Primitives ---------- */
.wrap {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 24px;
  width: 100%;
}

@media (min-width: 640px) {
  .wrap {
    padding: 0 32px;
  }
}

/* Eyebrow labels: the only place mono type appears on the page. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-eyebrow);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

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

/* Press feedback in one place, so every control gets it. */
button:not(:disabled):active,
a[data-press]:active {
  transform: scale(0.97);
}

button,
a[data-press] {
  transition-property: transform, background-color, border-color, color,
    box-shadow, opacity;
  transition-duration: 140ms;
  transition-timing-function: var(--ease-out);
}

/* -------------------------------------------------------------------
   Ink band. Not a dark theme — a section that inverts its own token
   scope, so everything inside it flips without a second set of classes.
   ------------------------------------------------------------------- */
.band-ink {
  --line: rgba(246, 245, 242, 0.12);
  --line-2: rgba(246, 245, 242, 0.22);
  --accent: #93a4ff; /* 8.1:1 on ink — the paper blue is unreadable here */
  --card: #1c1b16;
  --muted: #a5a096; /* 7.2:1 on ink */
  --muted-2: #a5a096;
  --ink-soft: var(--paper);
  --primary: var(--paper);
  --primary-fg: var(--ink);
  background-color: var(--ink);
  color: var(--paper);
}

.band-ink h1,
.band-ink h2,
.band-ink h3 {
  color: var(--paper);
}

/* -------------------------------------------------------------------
   Buttons. Pills, not rectangles, and the fill inverts inside an ink
   band because it reads --primary rather than naming a colour.
   ------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  border-radius: 999px;
  font-family: inherit;
  font-weight: 600;
  white-space: nowrap;
  text-align: center;
}

.btn svg {
  width: 16px;
  height: 16px;
  flex: none;
}

.btn-primary {
  background-color: var(--primary);
  color: var(--primary-fg);
}

.btn-primary:hover {
  background-color: color-mix(in srgb, var(--primary) 80%, transparent);
}

.btn-sm {
  height: 40px;
  padding: 0 16px;
  font-size: 13px;
  gap: 8px;
}

.btn-md {
  height: 44px;
  padding: 0 20px;
  font-size: 14px;
  gap: 8px;
}

.btn-lg {
  height: 48px;
  padding: 0 24px;
  font-size: 15px;
  gap: 10px;
}

/* The largest control on the page, on purpose: the booked call is the only
   action the hero asks for. */
.btn-xl {
  height: 56px;
  padding: 0 28px;
  font-size: 17px;
  gap: 10px;
}

/* Every locale label stacked into one grid cell. The hidden ones set the
   width, the visible one is centred over them, so changing the copy moves
   the reservation with it - which a hardcoded min-width would not.
   visibility, not display:none, so the reserved text is not selectable, is
   not found by Ctrl+F, and stays out of the accessible name. */
.btn-sizer {
  display: grid;
  justify-items: center;
}

.btn-sizer > * {
  grid-column: 1;
  grid-row: 1;
  white-space: nowrap;
}

.btn-sizer > [aria-hidden] {
  visibility: hidden;
}

/* ---------- Language switch ---------- */
.lang {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 999px;
}

.lang a {
  position: relative;
  padding: 4px 10px;
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.lang a:hover {
  color: var(--ink);
}

.lang a[aria-current] {
  background-color: var(--ink);
  color: var(--paper);
}

/* -------------------------------------------------------------------
   Tap targets.

   Three controls on this page are drawn smaller than a thumb: the
   service link (28px tall), the footer email (20px) and the language
   pills (35x25). All three clear WCAG 2.5.8's 24px minimum, and all
   three are still an annoying poke on a phone - 2.5.5's 44px is the
   size a thumb actually wants.

   Grown with a pseudo-element rather than padding: padding would move
   the underline off the text on the service link, push the footer
   email away from the line above it, and inflate the language pill
   into a lozenge. This adds the hit area and draws nothing.

   `position: relative` on the control is what the inset is measured
   against. No negative z-index: behind the content is behind the
   paragraph above it too, and a tap ten pixels over the footer email
   landed on that paragraph instead of the link. The box has to sit on
   top to catch anything. It is transparent and it only ever covers
   text that is not interactive.
   ------------------------------------------------------------------- */
.tap {
  position: relative;
}

.tap::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  min-width: 44px;
  min-height: 44px;
  transform: translate(-50%, -50%);
}

/* Height only, for controls that sit in a row next to each other.
   The language pills are 35px wide with a 4px gap, so growing them to
   44px WIDE overlaps the neighbour by five pixels and the second pill
   — later in the DOM, so painted on top — quietly steals the right
   edge of the first. Taller is all the thumb needed anyway. */
.tap-tall::after {
  min-width: 0;
}

/* ---------- Wordmark ---------- */
.wordmark {
  height: 17px;
  width: auto;
}

@media (min-width: 640px) {
  .wordmark {
    height: 19px;
  }
}

.wordmark-sm {
  height: 18px;
}

/* -------------------------------------------------------------------
   Nav
   ------------------------------------------------------------------- */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background-color: rgba(246, 245, 242, 0.8);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color 200ms var(--ease), background-color 200ms var(--ease);
}

.nav[data-scrolled="true"] {
  border-bottom-color: var(--line);
}

.nav-bar {
  display: flex;
  height: 64px;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

.nav-brand {
  flex-shrink: 0;
  display: inline-flex;
}

.nav-links {
  display: none;
  align-items: center;
  gap: 32px;
}

.nav-links a {
  font-size: 14px;
  line-height: 20px;
  color: var(--ink-soft);
  transition: color 150ms var(--ease);
}

.nav-links a:hover {
  color: var(--ink);
}

.nav-right {
  display: flex;
  align-items: center;
  gap: 12px;
}

.nav-lang,
.nav-book {
  display: none;
}

/* Ghost icon button. 40px rather than the 32 the reference uses: a 32px
   target is under the 44px a thumb needs, and at rest this control has no
   fill, so the larger hit area is invisible. */
.nav-burger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background-color: transparent;
  color: var(--ink);
}

.nav-burger svg {
  width: 18px;
  height: 18px;
}

.nav-burger:hover {
  background-color: #edebe6;
}

@media (min-width: 640px) {
  .nav-lang {
    display: inline-flex;
  }

  .nav-book {
    display: inline-flex;
  }
}

@media (min-width: 768px) {
  .nav-links {
    display: flex;
  }

  .nav-burger {
    display: none;
  }
}

/* -------------------------------------------------------------------
   Mobile drawer.

   A <dialog> opened with showModal(), which is where Escape, the focus
   trap and an inert background come from without writing any of the
   three. Scroll lock is the one part the element does not provide.
   ------------------------------------------------------------------- */
/* Pinned explicitly rather than pushed with margin. The UA rule for a modal
   dialog only zeroes the BLOCK insets; the inline ones stay auto, so
   margin-left:auto has nothing to push against and the panel lands wherever
   its static position happens to be — 49px past the right edge, measured. */
.drawer {
  position: fixed;
  inset: 0 0 0 auto;
  width: 86vw;
  max-width: 384px;
  height: 100dvh;
  max-height: 100dvh;
  margin: 0;
  padding: 0;
  border: 0;
  background-color: var(--paper);
  color: var(--ink-soft);
  flex-direction: column;
}

.drawer[open] {
  display: flex;
}

.drawer::backdrop {
  background-color: rgba(20, 19, 15, 0.5);
}

.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px;
}

.drawer-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background-color: transparent;
  color: var(--ink);
}

.drawer-close svg {
  width: 18px;
  height: 18px;
}

.drawer-close:hover {
  background-color: #edebe6;
}

.drawer-links {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 0 16px;
}

.drawer-links a {
  padding: 12px 8px;
  border-radius: 8px;
  font-family: var(--font-heading);
  font-size: 20px;
  line-height: 28px;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--ink);
}

.drawer-foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
  align-items: flex-start;
  padding: 16px;
  border-top: 1px solid var(--line);
}

@media (prefers-reduced-motion: no-preference) {
  .drawer[open] {
    animation: drawer-in 260ms var(--ease-out);
  }

  .drawer[open]::backdrop {
    animation: fade-in 260ms var(--ease-out);
  }

  .drawer.is-closing {
    animation: drawer-out 200ms var(--ease-out) forwards;
  }

  .drawer.is-closing::backdrop {
    animation: fade-in 200ms var(--ease-out) reverse forwards;
  }
}

@keyframes drawer-in {
  from {
    transform: translateX(100%);
  }
}

@keyframes drawer-out {
  to {
    transform: translateX(100%);
  }
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
}

/* -------------------------------------------------------------------
   Footer
   ------------------------------------------------------------------- */
.foot {
  padding: 56px 0;
}

.foot-row {
  display: flex;
  flex-direction: column;
  gap: 40px;
}

.foot-tagline,
.foot-where {
  margin-top: 16px;
  font-size: 14px;
  line-height: 20px;
  color: var(--muted);
}

.foot-tagline {
  max-width: 32ch;
}

.foot-mail {
  position: relative;
  display: inline-block;
  margin-top: 4px;
  font-size: 14px;
  line-height: 20px;
  color: var(--ink-soft);
  text-decoration: underline;
  text-underline-offset: 4px;
}

.foot-right {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 24px;
}

.foot-nav {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.foot-nav a {
  font-size: 14px;
  line-height: 20px;
  color: var(--ink-soft);
  transition: color 150ms var(--ease);
}

.foot-nav a:hover {
  color: var(--ink);
}

.foot-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px 16px;
  margin-top: 48px;
  padding-top: 24px;
  border-top: 1px solid var(--line);
  text-align: center;
}

.foot-bottom p,
.foot-bottom a {
  font-size: 12px;
  line-height: 16px;
  color: var(--muted);
}

/* The privacy link sits at the very bottom of a page whose every other link
   is a call to action, so it stays quiet until it is wanted. */
.foot-bottom a {
  transition: color 150ms var(--ease);
}

.foot-bottom a:hover {
  color: var(--ink);
}

@media (min-width: 640px) {
  .foot-row {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
  }

  .foot-right {
    align-items: flex-end;
  }

  .foot-nav {
    align-items: flex-end;
  }
}

/* ===================================================================
   HERO

   Three photographs crossfading behind a paper scrim. This is the page
   one authored moment; everything else is state feedback.

   DOM order is scrim -> inner -> media on purpose. On desktop the media
   is absolute and z-index decides paint order; under 900px it returns to
   normal flow and lands after the copy, where it becomes a band. One DOM
   order serves both layouts. Do not "fix" it.
   =================================================================== */
.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: 72px 0 84px;
  border-bottom: 1px solid var(--line);
  /* Longhand, not the `background` shorthand: a shorthand carrying a var()
     does not reliably decompose into a resolvable colour. */
  background-color: var(--paper);
  /* INVARIANT. Derived, not guessed: centring gutter + wrap padding + copy
     width. A fixed percentage and a fixed-width column diverge as the
     viewport narrows, and the copy walks off the opaque hold - at 901px it
     overran a 64% hold by 11px. Both the scrim and the media layer are
     clamped against this one number. Raise .hero-copy max-width and raise
     the 560 here by the same amount. */
  --edge: calc(max(0px, (100% - var(--maxw)) / 2) + 32px + 560px);
}

/* Full bleed, no inner inset: a media layer with an inset has a vertical
   edge, and at some width that edge lands on the translucent border of a
   control and composites into a visible seam. */
.hero-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-color: #d6d1ca; /* a frame of stone before decode, never white */
  /* INVARIANT. Cap how wide the photographic band gets. Bled across a 2560px
     monitor a 16:10 frame lands in a 3.2:1 slot and object-fit throws away
     half the picture - the laptop stops reading as a laptop. The second term
     keeps the left edge at least 220px inside the opaque part of the scrim,
     so capping the width can never expose a seam. */
  left: min(max(0px, 100% - 1580px), calc(var(--edge) - 220px));
}

.hero-shot {
  position: absolute;
  inset: 0;
  display: block;
  opacity: 0;
  transition: opacity 1100ms var(--ease-out);
}

.hero-shot.is-active {
  opacity: 1;
}

.hero-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--pos, 100% 50%);
  /* The whole palette discipline is this one filter. No blur, no duotone,
     no glow - it just puts the photograph in the paper world. */
  filter: saturate(0.93) contrast(1.02);
  /* INVARIANT. The drift never goes below 1.0, so no gap can open at the
     edge. 7600ms against a 6500ms dwell on purpose: the drift has to outlast
     the hold or it freezes before the photograph changes. */
  transform: scale(1.042);
  transition: transform 7600ms linear;
}

.hero-shot.is-active img {
  transform: scale(1);
}

.hero-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background-image: linear-gradient(
      90deg,
      var(--paper) 0,
      var(--paper) calc(var(--edge) + 14px),
      var(--paper-60) calc(var(--edge) + 170px),
      var(--paper-0) calc(var(--edge) + 340px)
    ),
    linear-gradient(
      180deg,
      var(--paper-70) 0,
      var(--paper-0) 20%,
      var(--paper-0) 76%,
      var(--paper-70) 100%
    );
}

.hero-inner {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: minmax(0, 560px) minmax(0, 1fr);
  gap: 56px;
  align-items: center;
  min-height: min(76vh, 640px);
}

/* INVARIANT: the 560px inside --edge is derived from this. Move one, move
   the other by the same amount. */
.hero-copy {
  max-width: 560px;
}

.hero h1 {
  font-size: var(--text-display);
  max-width: 15ch;
}

/* Emphasis by colour, not by italics. */
.hero h1 em {
  font-style: normal;
  color: var(--accent);
}

/* --ink-soft (10.7:1), never --muted: secondary text sits over the scrim and
   4.65:1 leaves no headroom. */
.hero-sub {
  margin-top: 22px;
  max-width: 44ch;
  font-size: var(--text-lead);
  color: var(--ink-soft);
}

.hero-cta {
  margin-top: 32px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
}

.hero-note {
  max-width: 46ch;
  font-size: 14px;
  line-height: 20px;
  color: var(--muted);
}

.hero-note strong {
  font-weight: 600;
  color: var(--ink-soft);
}

/* ---------- Service switch ----------
   Three independent toggles, not a tablist: every button keeps its place in
   the tab order, so there is no roving tabindex to get wrong. */
.hero-switch {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Top-aligned, not stretched. Once one name wraps to two lines the row grows,
     and a stretched button centres its own contents inside the taller cell -
     which drops that rail 9.3px below its neighbours, measured. The three rails
     read as one control only while they sit on one line. */
  align-items: start;
  gap: 14px;
  margin-top: 40px;
}

@media (min-width: 640px) {
  .hero-switch {
    gap: 28px;
    margin-top: 48px;
  }
}

/* display:block, because the UA centres a button's contents vertically. Once
   one name wraps to two lines the cells stretch to match, and the single-line
   button would sit its rail 9.3px below its neighbours - measured. The rails
   read as one control only while they are on one line. */
.sw {
  display: block;
  min-width: 0;
  padding: 12px 0 0;
  margin: 0;
  border: 0;
  background: none;
  text-align: left;
  font-family: inherit;
}

/* A progress bar for the dwell, not a decoration. */
.sw-rail {
  display: block;
  width: 100%;
  height: 2px;
  overflow: hidden;
  background-color: var(--line-2);
}

.sw-fill {
  display: block;
  width: 100%;
  height: 100%;
  background-color: var(--ink);
  transform-origin: left center;
  transform: scaleX(0);
}

/* Driven by CSS off --dwell so the rail and the timer cannot drift apart.
   The script restarts the animation at the same moment it restarts the
   timer, which is the only way the two stay in step. */
.sw-fill[data-state="running"] {
  animation: hero-progress var(--dwell, 6500ms) linear forwards;
}

/* Active but not advancing: the visitor took control, or the tab is hidden. */
.sw-fill[data-state="held"] {
  transform: scaleX(1);
}

@keyframes hero-progress {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

.sw-name {
  display: block;
  margin-top: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--muted);
  transition: color 150ms var(--ease);
}

.sw.is-active .sw-name {
  color: var(--ink);
}

.sw:hover .sw-name,
.sw:focus-visible .sw-name {
  color: var(--ink-soft);
}

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

/* Below the tablet breakpoint the names are allowed to wrap instead of being
   cut off. Measured at 390px: the Spanish labels need 111.5px and 120.1px in
   a 104.7px cell, so two of the three services render as "Automatizació…" and
   "Video para red…" — on the page whose whole job is naming those three
   services. The reference truncates them; its hero was never checked at phone
   width, which is exactly what this was meant to catch.

   This is permission, not instruction: at these widths no English label is
   long enough to take a second line. */
@media (max-width: 639px) {
  .sw-name {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    /* "Automatizacion" is one word with nowhere to break, so wrapping alone
       leaves it spilling into the next column. The page declares its language,
       so the browser can hyphenate it properly; break-word is the backstop for
       a browser that has no patterns for that language. */
    hyphens: auto;
    overflow-wrap: break-word;
  }
}

/* Under 900px the photograph stops being a background and becomes a band.
   The copy sits on flat paper: contrast by construction, not by tuning a
   gradient against an unknown image. */
@media (max-width: 900px) {
  .hero {
    padding: 48px 0 0;
  }

  .hero-inner {
    grid-template-columns: 1fr;
    gap: 0;
    min-height: auto;
  }

  .hero-copy {
    max-width: none;
    padding-bottom: 36px;
  }

  .hero-media {
    position: relative;
    inset: auto;
    height: clamp(230px, 56vw, 340px);
  }

  .hero-scrim {
    display: none;
  }

  .hero h1 {
    max-width: none;
  }
}

/* Gentler, not zero: the crossfade stays, the travel goes. Only the active
   rail fills - the reference fills all three, which leaves nothing marking
   which service is showing. */
@media (prefers-reduced-motion: reduce) {
  .hero-shot img {
    transition: none;
    transform: none;
  }

  .sw-fill {
    animation: none !important;
  }

  .sw.is-active .sw-fill {
    transform: scaleX(1);
  }
}

/* ===================================================================
   CAPABILITY STRIP

   Three entry points. Not cards: no surface, no radius, no shadow, no
   headings - hairlines describing real structure. It sits on white
   rather than paper, which is what makes it read as a register of its
   own instead of more hero.
   =================================================================== */
.strip {
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background-color: var(--paper-2);
  padding: 16px 0;
}

.strip-grid {
  display: grid;
  grid-template-columns: 1fr;
}

.strip-item {
  display: block;
  padding: 22px 0;
}

.strip-item + .strip-item {
  border-top: 1px solid var(--line);
}

/* Inset focus ring: at the usual offset it would cross the divider into the
   neighbouring column. */
.strip-item:focus-visible {
  outline-offset: -2px;
}

.strip-name {
  display: block;
  font-family: var(--font-heading);
  font-size: 17px;
  line-height: 1.5;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
  transition: color 200ms var(--ease);
}

.strip-line {
  display: block;
  margin-top: 7px;
  max-width: 34ch;
  font-size: 14px;
  line-height: 1.5;
  color: var(--ink-soft);
}

.strip-item:hover .strip-name {
  color: var(--accent);
}

/* Optical columns, not equal thirds: the three lines are not the same length,
   and equal thirds leave the middle one looking cramped. */
@media (min-width: 820px) {
  .strip-grid {
    grid-template-columns: 1.06fr 1fr 0.92fr;
  }

  .strip-item {
    padding: 14px 34px 16px 0;
  }

  .strip-item + .strip-item {
    border-top: 0;
    border-left: 1px solid var(--line);
    padding-left: 34px;
  }
}

/* ===================================================================
   SECTION SHELL

   One 92px band on every section, at every width. Flat, not fluid.
   =================================================================== */
.block {
  padding: var(--band) 0;
  border-bottom: 1px solid var(--line);
}

.sec-head {
  max-width: 60ch;
}

.sec-head h2 {
  margin-top: 16px;
  font-size: var(--text-h2);
}

.sec-head .lead {
  margin-top: 18px;
  max-width: 52ch;
  font-size: 18px;
  line-height: 1.6;
  color: var(--ink-soft);
}

/* ===================================================================
   THE PROBLEM

   Not cards. The same grammar as the capability strip - a heavier rule
   on top, a divider, and text - so the page has one way of separating
   things instead of a new one per section.
   =================================================================== */
.prob-grid {
  margin-top: 58px;
  display: grid;
  grid-template-columns: 1fr;
  border-top: 1px solid var(--line-2);
}

.prob-item {
  position: relative;
  padding: 26px 0;
}

/* The divider is a pseudo-element, not a border. A real border on a
   text-bearing block reads as a container edge the text sits flush against,
   and the detector reads it the same way; this is a rule BETWEEN columns,
   which is a different thing. It runs horizontally on the stacked layout and
   turns vertical once the three columns exist, so one set of rules serves
   both. */
.prob-item + .prob-item::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 1px;
  background-color: var(--line);
}

.prob-item h3 {
  font-size: 23px;
  line-height: 1.06;
  letter-spacing: -0.02em;
}

.prob-item p {
  margin-top: 13px;
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--ink-soft);
}

@media (min-width: 820px) {
  .prob-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .prob-item {
    padding: 32px 32px 0 0;
  }

  .prob-item + .prob-item {
    padding-left: 32px;
  }

  .prob-item + .prob-item::before {
    inset-block: 0;
    inset-inline: 0 auto;
    width: 1px;
    height: auto;
  }

  .prob-item h3 {
    max-width: 15ch;
  }

  .prob-item p {
    max-width: 36ch;
  }
}

/* ===================================================================
   SERVICES

   One shape used three times, alternating sides. No section head: the
   service names are the h2s of the section.

   No 01/02/03 markers either - three parallel services are not a
   sequence, and numbering them decorates. The same rule is why the
   numbers stay in How it works, where they are real order.
   =================================================================== */
.svc-row + .svc-row {
  margin-top: 58px;
}

/* Uneven on purpose, so no row weighs the same as another and the largest
   gap on the page falls just before the core. */
.svc-row.is-core {
  margin-top: 68px;
}

.svc-grid {
  display: grid;
  gap: 32px;
  align-items: center;
}

.svc-copy h2 {
  font-size: var(--text-h2);
}

/* The core carries its extra weight typographically, not structurally. */
.svc-row.is-core .svc-copy h2 {
  font-size: calc(var(--text-h2) * 1.24);
  line-height: 1.06;
}

.svc-lead {
  margin-top: 16px;
  max-width: 44ch;
  font-size: 17.5px;
  line-height: 1.62;
  color: var(--ink-soft);
}

/* Hairline-ruled, not bulleted. The rule is the page one way of separating
   things; an icon per line would be the only decorative glyph on the whole
   site. The bold lead-in carries the scanning job the icon pretended to do. */
.svc-points {
  margin-top: 26px;
  max-width: 46ch;
}

.svc-points li {
  border-top: 1px solid var(--line);
  padding: 13px 0;
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink-soft);
}

.svc-points strong {
  font-weight: 600;
  color: var(--ink);
}

.svc-cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin-top: 28px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--line-2);
  font-size: 15px;
  font-weight: 500;
  color: var(--accent);
  transition: border-color 200ms var(--ease);
}

.svc-cta:hover {
  border-bottom-color: var(--accent);
}

.svc-cta span {
  transition: transform 200ms var(--ease-out);
}

.svc-cta:hover span {
  transform: translateX(4px);
}

/* All three frames identical: 16:10, same width, same radius, same shadow. */
.svc-frame {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 25.2px;
  background-color: var(--paper-2);
  box-shadow: var(--sh-2);
}

.svc-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (min-width: 1024px) {
  .svc-row + .svc-row {
    margin-top: 74px;
  }

  .svc-row.is-core {
    margin-top: 88px;
  }

  .svc-grid {
    gap: 52px;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.94fr);
  }

  .svc-row.is-shot-first .svc-grid {
    grid-template-columns: minmax(0, 0.94fr) minmax(0, 1fr);
  }

  /* Only above 1024. Stacked, every row reads copy then photograph, which is
     the DOM order - so no row inverts, no two photographs end up adjacent,
     and the title is what gets scanned first on a phone. */
  .svc-row.is-shot-first .svc-shot {
    order: 1;
  }

  .svc-row.is-shot-first .svc-copy {
    order: 2;
  }
}

/* ===================================================================
   HOW IT WORKS

   The only section on the page with cards, and the only one that keeps
   its numbers: these three are a real sequence, so 01/02/03 is
   information rather than decoration.
   =================================================================== */

/* An ink band closes itself by changing colour. A hairline inside it would
   be a second, weaker way of saying the same thing. */
.block.band-ink {
  border-bottom: 0;
}

/* Inside a band the lead steps back to --muted, which the band redefines to
   a value that clears 7:1 on ink. */
.band-ink .lead {
  color: var(--muted);
}

.how-steps {
  margin-top: 52px;
  display: grid;
  gap: 22px;
}

.how-card {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  padding: 26px;
  border: 1px solid var(--line);
  border-radius: 19.6px;
  background-color: var(--card);
  box-shadow: var(--sh-2);
  transition: border-color 250ms var(--ease-soft),
    transform 250ms var(--ease-soft), box-shadow 250ms var(--ease-soft);
}

/* 250ms, not 400: the hover is feedback, not decoration. */
.how-card:hover {
  transform: translateY(-4px);
  border-color: var(--line-2);
  box-shadow: var(--sh-3);
}

.how-n {
  display: block;
  font-family: var(--font-heading);
  font-size: 12px;
  line-height: 16px;
  font-weight: 600;
  letter-spacing: 0.12em;
  color: var(--muted);
}

.how-card h3 {
  margin-top: 14px;
  font-size: 17px;
  letter-spacing: -0.01em;
}

.how-card p {
  margin-top: 8px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted);
}

/* A real element, matching the reference, and it wipes in from the left. */
.how-wipe {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 2px;
  background-color: var(--accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 300ms var(--ease-out);
}

.how-card:hover .how-wipe {
  transform: scaleX(1);
}

@media (min-width: 820px) {
  .how-steps {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

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

   details/summary, because this needs neither a focus trap nor a scroll
   lock. The opening height is animated with grid-template-rows 0fr -> 1fr,
   which is the only way to transition to an automatic height.
   =================================================================== */
.faq-grid {
  display: grid;
  gap: 40px;
}

.faq-head h2 {
  margin-top: 20px;
  font-size: var(--text-h2);
}

.faq-list {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.faq-item:not(:last-child) {
  border-bottom: 1px solid var(--line);
}

.faq-item summary {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 0;
  border-radius: 14px;
  font-family: var(--font-heading);
  font-size: 17px;
  line-height: 20px;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}

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

.faq-item summary:hover span {
  text-decoration: underline;
}

.faq-item summary svg {
  width: 16px;
  height: 16px;
  flex: none;
  margin-top: 2px;
  color: var(--muted-2);
  transition: transform 200ms var(--ease-out);
}

/* One chevron rotated, rather than two swapped: an up chevron IS a rotated
   down chevron, and one element cannot get out of step with the other. */
.faq-item[open] summary svg {
  transform: rotate(180deg);
}

/* grid-template-rows 0fr -> 1fr, which is the only way to transition to an
   automatic height without measuring it. Opening is pure CSS. Closing is not:
   setting `open` to false hides the panel in the same frame, so the script
   pins the rows to 0fr, waits out the transition, and only then closes the
   element. */
.faq-body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 300ms var(--ease-out);
}

.faq-item[open] .faq-body {
  grid-template-rows: 1fr;
}

.faq-item[open][data-collapsed] .faq-body {
  grid-template-rows: 0fr;
}

.faq-answer {
  overflow: hidden;
}

.faq-answer p {
  padding-bottom: 10px;
  max-width: 58ch;
  font-size: 15px;
  line-height: 1.4286;
  color: var(--ink-soft);
}

@media (min-width: 1024px) {
  .faq-grid {
    grid-template-columns: minmax(0, 0.7fr) minmax(0, 1fr);
    gap: 80px;
  }
}

/* ===================================================================
   FINAL CTA

   The crescendo. The only h2 on the page allowed past the section
   scale, and the only form. It closes on ink rather than on paper -
   the reference ends light; this ends with weight.
   =================================================================== */
.cta {
  position: relative;
  overflow: hidden;
  padding: var(--band) 0;
}

/* The symbol as a watermark. It only works over ink, and it only appears
   where there is room for it. */
.cta-mark {
  position: absolute;
  right: -64px;
  top: 50%;
  transform: translateY(-50%);
  display: none;
  height: 320px;
  width: auto;
  opacity: 0.06;
  color: var(--paper);
  pointer-events: none;
}

.cta-inner {
  position: relative;
  text-align: center;
}

.cta h2 {
  margin: 20px auto 0;
  max-width: 18ch;
  font-size: clamp(2.25rem, 5.5vw, 3.875rem);
  line-height: 1.06;
}

.cta-sub {
  margin: 22px auto 0;
  max-width: 46ch;
  font-size: 18px;
  line-height: 1.6;
  color: var(--muted);
}

.cta-action,
.cta-form {
  margin-top: 32px;
}

.cta-form form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

.field {
  height: 48px;
  width: 100%;
  max-width: 300px;
  padding: 0 20px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background-color: transparent;
  font-family: inherit;
  font-size: 15px;
  color: var(--paper);
}

.field::placeholder {
  color: var(--muted);
}

.field:focus-visible {
  border-color: var(--accent);
}

.form-submit {
  height: 48px;
  padding: 0 24px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background-color: transparent;
  font-size: 15px;
  font-weight: 600;
  color: var(--paper);
  transition: border-color 200ms var(--ease);
}

.form-submit:hover {
  border-color: var(--accent);
}

.form-submit:disabled {
  opacity: 0.6;
}

/* Always in the DOM, never mounted on demand: a live region that appears at
   the same moment as its text is announced inconsistently across screen
   readers. It is also what keeps the layout from jumping when a message
   arrives. */
.form-status {
  margin-top: 16px;
  min-height: 20px;
  font-size: 14px;
  color: var(--paper);
}

/* Success and failure said the same thing in the same colour, so the only
   difference between "we have it" and "it never left" was reading the
   sentence. The colour is reinforcement, never the message: each string
   states its own outcome, which is what keeps this out of WCAG 1.4.1.
   Both measured on --ink #14130f: 11.2:1 and 9.3:1. */
.form-status[data-tone="ok"] {
  color: #8fd9a8;
}

.form-status[data-tone="error"] {
  color: #ff9e93;
}

.form-note {
  margin-top: 8px;
  font-size: 14px;
  line-height: 20px;
  color: var(--muted);
}

@media (min-width: 1024px) {
  .cta-mark {
    display: block;
  }
}

/* ===================================================================
   THE MOBILE BOOKING DOCK

   Hidden by default at every width, and only ever shown below 640px -
   which is exactly the range where the header drops the book button.
   Above that the header carries it and a second one would be noise.

   Gated on html[data-hydrated] for the same reason the dock exists at
   all: it is the script that pulls it back down over the closing band
   and the form. Without the script it never appears, which is the safe
   direction here - the page already carries five inline calls to
   action, and an undismissable bar over the form is worse than none.
   =================================================================== */
.dock {
  display: none;
}

@media (max-width: 639px) {
  html[data-hydrated] .dock {
    display: block;
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 40;
    /* The home indicator on a modern iPhone sits in the bottom 34px. Without
       this the button is under the reader's thumb rest and half of the taps
       go to the system, not the page. */
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--line);
    /* Paper, not a blur: the page behind this is photographs and ink type,
       and a translucent bar over either reads as a rendering fault. */
    background-color: var(--paper);
    box-shadow: 0 -10px 30px -22px rgba(var(--sh-amb), 0.55);
  }

  .dock-inner {
    max-width: var(--maxw);
    margin: 0 auto;
  }

  .dock .btn {
    width: 100%;
  }

  /* Off-state is a real removal, not just an offset: visibility is what
     takes it out of the tab order and off a screen reader, and it is
     animatable, so it waits for the slide instead of cutting it short. */
  html[data-hydrated] .dock[data-state="away"] {
    visibility: hidden;
    transform: translateY(100%);
  }

  html[data-hydrated] .dock[data-state="here"] {
    visibility: visible;
    transform: translateY(0);
  }
}

@media (max-width: 639px) and (prefers-reduced-motion: no-preference) {
  html[data-hydrated] .dock {
    transition: transform 260ms var(--ease), visibility 260ms var(--ease);
  }
}

/* ===================================================================
   SCROLL REVEAL, WHICH FAILS OPEN TWICE AND WITH NO SCRIPT INVOLVED

   `scripting: enabled` is the entire JavaScript gate. With JavaScript
   off, or in a browser too old to know the query, nothing in here
   matches and the page renders complete.

   The second fail-open is the timer, which is a delayed animation
   rather than a setTimeout: if the script never runs, nothing stamps
   data-hydrated on <html> and the hidden state is withdrawn at 2.5s -
   rather than leaving a column of empty sections forever.
   =================================================================== */
.reveal {
  opacity: 1;
}

@media (scripting: enabled) {
  .reveal {
    opacity: 0;
    transform: translateY(14px);
    animation: reveal-failopen 600ms var(--ease-out) 2500ms forwards;
  }

  /* The script is alive, so the scroll observer owns the reveal from here and
     the fail-open never gets to fire. */
  html[data-hydrated] .reveal {
    animation: none;
  }

  .reveal[data-visible] {
    opacity: 1;
    transform: translateY(0);
    animation: none;
    transition: opacity 600ms var(--ease-out) var(--reveal-delay, 0s),
      transform 600ms var(--ease-out) var(--reveal-delay, 0s);
  }
}

@keyframes reveal-failopen {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Gentler, not zero: the fade stays, the travel goes. This block sits AFTER
   the one above on purpose - the selectors have the same specificity, so it
   wins on order of appearance. Do not move it. */
@media (prefers-reduced-motion: reduce) {
  .reveal {
    transform: none;
  }

  .reveal[data-visible] {
    transition: opacity 300ms var(--ease-out);
  }
}
