/*
 * Generated once, then owned by hand.
 *
 * EVERY colour below is resolved at GENERATE time and written out as a literal.
 * That is deliberate: a later theme change in the Swellbase app must NOT propagate
 * to the website, and cannot — nothing here reads the theme at serve time. Seeding,
 * not binding.
 *
 * The two brand colours come from lib/theme-colours.ts. The neutrals are DERIVED
 * from the brand hue by scripts/websites/palette.ts — same hue, fixed saturation
 * and lightness steps — so the furniture belongs to the operator's colour instead
 * of being slate-blue under every theme. Change one and the ladder stops being a
 * ladder; change them in palette.ts if you want to move them for everyone.
 *
 * No framework and no build step: this file is served byte-for-byte out of the
 * bundle. Section markers (data-section="hero" | "about" | "team" | "social-proof" |
 * "contact" | "footer") are structural invariants — keep them on hand-built pages
 * too, they are what a later AI editing harness anchors on.
 */

:root {
  --brand-primary: #0284c7;
  --brand-primary-hover: #026da4;
  --brand-secondary: #075985;
  --ink: #172127;
  --ink-soft: #4b5e68;
  --line: #e0e7eb;
  --surface: #ffffff;
  --surface-muted: #f5f8fa;
  --star-empty: #d7dde0;
  /* An `r, g, b` triple, so the hero gradient can vary alpha against it. */
  --hero-overlay-rgb: 7, 21, 28;
  --radius: 10px;
  --wrap: 1120px;
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  /*
   * Headline face. Body stays on the system stack — this is for character on
   * headings only, so the page still renders instantly and only the headlines
   * wait on a network font (`display=swap` in the <head> link).
   *
   * Georgia is the fallback rather than a sans: Coustard is a slab serif, and
   * swapping to a sans mid-load would reflow and change the page's voice. The
   * serif stack keeps the shape close while the web font arrives.
   */
  --font-display: "Coustard", Georgia, "Times New Roman", serif;
}

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

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

body {
  margin: 0;
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink);
  background: var(--surface);
}

h1,
h2,
h3 {
  font-family: var(--font-display);
  /*
   * WEIGHT 400, EXPLICITLY, and it is load-bearing.
   *
   * Coustard ships two weights only — 400 and 900; a request for 700 is
   * rejected by Google Fonts outright. Headings default to `bold`, so without
   * this the browser would synthesise a faux-bold from the 400 file and every
   * heading on the site would render smeared. 400 is already substantial: it
   * is a slab serif, not a text face.
   */
  font-weight: 400;
  line-height: 1.18;
  margin: 0 0 0.5em;
  /*
   * No negative tracking. The -0.01em here was tuned for the geometric sans
   * this replaced; pulling a slab serif's serifs together muddies it.
   */
  letter-spacing: 0;
}

h1 {
  font-size: clamp(2.1rem, 5.2vw, 3.4rem);
}

h2 {
  font-size: clamp(1.5rem, 3vw, 2.1rem);
}

h3 {
  font-size: 1.15rem;
}

p {
  margin: 0 0 1em;
}

a {
  color: var(--brand-secondary);
}

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

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 20px;
}

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

.center {
  text-align: center;
}

.lede {
  font-size: 1.15rem;
  color: var(--ink-soft);
}

/* --- Buttons ------------------------------------------------------------- */

.button {
  display: inline-block;
  padding: 12px 22px;
  border-radius: var(--radius);
  font-weight: 600;
  text-decoration: none;
  border: 1px solid transparent;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.button-primary {
  background: var(--brand-primary);
  color: #ffffff;
}

/*
 * One step darker, not all the way to `--brand-secondary`. Secondary is the
 * Tailwind -800 against a -600 primary — a section-background colour, and far
 * too heavy a jump for a button hover.
 */
.button-primary:hover {
  background: var(--brand-primary-hover);
}

.button-secondary {
  background: transparent;
  color: var(--brand-secondary);
  border-color: var(--line);
}

.button-secondary:hover {
  border-color: var(--brand-primary);
}

.button-lg {
  padding: 15px 30px;
  font-size: 1.05rem;
}

.link-arrow {
  font-weight: 600;
  text-decoration: none;
}

.link-arrow::after {
  content: " \2192";
}

/* --- Header -------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: rgba(255, 255, 255, 0.94);
  backdrop-filter: saturate(160%) blur(8px);
  border-bottom: 1px solid var(--line);
}

/*
 * Logo left, page links centred, Book Now right.
 *
 * `1fr auto 1fr` rather than flex with space-between: the outer tracks take an
 * equal share of whatever is left over, so the nav lands on the header's true
 * midpoint no matter how wide the logo or the CTA are. A flex row would centre it
 * on the gap between them instead, which drifts as soon as one side grows — and
 * the logo is operator-supplied, so it always will.
 */
.header-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  min-height: 68px;
  padding-top: 10px;
  padding-bottom: 10px;
}

.header-cta {
  justify-self: end;
}

/* --- Mobile menu toggle --------------------------------------------------- */

/*
 * Hidden until the media query below reveals it, and even then only when the
 * header script has marked the header collapsible — so it can never appear
 * without something behind it to press.
 */
.nav-toggle {
  display: none;
  grid-area: toggle;
  justify-self: end;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  align-items: center;
  justify-content: center;
}

.nav-toggle:hover {
  border-color: var(--brand-primary);
}

/* The middle bar is the element; the other two are its pseudo-elements. */
.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  display: block;
  width: 20px;
  height: 2px;
  border-radius: 2px;
  background: var(--ink);
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.nav-toggle-bars {
  position: relative;
}

.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
}

.nav-toggle-bars::before {
  top: -6px;
}

.nav-toggle-bars::after {
  top: 6px;
}

/* Open: the outer bars rotate into a cross and the middle one fades out. */
.site-header[data-nav-open="true"] .nav-toggle-bars {
  background: transparent;
}

.site-header[data-nav-open="true"] .nav-toggle-bars::before {
  transform: translateY(6px) rotate(45deg);
}

.site-header[data-nav-open="true"] .nav-toggle-bars::after {
  transform: translateY(-6px) rotate(-45deg);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--ink);
  text-decoration: none;
}

.brand img {
  max-height: 38px;
  width: auto;
}

.nav {
  display: flex;
  align-items: center;
  justify-content: center;
  /*
   * Row gap stays tight; the column gap is what separates the links. 28px rather
   * than 20 — with up to six items (Home, Activities, Hire, Gift Cards, About,
   * Contact) the row reads as one run of words at the tighter spacing.
   */
  gap: 8px 28px;
  flex-wrap: wrap;
}

.nav a {
  color: var(--ink);
  text-decoration: none;
  font-weight: 500;
}

/*
 * `[data-current]` is the submenu parent on an activity page — highlighted as the
 * branch the visitor is in, without claiming to BE the current page. Only one
 * element per page carries `aria-current="page"`; see `activityNavMarkup`.
 */
.nav a:hover,
.nav a[aria-current="page"],
.nav a[data-current="true"] {
  color: var(--brand-secondary);
}

.nav a[aria-current="page"],
.nav a[data-current="true"] {
  font-weight: 600;
}

/* Not `.nav .nav-cta` any more — the CTA lives outside the nav landmark. */
.nav-cta,
.nav-cta:hover {
  color: #ffffff;
  padding: 9px 18px;
  white-space: nowrap;
}

/* --- Activities submenu --------------------------------------------------- */

/*
 * One control: a disclosure button carrying the label and the chevron. There is no
 * activities index page for a link to point at — the menu is the index.
 *
 * The vertical padding is cancelled by an equal negative margin, so the group still
 * sits on the nav's baseline while its HOVER BOX reaches the bottom of the header.
 * Without that there is a dead gap between the button and a menu positioned at
 * `top: 100%`, and the menu closes as the pointer crosses it.
 */
.nav-group {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 14px 0;
  margin: -14px 0;
}

/* Matches `.nav a` exactly — it sits in the same row and must not read as a button. */
.nav-group-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  font: inherit;
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
}

.nav-group-toggle:hover,
.nav-group[data-open="true"] .nav-group-toggle,
.nav-group-toggle[data-current="true"] {
  color: var(--brand-secondary);
}

.nav-group-toggle[data-current="true"] {
  font-weight: 600;
}

.nav-group-chevron {
  transition: transform 0.18s ease;
}

/*
 * The chevron follows `data-open` everywhere, and additionally follows the
 * pointer ONLY where the pointer is what opens the menu.
 *
 * It used to rotate on `:hover`/`:focus-within` unconditionally. A tap leaves
 * the toggle focused, so in the hamburger panel the group closed but
 * `:focus-within` still matched and the chevron stayed pointing up — it read as
 * open when it was shut. A programmatic .click() does not focus a button, which
 * is why this only showed up on a real device.
 */
.nav-group[data-open="true"] .nav-group-chevron {
  transform: rotate(180deg);
}

@media (hover: hover) and (pointer: fine) and (min-width: 1025px) {
  .nav-group:hover .nav-group-chevron,
  .nav-group:focus-within .nav-group-chevron {
    transform: rotate(180deg);
  }
}

/*
 * `visibility` + `opacity` rather than `display: none`, so the menu can transition
 * and so its links stay out of the tab order while hidden (a `display: none`
 * subtree is untabbable too, but cannot animate; `opacity` alone would leave
 * invisible links focusable).
 */
.nav-menu {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 30;
  min-width: 250px;
  /* A long catalogue scrolls inside the menu instead of running off the viewport. */
  max-height: min(70vh, 460px);
  overflow-y: auto;
  /*
   * A slim, brand-neutral scrollbar rather than the OS default, which on Windows is
   * a 17px grey slab that visually halves the menu's padding on one side. Same
   * treatment the testimonial quotes already use, minus the reveal-on-hover — a
   * menu that can scroll should say so the moment it opens.
   */
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
  margin: 0;
  padding: 8px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 12px 28px rgba(var(--hero-overlay-rgb), 0.14);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s;
}

/*
 * Three ways in, and the first two need no JavaScript: pointer hover, keyboard
 * focus, and the script's latched `data-open` for touch. The script is an
 * enhancement here, exactly as it is for the mobile panel.
 */
.nav-group:hover .nav-menu,
.nav-group:focus-within .nav-menu,
.nav-group[data-open="true"] .nav-menu {
  opacity: 1;
  visibility: visible;
  transform: none;
}

.nav-menu a {
  display: block;
  padding: 9px 12px;
  border-radius: 7px;
  font-size: 0.97rem;
  font-weight: 500;
  line-height: 1.35;
}

