/* ---- fonts.css ---- */
/* Vendored by tools/fetch_fonts.py — do not hand-edit.
 * Newsreader, Literata and Hanken Grotesk, SIL Open Font License 1.1.
 */
/* latin-ext */
@font-face {
  font-family: 'Newsreader';
  font-style: italic;
  font-weight: 200 500;
  font-display: swap;
  src: url(/assets/fonts/newsreader-latin-ext-200-500-italic.750e9629f5.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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
  }
/* latin */
@font-face {
  font-family: 'Newsreader';
  font-style: italic;
  font-weight: 200 500;
  font-display: swap;
  src: url(/assets/fonts/newsreader-latin-200-500-italic.a99fb12768.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;
  }
/* latin-ext */
@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 200 500;
  font-display: swap;
  src: url(/assets/fonts/newsreader-latin-ext-200-500-normal.cbca001188.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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
  }
/* latin */
@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 200 500;
  font-display: swap;
  src: url(/assets/fonts/newsreader-latin-200-500-normal.01817351be.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;
  }
/* cyrillic-ext */
@font-face {
  font-family: 'Literata';
  font-style: italic;
  font-weight: 200 500;
  font-display: swap;
  src: url(/assets/fonts/literata-cyrillic-ext-200-500-italic.a9cae293aa.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
  }
/* cyrillic */
@font-face {
  font-family: 'Literata';
  font-style: italic;
  font-weight: 200 500;
  font-display: swap;
  src: url(/assets/fonts/literata-cyrillic-200-500-italic.f23806cc60.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
  }
/* cyrillic-ext */
@font-face {
  font-family: 'Literata';
  font-style: normal;
  font-weight: 200 500;
  font-display: swap;
  src: url(/assets/fonts/literata-cyrillic-ext-200-500-normal.e3a9b2371d.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
  }
/* cyrillic */
@font-face {
  font-family: 'Literata';
  font-style: normal;
  font-weight: 200 500;
  font-display: swap;
  src: url(/assets/fonts/literata-cyrillic-200-500-normal.33ca48972e.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
  }
/* latin-ext */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(/assets/fonts/hanken-grotesk-latin-ext-400-600-normal.a1376a5637.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-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
  }
/* latin */
@font-face {
  font-family: 'Hanken Grotesk';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(/assets/fonts/hanken-grotesk-latin-400-600-normal.1f21c6eaa0.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;
  }
/* cyrillic-ext */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(/assets/fonts/manrope-cyrillic-ext-400-600-normal.e8c0b39992.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
  }
/* cyrillic */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url(/assets/fonts/manrope-cyrillic-400-600-normal.95a493061f.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
  }

/* ---- 01-tokens.css ---- */
/* ===========================================================================
   Design tokens — extracted from the brochures, not invented.
   Colours are the literal fill and text colours used across the four PDFs;
   --sea is sampled from the corners of the aerial plate so the WebGL water
   and the photograph read as one body of water.
   ========================================================================== */

:root {
  /* --- ground ------------------------------------------------------------ */
  --navy-900: #080f16;
  --navy-800: #0b1218;
  --navy-700: #0e1a24;
  --navy-600: #0b1d29;
  --sea:      #1a253a;

  /* --- ink --------------------------------------------------------------- */
  --slate-700: #2c383f;
  --slate-600: #33404a;
  --slate-500: #40525c;
  /* Measured, not chosen: at #6e7a80 every label, hint and caption on a light
     ground came out at 4.41:1 against the card and 4.05:1 against the section
     — both under the 4.5 they need, and both wrong since long before the
     enquiry page was rebuilt. #616c72 gives 5.31 and 4.87, which holds on
     the alternate ground too, where #667177 came in at 4.47. The dark-ground
     equivalent is --mist-400 under .on-dark and is untouched. */
  --muted:     #616c72;

  /* --- mist -------------------------------------------------------------- */
  --mist-400: #8ea0a8;
  --mist-300: #aebbc0;
  --mist-200: #c9d2d5;
  --mist-100: #e3eaeb;

  /* --- warm -------------------------------------------------------------- */
  --sand:       #d9d2c6;
  --ivory:      #f4f2ec;
  --ivory-pale: #f7f5ef;
  --champagne:  #cfae6e;
  /* The UI accent on light grounds. Measured at #9c7a3c every eyebrow on the
     site came out at 3.99:1 on the card and 3.66:1 on the section — under the
     4.5 an 11px label needs, and wrong everywhere, not only on the enquiry
     page. #866934 is the lightest bronze that clears all three light grounds
     the site uses — white 5.14, ivory-pale 4.72, ivory 4.59. At #896b35 the
     darkest of the three still came in at 4.45.
     Dark grounds use --champagne and are untouched, and the logo's own ink is
     --brand-bronze below, which is why this one can be tuned at all. */
  --bronze:     #866934;
  /* The logo's own bronze, lighter than the UI accent above. The accent has
     to clear 4.5:1 against the grounds it labels; this one only has to be
     the ink the mark is printed in, and matching it to the accent would
     reprint the brand to suit the interface. */
  --brand-bronze: #b18243;

  /* --- semantic, light ground (the default) ------------------------------ */
  --bg:          var(--ivory-pale);
  --bg-alt:      var(--ivory);
  --fg:          var(--slate-700);
  --fg-strong:   var(--navy-700);
  --fg-dim:      var(--muted);
  --rule:        color-mix(in srgb, var(--slate-700) 14%, transparent);
  --rule-strong: color-mix(in srgb, var(--slate-700) 26%, transparent);
  --accent:      var(--bronze);
  --accent-hair: var(--champagne);
  /* The oversized index numerals. They are text, so they answer to contrast,
     and on a light ground the printed champagne measured 1.89:1. At 36px the
     bar is 3:1, which this clears on white, ivory-pale and ivory (3.49, 3.20,
     3.12) while staying a gold rather than becoming a brown. Hairlines keep
     --accent-hair, because a rule is not text. */
  --numeral:     #9f8655;
  --selection:   color-mix(in srgb, var(--champagne) 36%, transparent);

  /* --- type -------------------------------------------------------------- */
  /* One stack, every script. `unicode-range` on the vendored faces already
     decides which family a glyph comes from, so a Cyrillic page reaches
     Literata and Manrope for Cyrillic and still gets Newsreader for the
     Latin in "Meridian" — no per-language CSS, no flash of the wrong face.
     The CJK names at the end are system fonts on purpose: a Chinese web font
     with real coverage is measured in megabytes, which would undo the whole
     weight budget for one edition. Every platform ships a good one. */
  --serif: 'Newsreader', 'Literata', 'Iowan Old Style', 'Palatino Linotype',
           Palatino, 'Songti SC', 'Noto Serif CJK SC', 'Source Han Serif SC',
           SimSun, 'Times New Roman', serif;
  --sans:  'Hanken Grotesk', 'Manrope', ui-sans-serif, -apple-system,
           'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB',
           'Microsoft YaHei', 'Noto Sans CJK SC', 'Helvetica Neue', Arial,
           sans-serif;

  /* Fluid display sizes. The brochures set headlines very large relative to
     the page, with tight leading and a light weight — the proportions below
     reproduce that at screen scale. */
  --d1: clamp(2.5rem, 1.35rem + 4.9vw, 5.5rem);
  --d2: clamp(2rem, 1.3rem + 2.95vw, 3.6rem);
  --d3: clamp(1.5rem, 1.17rem + 1.4vw, 2.25rem);
  --d4: clamp(1.25rem, 1.1rem + 0.62vw, 1.6rem);

  --t-lede:  clamp(1.0625rem, 1.01rem + 0.27vw, 1.25rem);
  --t-body:  1rem;
  --t-small: 0.875rem;
  --t-micro: 0.75rem;
  --t-label: 0.6875rem;

  --lh-display: 1.08;
  --lh-lede:    1.6;
  --lh-body:    1.68;

  --track-label: 0.24em;
  --track-display: -0.014em;

  /* --- measure ----------------------------------------------------------- */
  --measure:        62ch;
  --measure-narrow: 44ch;
  --measure-lede:   52ch;

  /* --- space (4px base) --------------------------------------------------- */
  --s1: 0.25rem;  --s2: 0.5rem;   --s3: 0.75rem;  --s4: 1rem;
  --s5: 1.5rem;   --s6: 2rem;     --s7: 2.5rem;   --s8: 3rem;
  --s9: 4rem;     --s10: 5rem;    --s11: 7rem;    --s12: 9rem;

  /* The header's height is a design decision, not a measurement. It was
     briefly derived from the rendered element and fed back into the element's
     own min-height, which the 1px border on light sections turned into a
     1px-per-frame growth loop. A token cannot do that. */
  --header-h: 4.25rem;

  --gutter: clamp(1.25rem, 0.6rem + 2.6vw, 4.5rem);
  --shell:  84rem;
  --section-y: clamp(4.5rem, 2.5rem + 8vw, 10rem);

  /* --- motion ------------------------------------------------------------- */
  --ease-out:  cubic-bezier(0.22, 1, 0.36, 1);
  --ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
  --dur-fast: 180ms;
  --dur:      420ms;
  --dur-slow: 900ms;

  --z-canvas: 0;
  --z-content: 10;
  --z-index-rail: 40;
  --z-header: 50;
  /* The menu overlay is a descendant of the header, so these two values order
     them *inside* the header's own stacking context: the bar — wordmark and
     the button that closes the menu — paints above the overlay. */
  --z-menu: 1;
  --z-bar: 2;
}

/* A dark section inverts the semantic layer rather than restating colours. */
.on-dark {
  --bg:          var(--navy-700);
  --bg-alt:      var(--navy-800);
  --fg:          var(--mist-200);
  --fg-strong:   var(--ivory);
  --fg-dim:      var(--mist-400);
  --rule:        color-mix(in srgb, var(--mist-200) 18%, transparent);
  --rule-strong: color-mix(in srgb, var(--mist-200) 34%, transparent);
  --accent:      var(--champagne);
  --numeral:     var(--accent-hair);
  color: var(--fg);
}

/* The site has one deliberate appearance: an ivory editorial ground with
   full-bleed dark scenes. It is not theme-switched, so a viewer whose OS is
   in dark mode still sees the brand. Only the form controls follow the OS, so
   that native widgets do not render unreadable. */
:root { color-scheme: light; }

@media (max-width: 48rem) { :root { --header-h: 3.75rem; } }

/* ---- 02-base.css ---- */
/* ===========================================================================
   Base — reset, typography primitives, accessibility
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The sticky header must not cover a heading jumped to from the index. */
  scroll-padding-top: calc(var(--header-h) + var(--s6));
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* Horizontal bleed from a full-width wash is clipped here rather than
     allowed to widen the page. `clip` and not `hidden` on purpose: `hidden`
     would turn the other axis into a scroll container, `clip` leaves the
     viewport scrolling the document as usual. */
  overflow-x: clip;
}

::selection { background: var(--selection); color: var(--navy-700); }

h1, h2, h3, h4, p, figure, blockquote, ul, ol, dl, dd { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg, canvas, video { display: block; max-width: 100%; }
img { height: auto; }

a { color: inherit; text-decoration-thickness: from-font; text-underline-offset: 0.22em; }

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; padding: 0; }

/* --- focus --------------------------------------------------------------
   One visible focus treatment everywhere, legible on ivory and on navy. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 1px;
}
.on-dark :focus-visible, .scene--dark :focus-visible { outline-color: var(--champagne); }
:focus:not(:focus-visible) { outline: none; }

/* --- type primitives ----------------------------------------------------- */
.display {
  font-family: var(--serif);
  font-weight: 300;
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  color: var(--fg-strong);
  text-wrap: balance;
}
.display em { font-style: italic; font-weight: 300; }
/* Headlines are written as explicit lines; each one is its own block so the
   break is a typographic decision rather than whatever the browser does. */
.display span { display: block; }
/* Below the smallest breakpoint the authored breaks would strand single
   words, so the lines are allowed to reflow as one paragraph. */
@media (max-width: 30rem) {
  .display span { display: inline; }
  .display span + span::before { content: " "; }
}

/* Measures are in em so each size keeps the same line length in characters. */
.d1 { font-size: var(--d1); max-width: 13em; }
.d2 { font-size: var(--d2); max-width: 15em; }
.d3 { font-size: var(--d3); max-width: 18em; }
.d4 { font-size: var(--d4); line-height: 1.22; max-width: 20em; }

.label {
  font-size: var(--t-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--accent);
  margin: 0;
}
.label--mute { color: var(--fg-dim); }

.lede {
  font-size: var(--t-lede);
  line-height: var(--lh-lede);
  color: var(--fg);
  max-width: var(--measure-lede);
  text-wrap: pretty;
}

.prose { max-width: var(--measure); text-wrap: pretty; }
.prose > * + * { margin-top: var(--s4); }

.small { font-size: var(--t-small); }
.micro { font-size: var(--t-micro); line-height: 1.5; color: var(--fg-dim); }

.note {
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--fg-dim);
  max-width: var(--measure-narrow);
  padding-left: var(--s4);
  border-left: 1px solid var(--rule-strong);
}

.quote {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 300;
  font-size: var(--d4);
  line-height: 1.34;
  color: var(--fg-strong);
  max-width: 30ch;
  text-wrap: pretty;
}

/* A hairline the colour of the brochures' gold rules. */
.hairline {
  width: clamp(2.5rem, 5vw, 4.5rem);
  height: 1px;
  background: var(--accent-hair);
  border: 0;
  margin: 0;
}

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

/* Fixed, not absolute: an absolutely positioned skip link resolves against
   the document, so a visitor who tabs into the page after scrolling finds it
   thousands of pixels above the viewport. */
.skip-link {
  position: fixed; top: var(--s3); left: var(--s3);
  z-index: 100;
  padding: var(--s3) var(--s5);
  background: var(--ivory);
  color: var(--navy-700);
  font-size: var(--t-small);
  font-weight: 500;
  border: 1px solid var(--navy-700);
  transform: translateY(-250%);
  transition: transform var(--dur-fast) var(--ease-out);
}
.skip-link:focus { transform: none; }

.shell {
  width: min(100% - var(--gutter) * 2, var(--shell));
  margin-inline: auto;
}
.shell--wide { width: min(100% - var(--gutter) * 2, 102rem); }

/* --- reduced motion -------------------------------------------------------
   The scene stops animating, reveals resolve instantly, and nothing that
   carries meaning depends on movement. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }
}

@media print {
  .site-header, .chapter-rail, .scene-canvas, .scroll-cue, .site-footer nav { display: none; }
  body { background: #fff; color: #000; }
  .scene, .section { page-break-inside: avoid; }
}

/* Chinese sets no italics. The display face's emphasis is a synthetic
   oblique applied to Han characters, which no Chinese typographer would
   accept; the champagne colour the same rule gives is emphasis enough. */
:lang(zh) .display em,
:lang(zh) em { font-style: normal; }

/* ---- 03-chrome.css ---- */
/* ===========================================================================
   Chrome — header, chapter rail, footer
   ========================================================================== */

.site-header {
  position: fixed; inset: 0 0 auto 0;
  z-index: var(--z-header);
  --header-fg: var(--ivory);
  --header-rule: color-mix(in srgb, var(--ivory) 20%, transparent);
  color: var(--header-fg);
  transition: background-color var(--dur) var(--ease-soft),
              color var(--dur) var(--ease-soft),
              backdrop-filter var(--dur) var(--ease-soft);
}
/* Over a photographic scene the header is bare, with a scrim so the wordmark
   and the navigation stay legible whatever is underneath — including the end
   of the approach, where the frame is filled by white superstructure. It is
   extended past the header's own height so it reads as a top vignette rather
   than a bar. */
.site-header::before {
  content: ""; position: absolute;
  inset: 0 0 -2.75rem 0;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--navy-900) 76%, transparent) 0%,
    color-mix(in srgb, var(--navy-900) 58%, transparent) 46%,
    color-mix(in srgb, var(--navy-900) 22%, transparent) 78%,
    transparent 100%);
  opacity: 1; transition: opacity var(--dur) var(--ease-soft);
  pointer-events: none;
}
/* Over a dark section, once scrolled: a navy bar rather than a scrim, so
   nothing passes underneath half-legible. */
.site-header[data-ground="dark"][data-scrolled="true"] {
  background: color-mix(in srgb, var(--navy-800) 88%, transparent);
  backdrop-filter: saturate(1.2) blur(14px);
  border-bottom: 1px solid color-mix(in srgb, var(--mist-200) 12%, transparent);
}
.site-header[data-ground="dark"][data-scrolled="true"]::before { opacity: 0; }

.site-header[data-ground="light"] {
  --header-fg: var(--navy-700);
  --header-rule: color-mix(in srgb, var(--navy-700) 14%, transparent);
  background: color-mix(in srgb, var(--ivory-pale) 86%, transparent);
  backdrop-filter: saturate(1.4) blur(14px);
  border-bottom: 1px solid var(--header-rule);
}
.site-header[data-ground="light"]::before { opacity: 0; }

.site-header__inner {
  display: flex; align-items: center; gap: var(--s6);
  min-height: var(--header-h);
  position: relative;
}

.wordmark {
  display: inline-flex; align-items: center; gap: var(--s3);
  text-decoration: none; color: inherit;
  flex: 0 0 auto;
  position: relative; z-index: var(--z-bar);
  /* The monogram is 30px tall; the link that carries it home should be a
     finger's worth. The glyph stays where it is — only the box grows. */
  min-height: 2.75rem;
}
.wordmark__mark {
  /* The official mark's own proportion, 943x266 — wide, because the bronze
     wave's tail runs out to the left of the M. The letters fill the whole
     height, so unlike the drawn mark this one is legible small; it is set at
     28px so MYP's cap height sits just above MERIDIAN's beside it, which is
     the relationship the printed lockup has. */
  width: 6.2rem; height: 1.75rem; flex: 0 0 auto;
  position: relative;
  background: currentColor;
  -webkit-mask: url(/assets/brand/mark-mask.3781272882.png) center / contain no-repeat;
          mask: url(/assets/brand/mark-mask.3781272882.png) center / contain no-repeat;
}
/* The wave is the mark's second ink and its only warmth, so it is painted
   bronze rather than taking the ground's colour with the letters. The two
   masks are cut from the same box upstream, which is what lets them simply
   sit on top of one another here. */
.wordmark__mark::after {
  content: ""; position: absolute; inset: 0;
  background: var(--brand-bronze);
  -webkit-mask: url(/assets/brand/mark-wave.02667205c1.png) center / contain no-repeat;
          mask: url(/assets/brand/mark-wave.02667205c1.png) center / contain no-repeat;
}
.wordmark__text { display: flex; flex-direction: column; line-height: 1; }
.wordmark__name {
  font-family: var(--serif); font-weight: 300;
  font-size: 1.0625rem; letter-spacing: 0.055em;
  text-transform: uppercase;
}
.wordmark__sub {
  font-size: 0.5625rem; letter-spacing: 0.26em; text-transform: uppercase;
  color: color-mix(in srgb, currentColor 62%, transparent);
  margin-top: 0.3em;
}

/* A flex row, not a block: the list and the language link are siblings, and
   a block <ul> followed by an inline link stacks them into two rows — which
   is exactly what happened once a second edition made the switch appear. */
.site-nav { margin-left: auto; display: flex; align-items: center; }
.site-nav__list { display: flex; align-items: center; gap: clamp(1rem, 2vw, 2rem); }
.site-nav__link {
  display: inline-flex; align-items: center;
  min-height: 2.75rem;
  font-size: var(--t-small); text-decoration: none;
  color: color-mix(in srgb, currentColor 82%, transparent);
  position: relative;
  transition: color var(--dur-fast) var(--ease-soft);
}
.site-nav__link::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0.7rem;
  height: 1px; background: var(--accent-hair);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur) var(--ease-out);
}
.site-nav__link:hover { color: currentColor; }
.site-nav__link:hover::after, .site-nav__link[aria-current]::after { transform: scaleX(1); }
.site-nav__link[aria-current] { color: currentColor; }

