/* ==========================================================================
   Riverview Vets - page layer

   Filled at BLD-03 on 2026-09-17. The last file in the cascade, so anything
   here can override a component rule, which is the point of the order.

   Every section of all three pages, in the order the landing page renders
   them, then the two legal pages. Transcribed from the 280 inline styles in
   the Claude Design export; where a value is not the export's, the comment
   above it says so and says why.

   The breakpoint is 860px everywhere, from the export's single matchMedia
   call. See the header of layout.css.

   Cascade order, set in the <link> tags and deliberate:
     tokens -> fonts -> base -> layout -> components -> page
   ========================================================================== */


/* --- Shared across sections ----------------------------------------------- */

/* Every <h2> that opens a band. #why's is a hair wider at the top of its
   clamp in the export, which is the only variation, and it is below. */
.section-heading {
  font-size: clamp(1.5rem, 3vw, 1.875rem);
  line-height: var(--lh-heading);
  font-weight: var(--fw-semibold);
  color: var(--forest);
}

/* The bordered eyebrow, used on the hero and on #mobile-vet. Both sit on a
   dark ground, so the base .eyebrow's muted ink is replaced by cream. The
   hero's narrow branch is the one exception and it is under .hero below. */
.eyebrow--badge {
  align-self: flex-start;
  padding: var(--sp-sm) var(--sp-lg);
  border: var(--border-width) solid rgba(245, 242, 236, .55);
  border-radius: var(--radius-control);
  color: var(--cream-100);
}

/* The chalk illustrations and the two that rise into view on scroll. The
   wrapper carries the position and the size; the image fills it. Doing it
   this way rather than on the <img> keeps the rules readable now that every
   image is inside a <picture>. */
.decoration {
  position: absolute;
  pointer-events: none;
  line-height: 0;
}

.decoration img {
  width: 100%;
  height: auto;
  display: block;
  mix-blend-mode: multiply;
}


/* --- Hero -----------------------------------------------------------------
   The export's two branches, collapsed. Above 860px the copy is overlaid on
   the photograph; at or below it, the two stack and the copy inverts to dark
   on cream. The markup is one tree and nothing is duplicated.
   -------------------------------------------------------------------------- */

.hero { position: relative; }

.hero__media {
  background: var(--forest-900);
  overflow: hidden;
}

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

.hero__body {
  display: flex;
  flex-direction: column;
}

.hero__lede { max-width: 30rem; }

@media (min-width: 861px) {

  .hero__media { height: clamp(540px, 70vh, 700px); }
  .hero__media img { object-position: 75% 50%; }

  /* Overlaid, vertically centred, inside the same 1240px column as every
     other band. The gutter floor is 20px here, not 24px, which is the
     export's own value for this one block. */
  .hero__body {
    position: absolute;
    inset: 0;
    justify-content: center;
    align-items: flex-start;
    gap: var(--sp-xl);
    padding-inline: calc(max(0px, (100% - var(--width-content)) / 2) + clamp(20px, 5vw, 80px));
  }

  .hero__body > * { max-width: 34rem; }

  .hero__heading {
    font-size: clamp(2.1rem, 4.6vw, 3rem);
    line-height: var(--lh-display);
    font-weight: var(--fw-bold);
    letter-spacing: var(--ls-display);
    color: var(--cream-50);
  }

  .hero__lede {
    font-size: clamp(1.0625rem, 1.6vw, 1.1875rem);
    line-height: var(--lh-lead);
    color: var(--cream-100);
  }

  .hero__cta { margin-top: var(--sp-xs); }
}

@media (max-width: 860px) {

  .hero__media { aspect-ratio: 4 / 3; }
  .hero__media img { object-position: 78% 50%; }

  .hero__body {
    gap: var(--sp-lg);
    padding: var(--sp-xxl) clamp(24px, 5vw, 32px);
  }

  /* Solid sage tint rather than an outline, because there is no photograph
     behind it here. */
  .hero .eyebrow--badge {
    background: var(--sage-100);
    color: var(--forest);
    border-color: transparent;
    padding: var(--sp-sm) var(--sp-md);
  }

  .hero__heading {
    font-size: clamp(1.75rem, 8vw, 2rem);
    line-height: 1.15;
    font-weight: var(--fw-bold);
    letter-spacing: var(--ls-heading);
    color: var(--forest);
  }

  .hero__lede {
    font-size: var(--fs-body);
    line-height: var(--lh-lead);
    color: var(--ink-700);
  }

  .hero__cta { width: 100%; }
}


/* --- Quote strip ----------------------------------------------------------
   Four quotes on one 16 second cycle, staggered by animation-delay. No
   JavaScript touches this and none should.
   -------------------------------------------------------------------------- */

.quote-strip {
  background: var(--grape-100);
  border-top: var(--border-width) solid var(--cream-300);
  border-bottom: var(--border-width) solid var(--cream-300);
  position: relative;
  overflow: hidden;
}

.quote-strip__inner {
  position: relative;
  height: clamp(150px, 17vw, 168px);
}

.quote {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-lg);
  text-align: center;
  padding: 0 clamp(24px, 7vw, 96px);
  animation: rvQuote 16s infinite;
}

.quote blockquote { max-width: 56rem; }

.quote blockquote p {
  font-family: var(--font-serif);
  font-size: clamp(1.5rem, 3.4vw, 2.375rem);
  line-height: 1.25;
  color: var(--forest);
  text-wrap: balance;
}

.quote figcaption {
  font-family: var(--font-sans);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-regular);
  color: var(--grape-700);
}

.quote--1 { animation-delay: 0s; }
.quote--2 { animation-delay: -4s; }
.quote--3 { animation-delay: -8s; }
.quote--4 { animation-delay: -12s; }

