@font-face {
  font-family: "EB Garamond";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ebgaramond-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "EB Garamond";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ebgaramond-400-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "EB Garamond";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ebgaramond-400-italic-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "EB Garamond";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ebgaramond-400-italic-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Schibsted Grotesk";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("fonts/schibstedgrotesk-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Schibsted Grotesk";
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url("fonts/schibstedgrotesk-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}


@property --p {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

:root {
  color-scheme: light;

  --chalk: #E9EBE4;
  --card: #F2F3EE;
  --band: #DCE0D8;
  --ink: #14181A;
  --muted: #525C5A;
  --hairline: rgba(20, 24, 26, 0.20);
  --cobalt: #16468C;
  --cobalt-deep: #0E2F63;
  --on-cobalt: #F7F8F4;
  --on-image: #F7F8F4;
  --over-photo: #3D4746;
  --empty-slot: #C9CFC6;
  --slot-ink: #3F4845;

  --display: "EB Garamond", Georgia, "Times New Roman", serif;
  --text: "Schibsted Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;

  --ease-swell: cubic-bezier(0.34, 0.02, 0.10, 1);
  --ease-tide: cubic-bezier(0.45, 0, 0.55, 1);
  --ease-calm: cubic-bezier(0.32, 0.08, 0.24, 1);

  --hover: 420ms;
  --enter: 1400ms;
  --dissolve: 1800ms;
  --line: 1100ms;
  --navmove: 560ms;
  --tick: 640ms;
  --quote-out: 300ms;
  --quote-in: 720ms;
  --quote-lift: 12px;

  --swell-in: 260ms;
  --swell-out: 560ms;

  --drift: 24px;
  --sway: 10px;
  --stagger: 140ms;

  --par-shift: -7%;

  --bleed-inset: 150px;
  --gutter: 24px;
}

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

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

body {
  margin: 0;
  background: var(--chalk);
  color: var(--ink);
  font-family: var(--text);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}

h1, h2, h3 {
  margin: 0;
  font-family: var(--display);
  font-weight: 400;
  line-height: 1.02;
}

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

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

a {
  color: var(--cobalt);
  text-decoration: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: rgba(22, 70, 140, 0.16);
}

button {
  font: inherit;
  color: inherit;
  touch-action: manipulation;
  -webkit-tap-highlight-color: rgba(22, 70, 140, 0.16);
}

:focus-visible {
  outline: 2px solid var(--cobalt);
  outline-offset: 3px;
}

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

.skip {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 100;
  padding: 11px 26px;
  border-radius: 999px;
  background: var(--cobalt);
  color: var(--on-cobalt);
  font-family: var(--text);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  transform: translateY(-160%);
  transition: transform var(--navmove) var(--ease-tide);
}
.skip:focus-visible { transform: none; }

/* No max-width: full-bleed means full-bleed, including on a monitor wider
   than 1440px. Inset sections are held by .wrap, and full-bleed *text* is
   held to the same 1440 content column by the max() below, so the story
   sections bleed while their copy stays aligned with everything else. */
.shell {
  position: relative;
}

/* The nav is fixed, so an anchor that lands a target at viewport top lands it
   under the bar. This covers #tours from the Book pills and #main from the
   skip link. Smooth travel is opt-in, never under a reduce preference. */
html { scroll-padding-top: 72px; }
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

.wrap {
  width: min(1120px, 100% - (var(--gutter) * 2));
  margin-inline: auto;
}

.label {
  font-family: var(--text);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.65;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.label--cobalt { color: var(--cobalt); }

.slot-label {
  font-family: var(--text);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  color: var(--slot-ink);
}

/* Image frames */

.frame {
  position: relative;
  overflow: hidden;
  background: var(--band);
}

.frame__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.scrim--hero {
  background: linear-gradient(180deg, rgba(233,235,228,0.70) 0%, rgba(233,235,228,0.48) 45%, rgba(233,235,228,0.62) 100%);
}
.scrim--left {
  background: linear-gradient(90deg, rgba(233,235,228,0.94) 0%, rgba(233,235,228,0.44) 52%, transparent 80%);
}
.scrim--right {
  background: linear-gradient(270deg, rgba(233,235,228,0.94) 0%, rgba(233,235,228,0.44) 52%, transparent 80%);
}
.scrim--right-4 {
  background: linear-gradient(270deg, rgba(233,235,228,0.94) 0%, rgba(233,235,228,0.46) 54%, transparent 82%);
}
.scrim--cta {
  background: radial-gradient(95% 65% at 50% 52%, rgba(20,24,26,0.44) 0%, rgba(20,24,26,0.70) 100%);
}

/* Pills */

.pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 999px;
  padding: 11px 26px;
  background: var(--cobalt);
  color: var(--on-cobalt);
  font-family: var(--text);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: background-color var(--hover) var(--ease-calm), color var(--hover) var(--ease-calm), transform var(--hover) var(--ease-calm);
}
.pill:active { transform: translateY(1px); transition-duration: 90ms; }

/* An unresolved slot: no href, so not focusable, and visibly not a button.
   Dimmed with filter, not opacity — the hero entrance animates opacity and
   would otherwise land the disabled pill back at full strength. */
[data-slot][aria-disabled="true"] {
  filter: opacity(0.55);
  cursor: not-allowed;
}

/* The hero's own low-emphasis ask. A chalk plate rather than solid cobalt,
   so it reads under the wordmark, not against it. */
.pill--quiet {
  background: rgba(233, 235, 228, 0.92);
  color: var(--cobalt);
  box-shadow: inset 0 0 0 1px rgba(22, 70, 140, 0.30);
}

@media (hover: hover) {
  .pill:hover { background: var(--cobalt-deep); }
  .pill--quiet:hover { background: var(--chalk); color: var(--cobalt-deep); }
}

.pill--ghost {
  align-self: flex-start;
  padding: 10px 22px;
  background: transparent;
  border: 1px solid var(--cobalt);
  color: var(--cobalt);
}
@media (hover: hover) {
  .pill--ghost:hover,
  .card:hover .pill--ghost {
    background: var(--cobalt);
    color: var(--on-cobalt);
  }
}

.pill--lg {
  padding: 21px 44px;
  font-size: 13px;
  letter-spacing: 0.14em;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  touch-action: manipulation;
}

/* Press feedback, in the page's own vocabulary: a deeper cobalt rises through
   the pill from the bottom on the swell curve, and drains back out more slowly
   than it came in. Quicker in than out is what makes it read as water rather
   than as a fill bar. --swell-out sits on the base rule so a released press
   always recedes; --swell-in only applies while the press is held.

   isolation + z-index -1 puts the wash above the pill's own background and
   below its label, which is a bare text node and cannot be layered directly.
   overflow clips the wash to the pill radius. */
.pill--lg::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--cobalt-deep);
  transform: translate3d(0, 101%, 0);
  transition: transform var(--swell-out) var(--ease-tide);
}
.pill--lg.is-pressed::after {
  transform: none;
  transition: transform var(--swell-in) var(--ease-swell);
}
.pill--lg.is-pressed { transform: translateY(1px); }

/* Explicit exception to the dimmed-slot rule above, at the user's request:
   the closing ask stays at full strength and takes a press even while its URL
   slot is unresolved, so the button is something you can push. The label is
   still the bracketed [ BOOKING URL ], so the page has not started pretending
   the link exists. */
.cta .pill--lg[aria-disabled="true"] {
  filter: none;
  cursor: pointer;
}

/* Nav */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 40;
  height: 72px;
  background: rgba(233, 235, 228, 0.88);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  transition: opacity var(--navmove) var(--ease-tide), transform var(--navmove) var(--ease-tide);
}
.nav[data-state="hidden"] {
  opacity: 0;
  transform: translateY(-100%);
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--navmove) var(--ease-tide), transform var(--navmove) var(--ease-tide), visibility 0s linear var(--navmove);
}
.nav[data-state="shown"] {
  opacity: 1;
  transform: none;
  visibility: visible;
  transition: opacity var(--navmove) var(--ease-tide), transform var(--navmove) var(--ease-tide), visibility 0s linear 0s;
}
/* P0-2: without the script there is no hero observer to un-hide it. */
html:not(.js) .nav[data-state="hidden"] {
  opacity: 1;
  transform: none;
  visibility: visible;
  pointer-events: auto;
}
.nav__inner {
  max-width: 1440px;
  height: 100%;
  margin-inline: auto;
  padding-inline: 48px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.nav__mark {
  font-family: var(--display);
  font-size: 26px;
  line-height: 1.12;
  color: var(--ink);
}

/* Hero */

/* svh, not dvh, and the same everywhere a full-screen block is sized below.
   dvh changes value as the phone's address bar retracts and returns, so five
   stacked dvh blocks (hero + four chapters) made the document grow and shrink
   by several hundred pixels mid-scroll. Scrolling up past the last one landed
   in a feedback loop -- bar returns, page shortens, scroll corrects, bar
   hides -- which read as the page juddering back and forth. svh is a fixed
   number, so the document height never moves. */
.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  place-items: center;
  isolation: isolate;
}
.hero__frame {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--chalk);
}
.hero__body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 30px;
  padding: 72px var(--gutter);
  text-align: center;
  background: radial-gradient(64% 56% at 50% 48%, rgba(233,235,228,0.66) 0%, rgba(233,235,228,0.38) 42%, transparent 70%);
}
.hero__mark {
  font-size: 212px;
  line-height: 1.12;
  letter-spacing: -0.012em;
  padding-bottom: 8px;
  color: var(--ink);
}
.hero__mark .ital { font-style: italic; }
.hero__line {
  font-family: var(--display);
  font-size: 27px;
  line-height: 1.4;
  max-width: 680px;
  color: var(--ink);
}
.hero__cue {
  display: block;
  width: 1px;
  height: 92px;
  background: linear-gradient(180deg, rgba(20,24,26,0.6), transparent);
}