.site-nav__cta {
  border: 1px solid color-mix(in srgb, currentColor 36%, transparent);
  padding: 0.55rem 1.1rem;
  font-size: var(--t-small);
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-soft),
              border-color var(--dur-fast) var(--ease-soft);
}
.site-nav__cta::after { display: none; }
.site-nav__cta:hover {
  background: color-mix(in srgb, currentColor 10%, transparent);
  border-color: currentColor;
}

/* The switcher holds one link per other edition. With two editions it is the
   single "IT" it has always been; with five it is a short row, which is what
   the houses in this trade use rather than a dropdown nobody opens. */
.lang-switch {
  display: inline-flex; align-items: center; gap: var(--s3);
  min-height: 2.75rem;
  padding-inline-start: clamp(0.75rem, 1.4vw, 1.25rem);
  border-inline-start: 1px solid var(--header-rule);
  margin-inline-start: clamp(0.25rem, 1vw, 0.75rem);
}
.lang-switch__link {
  /* Each code is its own link now, so each needs its own hit area: as bare
     text "IT" measured 13px wide against a 24px minimum, and the audit
     caught it on all fourteen routes. */
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 2.75rem; min-width: 2rem; padding-inline: var(--s2);
  /* A language label is never allowed to break: 中文 is two characters
     and would stack if the row ever got tight. */
  white-space: nowrap;
  font-size: var(--t-micro); letter-spacing: 0.14em; text-transform: uppercase;
  text-decoration: none;
  color: color-mix(in srgb, currentColor 62%, transparent);
}
.lang-switch__link:hover,
.lang-switch__link:focus-visible { color: currentColor; }

/* --- mobile menu --------------------------------------------------------- */
.menu-toggle {
  display: none; margin-left: auto;
  position: relative; z-index: var(--z-bar);
  align-items: center; gap: var(--s3);
  font-size: var(--t-micro); letter-spacing: 0.18em; text-transform: uppercase;
  color: inherit; padding: var(--s3) 0;
}
.menu-toggle__bars { display: grid; gap: 4px; width: 1.25rem; }
.menu-toggle__bars span {
  height: 1px; background: currentColor;
  transition: transform var(--dur) var(--ease-out), opacity var(--dur-fast);
}
.menu-toggle[aria-expanded="true"] .menu-toggle__bars span:nth-child(1) { transform: translateY(5px) rotate(45deg); }
.menu-toggle[aria-expanded="true"] .menu-toggle__bars span:nth-child(2) { opacity: 0; }
.menu-toggle[aria-expanded="true"] .menu-toggle__bars span:nth-child(3) { transform: translateY(-5px) rotate(-45deg); }

@media (max-width: 62rem) {
  .menu-toggle { display: inline-flex; }
  .site-nav {
    position: fixed; inset: 0; z-index: var(--z-menu);
    background: var(--navy-700); color: var(--mist-200);
    display: grid; align-content: center; align-items: start;
    padding: var(--gutter);
    margin: 0;
    transform: translateY(-100%);
    visibility: hidden;
    /* Visibility flips instantly on open so the first link is focusable
       straight away, and only after the slide on close. */
    transition: transform var(--dur-slow) var(--ease-out),
                visibility 0s linear var(--dur-slow);
  }
  .site-nav[data-open="true"] {
    transform: none; visibility: visible;
    transition: transform var(--dur-slow) var(--ease-out), visibility 0s;
  }
  /* Clear the header bar, which stays above the overlay. */
  .site-nav__list { padding-top: var(--header-h); }
  .site-nav__list { flex-direction: column; align-items: flex-start; gap: var(--s5); }
  .site-nav__link, .site-nav__cta {
    font-family: var(--serif); font-size: var(--d3); font-weight: 300;
    color: var(--ivory); border: 0; padding: 0;
  }
  .site-nav__cta { color: var(--champagne); }
  .lang-switch { border: 0; padding: 0; margin: var(--s5) 0 0; gap: var(--s5); }
  .lang-switch__link { color: var(--mist-400); }
}

/* --- chapter rail --------------------------------------------------------
   A discreet index of the scenes. Real links to real section ids, so it works
   with the keyboard and without JavaScript. */