/* Reduced motion. The export's blanket rule renders this band EMPTY; the
   reasoning is written out at the bottom of base.css. Opting the strip out by
   name gives one quote, held still, which is the honest version of what the
   animation is for. BLD-09 re-checks this. */
@media (prefers-reduced-motion: reduce) {
  .quote { animation: none; opacity: 0; }
  .quote:first-child { opacity: 1; }
}


/* --- Founders -------------------------------------------------------------
   Three slides, all in the markup. The two chalk decorations are positioned
   against the 1240px column, not the viewport, which is why .founders is a
   centred container and not a full-bleed one; see layout.css.
   -------------------------------------------------------------------------- */

.founders {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: clamp(24px, 3vw, 32px);
}

/* The decorations are absolutely positioned and would otherwise paint over
   the text. */
.founders > :not(.decoration) { position: relative; }

.decoration--paw {
  left: 2px;
  top: clamp(24px, 4vw, 56px);
  width: clamp(40px, 4.2vw, 76px);
  animation: rvWobblePaw 2.4s steps(1, end) infinite;
}

.decoration--heart {
  right: clamp(8px, 2vw, 24px);
  top: clamp(28px, 5vw, 56px);
  width: clamp(72px, 8vw, 110px);
  animation: rvWobbleHeart 2.8s steps(1, end) infinite;
  animation-delay: -.7s;
}

/* BLD-04 CONTRACT. The dog rises out of the bottom edge when the section
   comes into view. main.js adds `is-revealed` to the wrapper and writes no
   styles. overflow:hidden on the wrapper is what hides it beforehand.

   In the export this sat in the same flex row as the team counts. It is a
   sibling of them in the markup, so it is positioned against the section
   instead; it lands in the same place and it does not push the row about. */
.decoration--dog-rise {
  right: 0;
  bottom: 0;
  width: clamp(120px, 14vw, 190px);
  overflow: hidden;
}

.decoration--dog-rise img {
  transform: translateY(110%);
  transition: transform 700ms var(--ease-standard);
}

.decoration--dog-rise.is-revealed img { transform: translateY(0); }

/* The four wide-only decorations. They keep loading="lazy" in the markup and
   that is not cosmetic: a display:none image still downloads if it is eager,
   so without it a phone pays for all four. BLD-05 finding 2. */
@media (max-width: 860px) {
  .decoration--paw,
  .decoration--heart,
  .decoration--dog-rise,
  .decoration--cat-face { display: none; }
}

.deck--founders {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 48px);
  align-items: stretch;
}

.founder {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 48px);
  align-items: stretch;
  width: 100%;
}

.founder__portrait {
  flex: 1 1 300px;
  min-width: min(100%, 280px);
  max-width: 400px;
}

.founder__portrait img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: 50% 22%;
  display: block;
  background: var(--cream-200);
  border-radius: var(--radius-card);
}

/* Transcribed per founder from the export's founders() array. */
#founder-2 .founder__portrait img,
#founder-3 .founder__portrait img { object-position: 50% 25%; }

.founder__body {
  flex: 2 1 460px;
  min-width: min(100%, 300px);
  max-width: var(--measure-prose);
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.founder__body .eyebrow { color: var(--teal); }

.founder__title {
  font-size: clamp(1.125rem, 2.2vw, 1.25rem);
  line-height: var(--lh-snug);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-heading);
  color: var(--forest);
}

/* The founder deck's arrows are 56px where the testimonial deck's are 64.
   Both draw from the same 64/128 ladder, so no extra file is fetched. */
.deck--founders ~ .deck-controls .deck-controls__arrow img {
  width: 56px;
  height: 56px;
}

.deck--founders ~ .deck-controls { gap: 20px; }

.team-count {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
  max-width: 640px;
}

.team-count__heading {
  font-size: var(--fs-body);
  line-height: 1.4;
  font-weight: var(--fw-medium);
  color: var(--forest);
}

.team-count__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-md);
  list-style: none;
  padding: 0;
}

.stat {
  background: var(--surface-calm);
  border-radius: var(--radius-card);
  padding: var(--sp-xl) var(--sp-md);
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

.stat__value {
  font-size: clamp(1.75rem, 3.4vw, 2.125rem);
  line-height: var(--lh-display);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-display);
  color: var(--teal);
}

.stat__label {
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
  color: var(--forest);
}


/* --- Mobile vet ----------------------------------------------------------- */

.mobile-vet {
  background: var(--grape);
  overflow: hidden;
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 48px);
  align-items: center;
}

.mobile-vet__body {
  flex: 1 1 380px;
  min-width: min(100%, 300px);
  max-width: 52ch;
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.mobile-vet .eyebrow--badge { padding: var(--sp-sm) var(--sp-md); }

.mobile-vet .section-heading { color: var(--cream-50); }

.mobile-vet__lede {
  font-size: 19px;
  line-height: var(--lh-lead);
  color: var(--cream-100);
}

.mobile-vet__media {
  flex: 1 1 420px;
  min-width: min(100%, 300px);
}

.mobile-vet__media img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  object-fit: contain;
  display: block;
  /* The van is drawn facing the other way in the artwork, and the 1.12 is the
     export's own overscale so it fills the box. */
  transform: scaleX(-1) scale(1.12);
}


/* --- Sage strip -----------------------------------------------------------
   The cat and the tail are positioned against the full-width band, which is
   what the export did, so this section is the bleed box and its inner is the
   centred container.
   -------------------------------------------------------------------------- */

.sage-strip {
  background: var(--sage);
  position: relative;
  overflow: hidden;
}

.sage-strip__inner {
  position: relative;
  padding-block: var(--sp-xxl);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-xs);
  text-align: center;
}