.nav-menu a:hover,
.nav-menu a[aria-current="page"] {
  background: var(--surface-muted);
}

/* WebKit/Blink ignore `scrollbar-width`, so the same bar again in their own syntax. */
.nav-menu::-webkit-scrollbar {
  width: 8px;
}

.nav-menu::-webkit-scrollbar-track {
  background: transparent;
  /* Keeps the thumb off the rounded corner of the menu. */
  margin: 8px 0;
}

.nav-menu::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 999px;
  /* Inset via a transparent border, so the thumb reads as 4px inside an 8px gutter. */
  border: 2px solid transparent;
  background-clip: content-box;
}

.nav-menu::-webkit-scrollbar-thumb:hover {
  background: var(--ink-soft);
  background-clip: content-box;
}

/*
 * Below this width three columns cannot hold a logo, three links and a button on
 * one line without the links crushing against the CTA, so it becomes a hamburger:
 * logo and toggle on the bar, links and the Book Now button in the panel beneath.
 *
 * The collapse is keyed on `[data-nav="collapsible"]`, which ONLY the header
 * script sets. Without JavaScript the panel simply stays open and stacked — the
 * links and the CTA are still reachable, just always visible.
 *
 * 1024px, not the generator's 720px. This header carries six nav items (two of
 * them dropdowns), a 241px logo and the Book Now button, and measures 996px
 * before `.nav`'s `flex-wrap` gives up and drops the links onto a second line.
 * 720px therefore left every tablet portrait width — 768, 834, 1024 — showing a
 * wrapped, broken-looking desktop header instead of the hamburger. The JS
 * resize guard in the header script uses the same number; change both together.
 */
@media (max-width: 1024px) {
  /*
   * ONE row when the panel is closed. Declaring all three rows here instead would
   * leave the two empty ones in the template — a `display: none` child stops being
   * a grid item, but the rows it was assigned to still exist, and `row-gap` still
   * applies between them. That silently added 24px of dead space to the closed bar
   * (measured: `grid-template-rows: 44px 0px 0px`, an 88px header instead of 68px).
   */
  .header-inner {
    grid-template-columns: 1fr auto;
    grid-template-areas: "brand toggle";
    row-gap: 12px;
  }

  /*
   * The panel's rows appear when it opens — and also when the header was never
   * marked collapsible, which is the no-JavaScript case where nav and CTA stay
   * visible and therefore still need rows to sit in.
   */
  .site-header[data-nav-open="true"] .header-inner,
  .site-header:not([data-nav="collapsible"]) .header-inner {
    grid-template-areas:
      "brand toggle"
      "nav    nav"
      "cta    cta";
  }

  /*
   * An OPEN panel is the full height of the screen.
   *
   * It used to end wherever its content ended — measured at 463px on every
   * phone, which left 381px of page showing underneath on a 390x844 and 561px
   * at 768x1024. The `max-height: calc(100dvh - 200px)` below was never the
   * binding constraint: the nav's content is ~318px, so it never reached its
   * cap. Capping a box that is already short does nothing; the panel has to be
   * told to fill the screen.
   *
   * `dvh` after `vh` so the tall/short URL bar on mobile Safari and Chrome is
   * accounted for where supported, with a plain viewport height as the fallback.
   */
  .site-header[data-nav-open="true"] {
    height: 100vh;
    height: 100dvh;
  }

  .site-header[data-nav-open="true"] .header-inner {
    height: 100%;
    /*
     * Brand bar and CTA take what they need; the link column takes everything
     * left over. No magic number, so it adapts to a taller logo or a wrapped
     * button without anyone having to re-measure.
     */
    grid-template-rows: auto 1fr auto;
  }

  .brand {
    grid-area: brand;
  }

  .nav {
    grid-area: nav;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    /*
     * Both of these override base `.nav` rules that are correct for the desktop
     * row and actively broken for a scrollable mobile column. Neither is tidying.
     *
     * `flex-wrap: nowrap` — the base sets `wrap`. With `flex-direction: column`
     * and the `max-height` the open panel carries, a wrapping column flex
     * container lays its overflow out in ADDITIONAL COLUMNS rather than
     * scrolling: the panel rendered as three side-by-side columns (Home, then the
     * activity list, then Hire/Gift Cards/About/Contact) with a horizontal
     * scrollbar.
     *
     * `justify-content: flex-start` — the base sets `center`, which on this axis
     * centres the column vertically. Once the content is taller than the box that
     * makes it overflow BOTH ends equally, and overflow past a scroll container's
     * start edge is unreachable: measured `scrollTop: 0` with the first item 296px
     * ABOVE the container's top, so Home and the Activities toggle could not be
     * scrolled back to at all. This is the well-known centred-flexbox overflow
     * trap; start-alignment is the fix.
     */
    flex-wrap: nowrap;
    justify-content: flex-start;
  }

  /*
   * The rule below rules the TOP-LEVEL rows. `.nav a` also matches every link
   * inside an open submenu, so `.nav-menu a` has to take the border back off —
   * see the separator rules further down. Without that each card in the
   * Activities and Learn menus drew its own line above itself, including the
   * first one.
   */
  .nav a {
    padding: 12px 4px;
    border-top: 1px solid var(--line);
  }

  /*
   * THE OPEN PANEL IS ITS OWN SCROLL CONTAINER, and the page behind it is frozen.
   *
   * Without this the panel is just a tall grid row inside a `position: sticky`
   * header — part of normal page flow, with nothing to scroll *within*. Dragging
   * on the menu scrolled the document behind it instead, so on an operator with
   * twenty activities you had to scroll the entire page to the bottom before the
   * end of the menu came into view. Reported from a real phone; it is not
   * reproducible at desktop width because the panel never opens there.
   *
   * Three parts, all needed:
   *  - `max-height` + `overflow-y` make the list a scroller in its own right. The
   *    200px reserve is the bar above it plus the Book Now row below, so the CTA
   *    never gets pushed off-screen by a long catalogue.
   *  - `overscroll-behavior: contain` stops the scroll chaining onward to the
   *    document once the list hits its end — the specific thing that made this
   *    feel broken rather than merely long.
   *  - `dvh` so the reserve tracks mobile browser chrome as it hides and shows;
   *    the `vh` line above it is the fallback for browsers without `dvh`.
   */
  .site-header[data-nav-open="true"] .nav {
    /*
     * `.header-inner` sets `align-items: center`, which is right for the desktop
     * row and wrong here: it centred the link column inside its 1fr row, leaving
     * ~200px of dead space above AND below the links on a 390x844 phone.
     * Stretching makes the column own the whole row so the links start at the
     * top and the space falls below them.
     */
    align-self: stretch;
    /*
     * `min-height: 0` is load-bearing: a grid item's default `min-height: auto`
     * refuses to shrink below its content, so without it a long menu would push
     * the CTA off the bottom of the screen instead of scrolling within its row.
     */
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--line) transparent;

    /*
     * Bleed the SCROLL BOX out through the wrap's 20px padding, then put that
     * padding back on the inside.
     *
     * A scroll bar belongs to the box that scrolls, and without this that box is
     * `.nav` at its natural width — 20px short of the screen on each side, while
     * the panel the visitor sees is the full-bleed header behind it. The bar
     * therefore drew *inside* the panel, sitting on top of the link column
     * roughly where the row borders end, instead of at the panel's edge where a
     * scroll bar is meant to be.
     *
     * Bleeding the box to the viewport edge puts the bar where it belongs; the
     * inner padding keeps every link and row border on the same alignment they
     * had, so nothing else moves.
     */
    margin-inline: -20px;
    padding-inline: 20px;
  }

  /*
   * Freeze the document while the panel is open. Set by the header script, so it
   * can never strand a no-JavaScript visitor with an unscrollable page — and
   * scoped to this media query, so a stale attribute cannot lock a desktop
   * viewport after a rotate or resize.
   */
  html[data-nav-locked="true"] {
    overflow: hidden;
  }

  /*
   * In the collapsed panel the submenu stops floating and becomes an indented
   * nested list — but it stays COLLAPSIBLE rather than being forced open.
   *
   * It used to be forced open, with the toggle hidden, which worked while the
   * toggle was a bare chevron sitting next to an "Activities" link. It is not the
   * toggle any more: the label moved inside the button, so hiding it removes the
   * word "Activities" from the panel and leaves a bare indented list of product
   * names introduced by nothing. And an operator with twenty activities got all
   * twenty stacked into the phone menu above About and Contact.
   *
   * So the button becomes a full-width row like the other links, and the list
   * opens beneath it on tap.
   */
  .nav-group {
    display: block;
    padding: 0;
    margin: 0;
  }

  /* Sized and ruled exactly like `.nav a`, with the chevron pushed to the end. */
  .nav-group-toggle {
    display: flex;
    width: 100%;
    justify-content: space-between;
    padding: 12px 4px;
    border-top: 1px solid var(--line);
    border-radius: 0;
    font-size: inherit;
    text-align: left;
  }

  .nav-menu {
    position: static;
    max-height: none;
    overflow: visible;
    padding: 0 0 0 14px;
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    opacity: 1;
    visibility: visible;
    transform: none;
    /*
     * Collapsed by default. `display` rather than the desktop
     * opacity/visibility pair, so the closed list takes no vertical space in a
     * panel that is already competing for the viewport.
     */
    display: none;
  }

  /*
   * Open on tap — and open unconditionally when the header was never marked
   * collapsible, which is the no-JavaScript case. Without that second selector a
   * visitor with JS disabled could never reach an activity page from a phone,
   * because nothing would ever set `data-open`.
   */
  .nav-group[data-open="true"] .nav-menu,
  .site-header:not([data-nav="collapsible"]) .nav-menu {
    display: block;
  }

  .nav-menu a {
    border-radius: 0;
    padding: 11px 4px;
    font-size: 0.95rem;
  }

  .header-cta {
    grid-area: cta;
    justify-self: stretch;
  }

  /* Full-bleed CTA in the panel — a right-aligned pill reads as an afterthought. */
  .header-cta .nav-cta {
    display: block;
    text-align: center;
    padding: 13px 18px;
  }

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

  .site-header[data-nav="collapsible"][data-nav-open="false"] .nav,
  .site-header[data-nav="collapsible"][data-nav-open="false"] .header-cta {
    display: none;
  }
}

/* --- Hero ---------------------------------------------------------------- */

.hero {
  position: relative;
  display: flex;
  /* Centred in the image, not sitting on its bottom edge. */
  align-items: center;
  justify-content: center;
  min-height: min(72vh, 620px);
  background: var(--brand-secondary);
  overflow: hidden;
}