.chapter-rail {
  position: fixed; z-index: var(--z-index-rail);
  top: 50%; right: clamp(0.75rem, 1.6vw, 1.75rem);
  transform: translateY(-50%);
  opacity: 0; visibility: hidden;
  transition: opacity var(--dur) var(--ease-soft), visibility var(--dur);
}
.chapter-rail[data-visible="true"] { opacity: 1; visibility: visible; }
.chapter-rail__list { display: grid; gap: 2px; }
.chapter-rail__link {
  display: flex; align-items: center; justify-content: flex-end;
  gap: var(--s3); padding: 0.3rem 0;
  text-decoration: none; color: var(--mist-300);
  font-size: var(--t-micro); letter-spacing: 0.08em;
}
.chapter-rail__label {
  opacity: 0; transform: translateX(6px);
  transition: opacity var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
  white-space: nowrap;
  padding: 0.15rem 0.5rem;
  background: color-mix(in srgb, var(--navy-900) 82%, transparent);
  color: var(--ivory);
  backdrop-filter: blur(6px);
}
.chapter-rail__tick {
  width: 1.125rem; height: 1px; background: currentColor; opacity: 0.5;
  transition: width var(--dur) var(--ease-out), opacity var(--dur-fast);
}
.chapter-rail__link:hover .chapter-rail__label,
.chapter-rail__link:focus-visible .chapter-rail__label { opacity: 1; transform: none; }
.chapter-rail__link:hover .chapter-rail__tick { opacity: 1; width: 1.75rem; }
.chapter-rail__link[aria-current="true"] { color: var(--champagne); }
.chapter-rail__link[aria-current="true"] .chapter-rail__tick { width: 2.25rem; opacity: 1; }
.chapter-rail__link[aria-current="true"] .chapter-rail__label { opacity: 0; }
.chapter-rail:hover .chapter-rail__link[aria-current="true"] .chapter-rail__label,
.chapter-rail__link:hover .chapter-rail__label,
.chapter-rail__link:focus-visible .chapter-rail__label { opacity: 1; transform: none; }
.chapter-rail[data-ground="light"] .chapter-rail__link { color: var(--muted); }

@media (max-width: 62rem) { .chapter-rail { display: none; } }

/* --- footer -------------------------------------------------------------- */
.site-footer {
  background: var(--navy-700); color: var(--mist-300);
  padding-block: var(--s11) var(--s7);
}
.site-footer__tagline {
  font-family: var(--serif); font-weight: 300;
  font-size: var(--d3); line-height: 1.22; color: var(--ivory);
  max-width: 24ch;
}
.site-footer__grid {
  display: grid; gap: var(--s9) var(--s7);
  grid-template-columns: minmax(0, 1.4fr) repeat(auto-fit, minmax(9rem, 1fr));
  padding-block: var(--s9);
  border-bottom: 1px solid color-mix(in srgb, var(--mist-200) 16%, transparent);
}
.site-footer__col-head {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--mist-400); margin-bottom: var(--s4);
}
.site-footer__links { display: grid; }
.site-footer__links a {
  display: inline-flex; align-items: center; min-height: 2.75rem;
  font-size: var(--t-small); text-decoration: none; color: var(--mist-200);
  transition: color var(--dur-fast);
}
.site-footer__links a:hover { color: var(--champagne); }
.site-footer__base {
  display: flex; flex-wrap: wrap; gap: var(--s4) var(--s7);
  align-items: baseline; justify-content: space-between;
  padding-top: var(--s6);
  font-size: var(--t-micro); color: var(--mist-400);
}
.site-footer__boundary { max-width: 46ch; }

/* --- the language offer ---------------------------------------------------
   Shown to a visitor whose browser prefers the other edition. It sits at the
   bottom of the viewport so it never covers a headline, slides rather than
   appears, and is removed from the document once dismissed so it leaves
   nothing behind in the tab order. */
.lang-offer {
  position: fixed; z-index: calc(var(--z-header) + 1);
  left: var(--gutter); bottom: var(--s5);
  max-width: min(28rem, calc(100% - var(--gutter) * 2));
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3) var(--s5);
  padding: var(--s4) var(--s5);
  background: color-mix(in srgb, var(--navy-700) 94%, transparent);
  backdrop-filter: blur(14px);
  border: 1px solid color-mix(in srgb, var(--champagne) 30%, transparent);
  color: var(--mist-200);
}
.lang-offer[hidden] { display: none; }

/* The resting state is *visible*. An element parked off-screen that only
   arrives via a transition is unusable wherever that transition does not
   run — a backgrounded tab pauses compositor animations, which is precisely
   when someone comes back to the page and needs to see this. The motion is an
   animation played over a correct resting state, not the thing that creates
   it. */
@keyframes offer-in {
  from { transform: translateY(calc(100% + 2.5rem)); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
@keyframes offer-out {
  to { transform: translateY(calc(100% + 2.5rem)); opacity: 0; }
}
.lang-offer[data-in="true"]  { animation: offer-in 520ms var(--ease-out); }
.lang-offer[data-out="true"] { animation: offer-out 320ms var(--ease-soft) forwards; }
.lang-offer__text { flex: 1 1 14rem; font-size: var(--t-small); line-height: 1.45; }
.lang-offer__cta {
  display: inline-flex; align-items: center; min-height: 2.75rem;
  font-size: var(--t-small); font-weight: 500;
  color: var(--champagne); text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--champagne) 50%, transparent);
}
.lang-offer__cta:hover { border-bottom-color: var(--champagne); }
.lang-offer__close {
  display: inline-flex; align-items: center; min-height: 2.75rem;
  font-size: var(--t-micro); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--mist-400);
}
.lang-offer__close:hover { color: var(--mist-200); }

@media (prefers-reduced-motion: reduce) {
  .lang-offer[data-in="true"], .lang-offer[data-out="true"] { animation: none; }
}
@media (max-width: 48rem) {
  .lang-offer { left: var(--gutter); right: var(--gutter); max-width: none; bottom: var(--s4); }
}

/* The full lockup, once, where there is room for it to be read: ivory and
   gold over the navy footer. The header carries the monogram alone because
   22 to 30 pixels is no place for a three-part lockup. */
.site-footer__logo {
  display: block; width: min(15rem, 100%); height: auto;
  margin-bottom: var(--s5);
}

/* ---- 04-approach.css ---- */
/* ===========================================================================
   The approach — Act I and the threshold
   ---------------------------------------------------------------------------
   Structure: a tall section whose first child is sticky. Normal page scroll
   drives the camera; nothing is hijacked, the scrollbar stays truthful, and
   reverse scrolling is exact because position is derived from scrollY rather
   than accumulated from events.
   ========================================================================== */

.approach {
  position: relative;
  /* Scroll distance of Act I. Each viewport height is roughly one beat. */
  height: var(--approach-length, 440vh);
  background: var(--navy-900);
}

.approach__stage {
  position: sticky; top: 0;
  height: 100svh;
  overflow: clip;
  isolation: isolate;
}

/* --- the canvas ----------------------------------------------------------
   Behind everything. If WebGL is unavailable, or motion is reduced, the
   canvas never initialises and .approach__plate below carries the scene. */
.scene-canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  z-index: var(--z-canvas);
  opacity: 0;
  transition: opacity 1.2s var(--ease-soft);
}
.scene-canvas[data-ready="true"] { opacity: 1; }

/* --- the still fallback --------------------------------------------------
   Always in the markup. It is what search engines, printers, no-WebGL
   browsers and reduced-motion visitors get, and it is also the first paint
   while the canvas warms up, so the hero is never empty.

   It uses the landscape aerial rather than the zenithal plate: a portrait
   photograph cropped to a landscape window can only ever show a band of the
   hull, whereas this one opens on the same composition the camera does. */
.approach__plate {
  position: absolute; inset: 0;
  z-index: 1;
  background: var(--sea);
  transition: opacity 1.2s var(--ease-soft);
}
.approach__plate img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: 62% 50%;
}
/* The portrait crop is the zenithal photograph; centre it on the hull. */
@media (max-aspect-ratio: 4 / 5) {
  .approach__plate img { object-position: 64% 46%; }
}
/* Enough to hold the headline, not so much that it buries the photograph. */
.approach__plate::after {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(120% 96% at 62% 46%, transparent 34%,
      color-mix(in srgb, var(--navy-900) 46%, transparent) 100%),
    linear-gradient(100deg,
      color-mix(in srgb, var(--navy-900) 78%, transparent) 0%,
      color-mix(in srgb, var(--navy-900) 38%, transparent) 42%,
      transparent 70%),
    linear-gradient(to bottom,
      color-mix(in srgb, var(--navy-900) 34%, transparent) 0%,
      transparent 22%, transparent 62%,
      color-mix(in srgb, var(--navy-900) 60%, transparent) 100%);
}
.approach[data-canvas="on"] .approach__plate { opacity: 0; }

/* --- through the door ----------------------------------------------------
   The last beat of the descent. The camera has reached the deck; rather than
   letting the next section slide up over it with a visible edge, the
   threshold frame arrives here — scaling down from beyond the frame as the
   water fades — so the movement reads as passing through rather than cutting.
   The voyage below opens on the same photograph at rest, so the handover
   has nothing to hide. */
.approach__inside {
  position: absolute; inset: 0; z-index: 2;
  opacity: var(--inside, 0);
  pointer-events: none;
}
.approach__inside picture { display: contents; }
.approach__inside img {
  width: 100%; height: 100%; object-fit: cover;
  transform: scale(calc(1.22 - var(--inside, 0) * 0.22));
  transform-origin: 56% 50%;
}
.approach[data-canvas="off"] .approach__inside { display: none; }

@media (prefers-reduced-motion: reduce) {
  .approach__inside { display: none; }
}

/* --- the copy ------------------------------------------------------------- */
.approach__copy {
  position: relative; z-index: 3;
  height: 100%;
  display: grid; align-content: space-between;
  padding-block: calc(var(--header-h) + var(--s7)) var(--s7);
  color: var(--ivory);
}

/* No measure of its own: a rem cap here is a second, invisible constraint on
   the headline, and at 1500px it was the one that won — breaking "The yard
   takes care" after "takes". The headline caps itself in em (so the limit
   tracks its own size) and the standfirst caps itself in ch. */
.approach__head { max-width: 100%; }
.approach__eyebrow {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--champagne);
}
.approach__title {
  font-family: var(--serif); font-weight: 300;
  /* Also bounded by viewport height: four lines of display type must still
     leave room for the standfirst and the scroll cue on a short window. */
  font-size: min(var(--d1), 10.5svh);
  max-width: 11em;
  line-height: 1.04;
  letter-spacing: var(--track-display);
  color: var(--ivory);
  margin-top: var(--s5);
  text-shadow: 0 2px 40px color-mix(in srgb, var(--navy-900) 60%, transparent);
}
.approach__title em { font-style: italic; color: color-mix(in srgb, var(--ivory) 92%, var(--champagne)); }
.approach__title span { display: block; }
.approach__standfirst {
  margin-top: var(--s5);
  font-size: var(--t-lede); line-height: 1.55;
  color: color-mix(in srgb, var(--ivory) 80%, transparent);
  max-width: 38ch;
}

/* Act I's head and foot fade out as the camera closes in, so the yacht is
   never buried under type at the moment it fills the frame. */
.approach__head, .approach__foot {
  opacity: var(--copy-opacity, 1);
  transform: translateY(calc(var(--copy-lift, 0) * -1.5rem));
  transition: opacity 500ms var(--ease-soft);
}
/* Once the title has cleared the frame it is taken out of the page entirely:
   a ghost at 7% opacity still reads as a smudge behind the beats, and a link
   nobody can see should not be in the tab order. */
.approach__stage[data-copy="out"] .approach__head,
.approach__stage[data-copy="out"] .approach__foot { visibility: hidden; }

.approach__foot {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  justify-content: space-between; gap: var(--s5);
}
.approach__contexts {
  font-size: var(--t-micro); letter-spacing: 0.2em; text-transform: uppercase;
  color: color-mix(in srgb, var(--ivory) 64%, transparent);
}

.scroll-cue {
  display: inline-flex; align-items: center; gap: var(--s3);
  min-height: 2.75rem;
  font-size: var(--t-micro); letter-spacing: 0.18em; text-transform: uppercase;
  color: color-mix(in srgb, var(--ivory) 70%, transparent);
  text-decoration: none;
}
.scroll-cue__rule {
  position: relative; display: block;
  width: 2.75rem; height: 1px;
  background: color-mix(in srgb, var(--ivory) 30%, transparent);
  overflow: hidden;
}
.scroll-cue__rule::after {
  content: ""; position: absolute; inset: 0;
  background: var(--champagne);
  transform: translateX(-100%);
  animation: cue-sweep 2.8s var(--ease-soft) infinite;
}
@keyframes cue-sweep {
  0% { transform: translateX(-100%); }
  55%, 100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .scroll-cue__rule::after { animation: none; transform: none; opacity: 0.6; }
}