/* BLD-04 CONTRACT, the same shape as the dog. */
.decoration--cat-rise {
  left: clamp(4px, calc(22vw - 140px), 300px);
  bottom: 0;
  width: clamp(120px, 14vw, 180px);
  overflow: hidden;
}

.decoration--cat-rise img {
  transform: translateY(105%);
  transition: transform 700ms var(--ease-standard);
  /* The export did not blend this one, and blending it turns the black cat
     grey against the sage. */
  mix-blend-mode: normal;
}

.decoration--cat-rise.is-revealed img { transform: translateY(0); }

/* The cat sat ON the strip's copy on a phone, reported by the client
   2026-09-22. It is not a z-index accident and it is not the illustration
   being too big; it is that the cat is positioned against the full-width
   band while the copy is centred in the container, so the two are on
   independent x-axes. They stay apart on a desktop only because the band is
   wide enough for the copy to clear the cat's column.

   Measured on the built page, viewport widths not frame widths: the cat is a
   120 x 67 box at left:4px anywhere below 857px, and the collision starts at
   577px and gets worse all the way down. It clips 9px off the descender band
   of the lead from 577 to 500; below 500 it is on top of "Routine and
   emergency appointments" itself - 52px of that line at 401px wide, 87px of
   it at 331px, which is what the client saw.

   The fix stacks them rather than hiding the cat or shrinking it, because
   the strip is one of the few places the illustration set appears on a
   phone at all. The padding is a literal and not `var(--sp-xxl) + n`,
   because it is not page rhythm: 80px is the cat's own height at this width
   (120 / 1.8 = 67) plus 13px of air, measured from the same edge the cat is
   pinned to. If the cat's width clamp changes, this changes with it.

   The breakpoint is 599 and not 860, where the tail moves out: above 577px there
   is nothing to fix and a band of empty sage under the copy would be a
   regression on every tablet. 599 is the measured 577 plus room for the
   collision point to move if Figtree's metrics ever do, since it is the copy
   wrapping that drives it. */
@media (max-width: 599px) {
  .sage-strip__inner { padding-bottom: 80px; }
}

.sage-strip__lead {
  font-size: clamp(1.125rem, 2.2vw, 1.375rem);
  line-height: var(--lh-snug);
  font-weight: var(--fw-semibold);
  color: var(--forest);
}

.sage-strip__sub {
  font-size: 15px;
  line-height: 1.5;
  color: var(--forest);
}

/* The tail waving out of the sage strip. Decorative, on every width since
   2026-09-23 at the client's request; it was desktop only until then. main.js
   starts it; it does not autoplay, and preload="none" means nothing is
   fetched until it does.

   The sage ground is IN the video, baked by tools/build-video.sh, and that
   is why there is no filter or blend here. Until 2026-09-23 the video was on
   the artwork's cream ground and this rule knocked it out with
   filter: brightness(1.45) contrast(1.35) and mix-blend-mode: multiply. A
   physical iPhone composites a playing <video> on its own layer and drops
   the blend, so the client saw a pale square there in Safari and Firefox
   both. The cost of baking it is that the box is opaque: it can only sit on
   plain --sage, and if --sage changes the video has to be rebuilt. */
.tail-loop {
  position: absolute;
  right: clamp(16px, calc(4vw + 140px), 272px);
  bottom: 0;
  width: clamp(104px, 12vw, 152px);
  height: auto;
  display: block;
  pointer-events: none;
}

/* At and below 860 the tail mirrors the cat, which is pinned at left:4px from
   857 down, instead of following the desktop inset in towards the copy. The
   negative offset is not an overflow: the artwork's ink, measured across all
   145 frames of the loop, spans 30% to 78% of the box horizontally and never
   reaches the top 29%, so at the 104px minimum width the right 23px is empty
   ground and -12px leaves the ink 11px off the edge. The same measurement
   is why no extra padding is needed: 104px of box is 74px of ink, inside the
   80px the cat's rule below 599 already reserves under the copy. */
@media (max-width: 860px) { .tail-loop { right: -12px; } }

/* Both risers and the two wobbles. tokens/motion.css cannot reach these
   because their durations are literals, not --dur-* tokens. */
@media (prefers-reduced-motion: reduce) {
  .decoration--paw,
  .decoration--heart { animation: none; }

  .decoration--cat-rise img,
  .decoration--dog-rise img {
    transform: translateY(0);
    transition: none;
  }
}


/* --- Services ------------------------------------------------------------- */

.services {
  border-top: var(--border-width) solid var(--cream-300);
  display: flex;
  flex-direction: column;
  gap: clamp(20px, 3vw, 32px);
}

.services__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: clamp(16px, 2vw, 24px);
  list-style: none;
  padding: 0;
}

.service-card {
  background: var(--white);
  border: var(--border-width) solid var(--cream-300);
  border-radius: var(--radius-card);
  padding: clamp(24px, 2.5vw, 32px);
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--ink-700);
}


/* --- Where and when ------------------------------------------------------- */

.location {
  border-top: var(--border-width) solid var(--cream-300);
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 48px);
  align-items: center;
}

.location__body {
  flex: 1 1 420px;
  min-width: min(100%, 300px);
  max-width: 60ch;
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.location__body > p {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-700);
}

.location__points {
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
  padding: var(--sp-xs) 0 0;
  list-style: none;
}

.point {
  display: flex;
  gap: var(--sp-md);
  align-items: flex-start;
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--ink-700);
}

.point .icon {
  margin-top: var(--sp-xs);
  color: var(--teal);
}

.location__cta {
  align-self: flex-start;
  margin-top: var(--sp-sm);
}

.location__media {
  flex: 1 1 380px;
  min-width: min(100%, 280px);
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.location__media img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  display: block;
  background: var(--cream-200);
  border-radius: var(--radius-card);
}

.location__media figcaption {
  font-size: var(--fs-caption);
  color: var(--ink-500);
}