.hero-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /*
   * Same hook as `.activity-hero img`: the operator's focal point arrives as an
   * inline `--hero-pos`, and falls back to dead centre when there isn't one.
   */
  object-position: var(--hero-pos, 50% 50%);
}

/*
 * Below roughly 860px the hero box is TALLER than the source's aspect ratio, so
 * `cover` stops cropping vertically and starts cropping horizontally instead —
 * the opposite axis from the desktop case. A focal point tuned for the wide crop
 * is therefore the wrong one here, and on a phone the homepage hero's 45% X was
 * landing between the sailors and showing only boats.
 *
 * `--hero-pos-narrow` is optional: without it this falls back to `--hero-pos`,
 * and without that to dead centre, so nothing needs to opt in.
 */
@media (max-width: 860px) {
  .hero-image {
    object-position: var(--hero-pos-narrow, var(--hero-pos, 50% 50%));
  }
}

/*
 * Centre-weighted, because the content is centred. The previous gradient was
 * bottom-heavy (0.78 at the foot, 0.12 at the head) to sit behind bottom-aligned
 * text; against centred text that puts the lightest part of the scrim exactly
 * where the headline is.
 */
.hero-overlay {
  position: absolute;
  inset: 0;

  /* ---------------------------------------------------------------------
     THE DIALS. These four numbers are the whole appearance of the scrim, and
     they are here rather than inline so there is ONE place to change them —
     the breakpoints below only move the ellipse, never the darkness.

       --hero-scrim-peak  how dark it is dead centre, behind the headline
       --hero-scrim-mid   how dark at the edge of the text block
       --hero-scrim-edge  the last trace before it fades out
       --hero-scrim-rx    how wide the dark area is (per breakpoint)

     Lower peak/mid = brighter photo and less contrast behind the text. The
     text-shadow on .hero-content h1 / .hero-strapline is what lets these sit
     lower than they otherwise could.
     --------------------------------------------------------------------- */
  --hero-scrim-peak: 0.2;
  --hero-scrim-mid: 0.1;
  --hero-scrim-edge: 0.04;
  --hero-scrim-rx: 100%;

  /*
   * A SCRIM, not a wash.
   *
   * The linear version this replaces ran a flat 0.58 across the middle band,
   * and that number was set by one thing: the white sail sitting directly
   * behind the headline. Measured against the photo, 16% of the area under the
   * text is brighter than 50% luminance and peaks at rgb(252,255,254) — white
   * text on that is 1.01:1. Sizing a full-plate wash for the worst pixel
   * dragged the MEDIAN to 9.5:1, about twice what it needs, and the whole
   * photograph paid for it. So the darkness is spent where the text is.
   *
   * NO MATH FUNCTIONS IN THE RADIUS. A gradient radius resolves a percentage
   * against its own axis, so `max()`/`min()` mixing a percentage with a length
   * is INVALID there and Chrome drops the entire `background` declaration —
   * the overlay silently becomes nothing at all, which is not visible as an
   * error, only as a hero that looks oddly bright. `var()` holding a plain
   * percentage is fine, which is why the radius is a variable. Check any change
   * here against getComputedStyle(...).backgroundImage, never against how the
   * page looks.
   */
  background: radial-gradient(
    ellipse var(--hero-scrim-rx) 95% at 50% 50%,
    rgba(var(--hero-overlay-rgb), var(--hero-scrim-peak)) 0%,
    rgba(var(--hero-overlay-rgb), var(--hero-scrim-mid)) 62%,
    rgba(var(--hero-overlay-rgb), var(--hero-scrim-edge)) 85%,
    rgba(var(--hero-overlay-rgb), 0) 100%
  );
}

/*
 * Only the ellipse changes with width, because the text block does NOT shrink
 * in step with the frame — it is capped at 820px and then tracks the viewport.
 * One fixed radius either leaves the text's own corners outside the scrim on a
 * mid-width screen, or covers the whole frame on a wide one and defeats the
 * point. Wide enough here that the ellipse can pull in and start giving the
 * edges of the photograph back.
 */
@media (min-width: 1200px) {
  .hero-overlay {
    --hero-scrim-rx: 72%;
  }
}

/*
 * At 55% the horizontal radius is under half the box, which is what lets the
 * corners reach a true zero: the frame's corner lands at ~1.05 along the
 * gradient ray, past the last stop, while the text block's corners are at ~0.60.
 */
@media (min-width: 1600px) {
  .hero-overlay {
    --hero-scrim-rx: 55%;
  }
}

/*
 * Accreditation pill above the headline.
 *
 * Solid white rather than a translucent tint: the RYA mark is a JPEG on a white
 * field, so anything that lets the photo through would show as a pale rectangle
 * around it. The pill is the mark's background, extended to hold the wording.
 *
 * The parent centres inline content, so the `<p>` needs no alignment of its own
 * — `inline-flex` is what keeps the pill hugging its text instead of spanning
 * the column.
 */
.hero-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 18px;
  padding: 5px 14px 5px 6px;
  border-radius: 999px;
  background: #ffffff;
  color: var(--ink);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
  box-shadow: 0 2px 10px rgba(7, 21, 28, 0.18);
}

.hero-badge img {
  width: 26px;
  height: 26px;
  border-radius: 999px;
  display: block;
}

.hero-content {
  /*
   * `position: relative` is LOAD-BEARING, not tidiness. `.hero-overlay` is
   * absolutely positioned and sits earlier in the DOM; without a position here
   * the overlay paints on top of the copy and the whole hero block vanishes.
   */
  position: relative;
  color: #ffffff;
  padding-top: 80px;
  padding-bottom: 80px;
  max-width: 820px;
  text-align: center;
}

/*
 * Local insurance, so the scrim above does not have to be sized for the
 * brightest pixel in the frame. A shadow only darkens what is immediately
 * behind the letterforms, which is exactly where the sail causes trouble; two
 * layers because the tight one gives the edge definition and the broad one does
 * the lifting.
 */
.hero-content h1,
.hero-strapline {
  text-shadow:
    0 1px 3px rgba(var(--hero-overlay-rgb), 0.55),
    0 2px 16px rgba(var(--hero-overlay-rgb), 0.75);
}

.hero-content h1 {
  margin-bottom: 0.3em;
}

.hero-strapline {
  font-size: clamp(1.05rem, 2.2vw, 1.3rem);
  margin-bottom: 1.6em;
  /* The measure still caps at 46ch; `auto` margins keep that column centred. */
  max-width: 46ch;
  margin-left: auto;
  margin-right: auto;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin: 0;
}

/* --- Sections ------------------------------------------------------------ */

.section {
  padding: 72px 0;
}

.section-tall {
  padding: 120px 0;
}

.section-muted {
  background: var(--surface-muted);
}

/*
 * A section that CONTINUES the surface of the one above it rather than starting
 * a new one. Two stacked `.section`s pay 72px of padding each, so a 144px void
 * opens between two bands that are meant to read as one zone. Used by the
 * review band under the related-products band at the foot of a product page.
 */
.section-stack {
  padding-top: 0;
}

/*
 * Head and foot of a band inside a `.wrap`. The head sits above a grid that is
 * full-width, so it needs its own bottom margin; `.lede`-free heads would
 * otherwise butt straight onto the cards.
 */
.section-head {
  margin-bottom: 30px;
}

.section-foot {
  margin: 30px 0 0;
}

.section-cta {
  background: var(--brand-secondary);
  color: #ffffff;
}

/*
 * Fixed-background section. One photo, heavily washed out behind the copy, that
 * stays put while the page scrolls past it.
 *
 * The scrim is a two-stop gradient rather than a flat rgba() fill so the wash is
 * slightly lighter in the middle of the section than at its edges, which keeps
 * the section reading as one surface where it meets the plain white blocks above
 * and below it. Both stops sit at or above 0.86 alpha over --surface-muted, so
 * --ink (#172127) on top clears AA-large several times over; the photo is
 * texture, never contrast.
 *
 * `background-attachment: fixed` is gated to `(hover: hover) and (pointer: fine)`.
 * iOS Safari does not honour it: it sizes the layer to the viewport and then
 * paints it as if it were `scroll`, so the crop jumps as the section enters. A
 * coarse pointer falls through to the default `scroll` below and simply gets a
 * static, correctly-cropped photo, which is the honest degradation.
 *
 * A fixed layer is also a compositing cost on every scroll frame, which is the
 * second reason not to hand it to phones.
 */
.section-parallax {
  background-color: var(--surface-muted);
  background-image: linear-gradient(
      180deg,
      rgba(245, 248, 250, 0.93) 0%,
      rgba(245, 248, 250, 0.86) 45%,
      rgba(245, 248, 250, 0.93) 100%
    ),
    url("https://api.swellbase.io/storage/v1/render/image/public/company-media/dc1510e9-c89f-4e03-8620-c67b37c6a19c/3d3e4c90-55ac-48b8-aa4c-0d5753639707.jpg?width=1800&resize=contain");
  background-repeat: no-repeat;
  background-position: center 40%;
  background-size: cover;
  /*
   * The wash alone leaves the join with the white sections above and below a
   * hard line of photo. A hairline rule reads as deliberate where a raw edge
   * reads as a rendering fault.
   */
  border-block: 1px solid var(--line);
}

@media (hover: hover) and (pointer: fine) {
  .section-parallax {
    background-attachment: fixed;
    /*
     * A fixed layer is painted against the VIEWPORT, not the section, so
     * `cover` is now covering the whole window. `center` is right for that box;
     * the 40% bias above is for the scrolling case, where the box is the
     * section and the horizon wants to sit high.
     */
    background-position: center center;
  }
}

/*
 * Parallax IS motion — content moving at a different rate from the page is
 * exactly what the preference is about. Reduced motion pins the photo back to
 * the section so it travels with everything else.
 */
@media (prefers-reduced-motion: reduce) {
  .section-parallax {
    background-attachment: scroll;
    background-position: center 40%;
  }
}

.section-cta h2 {
  color: #ffffff;
}

.page-head {
  padding: 64px 0 0;
}

/*
 * A page head and the section under it are one unit. Left to their own paddings
 * they stacked 8px + 72px, dropping ~80px of dead space between a heading and
 * the first thing it introduces.
 */
.page-head + .section {
  padding-top: 32px;
}

.prose p {
  font-size: 1.05rem;
}

/* --- Aggregate rating line ------------------------------------------------ */

/*
 * A plain line under the heading, not a boxed badge — the section reads
 * heading, then rating, then quotes, all centred.
 */
.reviews-rating-inner {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}

.reviews-stars {
  display: inline-flex;
  gap: 2px;
}

.star {
  width: 20px;
  height: 20px;
  fill: #f59e0b;
}