/* --- the beats -----------------------------------------------------------
   Short lines that surface at fixed points of the descent. They sit on the
   opposite side from the hull so they never cover it. */
.approach__beats {
  position: absolute; z-index: 3;
  left: var(--gutter); bottom: clamp(7rem, 22vh, 13rem);
  display: grid;
}
.beat {
  grid-area: 1 / 1;
  max-width: 24rem;
  opacity: 0; transform: translateY(0.75rem);
  transition: opacity 700ms var(--ease-soft), transform 700ms var(--ease-out);
  pointer-events: none;
}
.beat[data-active="true"] { opacity: 1; transform: none; }
.beat__kicker {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--champagne);
  margin-bottom: var(--s3);
}
.beat__line {
  max-width: 13em;
  font-family: var(--serif); font-weight: 300; font-style: italic;
  font-size: var(--d3); line-height: 1.22;
  color: var(--ivory);
  text-shadow: 0 2px 30px color-mix(in srgb, var(--navy-900) 70%, transparent);
}

/* With motion reduced there is no camera move for the beats to punctuate, and
   they are stacked in a single grid cell by design. They are decorative —
   aria-hidden, and the threshold section below states the same thing in prose
   — so they are simply not shown. */
@media (prefers-reduced-motion: reduce) {
  .approach__beats, .approach__progress { display: none; }
  .scroll-cue__rule::after { display: none; }
}

/* --- progress ------------------------------------------------------------- */
.approach__progress {
  position: absolute; z-index: 3;
  right: var(--gutter); bottom: var(--s7);
  display: flex; align-items: center; gap: var(--s3);
  font-size: var(--t-micro); letter-spacing: 0.16em;
  color: color-mix(in srgb, var(--ivory) 55%, transparent);
}
.approach__progress-track {
  width: clamp(3rem, 7vw, 5.5rem); height: 1px;
  background: color-mix(in srgb, var(--ivory) 24%, transparent);
}
.approach__progress-fill {
  display: block; height: 100%; width: 100%;
  background: var(--champagne);
  transform: scaleX(var(--progress, 0)); transform-origin: left;
}

@media (max-aspect-ratio: 4 / 5) {
  .approach__plate::after {
    background:
      linear-gradient(to bottom,
        color-mix(in srgb, var(--navy-900) 82%, transparent) 0%,
        color-mix(in srgb, var(--navy-900) 58%, transparent) 46%,
        color-mix(in srgb, var(--navy-900) 30%, transparent) 68%,
        color-mix(in srgb, var(--navy-900) 72%, transparent) 100%);
  }
}

@media (max-width: 48rem) {
  .approach { height: var(--approach-length-sm, 320vh); }
  .approach__beats { bottom: clamp(5.5rem, 18vh, 9rem); }
  .approach__progress { display: none; }
}

/* ---------------------------------------------------------------------------
   The threshold — the moment the journey steps off the yacht
   -------------------------------------------------------------------------- */
.threshold {
  position: relative;
  background: var(--navy-800);
  color: var(--mist-200);
  overflow: clip;
}
.threshold__media { position: absolute; inset: 0; }
.threshold__media img {
  width: 100%; height: 100%; object-fit: cover;
  /* Continues the descent: the deck is still drifting past as the copy
     arrives, so the cut from Act I does not read as a hard stop. */
  transform: scale(var(--threshold-scale, 1.08)) translateY(var(--threshold-shift, 0));
  will-change: transform;
}
/* The deck is a bright, near-white photograph. A flat scrim over it reads as
   grey mud, so the grade keeps the top of the frame open and bright and lets
   the dark gather towards the copy, which is where it is needed. */
.threshold__media img { filter: contrast(1.12) saturate(0.92) brightness(0.86); }
/* The hand-off to the solid section below, so the photograph does not end on
   a visible edge. */
.threshold__media::before {
  content: ""; position: absolute; inset: auto 0 0 0; height: 14rem;
  background: linear-gradient(to bottom, transparent, var(--navy-800));
  z-index: 1;
}
.threshold__inner {
  position: relative; z-index: 1;
  min-height: 100svh;
  display: grid; align-content: end;
  padding-block: var(--s12) var(--section-y);
}
.threshold__title { color: var(--ivory); margin-top: var(--s4); }
.threshold__title em { color: var(--champagne); }
.threshold__body { margin-top: var(--s7); max-width: var(--measure); }
.threshold__body p { color: var(--mist-200); }
.threshold__body p + p { margin-top: var(--s4); }
.threshold__tail {
  margin-top: var(--s6); padding-top: var(--s6);
  border-top: 1px solid color-mix(in srgb, var(--mist-200) 18%, transparent);
  font-family: var(--serif); font-size: var(--d4); font-weight: 300;
  line-height: 1.34; color: var(--ivory); max-width: 40ch;
}

/* ---- 05-blocks.css ---- */
/* ===========================================================================
   Editorial blocks
   ========================================================================== */

.section { padding-block: var(--section-y); background: var(--bg); color: var(--fg); }
.section--alt { background: var(--bg-alt); }
.section--tight { padding-block: clamp(3rem, 1.5rem + 6vw, 6rem); }

/* rem, not ch: this box contains display type set in a different font. */
.section__head { max-width: min(100%, 40rem); }
.section__head .label + .display { margin-top: var(--s4); }
.section__head .display + .lede { margin-top: var(--s5); }

/* --- reveal ---------------------------------------------------------------
   One mechanism for every entrance: a class toggled by IntersectionObserver.
   No per-element timeline, nothing to leak, and it degrades to "visible" when
   JavaScript is off or motion is reduced. */
.reveal {
  opacity: 0;
  transform: translateY(1.25rem);
  transition: opacity 900ms var(--ease-soft), transform 900ms var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
.reveal.is-in { opacity: 1; transform: none; }
.no-js .reveal, html:not(.js) .reveal { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* --- figures --------------------------------------------------------------
   A blur-up placeholder sits behind every photograph, so a slow connection
   never shows an empty rectangle and the layout never shifts. */
.fig { position: relative; margin: 0; min-width: 0; background: var(--mist-100); overflow: clip; }
.fig::before {
  content: ""; position: absolute; inset: 0;
  background-image: var(--lqip);
  background-size: cover; background-position: center;
  filter: blur(18px); transform: scale(1.1);
  transition: opacity 600ms var(--ease-soft);
}
.fig picture { display: contents; }
.fig img {
  position: relative; width: 100%; height: 100%; object-fit: cover;
  /* Centre unless the figure says otherwise. A photograph much taller than
     its box loses most of itself to the crop, and which part it loses is a
     decision, not a default: tools/focus_points.py makes it per image. */
  object-position: 50% var(--focus-y, 50%);
}
.fig[data-loaded="true"]::before { opacity: 0; }
.fig--cool img { filter: saturate(0.42) contrast(1.04); }
.fig__caption {
  margin-top: var(--s3);
  font-size: var(--t-micro); color: var(--fg-dim);
  letter-spacing: 0.04em;
}

/* --- full-bleed scene ----------------------------------------------------- */
.scene {
  position: relative;
  min-height: min(100svh, 46rem);
  display: grid; align-items: end;
  overflow: clip;
  background: var(--navy-800);
  color: var(--mist-200);
}
.scene--short { min-height: min(72svh, 34rem); }
.scene__media { position: absolute; inset: 0; }
.scene__media picture, .gateway__media picture, .threshold__media picture,
.approach__plate picture { display: contents; }
.scene__media img { width: 100%; height: 100%; object-fit: cover; }
.scene--lift .scene__media img { object-position: 62% 34%; }
.scene--cool .scene__media img { filter: saturate(0.4) contrast(1.05); }

/* --- the legibility scrim -------------------------------------------------
   Copy on these sections sits bottom-left over photographs that range from
   near-black water to sunlit teak. Two things matter:

   1. The stops are LENGTHS, not percentages. A percentage scrim lands in a
      different place on a 34rem scene than on a full-height hero, so the same
      rule that protects one leaves the other unreadable. Lengths put the
      darkening where the copy actually is, which is a fixed distance from the
      bottom edge whatever the section's height.
   2. It gathers in the bottom-left corner, where the type is, and releases
      towards the top-right, where the photograph is.

   Verified against the brightest image in the set (sunlit teak) rather than
   assumed from the gradient values. */
.scene__media::after,
.gateway__media::after,
.threshold__media::after {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(to top,
      color-mix(in srgb, var(--navy-900) 90%, transparent) 0rem,
      color-mix(in srgb, var(--navy-900) 80%, transparent) 13rem,
      color-mix(in srgb, var(--navy-900) 46%, transparent) 29rem,
      color-mix(in srgb, var(--navy-900) 20%, transparent) 46rem),
    linear-gradient(to right,
      color-mix(in srgb, var(--navy-900) 60%, transparent) 0rem,
      color-mix(in srgb, var(--navy-900) 28%, transparent) 38rem,
      transparent 68rem);
}

/* The threshold refines the rule above — it must come after it, since both
   have the same specificity and the bundle is concatenated in file order. Its
   copy block is roughly twice a scene's height and sits over the brightest
   photograph in the set, so the dark has to reach further up the frame. */
.threshold__media::after {
  background:
    linear-gradient(to top,
      color-mix(in srgb, var(--navy-900) 92%, transparent) 0rem,
      color-mix(in srgb, var(--navy-900) 86%, transparent) 20rem,
      color-mix(in srgb, var(--navy-900) 54%, transparent) 36rem,
      color-mix(in srgb, var(--navy-900) 20%, transparent) 54rem),
    linear-gradient(to right,
      color-mix(in srgb, var(--navy-900) 64%, transparent) 0rem,
      color-mix(in srgb, var(--navy-900) 30%, transparent) 40rem,
      transparent 72rem);
}

/* These sections are dark ground, so they take the dark token set: it is what
   turns the eyebrow gold from bronze (legible on ivory) to champagne (legible
   on a photograph). */
.scene, .gateway, .threshold {
  --fg: var(--mist-200);
  --fg-strong: var(--ivory);
  --fg-dim: var(--mist-400);
  /* Champagne as printed is a hair under 4.5:1 for an 11px eyebrow over the
     brightest photograph in the set; warmed towards ivory it clears it while
     still reading as the brand's gold. Rules and hairlines, which are not
     text, keep the printed value. */
  --accent: color-mix(in srgb, var(--champagne) 70%, var(--ivory));
  --accent-hair: var(--champagne);
  --numeral: var(--champagne);
  --rule: color-mix(in srgb, var(--mist-200) 18%, transparent);
  --rule-strong: color-mix(in srgb, var(--mist-200) 34%, transparent);
}

/* The copy sits above the media and its scrim; without its own stacking
   position the absolutely positioned scrim paints straight over it. */
.scene__inner { position: relative; z-index: 1; padding-block: var(--s11) var(--s10); }

/* A second, local wash that follows the copy block itself.
   The scrim on the media is anchored to the bottom of the section, which is
   right for a short block and leaves the top of a tall one — the yard page's
   closing scene on a phone, with a headline, a lede, three steps and a button
   — sitting on open photograph. This one is as tall as whatever the block
   turns out to be, so it cannot be out-grown. It is deliberately gentle: the
   media scrim does most of the work and the photograph still reads. */
.scene__inner::before,
.gateway__inner::before,
.threshold__inner::before {
  content: ""; position: absolute; z-index: -1;
  inset: -4rem calc(var(--gutter) * -1) calc(var(--s8) * -1);
  /* It keeps a floor at the top of the block rather than fading to nothing:
     the eyebrow is the first line, and it is the smallest text on the page. */
  background: linear-gradient(to top,
    color-mix(in srgb, var(--navy-900) 44%, transparent) 0%,
    color-mix(in srgb, var(--navy-900) 36%, transparent) 62%,
    color-mix(in srgb, var(--navy-900) 22%, transparent) 92%,
    transparent 100%);
  pointer-events: none;
}
.scene__title { color: var(--ivory); margin-top: var(--s4); }
.scene__title em { color: var(--champagne); }
.scene__lede { margin-top: var(--s5); color: color-mix(in srgb, var(--ivory) 86%, transparent); }
.scene__meta {
  margin-top: var(--s6); padding-top: var(--s5);
  border-top: 1px solid color-mix(in srgb, var(--mist-200) 20%, transparent);
  font-size: var(--t-micro); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--mist-300);
}

/* --- page hero (service pages) -------------------------------------------- */
.page-hero { min-height: min(92svh, 52rem); }
.page-hero .scene__inner { padding-block: calc(var(--header-h) + var(--s11)) var(--s10); }

/* --- service entry --------------------------------------------------------
   The spine of both service pages: a numbered editorial column with an
   optional portrait inset, mirroring the brochure spreads. */
.service {
  display: grid;
  gap: var(--s7) clamp(2rem, 5vw, 5rem);
  grid-template-columns: minmax(0, 1fr);
  padding-block: var(--section-y);
  border-top: 1px solid var(--rule);
}
.service:first-of-type { border-top: 0; }
@media (min-width: 60rem) {
  .service { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); align-items: start; }
  /* Flipping swaps which side the copy sits on, so the track widths swap with
     it — otherwise the body lands in the narrow column and the headline sets
     four words to a line. */
  .service--flip { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
  .service--flip > .service__body { order: 2; }
  .service--flip > .service__aside { order: 1; }
}

.service__index {
  display: flex; align-items: baseline; gap: var(--s4);
  margin-bottom: var(--s5);
}
.service__number {
  font-family: var(--serif); font-weight: 300; font-size: var(--d3);
  color: var(--numeral); line-height: 1;
}
.service__kind {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--fg-dim);
}
.service__headline { margin-bottom: var(--s5); }
.service__lede { margin-bottom: var(--s7); }