/* The illustrated location map, which replaced the exterior photograph. The
   rules above are a photo box: a fixed 16:10 with object-fit:cover, a cream
   card behind it and a radius on the corners. None of the three is right for
   this one.

   The ratio is the master's own 4507 x 2999. That is not tidying. The
   derivative is matted to cream-50 rather than carrying its alpha - the
   reason is in tools/build-images.sh - so at 16:10 the browser would crop the
   bottom of the legend to make the artwork fit, and `contain` instead of
   `cover` would letterbox the matte into a visible band. At the artwork's own
   ratio there is nothing to crop and nothing to letterbox, and `cover` from
   the rule above is a no-op rather than something to override.

   IT IS WRITTEN OUT AND IT MUST NOT BE `auto`. That was the first version and
   it shipped a layout shift. `aspect-ratio: auto` on an <img> means "use the
   natural ratio", and the placeholder box a browser reserves for an image
   that has not loaded yet comes from an `aspect-ratio: attr(width) /
   attr(height)` rule in the UA stylesheet - which an author `auto` overrides
   and throws away.

   Measured in headless Chrome at 1280x800, on the real lazy path with nothing
   flipped to eager: with `auto` the box is 512x0 until the file arrives and
   512x341 after it, so everything below the map jumps 341px. With the ratio
   written out it is 512x341 before and after. The image loads either way -
   the first diagnosis of this was "the map never loads", and that was wrong,
   read off a probe tab that was visibilityState:hidden and therefore
   deferring every lazy image on the page. See PROGRESS, row 26d.

   The radius goes for a different reason: the top edge is a drawn wave, not a
   card edge, and a rounded corner under a wave reads as a mistake. The
   background goes because no part of the box is ever uncovered. */
.location__media--map img {
  aspect-ratio: 4507 / 2999;
  background: none;
  border-radius: 0;
}


/* --- The enlarge control, and the lightbox it opens ----------------------
   Row 26d. The client could not read the route at the size the layout gives
   the map, which is a real complaint: the artwork is hand-lettered and the
   box tops out at 520px on a desktop.
   -------------------------------------------------------------------------- */

/* The figure is the positioning context for the stretched link below. */
.location__media--map { position: relative; }

.map-zoom {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  font-size: var(--fs-caption);
  font-weight: var(--fw-semibold);
  color: var(--teal);
  text-decoration: none;
  /* The chip is the visible control, so it is also the focus ring's shape.
     Padding here rather than a .btn size: BLD-01 owns the button scale and
     three sizes is the set. */
  padding: var(--sp-xs) 0;
}

.map-zoom:hover { text-decoration: underline; }

/* THE STRETCHED LINK. The whole map is the click target, because a chip the
   size of two words is a poor one on a phone and because tapping a map to
   make it bigger is what everyone tries first.

   It covers the figure, which is the picture AND the chip, so the chip's own
   text is under its own hit area - harmless, it is the same link. It sits
   under nothing else, because the figure holds nothing else. */
.map-zoom::before {
  content: "";
  position: absolute;
  inset: 0;
  /* Above the image so the cursor is right, below nothing. */
  z-index: 1;
}

/* The cursor is the only affordance a mouse gets before the chip is read, so
   it is worth being explicit rather than inheriting the link default. */
@media (hover: hover) and (pointer: fine) {
  .map-zoom::before { cursor: zoom-in; }
}

.lightbox {
  /* inset:0 with a max of 100% rather than width/height:100%, so the dialog
     cannot force a scrollbar of its own on a viewport with one. */
  inset: 0;
  width: 100%;
  max-width: 100%;
  height: 100%;
  max-height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--cream-50);
  color: var(--forest);
  /* The frame scrolls; the dialog never does. Without this the zoomed image
     can push the bar off the top of the screen. */
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.lightbox::backdrop { background: rgba(26, 46, 37, .72); }

/* A <dialog> without the `open` attribute is display:none, which is what
   makes the JavaScript-off degradation free. Restated here because `display:
   flex` above would override it if it were set on the element rather than on
   the open state. */
.lightbox:not([open]) { display: none; }

.lightbox__bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-md) var(--page-gutter);
  border-bottom: var(--border-width) solid var(--cream-300);
  background: var(--cream-50);
}

.lightbox__title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--fs-lead);
  line-height: var(--lh-snug);
  color: var(--forest);
}

.lightbox__zoom {
  flex: none;
  padding: 8px 16px;
  font-size: var(--fs-caption);
}

.lightbox__close {
  flex: none;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  font-size: 28px;
  line-height: 1;
  color: var(--forest);
  background: transparent;
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-card);
  cursor: pointer;
}

.lightbox__close:hover { background: var(--cream-200); }

.lightbox__frame {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  /* Contained, so a flick at the end of the map does not scroll the page
     underneath and leave the visitor somewhere else when they close it. */
  overscroll-behavior: contain;
  /* pinch-zoom is left IN. The class-based zoom below is the discoverable
     control; a pinch on top of it is what a phone user does by reflex and
     there is no reason to take it away. */
  touch-action: pan-x pan-y pinch-zoom;
  display: grid;
  place-items: center;
  padding: var(--sp-md);
}

/* FIT. The whole map, never cropped, never larger than the space. */
.lightbox__frame img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
}

/* ZOOMED. `min()` and not a bare 2560px, because 2560px on a 390px phone is a
   6.7x jump and nobody can find their way around that. Measured against the
   fitted width at each size: 3.0x on a phone, 2.0x on a tablet, 2.1x on a
   1440 desktop, tapering on a very large screen where the fitted map is
   already legible. The 2560 ceiling is the top of the ladder, so nothing is
   ever upscaled past its own pixels. */
.lightbox__frame.is-zoomed {
  place-items: start;
  cursor: zoom-out;
}