.star-empty {
  fill: var(--star-empty);
}

.reviews-score {
  font-weight: 700;
}

.reviews-count {
  color: var(--ink-soft);
  font-size: 0.95rem;
}

.social-proof-head h2 {
  margin-bottom: 12px;
}

/* --- Testimonials --------------------------------------------------------- */

.testimonials {
  margin-top: 36px;
}

/*
 * A scroll-snap row, not a JS-driven slider. The script only adds arrows and
 * disables them at the ends — with no JavaScript this is still a swipeable,
 * keyboard-scrollable strip, which is why the markup needs no fallback.
 *
 * Full-bleed on purpose: the track sits OUTSIDE `.wrap` so cards scroll off both
 * edges of the viewport, and pads itself back to the content gutter so the first
 * card lines up with the heading above.
 */
.testimonial-track {
  /*
   * A PAGED row: `--per-view` cards fill the track exactly, with symmetric edges.
   *
   * The earlier version padded the left to the text gutter and let cards run to
   * the right edge. That is asymmetric by construction — a big gap on the left,
   * cards bleeding off the right, then the mirror of it once scrolled to the end.
   * Sizing each card as a fraction of the track and padding both sides equally
   * makes every scroll position symmetric, at every width.
   */
  --gap: 20px;
  --edge: 20px;
  --per-view: 1;

  display: grid;
  grid-auto-flow: column;
  /*
   * `100%` on a grid container resolves against its CONTENT box, which already
   * excludes `padding-inline`. Subtracting the edges here as well double-counts
   * them and leaves exactly `2 * --edge` of slack — which shows up as a sliver of
   * the next card hanging off the right. Gaps only.
   */
  grid-auto-columns: calc(
    (100% - (var(--per-view) - 1) * var(--gap)) / var(--per-view)
  );
  gap: var(--gap);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  padding-inline: var(--edge);
  scroll-padding-inline: var(--edge);
  padding-block: 4px;
  scrollbar-width: none;
  /* Dragging the row should feel like grabbing it, not selecting text. */
  cursor: grab;
  -webkit-user-select: none;
  user-select: none;
}

/*
 * How many cards a view holds. Chosen so a card stays roughly 280–380px at every
 * width rather than stretching; the step is always ONE card regardless.
 */
@media (min-width: 640px) {
  .testimonial-track {
    --per-view: 2;
  }
}

@media (min-width: 900px) {
  .testimonial-track {
    --per-view: 3;
  }
}

@media (min-width: 1280px) {
  .testimonial-track {
    --per-view: 4;
  }
}

@media (min-width: 1680px) {
  .testimonial-track {
    --per-view: 5;
  }
}

.testimonial-track::-webkit-scrollbar {
  display: none;
}

/*
 * Fewer cards than a view holds: centre them at their natural width instead of
 * stretching one card across four columns' worth of space.
 */
.testimonials[data-carousel="off"] .testimonial-track {
  grid-auto-columns: minmax(280px, 340px);
  justify-content: center;
  cursor: default;
}

.testimonial-track[data-dragging="true"] {
  cursor: grabbing;
  scroll-behavior: auto;
  scroll-snap-type: none;
}

.testimonial {
  margin: 0;
  padding: 24px;
  height: 240px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/*
 * The quote scrolls inside its own box so cards keep a common height. Its
 * scrollbar is an overlay hint, not furniture: no track, and the thumb only
 * appears while the pointer is over the card or it has keyboard focus. A quote
 * short enough to fit shows nothing at all.
 */
/*
 * Hiding the scrollbar removed the only signal that a long quote continues, so
 * an overflowing quote gets a soft fade at its foot instead. `data-scrollable`
 * is set by the carousel script — CSS cannot ask whether an element overflows.
 */
.testimonial-quote[data-scrollable="true"] {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 28px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 28px), transparent);
}

/* Once scrolled to the end there is nothing more to hint at, so drop the fade. */
.testimonial-quote[data-scrollable="end"] {
  -webkit-mask-image: none;
  mask-image: none;
}

.testimonial-quote {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
  transition: scrollbar-color 0.2s ease;
}

.testimonial:hover .testimonial-quote,
.testimonial-quote:focus-visible,
.testimonial-quote:focus-within {
  scrollbar-color: var(--line) transparent;
}

.testimonial-quote::-webkit-scrollbar {
  width: 6px;
}

.testimonial-quote::-webkit-scrollbar-track {
  background: transparent;
}

.testimonial-quote::-webkit-scrollbar-thumb {
  background: transparent;
  border-radius: 3px;
  transition: background 0.2s ease;
}

.testimonial:hover .testimonial-quote::-webkit-scrollbar-thumb,
.testimonial-quote:focus-within::-webkit-scrollbar-thumb {
  background: var(--line);
}

.testimonial-quote p {
  margin: 0;
}

.testimonial footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex: 0 0 auto;
}

.testimonial cite {
  font-style: normal;
  font-weight: 600;
  color: var(--ink-soft);
}

/*
 * Per-testimonial provenance mark — the real brand assets from the app's own
 * `public/` directory, served from one CDN-cached copy rather than duplicated
 * into every operator bundle. No mark renders unless `data-review-source` is set,
 * so an unattributed quote just shows the name.
 */
.review-source {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

.testimonial[data-review-source="google"] .review-source {
  background-image: url("https://book.swellbase.io/google.png");
}

.testimonial[data-review-source="tripadvisor"] .review-source {
  background-image: url("https://book.swellbase.io/tripadvisor.png");
}

/* --- Testimonial carousel controls ---------------------------------------- */

.testimonial-controls {
  display: none;
  gap: 10px;
  justify-content: center;
  margin-top: 20px;
}

/* Only shown once the script has confirmed the track actually overflows. */
.testimonials[data-carousel="on"] .testimonial-controls {
  display: flex;
}

.testimonial-nav {
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.05rem;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
}

.testimonial-nav:hover:not(:disabled) {
  border-color: var(--brand-primary);
  color: var(--brand-secondary);
}

.testimonial-nav:disabled {
  opacity: 0.4;
  cursor: default;
}

/* --- Team ---------------------------------------------------------------- */

.team-grid {
  display: grid;
  gap: 24px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

.team-member {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 24px;
}

.team-role {
  color: var(--brand-secondary);
  font-weight: 600;
  margin-bottom: 0.6em;
}

/* --- Contact ------------------------------------------------------------- */

/*
 * `companies.address` is a multi-line text block and every place it lands is an
 * <address>, which browsers italicise by default and which collapses its newlines
 * without `pre-line`. Set globally rather than per-block so a hand-built page that
 * reuses the markup does not have to rediscover it.
 */
address {
  font-style: normal;
  white-space: pre-line;
}

.contact-grid {
  display: grid;
  gap: 40px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
}

.contact-block h2 {
  font-size: 1.25rem;
  margin-bottom: 0.9em;
}

.contact-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/*
 * Flex rows with a real gap. Without this the inline icon butted straight
 * against the link text with no space at all, which is what made the column look
 * broken rather than merely plain.
 */
.contact-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
}

/* A multi-line address should sit against the TOP of its pin, not centred on it. */
.contact-list .contact-item-block {
  align-items: flex-start;
}

.contact-list .contact-item-block .contact-icon {
  margin-top: 4px;
}

.contact-list address {
  margin: 0;
}

.contact-list a {
  font-weight: 500;
  text-decoration: none;
}

.contact-list a:hover {
  text-decoration: underline;
}




/* --- Footer --------------------------------------------------------------- */

/*
 * The footer carries the brand colour, taking over the job the closing CTA
 * section used to do — which is why that section is gone from every page.
 *
 * Everything in here is monochrome and inherits this foreground, the logo aside:
 * no chips, no per-icon backgrounds. That is what the earlier near-black version
 * could not do, because colour brand PNGs each needed their own light backing.
 */
/*
 * A LIGHT footer, and the accreditation logos are the reason.
 *
 * The RYA mark the operator supplied is a JPEG — the "On Light" variant — so it
 * has no alpha channel and carries a solid white background. On the old
 * `--brand-secondary` footer it would have rendered as a white tile, which is
 * exactly what the operator's own logo was already doing up there. (An AALA mark
 * sat beside it until 2026-09-29; removed at the operator's request.)
 *
 * Pure `--surface` rather than `--surface-muted`: against white those white
 * backgrounds disappear completely, where a grey would leave three faint
 * rectangles. The section above it carries the colour instead — see
 * `.section-brand` — so the page still ends on something other than a long run
 * of white.
 */
.site-footer {
  background: var(--surface);
  color: var(--ink-soft);
  border-top: 1px solid var(--line);
  padding: 64px 0 24px;
}

.site-footer a {
  color: var(--ink);
  text-decoration: none;
}

/*
 * Only TEXT links underline. `:where()` contributes no specificity, so this rule
 * scores (0,1,1) and a component further down can opt out with a plain
 * `.thing:hover { text-decoration: none }`.
 *
 * Written as `.site-footer a:hover` it scored (0,2,1) — the extra element
 * selector beating `.powered-by:hover` at (0,2,0) — which silently underlined the
 * Swellbase badge and would have caught every component link added later.
 */
:where(.site-footer) a:hover {
  text-decoration: underline;
}

/*
 * Self-arranging blocks, deliberately — see the comment in the footer partial.
 * `auto-fit` means the column count follows the number of blocks, so dropping a
 * subscribe block in later needs no change here. `min(100%, 280px)` keeps the
 * minimum from overflowing a narrow phone.
 */
.footer-inner {
  display: grid;
  gap: 36px 56px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}

/* Keeps addresses and link lists at a readable measure in a wide column. */
.footer-block > * {
  max-width: 38ch;
}

.footer-title {
  color: var(--ink);
  font-weight: 700;
  margin-bottom: 0.6em;
}

.footer-logo {
  display: inline-block;
  margin-bottom: 18px;
  line-height: 0;
}

.footer-logo img {
  max-height: 44px;
  width: auto;
}

/* Contact icons follow the footer foreground like everything else here. */
.site-footer .contact-icon {
  opacity: 0.65;
}

.site-footer address {
  font-style: normal;
  white-space: pre-line;
  /* Matches the contact links beside it rather than sitting a shade back. */
  color: var(--ink);
}

.footer-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.footer-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}

/* Optical alignment: the stroke icons read slightly high against the text. */
.contact-icon {
  flex: 0 0 auto;
  opacity: 0.7;
  transform: translateY(0.5px);
}

/* --- Social icons --------------------------------------------------------- */

.footer-social {
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
}