.includes { border-top: 1px solid var(--rule); }
.includes__head {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--fg-dim);
  padding-block: var(--s4);
}
.includes__list { display: grid; }
/* Two columns and exactly two children: the ::before rule and the text.
   A third child would fall into the 1.25rem column and set one word a line. */
.includes__item {
  display: grid; grid-template-columns: 0.75rem 1fr; gap: var(--s3);
  padding-block: var(--s4);
  border-top: 1px solid var(--rule);
  font-size: var(--t-small); line-height: 1.6;
}
.includes__item::before {
  content: ""; width: 0.375rem; height: 1px;
  background: var(--accent-hair); margin-top: 0.75em;
}


.delivers {
  /* This panel sits on a tint two shades darker than the page, so the page's
     accent arrives at 3.42:1 on it. Redefined here rather than at each use,
     so anything accent-coloured inside the panel darkens with its ground. */
  --accent: color-mix(in srgb, var(--bronze) 82%, black);

  margin-top: var(--s6); padding: var(--s5);
  background: var(--sand);
  color: var(--navy-700);
}
.delivers__head {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  /* --accent, not --bronze: the panel redefines the accent for its own
     darker ground above, and a direct reference walks straight past it. */
  text-transform: uppercase; color: var(--accent); margin-bottom: var(--s3);
}
.delivers__text { font-family: var(--serif); font-size: var(--d4); line-height: 1.3; font-weight: 300; }

.service__aside { display: grid; gap: var(--s6); align-content: start; }
.service__inset { aspect-ratio: 3 / 4; }
.service__quote { padding-left: var(--s5); border-left: 1px solid var(--accent-hair); }

/* --- blocks (the execution service's three sub-blocks) --------------------- */
.subblocks { display: grid; gap: var(--s6); margin-top: var(--s6); }
.subblock { padding-top: var(--s5); border-top: 1px solid var(--rule); }
.subblock__label {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--fg-dim); margin-bottom: var(--s3);
}
.subblock__head { font-family: var(--serif); font-weight: 300; font-size: var(--d4); color: var(--fg-strong); }
.subblock__text { margin-top: var(--s3); font-size: var(--t-small); color: var(--fg); max-width: var(--measure-narrow); }

/* --- group header (Arrive / Stay / Visit, phases I–V) ---------------------- */
.group-head {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s4) var(--s6);
  padding-top: var(--section-y); padding-bottom: var(--s5);
  border-bottom: 1px solid var(--rule-strong);
}
.group-head__key {
  font-family: var(--serif); font-weight: 300; font-size: var(--d3);
  color: var(--numeral); line-height: 1;
}
.group-head__label { font-family: var(--serif); font-weight: 300; font-size: var(--d3); color: var(--fg-strong); line-height: 1; }
.group-head__note { font-size: var(--t-small); color: var(--fg-dim); margin-left: auto; max-width: 38ch; }

/* --- columns (the perimeter table) ---------------------------------------- */
.columns { display: grid; gap: var(--s8); margin-top: var(--s9); }
@media (min-width: 58rem) { .columns { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s7); } }
.column { padding-top: var(--s5); border-top: 2px solid var(--fg-strong); }
.column:nth-child(2) { border-top-color: var(--muted); }
.column:nth-child(3) { border-top-color: var(--accent-hair); }
.column__head {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--fg-strong);
}
.column__sub {
  font-family: var(--serif); font-weight: 300; font-size: var(--d4);
  line-height: 1.22; color: var(--fg-strong); margin-top: var(--s3);
  max-width: 16ch;
}
.column__list { margin-top: var(--s5); }
.column__item {
  padding-block: var(--s3); border-top: 1px solid var(--rule);
  font-size: var(--t-small); line-height: 1.55;
}

/* --- deliverables table ---------------------------------------------------- */
.table-wrap { margin-top: var(--s8); }
.dl-table { width: 100%; border-collapse: collapse; text-align: left; }
.dl-table th {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; font-weight: 500; color: var(--fg-dim);
  padding-bottom: var(--s4); border-bottom: 1px solid var(--rule-strong);
}
.dl-table td { padding-block: var(--s4); border-bottom: 1px solid var(--rule); vertical-align: baseline; }
.dl-table td:first-child { font-family: var(--serif); font-size: var(--d4); font-weight: 300; color: var(--fg-strong); padding-right: var(--s5); }
.dl-table td:last-child { font-size: var(--t-small); color: var(--fg-dim); white-space: normal; }
@media (max-width: 42rem) {
  .dl-table, .dl-table tbody, .dl-table tr, .dl-table td { display: block; width: 100%; }
  .dl-table thead { display: none; }
  .dl-table tr { padding-block: var(--s4); border-bottom: 1px solid var(--rule); }
  .dl-table td { border: 0; padding-block: 0; }
  .dl-table td:last-child { margin-top: var(--s2); }
}

/* --- numbered cards (commitments, steps) ----------------------------------- */
.cards { display: grid; gap: var(--s7); margin-top: var(--s9); }
@media (min-width: 48rem) { .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 72rem) { .cards--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } .cards--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.card { padding-top: var(--s5); border-top: 1px solid var(--rule-strong); }
.card__n {
  font-family: var(--serif); font-weight: 300;
  /* Never below 24px. --d4 scales with the viewport and lands at 20 on a
     phone, which drops the numeral under the size where 3:1 is enough and
     puts it against the 4.5 it cannot meet in gold. The floor keeps it on
     the right side of that line rather than repainting it. */
  font-size: max(1.5rem, var(--d4));
  color: var(--numeral); line-height: 1;
}
.card__label { font-family: var(--serif); font-weight: 300; font-size: var(--d4); color: var(--fg-strong); margin-top: var(--s4); line-height: 1.2; }
.card__text { margin-top: var(--s3); font-size: var(--t-small); color: var(--fg); }

/* --- audience cards (with imagery) ----------------------------------------- */
.audience { display: grid; gap: var(--s4); }
.audience__fig { aspect-ratio: 4 / 3; }
.audience__label { font-family: var(--serif); font-weight: 300; font-size: var(--d4); color: var(--fg-strong); margin-top: var(--s2); }
.audience__text { font-size: var(--t-small); color: var(--fg); }

/* --- gateway (home -> service world) ---------------------------------------
   A real link covering the whole card, not a decorative tile with a button. */
.gateways { display: grid; gap: var(--s6); margin-top: var(--s9); }
@media (min-width: 56rem) { .gateways { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.gateway {
  position: relative; display: grid; align-items: end;
  min-height: min(78svh, 34rem);
  overflow: clip;
  color: var(--ivory);
  text-decoration: none;
  background: var(--navy-800);
}
.gateway__media { position: absolute; inset: 0; }
.gateway__media img {
  width: 100%; height: 100%; object-fit: cover;
  transform: scale(1.01);
  transition: transform 1.4s var(--ease-out), filter 1.4s var(--ease-out);
}
.gateway--cool .gateway__media img { filter: saturate(0.4) contrast(1.05); }
.gateway:hover .gateway__media img { transform: scale(1.055); }
.gateway--cool:hover .gateway__media img { filter: saturate(0.55) contrast(1.05); }
.gateway__inner { position: relative; z-index: 1; padding: var(--s7); display: grid; gap: var(--s4); }
.gateway__index { font-size: var(--t-micro); letter-spacing: 0.2em; color: var(--champagne); }
.gateway__label { font-family: var(--serif); font-weight: 300; font-size: var(--d2); line-height: 1.06; }
.gateway__kicker { font-size: var(--t-micro); letter-spacing: 0.14em; text-transform: uppercase; color: color-mix(in srgb, var(--ivory) 66%, transparent); }
.gateway__text { font-size: var(--t-small); color: color-mix(in srgb, var(--ivory) 84%, transparent); max-width: 34ch; }

/* --- link affordances ------------------------------------------------------ */
.action {
  display: inline-flex; align-items: center; gap: var(--s3);
  font-size: var(--t-small); letter-spacing: 0.06em;
  text-decoration: none; color: inherit;
  padding-block: var(--s2);
  /* Every action on the site measured 40px tall on a phone — under a finger
     by four. They are all standalone, never inside a run of prose, so the
     height can be set here once instead of being patched one block at a
     time. */
  min-height: 2.75rem;
}
.action__rule {
  display: block; width: 1.75rem; height: 1px; background: currentColor;
  opacity: 0.6; transition: width var(--dur) var(--ease-out), opacity var(--dur-fast);
}
.action:hover .action__rule, .gateway:hover .action__rule { width: 2.75rem; opacity: 1; }

.button {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s3);
  padding: 0.95rem 1.75rem;
  font-size: var(--t-small); font-weight: 500; letter-spacing: 0.06em;
  text-decoration: none;
  background: var(--navy-700); color: var(--ivory);
  border: 1px solid var(--navy-700);
  transition: background-color var(--dur) var(--ease-soft), color var(--dur) var(--ease-soft);
}
.button:hover { background: transparent; color: var(--navy-700); }
.on-dark .button, .scene .button {
  background: var(--ivory); color: var(--navy-700); border-color: var(--ivory);
}
.on-dark .button:hover, .scene .button:hover { background: transparent; color: var(--ivory); }
.button[disabled] { opacity: 0.55; cursor: not-allowed; }

/* --- figures row (statistics) ---------------------------------------------- */
.figures { display: grid; gap: var(--s6); margin-top: var(--s9); }
@media (min-width: 40rem) { .figures { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .figures { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.figure-stat { padding-top: var(--s4); border-top: 1px solid var(--rule-strong); }
.figure-stat__value { font-family: var(--serif); font-weight: 300; font-size: var(--d2); line-height: 1; color: var(--fg-strong); }
.figure-stat__unit { margin-top: var(--s3); font-size: var(--t-small); color: var(--fg); }
.figure-stat__note { font-size: var(--t-micro); color: var(--fg-dim); margin-top: var(--s1); }

/* --- pending marker --------------------------------------------------------
   Used where the brochures leave a field blank. It is visible on purpose:
   nothing should look finished that is not. */
.pending {
  display: inline-flex; align-items: center; gap: var(--s3);
  font-size: var(--t-small); color: var(--fg-dim);
  padding: var(--s4) var(--s5);
  border: 1px dashed var(--rule-strong);
}

/* The way back to the room this service was first met in. Quiet: it is an
   aside to the reading, not a call to action. */
.service__aboard { margin-top: var(--s5); }
.service__aboard .action { font-size: var(--t-small); color: var(--muted); }
.service__aboard .action:hover,
.service__aboard .action:focus-visible { color: var(--fg); }

/* --- the service index ------------------------------------------------------
   Twelve services, named, linked, readable in fifteen seconds. It sits under
   the two gateways: the tiles are the invitation, this is the answer to
   "what exactly do you do", which every serious house in this trade puts
   within one screen of the top. */
.service-index {
  margin-top: var(--s9);
  padding-top: var(--s7);
  border-top: 1px solid var(--rule);
  display: grid; gap: var(--s7);
}
@media (min-width: 56rem) {
  .service-index { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--s8); }
  .service-index__title { grid-column: 1 / -1; }
}
.service-index__title {
  font-size: var(--t-micro); letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--muted); margin: 0;
}
.service-index__head {
  font-family: var(--serif); font-weight: 300; font-size: var(--d4);
  line-height: 1.2; margin: 0 0 var(--s4);
}
.service-index__head a {
  /* A link has to be at least 24px to hit, and a finger wants 44. As a bare
     line of text this one measured 20px on a phone; padding alone took it
     to 40, so the height is stated. */
  display: inline-flex; align-items: center; min-height: 2.75rem;
  padding-block: var(--s2);
  color: inherit; text-decoration: none;
}
.service-index__head a:hover,
.service-index__head a:focus-visible { color: var(--accent); }
.service-index__list { list-style: none; margin: 0; padding: 0; }
.service-index__item + .service-index__item { border-top: 1px solid var(--rule); }
.service-index__item a {
  display: grid; grid-template-columns: 2.4rem 1fr; align-items: baseline;
  gap: var(--s3);
  /* A comfortable row to hit on a phone, and a reading rhythm on a desktop. */
  padding-block: var(--s4);
  text-decoration: none; color: var(--fg);
}
.service-index__item a:hover .service-index__name,
.service-index__item a:focus-visible .service-index__name {
  text-decoration: underline; text-underline-offset: 0.25em;
}
.service-index__n {
  /* 12px is small text and answers to 4.5:1, which --numeral does not reach.
     The same decision as .jump-list__n: a number meant to be read takes the
     light ground's own accent. */
  font-size: var(--t-micro); letter-spacing: 0.12em; color: var(--accent);
}
.service-index__name { font-size: var(--t-small); }

/* The brochure's own mark on a service. Core is the house's own emphasis;
   shared says the crew side and the yard side draw on one arrangement. */
.badge {
  margin: 0 0 var(--s3);
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase;
}
.badge--core { color: var(--accent); }
.badge--shared { color: var(--muted); }
.badge span { display: inline-flex; align-items: center; gap: var(--s2); }
.badge span::before {
  content: ""; width: 0.3rem; height: 0.3rem; border-radius: 50%;
  background: currentColor; flex: 0 0 auto;
}

/* The shared operating core, between the two gateways and the service index:
   the sentence that makes them one company rather than two. */
.core { margin-top: var(--s9); max-width: 46rem; }
.core__title { margin-top: var(--s4); }
.core__lede { margin-top: var(--s5); }
.core__list {
  list-style: none; margin: var(--s6) 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--s3) var(--s4);
}
.core__item {
  font-size: var(--t-small);
  padding: var(--s2) var(--s4);
  border: 1px solid var(--rule-strong);
  border-radius: 2rem;
  color: var(--fg-strong);
}

/* The way out of a 404: the whole site, in one short list. */
.not-found__links {
  list-style: none; margin: var(--s7) 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--s3) var(--s6);
  font-size: var(--t-small);
}
.not-found__links a {
  /* Bare text links measured 18px tall against the 24px minimum — the same
     fault the language switcher had once each code became its own link. */
  display: inline-flex; align-items: center; min-height: 2.75rem;
  color: inherit;
}