.lightbox__frame.is-zoomed img {
  width: min(2560px, 300vw);
  max-width: none;
  max-height: none;
  height: auto;
}

@media (hover: hover) and (pointer: fine) {
  .lightbox__frame img { cursor: zoom-in; }
  .lightbox__frame.is-zoomed img { cursor: zoom-out; }
}


/* --- Why choose us -------------------------------------------------------- */

.why {
  background: var(--sage-100);
  border-top: var(--border-width) solid var(--cream-300);
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: clamp(24px, 3vw, 32px);
}

.why > :not(.decoration) { position: relative; }

.why .section-heading {
  font-size: clamp(1.5rem, 3.4vw, 1.875rem);
  max-width: 38rem;
}

.decoration--cat-face {
  right: clamp(24px, 5vw, 80px);
  bottom: 0;
  width: clamp(240px, 26vw, 380px);
}

.why__list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  gap: 0 clamp(24px, 4vw, 48px);
  list-style: none;
  padding: 0;
  /* Keeps the list off the cat. The clamp collapses to 0 as soon as the
     column is narrow enough that the cat is not drawn. */
  padding-right: clamp(0px, calc(100% - 620px), 400px);
}

.why__list li {
  padding: var(--sp-lg) 0;
  border-top: var(--border-width) solid var(--border-subtle);
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--ink-700);
}

.why__pullout {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-700);
  max-width: max(24rem, min(44rem, calc(100% - 400px)));
  background: var(--white);
  border: var(--border-width) solid var(--cream-300);
  border-radius: var(--radius-card);
  padding: var(--sp-xl);
}


/* --- Testimonials ---------------------------------------------------------
   Four slides, all in the markup, one shown at a time. The export stacked all
   four absolutely and restyled them into a 3D fan with drag; BLD-02's markup
   ships them in normal flow and html.js hides the inactive ones, which is the
   same degradation shape as the founder deck.
   -------------------------------------------------------------------------- */

.testimonials {
  border-top: var(--border-width) solid var(--cream-300);
  display: flex;
  flex-direction: column;
  gap: clamp(24px, 3vw, 32px);
}

/* The export's deck had 48px of side padding to make room for the two cards
   peeking behind the front one. It is still not carried across - on a 360px
   phone it takes a third of the card's width - but the reason given here on
   2026-09-17, "there is no fan here", was a description of a mistake rather
   than a decision. See "The fan" below. The 72px of top padding IS carried:
   it is what the avatar's -64px margin hangs into. */
.deck--testimonials {
  max-width: 1056px;
  margin-inline: auto;
  padding: 72px 0 var(--sp-xl);
}

/* --- The fan -------------------------------------------------------------
   RESTORED 2026-09-18. The design shows the active testimonial lifted and
   sharp with the previous and next cards behind it, offset, shrunk, dimmed
   and blurred, and the whole arrangement sliding on the brand easing when the
   deck moves. BLD-02 built both decks on one progressive-enhancement rule -
   every slide in the markup, `display:none` on the inactive ones - which is
   right for the founder deck and silently deletes this. It was never decided
   against; PROGRESS only recorded the consequence.

   Three departures from the export, all deliberate:

   1. GRID STACK, NOT ABSOLUTE POSITIONING. The export absolutely positioned
      four cards inside a deck of fixed `height: clamp(536px,76vw,556px)`, and
      had to set the fourth card's quote to 14px to stop it overflowing. Grid
      stacking keeps the deck as tall as its tallest card, so the long fourth
      quote fits at the same 15px as the other three. The overflow bug BLD-03
      fixed by moving to min-height is not being reintroduced to get the fan.

   2. NO SIDE PADDING. The export used 48px to give the neighbours room. The
      deck clips them instead, which is the same picture at desktop width and
      a much better one at 360px.

   3. `pointer-events` and `inert` on everything but the active card. The
      export had neither, so its half-visible neighbours were clickable and a
      screen reader read all four testimonials, two of them blurred past
      legibility. main.js sets `inert`; the rest is here.

   The two shadows are the export's literals rather than tokens: --shadow-md
   and --shadow-lg are the design system's general-purpose lifts and neither
   is this pair, which is tuned to sit a card above two copies of itself. */

/* THE CLIP IS HORIZONTAL ONLY, and the axis matters. What has to be clipped is
   the fanned neighbours, which run off the sides and would otherwise give the
   page a horizontal scrollbar at narrow widths. Nothing needs clipping
   vertically - and `overflow: hidden` clips both, which cut the active card's
   shadow flat 24px below it. That shadow reaches 58px down (`0 18px 40px`), so
   a third of it was being sliced off in a hard line the full width of the
   deck. Reported 2026-09-18.

   `overflow: clip` is the value that allows one axis to stay `visible`;
   `hidden` coerces the other axis to `auto` and cannot express this. The
   @supports is a real fallback rather than a formality: without `clip` the deck
   must still clip both axes, because a horizontal scrollbar on a phone is a
   worse fault than a cut shadow, and REFERENCE.md's "zero horizontal overflow
   at every width" is not negotiable for a cosmetic gain. */
html.js .deck--testimonials {
  display: grid;
  justify-items: center;
  overflow: hidden;
}

@supports (overflow: clip) {
  html.js .deck--testimonials {
    overflow-x: clip;
    overflow-y: visible;
  }
}

/* The resting state is a card that is neither active nor either neighbour:
   parked behind the active one, at zero opacity. With four cards exactly one
   is ever in this state. */
html.js .deck--testimonials > .tcard {
  grid-area: 1 / 1;
  transform: scale(.76);
  opacity: 0;
  filter: blur(1.5px);
  z-index: 0;
  box-shadow: 0 6px 14px rgba(27, 46, 36, .08);
  pointer-events: none;
  transition:
    transform var(--dur-slow) var(--ease-standard),
    opacity var(--dur-slow) var(--ease-standard),
    filter var(--dur-slow) var(--ease-standard),
    box-shadow var(--dur-slow) var(--ease-standard);
}