/*
 * Bare glyphs — no chip, no padding, no background. The base colours suit a light
 * background (the contact page); the footer overrides them for its own ground.
 */
.social-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
  transition: color 0.15s ease, transform 0.15s ease;
}

.social-icon:hover {
  color: var(--brand-secondary);
  transform: translateY(-2px);
  text-decoration: none;
}

.site-footer .social-icon {
  color: var(--ink-soft);
}

.site-footer .social-icon:hover {
  color: var(--brand-secondary);
}

/* Shared by the footer's social list and the contact page's. */
.social-row {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
}

.social-glyph {
  width: 22px;
  height: 22px;
}

/*
 * Copyright centred, badge centred beneath it — a column, not a spread row.
 */
.footer-legal {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  text-align: center;
  margin-top: 40px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
  font-size: 0.9rem;
}

.footer-legal p {
  margin: 0;
}

/*
 * The same badge as the booking funnel: same 22px mark, same gap, same muted
 * label beside it. It differs from the funnel in one deliberate way — no
 * greyscale filter, so the wordmark is always full colour rather than
 * desaturating until hovered.
 *
 * The generator's original `brightness(0) invert(1)`, which flattened the mark
 * to solid white, was for the old brand-coloured footer and is gone with it.
 */
.powered-by {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.8rem;
}

/*
 * The label is muted, the mark is not. The dimming used to sit on the whole
 * link at `opacity: 0.72`, which would now knock back the logo along with the
 * text — so it moves to the word "Powered by" alone, matching the funnel, where
 * that text is `text-gray-500` and the mark carries its own colour.
 */
.powered-by span {
  color: var(--ink-soft);
}

/*
 * `text-decoration: none` is load-bearing, not tidying: `:where(.site-footer) a:hover`
 * underlines every text link in the footer, and without this the badge picks
 * that up and draws a line under the wordmark.
 */
.powered-by:hover {
  text-decoration: none;
}

/*
 * `/logo.png` at the funnel's 22px, and no filter at all — the mark keeps its
 * own colour at rest and on hover.
 *
 * Nothing may be added here that depends on the footer being dark. The
 * generator's `brightness(0) invert(1)` flattened this to solid white for the
 * old brand-coloured footer, which on white was white-on-white: the image
 * loaded correctly and was simply invisible.
 */
.powered-by img {
  height: 22px;
  width: auto;
}

/*
 * The operator's floating Book Now button (#swellbase-floating-button, injected by
 * embed.js when they have it enabled) sits bottom-right at a fixed position. This
 * keeps the last section clear of it on narrow screens, where it would otherwise
 * land on the footer's legal line.
 */
/*
 * Clearance for the operator's floating Book Now button, which embed.js renders
 * as a FULL-WIDTH FIXED BAR — but only under `(max-width: 768px) and (hover: none)`.
 * This query mirrors that one exactly. Keyed on width alone it reserved 64px in a
 * narrowed desktop window where no bar exists, and reserved nothing between 640
 * and 768 on a phone, where one does.
 *
 * It is still unconditional in one sense: an operator with the button switched
 * off in their embed settings gets the gap anyway, because CSS cannot see that.
 * Delete this block by hand for them.
 */
@media (max-width: 768px) and (hover: none) {
  .footer-legal {
    padding-bottom: 76px;
  }
}

/* --- Rich text ------------------------------------------------------------ */

/*
 * `.prose` now carries markdown-rendered operator copy, not just generated
 * paragraphs, so it has to style everything react-markdown can emit — lists above
 * all, which is what "What's included" is almost always written as.
 */
.prose ul,
.prose ol {
  margin: 0 0 1em;
  padding-left: 1.3em;
}

.prose li {
  margin-bottom: 0.4em;
}

.prose li::marker {
  color: var(--brand-primary);
}

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

.prose h3 {
  font-size: 1.1rem;
  margin-top: 1.4em;
}

.prose a {
  color: var(--brand-secondary);
}

.prose strong {
  font-weight: 650;
}

.prose blockquote {
  margin: 0 0 1em;
  padding-left: 16px;
  border-left: 3px solid var(--line);
  color: var(--ink-soft);
}

/* --- Activity page -------------------------------------------------------- */

/*
 * Copy left, booking right — the layout this page exists for.
 *
 * `align-items: start` is load-bearing, not cosmetic: a grid item is stretched to
 * the row height by default, and a stretched item is exactly as tall as its row,
 * so `position: sticky` inside it has nothing to travel through and silently does
 * nothing.
 */
/*
 * Full-bleed product hero. Outside `.wrap`, so it runs edge to edge; a fixed
 * aspect ratio with `cover` so every operator's photo — portrait, landscape,
 * whatever — produces the same banner.
 */
.activity-hero {
  width: 100%;
  aspect-ratio: 12 / 5;
  max-height: 460px;
  overflow: hidden;
  background: var(--surface-muted);
}

.activity-hero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /*
   * The operator's own focal point, passed in per page as `--hero-pos` from
   * activity_media.image_position — the same X/Y percent pair they set in the
   * app. The generator never emitted it, so every hero was centre-cropped and
   * any subject off-centre (or any portrait source) lost its head to the 12/5
   * letterbox. Falls back to centre where they never set one.
   *
   * This only works because the hero <img> is requested WITHOUT the server-side
   * `height=1000&resize=cover` crop: that cropped to 2.4:1 before the browser
   * saw the file, and object-position cannot recover discarded pixels.
   */
  object-position: var(--hero-pos, 50% 50%);
}

.activity-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 370px;
  align-items: start;
  gap: 52px;
  padding-top: 44px;
  padding-bottom: 64px;
}

/*
 * The overlap: the whole grid is pulled up over the foot of the hero, and the copy
 * column is pushed back down by the same amount. Net effect — the booking card
 * floats on the image while the article still starts below it.
 *
 * DO NOT move this offset onto `.activity-aside` as a negative margin, which is
 * the obvious way to write it. A sticky element can only travel within its
 * containing block, and a negative top margin on the sticky item itself spends
 * that travel before it starts: measured, the card stuck at ~400px of scroll and
 * came unstuck again by ~700px, scrolling away with most of the article still to
 * read. Offsetting the CONTAINER leaves the aside at its natural position inside
 * the grid area, so it has the full column height to travel through.
 */
.activity-layout-overlap {
  margin-top: -180px;
}

.activity-layout-overlap .activity-main {
  padding-top: 180px;
}

.activity-header {
  margin-bottom: 34px;
}

.activity-header h1 {
  font-size: clamp(1.9rem, 4vw, 2.7rem);
  margin-bottom: 0.35em;
}

/*
 * The funnel's quick-info pills. Rounded, filled with the muted surface, icon then
 * value — the same shape the booking flow uses, sized up slightly for a page whose
 * body text is 17px rather than the funnel's 14px.
 */
.activity-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 20px 0 0;
  padding: 0;
  list-style: none;
}

.activity-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border-radius: 999px;
  background: var(--surface-muted);
  font-size: 0.92rem;
  line-height: 1.3;
}

.activity-pill-icon {
  flex: 0 0 auto;
  color: var(--ink-soft);
}

.activity-intro {
  margin-bottom: 34px;
}

/* --- Activity collapsible sections ---------------------------------------- */

/*
 * The booking funnel's `CollapsibleSection` stack, rebuilt with native <details>:
 * a top rule, one hairline between rows, an icon + title on the left and a chevron
 * on the right. Same headings, same order, same icons — see `activitySectionsMarkup`.
 */
.activity-sections {
  border-top: 1px solid var(--line);
}

.activity-section {
  border-bottom: 1px solid var(--line);
}

.activity-section-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 8px;
  border-radius: 8px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.15s ease;
  /* Removes the default disclosure triangle; the chevron on the right replaces it. */
  list-style: none;
}

/* Safari puts the triangle back unless this pseudo-element is hidden too. */
.activity-section-summary::-webkit-details-marker {
  display: none;
}

.activity-section-summary:hover {
  background: var(--surface-muted);
}

.activity-section-title {
  display: flex;
  align-items: center;
  gap: 12px;
}

.activity-section-icon {
  flex: 0 0 auto;
}

.activity-section-chevron {
  flex: 0 0 auto;
  color: var(--ink-soft);
  transition: transform 0.2s ease;
}

.activity-section[open] .activity-section-chevron {
  transform: rotate(180deg);
}

.activity-section-body {
  padding: 0 8px 18px;
  color: var(--ink-soft);
}

.activity-section-body > :last-child {
  margin-bottom: 0;
}

/* --- Booking panel -------------------------------------------------------- */

/*
 * 88px = the 68px sticky header plus 20px of air, so the panel never slides under
 * the header it shares the viewport with.
 */
.activity-aside {
  position: sticky;
  top: 88px;
}

.booking-panel {
  padding: 22px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 8px 22px rgba(var(--hero-overlay-rgb), 0.07);
}

.booking-panel-title {
  font-size: 1.1rem;
  margin-bottom: 14px;
}

/*
 * "From £30" on one line — a small muted prefix and a bold figure, following the
 * booking funnel rather than inventing a second treatment. Sized up from the
 * funnel's text-xs/text-lg to sit on a 17px page, but the relationship between
 * the two halves is the same.
 */
.booking-price {
  margin: 0 0 16px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--line);
}

.booking-price-label {
  font-size: 0.9rem;
  color: var(--ink-soft);
}