@media (prefers-reduced-motion: no-preference) {
  .hero__body > * {
    opacity: 0;
    transform: translate3d(0, var(--drift), 0);
    animation: rise var(--enter) var(--ease-swell) forwards;
  }
  .hero__mark { animation-delay: 160ms; }
  .hero__line { animation-delay: 420ms; }
  .hero__body .pill--quiet { animation-delay: 620ms; }
  .hero__cue { animation-delay: 820ms; }
}

@keyframes rise {
  from { opacity: 0; transform: translate3d(0, var(--drift), 0); }
  to { opacity: 1; transform: none; }
}

/* Chapters */

.chapters {
  position: relative;
  height: calc(4 * 140svh);
  view-timeline-name: --chaptersTL;
  timeline-scope: --ch1, --ch2, --ch3, --ch4;
}
/* The one full-screen box that is lvh rather than svh: it has to cover the
   screen with the address bar retracted, or a strip of page shows under it.
   lvh is a constant too, so the document height still does not move. */
.chapters__pin {
  position: sticky;
  top: 0;
  height: 100lvh;
  overflow: hidden;
}
/* Offset by half a viewport and shortened by a full one, so the four steps
   map onto the pin's actual travel (height - 100svh) rather than onto the
   section box. Without this, chapter 4 became active at 3*H/4 - 50svh but the
   sticky released at H - 100svh, giving it a hold 450px shorter than 1-3.
   With it, all four hold an equal 115svh and the transitions land on
   25 / 50 / 75 percent of the rail's own timeline. */