/* Reduced motion needs no rule of its own. --dur-slow is 0ms under
   prefers-reduced-motion in tokens.css, so the cards change position without
   travelling - which is the right answer here, unlike the quote strip, where
   the equivalent blanket treatment rendered the section empty. */

html.js .deck--testimonials > .tcard.is-prev,
html.js .deck--testimonials > .tcard.is-next {
  opacity: .5;
  z-index: 1;
}

html.js .deck--testimonials > .tcard.is-prev { transform: translateX(-44%) scale(.76); }
html.js .deck--testimonials > .tcard.is-next { transform: translateX(44%) scale(.76); }

/* BELOW 676px THE FAN IS NOT A FAN, and this is row 23i item (g) closing the
   judgement row 22b left open.

   The card is at its fluid 92% rather than its 560px cap down here, so the
   deck can only ever show 4% of it either side: MEASURED 13.69px at 390px and
   12.48px at 360px. That window falls INSIDE the neighbour's own text block -
   its border, its corner radius and its white margin are all clipped away - so
   what a visitor sees is a vertical strip of chopped, half-opacity, blurred
   letter fragments. It reads as text bleeding through from underneath rather
   than as a card behind a card.

   Above 676px the card is capped and every extra pixel of viewport goes to the
   neighbours: 65.6px each at 768px, 107px at 860px, 179.2px from about 1020px
   up, which is the fan the design shows. At 768px it is unambiguous. THAT IS
   WHY THIS IS NOT ON THE 860px RUNG: collapsing there would delete a fan that
   is working on iPad portrait and small laptops to fix a phone.

   676px IS NOT A NEW DESIGN BREAKPOINT, it is this card's own cap expressed as
   a viewport width, and every number in it is already in these files:
   width: min(92%, 560px) below, so the card stops growing once the deck
   reaches 560 / 0.92 = 608.7px, and the deck is 90% of the viewport while
   --page-gutter sits in the 5vw part of its clamp - 608.7 / 0.9 = 676.3.
   Verified in Chrome: the crossover lands within 0.2px.

   OPACITY, NOT display:none, AND THAT IS ROW 22b's WHOLE LESSON REPEATED.
   display:none cannot transition. It would take the 380ms slide with it and
   leave the deck cutting instantly between cards on the one device where the
   visitor swipes rather than clicks. At opacity 0 the outgoing card still
   slides out to -44% and the incoming one still slides in from +44%; only the
   two parked neighbours go. Nothing here changes layout, so the horizontal
   clip above still contains the cards, horizontal overflow stays at zero, and
   check-images.mjs still measures 873 image/viewport pairs. `inert` and
   `pointer-events: none` are untouched, so nothing changes for a screen reader
   or for the keyboard either.

   It must stay AFTER the .is-prev/.is-next rule above, which it ties with on
   specificity. */
@media (max-width: 676px) {
  html.js .deck--testimonials > .tcard.is-prev,
  html.js .deck--testimonials > .tcard.is-next { opacity: 0; }
}

html.js .deck--testimonials > .tcard.is-active {
  transform: none;
  opacity: 1;
  filter: none;
  z-index: 3;
  box-shadow: 0 18px 40px rgba(27, 46, 36, .18), 0 2px 6px rgba(27, 46, 36, .10);
  pointer-events: auto;
}

/* With JavaScript off all four are on the page at once. Without this the
   avatar's -64px margin hangs over the previous card's bottom edge and the
   two read as one broken box; 72px is the same clearance the deck's own top
   padding gives the first card. */
html:not(.js) .deck--testimonials {
  display: flex;
  flex-direction: column;
  gap: 72px;
}

.tcard {
  width: min(92%, 560px);
  margin-inline: auto;
  /* The export fixed this at clamp(420px,64vw,440px). min-height instead,
     because the longest quote overflows a fixed box once the cards are in
     normal flow rather than absolutely positioned. */
  min-height: clamp(420px, 64vw, 440px);
  background: var(--white);
  border: var(--border-width) solid var(--cream-300);
  border-radius: var(--radius-card);
  padding: clamp(20px, 2.5vw, 32px);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-md);
}

/* 88px square with a 4px border and border-box sizing, so 80px inside. With
   object-fit:cover a 1.19:1 illustration paints 95 CSS px wide, which is what
   the 105 step in the ladder is for. BLD-05 finding 4. */
.tcard__avatar {
  width: 88px;
  height: 88px;
  margin-top: -64px;
  border-radius: var(--radius-avatar);
  border: 4px solid var(--white);
  overflow: hidden;
  flex: 0 0 auto;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.tcard__avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  mix-blend-mode: multiply;
}

.tcard__avatar--peach { background: var(--peach-100); }
.tcard__avatar--sage { background: var(--sage-100); }
.tcard__avatar--lavender { background: var(--surface-lavender); }

.tcard__stars {
  display: flex;
  gap: var(--sp-xs);
  align-items: center;
  justify-content: center;
  color: var(--teal);
}

.tcard__stars svg {
  display: block;
  flex: none;
}

/* The export set the fourth card to 14px/1.6 and the other three to 15/1.65,
   to fit a longer quote into a fixed-height box. The box is min-height here
   and the fan stacks with grid rather than a fixed height, so all four carry
   the same type. */
.tcard__quote p {
  font-size: 15px;
  line-height: 1.65;
  color: var(--ink-700);
}

.tcard__attribution {
  margin-top: auto;
  font-size: 15px;
  font-weight: var(--fw-semibold);
  letter-spacing: .02em;
  color: var(--grape-700);
}