.booking-price-value {
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/*
 * embed.js replaces the placeholder div with an iframe and then drives its height
 * from inside via a resize message. This is only what it looks like before the
 * first message lands — a reserved block rather than a 150px default that would
 * visibly jump.
 */
.booking-panel iframe {
  width: 100%;
  border: 0;
  display: block;
}

.booking-panel-note {
  margin: 16px 0 0;
  font-size: 0.92rem;
}

/* --- Hire page ------------------------------------------------------------ */

.hire-block-headline {
  margin: -0.3em 0 1.4em;
  color: var(--ink-soft);
}

.hire-block + .hire-block {
  margin-top: 52px;
  padding-top: 44px;
  border-top: 1px solid var(--line);
}

.hire-block iframe {
  width: 100%;
  border: 0;
  display: block;
}

/*
 * The two-column split cannot hold a readable measure and a usable calendar much
 * below this, so the panel moves under the copy — and stops being sticky, because
 * a panel pinned to a phone viewport is a panel covering the article.
 */
@media (max-width: 900px) {
  .activity-layout {
    grid-template-columns: minmax(0, 1fr);
    gap: 40px;
  }

  /*
   * One column: hero, then copy, then the booking card. Both the overlap and the
   * stickiness go — there is no second column to float over, and a card pinned to
   * a phone viewport is a card covering the article.
   */
  .activity-aside {
    position: static;
  }

  .activity-layout-overlap {
    margin-top: 0;
  }

  .activity-layout-overlap .activity-main {
    padding-top: 0;
  }

  .activity-hero {
    aspect-ratio: 3 / 2;
  }
}

@media (max-width: 640px) {
  .section {
    padding: 52px 0;
  }

  .activity-layout {
    padding-top: 28px;
  }

  .activity-section-summary {
    padding: 14px 2px;
  }
}

/* ===========================================================================
   Category grid — the homepage's product section.

   Findhorn sell 29 activities across 5 operator-defined categories
   (`booking_flow_folders`). A flat list of 29 is not a homepage, so the section
   leads with the categories and lets the funnel do the drilling-down: each card
   links to `/findhornwatersports?folder=<slug>`, which is the same grouping the
   operator maintains in their booking flow. Add a category there and this
   section needs a card adding here — the bundle is a snapshot, not a sync.

   Two feature cards then three standard ones, on a 6-column grid: `span 3`
   twice fills row one, `span 2` three times fills row two. That is an editorial
   hierarchy, not a fallback — five equal cards would leave a ragged 3+2.

   Deliberately NO price on these cards. A "from" price is honest for one
   product and misleading for a category: "A Day on the Water" spans a £5
   wetsuit hire and a £75 powerboat session, and "Qualifications" a £15 online
   safeguarding course and £355 of powerboat training. Prices live on the
   activity pages, where they are per-product and carry the structured data.
   =========================================================================== */
.category-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 24px;
}

.category-card {
  grid-column: span 2;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
  transition: transform 0.18s ease, box-shadow 0.18s ease,
    border-color 0.18s ease;
}

.category-card[data-feature="true"] {
  grid-column: span 3;
}

.category-card:hover,
.category-card:focus-visible {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--brand-primary) 40%, var(--line));
  box-shadow: 0 14px 30px rgba(var(--hero-overlay-rgb), 0.13);
}

.category-media {
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--surface-muted);
}

/*
 * The feature cards are twice the width of the standard ones, so an identical
 * 16:9 crop would make them twice as tall and unbalance the row. A shallower
 * ratio keeps both rows to a similar card height.
 */
.category-card[data-feature="true"] .category-media {
  aspect-ratio: 21 / 9;
}

.category-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.4s ease;
}

.category-card:hover .category-media img {
  transform: scale(1.04);
}

.category-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 8px;
  padding: 20px;
}

.category-title {
  font-size: 1.15rem;
  font-weight: 650;
  line-height: 1.3;
}

.category-headline {
  flex: 1;
  color: var(--ink-soft);
  font-size: 0.96rem;
  line-height: 1.5;
}

.category-more {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--brand-primary);
  font-size: 0.94rem;
  font-weight: 600;
}

.category-more svg {
  transition: transform 0.18s ease;
}

.category-card:hover .category-more svg {
  transform: translateX(3px);
}

@media (max-width: 900px) {
  /* Both rows collapse to 2-up, so features and standards stop differing. */
  .category-card,
  .category-card[data-feature="true"] {
    grid-column: span 3;
  }

  .category-card[data-feature="true"] .category-media {
    aspect-ratio: 16 / 9;
  }
}

@media (max-width: 620px) {
  .category-grid {
    gap: 18px;
  }

  .category-card,
  .category-card[data-feature="true"] {
    grid-column: span 6;
  }
}


/* ===========================================================================
   Booking-panel photo carousel.

   Sits at the top of the sticky booking panel on a product page, above the
   "from" price. It carries photos 2..n — photo 1 is the page's hero, and
   repeating it here would open the strip on the image the visitor is already
   looking at. Pages whose activity has only one photo get no carousel at all
   rather than a one-slide one, so the markup is absent, not hidden.

   Scroll-snap row, so it works as a swipeable strip with no JavaScript;
   gallery.js adds the arrows and dots and only then reveals them via
   [data-gallery-ready].
   =========================================================================== */
.booking-gallery {
  position: relative;
  /*
   * Height of the dots row (dot + its margin-top), declared once because two
   * rules need it: the dots themselves, and the arrow centring below. The row
   * is pinned to an exact height rather than left to line-height — an <li>
   * around a 6px button renders 28px tall by default, which silently pushed the
   * arrows 11px below the photo's centre.
   */
  --gallery-dots: 6px;
  /*
   * Space between the bottom of the photo and the dots. Raising this is all
   * that is needed to re-space the row: `--gallery-dots-row` is derived from
   * it, so the arrows re-centre on the photo by themselves.
   */
  --gallery-dots-gap: 14px;
  --gallery-dots-row: calc(var(--gallery-dots) + var(--gallery-dots-gap));
  /*
   * Full bleed to the panel's inner edge. The panel pads 22px on every side, so
   * the gallery cancels that on three of them and keeps a bottom margin to sit
   * off the price. Pulling to -22px lands on the inside of the panel's 1px
   * border, which is why the track's corner radius is one pixel tighter than
   * the panel's.
   */
  margin: -22px -22px 16px;
}

.booking-gallery-track {
  display: flex;
  /*
   * The track is a <ul>, so the UA's default 40px padding-left and 1em block
   * margin have to go. Without the reset each slide's `flex: 0 0 100%` resolves
   * against a content box 40px narrower than the panel, so slide 1 never fills
   * the frame and slide 2 peeks in at the right.
   */
  margin: 0;
  padding: 0;
  list-style: none;
  /*
   * No gap: the slides are edge to edge, so a gap would show a stripe of panel
   * between photos mid-swipe. gallery.js reads the computed column-gap when it
   * pages, so 0 needs no change there.
   */
  gap: 0;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Inside the panel's border, hence the -1px. Bottom corners stay square. */
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
  /* The strip scrolls; its own bar would sit under the dots and double up. */
  scrollbar-width: none;
}

.booking-gallery-track::-webkit-scrollbar {
  display: none;
}

.booking-gallery-slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--surface-muted);
}

.booking-gallery-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* --- Arrows: hidden until gallery.js marks the gallery ready -------------- */

/*
 * Arrows are a desktop-only, hover-only affordance.
 *
 * `(hover: hover) and (pointer: fine)` is the whole gate: a touch device has no
 * hover to rest in, and swiping the strip is the native gesture there, so the
 * arrows never appear at all rather than sitting permanently on top of the
 * photo. On a mouse they fade in over the carousel and fade out again.
 *
 * Opacity rather than `display` for the reveal, so the buttons stay in the tab
 * order and `:focus-within` can bring them up for a keyboard user who never
 * hovers anything. `:not(:disabled)` keeps the end-of-strip arrow hidden even
 * while hovering — without it the hover rule outranks the :disabled rule below.
 */
.booking-gallery-nav {
  display: none;
  position: absolute;
  /*
   * The gallery box is the photo plus the dots row (6px dot + 8px margin =
   * 14px), so a plain `top: 50%` would sit the arrows below the photo's centre.
   * Subtracting the dots row first centres them on the image itself.
   */
  top: calc((100% - var(--gallery-dots-row)) / 2);
  transform: translateY(-50%);
  width: 30px;
  height: 30px;
  padding: 0;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 2px 8px rgba(var(--hero-overlay-rgb), 0.22);
  color: var(--ink);
  cursor: pointer;
  transition: opacity 0.15s ease;
}

@media (hover: hover) and (pointer: fine) {
  .booking-gallery[data-gallery-ready="true"] .booking-gallery-nav {
    display: flex;
    opacity: 0;
    pointer-events: none;
  }

  .booking-gallery[data-gallery-ready="true"]:hover .booking-gallery-nav:not(:disabled),
  .booking-gallery[data-gallery-ready="true"]:focus-within .booking-gallery-nav:not(:disabled) {
    opacity: 1;
    pointer-events: auto;
  }
}

.booking-gallery-nav[data-gallery-prev] {
  left: 8px;
}

.booking-gallery-nav[data-gallery-next] {
  right: 8px;
}

/* --- Dots ----------------------------------------------------------------- */

.booking-gallery-dots {
  display: none;
  gap: 6px;
  justify-content: center;
  align-items: center;
  height: var(--gallery-dots);
  margin: var(--gallery-dots-gap) 0 0;
  padding: 0;
  list-style: none;
  line-height: 0;
}

.booking-gallery-dots li {
  display: flex;
}

.booking-gallery[data-gallery-ready="true"] .booking-gallery-dots {
  display: flex;
}

.booking-gallery-dot {
  width: var(--gallery-dots);
  height: var(--gallery-dots);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--line);
  cursor: pointer;
  transition: background-color 0.15s ease;
}

.booking-gallery-dot[aria-current="true"] {
  background: var(--brand-primary);
}

@media (prefers-reduced-motion: reduce) {
  .booking-gallery-track {
    scroll-behavior: auto;
  }
}

/* ===========================================================================
   Category cards in the navigation.

   The dropdowns list the operator's CATEGORIES, not their 28 products, each as
   an image with a name and the operator's own headline underneath. Modelled on
   the pattern Blue Coast use on bluecoastcullen.co.uk: landscape image above
   the text, one card per row on narrow screens.

   NOTE: these cards link into the booking funnel, so the per-activity pages in
   this bundle are no longer reachable from the navigation. They remain in
   sitemap.xml. If that matters, the fix is a real category PAGE per folder
   listing its activities — a bigger change than this one.
   =========================================================================== */
/*
 * MUST stay inside a min-width query.
 *
 * The collapsed panel hides a closed menu with `.nav-menu { display: none }`
 * from inside a max-width query, and a media query adds no specificity — so an
 * unscoped `.nav-menu-cards { display: grid }` later in this file simply beat
 * it. The result was that on mobile the Activities and Learn menus were stuck
 * permanently open at their 300px desktop width while About, which has no
 * cards class, toggled correctly.
 */