/* --- jump list -------------------------------------------------------------
   Eleven services on one page. A reader looking for the eighth should not
   have to scroll past seven to find out it exists. Set as a quiet run of
   numbered links rather than a boxed menu: it is a table of contents, not a
   navigation bar. */
.jump-list { margin-top: var(--s7); }
.jump-list ul {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
}
.jump-list a {
  display: inline-flex; align-items: baseline; gap: var(--s3);
  /* A comfortable row to hit, and the 24px minimum met with room to spare. */
  padding-block: var(--s3);
  font-size: var(--t-small);
  color: var(--fg); text-decoration: none;
}
.jump-list a:hover span:last-child,
.jump-list a:focus-visible span:last-child {
  text-decoration: underline; text-underline-offset: 0.25em;
}
.jump-list__n {
  /* --champagne is the accent for dark grounds; the jump list sits on a light
     one, where it measured 1.94:1. These numerals are read — they are the
     service's place in the list — so they take the light-ground accent. */
  font-size: var(--t-micro); letter-spacing: 0.12em; color: var(--accent);
}

/* The way back to the first room, offered where the journey ends. */
.section__restart { margin-top: var(--s5); }
.section__restart .action { font-size: var(--t-small); color: var(--muted); }
.section__restart .action:hover,
.section__restart .action:focus-visible { color: var(--fg); }

/* --- what a service commits to -------------------------------------------
   Two columns: the situation, and the time we answer it in. Set quieter than
   the service's own copy — it is reference, read when the reader has decided
   to take the service seriously, not a headline. */
.commits { margin-top: var(--s6); }
.commits__label {
  margin: 0 0 var(--s3);
  font-size: var(--t-micro); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--fg-soft);
}
.commits__table {
  width: 100%; border-collapse: collapse;
  font-size: var(--t-small);
}
.commits__table tr { border-top: 1px solid var(--rule); }
.commits__table tr:last-child { border-bottom: 1px solid var(--rule); }
.commits__table th, .commits__table td {
  padding: var(--s3) 0; text-align: left; vertical-align: baseline;
  font-weight: inherit;
}
.commits__table th { color: var(--fg); padding-right: var(--s5); }
.commits__table td {
  color: var(--accent); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
@media (max-width: 34rem) {
  /* Too narrow for two columns: the time goes under the situation it answers,
     which keeps the pairing without squeezing either into one word a line. */
  .commits__table th, .commits__table td { display: block; padding-bottom: 0; }
  .commits__table td {
    padding-top: var(--s2); padding-bottom: var(--s3);
    /* The column is gone, so there is nothing left to keep on one line — and
       the longest of these ("taken on within 1 hour, resolved the same day")
       is wider than a phone. */
    white-space: normal;
  }
}

/* ---- 06-form.css ---- */
/* ===========================================================================
   Enquiry form and the map
   ========================================================================== */

.enquiry { display: grid; gap: var(--s8); margin-top: var(--s9); }
@media (min-width: 64rem) { .enquiry { grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); gap: var(--s10); } }

.form { display: grid; gap: var(--s8); }
.form fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: var(--s6); }

/* --- the card ------------------------------------------------------------
   The old form sat directly on the section's ground, which made seven inputs
   read as a column of obligations. On its own surface, with a rule and air
   round it, the same seven read as one considered thing to fill in. */
.enquiry__form {
  background: var(--paper, #fff);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: clamp(1.6rem, 4vw, 2.75rem);
  position: relative;
}
/* How long it takes, said before it is asked. Seven fields is two minutes and
   a reader who knows that starts; a reader who does not, guesses. */
.enquiry__time {
  position: absolute; top: 0; right: clamp(1.6rem, 4vw, 2.75rem);
  transform: translateY(-50%);
  margin: 0; padding: 0 var(--s4);
  background: var(--paper, #fff);
  font-size: var(--t-micro); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--fg-dim);
}

/* --- the two steps -------------------------------------------------------
   A numbered head on each, so the form shows its own length: a reader can see
   there are two of them before deciding to begin. */
.step { position: relative; }
.step + .step { margin-top: var(--s7); padding-top: var(--s7);
                border-top: 1px solid var(--rule); }
.step__head { display: grid; grid-template-columns: auto 1fr; gap: 0 var(--s4);
              padding: 0; margin-bottom: var(--s3); width: 100%; }
.step__n {
  grid-row: 1 / 3; align-self: start;
  font-family: var(--serif); font-weight: 300;
  /* 28px, not 20. The bronze on paper measures 3.99:1, which clears the 3:1
     that text of 24px and over has to meet and misses the 4.5:1 below it —
     so the numeral is set above the line rather than repainted a colour the
     brand does not have. It also reads better as a step marker. */
  font-size: 1.75rem; line-height: 1; color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.step__title { font-family: var(--serif); font-weight: 300; font-size: var(--d4);
               line-height: 1; color: var(--fg-strong); }
.step__note { font-size: var(--t-small); color: var(--fg-dim); margin-top: var(--s2); }

.field { display: grid; gap: var(--s2); }
.field__label { font-size: var(--t-label); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--fg-dim); }
.field__req { color: var(--accent); margin-left: 0.25em; }
/* What can be skipped is named; what is needed is not marked at all. Four of
   the seven are required, so starring those puts four marks on the page and
   naming these puts three — and each of these takes work away. */
.field__opt {
  margin-left: 0.5em; text-transform: none; letter-spacing: 0;
  font-size: var(--t-micro); color: var(--fg-dim); opacity: 0.8;
}
.field__hint { font-size: var(--t-micro); color: var(--fg-dim); }

.field input[type="text"], .field input[type="email"], .field select, .field textarea {
  width: 100%;
  padding: 0.85rem 0 0.75rem;
  background: transparent;
  color: var(--fg-strong);
  border: 0; border-bottom: 1px solid var(--rule-strong);
  border-radius: 0;
  font-size: var(--t-body);
  transition: border-color var(--dur-fast) var(--ease-soft);
}
.field textarea { resize: vertical; min-height: 7rem; line-height: 1.6; }
.field select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 0.55rem center, right 0.2rem center;
  background-size: 0.35rem 0.35rem;
  background-repeat: no-repeat;
  padding-right: 1.5rem;
}
.field :is(input, select, textarea):focus { border-bottom-color: var(--accent); outline: none; }
.field :is(input, select, textarea):focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