.chapters__steps {
  position: absolute;
  inset: 50svh 0 auto 0;
  height: calc(100% - 100svh);
  display: grid;
  grid-template-rows: repeat(4, 1fr);
  pointer-events: none;
}
.step { width: 1px; }
.step[data-step="1"] { view-timeline-name: --ch1; }
.step[data-step="2"] { view-timeline-name: --ch2; }
.step[data-step="3"] { view-timeline-name: --ch3; }
.step[data-step="4"] { view-timeline-name: --ch4; }

.chapter {
  position: absolute;
  inset: 0;
  transition: opacity var(--dissolve) var(--ease-tide), visibility 0s linear var(--dissolve);
}
/* Only hidden where a script exists to bring them back. */
.js .chapter {
  opacity: 0;
  visibility: hidden;
}
.chapter.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dissolve) var(--ease-tide), visibility 0s linear 0s;
}
.chapter__frame {
  position: absolute;
  inset: 0;
}
.chapter__text {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 540px;
  max-width: calc(100% - (var(--bleed-inset) * 2));
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.chapter--left .chapter__text { left: max(var(--bleed-inset), calc(50% - 720px + var(--bleed-inset))); }
.chapter--right .chapter__text { right: max(var(--bleed-inset), calc(50% - 720px + var(--bleed-inset))); text-align: right; }
.chapter--right .chapter__text .ln { display: block; }

/* The rail already prints the hour in the margin. Above 768px the chapter
   label is the duplicate, so it is hidden from sight but kept in the
   accessibility tree — the rail is aria-hidden, so this is the only place a
   screen reader gets the time. Below 768px the tick labels are gone and this
   label is the one that carries it. */
@media (min-width: 768px) {
  .chapter .label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

.chapter__h {
  font-size: 86px;
  color: var(--ink);
}
.chapter__p {
  font-size: 16px;
  line-height: 1.65;
  color: var(--over-photo);
  max-width: 65ch;
}

.chapter .label,
.chapter .ln,
.chapter__p {
  opacity: 0;
  transition: opacity var(--line) var(--ease-swell), transform var(--line) var(--ease-swell);
}
.chapter--left .label,
.chapter--left .ln,
.chapter--left .chapter__p { transform: translate3d(calc(var(--sway) * -1), var(--drift), 0); }
.chapter--right .label,
.chapter--right .ln,
.chapter--right .chapter__p { transform: translate3d(var(--sway), var(--drift), 0); }

.chapter__p { transition-duration: var(--enter); }
.chapter .ln { display: block; }

.chapter.is-active .label,
.chapter.is-active .ln,
.chapter.is-active .chapter__p,
.chapter.is-lit .label,
.chapter.is-lit .ln,
.chapter.is-lit .chapter__p {
  opacity: 1;
  transform: none;
}
.chapter.is-active .label,
.chapter.is-lit .label { transition-delay: 220ms; }
.chapter.is-active .ln:nth-child(1),
.chapter.is-lit .ln:nth-child(1) { transition-delay: 420ms; }
.chapter.is-active .ln:nth-child(2),
.chapter.is-lit .ln:nth-child(2) { transition-delay: 620ms; }
.chapter.is-active .chapter__p,
.chapter.is-lit .chapter__p { transition-delay: 800ms; }

/* Time rail */

.rail {
  position: fixed;
  top: 50%;
  left: calc(50% - min(720px, 50%) + 52px);
  height: 44vh;
  z-index: 20;
  opacity: 0;
  pointer-events: none;
  transform: translateY(-50%);
  transition: opacity var(--navmove) var(--ease-tide);
}
.rail.is-on { opacity: 1; }
.rail__track {
  position: absolute;
  inset: 0 auto 0 0;
  width: 1px;
  background: var(--hairline);
}
.rail__fill {
  position: absolute;
  inset: 0;
  background: var(--cobalt);
  transform-origin: top center;
  transform: scaleY(var(--p));
}
/* Ticks sit at their real position on the day, not at even intervals:
   06:00 / 10:00 / 15:00 / 20:00 is 4h + 5h + 5h, so spreading them evenly
   misreports the thing the rail exists to report. --t comes from the markup. */
.rail__ticks {
  position: absolute;
  left: 16px;
  top: 0;
  height: 100%;
}
.rail__tick {
  position: absolute;
  left: 0;
  top: calc(var(--t) * 100%);
  transform: translateY(-50%);
  white-space: nowrap;
  font-family: var(--text);
  font-size: 11px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: rgba(20, 24, 26, 0.68);
  transition: color var(--tick) var(--ease-tide);
}
.rail__tick:first-child { transform: none; }
.rail__tick:last-child { transform: translateY(-100%); }
.rail__tick.is-past { color: var(--ink); }
.rail__tick.is-now { color: var(--cobalt); }
/* Sits on the last line of .chapters. It enters the viewport exactly when the
   sticky pin releases, which is the beat the rail has to retire on: the day is
   over at 20:00 and the rail has nothing left to report. */
.rail__end {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

/* Parallax. Scroll-driven, so it runs on the compositor and costs the phone
   almost nothing — it is the one bespoke motion mobile keeps, at a reduced
   --par-shift. Overflow is absorbed by the frame. */

.frame__img.par,
.hero__img {
  height: 112%;
  bottom: auto;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .frame__img.par,
    .hero__img { will-change: transform; }

    .par[data-par="1"] { animation: par linear both; animation-timeline: --ch1; animation-range: cover 0% cover 100%; }
    .par[data-par="2"] { animation: par linear both; animation-timeline: --ch2; animation-range: cover 0% cover 100%; }
    .par[data-par="3"] { animation: par linear both; animation-timeline: --ch3; animation-range: cover 0% cover 100%; }
    .par[data-par="4"] { animation: par linear both; animation-timeline: --ch4; animation-range: cover 0% cover 100%; }
    .hero__img,
    .par[data-par="cta"] {
      animation: par linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    /* Stacked layout: .chapters__steps is display:none, so the named
       timelines resolve to nothing. Each image drives off its own view. */
    @media (max-width: 767px) {
      .frame__img.par {
        animation-timeline: view();
        animation-range: cover 0% cover 100%;
      }
    }
  }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .rail__fill {
      animation: fill linear both;
      animation-timeline: --chaptersTL;
      animation-range: contain 0% contain 100%;
    }
  }
}

/* The fill has to arrive at each tick exactly as that tick becomes "now",
   or a continuous fill and discrete ticks report two different times. With
   the steps grid above, chapters turn over at 25 / 50 / 75 percent of this
   timeline, and the ticks sit at 0 / 4-14ths / 9-14ths / 1 of the day. */
@keyframes fill {
  0%   { --p: 0; }
  25%  { --p: 0.285714; }
  50%  { --p: 0.642857; }
  75%  { --p: 1; }
  100% { --p: 1; }
}
@keyframes par {
  from { transform: translate3d(0, var(--par-shift), 0); }
  to { transform: translate3d(0, 0%, 0); }
}

/* Tours */

.tours { padding-block: 118px; }
.tours__h {
  font-size: 66px;
  margin-bottom: 52px;
}
.tours__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 34px;
}

.card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 18px 18px 26px;
  background: var(--card);
  transition: translate var(--hover) var(--ease-calm);
}
.card:has(.pill:focus-visible) { translate: 0 -4px; }
.card:active { translate: 0 -2px; transition-duration: 120ms; }
@media (hover: hover) {
  .card:hover { translate: 0 -4px; }
}
.card__media {
  aspect-ratio: 4 / 3;
}
.card__media .frame__img {
  transition: transform var(--hover) var(--ease-calm);
}
.card:has(.pill:focus-visible) .card__media .frame__img {
  transform: scale(1.03);
}
@media (hover: hover) {
  .card:hover .card__media .frame__img { transform: scale(1.03); }
}
.card:has(.pill:focus-visible) .pill--ghost {
  background: var(--cobalt);
  color: var(--on-cobalt);
}
.card__name {
  font-size: 36px;
  margin-top: 6px;
}
.card__meta { font-size: 11px; }
.card__p {
  font-size: 15px;
  line-height: 1.65;
  color: var(--muted);
  max-width: 65ch;
}
.card .pill--ghost { margin-top: 4px; }