@media (min-width: 1025px) {
  .nav-menu-cards {
    /*
     * A single vertical column. There are only ever two or three categories per
     * menu, so stacking them keeps the menu narrow enough to sit under its own
     * trigger.
     */
    display: grid;
    grid-auto-flow: row;
    grid-auto-columns: auto;
    width: 300px;
    gap: 4px;
    min-width: 0;
    padding: 10px;
    /*
     * Taller than the 460px the generator caps a list menu at. Three stacked
     * cards are ~625px and a menu that scrolls with only three things in it
     * reads as broken. Still vh-bound, so a short laptop scrolls rather than
     * running the menu off the bottom of the screen.
     */
    max-height: min(84vh, 660px);
  }

  /*
   * A shallower crop than the 16/9 used elsewhere. Stacked vertically, three
   * 16/9 images made the menu 719px tall; this keeps the photos legible while
   * bringing the whole menu inside the cap above.
   */
  .nav-menu-cards .nav-card-media {
    aspect-ratio: 21 / 9;
  }

  /*
   * Hairline BETWEEN cards, matching what the collapsed mobile panel does with
   * `.nav-menu li + li`. `li + li` rather than a border on every card, so there
   * is no line above the first or below the last — a ruled list, not a stack of
   * outlined boxes. The extra padding-top keeps the line off the card below it;
   * the 4px grid gap supplies the space above.
   */
  .nav-menu-cards li + li {
    border-top: 1px solid var(--line);
    padding-top: 6px;
  }

  /*
   * The same hairline in the plain dropdowns (About), for consistency with the
   * card menus beside them and with the collapsed mobile panel.
   *
   * Scoped with `:not(.nav-menu-cards)` rather than broadening the rule above:
   * the card menus get their space above the line from the grid's 4px `gap`,
   * which a plain <ul> has none of, so they need different numbers to land on
   * the same visual result. Splitting the space across margin and padding puts
   * an equal gap either side of the line here.
   */
  .nav-menu:not(.nav-menu-cards) li + li {
    border-top: 1px solid var(--line);
    margin-top: 6px;
    padding-top: 6px;
  }
}

/*
 * `.nav-menu .nav-card`, not `.nav-card`: the generator styles every link in a
 * menu with `.nav-menu a { display: block; padding; border-radius; font-size }`,
 * which is (0,1,1) and outranks a single class. A bare `.nav-card` lost its
 * `display: flex`, so on mobile the 88px thumbnail rule was inert and the image
 * filled the row.
 */
.nav-menu .nav-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  gap: 8px;
  padding: 8px;
  border-radius: 8px;
  color: inherit;
  text-decoration: none;
  transition: background-color 0.15s ease;
}

.nav-menu .nav-card:hover,
.nav-menu .nav-card:focus-visible {
  background: var(--surface-muted);
}

.nav-card-media {
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 6px;
  background: var(--surface-muted);
}

.nav-card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.nav-card-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav-card-title {
  font-size: 0.95rem;
  font-weight: 650;
  line-height: 1.3;
}

.nav-card-sub {
  color: var(--ink-soft);
  font-size: 0.83rem;
  line-height: 1.4;
}

@media (max-width: 1024px) {
  /*
   * Inside the collapsed panel the cards stack, and the image shrinks to a
   * thumbnail beside the text — a column of 16:9 photos would push the rest of
   * the menu off a phone screen.
   */
  /*
   * Matches the pair that reveals a plain menu in the panel, so a card menu
   * opens and closes on exactly the same condition — including the
   * no-JavaScript case, where the header is never marked collapsible.
   */
  .nav-group[data-open="true"] .nav-menu-cards,
  .site-header:not([data-nav="collapsible"]) .nav-menu-cards {
    display: grid;
    grid-auto-flow: row;
    grid-auto-columns: auto;
    width: auto;
    max-height: none;
    gap: 2px;
    padding: 4px 0 8px 14px;
  }

  .nav-menu .nav-card {
    flex-direction: row;
    align-items: center;
    gap: 12px;
    padding: 8px 4px;
  }

  .nav-card-media {
    flex: 0 0 88px;
    aspect-ratio: 4 / 3;
  }

  .nav-card-sub {
    font-size: 0.8rem;
  }
}


@media (max-width: 1024px) {
  /*
   * An open panel sits above the booking embed's floating button.
   *
   * `embed.js` pins that button at z-index 2147483640, against the header's 20,
   * so it punched through the open menu. This is one below the button's own
   * modal overlay (2147483647), which must still cover everything when a
   * visitor actually opens the booking flow.
   *
   * Only while open: a sticky header permanently at that level would sit over
   * anything else the embed puts on the page.
   */
  .site-header[data-nav-open="true"] {
    z-index: 2147483646;
  }
}

/* ===========================================================================
   Footer accreditations.

   Drops into `.footer-inner` as a third `.footer-block`, which is the extension
   point that grid was built for: `auto-fit` turns two columns into three on its
   own and stacks them on a phone. No layout change was needed.

   The marks are square JPEGs on white, so they are requested with BOTH
   dimensions — `?width=200&height=200` — and never width alone: the render
   endpoint scales one axis and leaves the other, which on a 709x709 source
   returns a 400x709 image with the emblem cropped off both sides.
   =========================================================================== */
.footer-accreditations {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 18px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer-accreditations img {
  display: block;
  width: auto;
  /*
   * Bigger now the block carries no heading — the marks ARE the label, so they
   * take the space the title used to. Fluid rather than fixed so they hold
   * their weight on a wide screen without crowding a phone, where the footer
   * has collapsed to one column.
   */
  height: clamp(96px, 10vw, 136px);
  /* The sources are square; contain keeps either mark from being cropped. */
  object-fit: contain;
}

/* ---------------------------------------------------------------------------
   The closing CTA carries the brand colour now that the footer does not.

   Without this the page ran white -> muted -> white -> muted -> white and ended
   completely flat. Putting the block of colour on the last call to action gives
   the page somewhere to land and makes the strongest CTA the most prominent
   thing on it.
   --------------------------------------------------------------------------- */
.section-brand {
  background: var(--brand-secondary);
  color: #ffffff;
}

.section-brand h2 {
  color: #ffffff;
}

.section-brand .lede {
  color: rgba(255, 255, 255, 0.86);
}

/*
 * On the coloured band the primary button would be brand-on-brand, so the two
 * swap roles: solid white leads, and the outline sits on white at low opacity.
 */
.section-brand .button-primary {
  background: #ffffff;
  color: var(--brand-secondary);
}

.section-brand .button-primary:hover {
  background: rgba(255, 255, 255, 0.88);
}

.section-brand .button-secondary {
  color: #ffffff;
  border-color: rgba(255, 255, 255, 0.5);
}

.section-brand .button-secondary:hover {
  border-color: #ffffff;
  background: rgba(255, 255, 255, 0.1);
}

/*
 * A line of copy under the logo, so the identity column carries some weight.
 * Without it the light footer opened on a small logo and a lot of white.
 */
.footer-blurb {
  max-width: 34ch;
  margin: 0;
  font-size: 0.92rem;
  line-height: 1.55;
}

/*
 * The mark is pinned to the top of its column, level with the logo and the
 * "Get in touch" heading. Centring suited two logos side by side; a single one
 * centred in the column just floated, with no edge lining up with anything.
 */
.footer-block-marks {
  display: flex;
  align-items: flex-start;
}

@media (max-width: 1024px) {
  /*
   * Submenu rows are separated BETWEEN items, not ruled individually.
   *
   * `.nav a { border-top }` above is meant for the top-level rows, but it also
   * matches every link inside an open submenu — so each option carried a line
   * above it, including the first, which read as an outline around each card
   * rather than a list with separators.
   *
   * Taking it off and using `li + li` instead puts a hairline between
   * consecutive options and none before the first or after the last, which is
   * what a separated list should look like. It covers both menu shapes: the
   * image cards in Activities/Learn and the plain rows in About.
   */
  .nav-menu a {
    border-top: 0;
  }

  .nav-menu li + li {
    border-top: 1px solid var(--line);
  }
}

/* ===========================================================================
   Product grid — the standard body of a category page.

   One layout, used by every category page, so adding a category is a data
   change rather than a design decision. Cards link to the per-product pages in
   this bundle, which is the whole point of these pages existing: before them
   the 28 activity pages were reachable only from sitemap.xml, because the
   navigation and the homepage both linked straight into the booking funnel.

   Card content is lifted from each product page at build time — name, headline,
   pills and "from" price — so a card and the page it points at cannot disagree.
   =========================================================================== */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
  gap: 26px;
}

.product-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}

.product-card:hover,
.product-card:focus-visible {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--brand-primary) 40%, var(--line));
  box-shadow: 0 14px 30px rgba(var(--hero-overlay-rgb), 0.13);
}

.product-media {
  display: block;
  /*
   * 4:3, not 16:9, because that is the shape of the operator's photos.
   *
   * 19 of the 28 primary images are 4:3, which a 4:3 card shows WHOLE — no crop
   * at all, so the focal point never even comes into play for them. At 16:9
   * those same images lost a quarter of their height, and the handful of
   * portrait sources lost 58% of theirs, which is what made the cards look
   * zoomed in: you were seeing the middle of a photo, not the photo.
   */
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--surface-muted);
}

.product-media img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* Same operator focal point the product page's own hero uses. */
  object-position: var(--hero-pos, 50% 50%);
  transition: transform 0.4s ease;
}

.product-card:hover .product-media img {
  transform: scale(1.04);
}

.product-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 10px;
  padding: 20px;
}

.product-title {
  font-size: 1.1rem;
  font-weight: 650;
  line-height: 1.3;
}

.product-headline {
  flex: 1;
  color: var(--ink-soft);
  font-size: 0.94rem;
  line-height: 1.5;
}

/* The same pills as the product page, at card scale. */
.product-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.product-pill {
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--surface-muted);
  color: var(--ink-soft);
  font-size: 0.78rem;
  white-space: nowrap;
}

.product-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-top: 2px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

.product-price {
  font-size: 0.92rem;
  color: var(--ink-soft);
}

.product-price strong {
  color: var(--ink);
  font-size: 1.05rem;
  font-weight: 700;
}

.product-more {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--brand-primary);
  font-size: 0.9rem;
  font-weight: 600;
}

.product-more svg {
  transition: transform 0.18s ease;
}

.product-card:hover .product-more svg {
  transform: translateX(3px);
}

/* The lead-in above a category's grid. */
.category-intro {
  margin: 0 auto 40px;
  text-align: center;
}

/*
 * Headline face on the pieces that read as titles rather than body.
 *
 * Deliberately NOT applied to `.footer-title`, `.booking-panel-title`, the
 * nav's own links or `.nav-card-title`: those are small UI labels, and a slab
 * serif at 0.8-0.95rem reads as heavy rather than characterful.
 *
 * The nav card titles are the pointed case. The About menu's rows are plain
 * links on the body stack, so putting the display face on the Activities and
 * Learn cards made the three dropdowns disagree with each other. Navigation
 * should read as one component; the headline face belongs on the page.
 */
.category-title,
.product-title {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: 0;
}

/*
 * The hero strapline and section ledes stay on the body stack, but the hero H1
 * gets a touch more room: a slab serif at 3.4rem sets tighter than the sans did
 * and the two-line headline was closing up.
 */
.hero h1 {
  line-height: 1.12;
}