.field[data-invalid="true"] :is(input, select, textarea) { border-bottom-color: #9d3b34; }
.field__error { font-size: var(--t-micro); color: #9d3b34; min-height: 1em; }
.field__error:empty { display: none; }

.field--check { grid-template-columns: auto 1fr; align-items: start; gap: var(--s3); }
/* 24px: the WCAG 2.2 minimum target. The label is clickable too, but the box
   itself has to be comfortable to hit. */
.field--check input { margin-top: 0.15em; accent-color: var(--bronze); width: 1.5rem; height: 1.5rem; }
.field--check label {
  font-size: var(--t-small); line-height: 1.5; color: var(--fg);
  padding-block: 0.3rem; cursor: pointer;
}
.field--check .field__error { grid-column: 1 / -1; }

/* Honeypot: a real field, visually removed, never announced. A bot that fills
   every input gives itself away without a captcha and without third parties. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form__foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s5); }
/* Beside a button a thumb's width tall, this link measured 24px. */
.form__foot .small {
  display: inline-flex; align-items: center; min-height: 2.75rem;
}
.form__status {
  font-size: var(--t-small);
  padding: var(--s4) var(--s5);
  border-left: 2px solid var(--accent);
  background: var(--bg-alt);
}
.form__status[hidden] { display: none; }
.form__status--error { border-left-color: #9d3b34; }
.form__status-title { font-family: var(--serif); font-size: var(--d4); font-weight: 300; color: var(--fg-strong); display: block; margin-bottom: var(--s2); }
.form__status code { font-family: var(--sans); letter-spacing: 0.08em; color: var(--fg-strong); }

.asks { display: grid; gap: var(--s5); }
.ask { padding-top: var(--s4); border-top: 1px solid var(--rule-strong); }
.ask__label { font-family: var(--serif); font-weight: 300; font-size: var(--d4); color: var(--fg-strong); }
.ask__text { font-size: var(--t-small); color: var(--fg-dim); margin-top: var(--s2); }

.contact-details { margin-top: var(--s7); display: grid; gap: var(--s4); }
.contact-detail__label { font-size: var(--t-label); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--fg-dim); }
.contact-detail__value { font-size: var(--t-lede); color: var(--fg-strong); text-decoration: none; }
.contact-detail__value:hover { color: var(--accent); }

/* ---------------------------------------------------------------------------
   The map — inline SVG, drawn from the distances the brochures publish.
   -------------------------------------------------------------------------- */
.map { margin-top: var(--s9); display: grid; gap: var(--s7); }
@media (min-width: 64rem) { .map { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); align-items: center; gap: var(--s10); } }
.map__figure { background: var(--navy-700); padding: clamp(1rem, 3vw, 2.5rem); }
.map__svg { width: 100%; height: auto; }
.map__list { display: grid; gap: var(--s4); }
.map__row {
  display: grid; grid-template-columns: auto 1fr auto; gap: var(--s4); align-items: baseline;
  padding-bottom: var(--s3); border-bottom: 1px solid var(--rule);
}
.map__code { font-size: var(--t-micro); letter-spacing: 0.16em; color: var(--accent); }
.map__name { font-size: var(--t-small); color: var(--fg); }
.map__km { font-family: var(--serif); font-size: var(--d4); font-weight: 300; color: var(--fg-strong); }
.map__note { font-size: var(--t-micro); color: var(--fg-dim); margin-top: var(--s2); }

/* --- gallery --------------------------------------------------------------- */
.gallery { display: grid; gap: var(--s6); margin-top: var(--s9); }
@media (min-width: 48rem) { .gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.gallery .fig { aspect-ratio: 4 / 5; }

/* --- legal prose ----------------------------------------------------------- */
.legal { max-width: var(--measure); }
.legal section + section { margin-top: var(--s8); }
.legal h2 { font-family: var(--serif); font-weight: 300; font-size: var(--d3); color: var(--fg-strong); margin-bottom: var(--s4); }
.legal p { max-width: var(--measure); }

/* --- the first question, as cards ----------------------------------------
   The two sides of the business are the first thing the form asks about, and
   a select hides them behind a click. Drawn out, the choice is made in one
   press and the reader is reminded what is on offer while making it. */
.field--choice { display: grid; gap: var(--s3); }
.choices { display: grid; gap: var(--s3); }
@media (min-width: 40rem) {
  .choices { grid-template-columns: 1fr 1fr; }
  /* The two short ones share the last row rather than each taking a full
     line: they are alternatives, not services. */
  .choice--plain { grid-column: span 1; }
}
.choice {
  display: block; position: relative; cursor: pointer;
  padding: var(--s5) var(--s5) var(--s5) calc(var(--s5) + 1.75rem);
  border: 1px solid var(--rule-strong); border-radius: 3px;
  background: transparent;
  transition: border-color var(--dur-fast) var(--ease-soft),
              background-color var(--dur-fast) var(--ease-soft);
}
.choice--plain { padding-block: var(--s4); }
.choice:hover { border-color: var(--fg-dim); }
.choice input {
  position: absolute; left: var(--s5); top: calc(var(--s5) + 0.15em);
  width: 1.05rem; height: 1.05rem; margin: 0; accent-color: var(--bronze);
}
.choice--plain input { top: calc(var(--s4) + 0.15em); }
.choice__body { display: grid; gap: var(--s2); }
.choice__name { font-size: var(--t-body); color: var(--fg-strong); }
.choice__note { font-size: var(--t-micro); color: var(--fg-dim); line-height: 1.5; }
/* :has is what lets the whole card show the choice rather than the dot alone.
   Without it the card is still usable — the radio is a real radio — so the
   rule is an enhancement and not a dependency. */
.choice:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}
.choice:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- the close ----------------------------------------------------------- */
.form__close { display: grid; gap: var(--s6); padding-top: var(--s6);
               border-top: 1px solid var(--rule); }
.button--send { padding-inline: var(--s8); }

/* --- what the reader wants to know before writing ------------------------ */
.assure { display: grid; gap: var(--s6); margin-top: var(--s5); }
.assure__item { padding-left: var(--s5); border-left: 1px solid var(--accent); }
.assure__label { font-size: var(--t-body); color: var(--fg-strong); }
.assure__text { font-size: var(--t-small); color: var(--fg-dim); margin-top: var(--s2);
                line-height: 1.6; }
.assure__places { margin-top: var(--s8); padding-top: var(--s5);
                  border-top: 1px solid var(--rule); }
/* Only the label above them, not the cities themselves: the first version of
   this selector caught both and set the three offices in small uppercase, so
   the one line that should carry weight read as a caption. */
.assure__places .assure__label {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--fg-dim);
}
.assure__cities {
  margin-top: var(--s3);
  font-family: var(--serif); font-weight: 300; font-size: var(--d4);
  line-height: 1.3; color: var(--fg-strong);
  text-transform: none; letter-spacing: 0;
}

/* ---- 07-voyage.css ---- */
/* ===========================================================================
   The voyage — moving through the vessel, one room per scroll
   ---------------------------------------------------------------------------
   A sticky backdrop of rooms with the copy scrolling over it. The copy stays
   in normal document flow, so reading order, keyboard navigation and search
   engines get the sequence as written; only the photograph behind it changes.

   Five editorial treatments, chosen per room in the content rather than
   applied uniformly. Fourteen identical left-hand cards read as a template and
   flatten the depth of the photographs, which is the whole point of them.
   ========================================================================== */

.voyage {
  position: relative;
  background: var(--navy-900);
  --fg: var(--mist-200);
  --fg-strong: var(--ivory);
  --fg-dim: var(--mist-400);
  --accent: color-mix(in srgb, var(--champagne) 70%, var(--ivory));
  --accent-hair: var(--champagne);
  --numeral: var(--champagne);
  --rule: color-mix(in srgb, var(--mist-200) 20%, transparent);
  --rule-strong: color-mix(in srgb, var(--mist-200) 36%, transparent);
  color: var(--fg);
}

/* --- the rooms ----------------------------------------------------------- */
.voyage__backdrop { position: sticky; top: 0; height: 100svh; overflow: clip; z-index: 0; }
.voyage__rooms { position: absolute; inset: 0; }

.voyage-room {
  position: absolute; inset: 0; margin: 0;
  opacity: 0;
  /* No `will-change` on all fourteen: that promotes fourteen full-viewport
     compositor layers and the renderer buckles. Only the live room needs one. */
}
.voyage-room[data-active="true"] { opacity: 1; will-change: opacity; }

/* The crossfade is enabled only once an animation frame has actually run.
   A transition declared unconditionally never completes in a tab the browser
   is not painting: the arriving room stays at zero and the one before it
   stays at one, so the photograph shows a room the copy is not talking
   about. Measured: active room opacity 0, a stale room at 1. With the
   transition gated, the resting state is right first and the fade is added
   when there is a frame to run it in. */
.voyage[data-fade="true"] .voyage-room {
  transition: opacity 900ms var(--ease-soft);
}

.voyage-room picture { display: contents; }

.voyage-room img, .voyage-step__spots {
  /* Both read the same transform, published on .voyage by the scroll
     handler, so a mark stays on the feature it points at while the room
     moves under the pointer. */
  /* Depth comes from two movements that are not the same movement: a slow
     push driven by scroll, and a small pointer-driven drift. Composed here so
     one transform carries both and the browser only has one thing to animate.
     Both default to a resting state that is already correct. */
  transform:
    translate3d(var(--pan-x, 0px), calc(var(--pan-y, 0px) + var(--drift, 0px)), 0)
    scale(var(--room-scale, 1.04));
  transform-origin: calc(var(--fx, 0.5) * 100%) calc(var(--fy, 0.5) * 100%);
}
.voyage-room img { width: 100%; height: 100%; object-fit: cover; }

.voyage__vignette {
  position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background:
    radial-gradient(128% 94% at calc(var(--vx, 0.5) * 100%) 42%, transparent 36%,
      color-mix(in srgb, var(--navy-900) 60%, transparent) 100%),
    linear-gradient(to top,
      color-mix(in srgb, var(--navy-900) 70%, transparent) 0rem,
      color-mix(in srgb, var(--navy-900) 34%, transparent) 16rem,
      transparent 36rem);
  transition: background-position 600ms var(--ease-soft);
}

/* --- the marks ------------------------------------------------------------
   Real buttons on real features of the room. They live in the step, after the
   copy they belong to, and are pinned to the frame by `sticky` so they track
   the room rather than scrolling with the text.

   Only the step being read puts its marks in the tab order: `visibility`
   takes the rest out of it and out of the accessibility tree, which is what
   keeps a keyboard from meeting twenty-six buttons for rooms it cannot see. */
.voyage-step { position: relative; }
/* Both children share one grid cell, so the marks overlay the step instead of
   being laid out after the copy — and `sticky` then pins them to the frame
   for as long as the step is passing, exactly as the room is pinned. */
.voyage-step > .shell { grid-area: 1 / 1; }
/* Fixed, not sticky. The backdrop is pinned to the frame, so the marks have
   to be pinned to the same thing or they drift off the features they point
   at; sticky inside a step that is itself taller than the window lands a few
   pixels out, and a few pixels is a mark sitting beside its subject instead
   of on it. Only the live step has a layer visible, so only ever one exists. */
.voyage-step__spots {
  grid-area: 1 / 1;
  position: fixed; inset: 0; z-index: 3;
  pointer-events: none;
  visibility: hidden;
}
/* The marks are fixed to the viewport, so "the step is the live one" is not
   enough: once the journey scrolls away the last step stays marked and its
   marks were painted over the page above — measured, on the opening
   headline. They also need the vessel itself to be what is on screen. */
.voyage[data-aboard="true"] .voyage-step[data-active="true"] .voyage-step__spots {
  visibility: visible;
}
.voyage-step__spots .spot { pointer-events: auto; }

.spot {
  position: absolute; left: var(--x); top: var(--y);
  width: 0; height: 0;
}
.spot__dot {
  position: absolute; left: -1.375rem; top: -1.375rem;
  width: 2.75rem; height: 2.75rem;        /* the target; the ring is smaller */
  display: grid; place-items: center;
  border-radius: 50%;
}
.spot__dot::before {
  content: ""; width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: var(--champagne);
  box-shadow: 0 0 0 0.3rem color-mix(in srgb, var(--navy-900) 52%, transparent);
  transition: transform 420ms var(--ease-out);
}
.spot__dot::after {
  content: ""; position: absolute;
  width: 1.75rem; height: 1.75rem; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--champagne) 70%, transparent);
  opacity: 0.55;
  transition: transform 620ms var(--ease-out), opacity 420ms var(--ease-soft);
}
.spot__dot:hover::before, .spot__dot:focus-visible::before { transform: scale(1.25); }
.spot__dot:hover::after, .spot__dot:focus-visible::after { transform: scale(1.3); opacity: 1; }

.spot__note {
  position: absolute; left: 1.5rem; top: -0.5rem;
  width: max-content; max-width: 19rem;
  padding: var(--s4) var(--s5);
  background: color-mix(in srgb, var(--navy-800) 94%, transparent);
  backdrop-filter: blur(16px);
  border: 1px solid color-mix(in srgb, var(--champagne) 28%, transparent);
  opacity: 0; visibility: hidden; transform: translateY(0.35rem);
  transition: opacity 320ms var(--ease-soft), transform 320ms var(--ease-out),
              visibility 320ms;
  pointer-events: none;
}
/* Opens to the left when the mark sits in the right third, so a note never
   runs off the frame. */
.spot[data-flip="true"] .spot__note { left: auto; right: 1.5rem; }
.spot__dot[aria-expanded="true"] + .spot__note,
.spot:hover .spot__note, .spot:focus-within .spot__note {
  opacity: 1; visibility: visible; transform: none;
}
.spot__label {
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--accent);
}
.spot__text {
  margin-top: var(--s2);
  font-size: var(--t-small); line-height: 1.5;
  color: color-mix(in srgb, var(--ivory) 86%, transparent);
}

/* --- the steps ----------------------------------------------------------- */
/* The band's wash is deliberately wider than the shell so it reaches the
   frame edge; clipped here so that bleed cannot become a horizontal scroll.
   `clip` rather than `hidden`: it does not create a scroll container, so the
   sticky backdrop above still sticks. */
.voyage__steps { position: relative; z-index: 2; margin-top: -100svh; overflow-x: clip; }
.voyage-step { min-height: 100svh; display: grid; }
.voyage-step > .shell { padding-block: var(--s9); }

.voyage-copy { position: relative; }

/* panel — a card of known colour, so legibility never depends on the room */
.voyage-step--panel-left { align-items: center; justify-items: start; }
.voyage-step--panel-right { align-items: center; justify-items: end; }
.voyage-copy--panel { max-width: min(34rem, 100%); }
.voyage-copy--right { margin-left: auto; }
.voyage-card {
  padding: clamp(1.5rem, 1rem + 1.6vw, 2.5rem);
  background: color-mix(in srgb, var(--navy-800) 90%, transparent);
  backdrop-filter: blur(18px) saturate(1.1);
  border: 1px solid color-mix(in srgb, var(--champagne) 26%, transparent);
  box-shadow: 0 2rem 5rem -2rem color-mix(in srgb, var(--navy-900) 80%, transparent);
}