/* --- Ethos ---------------------------------------------------------------- */

.ethos {
  border-top: var(--border-width) solid var(--cream-300);
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 48px);
  align-items: center;
}

.ethos__body {
  flex: 1 1 400px;
  min-width: min(100%, 300px);
  max-width: var(--measure-prose);
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.ethos__body p {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-700);
}

.ethos__media {
  flex: 1 1 440px;
  min-width: min(100%, 300px);
}

.ethos__media img {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  display: block;
  background: var(--cream-200);
  border-radius: var(--radius-card);
}


/* --- Register ------------------------------------------------------------- */

.register {
  background: var(--cream-100);
  border-top: var(--border-width) solid var(--cream-300);
  padding-block: clamp(48px, 6vw, 80px);
}

.register__card {
  background: var(--white);
  border: var(--border-width) solid var(--cream-300);
  border-radius: var(--radius-card);
  padding: clamp(24px, 4vw, 48px);
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 56px);
  align-items: flex-start;
}

.register__intro {
  flex: 1 1 360px;
  min-width: min(100%, 280px);
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.register__intro > p {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-700);
  max-width: 44ch;
}

.register__art {
  margin-top: var(--sp-sm);
  background: var(--sage-100);
  border-radius: var(--radius-card);
  padding: var(--sp-xl);
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

.register__art img {
  width: 100%;
  max-width: 220px;
  height: auto;
  display: block;
  mix-blend-mode: multiply;
}

.register__panel {
  flex: 1 1 400px;
  min-width: min(100%, 280px);
}

.register__form {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

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

.register__submit { align-self: flex-start; }

/* FRM-05. The button carries aria-disabled while the POST is in flight, and
   components.css dims anything with that attribute to 45% - which is right for
   a control that cannot be used and wrong for one that is working, because the
   label it has just been given is the whole of the in-flight feedback and 45%
   opacity is not a contrast ratio anyone should have to read at.

   So the dimming comes off for this one state and the label carries it
   instead. Nothing is lost: pointer-events: none still applies, the
   isSending() guard in main.js is what actually refuses a second submit, and
   "Sending..." says more about why the button will not respond than a faded
   "Register your interest" ever did. Scoped to the sending form by aria-busy,
   so a genuinely disabled button anywhere else is untouched. */
.register__form[aria-busy="true"] .register__submit[aria-disabled="true"] {
  opacity: 1;
}

.register__smallprint {
  font-size: var(--fs-caption);
  line-height: 1.6;
  color: var(--ink-500);
}


/* --- The success panel, and the no-JavaScript route to it -----------------

   The design's own panel, given an id, so a native submit that redirects to
   /#registered lands on a designed screen rather than an invented page.

   THE ADJACENT SIBLING SELECTOR IS THE WHOLE MECHANISM. #registered must stay
   the immediate previous sibling of the form. BLD-02's markup puts the
   <noscript> fallback BEFORE #registered for exactly this reason: a
   <noscript> element is a real element in the DOM even when scripting is on,
   so between the two it would break this silently.
   -------------------------------------------------------------------------- */

.success {
  background: var(--sage-100);
  border: var(--border-width) solid var(--sage);
  border-radius: var(--radius-card);
  padding: clamp(24px, 3vw, 32px);
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

#registered { display: none; }
#registered:target { display: flex; }
#registered:target + .register__form { display: none; }

.success__head {
  display: flex;
  align-items: center;
  gap: 10px;
}

.success__head .icon { color: var(--sage-600); }

.success__heading {
  font-size: 20px;
  line-height: var(--lh-snug);
  font-weight: var(--fw-medium);
  color: var(--forest);
}

.success p {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-700);
}

/* "Register another pet" and the modal's "Close". An <a> on the inline panel
   and a <button> in the modal, because one changes the URL and the other
   cannot; one class, so they read identically. */
.link-button {
  align-self: flex-start;
  margin-top: var(--sp-xs);
  background: transparent;
  color: var(--teal);
  border: none;
  padding: var(--sp-sm) 0;
  font-size: 15px;
  font-weight: var(--fw-semibold);
  cursor: pointer;
  text-decoration: underline;
}

.link-button:hover { color: var(--forest); }

/* No export equivalent: the design never had a JavaScript-off state, because
   it was a React page. Deliberately not the sage success panel and not a red
   error either. It is information, so it reads as the page's own voice. */
.noscript-fallback {
  background: var(--cream-100);
  border: var(--border-width) solid var(--cream-300);
  border-radius: var(--radius-card);
  padding: clamp(20px, 3vw, 24px);
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
}

.noscript-fallback h3 {
  font-size: 20px;
  line-height: var(--lh-snug);
  font-weight: var(--fw-medium);
  color: var(--forest);
}

.noscript-fallback p {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--ink-700);
}


/* ==========================================================================
   The legal pages

   privacy.html and cookies.html. The export's styling for both is a straight
   lift; the only thing without an export equivalent is the placeholder
   highlight, which is below.

   Two of these are <dl> where the export had div soup, so they want dt/dd
   rules rather than div rules. The rendered result is the same two-column and
   stacked-row shapes the export drew.
   ========================================================================== */

/* The legal header has no nav, only a "Back to home" button, so the flex row
   in layout.css needs nothing changed. The class exists as a hook and is
   deliberately left without declarations rather than removed from the markup:
   BLD-07 and BLD-09 both key off it. */

.legal-hero {
  padding-block: var(--section-y) clamp(32px, 4vw, 48px);
}

.legal-hero__inner {
  max-width: var(--measure-prose);
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.legal-hero__eyebrow {
  font-size: 11px;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  font-weight: var(--fw-semibold);
  color: var(--teal);
}

.legal-hero__title {
  font-size: clamp(34px, 5vw, 48px);
  line-height: var(--lh-display);
  font-weight: var(--fw-bold);
  color: var(--forest);
}

.legal-hero__lead {
  font-size: 19px;
  line-height: var(--lh-lead);
  color: var(--ink-700);
}

.legal-hero__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-sm) var(--sp-xl);
  font-size: var(--fs-body-sm);
  color: var(--ink-500);
  margin-top: var(--sp-sm);
}