/*
 * Star row on a review card.
 *
 * Reuses `.reviews-stars` and `.star` so a card's stars are the same glyph and
 * colour as the live aggregate badge at the top of the section — two different
 * star shapes in one block would read as a mistake. Smaller here because the
 * card is a supporting element, not the headline number.
 */
.testimonial-stars {
  flex: 0 0 auto;
}

.testimonial-stars .star {
  width: 15px;
  height: 15px;
}

/*
 * The attribution line. `.testimonial footer` already lays this out as
 * space-between, so the name sits left and the Google mark right.
 */
.testimonial footer cite {
  font-style: normal;
  font-weight: 600;
  font-size: 0.92rem;
  color: var(--ink);
}


/* ===========================================================================
   Places to Stay.

   One shape for every entry, deliberately. The temptation was to give the four
   places with their own website a card each at the top, but Findhorn is a small
   place and these are the operator's neighbours — a layout that says "these
   four first, then the other forty" is a recommendation whether or not it means
   to be. So everything is a row in a group, and having your own website shows
   up as a tag rather than as position.

   Groups are by KIND of stay and rows within them by how many the place sleeps,
   because that is what somebody booking around a course filters on.
   =========================================================================== */
.stay-group + .stay-group {
  margin-top: 56px;
}

.stay-group-head {
  max-width: 640px;
  margin: 0 0 22px;
}

.stay-group-head h2 {
  margin: 0 0 6px;
  font-size: clamp(1.3rem, 2.4vw, 1.6rem);
}

.stay-group-head p {
  margin: 0;
  color: var(--ink-soft);
  font-size: 0.95rem;
  line-height: 1.6;
}

.stay-items {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 330px), 1fr));
  gap: 12px;
}

/*
 * The row is the <li>, not the <a>: the tag has to sit outside the link so it
 * is not read out as part of the link text, and so a hover on "Airbnb" does not
 * look like a second target.
 */
.stay-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}

.stay-item:hover,
.stay-item:focus-within {
  border-color: var(--brand-primary);
  box-shadow: 0 4px 14px rgba(7, 21, 28, 0.07);
}

/*
 * The link stretches over the whole row so the entire card is clickable, while
 * the accessible name stays just the property name and its detail line.
 */
.stay-link {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  flex: 1;
  text-decoration: none;
  color: inherit;
}

.stay-link::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.stay-name {
  font-weight: 600;
  font-size: 0.97rem;
  line-height: 1.35;
}

.stay-item:hover .stay-name,
.stay-item:focus-within .stay-name {
  color: var(--brand-primary);
}

.stay-meta {
  color: var(--ink-soft);
  font-size: 0.83rem;
  line-height: 1.45;
}

.stay-tag {
  flex-shrink: 0;
  align-self: flex-start;
  margin-top: 2px;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--surface-muted);
  color: var(--ink-soft);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.stay-tag-direct {
  background: #e0f2fe;
  color: #075985;
}

/* ===========================================================================
   Inline availability calendar on a category page.

   A white panel on the muted band, so the embed reads as one object rather than
   as loose furniture floating on the section's background. Capped at 860px
   rather than running the full 1120 wrap: the embed's own layout goes
   side-by-side above 640px with the month grid pinned at 340px, so any width
   past this just stretches the day list and leaves the calendar marooned at the
   left edge.
   =========================================================================== */
.booking-embed {
  max-width: 860px;
  margin: 0 auto;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}

.stay-disclaimer {
  margin: 56px auto 0;
  max-width: 640px;
  text-align: center;
  color: var(--ink-soft);
  font-size: 0.875rem;
  line-height: 1.6;
}

/* ===========================================================================
   Sailing boat on the "Find your day on the water" heading.

   Decorative punctuation, so the whole thing is aria-hidden in the markup and
   the heading reads correctly without it.
   =========================================================================== */
/*
 * Keeps the boat welded to the last word. An inline element is a valid break
 * opportunity, so without this the icon wraps alone onto the next line as soon
 * as the heading is tight — which it is at 390px.
 */
.sail-word {
  white-space: nowrap;
}

.sail-icon {
  /*
   * `inline-block` so the wrapper can be transformed at all — an inline box
   * ignores transform. Sized in `em` so it tracks the heading through its
   * clamp() rather than needing its own breakpoints.
   */
  display: inline-block;
  width: 0.82em;
  height: 0.82em;
  margin-left: 0.3em;
  color: var(--brand-primary);
  /*
   * Sits on the baseline-ish rather than the text bottom: the glyphs around it
   * are a slab serif with a tall x-height, and a boat hanging off the true
   * baseline reads as misaligned.
   */
  vertical-align: -0.04em;
  /* The swell: a slow rise and fall. */
  animation: sail-bob 4.3s ease-in-out infinite;
}

.sail-icon svg {
  display: block;
  width: 100%;
  height: 100%;
  /*
   * Pivot near the waterline, not the centre of the box, so it rolls the way a
   * hull does instead of spinning about its mast.
   */
  transform-origin: 50% 78%;
  /*
   * Deliberately NOT a multiple of the bob above. Matched periods would put the
   * boat at the top of every rise at exactly the same angle, which is the thing
   * that makes this kind of motion look like a loop rather than like water.
   */
  animation: sail-roll 6.7s ease-in-out infinite;
}

@keyframes sail-bob {
  0%, 100% { transform: translateY(0.06em); }
  50% { transform: translateY(-0.06em); }
}

@keyframes sail-roll {
  0%, 100% { transform: rotate(-5deg); }
  50% { transform: rotate(5deg); }
}

/*
 * Constant motion beside text is exactly what this preference is for: it sits
 * next to a heading somebody may be trying to read.
 */
@media (prefers-reduced-motion: reduce) {
  .sail-icon,
  .sail-icon svg {
    animation: none;
  }
}

/* --- Added pages: parking, terms, privacy; values on About ------------------ */

/*
 * Legal links sit between the copyright line and the Swellbase badge. They are
 * text links, so they pick up the footer's hover underline on their own.
 */
.footer-legal-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px 22px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A picture inside running copy: never wider than the column, never stretched. */
.prose-figure {
  display: block;
  width: 100%;
  max-width: 600px;
  height: auto;
  margin: 0 0 1.4em;
  border-radius: var(--radius);
}

/*
 * `.prose h2` zeroes its top margin because it normally opens a section. In a
 * long document it follows a paragraph instead, so it needs the space back.
 */
.prose p + h2,
.prose ul + h2,
.prose address + h2,
.prose img + h2 {
  margin-top: 1.8em;
}

.prose address {
  margin: 0 0 1em;
  /* The global rule is pre-line for DB-sourced addresses; these use <br />. */
  white-space: normal;
}

/*
 * Numbered clauses hang their number in the margin so the text lines up. The
 * number is taken out of flow rather than indented with text-indent: the space
 * between it and the text would otherwise push the first line out of line.
 */
.legal .clause {
  position: relative;
  padding-left: 3.4em;
}

.legal .clause > strong:first-child {
  position: absolute;
  left: 0;
}

.legal .clause-sub {
  padding-left: 5.6em;
}

.legal .clause-sub > strong:first-child {
  left: 1.4em;
}

.values-intro,
.values-outro {
  max-width: 760px;
}

.values-intro {
  margin: 0 auto 40px;
}

/*
 * Three columns, then two, then one. The first card spans the row at every
 * width, which leaves six for the grid: six divides by all three column counts,
 * so there is never a lone card on the last row.
 */
.values-grid {
  display: grid;
  gap: 24px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

.value-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 28px;
}

.value-card h3 {
  margin: 0 0 0.5em;
}

.value-card p {
  margin: 0;
  color: var(--ink-soft);
}

/* The lead card: icon beside the text, on the muted surface instead of a border. */
.value-card[data-lead="true"] {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 32px;
  padding: 36px 40px;
  background: var(--surface-muted);
  border-color: transparent;
}

.value-card[data-lead="true"] h3 {
  font-size: 1.35rem;
}

.value-card[data-lead="true"] p {
  font-size: 1.05rem;
  max-width: 68ch;
}

/*
 * Same construction as .sail-icon: the wrapper rises and falls, the svg inside
 * rolls, on periods that are not multiples of each other so the motion never
 * resolves into a visible loop. Own keyframes in px because .sail-icon's are in
 * em against a heading's font size.
 */
.value-icon {
  display: block;
  width: 36px;
  height: 36px;
  margin-bottom: 16px;
  color: var(--brand-primary);
  animation: value-bob 4.3s ease-in-out infinite;
}

.value-icon svg {
  display: block;
  width: 100%;
  height: 100%;
  animation: value-roll 6.7s ease-in-out infinite;
}

.value-card[data-lead="true"] .value-icon {
  flex: none;
  width: 64px;
  height: 64px;
  margin-bottom: 0;
}

/*
 * Negative delays start each icon part-way through its cycle, so seven icons on
 * one screen are out of step with each other instead of rising as a row.
 */
.value-card:nth-child(2) .value-icon { animation-delay: -1.1s; }
.value-card:nth-child(3) .value-icon { animation-delay: -2.6s; }
.value-card:nth-child(4) .value-icon { animation-delay: -0.5s; }
.value-card:nth-child(5) .value-icon { animation-delay: -3.4s; }
.value-card:nth-child(6) .value-icon { animation-delay: -1.9s; }
.value-card:nth-child(7) .value-icon { animation-delay: -3s; }

.value-card:nth-child(2) .value-icon svg { animation-delay: -4.2s; }
.value-card:nth-child(3) .value-icon svg { animation-delay: -1.3s; }
.value-card:nth-child(4) .value-icon svg { animation-delay: -5.5s; }
.value-card:nth-child(5) .value-icon svg { animation-delay: -2.4s; }
.value-card:nth-child(6) .value-icon svg { animation-delay: -0.7s; }
.value-card:nth-child(7) .value-icon svg { animation-delay: -3.8s; }

@keyframes value-bob {
  0%, 100% { transform: translateY(2px); }
  50% { transform: translateY(-2px); }
}

@keyframes value-roll {
  0%, 100% { transform: rotate(-5deg); }
  50% { transform: rotate(5deg); }
}

@media (prefers-reduced-motion: reduce) {
  .value-icon,
  .value-icon svg {
    animation: none;
  }
}

@media (max-width: 900px) {
  .values-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 600px) {
  .values-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .value-card[data-lead="true"] {
    display: block;
    padding: 28px;
  }

  .value-card[data-lead="true"] .value-icon {
    width: 48px;
    height: 48px;
    margin-bottom: 16px;
  }
}

.values-outro {
  margin: 40px auto 0;
  color: var(--ink-soft);
}