/* plate — centred, no card. A wash sized to the text keeps it legible. */
.voyage-step--plate { align-items: end; justify-items: center; }
/* `justify-items` centres the shell, which was already centred; the copy
   inside it needs its own centring or a "centred plate" sits on the left. */
.voyage-copy--plate { max-width: min(46rem, 100%); margin-inline: auto; text-align: center; }
/* Wide and soft, but genuinely dark where the type is.
   Two measured facts shaped these. First: a background at the luminance the
   brightest rooms reach cannot support 4.5:1 with *any* foreground — pure
   white tops out near 2.9:1 — so there is no typographic way round it, the
   photograph has to be darkened. Second: the softer version of these washes
   was measured and fails in eight places, all of them the small type at the
   top of the block, where the gradient had already faded out. So each wash
   covers the whole copy block and feathers outside it, rather than being
   uniformly heavier. Worst measured result across all fourteen rooms: 4.66:1
   where 3 is required, 5.36:1 where 4.5 is. */
.voyage-copy--plate::before {
  content: ""; position: absolute; z-index: -1;
  inset: -9rem -14rem -8rem;
  /* Measured on the corridor, the brightest room this treatment lands on:
     the index number came out at 3.03:1 where 3 is required — a pass by
     three hundredths, which is no margin at all once a different image
     variant or a wider gamut shifts it. The core and the shoulder are
     stronger; the feather is unchanged, so the room is still a room. */
  background: radial-gradient(62% 64% at 50% 54%,
    color-mix(in srgb, var(--navy-900) 95%, transparent) 0%,
    color-mix(in srgb, var(--navy-900) 91%, transparent) 42%,
    color-mix(in srgb, var(--navy-900) 62%, transparent) 72%,
    transparent 100%);
}
.voyage-copy--plate .voyage-card__lede { margin-inline: auto; max-width: 46ch; }
.voyage-copy--plate .display { margin-inline: auto; }

/* band — a wide strip under the subject, for the statements */
.voyage-step--band { align-items: end; }
.voyage-copy--band { width: 100%; padding-top: var(--s7); }
/* A band is a wide, shallow treatment. Left to grow it reached 77% of the
   frame — at which point it is a panel, the composition stops varying and the
   marks have nowhere to sit. The title and lede keep a readable measure and
   the list is dropped; breadth is what this layout is for. */
.voyage-copy--band .voyage-card__title { max-width: 26ch; }
.voyage-copy--band .voyage-card__lede { max-width: 54ch; }
.voyage-copy--band::before {
  content: ""; position: absolute; z-index: -1;
  inset: -6rem calc(var(--gutter) * -2) calc(var(--s9) * -1);
  background: linear-gradient(to top,
    color-mix(in srgb, var(--navy-900) 94%, transparent) 0%,
    color-mix(in srgb, var(--navy-900) 90%, transparent) 64%,
    color-mix(in srgb, var(--navy-900) 70%, transparent) 88%,
    transparent 100%);
}
.voyage-band__cols {
  display: grid; gap: var(--s5) var(--s7); margin-top: var(--s6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
}
.voyage-band__col { padding-top: var(--s4); border-top: 1px solid var(--rule-strong); }
.voyage-band__head {
  font-family: var(--serif); font-weight: 300; font-size: var(--d4);
  line-height: 1.2; color: var(--ivory);
}
.voyage-band__text {
  margin-top: var(--s2); font-size: var(--t-small); line-height: 1.5;
  color: color-mix(in srgb, var(--ivory) 80%, transparent);
}

/* quote — the brochure's own line, given the room */
.voyage-step--quote { align-items: center; justify-items: start; }
.voyage-copy--quote { max-width: min(38rem, 100%); }
.voyage-copy--quote::before {
  content: ""; position: absolute; z-index: -1;
  inset: -7rem -11rem;
  background: radial-gradient(66% 70% at 38% 50%,
    color-mix(in srgb, var(--navy-900) 93%, transparent) 0%,
    color-mix(in srgb, var(--navy-900) 86%, transparent) 46%,
    color-mix(in srgb, var(--navy-900) 52%, transparent) 74%,
    transparent 100%);
}
.voyage-quote p {
  font-family: var(--serif); font-weight: 300; font-style: italic;
  font-size: var(--d2); line-height: 1.22; color: var(--ivory);
  margin-top: var(--s4);
}
.voyage-quote__attrib {
  margin-top: var(--s5); padding-top: var(--s4);
  border-top: 1px solid var(--rule-strong);
  font-size: var(--t-small); letter-spacing: 0.06em; color: var(--accent);
}

/* --- shared copy pieces --------------------------------------------------- */
.voyage-card__index {
  font-family: var(--serif); font-weight: 300; font-size: var(--d3);
  line-height: 1; color: var(--numeral);
}
.voyage-card__eyebrow {
  margin-top: var(--s3);
  font-size: var(--t-label); letter-spacing: var(--track-label);
  text-transform: uppercase; color: var(--accent);
}
.voyage-copy--plate .voyage-card__eyebrow,
.voyage-copy--band .voyage-card__eyebrow { margin-top: 0; }
.voyage-card__title { margin-top: var(--s4); color: var(--ivory); }
.voyage-card__title em { color: var(--champagne); }
.voyage-card__lede {
  margin-top: var(--s4); font-size: var(--t-body); line-height: 1.6;
  color: color-mix(in srgb, var(--ivory) 86%, transparent);
  max-width: 46ch;
}
.voyage-card__points { margin-top: var(--s5); display: grid; }
.voyage-card__points li {
  display: grid; grid-template-columns: 0.75rem 1fr; gap: var(--s3);
  padding-block: var(--s3); border-top: 1px solid var(--rule);
  font-size: var(--t-small); line-height: 1.5;
  color: color-mix(in srgb, var(--ivory) 80%, transparent);
}
.voyage-card__points li::before {
  content: ""; width: 0.375rem; height: 1px;
  background: var(--accent-hair); margin-top: 0.7em;
}
.voyage-card__more { margin-top: var(--s5); }

.voyage-copy__bypass { margin-top: var(--s6); }
.voyage-copy--plate .voyage-copy__bypass .action { color: var(--mist-300); }
/* On a phone this is the only way out of the journey, so it is a target
   for a thumb, not a line of text: measured at 40px, short of the 44 a
   finger needs. */
.voyage-copy__bypass .action {
  display: inline-flex; align-items: center; min-height: 2.75rem;
}

/* How far through the vessel you are. The chapter rail answers this on a wide
   window and is hidden on a narrow one, where a fourteen-room journey most
   needs an answer to "how much more of this is there". One pixel, sitting on
   the header's lower edge, and only while the journey is the thing on screen. */
/* The strip under the header while the vessel is on screen: how far through
   you are, and the way out. Both were missing — a reader fourteen rooms deep
   had a one-pixel line and no exit but the menu. */
.voyage__hud {
  position: fixed; left: 0; right: 0; top: var(--header-h);
  z-index: calc(var(--z-header) - 1);
  opacity: var(--journey-in, 0);
  pointer-events: none;
}
.voyage__hud-bar {
  position: relative;
  display: flex; align-items: center; justify-content: flex-end;
  gap: var(--s5);
  padding: var(--s3) var(--gutter) 0;
  font-size: var(--t-micro); letter-spacing: 0.14em; text-transform: uppercase;
}
.voyage__count { margin: 0; color: color-mix(in srgb, var(--mist-200) 58%, transparent); }
.voyage__count-n { color: var(--champagne); }
.voyage__exit {
  /* The one interactive thing in the strip, so the strip itself stays
     click-through and never steals a press meant for a room. */
  pointer-events: auto;
  display: inline-flex; align-items: center; min-height: 2.75rem;
  color: color-mix(in srgb, var(--mist-200) 78%, transparent);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--champagne) 40%, transparent);
}
.voyage__exit:hover, .voyage__exit:focus-visible {
  color: var(--ivory); border-bottom-color: var(--champagne);
}
@media (max-width: 52rem) {
  /* On a phone the card runs the full width and its copy reaches the top of
     the screen: measured with elementFromPoint, the counter sat on a
     service's own lede, and it reads "13" beside a card numbered "07",
     because the card numbers the service and the strip numbers the room.
     Two numbers, one place, neither clear. So the phone keeps the line
     alone — it carries the same position without a word over the copy —
     and the way out is the one in the first room, in the text itself. */
  .voyage__hud-bar { display: none; }
}

.voyage__progress {
  position: absolute; left: 0; right: 0; top: 0;
  height: 1px; z-index: calc(var(--z-header) - 1);
  background: color-mix(in srgb, var(--mist-200) 14%, transparent);
  /* Published by the scroll handler, not transitioned. A transition would
     mean the line is only visible once an animation frame has run, and a
     tab that is not being painted never gets one: the resting state has to
     be correct on its own. The fade comes from the value, which is already
     computed on every scroll. */
}
.voyage__progress-fill {
  display: block; height: 100%; width: 100%;
  background: var(--champagne);
  transform: scaleX(var(--journey, 0)); transform-origin: left;
}

.voyage-card__delivers {
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid color-mix(in srgb, var(--champagne) 34%, transparent);
  font-size: var(--t-small);
  max-width: 44ch;
}
.voyage-card__delivers-label {
  display: block;
  font-size: var(--t-micro); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--champagne);
  margin-bottom: var(--s2);
}

/* --- small screens --------------------------------------------------------
   The marks need a finger and the frame is narrow, so every treatment
   collapses to one: a card at the foot of the room. The photographs still
   change, the sequence still reads; only the composition stops varying,
   because at 375px there is no room for it to vary in. */
@media (max-width: 52rem) {
  .voyage-step,
  .voyage-step--panel-left, .voyage-step--panel-right,
  .voyage-step--plate, .voyage-step--band, .voyage-step--quote {
    align-items: end; justify-items: stretch; padding-block: var(--s7);
  }
  .voyage-copy--panel, .voyage-copy--plate, .voyage-copy--quote,
  .voyage-copy--band { max-width: none; text-align: left; }
  .voyage-copy--plate::before, .voyage-copy--quote::before,
  .voyage-copy--band::before { display: none; }
  .voyage-copy--plate, .voyage-copy--quote, .voyage-copy--band {
    padding: var(--s5);
    background: color-mix(in srgb, var(--navy-800) 92%, transparent);
    backdrop-filter: blur(18px);
    border: 1px solid color-mix(in srgb, var(--champagne) 24%, transparent);
  }
  .voyage-copy--plate .voyage-card__lede,
  .voyage-copy--plate .display { margin-inline: 0; }
  .voyage-card__points li:nth-child(n + 3) { display: none; }
  .voyage-band__cols { margin-top: var(--s5); gap: var(--s4); }
  .voyage-band__col:nth-child(n + 3) { display: none; }
  /* No marks on a phone.
     Measured at 375x812: the card runs from y=167 to y=754 of an 812px
     viewport, so there is no position left in the frame that is not on top
     of the copy — the two that collided were simply the worst of them, and
     the rest passed by landing in the gaps between lines. Remapping them
     into "the part of the room that is still open" was describing a part of
     the room that no longer exists once the cards carry a deliverable.
     They are a pointer affordance over a photograph that a phone mostly
     hides behind the copy anyway, and everything they say is already in the
     service's own page. Hidden with `display: none`, so they leave the tab
     order rather than lurking in it. */
  .voyage-step__spots { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .voyage[data-fade="true"] .voyage-room { transition: none; }
  .voyage-room img { transform: none; }
  .spot__dot::before, .spot__dot::after, .spot__note { transition: none; }
}

/* Without JavaScript nothing swaps the rooms, so the stack lays out in flow:
   every room visible, each above the copy it belongs to, marks hidden
   because there is nothing to open them with. */
html:not(.js) .voyage__backdrop { position: static; height: auto; }
html:not(.js) .voyage__rooms { position: static; }
html:not(.js) .voyage-room { position: static; opacity: 1; aspect-ratio: 16 / 9; }
html:not(.js) .voyage__steps { margin-top: 0; }
html:not(.js) .voyage__vignette, html:not(.js) .voyage-room__spots { display: none; }