.legal-body {
  padding-block: 0 clamp(64px, 8vw, 96px);
}

.legal-body__inner {
  max-width: var(--measure-prose);
  display: flex;
  flex-direction: column;
  gap: clamp(40px, 5vw, 56px);
}

.legal-summary,
.consent-panel {
  background: var(--white);
  border: var(--border-width) solid var(--cream-300);
  border-radius: var(--radius-card);
  padding: clamp(20px, 3vw, 32px);
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

.consent-panel {
  gap: var(--sp-lg);
  align-items: flex-start;
}

.legal-summary__title,
.consent-panel__title {
  font-size: 20px;
  line-height: var(--lh-snug);
  font-weight: var(--fw-medium);
  color: var(--forest);
}

.legal-section {
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.legal-section__title {
  font-size: clamp(24px, 3vw, 30px);
  line-height: var(--lh-heading);
  font-weight: var(--fw-semibold);
  color: var(--forest);
}

.legal-section__subtitle {
  font-size: 20px;
  line-height: var(--lh-snug);
  font-weight: var(--fw-medium);
  color: var(--forest);
}

.legal-summary p,
.consent-panel p,
.legal-section > p {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

.consent-panel strong {
  font-weight: var(--fw-semibold);
  color: var(--forest);
}

/* Label and value on one line, ruled top and bottom. A <dl>, so two columns
   rather than the export's inline <strong>. */
.legal-facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-sm) var(--sp-md);
  padding: var(--sp-lg) 0;
  border-top: var(--border-width) solid var(--cream-300);
  border-bottom: var(--border-width) solid var(--cream-300);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

.legal-facts dt {
  font-weight: var(--fw-semibold);
  color: var(--forest);
}

/* The 1fr track has to be allowed to shrink below its content, or a value
   with no break opportunity in it widens the page instead of wrapping. The
   two unfilled LGL-05 tokens are exactly that shape. */
.legal-facts dd { min-width: 0; }

/* On a narrow phone the label column leaves too little for the value, so the
   pairs stack. This is also nearer the export, which set them as one wrapping
   line with an inline <strong> rather than as columns. */
@media (max-width: 480px) {
  .legal-facts { grid-template-columns: 1fr; gap: 0; }
  .legal-facts dt { padding-top: var(--sp-sm); }
  .legal-facts dt:first-child { padding-top: 0; }
}

/* The ICO block is an <address> of <span>, not a <dl>, so it stacks. */
.legal-facts--address {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
}

.legal-facts__name {
  font-weight: var(--fw-semibold);
  color: var(--forest);
}

/* Term over definition, one ruled row each, the last one closed off. */
.legal-rows {
  display: flex;
  flex-direction: column;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

.legal-rows dt {
  padding-top: var(--sp-lg);
  border-top: var(--border-width) solid var(--cream-300);
  font-weight: var(--fw-semibold);
  color: var(--forest);
}

.legal-rows dd { padding: var(--sp-xs) 0 var(--sp-lg); }

.legal-rows dd:last-of-type { border-bottom: var(--border-width) solid var(--cream-300); }

/* Every unfilled token on the site, highlighted so it cannot ship unnoticed.
   LGL-05 fills them, in privacy.html section 1. There were two; the entity
   was filled on 2026-09-22 and the registered address is the one left. The
   grep that finds it is `grep -rn '{{' src/`, which also returns the two
   comment lines that explain the convention and nothing else. */
.legal-placeholder {
  background: var(--cream-300);
  border-bottom: var(--border-width) dashed var(--coral);
  padding: 0 var(--sp-xs);
  /* A token is one long word with no space in it, so without this it cannot
     wrap and it pushes the page sideways on a narrow phone. Measured at
     320px, where it gave privacy.html 1px of horizontal scroll. */
  overflow-wrap: anywhere;
}


/* ==========================================================================
   404.html. BLD-08b.

   Nothing here is new to look at, and that is the point: a visitor who lands
   on a dead link should be in no doubt they are still on the same site. The
   eyebrow is base.css's, the buttons are components.css's, and the measure is
   the legal pages'. Five rules is the whole page.

   It is centred rather than left-aligned like .legal-hero, because there is no
   body copy underneath for a left edge to line up with.
   ========================================================================== */

.notfound {
  padding-block: var(--section-y) clamp(64px, 8vw, 96px);
  /* The header and the footer are both short. Without this the whole page is
     about 60% of a desktop viewport and the footer's wordmark sits halfway up
     the screen with nothing under it. A min-height on the one section pushes
     it down without a fixed height anywhere. */
  min-height: 52vh;
  display: flex;
  align-items: center;
}

.notfound__inner {
  max-width: var(--measure-prose);
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-lg);
}

.notfound__title {
  font-size: clamp(34px, 5vw, 48px);
  line-height: var(--lh-display);
  font-weight: var(--fw-bold);
  color: var(--forest);
}

.notfound__lead {
  font-size: 19px;
  line-height: var(--lh-lead);
  color: var(--ink-700);
}

/* Both rows wrap, and both are centred, so a phone gets the primary action on
   its own line rather than two half-width buttons. */
.notfound__actions,
.notfound__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: var(--sp-sm);
}

.notfound__actions { gap: var(--sp-lg); }

.notfound__links {
  gap: var(--sp-sm) var(--sp-xl);
  font-size: var(--fs-body-sm);
}