/* Guides */

.guides {
  background: var(--band);
  padding-block: 94px;
}
.guides__h {
  font-size: 54px;
  margin-bottom: 44px;
}
.guides__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 28px;
}
.guide {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.guide__media {
  aspect-ratio: 4 / 5;
  background: var(--empty-slot);
}
.guide__media--empty {
  display: flex;
  align-items: flex-end;
  padding: 16px;
}
.guide__name { font-size: 24px; }
.guide__p {
  font-size: 14px;
  line-height: 1.6;
  color: var(--muted);
}

/* Testimonials */

.voices {
  position: relative;
  padding-block: 130px;
}
.voices__inner {
  display: flex;
  align-items: center;
  gap: 64px;
}
/* Stretched to the quote block rather than floating at a fixed 440px, so it
   reads as a deliberate plate the type sits against and responds to the
   length of the quote beside it. */
.voices__plate {
  flex: 0 0 240px;
  width: 240px;
  align-self: stretch;
  min-height: 440px;
}
.voices__col {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 32px;
  align-items: flex-start;
}
.quotes {
  display: grid;
  width: 100%;
}
/* Quote swap.
   -----------
   Two earlier attempts were wrong for the same underlying reason. The 1600ms
   cross-fade held two blocks of 36px serif on top of each other at partial
   opacity, which reads as double vision. The horizontal slide that replaced it
   fixed the ghosting but bought a carousel gesture, and a dot carousel is
   already the most category-interchangeable thing in this section. The motion
   made the page more generic, not less.

   This version borrows the page's own arrival grammar instead of importing a
   slider's. Everywhere else on this site, content rises into place from
   --drift on --ease-swell and the body copy trails the heading by --stagger.
   A testimonial is a voice arriving, so it arrives the same way: the outgoing
   quote lifts clear on a short --ease-calm beat, then the new quote rises from
   below with its attribution one stagger behind. No lateral travel, nothing
   overlapping, and no gesture borrowed from a component library.

   The figure carries visibility only; the two children carry the motion, which
   is what makes the attribution trail possible. */
.quote {
  grid-area: 1 / 1;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 32px;
  align-items: flex-start;
  transition: visibility 0s linear var(--quote-out);
}
.js .quote { visibility: hidden; }
/* Without a script the three quotes would stack in one grid cell. */
html:not(.js) .quote:not(.is-active) { display: none; }
.js .quote.is-active {
  visibility: visible;
  transition: visibility 0s linear 0s;
}

/* Resting state: waiting below, ready to rise. The --quote-out delay is what
   holds the incoming quote back until the outgoing one has finished leaving,
   so the two are never legible at the same time. */
.js .quote > * {
  opacity: 0;
  transform: translate3d(0, var(--drift), 0);
  transition:
    opacity var(--quote-in) var(--ease-swell) var(--quote-out),
    transform var(--quote-in) var(--ease-swell) var(--quote-out);
}
.js .quote.is-active > * {
  opacity: 1;
  transform: none;
}
.js .quote.is-active > .quote__by {
  transition-delay: calc(var(--quote-out) + var(--stagger));
}

/* Leaving: lifts and clears. Shorter and on --ease-calm, because a dismissal
   should weigh less than an arrival, and it runs immediately with no delay. */
.js .quote.is-leaving > * {
  opacity: 0;
  transform: translate3d(0, calc(var(--quote-lift) * -1), 0);
  transition:
    opacity var(--quote-out) var(--ease-calm),
    transform var(--quote-out) var(--ease-calm);
}
.js .quote.is-leaving > .quote__by { transition-delay: 0s; }
.quote__text {
  margin: 0;
  font-family: var(--display);
  font-size: 36px;
  line-height: 1.35;
  color: var(--ink);
  max-width: 760px;
  text-align: left;
}
.quote__by {
  color: var(--muted);
  text-align: left;
}
/* Without a script nothing advances the quotes, so three dots that cannot do
   anything are worse than no dots. Quote 1 stands alone. */
html:not(.js) .dots { display: none; }
/* The 9px dot IS the 44px target, rather than carrying a 44px pseudo-element
   over a 23px stride — that overlapped its neighbour by ~21px with no z-index
   between them, so tapping the right edge of dot 1 selected dot 2. Centre to
   centre is now exactly 44px. The negative margin pulls the first dot's core
   back onto the text's left edge. */
.dots {
  display: flex;
  gap: 0;
  margin-left: -17.5px;
}
.dot {
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  display: grid;
  place-items: center;
}
.dot::after {
  content: "";
  width: 9px;
  height: 9px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  transition: background-color var(--hover) var(--ease-calm), border-color var(--hover) var(--ease-calm);
}
.dot.is-active::after {
  background: var(--cobalt);
  border-color: var(--cobalt);
}
.voices__steps {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.vstep { position: absolute; left: 0; width: 1px; height: 1px; }
.vstep[data-vstep="1"] { top: 15%; }
.vstep[data-vstep="2"] { top: 50%; }
.vstep[data-vstep="3"] { top: 85%; }

/* Booking CTA */

.cta {
  position: relative;
  height: 660px;
  display: grid;
  place-items: center;
  isolation: isolate;
  /* .cta__body::before is inset -20% horizontally, so on a phone the glow
     reached ~52px past both edges of the document. Nothing scrolled -- the
     browser just fitted the wider document to the screen, which is what put
     the empty bands down the left and right of the page. Clip it here: the
     gradient is already transparent by the section edge, so this costs the
     glow nothing. */
  overflow: clip;
}
.cta__frame {
  position: absolute;
  inset: 0;
  z-index: -1;
}
.cta__body {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 40px;
  padding-inline: var(--gutter);
  text-align: center;
}

/* Local ink plate under the closing copy, the same device the hero uses for
   the same reason. Measured on the real photograph: the frame carries a broad
   blown-out glare band, and chalk type over it composites at 1.4:1 through the
   CTA scrim alone, at every crop. Raising .scrim--cta instead would have to go
   past 0.7 alpha across the whole frame to fix it, which flattens the picture,
   and the picture is the direction. This is local and reaches full
   transparency inside its own box, so no edge is visible. Measured again with
   the plate: minimum 3.1:1 across the headline, nothing below 3:1. */
.cta__body::before {
  content: "";
  position: absolute;
  inset: -40% -20%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(70% 66% at 50% 50%,
    rgba(20, 24, 26, 0.78) 0%,
    rgba(20, 24, 26, 0.62) 46%,
    rgba(20, 24, 26, 0) 76%);
}
.cta__h {
  font-size: 84px;
  max-width: 920px;
  color: var(--on-image);
  text-wrap: balance;
}

/* Footer */

.foot {
  background: var(--chalk);
  border-top: 1px solid var(--hairline);
  padding-block: 56px;
}
.foot__inner {
  min-height: 148px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 44px;
}
.foot__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 36px;
}
.foot__mark {
  font-family: var(--display);
  font-size: 30px;
  line-height: 1.12;
  color: var(--ink);
}
.foot__links {
  display: flex;
  flex-wrap: wrap;
  gap: 36px;
}
.foot__links a { color: var(--cobalt); transition: color var(--hover) var(--ease-calm); }
.foot__links a:hover { color: var(--cobalt-deep); }
.foot__where { font-size: 14px; color: var(--muted); }
.foot__legal { font-size: 12px; color: var(--muted); }

/* Entrances */

/* The CTA lives inside a .reveal. It may never depend on a script running. */
.reveal {
  transition: opacity var(--enter) var(--ease-swell), transform var(--enter) var(--ease-swell);
  transition-delay: calc(var(--i, 0) * var(--stagger));
}
.js .reveal {
  opacity: 0;
  transform: translate3d(0, var(--drift), 0);
}
.reveal.is-in {
  opacity: 1;
  transform: none;
}

/* Breakpoints */

@media (max-width: 1279px) {
  :root { --bleed-inset: 96px; }
}

@media (max-width: 1023px) {
  :root { --bleed-inset: 64px; }
  .hero__mark { font-size: 148px; }
  .hero__line { font-size: 23px; }
  .chapter__h { font-size: 64px; }
  .tours__h { font-size: 54px; }
  .cta__h { font-size: 64px; }
  .quote__text { font-size: 30px; }
  .voices__inner { gap: 44px; }
}

@media (max-width: 767px) {
  :root {
    --bleed-inset: 24px;
    --drift: 14px;
    --sway: 0px;
    --par-shift: -3%;

    /* A phone shows less motion, so it must not also wait the longest for
       it. Roughly half the desktop budget across the board. */
    --enter: 700ms;
    --dissolve: 900ms;
    --line: 560ms;
    --quote-out: 180ms;
    --quote-in: 420ms;
    --quote-lift: 8px;
    --swell-in: 200ms;
    --swell-out: 420ms;
    --stagger: 80ms;
    --navmove: 360ms;
    --tick: 380ms;
  }

  .nav {
    height: 64px;
    background: rgba(233, 235, 228, 0.94);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .nav__inner { padding-inline: var(--gutter); }
  .nav__mark { font-size: 22px; }
  html { scroll-padding-top: 64px; }
  .pill { padding: 15px 26px; }
  .pill--ghost { padding: 14px 22px; }
  .pill--lg { padding: 18px 32px; }

  .hero__body { gap: 16px; padding-inline: var(--gutter); }
  .hero__mark { font-size: 56px; }
  .hero__line { font-size: 19px; }
  .hero__cue { height: 44px; }

  .chapters { height: auto; }
  .chapters__pin {
    position: static;
    height: auto;
    overflow: visible;
  }
  .chapters__steps { display: none; }
  .chapter {
    position: relative;
    inset: auto;
    min-height: 100svh;
    overflow: hidden;
    transition: none;
  }
  .js .chapter {
    opacity: 1;
    visibility: visible;
  }
  .frame__img.par,
  .hero__img { height: 105%; }

  /* Halved cascade: copy lands ~1.1s after trigger, not 2.2s. */
  .chapter.is-active .label,
  .chapter.is-lit .label { transition-delay: 110ms; }
  .chapter.is-active .ln:nth-child(1),
  .chapter.is-lit .ln:nth-child(1) { transition-delay: 210ms; }
  .chapter.is-active .ln:nth-child(2),
  .chapter.is-lit .ln:nth-child(2) { transition-delay: 310ms; }
  .chapter.is-active .chapter__p,
  .chapter.is-lit .chapter__p { transition-delay: 400ms; }

  .chapter__text {
    top: auto;
    bottom: 72px;
    left: var(--gutter);
    right: var(--gutter);
    width: auto;
    max-width: none;
    transform: none;
    text-align: left;
    gap: 16px;
  }
  .chapter--right .chapter__text { text-align: left; }
  .chapter--left .label,
  .chapter--left .ln,
  .chapter--left .chapter__p,
  .chapter--right .label,
  .chapter--right .ln,
  .chapter--right .chapter__p {
    transform: translate3d(0, var(--drift), 0);
  }
  .chapter__h { font-size: 40px; }
  .chapter__p { font-size: 15px; }
  .scrim--left,
  .scrim--right,
  .scrim--right-4 {
    background: linear-gradient(180deg, rgba(233,235,228,0.16) 0%, rgba(233,235,228,0.52) 46%, rgba(233,235,228,0.94) 78%, var(--chalk) 100%);
  }

  .rail {
    top: 0;
    left: 0;
    width: 100%;
    height: 3px;
    z-index: 50;
    transform: none;
  }
  .rail__track { inset: 0; width: 100%; height: 3px; }
  .rail__fill {
    transform-origin: left center;
    transform: scaleX(var(--p));
  }
  .rail__ticks { display: none; }

  .tours { padding-block: 64px; }
  .tours__h { font-size: 34px; margin-bottom: 32px; }
  .tours__grid { grid-template-columns: minmax(0, 1fr); gap: 24px; }
  .card__name { font-size: 28px; }

  .guides { padding-block: 56px; }
  .guides__h { font-size: 30px; margin-bottom: 32px; }
  .guides__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
  .guide__name { font-size: 20px; }

  .voices { padding-block: 72px; }
  .voices__inner { flex-direction: column; align-items: stretch; gap: 28px; }
  .voices__plate { flex: 0 0 auto; width: 100%; height: 200px; min-height: 0; align-self: auto; }
  .quote__text { font-size: 24px; }

  .cta { height: 560px; }
  .cta__h { font-size: 38px; }
  .cta__body { gap: 28px; }

  .foot { padding-block: 40px; }
  .foot__inner { min-height: 0; gap: 32px; }
  .foot__row {
    flex-direction: column;
    align-items: flex-start;
    gap: 24px;
  }
  .foot__links { gap: 24px; }
}

@media (prefers-reduced-transparency: reduce) {
  .nav {
    background: var(--chalk);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .chapters { height: auto; }
  .chapters__pin {
    position: static;
    height: auto;
    overflow: visible;
  }
  .chapters__steps { display: none; }
  .chapter {
    position: relative;
    inset: auto;
    min-height: 100svh;
    overflow: hidden;
    transition: none;
  }
  .js .chapter {
    opacity: 1;
    visibility: visible;
  }
  .frame__img.par,
  .hero__img {
    height: 100%;
    animation: none;
    will-change: auto;
  }

  .chapter .label,
  .chapter .ln,
  .chapter__p,
  .js .reveal {
    transform: none;
    transition: opacity 200ms linear;
    transition-delay: 0s;
  }
  .chapter--left .label,
  .chapter--left .ln,
  .chapter--left .chapter__p,
  .chapter--right .label,
  .chapter--right .ln,
  .chapter--right .chapter__p { transform: none; }
  .chapter.is-active .label,
  .chapter.is-active .ln,
  .chapter.is-active .chapter__p,
  .chapter.is-lit .label,
  .chapter.is-lit .ln,
  .chapter.is-lit .chapter__p { transition-delay: 0s; }

  /* The motion now lives on the children, so the reduce override has to reach
     them too, and has to cancel the rise as well as the timing or the quote
     would jump --drift vertically. */
  .quote,
  .js .quote,
  .js .quote > *,
  .js .quote.is-active > *,
  .js .quote.is-active > .quote__by,
  .js .quote.is-leaving > *,
  .js .quote.is-leaving > .quote__by {
    transform: none;
    transition: opacity 0s linear, visibility 0s linear;
    transition-delay: 0s;
  }

  /* Everything else that still carried a transition into the reduce block:
     the nav's 560ms 72px slide, the pills, the card lift and its image
     scale, the skip link and the footer link colour. */
  .pill--lg::after,
  .pill--lg.is-pressed::after { transition: none; }
  .pill--lg.is-pressed { transform: none; }

  .nav,
  .nav[data-state="hidden"],
  .nav[data-state="shown"],
  .pill,
  .card,
  .card__media .frame__img,
  .skip,
  .foot__links a,
  .dot::after,
  .rail__tick,
  .rail { transition: none; }

  .hero__body > * { opacity: 1; transform: none; animation: none; }
}

/* ==========================================================================
   Intro loader
   The SVG intro sequence (svg/intro_loader_edited.svg, mobile variant
   alongside it) mounted over the page. This replaces the earlier .tide bar:
   the tide is now the artwork's own surge rather than a CSS channel.

   The overlay ground is painted from the first frame via html.intro, so the
   hero never flashes before site.js inlines the artwork. The head script
   withholds .intro on a reduced-motion preference and on every visit after
   the first, which is what keeps the intro from being a toll booth.
   ========================================================================== */

/* No script, no overlay. The page is never held behind something that cannot
   be taken away again. */
#intro { display: none; }

.intro #intro {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  background: var(--chalk);
  opacity: 1;
}

#intro-stage,
#intro-stage svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Held still while the overlay is up: the hero's own entry is the payoff of
   the handover, so it must not have played out underneath. */
.preloading,
.preloading body { overflow: hidden; }
.preloading .hero__body > * { animation-play-state: paused; }

/* Thumb zone, not the top-right corner. */
#intro-skip {
  position: absolute;
  left: 50%;
  /* The page ships viewport-fit=cover, so a flat offset puts the control under
     a home indicator. Clears the inset where there is one, 28px where not. */
  bottom: max(28px, calc(env(safe-area-inset-bottom) + 12px));
  transform: translateX(-50%);
  min-width: 44px;
  min-height: 44px;
  padding: 15px 30px;
  border: 0;
  border-radius: 999px;
  background: var(--cobalt);
  color: var(--on-cobalt);
  font-family: var(--text);
  font-size: 11px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  cursor: pointer;
  /* The control has to survive two different grounds. Measured on the real
     composite: the cobalt pill reads 7.37:1 against the chalk loading screen,
     but only 1.16:1 once the tide surges over it -- it disappears at exactly
     the moment someone reaching for it wants out. The chalk ring is what keeps
     the pill findable in the water. Against chalk it is a 1.1:1 halo, which is
     invisible and intentional; the pill's own edge is doing the work there. */
  box-shadow: 0 0 0 2px var(--on-cobalt);
}
#intro-skip:hover { background: var(--cobalt-deep); }
#intro-skip:active { transform: translateX(-50%) translateY(1px); }
#intro-skip:focus-visible { outline: 2px solid var(--cobalt); outline-offset: 3px; }

.intro #intro.is-leaving {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--quote-in) var(--ease-calm);
}

/* Never mounted under a reduced-motion preference. Repeated here in case the
   preference changes after the head script has already run. */
@media (prefers-reduced-motion: reduce) {
  .intro #intro { display: none; }
  .intro #intro.is-leaving { transition: none; }
}
